Home | History | Annotate | Download | only in camerafocus
      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 {
    137         public void onPieOpened(int centerX, int centerY);
    138         public void onPieClosed();
    139     }
    140 
    141     public void setPieListener(PieListener pl) {
    142         mListener = pl;
    143     }
    144 
    145     public PieRenderer(Context context) {
    146         init(context);
    147     }
    148 
    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 
    184     public boolean showsItems() {
    185         return mTapMode;
    186     }
    187 
    188     public void addItem(PieItem item) {
    189         // add the item to the pie itself
    190         mItems.add(item);
    191     }
    192 
    193     public void removeItem(PieItem item) {
    194         mItems.remove(item);
    195     }
    196 
    197     public void clearItems() {
    198         mItems.clear();
    199     }
    200 
    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 
    216     public void hide() {
    217         show(false);
    218     }
    219 
    220     /**
    221      * guaranteed has center set
    222      * @param show
    223      */
    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 
    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 
    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 
    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 
    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 
    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      */
    341     private float getDegrees(double angle) {
    342         return (float) (360 - 180 * angle / Math.PI);
    343     }
    344 
    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
    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 
    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
    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 
    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      */
    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(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 
    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      */
    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 
    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
    581     public boolean handlesTouch() {
    582         return true;
    583     }
    584 
    585     // focus specific code
    586 
    587     public void setBlockFocus(boolean blocked) {
    588         mBlockFocus = blocked;
    589         if (blocked) {
    590             clear();
    591         }
    592     }
    593 
    594     public void setFocus(int x, int y) {
    595         mFocusX = x;
    596         mFocusY = y;
    597         setCircle(mFocusX, mFocusY);
    598     }
    599 
    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 
    611     public int getSize() {
    612         return 2 * mCircleSize;
    613     }
    614 
    615     private int getRandomRange() {
    616         return (int) (-60 + 120 * Math.random());
    617     }
    618 
    619     @Override
    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 
    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 
    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 
    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 
    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
    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
    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
    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 
    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
    725     public void clear() {
    726         if (mState == STATE_PIE) {
    727             return;
    728         }
    729         cancelFocus();
    730         mOverlay.post(mDisappear);
    731     }
    732 
    733     private void startAnimation(long duration, boolean timeout,
    734                                 float toScale) {
    735         startAnimation(duration, timeout, mDialAngle,
    736                 toScale);
    737     }
    738 
    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
    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
    760         public void onAnimationRepeat(Animation animation) {
    761         }
    762 
    763         @Override
    764         public void onAnimationStart(Animation animation) {
    765         }
    766     }
    767 
    768     private class Disappear implements Runnable {
    769         @Override
    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 
    787         public ScaleAnimation() {
    788             setFillAfter(true);
    789         }
    790 
    791         public void setScale(float from, float to) {
    792             mFrom = from;
    793             mTo = to;
    794         }
    795 
    796         @Override
    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 
    808         public LinearAnimation(float from, float to) {
    809             setFillAfter(true);
    810             setInterpolator(new LinearInterpolator());
    811             mFrom = from;
    812             mTo = to;
    813         }
    814 
    815         public float getValue() {
    816             return mValue;
    817         }
    818 
    819         @Override
    820         protected void applyTransformation(float interpolatedTime, Transformation t) {
    821             mValue = (mFrom + (mTo - mFrom) * interpolatedTime);
    822         }
    823     }
    824 
    825 }