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