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