Home | History | Annotate | Download | only in view
      1 /*
      2  * Copyright (C) 2010 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.browser.view;
     18 
     19 import android.animation.Animator;
     20 import android.animation.Animator.AnimatorListener;
     21 import android.animation.AnimatorListenerAdapter;
     22 import android.animation.ValueAnimator;
     23 import android.animation.ValueAnimator.AnimatorUpdateListener;
     24 import android.content.Context;
     25 import android.content.res.Resources;
     26 import android.graphics.Canvas;
     27 import android.graphics.Paint;
     28 import android.graphics.Path;
     29 import android.graphics.Point;
     30 import android.graphics.PointF;
     31 import android.graphics.RectF;
     32 import android.graphics.drawable.Drawable;
     33 import android.util.AttributeSet;
     34 import android.view.MotionEvent;
     35 import android.view.SoundEffectConstants;
     36 import android.view.View;
     37 import android.view.ViewGroup;
     38 import android.widget.FrameLayout;
     39 
     40 import com.android.browser.R;
     41 
     42 import java.util.ArrayList;
     43 import java.util.List;
     44 
     45 public class PieMenu extends FrameLayout {
     46 
     47     private static final int MAX_LEVELS = 5;
     48     private static final long ANIMATION = 80;
     49 
     50     public interface PieController {
     51         /**
     52          * called before menu opens to customize menu
     53          * returns if pie state has been changed
     54          */
     55         public boolean onOpen();
     56         public void stopEditingUrl();
     57 
     58     }
     59 
     60     /**
     61      * A view like object that lives off of the pie menu
     62      */
     63     public interface PieView {
     64 
     65         public interface OnLayoutListener {
     66             public void onLayout(int ax, int ay, boolean left);
     67         }
     68 
     69         public void setLayoutListener(OnLayoutListener l);
     70 
     71         public void layout(int anchorX, int anchorY, boolean onleft, float angle,
     72                 int parentHeight);
     73 
     74         public void draw(Canvas c);
     75 
     76         public boolean onTouchEvent(MotionEvent evt);
     77 
     78     }
     79 
     80     private Point mCenter;
     81     private int mRadius;
     82     private int mRadiusInc;
     83     private int mSlop;
     84     private int mTouchOffset;
     85     private Path mPath;
     86 
     87     private boolean mOpen;
     88     private PieController mController;
     89 
     90     private List<PieItem> mItems;
     91     private int mLevels;
     92     private int[] mCounts;
     93     private PieView mPieView = null;
     94 
     95     // sub menus
     96     private List<PieItem> mCurrentItems;
     97     private PieItem mOpenItem;
     98 
     99     private Drawable mBackground;
    100     private Paint mNormalPaint;
    101     private Paint mSelectedPaint;
    102     private Paint mSubPaint;
    103 
    104     // touch handling
    105     private PieItem mCurrentItem;
    106 
    107     private boolean mUseBackground;
    108     private boolean mAnimating;
    109 
    110     /**
    111      * @param context
    112      * @param attrs
    113      * @param defStyle
    114      */
    115     public PieMenu(Context context, AttributeSet attrs, int defStyle) {
    116         super(context, attrs, defStyle);
    117         init(context);
    118     }
    119 
    120     /**
    121      * @param context
    122      * @param attrs
    123      */
    124     public PieMenu(Context context, AttributeSet attrs) {
    125         super(context, attrs);
    126         init(context);
    127     }
    128 
    129     /**
    130      * @param context
    131      */
    132     public PieMenu(Context context) {
    133         super(context);
    134         init(context);
    135     }
    136 
    137     private void init(Context ctx) {
    138         mItems = new ArrayList<PieItem>();
    139         mLevels = 0;
    140         mCounts = new int[MAX_LEVELS];
    141         Resources res = ctx.getResources();
    142         mRadius = (int) res.getDimension(R.dimen.qc_radius_start);
    143         mRadiusInc = (int) res.getDimension(R.dimen.qc_radius_increment);
    144         mSlop = (int) res.getDimension(R.dimen.qc_slop);
    145         mTouchOffset = (int) res.getDimension(R.dimen.qc_touch_offset);
    146         mOpen = false;
    147         setWillNotDraw(false);
    148         setDrawingCacheEnabled(false);
    149         mCenter = new Point(0,0);
    150         mBackground = res.getDrawable(R.drawable.qc_background_normal);
    151         mNormalPaint = new Paint();
    152         mNormalPaint.setColor(res.getColor(R.color.qc_normal));
    153         mNormalPaint.setAntiAlias(true);
    154         mSelectedPaint = new Paint();
    155         mSelectedPaint.setColor(res.getColor(R.color.qc_selected));
    156         mSelectedPaint.setAntiAlias(true);
    157         mSubPaint = new Paint();
    158         mSubPaint.setAntiAlias(true);
    159         mSubPaint.setColor(res.getColor(R.color.qc_sub));
    160     }
    161 
    162     public void setController(PieController ctl) {
    163         mController = ctl;
    164     }
    165 
    166     public void setUseBackground(boolean useBackground) {
    167         mUseBackground = useBackground;
    168     }
    169 
    170     public void addItem(PieItem item) {
    171         // add the item to the pie itself
    172         mItems.add(item);
    173         int l = item.getLevel();
    174         mLevels = Math.max(mLevels, l);
    175         mCounts[l]++;
    176     }
    177 
    178     public void removeItem(PieItem item) {
    179         mItems.remove(item);
    180     }
    181 
    182     public void clearItems() {
    183         mItems.clear();
    184     }
    185 
    186     private boolean onTheLeft() {
    187         return mCenter.x < mSlop;
    188     }
    189 
    190     /**
    191      * guaranteed has center set
    192      * @param show
    193      */
    194     private void show(boolean show) {
    195         mOpen = show;
    196         if (mOpen) {
    197             // ensure clean state
    198             mAnimating = false;
    199             mCurrentItem = null;
    200             mOpenItem = null;
    201             mPieView = null;
    202             mController.stopEditingUrl();
    203             mCurrentItems = mItems;
    204             for (PieItem item : mCurrentItems) {
    205                 item.setSelected(false);
    206             }
    207             if (mController != null) {
    208                 boolean changed = mController.onOpen();
    209             }
    210             layoutPie();
    211             animateOpen();
    212         }
    213         invalidate();
    214     }
    215 
    216     private void animateOpen() {
    217         ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
    218         anim.addUpdateListener(new AnimatorUpdateListener() {
    219             @Override
    220             public void onAnimationUpdate(ValueAnimator animation) {
    221                 for (PieItem item : mCurrentItems) {
    222                     item.setAnimationAngle((1 - animation.getAnimatedFraction()) * (- item.getStart()));
    223                 }
    224                 invalidate();
    225             }
    226 
    227         });
    228         anim.setDuration(2*ANIMATION);
    229         anim.start();
    230     }
    231 
    232     private void setCenter(int x, int y) {
    233         if (x < mSlop) {
    234             mCenter.x = 0;
    235         } else {
    236             mCenter.x = getWidth();
    237         }
    238         mCenter.y = y;
    239     }
    240 
    241     private void layoutPie() {
    242         float emptyangle = (float) Math.PI / 16;
    243         int rgap = 2;
    244         int inner = mRadius + rgap;
    245         int outer = mRadius + mRadiusInc - rgap;
    246         int gap = 1;
    247         for (int i = 0; i < mLevels; i++) {
    248             int level = i + 1;
    249             float sweep = (float) (Math.PI - 2 * emptyangle) / mCounts[level];
    250             float angle = emptyangle + sweep / 2;
    251             mPath = makeSlice(getDegrees(0) - gap, getDegrees(sweep) + gap, outer, inner, mCenter);
    252             for (PieItem item : mCurrentItems) {
    253                 if (item.getLevel() == level) {
    254                     View view = item.getView();
    255                     if (view != null) {
    256                         view.measure(view.getLayoutParams().width,
    257                                 view.getLayoutParams().height);
    258                         int w = view.getMeasuredWidth();
    259                         int h = view.getMeasuredHeight();
    260                         int r = inner + (outer - inner) * 2 / 3;
    261                         int x = (int) (r * Math.sin(angle));
    262                         int y = mCenter.y - (int) (r * Math.cos(angle)) - h / 2;
    263                         if (onTheLeft()) {
    264                             x = mCenter.x + x - w / 2;
    265                         } else {
    266                             x = mCenter.x - x - w / 2;
    267                         }
    268                         view.layout(x, y, x + w, y + h);
    269                     }
    270                     float itemstart = angle - sweep / 2;
    271                     item.setGeometry(itemstart, sweep, inner, outer);
    272                     angle += sweep;
    273                 }
    274             }
    275             inner += mRadiusInc;
    276             outer += mRadiusInc;
    277         }
    278     }
    279 
    280 
    281     /**
    282      * converts a
    283      *
    284      * @param angle from 0..PI to Android degrees (clockwise starting at 3
    285      *        o'clock)
    286      * @return skia angle
    287      */
    288     private float getDegrees(double angle) {
    289         return (float) (270 - 180 * angle / Math.PI);
    290     }
    291 
    292     @Override
    293     protected void onDraw(Canvas canvas) {
    294         if (mOpen) {
    295             int state;
    296             if (mUseBackground) {
    297                 int w = mBackground.getIntrinsicWidth();
    298                 int h = mBackground.getIntrinsicHeight();
    299                 int left = mCenter.x - w;
    300                 int top = mCenter.y - h / 2;
    301                 mBackground.setBounds(left, top, left + w, top + h);
    302                 state = canvas.save();
    303                 if (onTheLeft()) {
    304                     canvas.scale(-1, 1);
    305                 }
    306                 mBackground.draw(canvas);
    307                 canvas.restoreToCount(state);
    308             }
    309             // draw base menu
    310             PieItem last = mCurrentItem;
    311             if (mOpenItem != null) {
    312                 last = mOpenItem;
    313             }
    314             for (PieItem item : mCurrentItems) {
    315                 if (item != last) {
    316                     drawItem(canvas, item);
    317                 }
    318             }
    319             if (last != null) {
    320                 drawItem(canvas, last);
    321             }
    322             if (mPieView != null) {
    323                 mPieView.draw(canvas);
    324             }
    325         }
    326     }
    327 
    328     private void drawItem(Canvas canvas, PieItem item) {
    329         if (item.getView() != null) {
    330             Paint p = item.isSelected() ? mSelectedPaint : mNormalPaint;
    331             if (!mItems.contains(item)) {
    332                 p = item.isSelected() ? mSelectedPaint : mSubPaint;
    333             }
    334             int state = canvas.save();
    335             if (onTheLeft()) {
    336                 canvas.scale(-1, 1);
    337             }
    338             float r = getDegrees(item.getStartAngle()) - 270; // degrees(0)
    339             canvas.rotate(r, mCenter.x, mCenter.y);
    340             canvas.drawPath(mPath, p);
    341             canvas.restoreToCount(state);
    342             // draw the item view
    343             View view = item.getView();
    344             state = canvas.save();
    345             canvas.translate(view.getX(), view.getY());
    346             view.draw(canvas);
    347             canvas.restoreToCount(state);
    348         }
    349     }
    350 
    351     private Path makeSlice(float start, float end, int outer, int inner, Point center) {
    352         RectF bb =
    353                 new RectF(center.x - outer, center.y - outer, center.x + outer,
    354                         center.y + outer);
    355         RectF bbi =
    356                 new RectF(center.x - inner, center.y - inner, center.x + inner,
    357                         center.y + inner);
    358         Path path = new Path();
    359         path.arcTo(bb, start, end - start, true);
    360         path.arcTo(bbi, end, start - end);
    361         path.close();
    362         return path;
    363     }
    364 
    365     // touch handling for pie
    366 
    367     @Override
    368     public boolean onTouchEvent(MotionEvent evt) {
    369         float x = evt.getX();
    370         float y = evt.getY();
    371         int action = evt.getActionMasked();
    372         if (MotionEvent.ACTION_DOWN == action) {
    373             if ((x > getWidth() - mSlop) || (x < mSlop)) {
    374                 setCenter((int) x, (int) y);
    375                 show(true);
    376                 return true;
    377             }
    378         } else if (MotionEvent.ACTION_UP == action) {
    379             if (mOpen) {
    380                 boolean handled = false;
    381                 if (mPieView != null) {
    382                     handled = mPieView.onTouchEvent(evt);
    383                 }
    384                 PieItem item = mCurrentItem;
    385                 if (!mAnimating) {
    386                     deselect();
    387                 }
    388                 show(false);
    389                 if (!handled && (item != null) && (item.getView() != null)) {
    390                     if ((item == mOpenItem) || !mAnimating) {
    391                         item.getView().performClick();
    392                     }
    393                 }
    394                 return true;
    395             }
    396         } else if (MotionEvent.ACTION_CANCEL == action) {
    397             if (mOpen) {
    398                 show(false);
    399             }
    400             if (!mAnimating) {
    401                 deselect();
    402                 invalidate();
    403             }
    404             return false;
    405         } else if (MotionEvent.ACTION_MOVE == action) {
    406             if (mAnimating) return false;
    407             boolean handled = false;
    408             PointF polar = getPolar(x, y);
    409             int maxr = mRadius + mLevels * mRadiusInc + 50;
    410             if (mPieView != null) {
    411                 handled = mPieView.onTouchEvent(evt);
    412             }
    413             if (handled) {
    414                 invalidate();
    415                 return false;
    416             }
    417             if (polar.y < mRadius) {
    418                 if (mOpenItem != null) {
    419                     closeSub();
    420                 } else if (!mAnimating) {
    421                     deselect();
    422                     invalidate();
    423                 }
    424                 return false;
    425             }
    426             if (polar.y > maxr) {
    427                 deselect();
    428                 show(false);
    429                 evt.setAction(MotionEvent.ACTION_DOWN);
    430                 if (getParent() != null) {
    431                     ((ViewGroup) getParent()).dispatchTouchEvent(evt);
    432                 }
    433                 return false;
    434             }
    435             PieItem item = findItem(polar);
    436             if (item == null) {
    437             } else if (mCurrentItem != item) {
    438                 onEnter(item);
    439                 if ((item != null) && item.isPieView() && (item.getView() != null)) {
    440                     int cx = item.getView().getLeft() + (onTheLeft()
    441                             ? item.getView().getWidth() : 0);
    442                     int cy = item.getView().getTop();
    443                     mPieView = item.getPieView();
    444                     layoutPieView(mPieView, cx, cy,
    445                             (item.getStartAngle() + item.getSweep()) / 2);
    446                 }
    447                 invalidate();
    448             }
    449         }
    450         // always re-dispatch event
    451         return false;
    452     }
    453 
    454     private void layoutPieView(PieView pv, int x, int y, float angle) {
    455         pv.layout(x, y, onTheLeft(), angle, getHeight());
    456     }
    457 
    458     /**
    459      * enter a slice for a view
    460      * updates model only
    461      * @param item
    462      */
    463     private void onEnter(PieItem item) {
    464         // deselect
    465         if (mCurrentItem != null) {
    466             mCurrentItem.setSelected(false);
    467         }
    468         if (item != null) {
    469             // clear up stack
    470             playSoundEffect(SoundEffectConstants.CLICK);
    471             item.setSelected(true);
    472             mPieView = null;
    473             mCurrentItem = item;
    474             if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) {
    475                 openSub(mCurrentItem);
    476                 mOpenItem = item;
    477             }
    478         } else {
    479             mCurrentItem = null;
    480         }
    481 
    482     }
    483 
    484     private void animateOut(final PieItem fixed, AnimatorListener listener) {
    485         if ((mCurrentItems == null) || (fixed == null)) return;
    486         final float target = fixed.getStartAngle();
    487         ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
    488         anim.addUpdateListener(new AnimatorUpdateListener() {
    489             @Override
    490             public void onAnimationUpdate(ValueAnimator animation) {
    491                 for (PieItem item : mCurrentItems) {
    492                     if (item != fixed) {
    493                         item.setAnimationAngle(animation.getAnimatedFraction()
    494                                 * (target - item.getStart()));
    495                     }
    496                 }
    497                 invalidate();
    498             }
    499         });
    500         anim.setDuration(ANIMATION);
    501         anim.addListener(listener);
    502         anim.start();
    503     }
    504 
    505     private void animateIn(final PieItem fixed, AnimatorListener listener) {
    506         if ((mCurrentItems == null) || (fixed == null)) return;
    507         final float target = fixed.getStartAngle();
    508         ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
    509         anim.addUpdateListener(new AnimatorUpdateListener() {
    510             @Override
    511             public void onAnimationUpdate(ValueAnimator animation) {
    512                 for (PieItem item : mCurrentItems) {
    513                     if (item != fixed) {
    514                         item.setAnimationAngle((1 - animation.getAnimatedFraction())
    515                                 * (target - item.getStart()));
    516                     }
    517                 }
    518                 invalidate();
    519 
    520             }
    521 
    522         });
    523         anim.setDuration(ANIMATION);
    524         anim.addListener(listener);
    525         anim.start();
    526     }
    527 
    528     private void openSub(final PieItem item) {
    529         mAnimating = true;
    530         animateOut(item, new AnimatorListenerAdapter() {
    531             public void onAnimationEnd(Animator a) {
    532                 for (PieItem item : mCurrentItems) {
    533                     item.setAnimationAngle(0);
    534                 }
    535                 mCurrentItems = new ArrayList<PieItem>(mItems.size());
    536                 int i = 0, j = 0;
    537                 while (i < mItems.size()) {
    538                     if (mItems.get(i) == item) {
    539                         mCurrentItems.add(item);
    540                     } else {
    541                         mCurrentItems.add(item.getItems().get(j++));
    542                     }
    543                     i++;
    544                 }
    545                 layoutPie();
    546                 animateIn(item, new AnimatorListenerAdapter() {
    547                     public void onAnimationEnd(Animator a) {
    548                         for (PieItem item : mCurrentItems) {
    549                             item.setAnimationAngle(0);
    550                         }
    551                         mAnimating = false;
    552                     }
    553                 });
    554             }
    555         });
    556     }
    557 
    558     private void closeSub() {
    559         mAnimating = true;
    560         if (mCurrentItem != null) {
    561             mCurrentItem.setSelected(false);
    562         }
    563         animateOut(mOpenItem, new AnimatorListenerAdapter() {
    564             public void onAnimationEnd(Animator a) {
    565                 for (PieItem item : mCurrentItems) {
    566                     item.setAnimationAngle(0);
    567                 }
    568                 mCurrentItems = mItems;
    569                 mPieView = null;
    570                 animateIn(mOpenItem, new AnimatorListenerAdapter() {
    571                     public void onAnimationEnd(Animator a) {
    572                         for (PieItem item : mCurrentItems) {
    573                             item.setAnimationAngle(0);
    574                         }
    575                         mAnimating = false;
    576                         mOpenItem = null;
    577                         mCurrentItem = null;
    578                     }
    579                 });
    580             }
    581         });
    582     }
    583 
    584     private void deselect() {
    585         if (mCurrentItem != null) {
    586             mCurrentItem.setSelected(false);
    587         }
    588         if (mOpenItem != null) {
    589             mOpenItem = null;
    590             mCurrentItems = mItems;
    591         }
    592         mCurrentItem = null;
    593         mPieView = null;
    594     }
    595 
    596     private PointF getPolar(float x, float y) {
    597         PointF res = new PointF();
    598         // get angle and radius from x/y
    599         res.x = (float) Math.PI / 2;
    600         x = mCenter.x - x;
    601         if (mCenter.x < mSlop) {
    602             x = -x;
    603         }
    604         y = mCenter.y - y;
    605         res.y = (float) Math.sqrt(x * x + y * y);
    606         if (y > 0) {
    607             res.x = (float) Math.asin(x / res.y);
    608         } else if (y < 0) {
    609             res.x = (float) (Math.PI - Math.asin(x / res.y ));
    610         }
    611         return res;
    612     }
    613 
    614     /**
    615      *
    616      * @param polar x: angle, y: dist
    617      * @return the item at angle/dist or null
    618      */
    619     private PieItem findItem(PointF polar) {
    620         // find the matching item:
    621         for (PieItem item : mCurrentItems) {
    622             if (inside(polar, mTouchOffset, item)) {
    623                 return item;
    624             }
    625         }
    626         return null;
    627     }
    628 
    629     private boolean inside(PointF polar, float offset, PieItem item) {
    630         return (item.getInnerRadius() - offset < polar.y)
    631         && (item.getOuterRadius() - offset > polar.y)
    632         && (item.getStartAngle() < polar.x)
    633         && (item.getStartAngle() + item.getSweep() > polar.x);
    634     }
    635 
    636 }
    637