• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.browser.view;
18 
19 import android.animation.Animator;
20 import android.animation.Animator.AnimatorListener;
21 import android.animation.AnimatorListenerAdapter;
22 import android.animation.ValueAnimator;
23 import android.animation.ValueAnimator.AnimatorUpdateListener;
24 import android.content.Context;
25 import android.content.res.Resources;
26 import android.graphics.Canvas;
27 import android.graphics.Paint;
28 import android.graphics.Path;
29 import android.graphics.Point;
30 import android.graphics.PointF;
31 import android.graphics.RectF;
32 import android.graphics.drawable.Drawable;
33 import android.util.AttributeSet;
34 import android.view.MotionEvent;
35 import android.view.SoundEffectConstants;
36 import android.view.View;
37 import android.view.ViewGroup;
38 import android.widget.FrameLayout;
39 
40 import com.android.browser.R;
41 
42 import java.util.ArrayList;
43 import java.util.List;
44 
45 public class PieMenu extends FrameLayout {
46 
47     private static final int MAX_LEVELS = 5;
48     private static final long ANIMATION = 80;
49 
50     public interface PieController {
51         /**
52          * called before menu opens to customize menu
53          * returns if pie state has been changed
54          */
onOpen()55         public boolean onOpen();
stopEditingUrl()56         public void stopEditingUrl();
57 
58     }
59 
60     /**
61      * A view like object that lives off of the pie menu
62      */
63     public interface PieView {
64 
65         public interface OnLayoutListener {
onLayout(int ax, int ay, boolean left)66             public void onLayout(int ax, int ay, boolean left);
67         }
68 
setLayoutListener(OnLayoutListener l)69         public void setLayoutListener(OnLayoutListener l);
70 
layout(int anchorX, int anchorY, boolean onleft, float angle, int parentHeight)71         public void layout(int anchorX, int anchorY, boolean onleft, float angle,
72                 int parentHeight);
73 
draw(Canvas c)74         public void draw(Canvas c);
75 
onTouchEvent(MotionEvent evt)76         public boolean onTouchEvent(MotionEvent evt);
77 
78     }
79 
80     private Point mCenter;
81     private int mRadius;
82     private int mRadiusInc;
83     private int mSlop;
84     private int mTouchOffset;
85     private Path mPath;
86 
87     private boolean mOpen;
88     private PieController mController;
89 
90     private List<PieItem> mItems;
91     private int mLevels;
92     private int[] mCounts;
93     private PieView mPieView = null;
94 
95     // sub menus
96     private List<PieItem> mCurrentItems;
97     private PieItem mOpenItem;
98 
99     private Drawable mBackground;
100     private Paint mNormalPaint;
101     private Paint mSelectedPaint;
102     private Paint mSubPaint;
103 
104     // touch handling
105     private PieItem mCurrentItem;
106 
107     private boolean mUseBackground;
108     private boolean mAnimating;
109 
110     /**
111      * @param context
112      * @param attrs
113      * @param defStyle
114      */
PieMenu(Context context, AttributeSet attrs, int defStyle)115     public PieMenu(Context context, AttributeSet attrs, int defStyle) {
116         super(context, attrs, defStyle);
117         init(context);
118     }
119 
120     /**
121      * @param context
122      * @param attrs
123      */
PieMenu(Context context, AttributeSet attrs)124     public PieMenu(Context context, AttributeSet attrs) {
125         super(context, attrs);
126         init(context);
127     }
128 
129     /**
130      * @param context
131      */
PieMenu(Context context)132     public PieMenu(Context context) {
133         super(context);
134         init(context);
135     }
136 
init(Context ctx)137     private void init(Context ctx) {
138         mItems = new ArrayList<PieItem>();
139         mLevels = 0;
140         mCounts = new int[MAX_LEVELS];
141         Resources res = ctx.getResources();
142         mRadius = (int) res.getDimension(R.dimen.qc_radius_start);
143         mRadiusInc = (int) res.getDimension(R.dimen.qc_radius_increment);
144         mSlop = (int) res.getDimension(R.dimen.qc_slop);
145         mTouchOffset = (int) res.getDimension(R.dimen.qc_touch_offset);
146         mOpen = false;
147         setWillNotDraw(false);
148         setDrawingCacheEnabled(false);
149         mCenter = new Point(0,0);
150         mBackground = res.getDrawable(R.drawable.qc_background_normal);
151         mNormalPaint = new Paint();
152         mNormalPaint.setColor(res.getColor(R.color.qc_normal));
153         mNormalPaint.setAntiAlias(true);
154         mSelectedPaint = new Paint();
155         mSelectedPaint.setColor(res.getColor(R.color.qc_selected));
156         mSelectedPaint.setAntiAlias(true);
157         mSubPaint = new Paint();
158         mSubPaint.setAntiAlias(true);
159         mSubPaint.setColor(res.getColor(R.color.qc_sub));
160     }
161 
setController(PieController ctl)162     public void setController(PieController ctl) {
163         mController = ctl;
164     }
165 
setUseBackground(boolean useBackground)166     public void setUseBackground(boolean useBackground) {
167         mUseBackground = useBackground;
168     }
169 
addItem(PieItem item)170     public void addItem(PieItem item) {
171         // add the item to the pie itself
172         mItems.add(item);
173         int l = item.getLevel();
174         mLevels = Math.max(mLevels, l);
175         mCounts[l]++;
176     }
177 
removeItem(PieItem item)178     public void removeItem(PieItem item) {
179         mItems.remove(item);
180     }
181 
clearItems()182     public void clearItems() {
183         mItems.clear();
184     }
185 
onTheLeft()186     private boolean onTheLeft() {
187         return mCenter.x < mSlop;
188     }
189 
190     /**
191      * guaranteed has center set
192      * @param show
193      */
show(boolean show)194     private void show(boolean show) {
195         mOpen = show;
196         if (mOpen) {
197             // ensure clean state
198             mAnimating = false;
199             mCurrentItem = null;
200             mOpenItem = null;
201             mPieView = null;
202             mController.stopEditingUrl();
203             mCurrentItems = mItems;
204             for (PieItem item : mCurrentItems) {
205                 item.setSelected(false);
206             }
207             if (mController != null) {
208                 boolean changed = mController.onOpen();
209             }
210             layoutPie();
211             animateOpen();
212         }
213         invalidate();
214     }
215 
animateOpen()216     private void animateOpen() {
217         ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
218         anim.addUpdateListener(new AnimatorUpdateListener() {
219             @Override
220             public void onAnimationUpdate(ValueAnimator animation) {
221                 for (PieItem item : mCurrentItems) {
222                     item.setAnimationAngle((1 - animation.getAnimatedFraction()) * (- item.getStart()));
223                 }
224                 invalidate();
225             }
226 
227         });
228         anim.setDuration(2*ANIMATION);
229         anim.start();
230     }
231 
setCenter(int x, int y)232     private void setCenter(int x, int y) {
233         if (x < mSlop) {
234             mCenter.x = 0;
235         } else {
236             mCenter.x = getWidth();
237         }
238         mCenter.y = y;
239     }
240 
layoutPie()241     private void layoutPie() {
242         float emptyangle = (float) Math.PI / 16;
243         int rgap = 2;
244         int inner = mRadius + rgap;
245         int outer = mRadius + mRadiusInc - rgap;
246         int gap = 1;
247         for (int i = 0; i < mLevels; i++) {
248             int level = i + 1;
249             float sweep = (float) (Math.PI - 2 * emptyangle) / mCounts[level];
250             float angle = emptyangle + sweep / 2;
251             mPath = makeSlice(getDegrees(0) - gap, getDegrees(sweep) + gap, outer, inner, mCenter);
252             for (PieItem item : mCurrentItems) {
253                 if (item.getLevel() == level) {
254                     View view = item.getView();
255                     if (view != null) {
256                         view.measure(view.getLayoutParams().width,
257                                 view.getLayoutParams().height);
258                         int w = view.getMeasuredWidth();
259                         int h = view.getMeasuredHeight();
260                         int r = inner + (outer - inner) * 2 / 3;
261                         int x = (int) (r * Math.sin(angle));
262                         int y = mCenter.y - (int) (r * Math.cos(angle)) - h / 2;
263                         if (onTheLeft()) {
264                             x = mCenter.x + x - w / 2;
265                         } else {
266                             x = mCenter.x - x - w / 2;
267                         }
268                         view.layout(x, y, x + w, y + h);
269                     }
270                     float itemstart = angle - sweep / 2;
271                     item.setGeometry(itemstart, sweep, inner, outer);
272                     angle += sweep;
273                 }
274             }
275             inner += mRadiusInc;
276             outer += mRadiusInc;
277         }
278     }
279 
280 
281     /**
282      * converts a
283      *
284      * @param angle from 0..PI to Android degrees (clockwise starting at 3
285      *        o'clock)
286      * @return skia angle
287      */
getDegrees(double angle)288     private float getDegrees(double angle) {
289         return (float) (270 - 180 * angle / Math.PI);
290     }
291 
292     @Override
onDraw(Canvas canvas)293     protected void onDraw(Canvas canvas) {
294         if (mOpen) {
295             int state;
296             if (mUseBackground) {
297                 int w = mBackground.getIntrinsicWidth();
298                 int h = mBackground.getIntrinsicHeight();
299                 int left = mCenter.x - w;
300                 int top = mCenter.y - h / 2;
301                 mBackground.setBounds(left, top, left + w, top + h);
302                 state = canvas.save();
303                 if (onTheLeft()) {
304                     canvas.scale(-1, 1);
305                 }
306                 mBackground.draw(canvas);
307                 canvas.restoreToCount(state);
308             }
309             // draw base menu
310             PieItem last = mCurrentItem;
311             if (mOpenItem != null) {
312                 last = mOpenItem;
313             }
314             for (PieItem item : mCurrentItems) {
315                 if (item != last) {
316                     drawItem(canvas, item);
317                 }
318             }
319             if (last != null) {
320                 drawItem(canvas, last);
321             }
322             if (mPieView != null) {
323                 mPieView.draw(canvas);
324             }
325         }
326     }
327 
drawItem(Canvas canvas, PieItem item)328     private void drawItem(Canvas canvas, PieItem item) {
329         if (item.getView() != null) {
330             Paint p = item.isSelected() ? mSelectedPaint : mNormalPaint;
331             if (!mItems.contains(item)) {
332                 p = item.isSelected() ? mSelectedPaint : mSubPaint;
333             }
334             int state = canvas.save();
335             if (onTheLeft()) {
336                 canvas.scale(-1, 1);
337             }
338             float r = getDegrees(item.getStartAngle()) - 270; // degrees(0)
339             canvas.rotate(r, mCenter.x, mCenter.y);
340             canvas.drawPath(mPath, p);
341             canvas.restoreToCount(state);
342             // draw the item view
343             View view = item.getView();
344             state = canvas.save();
345             canvas.translate(view.getX(), view.getY());
346             view.draw(canvas);
347             canvas.restoreToCount(state);
348         }
349     }
350 
makeSlice(float start, float end, int outer, int inner, Point center)351     private Path makeSlice(float start, float end, int outer, int inner, Point center) {
352         RectF bb =
353                 new RectF(center.x - outer, center.y - outer, center.x + outer,
354                         center.y + outer);
355         RectF bbi =
356                 new RectF(center.x - inner, center.y - inner, center.x + inner,
357                         center.y + inner);
358         Path path = new Path();
359         path.arcTo(bb, start, end - start, true);
360         path.arcTo(bbi, end, start - end);
361         path.close();
362         return path;
363     }
364 
365     // touch handling for pie
366 
367     @Override
onTouchEvent(MotionEvent evt)368     public boolean onTouchEvent(MotionEvent evt) {
369         float x = evt.getX();
370         float y = evt.getY();
371         int action = evt.getActionMasked();
372         if (MotionEvent.ACTION_DOWN == action) {
373             if ((x > getWidth() - mSlop) || (x < mSlop)) {
374                 setCenter((int) x, (int) y);
375                 show(true);
376                 return true;
377             }
378         } else if (MotionEvent.ACTION_UP == action) {
379             if (mOpen) {
380                 boolean handled = false;
381                 if (mPieView != null) {
382                     handled = mPieView.onTouchEvent(evt);
383                 }
384                 PieItem item = mCurrentItem;
385                 if (!mAnimating) {
386                     deselect();
387                 }
388                 show(false);
389                 if (!handled && (item != null) && (item.getView() != null)) {
390                     if ((item == mOpenItem) || !mAnimating) {
391                         item.getView().performClick();
392                     }
393                 }
394                 return true;
395             }
396         } else if (MotionEvent.ACTION_CANCEL == action) {
397             if (mOpen) {
398                 show(false);
399             }
400             if (!mAnimating) {
401                 deselect();
402                 invalidate();
403             }
404             return false;
405         } else if (MotionEvent.ACTION_MOVE == action) {
406             if (mAnimating) return false;
407             boolean handled = false;
408             PointF polar = getPolar(x, y);
409             int maxr = mRadius + mLevels * mRadiusInc + 50;
410             if (mPieView != null) {
411                 handled = mPieView.onTouchEvent(evt);
412             }
413             if (handled) {
414                 invalidate();
415                 return false;
416             }
417             if (polar.y < mRadius) {
418                 if (mOpenItem != null) {
419                     closeSub();
420                 } else if (!mAnimating) {
421                     deselect();
422                     invalidate();
423                 }
424                 return false;
425             }
426             if (polar.y > maxr) {
427                 deselect();
428                 show(false);
429                 evt.setAction(MotionEvent.ACTION_DOWN);
430                 if (getParent() != null) {
431                     ((ViewGroup) getParent()).dispatchTouchEvent(evt);
432                 }
433                 return false;
434             }
435             PieItem item = findItem(polar);
436             if (item == null) {
437             } else if (mCurrentItem != item) {
438                 onEnter(item);
439                 if ((item != null) && item.isPieView() && (item.getView() != null)) {
440                     int cx = item.getView().getLeft() + (onTheLeft()
441                             ? item.getView().getWidth() : 0);
442                     int cy = item.getView().getTop();
443                     mPieView = item.getPieView();
444                     layoutPieView(mPieView, cx, cy,
445                             (item.getStartAngle() + item.getSweep()) / 2);
446                 }
447                 invalidate();
448             }
449         }
450         // always re-dispatch event
451         return false;
452     }
453 
layoutPieView(PieView pv, int x, int y, float angle)454     private void layoutPieView(PieView pv, int x, int y, float angle) {
455         pv.layout(x, y, onTheLeft(), angle, getHeight());
456     }
457 
458     /**
459      * enter a slice for a view
460      * updates model only
461      * @param item
462      */
onEnter(PieItem item)463     private void onEnter(PieItem item) {
464         // deselect
465         if (mCurrentItem != null) {
466             mCurrentItem.setSelected(false);
467         }
468         if (item != null) {
469             // clear up stack
470             playSoundEffect(SoundEffectConstants.CLICK);
471             item.setSelected(true);
472             mPieView = null;
473             mCurrentItem = item;
474             if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) {
475                 openSub(mCurrentItem);
476                 mOpenItem = item;
477             }
478         } else {
479             mCurrentItem = null;
480         }
481 
482     }
483 
animateOut(final PieItem fixed, AnimatorListener listener)484     private void animateOut(final PieItem fixed, AnimatorListener listener) {
485         if ((mCurrentItems == null) || (fixed == null)) return;
486         final float target = fixed.getStartAngle();
487         ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
488         anim.addUpdateListener(new AnimatorUpdateListener() {
489             @Override
490             public void onAnimationUpdate(ValueAnimator animation) {
491                 for (PieItem item : mCurrentItems) {
492                     if (item != fixed) {
493                         item.setAnimationAngle(animation.getAnimatedFraction()
494                                 * (target - item.getStart()));
495                     }
496                 }
497                 invalidate();
498             }
499         });
500         anim.setDuration(ANIMATION);
501         anim.addListener(listener);
502         anim.start();
503     }
504 
animateIn(final PieItem fixed, AnimatorListener listener)505     private void animateIn(final PieItem fixed, AnimatorListener listener) {
506         if ((mCurrentItems == null) || (fixed == null)) return;
507         final float target = fixed.getStartAngle();
508         ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
509         anim.addUpdateListener(new AnimatorUpdateListener() {
510             @Override
511             public void onAnimationUpdate(ValueAnimator animation) {
512                 for (PieItem item : mCurrentItems) {
513                     if (item != fixed) {
514                         item.setAnimationAngle((1 - animation.getAnimatedFraction())
515                                 * (target - item.getStart()));
516                     }
517                 }
518                 invalidate();
519 
520             }
521 
522         });
523         anim.setDuration(ANIMATION);
524         anim.addListener(listener);
525         anim.start();
526     }
527 
openSub(final PieItem item)528     private void openSub(final PieItem item) {
529         mAnimating = true;
530         animateOut(item, new AnimatorListenerAdapter() {
531             public void onAnimationEnd(Animator a) {
532                 for (PieItem item : mCurrentItems) {
533                     item.setAnimationAngle(0);
534                 }
535                 mCurrentItems = new ArrayList<PieItem>(mItems.size());
536                 int i = 0, j = 0;
537                 while (i < mItems.size()) {
538                     if (mItems.get(i) == item) {
539                         mCurrentItems.add(item);
540                     } else {
541                         mCurrentItems.add(item.getItems().get(j++));
542                     }
543                     i++;
544                 }
545                 layoutPie();
546                 animateIn(item, new AnimatorListenerAdapter() {
547                     public void onAnimationEnd(Animator a) {
548                         for (PieItem item : mCurrentItems) {
549                             item.setAnimationAngle(0);
550                         }
551                         mAnimating = false;
552                     }
553                 });
554             }
555         });
556     }
557 
closeSub()558     private void closeSub() {
559         mAnimating = true;
560         if (mCurrentItem != null) {
561             mCurrentItem.setSelected(false);
562         }
563         animateOut(mOpenItem, new AnimatorListenerAdapter() {
564             public void onAnimationEnd(Animator a) {
565                 for (PieItem item : mCurrentItems) {
566                     item.setAnimationAngle(0);
567                 }
568                 mCurrentItems = mItems;
569                 mPieView = null;
570                 animateIn(mOpenItem, new AnimatorListenerAdapter() {
571                     public void onAnimationEnd(Animator a) {
572                         for (PieItem item : mCurrentItems) {
573                             item.setAnimationAngle(0);
574                         }
575                         mAnimating = false;
576                         mOpenItem = null;
577                         mCurrentItem = null;
578                     }
579                 });
580             }
581         });
582     }
583 
deselect()584     private void deselect() {
585         if (mCurrentItem != null) {
586             mCurrentItem.setSelected(false);
587         }
588         if (mOpenItem != null) {
589             mOpenItem = null;
590             mCurrentItems = mItems;
591         }
592         mCurrentItem = null;
593         mPieView = null;
594     }
595 
getPolar(float x, float y)596     private PointF getPolar(float x, float y) {
597         PointF res = new PointF();
598         // get angle and radius from x/y
599         res.x = (float) Math.PI / 2;
600         x = mCenter.x - x;
601         if (mCenter.x < mSlop) {
602             x = -x;
603         }
604         y = mCenter.y - y;
605         res.y = (float) Math.sqrt(x * x + y * y);
606         if (y > 0) {
607             res.x = (float) Math.asin(x / res.y);
608         } else if (y < 0) {
609             res.x = (float) (Math.PI - Math.asin(x / res.y ));
610         }
611         return res;
612     }
613 
614     /**
615      *
616      * @param polar x: angle, y: dist
617      * @return the item at angle/dist or null
618      */
findItem(PointF polar)619     private PieItem findItem(PointF polar) {
620         // find the matching item:
621         for (PieItem item : mCurrentItems) {
622             if (inside(polar, mTouchOffset, item)) {
623                 return item;
624             }
625         }
626         return null;
627     }
628 
inside(PointF polar, float offset, PieItem item)629     private boolean inside(PointF polar, float offset, PieItem item) {
630         return (item.getInnerRadius() - offset < polar.y)
631         && (item.getOuterRadius() - offset > polar.y)
632         && (item.getStartAngle() < polar.x)
633         && (item.getStartAngle() + item.getSweep() > polar.x);
634     }
635 
636 }
637