/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.browser.view; import android.animation.Animator; import android.animation.Animator.AnimatorListener; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Point; import android.graphics.PointF; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.SoundEffectConstants; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import com.android.browser.R; import java.util.ArrayList; import java.util.List; public class PieMenu extends FrameLayout { private static final int MAX_LEVELS = 5; private static final long ANIMATION = 80; public interface PieController { /** * called before menu opens to customize menu * returns if pie state has been changed */ public boolean onOpen(); public void stopEditingUrl(); } /** * A view like object that lives off of the pie menu */ public interface PieView { public interface OnLayoutListener { public void onLayout(int ax, int ay, boolean left); } public void setLayoutListener(OnLayoutListener l); public void layout(int anchorX, int anchorY, boolean onleft, float angle, int parentHeight); public void draw(Canvas c); public boolean onTouchEvent(MotionEvent evt); } private Point mCenter; private int mRadius; private int mRadiusInc; private int mSlop; private int mTouchOffset; private Path mPath; private boolean mOpen; private PieController mController; private List<PieItem> mItems; private int mLevels; private int[] mCounts; private PieView mPieView = null; // sub menus private List<PieItem> mCurrentItems; private PieItem mOpenItem; private Drawable mBackground; private Paint mNormalPaint; private Paint mSelectedPaint; private Paint mSubPaint; // touch handling private PieItem mCurrentItem; private boolean mUseBackground; private boolean mAnimating; /** * @param context * @param attrs * @param defStyle */ public PieMenu(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context); } /** * @param context * @param attrs */ public PieMenu(Context context, AttributeSet attrs) { super(context, attrs); init(context); } /** * @param context */ public PieMenu(Context context) { super(context); init(context); } private void init(Context ctx) { mItems = new ArrayList<PieItem>(); mLevels = 0; mCounts = new int[MAX_LEVELS]; Resources res = ctx.getResources(); mRadius = (int) res.getDimension(R.dimen.qc_radius_start); mRadiusInc = (int) res.getDimension(R.dimen.qc_radius_increment); mSlop = (int) res.getDimension(R.dimen.qc_slop); mTouchOffset = (int) res.getDimension(R.dimen.qc_touch_offset); mOpen = false; setWillNotDraw(false); setDrawingCacheEnabled(false); mCenter = new Point(0,0); mBackground = res.getDrawable(R.drawable.qc_background_normal); mNormalPaint = new Paint(); mNormalPaint.setColor(res.getColor(R.color.qc_normal)); mNormalPaint.setAntiAlias(true); mSelectedPaint = new Paint(); mSelectedPaint.setColor(res.getColor(R.color.qc_selected)); mSelectedPaint.setAntiAlias(true); mSubPaint = new Paint(); mSubPaint.setAntiAlias(true); mSubPaint.setColor(res.getColor(R.color.qc_sub)); } public void setController(PieController ctl) { mController = ctl; } public void setUseBackground(boolean useBackground) { mUseBackground = useBackground; } public void addItem(PieItem item) { // add the item to the pie itself mItems.add(item); int l = item.getLevel(); mLevels = Math.max(mLevels, l); mCounts[l]++; } public void removeItem(PieItem item) { mItems.remove(item); } public void clearItems() { mItems.clear(); } private boolean onTheLeft() { return mCenter.x < mSlop; } /** * guaranteed has center set * @param show */ private void show(boolean show) { mOpen = show; if (mOpen) { // ensure clean state mAnimating = false; mCurrentItem = null; mOpenItem = null; mPieView = null; mController.stopEditingUrl(); mCurrentItems = mItems; for (PieItem item : mCurrentItems) { item.setSelected(false); } if (mController != null) { boolean changed = mController.onOpen(); } layoutPie(); animateOpen(); } invalidate(); } private void animateOpen() { ValueAnimator anim = ValueAnimator.ofFloat(0, 1); anim.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { for (PieItem item : mCurrentItems) { item.setAnimationAngle((1 - animation.getAnimatedFraction()) * (- item.getStart())); } invalidate(); } }); anim.setDuration(2*ANIMATION); anim.start(); } private void setCenter(int x, int y) { if (x < mSlop) { mCenter.x = 0; } else { mCenter.x = getWidth(); } mCenter.y = y; } private void layoutPie() { float emptyangle = (float) Math.PI / 16; int rgap = 2; int inner = mRadius + rgap; int outer = mRadius + mRadiusInc - rgap; int gap = 1; for (int i = 0; i < mLevels; i++) { int level = i + 1; float sweep = (float) (Math.PI - 2 * emptyangle) / mCounts[level]; float angle = emptyangle + sweep / 2; mPath = makeSlice(getDegrees(0) - gap, getDegrees(sweep) + gap, outer, inner, mCenter); for (PieItem item : mCurrentItems) { if (item.getLevel() == level) { View view = item.getView(); if (view != null) { view.measure(view.getLayoutParams().width, view.getLayoutParams().height); int w = view.getMeasuredWidth(); int h = view.getMeasuredHeight(); int r = inner + (outer - inner) * 2 / 3; int x = (int) (r * Math.sin(angle)); int y = mCenter.y - (int) (r * Math.cos(angle)) - h / 2; if (onTheLeft()) { x = mCenter.x + x - w / 2; } else { x = mCenter.x - x - w / 2; } view.layout(x, y, x + w, y + h); } float itemstart = angle - sweep / 2; item.setGeometry(itemstart, sweep, inner, outer); angle += sweep; } } inner += mRadiusInc; outer += mRadiusInc; } } /** * converts a * * @param angle from 0..PI to Android degrees (clockwise starting at 3 * o'clock) * @return skia angle */ private float getDegrees(double angle) { return (float) (270 - 180 * angle / Math.PI); } @Override protected void onDraw(Canvas canvas) { if (mOpen) { int state; if (mUseBackground) { int w = mBackground.getIntrinsicWidth(); int h = mBackground.getIntrinsicHeight(); int left = mCenter.x - w; int top = mCenter.y - h / 2; mBackground.setBounds(left, top, left + w, top + h); state = canvas.save(); if (onTheLeft()) { canvas.scale(-1, 1); } mBackground.draw(canvas); canvas.restoreToCount(state); } // draw base menu PieItem last = mCurrentItem; if (mOpenItem != null) { last = mOpenItem; } for (PieItem item : mCurrentItems) { if (item != last) { drawItem(canvas, item); } } if (last != null) { drawItem(canvas, last); } if (mPieView != null) { mPieView.draw(canvas); } } } private void drawItem(Canvas canvas, PieItem item) { if (item.getView() != null) { Paint p = item.isSelected() ? mSelectedPaint : mNormalPaint; if (!mItems.contains(item)) { p = item.isSelected() ? mSelectedPaint : mSubPaint; } int state = canvas.save(); if (onTheLeft()) { canvas.scale(-1, 1); } float r = getDegrees(item.getStartAngle()) - 270; // degrees(0) canvas.rotate(r, mCenter.x, mCenter.y); canvas.drawPath(mPath, p); canvas.restoreToCount(state); // draw the item view View view = item.getView(); state = canvas.save(); canvas.translate(view.getX(), view.getY()); view.draw(canvas); canvas.restoreToCount(state); } } private Path makeSlice(float start, float end, int outer, int inner, Point center) { RectF bb = new RectF(center.x - outer, center.y - outer, center.x + outer, center.y + outer); RectF bbi = new RectF(center.x - inner, center.y - inner, center.x + inner, center.y + inner); Path path = new Path(); path.arcTo(bb, start, end - start, true); path.arcTo(bbi, end, start - end); path.close(); return path; } // touch handling for pie @Override public boolean onTouchEvent(MotionEvent evt) { float x = evt.getX(); float y = evt.getY(); int action = evt.getActionMasked(); if (MotionEvent.ACTION_DOWN == action) { if ((x > getWidth() - mSlop) || (x < mSlop)) { setCenter((int) x, (int) y); show(true); return true; } } else if (MotionEvent.ACTION_UP == action) { if (mOpen) { boolean handled = false; if (mPieView != null) { handled = mPieView.onTouchEvent(evt); } PieItem item = mCurrentItem; if (!mAnimating) { deselect(); } show(false); if (!handled && (item != null) && (item.getView() != null)) { if ((item == mOpenItem) || !mAnimating) { item.getView().performClick(); } } return true; } } else if (MotionEvent.ACTION_CANCEL == action) { if (mOpen) { show(false); } if (!mAnimating) { deselect(); invalidate(); } return false; } else if (MotionEvent.ACTION_MOVE == action) { if (mAnimating) return false; boolean handled = false; PointF polar = getPolar(x, y); int maxr = mRadius + mLevels * mRadiusInc + 50; if (mPieView != null) { handled = mPieView.onTouchEvent(evt); } if (handled) { invalidate(); return false; } if (polar.y < mRadius) { if (mOpenItem != null) { closeSub(); } else if (!mAnimating) { deselect(); invalidate(); } return false; } if (polar.y > maxr) { deselect(); show(false); evt.setAction(MotionEvent.ACTION_DOWN); if (getParent() != null) { ((ViewGroup) getParent()).dispatchTouchEvent(evt); } return false; } PieItem item = findItem(polar); if (item == null) { } else if (mCurrentItem != item) { onEnter(item); if ((item != null) && item.isPieView() && (item.getView() != null)) { int cx = item.getView().getLeft() + (onTheLeft() ? item.getView().getWidth() : 0); int cy = item.getView().getTop(); mPieView = item.getPieView(); layoutPieView(mPieView, cx, cy, (item.getStartAngle() + item.getSweep()) / 2); } invalidate(); } } // always re-dispatch event return false; } private void layoutPieView(PieView pv, int x, int y, float angle) { pv.layout(x, y, onTheLeft(), angle, getHeight()); } /** * enter a slice for a view * updates model only * @param item */ private void onEnter(PieItem item) { // deselect if (mCurrentItem != null) { mCurrentItem.setSelected(false); } if (item != null) { // clear up stack playSoundEffect(SoundEffectConstants.CLICK); item.setSelected(true); mPieView = null; mCurrentItem = item; if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) { openSub(mCurrentItem); mOpenItem = item; } } else { mCurrentItem = null; } } private void animateOut(final PieItem fixed, AnimatorListener listener) { if ((mCurrentItems == null) || (fixed == null)) return; final float target = fixed.getStartAngle(); ValueAnimator anim = ValueAnimator.ofFloat(0, 1); anim.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { for (PieItem item : mCurrentItems) { if (item != fixed) { item.setAnimationAngle(animation.getAnimatedFraction() * (target - item.getStart())); } } invalidate(); } }); anim.setDuration(ANIMATION); anim.addListener(listener); anim.start(); } private void animateIn(final PieItem fixed, AnimatorListener listener) { if ((mCurrentItems == null) || (fixed == null)) return; final float target = fixed.getStartAngle(); ValueAnimator anim = ValueAnimator.ofFloat(0, 1); anim.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { for (PieItem item : mCurrentItems) { if (item != fixed) { item.setAnimationAngle((1 - animation.getAnimatedFraction()) * (target - item.getStart())); } } invalidate(); } }); anim.setDuration(ANIMATION); anim.addListener(listener); anim.start(); } private void openSub(final PieItem item) { mAnimating = true; animateOut(item, new AnimatorListenerAdapter() { public void onAnimationEnd(Animator a) { for (PieItem item : mCurrentItems) { item.setAnimationAngle(0); } mCurrentItems = new ArrayList<PieItem>(mItems.size()); int i = 0, j = 0; while (i < mItems.size()) { if (mItems.get(i) == item) { mCurrentItems.add(item); } else { mCurrentItems.add(item.getItems().get(j++)); } i++; } layoutPie(); animateIn(item, new AnimatorListenerAdapter() { public void onAnimationEnd(Animator a) { for (PieItem item : mCurrentItems) { item.setAnimationAngle(0); } mAnimating = false; } }); } }); } private void closeSub() { mAnimating = true; if (mCurrentItem != null) { mCurrentItem.setSelected(false); } animateOut(mOpenItem, new AnimatorListenerAdapter() { public void onAnimationEnd(Animator a) { for (PieItem item : mCurrentItems) { item.setAnimationAngle(0); } mCurrentItems = mItems; mPieView = null; animateIn(mOpenItem, new AnimatorListenerAdapter() { public void onAnimationEnd(Animator a) { for (PieItem item : mCurrentItems) { item.setAnimationAngle(0); } mAnimating = false; mOpenItem = null; mCurrentItem = null; } }); } }); } private void deselect() { if (mCurrentItem != null) { mCurrentItem.setSelected(false); } if (mOpenItem != null) { mOpenItem = null; mCurrentItems = mItems; } mCurrentItem = null; mPieView = null; } private PointF getPolar(float x, float y) { PointF res = new PointF(); // get angle and radius from x/y res.x = (float) Math.PI / 2; x = mCenter.x - x; if (mCenter.x < mSlop) { x = -x; } y = mCenter.y - y; res.y = (float) Math.sqrt(x * x + y * y); if (y > 0) { res.x = (float) Math.asin(x / res.y); } else if (y < 0) { res.x = (float) (Math.PI - Math.asin(x / res.y )); } return res; } /** * * @param polar x: angle, y: dist * @return the item at angle/dist or null */ private PieItem findItem(PointF polar) { // find the matching item: for (PieItem item : mCurrentItems) { if (inside(polar, mTouchOffset, item)) { return item; } } return null; } private boolean inside(PointF polar, float offset, PieItem item) { return (item.getInnerRadius() - offset < polar.y) && (item.getOuterRadius() - offset > polar.y) && (item.getStartAngle() < polar.x) && (item.getStartAngle() + item.getSweep() > polar.x); } }