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