Home | History | Annotate | Download | only in ui
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.camera.ui;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.content.Context;
     22 import android.content.res.Resources;
     23 import android.graphics.Canvas;
     24 import android.graphics.Color;
     25 import android.graphics.Paint;
     26 import android.graphics.Path;
     27 import android.graphics.Point;
     28 import android.graphics.PointF;
     29 import android.graphics.RectF;
     30 import android.os.Handler;
     31 import android.os.Message;
     32 import android.view.MotionEvent;
     33 import android.view.ViewConfiguration;
     34 import android.view.animation.Animation;
     35 import android.view.animation.Animation.AnimationListener;
     36 import android.view.animation.LinearInterpolator;
     37 import android.view.animation.Transformation;
     38 
     39 import com.android.camera.R;
     40 import com.android.gallery3d.common.ApiHelper;
     41 
     42 import java.util.ArrayList;
     43 import java.util.List;
     44 
     45 public class PieRenderer extends OverlayRenderer
     46         implements FocusIndicator {
     47 
     48     private static final String TAG = "CAM Pie";
     49 
     50     // Sometimes continuous autofocus starts and stops several times quickly.
     51     // These states are used to make sure the animation is run for at least some
     52     // time.
     53     private volatile int mState;
     54     private ScaleAnimation mAnimation = new ScaleAnimation();
     55     private static final int STATE_IDLE = 0;
     56     private static final int STATE_FOCUSING = 1;
     57     private static final int STATE_FINISHING = 2;
     58     private static final int STATE_PIE = 8;
     59 
     60     private Runnable mDisappear = new Disappear();
     61     private Animation.AnimationListener mEndAction = new EndAction();
     62     private static final int SCALING_UP_TIME = 600;
     63     private static final int SCALING_DOWN_TIME = 100;
     64     private static final int DISAPPEAR_TIMEOUT = 200;
     65     private static final int DIAL_HORIZONTAL = 157;
     66 
     67     private static final long PIE_FADE_IN_DURATION = 200;
     68     private static final long PIE_XFADE_DURATION = 200;
     69     private static final long PIE_SELECT_FADE_DURATION = 300;
     70 
     71     private static final int MSG_OPEN = 0;
     72     private static final int MSG_CLOSE = 1;
     73     private static final float PIE_SWEEP = (float)(Math.PI * 2 / 3);
     74     // geometry
     75     private Point mCenter;
     76     private int mRadius;
     77     private int mRadiusInc;
     78 
     79     // the detection if touch is inside a slice is offset
     80     // inbounds by this amount to allow the selection to show before the
     81     // finger covers it
     82     private int mTouchOffset;
     83 
     84     private List<PieItem> mItems;
     85 
     86     private PieItem mOpenItem;
     87 
     88     private Paint mSelectedPaint;
     89     private Paint mSubPaint;
     90 
     91     // touch handling
     92     private PieItem mCurrentItem;
     93 
     94     private Paint mFocusPaint;
     95     private int mSuccessColor;
     96     private int mFailColor;
     97     private int mCircleSize;
     98     private int mFocusX;
     99     private int mFocusY;
    100     private int mCenterX;
    101     private int mCenterY;
    102 
    103     private int mDialAngle;
    104     private RectF mCircle;
    105     private RectF mDial;
    106     private Point mPoint1;
    107     private Point mPoint2;
    108     private int mStartAnimationAngle;
    109     private boolean mFocused;
    110     private int mInnerOffset;
    111     private int mOuterStroke;
    112     private int mInnerStroke;
    113     private boolean mTapMode;
    114     private boolean mBlockFocus;
    115     private int mTouchSlopSquared;
    116     private Point mDown;
    117     private boolean mOpening;
    118     private LinearAnimation mXFade;
    119     private LinearAnimation mFadeIn;
    120     private volatile boolean mFocusCancelled;
    121 
    122     private Handler mHandler = new Handler() {
    123         public void handleMessage(Message msg) {
    124             switch(msg.what) {
    125             case MSG_OPEN:
    126                 if (mListener != null) {
    127                     mListener.onPieOpened(mCenter.x, mCenter.y);
    128                 }
    129                 break;
    130             case MSG_CLOSE:
    131                 if (mListener != null) {
    132                     mListener.onPieClosed();
    133                 }
    134                 break;
    135             }
    136         }
    137     };
    138 
    139     private PieListener mListener;
    140 
    141     static public interface PieListener {
    142         public void onPieOpened(int centerX, int centerY);
    143         public void onPieClosed();
    144     }
    145 
    146     public void setPieListener(PieListener pl) {
    147         mListener = pl;
    148     }
    149 
    150     public PieRenderer(Context context) {
    151         init(context);
    152     }
    153 
    154     private void init(Context ctx) {
    155         setVisible(false);
    156         mItems = new ArrayList<PieItem>();
    157         Resources res = ctx.getResources();
    158         mRadius = (int) res.getDimensionPixelSize(R.dimen.pie_radius_start);
    159         mCircleSize = mRadius - res.getDimensionPixelSize(R.dimen.focus_radius_offset);
    160         mRadiusInc =  (int) res.getDimensionPixelSize(R.dimen.pie_radius_increment);
    161         mTouchOffset = (int) res.getDimensionPixelSize(R.dimen.pie_touch_offset);
    162         mCenter = new Point(0,0);
    163         mSelectedPaint = new Paint();
    164         mSelectedPaint.setColor(Color.argb(255, 51, 181, 229));
    165         mSelectedPaint.setAntiAlias(true);
    166         mSubPaint = new Paint();
    167         mSubPaint.setAntiAlias(true);
    168         mSubPaint.setColor(Color.argb(200, 250, 230, 128));
    169         mFocusPaint = new Paint();
    170         mFocusPaint.setAntiAlias(true);
    171         mFocusPaint.setColor(Color.WHITE);
    172         mFocusPaint.setStyle(Paint.Style.STROKE);
    173         mSuccessColor = Color.GREEN;
    174         mFailColor = Color.RED;
    175         mCircle = new RectF();
    176         mDial = new RectF();
    177         mPoint1 = new Point();
    178         mPoint2 = new Point();
    179         mInnerOffset = res.getDimensionPixelSize(R.dimen.focus_inner_offset);
    180         mOuterStroke = res.getDimensionPixelSize(R.dimen.focus_outer_stroke);
    181         mInnerStroke = res.getDimensionPixelSize(R.dimen.focus_inner_stroke);
    182         mState = STATE_IDLE;
    183         mBlockFocus = false;
    184         mTouchSlopSquared = ViewConfiguration.get(ctx).getScaledTouchSlop();
    185         mTouchSlopSquared = mTouchSlopSquared * mTouchSlopSquared;
    186         mDown = new Point();
    187     }
    188 
    189     public boolean showsItems() {
    190         return mTapMode;
    191     }
    192 
    193     public void addItem(PieItem item) {
    194         // add the item to the pie itself
    195         mItems.add(item);
    196     }
    197 
    198     public void removeItem(PieItem item) {
    199         mItems.remove(item);
    200     }
    201 
    202     public void clearItems() {
    203         mItems.clear();
    204     }
    205 
    206     public void showInCenter() {
    207         if ((mState == STATE_PIE) && isVisible()) {
    208             mTapMode = false;
    209             show(false);
    210         } else {
    211             if (mState != STATE_IDLE) {
    212                 cancelFocus();
    213             }
    214             mState = STATE_PIE;
    215             setCenter(mCenterX, mCenterY);
    216             mTapMode = true;
    217             show(true);
    218         }
    219     }
    220 
    221     public void hide() {
    222         show(false);
    223     }
    224 
    225     /**
    226      * guaranteed has center set
    227      * @param show
    228      */
    229     private void show(boolean show) {
    230         if (show) {
    231             mState = STATE_PIE;
    232             // ensure clean state
    233             mCurrentItem = null;
    234             mOpenItem = null;
    235             for (PieItem item : mItems) {
    236                 item.setSelected(false);
    237             }
    238             layoutPie();
    239             fadeIn();
    240         } else {
    241             mState = STATE_IDLE;
    242             mTapMode = false;
    243             if (mXFade != null) {
    244                 mXFade.cancel();
    245             }
    246         }
    247         setVisible(show);
    248         mHandler.sendEmptyMessage(show ? MSG_OPEN : MSG_CLOSE);
    249     }
    250 
    251     private void fadeIn() {
    252         mFadeIn = new LinearAnimation(0, 1);
    253         mFadeIn.setDuration(PIE_FADE_IN_DURATION);
    254         mFadeIn.setAnimationListener(new AnimationListener() {
    255             @Override
    256             public void onAnimationStart(Animation animation) {
    257             }
    258 
    259             @Override
    260             public void onAnimationEnd(Animation animation) {
    261                 mFadeIn = null;
    262             }
    263 
    264             @Override
    265             public void onAnimationRepeat(Animation animation) {
    266             }
    267         });
    268         mFadeIn.startNow();
    269         mOverlay.startAnimation(mFadeIn);
    270     }
    271 
    272     public void setCenter(int x, int y) {
    273         mCenter.x = x;
    274         mCenter.y = y;
    275         // when using the pie menu, align the focus ring
    276         alignFocus(x, y);
    277     }
    278 
    279     private void layoutPie() {
    280         int rgap = 2;
    281         int inner = mRadius + rgap;
    282         int outer = mRadius + mRadiusInc - rgap;
    283         int gap = 1;
    284         layoutItems(mItems, (float) (Math.PI / 2), inner, outer, gap);
    285     }
    286 
    287     private void layoutItems(List<PieItem> items, float centerAngle, int inner,
    288             int outer, int gap) {
    289         float emptyangle = PIE_SWEEP / 16;
    290         float sweep = (float) (PIE_SWEEP - 2 * emptyangle) / items.size();
    291         float angle = centerAngle - PIE_SWEEP / 2 + emptyangle + sweep / 2;
    292         // check if we have custom geometry
    293         // first item we find triggers custom sweep for all
    294         // this allows us to re-use the path
    295         for (PieItem item : items) {
    296             if (item.getCenter() >= 0) {
    297                 sweep = item.getSweep();
    298                 break;
    299             }
    300         }
    301         Path path = makeSlice(getDegrees(0) - gap, getDegrees(sweep) + gap,
    302                 outer, inner, mCenter);
    303         for (PieItem item : items) {
    304             // shared between items
    305             item.setPath(path);
    306             if (item.getCenter() >= 0) {
    307                 angle = item.getCenter();
    308             }
    309             int w = item.getIntrinsicWidth();
    310             int h = item.getIntrinsicHeight();
    311             // move views to outer border
    312             int r = inner + (outer - inner) * 2 / 3;
    313             int x = (int) (r * Math.cos(angle));
    314             int y = mCenter.y - (int) (r * Math.sin(angle)) - h / 2;
    315             x = mCenter.x + x - w / 2;
    316             item.setBounds(x, y, x + w, y + h);
    317             float itemstart = angle - sweep / 2;
    318             item.setGeometry(itemstart, sweep, inner, outer);
    319             if (item.hasItems()) {
    320                 layoutItems(item.getItems(), angle, inner,
    321                         outer + mRadiusInc / 2, gap);
    322             }
    323             angle += sweep;
    324         }
    325     }
    326 
    327     private Path makeSlice(float start, float end, int outer, int inner, Point center) {
    328         RectF bb =
    329                 new RectF(center.x - outer, center.y - outer, center.x + outer,
    330                         center.y + outer);
    331         RectF bbi =
    332                 new RectF(center.x - inner, center.y - inner, center.x + inner,
    333                         center.y + inner);
    334         Path path = new Path();
    335         path.arcTo(bb, start, end - start, true);
    336         path.arcTo(bbi, end, start - end);
    337         path.close();
    338         return path;
    339     }
    340 
    341     /**
    342      * converts a
    343      * @param angle from 0..PI to Android degrees (clockwise starting at 3 o'clock)
    344      * @return skia angle
    345      */
    346     private float getDegrees(double angle) {
    347         return (float) (360 - 180 * angle / Math.PI);
    348     }
    349 
    350     private void startFadeOut() {
    351         if (ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) {
    352             mOverlay.animate().alpha(0).setListener(new AnimatorListenerAdapter() {
    353                 @Override
    354                 public void onAnimationEnd(Animator animation) {
    355                     deselect();
    356                     show(false);
    357                     mOverlay.setAlpha(1);
    358                     super.onAnimationEnd(animation);
    359                 }
    360             }).setDuration(PIE_SELECT_FADE_DURATION);
    361         } else {
    362             deselect();
    363             show(false);
    364         }
    365     }
    366 
    367     @Override
    368     public void onDraw(Canvas canvas) {
    369         float alpha = 1;
    370         if (mXFade != null) {
    371             alpha = mXFade.getValue();
    372         } else if (mFadeIn != null) {
    373             alpha = mFadeIn.getValue();
    374         }
    375         int state = canvas.save();
    376         if (mFadeIn != null) {
    377             float sf = 0.9f + alpha * 0.1f;
    378             canvas.scale(sf, sf, mCenter.x, mCenter.y);
    379         }
    380         drawFocus(canvas);
    381         if (mState == STATE_FINISHING) {
    382             canvas.restoreToCount(state);
    383             return;
    384         }
    385         if ((mOpenItem == null) || (mXFade != null)) {
    386             // draw base menu
    387             for (PieItem item : mItems) {
    388                 drawItem(canvas, item, alpha);
    389             }
    390         }
    391         if (mOpenItem != null) {
    392             for (PieItem inner : mOpenItem.getItems()) {
    393                 drawItem(canvas, inner, (mXFade != null) ? (1 - 0.5f * alpha) : 1);
    394             }
    395         }
    396         canvas.restoreToCount(state);
    397     }
    398 
    399     private void drawItem(Canvas canvas, PieItem item, float alpha) {
    400         if (mState == STATE_PIE) {
    401             if (item.getPath() != null) {
    402                 if (item.isSelected()) {
    403                     Paint p = mSelectedPaint;
    404                     int state = canvas.save();
    405                     float r = getDegrees(item.getStartAngle());
    406                     canvas.rotate(r, mCenter.x, mCenter.y);
    407                     canvas.drawPath(item.getPath(), p);
    408                     canvas.restoreToCount(state);
    409                 }
    410                 alpha = alpha * (item.isEnabled() ? 1 : 0.3f);
    411                 // draw the item view
    412                 item.setAlpha(alpha);
    413                 item.draw(canvas);
    414             }
    415         }
    416     }
    417 
    418     @Override
    419     public boolean onTouchEvent(MotionEvent evt) {
    420         float x = evt.getX();
    421         float y = evt.getY();
    422         int action = evt.getActionMasked();
    423         PointF polar = getPolar(x, y, !(mTapMode));
    424         if (MotionEvent.ACTION_DOWN == action) {
    425             mDown.x = (int) evt.getX();
    426             mDown.y = (int) evt.getY();
    427             mOpening = false;
    428             if (mTapMode) {
    429                 PieItem item = findItem(polar);
    430                 if ((item != null) && (mCurrentItem != item)) {
    431                     mState = STATE_PIE;
    432                     onEnter(item);
    433                 }
    434             } else {
    435                 setCenter((int) x, (int) y);
    436                 show(true);
    437             }
    438             return true;
    439         } else if (MotionEvent.ACTION_UP == action) {
    440             if (isVisible()) {
    441                 PieItem item = mCurrentItem;
    442                 if (mTapMode) {
    443                     item = findItem(polar);
    444                     if (item != null && mOpening) {
    445                         mOpening = false;
    446                         return true;
    447                     }
    448                 }
    449                 if (item == null) {
    450                     mTapMode = false;
    451                     show(false);
    452                 } else if (!mOpening
    453                         && !item.hasItems()) {
    454                     item.performClick();
    455                     startFadeOut();
    456                     mTapMode = false;
    457                 }
    458                 return true;
    459             }
    460         } else if (MotionEvent.ACTION_CANCEL == action) {
    461             if (isVisible() || mTapMode) {
    462                 show(false);
    463             }
    464             deselect();
    465             return false;
    466         } else if (MotionEvent.ACTION_MOVE == action) {
    467             if (polar.y < mRadius) {
    468                 if (mOpenItem != null) {
    469                     mOpenItem = null;
    470                 } else {
    471                     deselect();
    472                 }
    473                 return false;
    474             }
    475             PieItem item = findItem(polar);
    476             boolean moved = hasMoved(evt);
    477             if ((item != null) && (mCurrentItem != item) && (!mOpening || moved)) {
    478                 // only select if we didn't just open or have moved past slop
    479                 mOpening = false;
    480                 if (moved) {
    481                     // switch back to swipe mode
    482                     mTapMode = false;
    483                 }
    484                 onEnter(item);
    485             }
    486         }
    487         return false;
    488     }
    489 
    490     private boolean hasMoved(MotionEvent e) {
    491         return mTouchSlopSquared < (e.getX() - mDown.x) * (e.getX() - mDown.x)
    492                 + (e.getY() - mDown.y) * (e.getY() - mDown.y);
    493     }
    494 
    495     /**
    496      * enter a slice for a view
    497      * updates model only
    498      * @param item
    499      */
    500     private void onEnter(PieItem item) {
    501         if (mCurrentItem != null) {
    502             mCurrentItem.setSelected(false);
    503         }
    504         if (item != null && item.isEnabled()) {
    505             item.setSelected(true);
    506             mCurrentItem = item;
    507             if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) {
    508                 openCurrentItem();
    509             }
    510         } else {
    511             mCurrentItem = null;
    512         }
    513     }
    514 
    515     private void deselect() {
    516         if (mCurrentItem != null) {
    517             mCurrentItem.setSelected(false);
    518         }
    519         if (mOpenItem != null) {
    520             mOpenItem = null;
    521         }
    522         mCurrentItem = null;
    523     }
    524 
    525     private void openCurrentItem() {
    526         if ((mCurrentItem != null) && mCurrentItem.hasItems()) {
    527             mCurrentItem.setSelected(false);
    528             mOpenItem = mCurrentItem;
    529             mOpening = true;
    530             mXFade = new LinearAnimation(1, 0);
    531             mXFade.setDuration(PIE_XFADE_DURATION);
    532             mXFade.setAnimationListener(new AnimationListener() {
    533                 @Override
    534                 public void onAnimationStart(Animation animation) {
    535                 }
    536 
    537                 @Override
    538                 public void onAnimationEnd(Animation animation) {
    539                     mXFade = null;
    540                 }
    541 
    542                 @Override
    543                 public void onAnimationRepeat(Animation animation) {
    544                 }
    545             });
    546             mXFade.startNow();
    547             mOverlay.startAnimation(mXFade);
    548         }
    549     }
    550 
    551     private PointF getPolar(float x, float y, boolean useOffset) {
    552         PointF res = new PointF();
    553         // get angle and radius from x/y
    554         res.x = (float) Math.PI / 2;
    555         x = x - mCenter.x;
    556         y = mCenter.y - y;
    557         res.y = (float) Math.sqrt(x * x + y * y);
    558         if (x != 0) {
    559             res.x = (float) Math.atan2(y,  x);
    560             if (res.x < 0) {
    561                 res.x = (float) (2 * Math.PI + res.x);
    562             }
    563         }
    564         res.y = res.y + (useOffset ? mTouchOffset : 0);
    565         return res;
    566     }
    567 
    568     /**
    569      * @param polar x: angle, y: dist
    570      * @return the item at angle/dist or null
    571      */
    572     private PieItem findItem(PointF polar) {
    573         // find the matching item:
    574         List<PieItem> items = (mOpenItem != null) ? mOpenItem.getItems() : mItems;
    575         for (PieItem item : items) {
    576             if (inside(polar, item)) {
    577                 return item;
    578             }
    579         }
    580         return null;
    581     }
    582 
    583     private boolean inside(PointF polar, PieItem item) {
    584         return (item.getInnerRadius() < polar.y)
    585                 && (item.getStartAngle() < polar.x)
    586                 && (item.getStartAngle() + item.getSweep() > polar.x)
    587                 && (!mTapMode || (item.getOuterRadius() > polar.y));
    588     }
    589 
    590     @Override
    591     public boolean handlesTouch() {
    592         return true;
    593     }
    594 
    595     // focus specific code
    596 
    597     public void setBlockFocus(boolean blocked) {
    598         mBlockFocus = blocked;
    599         if (blocked) {
    600             clear();
    601         }
    602     }
    603 
    604     public void setFocus(int x, int y) {
    605         mFocusX = x;
    606         mFocusY = y;
    607         setCircle(mFocusX, mFocusY);
    608     }
    609 
    610     public void alignFocus(int x, int y) {
    611         mOverlay.removeCallbacks(mDisappear);
    612         mAnimation.cancel();
    613         mAnimation.reset();
    614         mFocusX = x;
    615         mFocusY = y;
    616         mDialAngle = DIAL_HORIZONTAL;
    617         setCircle(x, y);
    618         mFocused = false;
    619     }
    620 
    621     public int getSize() {
    622         return 2 * mCircleSize;
    623     }
    624 
    625     private int getRandomRange() {
    626         return (int)(-60 + 120 * Math.random());
    627     }
    628 
    629     @Override
    630     public void layout(int l, int t, int r, int b) {
    631         super.layout(l, t, r, b);
    632         mCenterX = (r - l) / 2;
    633         mCenterY = (b - t) / 2;
    634         mFocusX = mCenterX;
    635         mFocusY = mCenterY;
    636         setCircle(mFocusX, mFocusY);
    637         if (isVisible() && mState == STATE_PIE) {
    638             setCenter(mCenterX, mCenterY);
    639             layoutPie();
    640         }
    641     }
    642 
    643     private void setCircle(int cx, int cy) {
    644         mCircle.set(cx - mCircleSize, cy - mCircleSize,
    645                 cx + mCircleSize, cy + mCircleSize);
    646         mDial.set(cx - mCircleSize + mInnerOffset, cy - mCircleSize + mInnerOffset,
    647                 cx + mCircleSize - mInnerOffset, cy + mCircleSize - mInnerOffset);
    648     }
    649 
    650     public void drawFocus(Canvas canvas) {
    651         if (mBlockFocus) return;
    652         mFocusPaint.setStrokeWidth(mOuterStroke);
    653         canvas.drawCircle((float) mFocusX, (float) mFocusY, (float) mCircleSize, mFocusPaint);
    654         if (mState == STATE_PIE) return;
    655         int color = mFocusPaint.getColor();
    656         if (mState == STATE_FINISHING) {
    657             mFocusPaint.setColor(mFocused ? mSuccessColor : mFailColor);
    658         }
    659         mFocusPaint.setStrokeWidth(mInnerStroke);
    660         drawLine(canvas, mDialAngle, mFocusPaint);
    661         drawLine(canvas, mDialAngle + 45, mFocusPaint);
    662         drawLine(canvas, mDialAngle + 180, mFocusPaint);
    663         drawLine(canvas, mDialAngle + 225, mFocusPaint);
    664         canvas.save();
    665         // rotate the arc instead of its offset to better use framework's shape caching
    666         canvas.rotate(mDialAngle, mFocusX, mFocusY);
    667         canvas.drawArc(mDial, 0, 45, false, mFocusPaint);
    668         canvas.drawArc(mDial, 180, 45, false, mFocusPaint);
    669         canvas.restore();
    670         mFocusPaint.setColor(color);
    671     }
    672 
    673     private void drawLine(Canvas canvas, int angle, Paint p) {
    674         convertCart(angle, mCircleSize - mInnerOffset, mPoint1);
    675         convertCart(angle, mCircleSize - mInnerOffset + mInnerOffset / 3, mPoint2);
    676         canvas.drawLine(mPoint1.x + mFocusX, mPoint1.y + mFocusY,
    677                 mPoint2.x + mFocusX, mPoint2.y + mFocusY, p);
    678     }
    679 
    680     private static void convertCart(int angle, int radius, Point out) {
    681         double a = 2 * Math.PI * (angle % 360) / 360;
    682         out.x = (int) (radius * Math.cos(a) + 0.5);
    683         out.y = (int) (radius * Math.sin(a) + 0.5);
    684     }
    685 
    686     @Override
    687     public void showStart() {
    688         if (mState == STATE_PIE) return;
    689         cancelFocus();
    690         mStartAnimationAngle = 67;
    691         int range = getRandomRange();
    692         startAnimation(SCALING_UP_TIME,
    693                 false, mStartAnimationAngle, mStartAnimationAngle + range);
    694         mState = STATE_FOCUSING;
    695     }
    696 
    697     @Override
    698     public void showSuccess(boolean timeout) {
    699         if (mState == STATE_FOCUSING) {
    700             startAnimation(SCALING_DOWN_TIME,
    701                     timeout, mStartAnimationAngle);
    702             mState = STATE_FINISHING;
    703             mFocused = true;
    704         }
    705     }
    706 
    707     @Override
    708     public void showFail(boolean timeout) {
    709         if (mState == STATE_FOCUSING) {
    710             startAnimation(SCALING_DOWN_TIME,
    711                     timeout, mStartAnimationAngle);
    712             mState = STATE_FINISHING;
    713             mFocused = false;
    714         }
    715     }
    716 
    717     private void cancelFocus() {
    718         mFocusCancelled = true;
    719         mOverlay.removeCallbacks(mDisappear);
    720         if (mAnimation != null) {
    721             mAnimation.cancel();
    722         }
    723         mFocusCancelled = false;
    724         mFocused = false;
    725         mState = STATE_IDLE;
    726     }
    727 
    728     @Override
    729     public void clear() {
    730         if (mState == STATE_PIE) return;
    731         cancelFocus();
    732         mOverlay.post(mDisappear);
    733     }
    734 
    735     private void startAnimation(long duration, boolean timeout,
    736             float toScale) {
    737         startAnimation(duration, timeout, mDialAngle,
    738                 toScale);
    739     }
    740 
    741     private void startAnimation(long duration, boolean timeout,
    742             float fromScale, float toScale) {
    743         setVisible(true);
    744         mAnimation.reset();
    745         mAnimation.setDuration(duration);
    746         mAnimation.setScale(fromScale, toScale);
    747         mAnimation.setAnimationListener(timeout ? mEndAction : null);
    748         mOverlay.startAnimation(mAnimation);
    749         update();
    750     }
    751 
    752     private class EndAction implements Animation.AnimationListener {
    753         @Override
    754         public void onAnimationEnd(Animation animation) {
    755             // Keep the focus indicator for some time.
    756             if (!mFocusCancelled) {
    757                 mOverlay.postDelayed(mDisappear, DISAPPEAR_TIMEOUT);
    758             }
    759         }
    760 
    761         @Override
    762         public void onAnimationRepeat(Animation animation) {
    763         }
    764 
    765         @Override
    766         public void onAnimationStart(Animation animation) {
    767         }
    768     }
    769 
    770     private class Disappear implements Runnable {
    771         @Override
    772         public void run() {
    773             if (mState == STATE_PIE) return;
    774             setVisible(false);
    775             mFocusX = mCenterX;
    776             mFocusY = mCenterY;
    777             mState = STATE_IDLE;
    778             setCircle(mFocusX, mFocusY);
    779             mFocused = false;
    780         }
    781     }
    782 
    783     private class ScaleAnimation extends Animation {
    784         private float mFrom = 1f;
    785         private float mTo = 1f;
    786 
    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 }
    826