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