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.Animator.AnimatorListener;
     21 import android.animation.ValueAnimator;
     22 import android.content.Context;
     23 import android.content.res.Resources;
     24 import android.graphics.Canvas;
     25 import android.graphics.Color;
     26 import android.graphics.Paint;
     27 import android.graphics.Path;
     28 import android.graphics.Point;
     29 import android.graphics.PointF;
     30 import android.graphics.RectF;
     31 import android.os.Handler;
     32 import android.os.Message;
     33 import android.util.FloatMath;
     34 import android.view.MotionEvent;
     35 import android.view.ViewConfiguration;
     36 import android.view.animation.Animation;
     37 import android.view.animation.Transformation;
     38 
     39 import com.android.camera.drawable.TextDrawable;
     40 import com.android.camera.ui.ProgressRenderer.VisibilityListener;
     41 import com.android.camera2.R;
     42 
     43 import java.util.ArrayList;
     44 import java.util.List;
     45 
     46 /**
     47  * An overlay renderer that is used to display focus state and progress state.
     48  */
     49 public class PieRenderer extends OverlayRenderer
     50         implements FocusIndicator {
     51 
     52     private static final String TAG = "PieRenderer";
     53 
     54     // Sometimes continuous autofocus starts and stops several times quickly.
     55     // These states are used to make sure the animation is run for at least some
     56     // time.
     57     private volatile int mState;
     58     private ScaleAnimation mAnimation = new ScaleAnimation();
     59     private static final int STATE_IDLE = 0;
     60     private static final int STATE_FOCUSING = 1;
     61     private static final int STATE_FINISHING = 2;
     62     private static final int STATE_PIE = 8;
     63 
     64     private static final float MATH_PI_2 = (float)(Math.PI / 2);
     65 
     66     private Runnable mDisappear = new Disappear();
     67     private Animation.AnimationListener mEndAction = new EndAction();
     68     private static final int SCALING_UP_TIME = 600;
     69     private static final int SCALING_DOWN_TIME = 100;
     70     private static final int DISAPPEAR_TIMEOUT = 200;
     71     private static final int DIAL_HORIZONTAL = 157;
     72     // fade out timings
     73     private static final int PIE_FADE_OUT_DURATION = 600;
     74 
     75     private static final long PIE_FADE_IN_DURATION = 200;
     76     private static final long PIE_XFADE_DURATION = 200;
     77     private static final long PIE_SELECT_FADE_DURATION = 300;
     78     private static final long PIE_OPEN_SUB_DELAY = 400;
     79     private static final long PIE_SLICE_DURATION = 80;
     80 
     81     private static final int MSG_OPEN = 0;
     82     private static final int MSG_CLOSE = 1;
     83     private static final int MSG_OPENSUBMENU = 2;
     84 
     85     protected static float CENTER = (float) Math.PI / 2;
     86     protected static float RAD24 = (float)(24 * Math.PI / 180);
     87     protected static final float SWEEP_SLICE = 0.14f;
     88     protected static final float SWEEP_ARC = 0.23f;
     89 
     90     // geometry
     91     private int mRadius;
     92     private int mRadiusInc;
     93 
     94     // the detection if touch is inside a slice is offset
     95     // inbounds by this amount to allow the selection to show before the
     96     // finger covers it
     97     private int mTouchOffset;
     98 
     99     private List<PieItem> mOpen;
    100 
    101     private Paint mSelectedPaint;
    102     private Paint mSubPaint;
    103     private Paint mMenuArcPaint;
    104 
    105     // touch handling
    106     private PieItem mCurrentItem;
    107 
    108     private Paint mFocusPaint;
    109     private int mSuccessColor;
    110     private int mFailColor;
    111     private int mCircleSize;
    112     private int mFocusX;
    113     private int mFocusY;
    114     private int mCenterX;
    115     private int mCenterY;
    116     private int mArcCenterY;
    117     private int mSliceCenterY;
    118     private int mPieCenterX;
    119     private int mPieCenterY;
    120     private int mSliceRadius;
    121     private int mArcRadius;
    122     private int mArcOffset;
    123 
    124     private int mDialAngle;
    125     private RectF mCircle;
    126     private RectF mDial;
    127     private Point mPoint1;
    128     private Point mPoint2;
    129     private int mStartAnimationAngle;
    130     private boolean mFocused;
    131     private int mInnerOffset;
    132     private int mOuterStroke;
    133     private int mInnerStroke;
    134     private boolean mTapMode;
    135     private boolean mBlockFocus;
    136     private int mTouchSlopSquared;
    137     private Point mDown;
    138     private boolean mOpening;
    139     private ValueAnimator mXFade;
    140     private ValueAnimator mFadeIn;
    141     private ValueAnimator mFadeOut;
    142     private ValueAnimator mSlice;
    143     private volatile boolean mFocusCancelled;
    144     private PointF mPolar = new PointF();
    145     private TextDrawable mLabel;
    146     private int mDeadZone;
    147     private int mAngleZone;
    148     private float mCenterAngle;
    149 
    150     private ProgressRenderer mProgressRenderer;
    151 
    152     private Handler mHandler = new Handler() {
    153         public void handleMessage(Message msg) {
    154             switch(msg.what) {
    155             case MSG_OPEN:
    156                 if (mListener != null) {
    157                     mListener.onPieOpened(mPieCenterX, mPieCenterY);
    158                 }
    159                 break;
    160             case MSG_CLOSE:
    161                 if (mListener != null) {
    162                     mListener.onPieClosed();
    163                 }
    164                 break;
    165             case MSG_OPENSUBMENU:
    166                 onEnterOpen();
    167                 break;
    168             }
    169 
    170         }
    171     };
    172 
    173     private PieListener mListener;
    174 
    175     static public interface PieListener {
    176         public void onPieOpened(int centerX, int centerY);
    177         public void onPieClosed();
    178     }
    179 
    180     public void setPieListener(PieListener pl) {
    181         mListener = pl;
    182     }
    183 
    184     public PieRenderer(Context context) {
    185         init(context);
    186     }
    187 
    188     private void init(Context ctx) {
    189         setVisible(false);
    190         mOpen = new ArrayList<PieItem>();
    191         mOpen.add(new PieItem(null, 0));
    192         Resources res = ctx.getResources();
    193         mRadius = (int) res.getDimensionPixelSize(R.dimen.pie_radius_start);
    194         mRadiusInc = (int) res.getDimensionPixelSize(R.dimen.pie_radius_increment);
    195         mCircleSize = mRadius - res.getDimensionPixelSize(R.dimen.focus_radius_offset);
    196         mTouchOffset = (int) res.getDimensionPixelSize(R.dimen.pie_touch_offset);
    197         mSelectedPaint = new Paint();
    198         mSelectedPaint.setColor(Color.argb(255, 51, 181, 229));
    199         mSelectedPaint.setAntiAlias(true);
    200         mSubPaint = new Paint();
    201         mSubPaint.setAntiAlias(true);
    202         mSubPaint.setColor(Color.argb(200, 250, 230, 128));
    203         mFocusPaint = new Paint();
    204         mFocusPaint.setAntiAlias(true);
    205         mFocusPaint.setColor(Color.WHITE);
    206         mFocusPaint.setStyle(Paint.Style.STROKE);
    207         mSuccessColor = Color.GREEN;
    208         mFailColor = Color.RED;
    209         mCircle = new RectF();
    210         mDial = new RectF();
    211         mPoint1 = new Point();
    212         mPoint2 = new Point();
    213         mInnerOffset = res.getDimensionPixelSize(R.dimen.focus_inner_offset);
    214         mOuterStroke = res.getDimensionPixelSize(R.dimen.focus_outer_stroke);
    215         mInnerStroke = res.getDimensionPixelSize(R.dimen.focus_inner_stroke);
    216         mState = STATE_IDLE;
    217         mBlockFocus = false;
    218         mTouchSlopSquared = ViewConfiguration.get(ctx).getScaledTouchSlop();
    219         mTouchSlopSquared = mTouchSlopSquared * mTouchSlopSquared;
    220         mDown = new Point();
    221         mMenuArcPaint = new Paint();
    222         mMenuArcPaint.setAntiAlias(true);
    223         mMenuArcPaint.setColor(Color.argb(140, 255, 255, 255));
    224         mMenuArcPaint.setStrokeWidth(10);
    225         mMenuArcPaint.setStyle(Paint.Style.STROKE);
    226         mSliceRadius = res.getDimensionPixelSize(R.dimen.pie_item_radius);
    227         mArcRadius = res.getDimensionPixelSize(R.dimen.pie_arc_radius);
    228         mArcOffset = res.getDimensionPixelSize(R.dimen.pie_arc_offset);
    229         mLabel = new TextDrawable(res);
    230         mLabel.setDropShadow(true);
    231         mDeadZone = res.getDimensionPixelSize(R.dimen.pie_deadzone_width);
    232         mAngleZone = res.getDimensionPixelSize(R.dimen.pie_anglezone_width);
    233         mProgressRenderer = new ProgressRenderer(ctx);
    234     }
    235 
    236     private PieItem getRoot() {
    237         return mOpen.get(0);
    238     }
    239 
    240     public boolean showsItems() {
    241         return mTapMode;
    242     }
    243 
    244     public void addItem(PieItem item) {
    245         // add the item to the pie itself
    246         getRoot().addItem(item);
    247     }
    248 
    249     public void clearItems() {
    250         getRoot().clearItems();
    251     }
    252 
    253     public void showInCenter() {
    254         if ((mState == STATE_PIE) && isVisible()) {
    255             mTapMode = false;
    256             show(false);
    257         } else {
    258             if (mState != STATE_IDLE) {
    259                 cancelFocus();
    260             }
    261             mState = STATE_PIE;
    262             resetPieCenter();
    263             setCenter(mPieCenterX, mPieCenterY);
    264             mTapMode = true;
    265             show(true);
    266         }
    267     }
    268 
    269     public void hide() {
    270         show(false);
    271     }
    272 
    273     /**
    274      * guaranteed has center set
    275      * @param show
    276      */
    277     private void show(boolean show) {
    278         if (show) {
    279             if (mXFade != null) {
    280                 mXFade.cancel();
    281             }
    282             mState = STATE_PIE;
    283             // ensure clean state
    284             mCurrentItem = null;
    285             PieItem root = getRoot();
    286             for (PieItem openItem : mOpen) {
    287                 if (openItem.hasItems()) {
    288                     for (PieItem item : openItem.getItems()) {
    289                         item.setSelected(false);
    290                     }
    291                 }
    292             }
    293             mLabel.setText("");
    294             mOpen.clear();
    295             mOpen.add(root);
    296             layoutPie();
    297             fadeIn();
    298         } else {
    299             mState = STATE_IDLE;
    300             mTapMode = false;
    301             if (mXFade != null) {
    302                 mXFade.cancel();
    303             }
    304             if (mLabel != null) {
    305                 mLabel.setText("");
    306             }
    307         }
    308         setVisible(show);
    309         mHandler.sendEmptyMessage(show ? MSG_OPEN : MSG_CLOSE);
    310     }
    311 
    312     public boolean isOpen() {
    313         return mState == STATE_PIE && isVisible();
    314     }
    315 
    316     public void setProgress(int percent) {
    317         mProgressRenderer.setProgress(percent);
    318     }
    319 
    320     private void fadeIn() {
    321         mFadeIn = new ValueAnimator();
    322         mFadeIn.setFloatValues(0f, 1f);
    323         mFadeIn.setDuration(PIE_FADE_IN_DURATION);
    324         // linear interpolation
    325         mFadeIn.setInterpolator(null);
    326         mFadeIn.addListener(new AnimatorListener() {
    327             @Override
    328             public void onAnimationStart(Animator animation) {
    329             }
    330 
    331             @Override
    332             public void onAnimationEnd(Animator animation) {
    333                 mFadeIn = null;
    334             }
    335 
    336             @Override
    337             public void onAnimationRepeat(Animator animation) {
    338             }
    339 
    340             @Override
    341             public void onAnimationCancel(Animator arg0) {
    342             }
    343         });
    344         mFadeIn.start();
    345     }
    346 
    347     public void setCenter(int x, int y) {
    348         mPieCenterX = x;
    349         mPieCenterY = y;
    350         mSliceCenterY = y + mSliceRadius - mArcOffset;
    351         mArcCenterY = y - mArcOffset + mArcRadius;
    352     }
    353 
    354     @Override
    355     public void layout(int l, int t, int r, int b) {
    356         super.layout(l, t, r, b);
    357         mCenterX = (r - l) / 2;
    358         mCenterY = (b - t) / 2;
    359 
    360         mFocusX = mCenterX;
    361         mFocusY = mCenterY;
    362         resetPieCenter();
    363         setCircle(mFocusX, mFocusY);
    364         if (isVisible() && mState == STATE_PIE) {
    365             setCenter(mPieCenterX, mPieCenterY);
    366             layoutPie();
    367         }
    368     }
    369 
    370     private void resetPieCenter() {
    371         mPieCenterX = mCenterX;
    372         mPieCenterY = (int) (getHeight() - 2.5f * mDeadZone);
    373     }
    374 
    375     private void layoutPie() {
    376         mCenterAngle = getCenterAngle();
    377         layoutItems(0, getRoot().getItems());
    378         layoutLabel(getLevel());
    379     }
    380 
    381     private void layoutLabel(int level) {
    382         int x = mPieCenterX - (int) (FloatMath.sin(mCenterAngle - CENTER)
    383                 * (mArcRadius + (level + 2) * mRadiusInc));
    384         int y = mArcCenterY - mArcRadius - (level + 2) * mRadiusInc;
    385         int w = mLabel.getIntrinsicWidth();
    386         int h = mLabel.getIntrinsicHeight();
    387         mLabel.setBounds(x - w/2, y - h/2, x + w/2, y + h/2);
    388     }
    389 
    390     private void layoutItems(int level, List<PieItem> items) {
    391         int extend = 1;
    392         Path path = makeSlice(getDegrees(0) + extend, getDegrees(SWEEP_ARC) - extend,
    393                 mArcRadius, mArcRadius + mRadiusInc + mRadiusInc / 4,
    394                 mPieCenterX, mArcCenterY - level * mRadiusInc);
    395         final int count = items.size();
    396         int pos = 0;
    397         for (PieItem item : items) {
    398             // shared between items
    399             item.setPath(path);
    400             float angle = getArcCenter(item, pos, count);
    401             int w = item.getIntrinsicWidth();
    402             int h = item.getIntrinsicHeight();
    403             // move views to outer border
    404             int r = mArcRadius + mRadiusInc * 2 / 3;
    405             int x = (int) (r * Math.cos(angle));
    406             int y = mArcCenterY - (level * mRadiusInc) - (int) (r * Math.sin(angle)) - h / 2;
    407             x = mPieCenterX + x - w / 2;
    408             item.setBounds(x, y, x + w, y + h);
    409             item.setLevel(level);
    410             if (item.hasItems()) {
    411                 layoutItems(level + 1, item.getItems());
    412             }
    413             pos++;
    414         }
    415     }
    416 
    417     private Path makeSlice(float start, float end, int inner, int outer, int cx, int cy) {
    418         RectF bb =
    419                 new RectF(cx - outer, cy - outer, cx + outer,
    420                         cy + outer);
    421         RectF bbi =
    422                 new RectF(cx - inner, cy - inner, cx + inner,
    423                         cy + inner);
    424         Path path = new Path();
    425         path.arcTo(bb, start, end - start, true);
    426         path.arcTo(bbi, end, start - end);
    427         path.close();
    428         return path;
    429     }
    430 
    431     private float getArcCenter(PieItem item, int pos, int count) {
    432         return getCenter(pos, count, SWEEP_ARC);
    433     }
    434 
    435     private float getSliceCenter(PieItem item, int pos, int count) {
    436         float center = (getCenterAngle() - CENTER) * 0.5f + CENTER;
    437         return center + (count - 1) * SWEEP_SLICE / 2f
    438                 - pos * SWEEP_SLICE;
    439     }
    440 
    441     private float getCenter(int pos, int count, float sweep) {
    442         return mCenterAngle + (count - 1) * sweep / 2f - pos * sweep;
    443     }
    444 
    445     private float getCenterAngle() {
    446         float center = CENTER;
    447         if (mPieCenterX < mDeadZone + mAngleZone) {
    448             center = CENTER - (mAngleZone - mPieCenterX + mDeadZone) * RAD24
    449                     / (float) mAngleZone;
    450         } else if (mPieCenterX > getWidth() - mDeadZone - mAngleZone) {
    451             center = CENTER + (mPieCenterX - (getWidth() - mDeadZone - mAngleZone)) * RAD24
    452                     / (float) mAngleZone;
    453         }
    454         return center;
    455     }
    456 
    457     /**
    458      * converts a
    459      * @param angle from 0..PI to Android degrees (clockwise starting at 3 o'clock)
    460      * @return skia angle
    461      */
    462     private float getDegrees(double angle) {
    463         return (float) (360 - 180 * angle / Math.PI);
    464     }
    465 
    466     private void startFadeOut(final PieItem item) {
    467         if (mFadeIn != null) {
    468             mFadeIn.cancel();
    469         }
    470         if (mXFade != null) {
    471             mXFade.cancel();
    472         }
    473         mFadeOut = new ValueAnimator();
    474         mFadeOut.setFloatValues(1f, 0f);
    475         mFadeOut.setDuration(PIE_FADE_OUT_DURATION);
    476         mFadeOut.addListener(new AnimatorListener() {
    477             @Override
    478             public void onAnimationStart(Animator animator) {
    479             }
    480 
    481             @Override
    482             public void onAnimationEnd(Animator animator) {
    483                 item.performClick();
    484                 mFadeOut = null;
    485                 deselect();
    486                 show(false);
    487                 mOverlay.setAlpha(1);
    488             }
    489 
    490             @Override
    491             public void onAnimationRepeat(Animator animator) {
    492             }
    493 
    494             @Override
    495             public void onAnimationCancel(Animator animator) {
    496             }
    497 
    498         });
    499         mFadeOut.start();
    500     }
    501 
    502     // root does not count
    503     private boolean hasOpenItem() {
    504         return mOpen.size() > 1;
    505     }
    506 
    507     // pop an item of the open item stack
    508     private PieItem closeOpenItem() {
    509         PieItem item = getOpenItem();
    510         mOpen.remove(mOpen.size() -1);
    511         return item;
    512     }
    513 
    514     private PieItem getOpenItem() {
    515         return mOpen.get(mOpen.size() - 1);
    516     }
    517 
    518     // return the children either the root or parent of the current open item
    519     private PieItem getParent() {
    520         return mOpen.get(Math.max(0, mOpen.size() - 2));
    521     }
    522 
    523     private int getLevel() {
    524         return mOpen.size() - 1;
    525     }
    526 
    527     @Override
    528     public void onDraw(Canvas canvas) {
    529         mProgressRenderer.onDraw(canvas, mFocusX, mFocusY);
    530 
    531         float alpha = 1;
    532         if (mXFade != null) {
    533             alpha = (Float) mXFade.getAnimatedValue();
    534         } else if (mFadeIn != null) {
    535             alpha = (Float) mFadeIn.getAnimatedValue();
    536         } else if (mFadeOut != null) {
    537             alpha = (Float) mFadeOut.getAnimatedValue();
    538         }
    539         int state = canvas.save();
    540         if (mFadeIn != null) {
    541             float sf = 0.9f + alpha * 0.1f;
    542             canvas.scale(sf, sf, mPieCenterX, mPieCenterY);
    543         }
    544         if (mState != STATE_PIE) {
    545             drawFocus(canvas);
    546         }
    547         if (mState == STATE_FINISHING) {
    548             canvas.restoreToCount(state);
    549             return;
    550         }
    551         if (mState != STATE_PIE) return;
    552         if (!hasOpenItem() || (mXFade != null)) {
    553             // draw base menu
    554             drawArc(canvas, getLevel(), getParent());
    555             List<PieItem> items = getParent().getItems();
    556             final int count = items.size();
    557             int pos = 0;
    558             for (PieItem item : getParent().getItems()) {
    559                 drawItem(Math.max(0, mOpen.size() - 2), pos, count, canvas, item, alpha);
    560                 pos++;
    561             }
    562             mLabel.draw(canvas);
    563         }
    564         if (hasOpenItem()) {
    565             int level = getLevel();
    566             drawArc(canvas, level, getOpenItem());
    567             List<PieItem> items = getOpenItem().getItems();
    568             final int count = items.size();
    569             int pos = 0;
    570             for (PieItem inner : items) {
    571                 if (mFadeOut != null) {
    572                     drawItem(level, pos, count, canvas, inner, alpha);
    573                 } else {
    574                     drawItem(level, pos, count, canvas, inner, (mXFade != null) ? (1 - 0.5f * alpha) : 1);
    575                 }
    576                 pos++;
    577             }
    578             mLabel.draw(canvas);
    579         }
    580         canvas.restoreToCount(state);
    581     }
    582 
    583     private void drawArc(Canvas canvas, int level, PieItem item) {
    584         // arc
    585         if (mState == STATE_PIE) {
    586             final int count = item.getItems().size();
    587             float start = mCenterAngle + (count * SWEEP_ARC / 2f);
    588             float end =  mCenterAngle - (count * SWEEP_ARC / 2f);
    589             int cy = mArcCenterY - level * mRadiusInc;
    590             canvas.drawArc(new RectF(mPieCenterX - mArcRadius, cy - mArcRadius,
    591                     mPieCenterX + mArcRadius, cy + mArcRadius),
    592                     getDegrees(end), getDegrees(start) - getDegrees(end), false, mMenuArcPaint);
    593         }
    594     }
    595 
    596     private void drawItem(int level, int pos, int count, Canvas canvas, PieItem item, float alpha) {
    597         if (mState == STATE_PIE) {
    598             if (item.getPath() != null) {
    599                 int y = mArcCenterY - level * mRadiusInc;
    600                 if (item.isSelected()) {
    601                     Paint p = mSelectedPaint;
    602                     int state = canvas.save();
    603                     float angle = 0;
    604                     if (mSlice != null) {
    605                         angle = (Float) mSlice.getAnimatedValue();
    606                     } else {
    607                         angle = getArcCenter(item, pos, count) - SWEEP_ARC / 2f;
    608                     }
    609                     angle = getDegrees(angle);
    610                     canvas.rotate(angle, mPieCenterX, y);
    611                     if (mFadeOut != null) {
    612                         p.setAlpha((int)(255 * alpha));
    613                     }
    614                     canvas.drawPath(item.getPath(), p);
    615                     if (mFadeOut != null) {
    616                         p.setAlpha(255);
    617                     }
    618                     canvas.restoreToCount(state);
    619                 }
    620                 if (mFadeOut == null) {
    621                     alpha = alpha * (item.isEnabled() ? 1 : 0.3f);
    622                     // draw the item view
    623                     item.setAlpha(alpha);
    624                 }
    625                 item.draw(canvas);
    626             }
    627         }
    628     }
    629 
    630     @Override
    631     public boolean onTouchEvent(MotionEvent evt) {
    632         float x = evt.getX();
    633         float y = evt.getY();
    634         int action = evt.getActionMasked();
    635         getPolar(x, y, !mTapMode, mPolar);
    636         if (MotionEvent.ACTION_DOWN == action) {
    637             if ((x < mDeadZone) || (x > getWidth() - mDeadZone)) {
    638                 return false;
    639             }
    640             mDown.x = (int) evt.getX();
    641             mDown.y = (int) evt.getY();
    642             mOpening = false;
    643             if (mTapMode) {
    644                 PieItem item = findItem(mPolar);
    645                 if ((item != null) && (mCurrentItem != item)) {
    646                     mState = STATE_PIE;
    647                     onEnter(item);
    648                 }
    649             } else {
    650                 setCenter((int) x, (int) y);
    651                 show(true);
    652             }
    653             return true;
    654         } else if (MotionEvent.ACTION_UP == action) {
    655             if (isVisible()) {
    656                 PieItem item = mCurrentItem;
    657                 if (mTapMode) {
    658                     item = findItem(mPolar);
    659                     if (mOpening) {
    660                         mOpening = false;
    661                         return true;
    662                     }
    663                 }
    664                 if (item == null) {
    665                     mTapMode = false;
    666                     show(false);
    667                 } else if (!mOpening && !item.hasItems()) {
    668                         startFadeOut(item);
    669                         mTapMode = false;
    670                 } else {
    671                     mTapMode = true;
    672                 }
    673                 return true;
    674             }
    675         } else if (MotionEvent.ACTION_CANCEL == action) {
    676             if (isVisible() || mTapMode) {
    677                 show(false);
    678             }
    679             deselect();
    680             mHandler.removeMessages(MSG_OPENSUBMENU);
    681             return false;
    682         } else if (MotionEvent.ACTION_MOVE == action) {
    683             if (pulledToCenter(mPolar)) {
    684                 mHandler.removeMessages(MSG_OPENSUBMENU);
    685                 if (hasOpenItem()) {
    686                     if (mCurrentItem != null) {
    687                         mCurrentItem.setSelected(false);
    688                     }
    689                     closeOpenItem();
    690                     mCurrentItem = null;
    691                 } else {
    692                     deselect();
    693                 }
    694                 mLabel.setText("");
    695                 return false;
    696             }
    697             PieItem item = findItem(mPolar);
    698             boolean moved = hasMoved(evt);
    699             if ((item != null) && (mCurrentItem != item) && (!mOpening || moved)) {
    700                 mHandler.removeMessages(MSG_OPENSUBMENU);
    701                 // only select if we didn't just open or have moved past slop
    702                 if (moved) {
    703                     // switch back to swipe mode
    704                     mTapMode = false;
    705                 }
    706                 onEnterSelect(item);
    707                 mHandler.sendEmptyMessageDelayed(MSG_OPENSUBMENU, PIE_OPEN_SUB_DELAY);
    708             }
    709         }
    710         return false;
    711     }
    712 
    713     @Override
    714     public boolean isVisible() {
    715         return super.isVisible() || mProgressRenderer.isVisible();
    716     }
    717 
    718     private boolean pulledToCenter(PointF polarCoords) {
    719         return polarCoords.y < mArcRadius - mRadiusInc;
    720     }
    721 
    722     private boolean inside(PointF polar, PieItem item, int pos, int count) {
    723         float start = getSliceCenter(item, pos, count) - SWEEP_SLICE / 2f;
    724         boolean res =  (mArcRadius < polar.y)
    725                 && (start < polar.x)
    726                 && (start + SWEEP_SLICE > polar.x)
    727                 && (!mTapMode || (mArcRadius + mRadiusInc > polar.y));
    728         return res;
    729     }
    730 
    731     private void getPolar(float x, float y, boolean useOffset, PointF res) {
    732         // get angle and radius from x/y
    733         res.x = (float) Math.PI / 2;
    734         x = x - mPieCenterX;
    735         float y1 = mSliceCenterY - getLevel() * mRadiusInc - y;
    736         float y2 = mArcCenterY - getLevel() * mRadiusInc - y;
    737         res.y = (float) Math.sqrt(x * x + y2 * y2);
    738         if (x != 0) {
    739             res.x = (float) Math.atan2(y1,  x);
    740             if (res.x < 0) {
    741                 res.x = (float) (2 * Math.PI + res.x);
    742             }
    743         }
    744         res.y = res.y + (useOffset ? mTouchOffset : 0);
    745     }
    746 
    747     private boolean hasMoved(MotionEvent e) {
    748         return mTouchSlopSquared < (e.getX() - mDown.x) * (e.getX() - mDown.x)
    749                 + (e.getY() - mDown.y) * (e.getY() - mDown.y);
    750     }
    751 
    752     private void onEnterSelect(PieItem item) {
    753         if (mCurrentItem != null) {
    754             mCurrentItem.setSelected(false);
    755         }
    756         if (item != null && item.isEnabled()) {
    757             moveSelection(mCurrentItem, item);
    758             item.setSelected(true);
    759             mCurrentItem = item;
    760             mLabel.setText(mCurrentItem.getLabel());
    761             layoutLabel(getLevel());
    762         } else {
    763             mCurrentItem = null;
    764         }
    765     }
    766 
    767     private void onEnterOpen() {
    768         if ((mCurrentItem != null) && (mCurrentItem != getOpenItem()) && mCurrentItem.hasItems()) {
    769             openCurrentItem();
    770         }
    771     }
    772 
    773     /**
    774      * enter a slice for a view
    775      * updates model only
    776      * @param item
    777      */
    778     private void onEnter(PieItem item) {
    779         if (mCurrentItem != null) {
    780             mCurrentItem.setSelected(false);
    781         }
    782         if (item != null && item.isEnabled()) {
    783             item.setSelected(true);
    784             mCurrentItem = item;
    785             mLabel.setText(mCurrentItem.getLabel());
    786             if ((mCurrentItem != getOpenItem()) && mCurrentItem.hasItems()) {
    787                 openCurrentItem();
    788                 layoutLabel(getLevel());
    789             }
    790         } else {
    791             mCurrentItem = null;
    792         }
    793     }
    794 
    795     private void deselect() {
    796         if (mCurrentItem != null) {
    797             mCurrentItem.setSelected(false);
    798         }
    799         if (hasOpenItem()) {
    800             PieItem item = closeOpenItem();
    801             onEnter(item);
    802         } else {
    803             mCurrentItem = null;
    804         }
    805     }
    806 
    807     private int getItemPos(PieItem target) {
    808         List<PieItem> items = getOpenItem().getItems();
    809         return items.indexOf(target);
    810     }
    811 
    812     private int getCurrentCount() {
    813         return getOpenItem().getItems().size();
    814     }
    815 
    816     private void moveSelection(PieItem from, PieItem to) {
    817         final int count = getCurrentCount();
    818         final int fromPos = getItemPos(from);
    819         final int toPos = getItemPos(to);
    820         if (fromPos != -1 && toPos != -1) {
    821             float startAngle = getArcCenter(from, getItemPos(from), count)
    822                     - SWEEP_ARC / 2f;
    823             float endAngle = getArcCenter(to, getItemPos(to), count)
    824                     - SWEEP_ARC / 2f;
    825             mSlice = new ValueAnimator();
    826             mSlice.setFloatValues(startAngle, endAngle);
    827             // linear interpolater
    828             mSlice.setInterpolator(null);
    829             mSlice.setDuration(PIE_SLICE_DURATION);
    830             mSlice.addListener(new AnimatorListener() {
    831                 @Override
    832                 public void onAnimationEnd(Animator arg0) {
    833                     mSlice = null;
    834                 }
    835 
    836                 @Override
    837                 public void onAnimationRepeat(Animator arg0) {
    838                 }
    839 
    840                 @Override
    841                 public void onAnimationStart(Animator arg0) {
    842                 }
    843 
    844                 @Override
    845                 public void onAnimationCancel(Animator arg0) {
    846                 }
    847             });
    848             mSlice.start();
    849         }
    850     }
    851 
    852     private void openCurrentItem() {
    853         if ((mCurrentItem != null) && mCurrentItem.hasItems()) {
    854             mOpen.add(mCurrentItem);
    855             layoutLabel(getLevel());
    856             mOpening = true;
    857             if (mFadeIn != null) {
    858                 mFadeIn.cancel();
    859             }
    860             mXFade = new ValueAnimator();
    861             mXFade.setFloatValues(1f, 0f);
    862             mXFade.setDuration(PIE_XFADE_DURATION);
    863             // Linear interpolation
    864             mXFade.setInterpolator(null);
    865             final PieItem ci = mCurrentItem;
    866             mXFade.addListener(new AnimatorListener() {
    867                 @Override
    868                 public void onAnimationStart(Animator animation) {
    869                 }
    870 
    871                 @Override
    872                 public void onAnimationEnd(Animator animation) {
    873                     mXFade = null;
    874                     ci.setSelected(false);
    875                     mOpening = false;
    876                 }
    877 
    878                 @Override
    879                 public void onAnimationRepeat(Animator animation) {
    880                 }
    881 
    882                 @Override
    883                 public void onAnimationCancel(Animator arg0) {
    884                 }
    885             });
    886             mXFade.start();
    887         }
    888     }
    889 
    890     /**
    891      * @param polar x: angle, y: dist
    892      * @return the item at angle/dist or null
    893      */
    894     private PieItem findItem(PointF polar) {
    895         // find the matching item:
    896         List<PieItem> items = getOpenItem().getItems();
    897         final int count = items.size();
    898         int pos = 0;
    899         for (PieItem item : items) {
    900             if (inside(polar, item, pos, count)) {
    901                 return item;
    902             }
    903             pos++;
    904         }
    905         return null;
    906     }
    907 
    908 
    909     @Override
    910     public boolean handlesTouch() {
    911         return true;
    912     }
    913 
    914     // focus specific code
    915 
    916     public void setBlockFocus(boolean blocked) {
    917         mBlockFocus = blocked;
    918         if (blocked) {
    919             clear();
    920         }
    921     }
    922 
    923     public void setFocus(int x, int y) {
    924         mOverlay.removeCallbacks(mDisappear);
    925         mFocusX = x;
    926         mFocusY = y;
    927         setCircle(mFocusX, mFocusY);
    928     }
    929 
    930     public int getSize() {
    931         return 2 * mCircleSize;
    932     }
    933 
    934     private int getRandomRange() {
    935         return (int)(-60 + 120 * Math.random());
    936     }
    937 
    938     private void setCircle(int cx, int cy) {
    939         mCircle.set(cx - mCircleSize, cy - mCircleSize,
    940                 cx + mCircleSize, cy + mCircleSize);
    941         mDial.set(cx - mCircleSize + mInnerOffset, cy - mCircleSize + mInnerOffset,
    942                 cx + mCircleSize - mInnerOffset, cy + mCircleSize - mInnerOffset);
    943     }
    944 
    945     public void drawFocus(Canvas canvas) {
    946         if (mBlockFocus) return;
    947         mFocusPaint.setStrokeWidth(mOuterStroke);
    948         canvas.drawCircle((float) mFocusX, (float) mFocusY, (float) mCircleSize, mFocusPaint);
    949         if (mState == STATE_PIE) return;
    950         int color = mFocusPaint.getColor();
    951         if (mState == STATE_FINISHING) {
    952             mFocusPaint.setColor(mFocused ? mSuccessColor : mFailColor);
    953         }
    954         mFocusPaint.setStrokeWidth(mInnerStroke);
    955         drawLine(canvas, mDialAngle, mFocusPaint);
    956         drawLine(canvas, mDialAngle + 45, mFocusPaint);
    957         drawLine(canvas, mDialAngle + 180, mFocusPaint);
    958         drawLine(canvas, mDialAngle + 225, mFocusPaint);
    959         canvas.save();
    960         // rotate the arc instead of its offset to better use framework's shape caching
    961         canvas.rotate(mDialAngle, mFocusX, mFocusY);
    962         canvas.drawArc(mDial, 0, 45, false, mFocusPaint);
    963         canvas.drawArc(mDial, 180, 45, false, mFocusPaint);
    964         canvas.restore();
    965         mFocusPaint.setColor(color);
    966     }
    967 
    968     private void drawLine(Canvas canvas, int angle, Paint p) {
    969         convertCart(angle, mCircleSize - mInnerOffset, mPoint1);
    970         convertCart(angle, mCircleSize - mInnerOffset + mInnerOffset / 3, mPoint2);
    971         canvas.drawLine(mPoint1.x + mFocusX, mPoint1.y + mFocusY,
    972                 mPoint2.x + mFocusX, mPoint2.y + mFocusY, p);
    973     }
    974 
    975     private static void convertCart(int angle, int radius, Point out) {
    976         double a = 2 * Math.PI * (angle % 360) / 360;
    977         out.x = (int) (radius * Math.cos(a) + 0.5);
    978         out.y = (int) (radius * Math.sin(a) + 0.5);
    979     }
    980 
    981     @Override
    982     public void showStart() {
    983         if (mState == STATE_PIE) return;
    984         cancelFocus();
    985         mStartAnimationAngle = 67;
    986         int range = getRandomRange();
    987         startAnimation(SCALING_UP_TIME,
    988                 false, mStartAnimationAngle, mStartAnimationAngle + range);
    989         mState = STATE_FOCUSING;
    990     }
    991 
    992     @Override
    993     public void showSuccess(boolean timeout) {
    994         if (mState == STATE_FOCUSING) {
    995             startAnimation(SCALING_DOWN_TIME,
    996                     timeout, mStartAnimationAngle);
    997             mState = STATE_FINISHING;
    998             mFocused = true;
    999         }
   1000     }
   1001 
   1002     @Override
   1003     public void showFail(boolean timeout) {
   1004         if (mState == STATE_FOCUSING) {
   1005             startAnimation(SCALING_DOWN_TIME,
   1006                     timeout, mStartAnimationAngle);
   1007             mState = STATE_FINISHING;
   1008             mFocused = false;
   1009         }
   1010     }
   1011 
   1012     private void cancelFocus() {
   1013         mFocusCancelled = true;
   1014         mOverlay.removeCallbacks(mDisappear);
   1015         if (mAnimation != null && !mAnimation.hasEnded()) {
   1016             mAnimation.cancel();
   1017         }
   1018         mFocusCancelled = false;
   1019         mFocused = false;
   1020         mState = STATE_IDLE;
   1021     }
   1022 
   1023     public void clear(boolean waitUntilProgressIsHidden) {
   1024         if (mState == STATE_PIE)
   1025             return;
   1026         cancelFocus();
   1027 
   1028         if (waitUntilProgressIsHidden) {
   1029             mProgressRenderer.setVisibilityListener(new VisibilityListener() {
   1030                 @Override
   1031                 public void onHidden() {
   1032                     mOverlay.post(mDisappear);
   1033                 }
   1034             });
   1035         } else {
   1036             mOverlay.post(mDisappear);
   1037             mProgressRenderer.setVisibilityListener(null);
   1038         }
   1039     }
   1040 
   1041     @Override
   1042     public void clear() {
   1043         clear(false);
   1044     }
   1045 
   1046     private void startAnimation(long duration, boolean timeout,
   1047             float toScale) {
   1048         startAnimation(duration, timeout, mDialAngle,
   1049                 toScale);
   1050     }
   1051 
   1052     private void startAnimation(long duration, boolean timeout,
   1053             float fromScale, float toScale) {
   1054         setVisible(true);
   1055         mAnimation.reset();
   1056         mAnimation.setDuration(duration);
   1057         mAnimation.setScale(fromScale, toScale);
   1058         mAnimation.setAnimationListener(timeout ? mEndAction : null);
   1059         mOverlay.startAnimation(mAnimation);
   1060         update();
   1061     }
   1062 
   1063     private class EndAction implements Animation.AnimationListener {
   1064         @Override
   1065         public void onAnimationEnd(Animation animation) {
   1066             // Keep the focus indicator for some time.
   1067             if (!mFocusCancelled) {
   1068                 mOverlay.postDelayed(mDisappear, DISAPPEAR_TIMEOUT);
   1069             }
   1070         }
   1071 
   1072         @Override
   1073         public void onAnimationRepeat(Animation animation) {
   1074         }
   1075 
   1076         @Override
   1077         public void onAnimationStart(Animation animation) {
   1078         }
   1079     }
   1080 
   1081     private class Disappear implements Runnable {
   1082         @Override
   1083         public void run() {
   1084             if (mState == STATE_PIE) return;
   1085             setVisible(false);
   1086             mFocusX = mCenterX;
   1087             mFocusY = mCenterY;
   1088             mState = STATE_IDLE;
   1089             setCircle(mFocusX, mFocusY);
   1090             mFocused = false;
   1091         }
   1092     }
   1093 
   1094     private class ScaleAnimation extends Animation {
   1095         private float mFrom = 1f;
   1096         private float mTo = 1f;
   1097 
   1098         public ScaleAnimation() {
   1099             setFillAfter(true);
   1100         }
   1101 
   1102         public void setScale(float from, float to) {
   1103             mFrom = from;
   1104             mTo = to;
   1105         }
   1106 
   1107         @Override
   1108         protected void applyTransformation(float interpolatedTime, Transformation t) {
   1109             mDialAngle = (int)(mFrom + (mTo - mFrom) * interpolatedTime);
   1110         }
   1111     }
   1112 
   1113 }
   1114