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 
17 package com.android.messaging.ui.mediapicker.camerafocus;
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 import com.android.messaging.R;
39 
40 import java.util.ArrayList;
41 import java.util.List;
42 
43 public 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 {
onPieOpened(int centerX, int centerY)137         public void onPieOpened(int centerX, int centerY);
onPieClosed()138         public void onPieClosed();
139     }
140 
setPieListener(PieListener pl)141     public void setPieListener(PieListener pl) {
142         mListener = pl;
143     }
144 
PieRenderer(Context context)145     public PieRenderer(Context context) {
146         init(context);
147     }
148 
init(Context ctx)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 
showsItems()184     public boolean showsItems() {
185         return mTapMode;
186     }
187 
addItem(PieItem item)188     public void addItem(PieItem item) {
189         // add the item to the pie itself
190         mItems.add(item);
191     }
192 
removeItem(PieItem item)193     public void removeItem(PieItem item) {
194         mItems.remove(item);
195     }
196 
clearItems()197     public void clearItems() {
198         mItems.clear();
199     }
200 
showInCenter()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 
hide()216     public void hide() {
217         show(false);
218     }
219 
220     /**
221      * guaranteed has center set
222      * @param show
223      */
show(boolean show)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 
fadeIn()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 
setCenter(int x, int y)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 
layoutPie()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 
layoutItems(List<PieItem> items, float centerAngle, int inner, int outer, int gap)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 
makeSlice(float start, float end, int outer, int inner, Point center)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      */
getDegrees(double angle)341     private float getDegrees(double angle) {
342         return (float) (360 - 180 * angle / Math.PI);
343     }
344 
startFadeOut()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
onDraw(Canvas canvas)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 
drawItem(Canvas canvas, PieItem item, float alpha)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
onTouchEvent(MotionEvent evt)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 
hasMoved(MotionEvent e)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      */
onEnter(PieItem item)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 
deselect()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 
openCurrentItem()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 
getPolar(float x, float y, boolean useOffset)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      */
findItem(PointF polar)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 
inside(PointF polar, PieItem item)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
handlesTouch()581     public boolean handlesTouch() {
582         return true;
583     }
584 
585     // focus specific code
586 
setBlockFocus(boolean blocked)587     public void setBlockFocus(boolean blocked) {
588         mBlockFocus = blocked;
589         if (blocked) {
590             clear();
591         }
592     }
593 
setFocus(int x, int y)594     public void setFocus(int x, int y) {
595         mFocusX = x;
596         mFocusY = y;
597         setCircle(mFocusX, mFocusY);
598     }
599 
alignFocus(int x, int y)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 
getSize()611     public int getSize() {
612         return 2 * mCircleSize;
613     }
614 
getRandomRange()615     private int getRandomRange() {
616         return (int) (-60 + 120 * Math.random());
617     }
618 
619     @Override
layout(int l, int t, int r, int b)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 
setCircle(int cx, int cy)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 
drawFocus(Canvas canvas)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 
drawLine(Canvas canvas, int angle, Paint p)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 
convertCart(int angle, int radius, Point out)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
showStart()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
showSuccess(boolean timeout)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
showFail(boolean timeout)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 
cancelFocus()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
clear()725     public void clear() {
726         if (mState == STATE_PIE) {
727             return;
728         }
729         cancelFocus();
730         mOverlay.post(mDisappear);
731     }
732 
startAnimation(long duration, boolean timeout, float toScale)733     private void startAnimation(long duration, boolean timeout,
734                                 float toScale) {
735         startAnimation(duration, timeout, mDialAngle,
736                 toScale);
737     }
738 
startAnimation(long duration, boolean timeout, float fromScale, float toScale)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
onAnimationEnd(Animation animation)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
onAnimationRepeat(Animation animation)760         public void onAnimationRepeat(Animation animation) {
761         }
762 
763         @Override
onAnimationStart(Animation animation)764         public void onAnimationStart(Animation animation) {
765         }
766     }
767 
768     private class Disappear implements Runnable {
769         @Override
run()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 
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 }