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