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