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