• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2012 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.camera.ui;
18 
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.graphics.Canvas;
22 import android.graphics.Color;
23 import android.graphics.Paint;
24 import android.graphics.Path;
25 import android.graphics.Point;
26 import android.graphics.PointF;
27 import android.graphics.RectF;
28 import android.os.Handler;
29 import android.os.Message;
30 import android.util.FloatMath;
31 import android.view.MotionEvent;
32 import android.view.ViewConfiguration;
33 import android.view.animation.Animation;
34 import android.view.animation.Animation.AnimationListener;
35 import android.view.animation.LinearInterpolator;
36 import android.view.animation.Transformation;
37 
38 import com.android.camera.drawable.TextDrawable;
39 import com.android.gallery3d.R;
40 
41 import java.util.ArrayList;
42 import java.util.List;
43 
44 public class PieRenderer extends OverlayRenderer
45         implements FocusIndicator {
46 
47     private static final String TAG = "CAM Pie";
48 
49     // Sometimes continuous autofocus starts and stops several times quickly.
50     // These states are used to make sure the animation is run for at least some
51     // time.
52     private volatile int mState;
53     private ScaleAnimation mAnimation = new ScaleAnimation();
54     private static final int STATE_IDLE = 0;
55     private static final int STATE_FOCUSING = 1;
56     private static final int STATE_FINISHING = 2;
57     private static final int STATE_PIE = 8;
58 
59     private static final float MATH_PI_2 = (float)(Math.PI / 2);
60 
61     private Runnable mDisappear = new Disappear();
62     private Animation.AnimationListener mEndAction = new EndAction();
63     private static final int SCALING_UP_TIME = 600;
64     private static final int SCALING_DOWN_TIME = 100;
65     private static final int DISAPPEAR_TIMEOUT = 200;
66     private static final int DIAL_HORIZONTAL = 157;
67     // fade out timings
68     private static final int PIE_FADE_OUT_DURATION = 600;
69 
70     private static final long PIE_FADE_IN_DURATION = 200;
71     private static final long PIE_XFADE_DURATION = 200;
72     private static final long PIE_SELECT_FADE_DURATION = 300;
73     private static final long PIE_OPEN_SUB_DELAY = 400;
74     private static final long PIE_SLICE_DURATION = 80;
75 
76     private static final int MSG_OPEN = 0;
77     private static final int MSG_CLOSE = 1;
78     private static final int MSG_OPENSUBMENU = 2;
79 
80     protected static float CENTER = (float) Math.PI / 2;
81     protected static float RAD24 = (float)(24 * Math.PI / 180);
82     protected static final float SWEEP_SLICE = 0.14f;
83     protected static final float SWEEP_ARC = 0.23f;
84 
85     // geometry
86     private int mRadius;
87     private int mRadiusInc;
88 
89     // the detection if touch is inside a slice is offset
90     // inbounds by this amount to allow the selection to show before the
91     // finger covers it
92     private int mTouchOffset;
93 
94     private List<PieItem> mOpen;
95 
96     private Paint mSelectedPaint;
97     private Paint mSubPaint;
98     private Paint mMenuArcPaint;
99 
100     // touch handling
101     private PieItem mCurrentItem;
102 
103     private Paint mFocusPaint;
104     private int mSuccessColor;
105     private int mFailColor;
106     private int mCircleSize;
107     private int mFocusX;
108     private int mFocusY;
109     private int mCenterX;
110     private int mCenterY;
111     private int mArcCenterY;
112     private int mSliceCenterY;
113     private int mPieCenterX;
114     private int mPieCenterY;
115     private int mSliceRadius;
116     private int mArcRadius;
117     private int mArcOffset;
118 
119     private int mDialAngle;
120     private RectF mCircle;
121     private RectF mDial;
122     private Point mPoint1;
123     private Point mPoint2;
124     private int mStartAnimationAngle;
125     private boolean mFocused;
126     private int mInnerOffset;
127     private int mOuterStroke;
128     private int mInnerStroke;
129     private boolean mTapMode;
130     private boolean mBlockFocus;
131     private int mTouchSlopSquared;
132     private Point mDown;
133     private boolean mOpening;
134     private LinearAnimation mXFade;
135     private LinearAnimation mFadeIn;
136     private FadeOutAnimation mFadeOut;
137     private LinearAnimation mSlice;
138     private volatile boolean mFocusCancelled;
139     private PointF mPolar = new PointF();
140     private TextDrawable mLabel;
141     private int mDeadZone;
142     private int mAngleZone;
143     private float mCenterAngle;
144 
145 
146 
147     private Handler mHandler = new Handler() {
148         public void handleMessage(Message msg) {
149             switch(msg.what) {
150             case MSG_OPEN:
151                 if (mListener != null) {
152                     mListener.onPieOpened(mPieCenterX, mPieCenterY);
153                 }
154                 break;
155             case MSG_CLOSE:
156                 if (mListener != null) {
157                     mListener.onPieClosed();
158                 }
159                 break;
160             case MSG_OPENSUBMENU:
161                 onEnterOpen();
162                 break;
163             }
164 
165         }
166     };
167 
168     private PieListener mListener;
169 
170     static public interface PieListener {
onPieOpened(int centerX, int centerY)171         public void onPieOpened(int centerX, int centerY);
onPieClosed()172         public void onPieClosed();
173     }
174 
setPieListener(PieListener pl)175     public void setPieListener(PieListener pl) {
176         mListener = pl;
177     }
178 
PieRenderer(Context context)179     public PieRenderer(Context context) {
180         init(context);
181     }
182 
init(Context ctx)183     private void init(Context ctx) {
184         setVisible(false);
185         mOpen = new ArrayList<PieItem>();
186         mOpen.add(new PieItem(null, 0));
187         Resources res = ctx.getResources();
188         mRadius = (int) res.getDimensionPixelSize(R.dimen.pie_radius_start);
189         mRadiusInc = (int) res.getDimensionPixelSize(R.dimen.pie_radius_increment);
190         mCircleSize = mRadius - res.getDimensionPixelSize(R.dimen.focus_radius_offset);
191         mTouchOffset = (int) res.getDimensionPixelSize(R.dimen.pie_touch_offset);
192         mSelectedPaint = new Paint();
193         mSelectedPaint.setColor(Color.argb(255, 51, 181, 229));
194         mSelectedPaint.setAntiAlias(true);
195         mSubPaint = new Paint();
196         mSubPaint.setAntiAlias(true);
197         mSubPaint.setColor(Color.argb(200, 250, 230, 128));
198         mFocusPaint = new Paint();
199         mFocusPaint.setAntiAlias(true);
200         mFocusPaint.setColor(Color.WHITE);
201         mFocusPaint.setStyle(Paint.Style.STROKE);
202         mSuccessColor = Color.GREEN;
203         mFailColor = Color.RED;
204         mCircle = new RectF();
205         mDial = new RectF();
206         mPoint1 = new Point();
207         mPoint2 = new Point();
208         mInnerOffset = res.getDimensionPixelSize(R.dimen.focus_inner_offset);
209         mOuterStroke = res.getDimensionPixelSize(R.dimen.focus_outer_stroke);
210         mInnerStroke = res.getDimensionPixelSize(R.dimen.focus_inner_stroke);
211         mState = STATE_IDLE;
212         mBlockFocus = false;
213         mTouchSlopSquared = ViewConfiguration.get(ctx).getScaledTouchSlop();
214         mTouchSlopSquared = mTouchSlopSquared * mTouchSlopSquared;
215         mDown = new Point();
216         mMenuArcPaint = new Paint();
217         mMenuArcPaint.setAntiAlias(true);
218         mMenuArcPaint.setColor(Color.argb(140, 255, 255, 255));
219         mMenuArcPaint.setStrokeWidth(10);
220         mMenuArcPaint.setStyle(Paint.Style.STROKE);
221         mSliceRadius = res.getDimensionPixelSize(R.dimen.pie_item_radius);
222         mArcRadius = res.getDimensionPixelSize(R.dimen.pie_arc_radius);
223         mArcOffset = res.getDimensionPixelSize(R.dimen.pie_arc_offset);
224         mLabel = new TextDrawable(res);
225         mLabel.setDropShadow(true);
226         mDeadZone = res.getDimensionPixelSize(R.dimen.pie_deadzone_width);
227         mAngleZone = res.getDimensionPixelSize(R.dimen.pie_anglezone_width);
228     }
229 
getRoot()230     private PieItem getRoot() {
231         return mOpen.get(0);
232     }
233 
showsItems()234     public boolean showsItems() {
235         return mTapMode;
236     }
237 
addItem(PieItem item)238     public void addItem(PieItem item) {
239         // add the item to the pie itself
240         getRoot().addItem(item);
241     }
242 
clearItems()243     public void clearItems() {
244         getRoot().clearItems();
245     }
246 
showInCenter()247     public void showInCenter() {
248         if ((mState == STATE_PIE) && isVisible()) {
249             mTapMode = false;
250             show(false);
251         } else {
252             if (mState != STATE_IDLE) {
253                 cancelFocus();
254             }
255             mState = STATE_PIE;
256             resetPieCenter();
257             setCenter(mPieCenterX, mPieCenterY);
258             mTapMode = true;
259             show(true);
260         }
261     }
262 
hide()263     public void hide() {
264         show(false);
265     }
266 
267     /**
268      * guaranteed has center set
269      * @param show
270      */
show(boolean show)271     private void show(boolean show) {
272         if (show) {
273             if (mXFade != null) {
274                 mXFade.cancel();
275             }
276             mState = STATE_PIE;
277             // ensure clean state
278             mCurrentItem = null;
279             PieItem root = getRoot();
280             for (PieItem openItem : mOpen) {
281                 if (openItem.hasItems()) {
282                     for (PieItem item : openItem.getItems()) {
283                         item.setSelected(false);
284                     }
285                 }
286             }
287             mLabel.setText("");
288             mOpen.clear();
289             mOpen.add(root);
290             layoutPie();
291             fadeIn();
292         } else {
293             mState = STATE_IDLE;
294             mTapMode = false;
295             if (mXFade != null) {
296                 mXFade.cancel();
297             }
298             if (mLabel != null) {
299                 mLabel.setText("");
300             }
301         }
302         setVisible(show);
303         mHandler.sendEmptyMessage(show ? MSG_OPEN : MSG_CLOSE);
304     }
305 
fadeIn()306     private void fadeIn() {
307         mFadeIn = new LinearAnimation(0, 1);
308         mFadeIn.setDuration(PIE_FADE_IN_DURATION);
309         mFadeIn.setAnimationListener(new AnimationListener() {
310             @Override
311             public void onAnimationStart(Animation animation) {
312             }
313 
314             @Override
315             public void onAnimationEnd(Animation animation) {
316                 mFadeIn = null;
317             }
318 
319             @Override
320             public void onAnimationRepeat(Animation animation) {
321             }
322         });
323         mFadeIn.startNow();
324         mOverlay.startAnimation(mFadeIn);
325     }
326 
setCenter(int x, int y)327     public void setCenter(int x, int y) {
328         mPieCenterX = x;
329         mPieCenterY = y;
330         mSliceCenterY = y + mSliceRadius - mArcOffset;
331         mArcCenterY = y - mArcOffset + mArcRadius;
332     }
333 
334     @Override
layout(int l, int t, int r, int b)335     public void layout(int l, int t, int r, int b) {
336         super.layout(l, t, r, b);
337         mCenterX = (r - l) / 2;
338         mCenterY = (b - t) / 2;
339 
340         mFocusX = mCenterX;
341         mFocusY = mCenterY;
342         resetPieCenter();
343         setCircle(mFocusX, mFocusY);
344         if (isVisible() && mState == STATE_PIE) {
345             setCenter(mPieCenterX, mPieCenterY);
346             layoutPie();
347         }
348     }
349 
resetPieCenter()350     private void resetPieCenter() {
351         mPieCenterX = mCenterX;
352         mPieCenterY = (int) (getHeight() - 2.5f * mDeadZone);
353     }
354 
layoutPie()355     private void layoutPie() {
356         mCenterAngle = getCenterAngle();
357         layoutItems(0, getRoot().getItems());
358         layoutLabel(getLevel());
359     }
360 
layoutLabel(int level)361     private void layoutLabel(int level) {
362         int x = mPieCenterX - (int) (FloatMath.sin(mCenterAngle - CENTER)
363                 * (mArcRadius + (level + 2) * mRadiusInc));
364         int y = mArcCenterY - mArcRadius - (level + 2) * mRadiusInc;
365         int w = mLabel.getIntrinsicWidth();
366         int h = mLabel.getIntrinsicHeight();
367         mLabel.setBounds(x - w/2, y - h/2, x + w/2, y + h/2);
368     }
369 
layoutItems(int level, List<PieItem> items)370     private void layoutItems(int level, List<PieItem> items) {
371         int extend = 1;
372         Path path = makeSlice(getDegrees(0) + extend, getDegrees(SWEEP_ARC) - extend,
373                 mArcRadius, mArcRadius + mRadiusInc + mRadiusInc / 4,
374                 mPieCenterX, mArcCenterY - level * mRadiusInc);
375         final int count = items.size();
376         int pos = 0;
377         for (PieItem item : items) {
378             // shared between items
379             item.setPath(path);
380             float angle = getArcCenter(item, pos, count);
381             int w = item.getIntrinsicWidth();
382             int h = item.getIntrinsicHeight();
383             // move views to outer border
384             int r = mArcRadius + mRadiusInc * 2 / 3;
385             int x = (int) (r * Math.cos(angle));
386             int y = mArcCenterY - (level * mRadiusInc) - (int) (r * Math.sin(angle)) - h / 2;
387             x = mPieCenterX + x - w / 2;
388             item.setBounds(x, y, x + w, y + h);
389             item.setLevel(level);
390             if (item.hasItems()) {
391                 layoutItems(level + 1, item.getItems());
392             }
393             pos++;
394         }
395     }
396 
makeSlice(float start, float end, int inner, int outer, int cx, int cy)397     private Path makeSlice(float start, float end, int inner, int outer, int cx, int cy) {
398         RectF bb =
399                 new RectF(cx - outer, cy - outer, cx + outer,
400                         cy + outer);
401         RectF bbi =
402                 new RectF(cx - inner, cy - inner, cx + inner,
403                         cy + inner);
404         Path path = new Path();
405         path.arcTo(bb, start, end - start, true);
406         path.arcTo(bbi, end, start - end);
407         path.close();
408         return path;
409     }
410 
getArcCenter(PieItem item, int pos, int count)411     private float getArcCenter(PieItem item, int pos, int count) {
412         return getCenter(pos, count, SWEEP_ARC);
413     }
414 
getSliceCenter(PieItem item, int pos, int count)415     private float getSliceCenter(PieItem item, int pos, int count) {
416         float center = (getCenterAngle() - CENTER) * 0.5f + CENTER;
417         return center + (count - 1) * SWEEP_SLICE / 2f
418                 - pos * SWEEP_SLICE;
419     }
420 
getCenter(int pos, int count, float sweep)421     private float getCenter(int pos, int count, float sweep) {
422         return mCenterAngle + (count - 1) * sweep / 2f - pos * sweep;
423     }
424 
getCenterAngle()425     private float getCenterAngle() {
426         float center = CENTER;
427         if (mPieCenterX < mDeadZone + mAngleZone) {
428             center = CENTER - (mAngleZone - mPieCenterX + mDeadZone) * RAD24
429                     / (float) mAngleZone;
430         } else if (mPieCenterX > getWidth() - mDeadZone - mAngleZone) {
431             center = CENTER + (mPieCenterX - (getWidth() - mDeadZone - mAngleZone)) * RAD24
432                     / (float) mAngleZone;
433         }
434         return center;
435     }
436 
437     /**
438      * converts a
439      * @param angle from 0..PI to Android degrees (clockwise starting at 3 o'clock)
440      * @return skia angle
441      */
getDegrees(double angle)442     private float getDegrees(double angle) {
443         return (float) (360 - 180 * angle / Math.PI);
444     }
445 
startFadeOut(final PieItem item)446     private void startFadeOut(final PieItem item) {
447         if (mFadeIn != null) {
448             mFadeIn.cancel();
449         }
450         if (mXFade != null) {
451             mXFade.cancel();
452         }
453         mFadeOut = new FadeOutAnimation();
454         mFadeOut.setDuration(PIE_FADE_OUT_DURATION);
455         mFadeOut.setAnimationListener(new AnimationListener() {
456             @Override
457             public void onAnimationStart(Animation animation) {
458             }
459 
460             @Override
461             public void onAnimationEnd(Animation animation) {
462                 item.performClick();
463                 mFadeOut = null;
464                 deselect();
465                 show(false);
466                 mOverlay.setAlpha(1);
467             }
468 
469             @Override
470             public void onAnimationRepeat(Animation animation) {
471             }
472         });
473         mFadeOut.startNow();
474         mOverlay.startAnimation(mFadeOut);
475     }
476 
477     // root does not count
hasOpenItem()478     private boolean hasOpenItem() {
479         return mOpen.size() > 1;
480     }
481 
482     // pop an item of the open item stack
closeOpenItem()483     private PieItem closeOpenItem() {
484         PieItem item = getOpenItem();
485         mOpen.remove(mOpen.size() -1);
486         return item;
487     }
488 
getOpenItem()489     private PieItem getOpenItem() {
490         return mOpen.get(mOpen.size() - 1);
491     }
492 
493     // return the children either the root or parent of the current open item
getParent()494     private PieItem getParent() {
495         return mOpen.get(Math.max(0, mOpen.size() - 2));
496     }
497 
getLevel()498     private int getLevel() {
499         return mOpen.size() - 1;
500     }
501 
502     @Override
onDraw(Canvas canvas)503     public void onDraw(Canvas canvas) {
504         float alpha = 1;
505         if (mXFade != null) {
506             alpha = mXFade.getValue();
507         } else if (mFadeIn != null) {
508             alpha = mFadeIn.getValue();
509         } else if (mFadeOut != null) {
510             alpha = mFadeOut.getValue();
511         }
512         int state = canvas.save();
513         if (mFadeIn != null) {
514             float sf = 0.9f + alpha * 0.1f;
515             canvas.scale(sf, sf, mPieCenterX, mPieCenterY);
516         }
517         if (mState != STATE_PIE) {
518             drawFocus(canvas);
519         }
520         if (mState == STATE_FINISHING) {
521             canvas.restoreToCount(state);
522             return;
523         }
524         if (mState != STATE_PIE) return;
525         if (!hasOpenItem() || (mXFade != null)) {
526             // draw base menu
527             drawArc(canvas, getLevel(), getParent());
528             List<PieItem> items = getParent().getItems();
529             final int count = items.size();
530             int pos = 0;
531             for (PieItem item : getParent().getItems()) {
532                 drawItem(Math.max(0, mOpen.size() - 2), pos, count, canvas, item, alpha);
533                 pos++;
534             }
535             mLabel.draw(canvas);
536         }
537         if (hasOpenItem()) {
538             int level = getLevel();
539             drawArc(canvas, level, getOpenItem());
540             List<PieItem> items = getOpenItem().getItems();
541             final int count = items.size();
542             int pos = 0;
543             for (PieItem inner : items) {
544                 if (mFadeOut != null) {
545                     drawItem(level, pos, count, canvas, inner, alpha);
546                 } else {
547                     drawItem(level, pos, count, canvas, inner, (mXFade != null) ? (1 - 0.5f * alpha) : 1);
548                 }
549                 pos++;
550             }
551             mLabel.draw(canvas);
552         }
553         canvas.restoreToCount(state);
554     }
555 
drawArc(Canvas canvas, int level, PieItem item)556     private void drawArc(Canvas canvas, int level, PieItem item) {
557         // arc
558         if (mState == STATE_PIE) {
559             final int count = item.getItems().size();
560             float start = mCenterAngle + (count * SWEEP_ARC / 2f);
561             float end =  mCenterAngle - (count * SWEEP_ARC / 2f);
562             int cy = mArcCenterY - level * mRadiusInc;
563             canvas.drawArc(new RectF(mPieCenterX - mArcRadius, cy - mArcRadius,
564                     mPieCenterX + mArcRadius, cy + mArcRadius),
565                     getDegrees(end), getDegrees(start) - getDegrees(end), false, mMenuArcPaint);
566         }
567     }
568 
drawItem(int level, int pos, int count, Canvas canvas, PieItem item, float alpha)569     private void drawItem(int level, int pos, int count, Canvas canvas, PieItem item, float alpha) {
570         if (mState == STATE_PIE) {
571             if (item.getPath() != null) {
572                 int y = mArcCenterY - level * mRadiusInc;
573                 if (item.isSelected()) {
574                     Paint p = mSelectedPaint;
575                     int state = canvas.save();
576                     float angle = 0;
577                     if (mSlice != null) {
578                         angle = mSlice.getValue();
579                     } else {
580                         angle = getArcCenter(item, pos, count) - SWEEP_ARC / 2f;
581                     }
582                     angle = getDegrees(angle);
583                     canvas.rotate(angle, mPieCenterX, y);
584                     if (mFadeOut != null) {
585                         p.setAlpha((int)(255 * alpha));
586                     }
587                     canvas.drawPath(item.getPath(), p);
588                     if (mFadeOut != null) {
589                         p.setAlpha(255);
590                     }
591                     canvas.restoreToCount(state);
592                 }
593                 if (mFadeOut == null) {
594                     alpha = alpha * (item.isEnabled() ? 1 : 0.3f);
595                     // draw the item view
596                     item.setAlpha(alpha);
597                 }
598                 item.draw(canvas);
599             }
600         }
601     }
602 
603     @Override
onTouchEvent(MotionEvent evt)604     public boolean onTouchEvent(MotionEvent evt) {
605         float x = evt.getX();
606         float y = evt.getY();
607         int action = evt.getActionMasked();
608         getPolar(x, y, !mTapMode, mPolar);
609         if (MotionEvent.ACTION_DOWN == action) {
610             if ((x < mDeadZone) || (x > getWidth() - mDeadZone)) {
611                 return false;
612             }
613             mDown.x = (int) evt.getX();
614             mDown.y = (int) evt.getY();
615             mOpening = false;
616             if (mTapMode) {
617                 PieItem item = findItem(mPolar);
618                 if ((item != null) && (mCurrentItem != item)) {
619                     mState = STATE_PIE;
620                     onEnter(item);
621                 }
622             } else {
623                 setCenter((int) x, (int) y);
624                 show(true);
625             }
626             return true;
627         } else if (MotionEvent.ACTION_UP == action) {
628             if (isVisible()) {
629                 PieItem item = mCurrentItem;
630                 if (mTapMode) {
631                     item = findItem(mPolar);
632                     if (mOpening) {
633                         mOpening = false;
634                         return true;
635                     }
636                 }
637                 if (item == null) {
638                     mTapMode = false;
639                     show(false);
640                 } else if (!mOpening && !item.hasItems()) {
641                         startFadeOut(item);
642                         mTapMode = false;
643                 } else {
644                     mTapMode = true;
645                 }
646                 return true;
647             }
648         } else if (MotionEvent.ACTION_CANCEL == action) {
649             if (isVisible() || mTapMode) {
650                 show(false);
651             }
652             deselect();
653             mHandler.removeMessages(MSG_OPENSUBMENU);
654             return false;
655         } else if (MotionEvent.ACTION_MOVE == action) {
656             if (pulledToCenter(mPolar)) {
657                 mHandler.removeMessages(MSG_OPENSUBMENU);
658                 if (hasOpenItem()) {
659                     if (mCurrentItem != null) {
660                         mCurrentItem.setSelected(false);
661                     }
662                     closeOpenItem();
663                     mCurrentItem = null;
664                 } else {
665                     deselect();
666                 }
667                 mLabel.setText("");
668                 return false;
669             }
670             PieItem item = findItem(mPolar);
671             boolean moved = hasMoved(evt);
672             if ((item != null) && (mCurrentItem != item) && (!mOpening || moved)) {
673                 mHandler.removeMessages(MSG_OPENSUBMENU);
674                 // only select if we didn't just open or have moved past slop
675                 if (moved) {
676                     // switch back to swipe mode
677                     mTapMode = false;
678                 }
679                 onEnterSelect(item);
680                 mHandler.sendEmptyMessageDelayed(MSG_OPENSUBMENU, PIE_OPEN_SUB_DELAY);
681             }
682         }
683         return false;
684     }
685 
pulledToCenter(PointF polarCoords)686     private boolean pulledToCenter(PointF polarCoords) {
687         return polarCoords.y < mArcRadius - mRadiusInc;
688     }
689 
inside(PointF polar, PieItem item, int pos, int count)690     private boolean inside(PointF polar, PieItem item, int pos, int count) {
691         float start = getSliceCenter(item, pos, count) - SWEEP_SLICE / 2f;
692         boolean res =  (mArcRadius < polar.y)
693                 && (start < polar.x)
694                 && (start + SWEEP_SLICE > polar.x)
695                 && (!mTapMode || (mArcRadius + mRadiusInc > polar.y));
696         return res;
697     }
698 
getPolar(float x, float y, boolean useOffset, PointF res)699     private void getPolar(float x, float y, boolean useOffset, PointF res) {
700         // get angle and radius from x/y
701         res.x = (float) Math.PI / 2;
702         x = x - mPieCenterX;
703         float y1 = mSliceCenterY - getLevel() * mRadiusInc - y;
704         float y2 = mArcCenterY - getLevel() * mRadiusInc - y;
705         res.y = (float) Math.sqrt(x * x + y2 * y2);
706         if (x != 0) {
707             res.x = (float) Math.atan2(y1,  x);
708             if (res.x < 0) {
709                 res.x = (float) (2 * Math.PI + res.x);
710             }
711         }
712         res.y = res.y + (useOffset ? mTouchOffset : 0);
713     }
714 
hasMoved(MotionEvent e)715     private boolean hasMoved(MotionEvent e) {
716         return mTouchSlopSquared < (e.getX() - mDown.x) * (e.getX() - mDown.x)
717                 + (e.getY() - mDown.y) * (e.getY() - mDown.y);
718     }
719 
onEnterSelect(PieItem item)720     private void onEnterSelect(PieItem item) {
721         if (mCurrentItem != null) {
722             mCurrentItem.setSelected(false);
723         }
724         if (item != null && item.isEnabled()) {
725             moveSelection(mCurrentItem, item);
726             item.setSelected(true);
727             mCurrentItem = item;
728             mLabel.setText(mCurrentItem.getLabel());
729             layoutLabel(getLevel());
730         } else {
731             mCurrentItem = null;
732         }
733     }
734 
onEnterOpen()735     private void onEnterOpen() {
736         if ((mCurrentItem != null) && (mCurrentItem != getOpenItem()) && mCurrentItem.hasItems()) {
737             openCurrentItem();
738         }
739     }
740 
741     /**
742      * enter a slice for a view
743      * updates model only
744      * @param item
745      */
onEnter(PieItem item)746     private void onEnter(PieItem item) {
747         if (mCurrentItem != null) {
748             mCurrentItem.setSelected(false);
749         }
750         if (item != null && item.isEnabled()) {
751             item.setSelected(true);
752             mCurrentItem = item;
753             mLabel.setText(mCurrentItem.getLabel());
754             if ((mCurrentItem != getOpenItem()) && mCurrentItem.hasItems()) {
755                 openCurrentItem();
756                 layoutLabel(getLevel());
757             }
758         } else {
759             mCurrentItem = null;
760         }
761     }
762 
deselect()763     private void deselect() {
764         if (mCurrentItem != null) {
765             mCurrentItem.setSelected(false);
766         }
767         if (hasOpenItem()) {
768             PieItem item = closeOpenItem();
769             onEnter(item);
770         } else {
771             mCurrentItem = null;
772         }
773     }
774 
getItemPos(PieItem target)775     private int getItemPos(PieItem target) {
776         List<PieItem> items = getOpenItem().getItems();
777         return items.indexOf(target);
778     }
779 
getCurrentCount()780     private int getCurrentCount() {
781         return getOpenItem().getItems().size();
782     }
783 
moveSelection(PieItem from, PieItem to)784     private void moveSelection(PieItem from, PieItem to) {
785         final int count = getCurrentCount();
786         final int fromPos = getItemPos(from);
787         final int toPos = getItemPos(to);
788         if (fromPos != -1 && toPos != -1) {
789             float startAngle = getArcCenter(from, getItemPos(from), count)
790                     - SWEEP_ARC / 2f;
791             float endAngle = getArcCenter(to, getItemPos(to), count)
792                     - SWEEP_ARC / 2f;
793             mSlice = new LinearAnimation(startAngle, endAngle);
794             mSlice.setDuration(PIE_SLICE_DURATION);
795             mSlice.setAnimationListener(new AnimationListener() {
796                 @Override
797                 public void onAnimationEnd(Animation arg0) {
798                     mSlice = null;
799                 }
800 
801                 @Override
802                 public void onAnimationRepeat(Animation arg0) {
803                 }
804 
805                 @Override
806                 public void onAnimationStart(Animation arg0) {
807                 }
808             });
809             mOverlay.startAnimation(mSlice);
810         }
811     }
812 
openCurrentItem()813     private void openCurrentItem() {
814         if ((mCurrentItem != null) && mCurrentItem.hasItems()) {
815             mOpen.add(mCurrentItem);
816             layoutLabel(getLevel());
817             mOpening = true;
818             if (mFadeIn != null) {
819                 mFadeIn.cancel();
820             }
821             mXFade = new LinearAnimation(1, 0);
822             mXFade.setDuration(PIE_XFADE_DURATION);
823             final PieItem ci = mCurrentItem;
824             mXFade.setAnimationListener(new AnimationListener() {
825                 @Override
826                 public void onAnimationStart(Animation animation) {
827                 }
828 
829                 @Override
830                 public void onAnimationEnd(Animation animation) {
831                     mXFade = null;
832                     ci.setSelected(false);
833                     mOpening = false;
834                 }
835 
836                 @Override
837                 public void onAnimationRepeat(Animation animation) {
838                 }
839             });
840             mXFade.startNow();
841             mOverlay.startAnimation(mXFade);
842         }
843     }
844 
845     /**
846      * @param polar x: angle, y: dist
847      * @return the item at angle/dist or null
848      */
findItem(PointF polar)849     private PieItem findItem(PointF polar) {
850         // find the matching item:
851         List<PieItem> items = getOpenItem().getItems();
852         final int count = items.size();
853         int pos = 0;
854         for (PieItem item : items) {
855             if (inside(polar, item, pos, count)) {
856                 return item;
857             }
858             pos++;
859         }
860         return null;
861     }
862 
863 
864     @Override
handlesTouch()865     public boolean handlesTouch() {
866         return true;
867     }
868 
869     // focus specific code
870 
setBlockFocus(boolean blocked)871     public void setBlockFocus(boolean blocked) {
872         mBlockFocus = blocked;
873         if (blocked) {
874             clear();
875         }
876     }
877 
setFocus(int x, int y)878     public void setFocus(int x, int y) {
879         mFocusX = x;
880         mFocusY = y;
881         setCircle(mFocusX, mFocusY);
882     }
883 
alignFocus(int x, int y)884     public void alignFocus(int x, int y) {
885         mOverlay.removeCallbacks(mDisappear);
886         mAnimation.cancel();
887         mAnimation.reset();
888         mFocusX = x;
889         mFocusY = y;
890         mDialAngle = DIAL_HORIZONTAL;
891         setCircle(x, y);
892         mFocused = false;
893     }
894 
getSize()895     public int getSize() {
896         return 2 * mCircleSize;
897     }
898 
getRandomRange()899     private int getRandomRange() {
900         return (int)(-60 + 120 * Math.random());
901     }
902 
setCircle(int cx, int cy)903     private void setCircle(int cx, int cy) {
904         mCircle.set(cx - mCircleSize, cy - mCircleSize,
905                 cx + mCircleSize, cy + mCircleSize);
906         mDial.set(cx - mCircleSize + mInnerOffset, cy - mCircleSize + mInnerOffset,
907                 cx + mCircleSize - mInnerOffset, cy + mCircleSize - mInnerOffset);
908     }
909 
drawFocus(Canvas canvas)910     public void drawFocus(Canvas canvas) {
911         if (mBlockFocus) return;
912         mFocusPaint.setStrokeWidth(mOuterStroke);
913         canvas.drawCircle((float) mFocusX, (float) mFocusY, (float) mCircleSize, mFocusPaint);
914         if (mState == STATE_PIE) return;
915         int color = mFocusPaint.getColor();
916         if (mState == STATE_FINISHING) {
917             mFocusPaint.setColor(mFocused ? mSuccessColor : mFailColor);
918         }
919         mFocusPaint.setStrokeWidth(mInnerStroke);
920         drawLine(canvas, mDialAngle, mFocusPaint);
921         drawLine(canvas, mDialAngle + 45, mFocusPaint);
922         drawLine(canvas, mDialAngle + 180, mFocusPaint);
923         drawLine(canvas, mDialAngle + 225, mFocusPaint);
924         canvas.save();
925         // rotate the arc instead of its offset to better use framework's shape caching
926         canvas.rotate(mDialAngle, mFocusX, mFocusY);
927         canvas.drawArc(mDial, 0, 45, false, mFocusPaint);
928         canvas.drawArc(mDial, 180, 45, false, mFocusPaint);
929         canvas.restore();
930         mFocusPaint.setColor(color);
931     }
932 
drawLine(Canvas canvas, int angle, Paint p)933     private void drawLine(Canvas canvas, int angle, Paint p) {
934         convertCart(angle, mCircleSize - mInnerOffset, mPoint1);
935         convertCart(angle, mCircleSize - mInnerOffset + mInnerOffset / 3, mPoint2);
936         canvas.drawLine(mPoint1.x + mFocusX, mPoint1.y + mFocusY,
937                 mPoint2.x + mFocusX, mPoint2.y + mFocusY, p);
938     }
939 
convertCart(int angle, int radius, Point out)940     private static void convertCart(int angle, int radius, Point out) {
941         double a = 2 * Math.PI * (angle % 360) / 360;
942         out.x = (int) (radius * Math.cos(a) + 0.5);
943         out.y = (int) (radius * Math.sin(a) + 0.5);
944     }
945 
946     @Override
showStart()947     public void showStart() {
948         if (mState == STATE_PIE) return;
949         cancelFocus();
950         mStartAnimationAngle = 67;
951         int range = getRandomRange();
952         startAnimation(SCALING_UP_TIME,
953                 false, mStartAnimationAngle, mStartAnimationAngle + range);
954         mState = STATE_FOCUSING;
955     }
956 
957     @Override
showSuccess(boolean timeout)958     public void showSuccess(boolean timeout) {
959         if (mState == STATE_FOCUSING) {
960             startAnimation(SCALING_DOWN_TIME,
961                     timeout, mStartAnimationAngle);
962             mState = STATE_FINISHING;
963             mFocused = true;
964         }
965     }
966 
967     @Override
showFail(boolean timeout)968     public void showFail(boolean timeout) {
969         if (mState == STATE_FOCUSING) {
970             startAnimation(SCALING_DOWN_TIME,
971                     timeout, mStartAnimationAngle);
972             mState = STATE_FINISHING;
973             mFocused = false;
974         }
975     }
976 
cancelFocus()977     private void cancelFocus() {
978         mFocusCancelled = true;
979         mOverlay.removeCallbacks(mDisappear);
980         if (mAnimation != null && !mAnimation.hasEnded()) {
981             mAnimation.cancel();
982         }
983         mFocusCancelled = false;
984         mFocused = false;
985         mState = STATE_IDLE;
986     }
987 
988     @Override
clear()989     public void clear() {
990         if (mState == STATE_PIE) return;
991         cancelFocus();
992         mOverlay.post(mDisappear);
993     }
994 
startAnimation(long duration, boolean timeout, float toScale)995     private void startAnimation(long duration, boolean timeout,
996             float toScale) {
997         startAnimation(duration, timeout, mDialAngle,
998                 toScale);
999     }
1000 
startAnimation(long duration, boolean timeout, float fromScale, float toScale)1001     private void startAnimation(long duration, boolean timeout,
1002             float fromScale, float toScale) {
1003         setVisible(true);
1004         mAnimation.reset();
1005         mAnimation.setDuration(duration);
1006         mAnimation.setScale(fromScale, toScale);
1007         mAnimation.setAnimationListener(timeout ? mEndAction : null);
1008         mOverlay.startAnimation(mAnimation);
1009         update();
1010     }
1011 
1012     private class EndAction implements Animation.AnimationListener {
1013         @Override
onAnimationEnd(Animation animation)1014         public void onAnimationEnd(Animation animation) {
1015             // Keep the focus indicator for some time.
1016             if (!mFocusCancelled) {
1017                 mOverlay.postDelayed(mDisappear, DISAPPEAR_TIMEOUT);
1018             }
1019         }
1020 
1021         @Override
onAnimationRepeat(Animation animation)1022         public void onAnimationRepeat(Animation animation) {
1023         }
1024 
1025         @Override
onAnimationStart(Animation animation)1026         public void onAnimationStart(Animation animation) {
1027         }
1028     }
1029 
1030     private class Disappear implements Runnable {
1031         @Override
run()1032         public void run() {
1033             if (mState == STATE_PIE) return;
1034             setVisible(false);
1035             mFocusX = mCenterX;
1036             mFocusY = mCenterY;
1037             mState = STATE_IDLE;
1038             setCircle(mFocusX, mFocusY);
1039             mFocused = false;
1040         }
1041     }
1042 
1043     private class FadeOutAnimation extends Animation {
1044 
1045         private float mAlpha;
1046 
getValue()1047         public float getValue() {
1048             return mAlpha;
1049         }
1050 
1051         @Override
applyTransformation(float interpolatedTime, Transformation t)1052         protected void applyTransformation(float interpolatedTime, Transformation t) {
1053             if (interpolatedTime < 0.2) {
1054                 mAlpha = 1;
1055             } else if (interpolatedTime < 0.3) {
1056                 mAlpha = 0;
1057             } else {
1058                 mAlpha = 1 - (interpolatedTime - 0.3f) / 0.7f;
1059             }
1060         }
1061     }
1062 
1063     private class ScaleAnimation extends Animation {
1064         private float mFrom = 1f;
1065         private float mTo = 1f;
1066 
ScaleAnimation()1067         public ScaleAnimation() {
1068             setFillAfter(true);
1069         }
1070 
setScale(float from, float to)1071         public void setScale(float from, float to) {
1072             mFrom = from;
1073             mTo = to;
1074         }
1075 
1076         @Override
applyTransformation(float interpolatedTime, Transformation t)1077         protected void applyTransformation(float interpolatedTime, Transformation t) {
1078             mDialAngle = (int)(mFrom + (mTo - mFrom) * interpolatedTime);
1079         }
1080     }
1081 
1082     private class LinearAnimation extends Animation {
1083         private float mFrom;
1084         private float mTo;
1085         private float mValue;
1086 
LinearAnimation(float from, float to)1087         public LinearAnimation(float from, float to) {
1088             setFillAfter(true);
1089             setInterpolator(new LinearInterpolator());
1090             mFrom = from;
1091             mTo = to;
1092         }
1093 
getValue()1094         public float getValue() {
1095             return mValue;
1096         }
1097 
1098         @Override
applyTransformation(float interpolatedTime, Transformation t)1099         protected void applyTransformation(float interpolatedTime, Transformation t) {
1100             mValue = (mFrom + (mTo - mFrom) * interpolatedTime);
1101         }
1102     }
1103 
1104 }
1105