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