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 com.android.browser.R;
     20 
     21 import android.content.Context;
     22 import android.content.res.Resources;
     23 import android.graphics.Canvas;
     24 import android.graphics.Paint;
     25 import android.graphics.Path;
     26 import android.graphics.Point;
     27 import android.graphics.PointF;
     28 import android.graphics.RectF;
     29 import android.graphics.drawable.Drawable;
     30 import android.util.AttributeSet;
     31 import android.view.MotionEvent;
     32 import android.view.SoundEffectConstants;
     33 import android.view.View;
     34 import android.view.ViewGroup;
     35 import android.widget.FrameLayout;
     36 
     37 import java.util.ArrayList;
     38 import java.util.List;
     39 
     40 public class PieMenu extends FrameLayout {
     41 
     42     private static final int MAX_LEVELS = 5;
     43 
     44     public interface PieController {
     45         /**
     46          * called before menu opens to customize menu
     47          * returns if pie state has been changed
     48          */
     49         public boolean onOpen();
     50     }
     51 
     52     /**
     53      * A view like object that lives off of the pie menu
     54      */
     55     public interface PieView {
     56 
     57         public interface OnLayoutListener {
     58             public void onLayout(int ax, int ay, boolean left);
     59         }
     60 
     61         public void setLayoutListener(OnLayoutListener l);
     62 
     63         public void layout(int anchorX, int anchorY, boolean onleft, float angle);
     64 
     65         public void draw(Canvas c);
     66 
     67         public boolean onTouchEvent(MotionEvent evt);
     68 
     69     }
     70 
     71     private Point mCenter;
     72     private int mRadius;
     73     private int mRadiusInc;
     74     private int mSlop;
     75     private int mTouchOffset;
     76 
     77     private boolean mOpen;
     78     private PieController mController;
     79 
     80     private List<PieItem> mItems;
     81     private int mLevels;
     82     private int[] mCounts;
     83     private PieView mPieView = null;
     84 
     85     private Drawable mBackground;
     86     private Paint mNormalPaint;
     87     private Paint mSelectedPaint;
     88 
     89     // touch handling
     90     PieItem mCurrentItem;
     91 
     92     private boolean mUseBackground;
     93 
     94     /**
     95      * @param context
     96      * @param attrs
     97      * @param defStyle
     98      */
     99     public PieMenu(Context context, AttributeSet attrs, int defStyle) {
    100         super(context, attrs, defStyle);
    101         init(context);
    102     }
    103 
    104     /**
    105      * @param context
    106      * @param attrs
    107      */
    108     public PieMenu(Context context, AttributeSet attrs) {
    109         super(context, attrs);
    110         init(context);
    111     }
    112 
    113     /**
    114      * @param context
    115      */
    116     public PieMenu(Context context) {
    117         super(context);
    118         init(context);
    119     }
    120 
    121     private void init(Context ctx) {
    122         mItems = new ArrayList<PieItem>();
    123         mLevels = 0;
    124         mCounts = new int[MAX_LEVELS];
    125         Resources res = ctx.getResources();
    126         mRadius = (int) res.getDimension(R.dimen.qc_radius_start);
    127         mRadiusInc = (int) res.getDimension(R.dimen.qc_radius_increment);
    128         mSlop = (int) res.getDimension(R.dimen.qc_slop);
    129         mTouchOffset = (int) res.getDimension(R.dimen.qc_touch_offset);
    130         mOpen = false;
    131         setWillNotDraw(false);
    132         setDrawingCacheEnabled(false);
    133         mCenter = new Point(0,0);
    134         mBackground = res.getDrawable(R.drawable.qc_background_normal);
    135         mNormalPaint = new Paint();
    136         mNormalPaint.setColor(res.getColor(R.color.qc_normal));
    137         mNormalPaint.setAntiAlias(true);
    138         mSelectedPaint = new Paint();
    139         mSelectedPaint.setColor(res.getColor(R.color.qc_selected));
    140         mSelectedPaint.setAntiAlias(true);
    141     }
    142 
    143     public void setController(PieController ctl) {
    144         mController = ctl;
    145     }
    146 
    147     public void setUseBackground(boolean useBackground) {
    148         mUseBackground = useBackground;
    149     }
    150 
    151     public void addItem(PieItem item) {
    152         // add the item to the pie itself
    153         mItems.add(item);
    154         int l = item.getLevel();
    155         mLevels = Math.max(mLevels, l);
    156         mCounts[l]++;
    157     }
    158 
    159     public void removeItem(PieItem item) {
    160         mItems.remove(item);
    161     }
    162 
    163     public void clearItems() {
    164         mItems.clear();
    165     }
    166 
    167     private boolean onTheLeft() {
    168         return mCenter.x < mSlop;
    169     }
    170 
    171     /**
    172      * guaranteed has center set
    173      * @param show
    174      */
    175     private void show(boolean show) {
    176         mOpen = show;
    177         if (mOpen) {
    178             if (mController != null) {
    179                 boolean changed = mController.onOpen();
    180             }
    181             layoutPie();
    182         }
    183         if (!show) {
    184             mCurrentItem = null;
    185             mPieView = null;
    186         }
    187         invalidate();
    188     }
    189 
    190     private void setCenter(int x, int y) {
    191         if (x < mSlop) {
    192             mCenter.x = 0;
    193         } else {
    194             mCenter.x = getWidth();
    195         }
    196         mCenter.y = y;
    197     }
    198 
    199     private void layoutPie() {
    200         float emptyangle = (float) Math.PI / 16;
    201         int rgap = 2;
    202         int inner = mRadius + rgap;
    203         int outer = mRadius + mRadiusInc - rgap;
    204         int radius = mRadius;
    205         int gap = 1;
    206         for (int i = 0; i < mLevels; i++) {
    207             int level = i + 1;
    208             float sweep = (float) (Math.PI - 2 * emptyangle) / mCounts[level];
    209             float angle = emptyangle + sweep / 2;
    210             for (PieItem item : mItems) {
    211                 if (item.getLevel() == level) {
    212                     View view = item.getView();
    213                     view.measure(view.getLayoutParams().width,
    214                             view.getLayoutParams().height);
    215                     int w = view.getMeasuredWidth();
    216                     int h = view.getMeasuredHeight();
    217                     int r = inner + (outer - inner) * 2 / 3;
    218                     int x = (int) (r * Math.sin(angle));
    219                     int y = mCenter.y - (int) (r * Math.cos(angle)) - h / 2;
    220                     if (onTheLeft()) {
    221                         x = mCenter.x + x - w / 2;
    222                     } else {
    223                         x = mCenter.x - x - w / 2;
    224                     }
    225                     view.layout(x, y, x + w, y + h);
    226                     float itemstart = angle - sweep / 2;
    227                     Path slice = makeSlice(getDegrees(itemstart) - gap,
    228                             getDegrees(itemstart + sweep) + gap,
    229                             outer, inner, mCenter);
    230                     item.setGeometry(itemstart, sweep, inner, outer, slice);
    231                     angle += sweep;
    232                 }
    233             }
    234             inner += mRadiusInc;
    235             outer += mRadiusInc;
    236         }
    237     }
    238 
    239 
    240     /**
    241      * converts a
    242      *
    243      * @param angle from 0..PI to Android degrees (clockwise starting at 3
    244      *        o'clock)
    245      * @return skia angle
    246      */
    247     private float getDegrees(double angle) {
    248         return (float) (270 - 180 * angle / Math.PI);
    249     }
    250 
    251     @Override
    252     protected void onDraw(Canvas canvas) {
    253         if (mOpen) {
    254             int state;
    255             if (mUseBackground) {
    256                 int w = mBackground.getIntrinsicWidth();
    257                 int h = mBackground.getIntrinsicHeight();
    258                 int left = mCenter.x - w;
    259                 int top = mCenter.y - h / 2;
    260                 mBackground.setBounds(left, top, left + w, top + h);
    261                 state = canvas.save();
    262                 if (onTheLeft()) {
    263                     canvas.scale(-1, 1);
    264                 }
    265                 mBackground.draw(canvas);
    266                 canvas.restoreToCount(state);
    267             }
    268             for (PieItem item : mItems) {
    269                 Paint p = item.isSelected() ? mSelectedPaint : mNormalPaint;
    270                 state = canvas.save();
    271                 if (onTheLeft()) {
    272                     canvas.scale(-1, 1);
    273                 }
    274                 drawPath(canvas, item.getPath(), p);
    275                 canvas.restoreToCount(state);
    276                 drawItem(canvas, item);
    277             }
    278             if (mPieView != null) {
    279                 mPieView.draw(canvas);
    280             }
    281         }
    282     }
    283 
    284     private void drawItem(Canvas canvas, PieItem item) {
    285         int outer = item.getOuterRadius();
    286         int left = mCenter.x - outer;
    287         int top = mCenter.y - outer;
    288         // draw the item view
    289         View view = item.getView();
    290         int state = canvas.save();
    291         canvas.translate(view.getX(), view.getY());
    292         view.draw(canvas);
    293         canvas.restoreToCount(state);
    294     }
    295 
    296     private void drawPath(Canvas canvas, Path path, Paint paint) {
    297         canvas.drawPath(path, paint);
    298     }
    299 
    300     private Path makeSlice(float start, float end, int outer, int inner, Point center) {
    301         RectF bb =
    302                 new RectF(center.x - outer, center.y - outer, center.x + outer,
    303                         center.y + outer);
    304         RectF bbi =
    305                 new RectF(center.x - inner, center.y - inner, center.x + inner,
    306                         center.y + inner);
    307         Path path = new Path();
    308         path.arcTo(bb, start, end - start, true);
    309         path.arcTo(bbi, end, start - end);
    310         path.close();
    311         return path;
    312     }
    313 
    314     // touch handling for pie
    315 
    316     @Override
    317     public boolean onTouchEvent(MotionEvent evt) {
    318         float x = evt.getX();
    319         float y = evt.getY();
    320         int action = evt.getActionMasked();
    321         if (MotionEvent.ACTION_DOWN == action) {
    322             if ((x > getWidth() - mSlop) || (x < mSlop)) {
    323                 setCenter((int) x, (int) y);
    324                 show(true);
    325                 return true;
    326             }
    327         } else if (MotionEvent.ACTION_UP == action) {
    328             if (mOpen) {
    329                 boolean handled = false;
    330                 if (mPieView != null) {
    331                     handled = mPieView.onTouchEvent(evt);
    332                 }
    333                 PieItem item = mCurrentItem;
    334                 deselect();
    335                 show(false);
    336                 if (!handled && (item != null)) {
    337                     item.getView().performClick();
    338                 }
    339                 return true;
    340             }
    341         } else if (MotionEvent.ACTION_CANCEL == action) {
    342             if (mOpen) {
    343                 show(false);
    344             }
    345             deselect();
    346             return false;
    347         } else if (MotionEvent.ACTION_MOVE == action) {
    348             boolean handled = false;
    349             PointF polar = getPolar(x, y);
    350             int maxr = mRadius + mLevels * mRadiusInc + 50;
    351             if (mPieView != null) {
    352                 handled = mPieView.onTouchEvent(evt);
    353             }
    354             if (handled) {
    355                 invalidate();
    356                 return false;
    357             }
    358             if (polar.y > maxr) {
    359                 deselect();
    360                 show(false);
    361                 evt.setAction(MotionEvent.ACTION_DOWN);
    362                 if (getParent() != null) {
    363                     ((ViewGroup) getParent()).dispatchTouchEvent(evt);
    364                 }
    365                 return false;
    366             }
    367             PieItem item = findItem(polar);
    368             if (mCurrentItem != item) {
    369                 onEnter(item);
    370                 if ((item != null) && item.isPieView()) {
    371                     int cx = item.getView().getLeft() + (onTheLeft()
    372                             ? item.getView().getWidth() : 0);
    373                     int cy = item.getView().getTop();
    374                     mPieView = item.getPieView();
    375                     layoutPieView(mPieView, cx, cy,
    376                             (item.getStartAngle() + item.getSweep()) / 2);
    377                 }
    378                 invalidate();
    379             }
    380         }
    381         // always re-dispatch event
    382         return false;
    383     }
    384 
    385     private void layoutPieView(PieView pv, int x, int y, float angle) {
    386         pv.layout(x, y, onTheLeft(), angle);
    387     }
    388 
    389     /**
    390      * enter a slice for a view
    391      * updates model only
    392      * @param item
    393      */
    394     private void onEnter(PieItem item) {
    395         // deselect
    396         if (mCurrentItem != null) {
    397             mCurrentItem.setSelected(false);
    398         }
    399         if (item != null) {
    400             // clear up stack
    401             playSoundEffect(SoundEffectConstants.CLICK);
    402             item.setSelected(true);
    403             mPieView = null;
    404         }
    405         mCurrentItem = item;
    406     }
    407 
    408     private void deselect() {
    409         if (mCurrentItem != null) {
    410             mCurrentItem.setSelected(false);
    411         }
    412         mCurrentItem = null;
    413         mPieView = null;
    414     }
    415 
    416     private PointF getPolar(float x, float y) {
    417         PointF res = new PointF();
    418         // get angle and radius from x/y
    419         res.x = (float) Math.PI / 2;
    420         x = mCenter.x - x;
    421         if (mCenter.x < mSlop) {
    422             x = -x;
    423         }
    424         y = mCenter.y - y;
    425         res.y = (float) Math.sqrt(x * x + y * y);
    426         if (y > 0) {
    427             res.x = (float) Math.asin(x / res.y);
    428         } else if (y < 0) {
    429             res.x = (float) (Math.PI - Math.asin(x / res.y ));
    430         }
    431         return res;
    432     }
    433 
    434     /**
    435      *
    436      * @param polar x: angle, y: dist
    437      * @return the item at angle/dist or null
    438      */
    439     private PieItem findItem(PointF polar) {
    440         // find the matching item:
    441         for (PieItem item : mItems) {
    442             if ((item.getInnerRadius() - mTouchOffset < polar.y)
    443                     && (item.getOuterRadius() - mTouchOffset > polar.y)
    444                     && (item.getStartAngle() < polar.x)
    445                     && (item.getStartAngle() + item.getSweep() > polar.x)) {
    446                 return item;
    447             }
    448         }
    449         return null;
    450     }
    451 
    452 }
    453