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