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 android.animation.Animator;
20import android.animation.Animator.AnimatorListener;
21import android.animation.ValueAnimator;
22import android.content.Context;
23import android.content.res.Resources;
24import android.graphics.Canvas;
25import android.graphics.Color;
26import android.graphics.Paint;
27import android.graphics.Path;
28import android.graphics.Point;
29import android.graphics.PointF;
30import android.graphics.RectF;
31import android.os.Handler;
32import android.os.Message;
33import android.util.FloatMath;
34import android.view.MotionEvent;
35import android.view.ViewConfiguration;
36import android.view.animation.Animation;
37import android.view.animation.Transformation;
38
39import com.android.camera.drawable.TextDrawable;
40import com.android.camera.ui.ProgressRenderer.VisibilityListener;
41import com.android.camera2.R;
42
43import java.util.ArrayList;
44import java.util.List;
45
46/**
47 * An overlay renderer that is used to display focus state and progress state.
48 */
49public 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 {
176        public void onPieOpened(int centerX, int centerY);
177        public void onPieClosed();
178    }
179
180    public void setPieListener(PieListener pl) {
181        mListener = pl;
182    }
183
184    public PieRenderer(Context context) {
185        init(context);
186    }
187
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
236    private PieItem getRoot() {
237        return mOpen.get(0);
238    }
239
240    public boolean showsItems() {
241        return mTapMode;
242    }
243
244    public void addItem(PieItem item) {
245        // add the item to the pie itself
246        getRoot().addItem(item);
247    }
248
249    public void clearItems() {
250        getRoot().clearItems();
251    }
252
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
269    public void hide() {
270        show(false);
271    }
272
273    /**
274     * guaranteed has center set
275     * @param show
276     */
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
312    public boolean isOpen() {
313        return mState == STATE_PIE && isVisible();
314    }
315
316    public void setProgress(int percent) {
317        mProgressRenderer.setProgress(percent);
318    }
319
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
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
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
370    private void resetPieCenter() {
371        mPieCenterX = mCenterX;
372        mPieCenterY = (int) (getHeight() - 2.5f * mDeadZone);
373    }
374
375    private void layoutPie() {
376        mCenterAngle = getCenterAngle();
377        layoutItems(0, getRoot().getItems());
378        layoutLabel(getLevel());
379    }
380
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
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
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
431    private float getArcCenter(PieItem item, int pos, int count) {
432        return getCenter(pos, count, SWEEP_ARC);
433    }
434
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
441    private float getCenter(int pos, int count, float sweep) {
442        return mCenterAngle + (count - 1) * sweep / 2f - pos * sweep;
443    }
444
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     */
462    private float getDegrees(double angle) {
463        return (float) (360 - 180 * angle / Math.PI);
464    }
465
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
503    private boolean hasOpenItem() {
504        return mOpen.size() > 1;
505    }
506
507    // pop an item of the open item stack
508    private PieItem closeOpenItem() {
509        PieItem item = getOpenItem();
510        mOpen.remove(mOpen.size() -1);
511        return item;
512    }
513
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
519    private PieItem getParent() {
520        return mOpen.get(Math.max(0, mOpen.size() - 2));
521    }
522
523    private int getLevel() {
524        return mOpen.size() - 1;
525    }
526
527    @Override
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
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
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
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
714    public boolean isVisible() {
715        return super.isVisible() || mProgressRenderer.isVisible();
716    }
717
718    private boolean pulledToCenter(PointF polarCoords) {
719        return polarCoords.y < mArcRadius - mRadiusInc;
720    }
721
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
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
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
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
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     */
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
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
807    private int getItemPos(PieItem target) {
808        List<PieItem> items = getOpenItem().getItems();
809        return items.indexOf(target);
810    }
811
812    private int getCurrentCount() {
813        return getOpenItem().getItems().size();
814    }
815
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
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     */
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
910    public boolean handlesTouch() {
911        return true;
912    }
913
914    // focus specific code
915
916    public void setBlockFocus(boolean blocked) {
917        mBlockFocus = blocked;
918        if (blocked) {
919            clear();
920        }
921    }
922
923    public void setFocus(int x, int y) {
924        mOverlay.removeCallbacks(mDisappear);
925        mFocusX = x;
926        mFocusY = y;
927        setCircle(mFocusX, mFocusY);
928    }
929
930    public int getSize() {
931        return 2 * mCircleSize;
932    }
933
934    private int getRandomRange() {
935        return (int)(-60 + 120 * Math.random());
936    }
937
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
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
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
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
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
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
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
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
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
1042    public void clear() {
1043        clear(false);
1044    }
1045
1046    private void startAnimation(long duration, boolean timeout,
1047            float toScale) {
1048        startAnimation(duration, timeout, mDialAngle,
1049                toScale);
1050    }
1051
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
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
1073        public void onAnimationRepeat(Animation animation) {
1074        }
1075
1076        @Override
1077        public void onAnimationStart(Animation animation) {
1078        }
1079    }
1080
1081    private class Disappear implements Runnable {
1082        @Override
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
1098        public ScaleAnimation() {
1099            setFillAfter(true);
1100        }
1101
1102        public void setScale(float from, float to) {
1103            mFrom = from;
1104            mTo = to;
1105        }
1106
1107        @Override
1108        protected void applyTransformation(float interpolatedTime, Transformation t) {
1109            mDialAngle = (int)(mFrom + (mTo - mFrom) * interpolatedTime);
1110        }
1111    }
1112
1113}
1114