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