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