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