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 
17 package com.android.camera.ui;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.content.Context;
22 import android.content.res.Resources;
23 import android.graphics.Canvas;
24 import android.graphics.Color;
25 import android.graphics.Paint;
26 import android.graphics.Path;
27 import android.graphics.Point;
28 import android.graphics.PointF;
29 import android.graphics.RectF;
30 import android.os.Handler;
31 import android.os.Message;
32 import android.view.MotionEvent;
33 import android.view.ViewConfiguration;
34 import android.view.animation.Animation;
35 import android.view.animation.Animation.AnimationListener;
36 import android.view.animation.LinearInterpolator;
37 import android.view.animation.Transformation;
38 
39 import com.android.camera.R;
40 import com.android.gallery3d.common.ApiHelper;
41 
42 import java.util.ArrayList;
43 import java.util.List;
44 
45 public 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 volatile 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 = 8;
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 PIE_SELECT_FADE_DURATION = 300;
70 
71     private static final int MSG_OPEN = 0;
72     private static final int MSG_CLOSE = 1;
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 mSelectedPaint;
89     private Paint mSubPaint;
90 
91     // touch handling
92     private PieItem mCurrentItem;
93 
94     private Paint mFocusPaint;
95     private int mSuccessColor;
96     private int mFailColor;
97     private int mCircleSize;
98     private int mFocusX;
99     private int mFocusY;
100     private int mCenterX;
101     private int mCenterY;
102 
103     private int mDialAngle;
104     private RectF mCircle;
105     private RectF mDial;
106     private Point mPoint1;
107     private Point mPoint2;
108     private int mStartAnimationAngle;
109     private boolean mFocused;
110     private int mInnerOffset;
111     private int mOuterStroke;
112     private int mInnerStroke;
113     private boolean mTapMode;
114     private boolean mBlockFocus;
115     private int mTouchSlopSquared;
116     private Point mDown;
117     private boolean mOpening;
118     private LinearAnimation mXFade;
119     private LinearAnimation mFadeIn;
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 {
onPieOpened(int centerX, int centerY)142         public void onPieOpened(int centerX, int centerY);
onPieClosed()143         public void onPieClosed();
144     }
145 
setPieListener(PieListener pl)146     public void setPieListener(PieListener pl) {
147         mListener = pl;
148     }
149 
PieRenderer(Context context)150     public PieRenderer(Context context) {
151         init(context);
152     }
153 
init(Context ctx)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 
showsItems()189     public boolean showsItems() {
190         return mTapMode;
191     }
192 
addItem(PieItem item)193     public void addItem(PieItem item) {
194         // add the item to the pie itself
195         mItems.add(item);
196     }
197 
removeItem(PieItem item)198     public void removeItem(PieItem item) {
199         mItems.remove(item);
200     }
201 
clearItems()202     public void clearItems() {
203         mItems.clear();
204     }
205 
showInCenter()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 
hide()221     public void hide() {
222         show(false);
223     }
224 
225     /**
226      * guaranteed has center set
227      * @param show
228      */
show(boolean show)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 
fadeIn()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 
setCenter(int x, int y)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 
layoutPie()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 
layoutItems(List<PieItem> items, float centerAngle, int inner, int outer, int gap)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 
makeSlice(float start, float end, int outer, int inner, Point center)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      */
getDegrees(double angle)346     private float getDegrees(double angle) {
347         return (float) (360 - 180 * angle / Math.PI);
348     }
349 
startFadeOut()350     private void startFadeOut() {
351         if (ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) {
352             mOverlay.animate().alpha(0).setListener(new AnimatorListenerAdapter() {
353                 @Override
354                 public void onAnimationEnd(Animator animation) {
355                     deselect();
356                     show(false);
357                     mOverlay.setAlpha(1);
358                     super.onAnimationEnd(animation);
359                 }
360             }).setDuration(PIE_SELECT_FADE_DURATION);
361         } else {
362             deselect();
363             show(false);
364         }
365     }
366 
367     @Override
onDraw(Canvas canvas)368     public void onDraw(Canvas canvas) {
369         float alpha = 1;
370         if (mXFade != null) {
371             alpha = mXFade.getValue();
372         } else if (mFadeIn != null) {
373             alpha = mFadeIn.getValue();
374         }
375         int state = canvas.save();
376         if (mFadeIn != null) {
377             float sf = 0.9f + alpha * 0.1f;
378             canvas.scale(sf, sf, mCenter.x, mCenter.y);
379         }
380         drawFocus(canvas);
381         if (mState == STATE_FINISHING) {
382             canvas.restoreToCount(state);
383             return;
384         }
385         if ((mOpenItem == null) || (mXFade != null)) {
386             // draw base menu
387             for (PieItem item : mItems) {
388                 drawItem(canvas, item, alpha);
389             }
390         }
391         if (mOpenItem != null) {
392             for (PieItem inner : mOpenItem.getItems()) {
393                 drawItem(canvas, inner, (mXFade != null) ? (1 - 0.5f * alpha) : 1);
394             }
395         }
396         canvas.restoreToCount(state);
397     }
398 
drawItem(Canvas canvas, PieItem item, float alpha)399     private void drawItem(Canvas canvas, PieItem item, float alpha) {
400         if (mState == STATE_PIE) {
401             if (item.getPath() != null) {
402                 if (item.isSelected()) {
403                     Paint p = mSelectedPaint;
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                 }
410                 alpha = alpha * (item.isEnabled() ? 1 : 0.3f);
411                 // draw the item view
412                 item.setAlpha(alpha);
413                 item.draw(canvas);
414             }
415         }
416     }
417 
418     @Override
onTouchEvent(MotionEvent evt)419     public boolean onTouchEvent(MotionEvent evt) {
420         float x = evt.getX();
421         float y = evt.getY();
422         int action = evt.getActionMasked();
423         PointF polar = getPolar(x, y, !(mTapMode));
424         if (MotionEvent.ACTION_DOWN == action) {
425             mDown.x = (int) evt.getX();
426             mDown.y = (int) evt.getY();
427             mOpening = false;
428             if (mTapMode) {
429                 PieItem item = findItem(polar);
430                 if ((item != null) && (mCurrentItem != item)) {
431                     mState = STATE_PIE;
432                     onEnter(item);
433                 }
434             } else {
435                 setCenter((int) x, (int) y);
436                 show(true);
437             }
438             return true;
439         } else if (MotionEvent.ACTION_UP == action) {
440             if (isVisible()) {
441                 PieItem item = mCurrentItem;
442                 if (mTapMode) {
443                     item = findItem(polar);
444                     if (item != null && mOpening) {
445                         mOpening = false;
446                         return true;
447                     }
448                 }
449                 if (item == null) {
450                     mTapMode = false;
451                     show(false);
452                 } else if (!mOpening
453                         && !item.hasItems()) {
454                     item.performClick();
455                     startFadeOut();
456                     mTapMode = false;
457                 }
458                 return true;
459             }
460         } else if (MotionEvent.ACTION_CANCEL == action) {
461             if (isVisible() || mTapMode) {
462                 show(false);
463             }
464             deselect();
465             return false;
466         } else if (MotionEvent.ACTION_MOVE == action) {
467             if (polar.y < mRadius) {
468                 if (mOpenItem != null) {
469                     mOpenItem = null;
470                 } else {
471                     deselect();
472                 }
473                 return false;
474             }
475             PieItem item = findItem(polar);
476             boolean moved = hasMoved(evt);
477             if ((item != null) && (mCurrentItem != item) && (!mOpening || moved)) {
478                 // only select if we didn't just open or have moved past slop
479                 mOpening = false;
480                 if (moved) {
481                     // switch back to swipe mode
482                     mTapMode = false;
483                 }
484                 onEnter(item);
485             }
486         }
487         return false;
488     }
489 
hasMoved(MotionEvent e)490     private boolean hasMoved(MotionEvent e) {
491         return mTouchSlopSquared < (e.getX() - mDown.x) * (e.getX() - mDown.x)
492                 + (e.getY() - mDown.y) * (e.getY() - mDown.y);
493     }
494 
495     /**
496      * enter a slice for a view
497      * updates model only
498      * @param item
499      */
onEnter(PieItem item)500     private void onEnter(PieItem item) {
501         if (mCurrentItem != null) {
502             mCurrentItem.setSelected(false);
503         }
504         if (item != null && item.isEnabled()) {
505             item.setSelected(true);
506             mCurrentItem = item;
507             if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) {
508                 openCurrentItem();
509             }
510         } else {
511             mCurrentItem = null;
512         }
513     }
514 
deselect()515     private void deselect() {
516         if (mCurrentItem != null) {
517             mCurrentItem.setSelected(false);
518         }
519         if (mOpenItem != null) {
520             mOpenItem = null;
521         }
522         mCurrentItem = null;
523     }
524 
openCurrentItem()525     private void openCurrentItem() {
526         if ((mCurrentItem != null) && mCurrentItem.hasItems()) {
527             mCurrentItem.setSelected(false);
528             mOpenItem = mCurrentItem;
529             mOpening = true;
530             mXFade = new LinearAnimation(1, 0);
531             mXFade.setDuration(PIE_XFADE_DURATION);
532             mXFade.setAnimationListener(new AnimationListener() {
533                 @Override
534                 public void onAnimationStart(Animation animation) {
535                 }
536 
537                 @Override
538                 public void onAnimationEnd(Animation animation) {
539                     mXFade = null;
540                 }
541 
542                 @Override
543                 public void onAnimationRepeat(Animation animation) {
544                 }
545             });
546             mXFade.startNow();
547             mOverlay.startAnimation(mXFade);
548         }
549     }
550 
getPolar(float x, float y, boolean useOffset)551     private PointF getPolar(float x, float y, boolean useOffset) {
552         PointF res = new PointF();
553         // get angle and radius from x/y
554         res.x = (float) Math.PI / 2;
555         x = x - mCenter.x;
556         y = mCenter.y - y;
557         res.y = (float) Math.sqrt(x * x + y * y);
558         if (x != 0) {
559             res.x = (float) Math.atan2(y,  x);
560             if (res.x < 0) {
561                 res.x = (float) (2 * Math.PI + res.x);
562             }
563         }
564         res.y = res.y + (useOffset ? mTouchOffset : 0);
565         return res;
566     }
567 
568     /**
569      * @param polar x: angle, y: dist
570      * @return the item at angle/dist or null
571      */
findItem(PointF polar)572     private PieItem findItem(PointF polar) {
573         // find the matching item:
574         List<PieItem> items = (mOpenItem != null) ? mOpenItem.getItems() : mItems;
575         for (PieItem item : items) {
576             if (inside(polar, item)) {
577                 return item;
578             }
579         }
580         return null;
581     }
582 
inside(PointF polar, PieItem item)583     private boolean inside(PointF polar, PieItem item) {
584         return (item.getInnerRadius() < polar.y)
585                 && (item.getStartAngle() < polar.x)
586                 && (item.getStartAngle() + item.getSweep() > polar.x)
587                 && (!mTapMode || (item.getOuterRadius() > polar.y));
588     }
589 
590     @Override
handlesTouch()591     public boolean handlesTouch() {
592         return true;
593     }
594 
595     // focus specific code
596 
setBlockFocus(boolean blocked)597     public void setBlockFocus(boolean blocked) {
598         mBlockFocus = blocked;
599         if (blocked) {
600             clear();
601         }
602     }
603 
setFocus(int x, int y)604     public void setFocus(int x, int y) {
605         mFocusX = x;
606         mFocusY = y;
607         setCircle(mFocusX, mFocusY);
608     }
609 
alignFocus(int x, int y)610     public void alignFocus(int x, int y) {
611         mOverlay.removeCallbacks(mDisappear);
612         mAnimation.cancel();
613         mAnimation.reset();
614         mFocusX = x;
615         mFocusY = y;
616         mDialAngle = DIAL_HORIZONTAL;
617         setCircle(x, y);
618         mFocused = false;
619     }
620 
getSize()621     public int getSize() {
622         return 2 * mCircleSize;
623     }
624 
getRandomRange()625     private int getRandomRange() {
626         return (int)(-60 + 120 * Math.random());
627     }
628 
629     @Override
layout(int l, int t, int r, int b)630     public void layout(int l, int t, int r, int b) {
631         super.layout(l, t, r, b);
632         mCenterX = (r - l) / 2;
633         mCenterY = (b - t) / 2;
634         mFocusX = mCenterX;
635         mFocusY = mCenterY;
636         setCircle(mFocusX, mFocusY);
637         if (isVisible() && mState == STATE_PIE) {
638             setCenter(mCenterX, mCenterY);
639             layoutPie();
640         }
641     }
642 
setCircle(int cx, int cy)643     private void setCircle(int cx, int cy) {
644         mCircle.set(cx - mCircleSize, cy - mCircleSize,
645                 cx + mCircleSize, cy + mCircleSize);
646         mDial.set(cx - mCircleSize + mInnerOffset, cy - mCircleSize + mInnerOffset,
647                 cx + mCircleSize - mInnerOffset, cy + mCircleSize - mInnerOffset);
648     }
649 
drawFocus(Canvas canvas)650     public void drawFocus(Canvas canvas) {
651         if (mBlockFocus) return;
652         mFocusPaint.setStrokeWidth(mOuterStroke);
653         canvas.drawCircle((float) mFocusX, (float) mFocusY, (float) mCircleSize, mFocusPaint);
654         if (mState == STATE_PIE) return;
655         int color = mFocusPaint.getColor();
656         if (mState == STATE_FINISHING) {
657             mFocusPaint.setColor(mFocused ? mSuccessColor : mFailColor);
658         }
659         mFocusPaint.setStrokeWidth(mInnerStroke);
660         drawLine(canvas, mDialAngle, mFocusPaint);
661         drawLine(canvas, mDialAngle + 45, mFocusPaint);
662         drawLine(canvas, mDialAngle + 180, mFocusPaint);
663         drawLine(canvas, mDialAngle + 225, mFocusPaint);
664         canvas.save();
665         // rotate the arc instead of its offset to better use framework's shape caching
666         canvas.rotate(mDialAngle, mFocusX, mFocusY);
667         canvas.drawArc(mDial, 0, 45, false, mFocusPaint);
668         canvas.drawArc(mDial, 180, 45, false, mFocusPaint);
669         canvas.restore();
670         mFocusPaint.setColor(color);
671     }
672 
drawLine(Canvas canvas, int angle, Paint p)673     private void drawLine(Canvas canvas, int angle, Paint p) {
674         convertCart(angle, mCircleSize - mInnerOffset, mPoint1);
675         convertCart(angle, mCircleSize - mInnerOffset + mInnerOffset / 3, mPoint2);
676         canvas.drawLine(mPoint1.x + mFocusX, mPoint1.y + mFocusY,
677                 mPoint2.x + mFocusX, mPoint2.y + mFocusY, p);
678     }
679 
convertCart(int angle, int radius, Point out)680     private static void convertCart(int angle, int radius, Point out) {
681         double a = 2 * Math.PI * (angle % 360) / 360;
682         out.x = (int) (radius * Math.cos(a) + 0.5);
683         out.y = (int) (radius * Math.sin(a) + 0.5);
684     }
685 
686     @Override
showStart()687     public void showStart() {
688         if (mState == STATE_PIE) return;
689         cancelFocus();
690         mStartAnimationAngle = 67;
691         int range = getRandomRange();
692         startAnimation(SCALING_UP_TIME,
693                 false, mStartAnimationAngle, mStartAnimationAngle + range);
694         mState = STATE_FOCUSING;
695     }
696 
697     @Override
showSuccess(boolean timeout)698     public void showSuccess(boolean timeout) {
699         if (mState == STATE_FOCUSING) {
700             startAnimation(SCALING_DOWN_TIME,
701                     timeout, mStartAnimationAngle);
702             mState = STATE_FINISHING;
703             mFocused = true;
704         }
705     }
706 
707     @Override
showFail(boolean timeout)708     public void showFail(boolean timeout) {
709         if (mState == STATE_FOCUSING) {
710             startAnimation(SCALING_DOWN_TIME,
711                     timeout, mStartAnimationAngle);
712             mState = STATE_FINISHING;
713             mFocused = false;
714         }
715     }
716 
cancelFocus()717     private void cancelFocus() {
718         mFocusCancelled = true;
719         mOverlay.removeCallbacks(mDisappear);
720         if (mAnimation != null) {
721             mAnimation.cancel();
722         }
723         mFocusCancelled = false;
724         mFocused = false;
725         mState = STATE_IDLE;
726     }
727 
728     @Override
clear()729     public void clear() {
730         if (mState == STATE_PIE) return;
731         cancelFocus();
732         mOverlay.post(mDisappear);
733     }
734 
startAnimation(long duration, boolean timeout, float toScale)735     private void startAnimation(long duration, boolean timeout,
736             float toScale) {
737         startAnimation(duration, timeout, mDialAngle,
738                 toScale);
739     }
740 
startAnimation(long duration, boolean timeout, float fromScale, float toScale)741     private void startAnimation(long duration, boolean timeout,
742             float fromScale, float toScale) {
743         setVisible(true);
744         mAnimation.reset();
745         mAnimation.setDuration(duration);
746         mAnimation.setScale(fromScale, toScale);
747         mAnimation.setAnimationListener(timeout ? mEndAction : null);
748         mOverlay.startAnimation(mAnimation);
749         update();
750     }
751 
752     private class EndAction implements Animation.AnimationListener {
753         @Override
onAnimationEnd(Animation animation)754         public void onAnimationEnd(Animation animation) {
755             // Keep the focus indicator for some time.
756             if (!mFocusCancelled) {
757                 mOverlay.postDelayed(mDisappear, DISAPPEAR_TIMEOUT);
758             }
759         }
760 
761         @Override
onAnimationRepeat(Animation animation)762         public void onAnimationRepeat(Animation animation) {
763         }
764 
765         @Override
onAnimationStart(Animation animation)766         public void onAnimationStart(Animation animation) {
767         }
768     }
769 
770     private class Disappear implements Runnable {
771         @Override
run()772         public void run() {
773             if (mState == STATE_PIE) return;
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 
ScaleAnimation()787         public ScaleAnimation() {
788             setFillAfter(true);
789         }
790 
setScale(float from, float to)791         public void setScale(float from, float to) {
792             mFrom = from;
793             mTo = to;
794         }
795 
796         @Override
applyTransformation(float interpolatedTime, Transformation t)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 
LinearAnimation(float from, float to)808         public LinearAnimation(float from, float to) {
809             setFillAfter(true);
810             setInterpolator(new LinearInterpolator());
811             mFrom = from;
812             mTo = to;
813         }
814 
getValue()815         public float getValue() {
816             return mValue;
817         }
818 
819         @Override
applyTransformation(float interpolatedTime, Transformation t)820         protected void applyTransformation(float interpolatedTime, Transformation t) {
821             mValue = (mFrom + (mTo - mFrom) * interpolatedTime);
822         }
823     }
824 
825 }
826