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