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