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