Home | History | Annotate | Download | only in camerafocus
      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 {
    138     void onPieOpened(int centerX, int centerY);
    139 
    140     void onPieClosed();
    141   }
    142 
    143   public void setPieListener(PieListener pl) {
    144     mListener = pl;
    145   }
    146 
    147   public PieRenderer(Context context) {
    148     init(context);
    149   }
    150 
    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 
    186   public boolean showsItems() {
    187     return mTapMode;
    188   }
    189 
    190   public void addItem(PieItem item) {
    191     // add the item to the pie itself
    192     mItems.add(item);
    193   }
    194 
    195   public void removeItem(PieItem item) {
    196     mItems.remove(item);
    197   }
    198 
    199   public void clearItems() {
    200     mItems.clear();
    201   }
    202 
    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 
    218   public void hide() {
    219     show(false);
    220   }
    221 
    222   /**
    223    * guaranteed has center set
    224    *
    225    * @param show
    226    */
    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 
    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 
    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 
    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 
    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 
    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    */
    337   private float getDegrees(double angle) {
    338     return (float) (360 - 180 * angle / Math.PI);
    339   }
    340 
    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
    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 
    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
    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 
    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    */
    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 
    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 
    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 
    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    */
    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 
    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
    580   public boolean handlesTouch() {
    581     return true;
    582   }
    583 
    584   // focus specific code
    585 
    586   public void setBlockFocus(boolean blocked) {
    587     mBlockFocus = blocked;
    588     if (blocked) {
    589       clear();
    590     }
    591   }
    592 
    593   public void setFocus(int x, int y) {
    594     mFocusX = x;
    595     mFocusY = y;
    596     setCircle(mFocusX, mFocusY);
    597   }
    598 
    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 
    610   public int getSize() {
    611     return 2 * mCircleSize;
    612   }
    613 
    614   private int getRandomRange() {
    615     return (int) (-60 + 120 * Math.random());
    616   }
    617 
    618   @Override
    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 
    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 
    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 
    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 
    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
    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
    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
    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 
    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
    723   public void clear() {
    724     if (mState == STATE_PIE) {
    725       return;
    726     }
    727     cancelFocus();
    728     mOverlay.post(mDisappear);
    729   }
    730 
    731   private void startAnimation(long duration, boolean timeout, float toScale) {
    732     startAnimation(duration, timeout, mDialAngle, toScale);
    733   }
    734 
    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
    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
    755     public void onAnimationRepeat(Animation animation) {}
    756 
    757     @Override
    758     public void onAnimationStart(Animation animation) {}
    759   }
    760 
    761   private class Disappear implements Runnable {
    762     @Override
    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 
    780     public ScaleAnimation() {
    781       setFillAfter(true);
    782     }
    783 
    784     public void setScale(float from, float to) {
    785       mFrom = from;
    786       mTo = to;
    787     }
    788 
    789     @Override
    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 
    800     public LinearAnimation(float from, float to) {
    801       setFillAfter(true);
    802       setInterpolator(new LinearInterpolator());
    803       mFrom = from;
    804       mTo = to;
    805     }
    806 
    807     public float getValue() {
    808       return mValue;
    809     }
    810 
    811     @Override
    812     protected void applyTransformation(float interpolatedTime, Transformation t) {
    813       mValue = (mFrom + (mTo - mFrom) * interpolatedTime);
    814     }
    815   }
    816 }
    817