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