PieMenu.java revision 2a56ecaf153d788a1acebc54b670347a1a58d693
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
17package com.android.browser.view;
18
19import com.android.browser.R;
20
21import android.content.Context;
22import android.content.res.Resources;
23import android.graphics.Bitmap;
24import android.graphics.BitmapShader;
25import android.graphics.Canvas;
26import android.graphics.Matrix;
27import android.graphics.Paint;
28import android.graphics.Path;
29import android.graphics.Point;
30import android.graphics.PointF;
31import android.graphics.Rect;
32import android.graphics.RectF;
33import android.graphics.Shader;
34import android.graphics.drawable.Drawable;
35import android.util.AttributeSet;
36import android.view.MotionEvent;
37import android.view.SoundEffectConstants;
38import android.view.View;
39import android.view.ViewGroup;
40import android.widget.FrameLayout;
41
42import java.util.ArrayList;
43import java.util.HashMap;
44import java.util.List;
45import java.util.Map;
46
47public class PieMenu extends FrameLayout {
48
49    private static final int RADIUS_GAP = 10;
50
51    public interface PieController {
52        /**
53         * called before menu opens to customize menu
54         * returns if pie state has been changed
55         */
56        public boolean onOpen();
57    }
58    private Point mCenter;
59    private int mRadius;
60    private int mRadiusInc;
61    private int mSlop;
62
63    private boolean mOpen;
64    private Paint mPaint;
65    private Paint mSelectedPaint;
66    private PieController mController;
67
68    private Map<View, List<View>> mMenu;
69    private List<View> mStack;
70
71    private boolean mDirty;
72
73    private Drawable mActiveDrawable;
74    private Drawable mInactiveDrawable;
75    private final Paint mActiveShaderPaint = new Paint();
76    private final Paint mInactiveShaderPaint = new Paint();
77    private final Matrix mActiveMatrix = new Matrix();
78    private final Matrix mInactiveMatrix = new Matrix();
79
80    private BitmapShader mActiveShader;
81    private BitmapShader mInactiveShader;
82
83
84    /**
85     * @param context
86     * @param attrs
87     * @param defStyle
88     */
89    public PieMenu(Context context, AttributeSet attrs, int defStyle) {
90        super(context, attrs, defStyle);
91        init(context);
92    }
93
94    /**
95     * @param context
96     * @param attrs
97     */
98    public PieMenu(Context context, AttributeSet attrs) {
99        super(context, attrs);
100        init(context);
101    }
102
103    /**
104     * @param context
105     */
106    public PieMenu(Context context) {
107        super(context);
108        init(context);
109    }
110
111    private void init(Context ctx) {
112        this.setTag(new MenuTag(0));
113        mStack = new ArrayList<View>();
114        mStack.add(this);
115        Resources res = ctx.getResources();
116        mRadius = (int) res.getDimension(R.dimen.qc_radius);
117        mRadiusInc = (int) res.getDimension(R.dimen.qc_radius_inc);
118        mSlop = (int) res.getDimension(R.dimen.qc_slop);
119        mPaint = new Paint();
120        mPaint.setAntiAlias(true);
121        mPaint.setColor(res.getColor(R.color.qc_slice_normal));
122        mSelectedPaint = new Paint();
123        mSelectedPaint.setAntiAlias(true);
124        mSelectedPaint.setColor(res.getColor(R.color.qc_slice_active));
125        mOpen = false;
126        mMenu = new HashMap<View, List<View>>();
127        setWillNotDraw(false);
128        setDrawingCacheEnabled(false);
129        mCenter = new Point(0,0);
130        mDirty = true;
131        mActiveShaderPaint.setStyle(Paint.Style.FILL);
132        mActiveShaderPaint.setAntiAlias(true);
133
134        mInactiveShaderPaint.setStyle(Paint.Style.FILL);
135        mInactiveShaderPaint.setAntiAlias(true);
136        mActiveDrawable = res.getDrawable(R.drawable.qc_background_selected);
137        mInactiveDrawable = res.getDrawable(R.drawable.qc_background_normal);
138
139        Bitmap activeTexture = getDrawableAsBitmap(mActiveDrawable,
140                mActiveDrawable.getIntrinsicWidth(),
141                mActiveDrawable.getIntrinsicHeight());
142        Bitmap inactiveTexture = getDrawableAsBitmap(mInactiveDrawable,
143                mInactiveDrawable.getIntrinsicWidth(),
144                mInactiveDrawable.getIntrinsicHeight());
145
146        mActiveShader = new BitmapShader(activeTexture,
147                Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
148        mActiveShaderPaint.setShader(mActiveShader);
149
150        mInactiveShader = new BitmapShader(inactiveTexture,
151                Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
152        mInactiveShaderPaint.setShader(mInactiveShader);
153
154    }
155
156    private static Bitmap getDrawableAsBitmap(Drawable drawable, int width, int height) {
157        Bitmap b = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
158        Canvas c = new Canvas(b);
159        drawable.setBounds(0, 0, width, height);
160        drawable.draw(c);
161        return b;
162    }
163
164    public void setController(PieController ctl) {
165        mController = ctl;
166    }
167
168    public void setRadius(int r) {
169        mRadius = r;
170        requestLayout();
171    }
172
173    public void setRadiusIncrement(int ri) {
174        mRadiusInc = ri;
175        requestLayout();
176    }
177
178    /**
179     * add a menu item to another item as a submenu
180     * @param item
181     * @param parent
182     */
183    public void addItem(View item, View parent) {
184        List<View> subs = mMenu.get(parent);
185        if (subs == null) {
186            subs = new ArrayList<View>();
187            mMenu.put(parent, subs);
188        }
189        subs.add(item);
190        MenuTag tag = new MenuTag(((MenuTag) parent.getTag()).level + 1);
191        item.setTag(tag);
192    }
193
194    public void addItem(View view) {
195        // add the item to the pie itself
196        addItem(view, this);
197    }
198
199    public void removeItem(View view) {
200        List<View> subs = mMenu.get(view);
201        mMenu.remove(view);
202        for (View p : mMenu.keySet()) {
203            List<View> sl = mMenu.get(p);
204            if (sl != null) {
205                sl.remove(view);
206            }
207        }
208    }
209
210    public void clearItems(View parent) {
211        List<View> subs = mMenu.remove(parent);
212        if (subs != null) {
213            for (View sub: subs) {
214                clearItems(sub);
215            }
216        }
217    }
218
219    public void clearItems() {
220        mMenu.clear();
221    }
222
223
224    public void show(boolean show) {
225        mOpen = show;
226        if (mOpen) {
227            if (mController != null) {
228                boolean changed = mController.onOpen();
229            }
230            mDirty = true;
231        }
232        if (!show) {
233            // hide sub items
234            mStack.clear();
235            mStack.add(this);
236        }
237        invalidate();
238    }
239
240    private void setCenter(int x, int y) {
241        if (x < mSlop) {
242            mCenter.x = 0;
243        } else {
244            mCenter.x = getWidth();
245        }
246        mCenter.y = y;
247    }
248
249    private boolean onTheLeft() {
250        return mCenter.x < mSlop;
251    }
252
253    @Override
254    protected void onDraw(Canvas canvas) {
255        if (mOpen) {
256            int radius = mRadius;
257            // start in the center for 0 level menu
258            float anchor = (float) Math.PI / 2;
259            PointF angles = new PointF();
260            int state = canvas.save();
261            if (onTheLeft()) {
262                // left handed
263                canvas.scale(-1, 1);
264            }
265            for (View parent : mStack) {
266                List<View> subs = mMenu.get(parent);
267                if (subs != null) {
268                    setGeometry(anchor, subs.size(), angles);
269                }
270                anchor = drawSlices(canvas, subs, radius, angles.x, angles.y);
271                radius += mRadiusInc + RADIUS_GAP;
272            }
273            canvas.restoreToCount(state);
274            mDirty = false;
275        }
276    }
277
278    /**
279     * draw the set of slices
280     * @param canvas
281     * @param items
282     * @param radius
283     * @param start
284     * @param sweep
285     * @return the angle of the selected slice
286     */
287    private float drawSlices(Canvas canvas, List<View> items, int radius,
288            float start, float sweep) {
289        float angle = start + sweep / 2;
290        // gap between slices in degrees
291        float gap = 1f;
292        float newanchor = 0f;
293        for (View item : items) {
294            if (mDirty) {
295                item.measure(item.getLayoutParams().width,
296                        item.getLayoutParams().height);
297                int w = item.getMeasuredWidth();
298                int h = item.getMeasuredHeight();
299                int x = (int) (radius * Math.sin(angle));
300                int y =  mCenter.y - (int) (radius * Math.cos(angle)) - h / 2;
301                if (onTheLeft()) {
302                    x = mCenter.x + x - w / 2;
303                } else {
304                    x = mCenter.x - x - w / 2;
305                }
306                item.layout(x, y, x + w, y + h);
307            }
308            float itemstart = angle - sweep / 2;
309            int inner = radius - mRadiusInc / 2;
310            int outer = radius + mRadiusInc / 2;
311            Path slice = makeSlice(getDegrees(itemstart) - gap,
312                    getDegrees(itemstart + sweep) + gap,
313                    outer, inner, mCenter);
314            MenuTag tag = (MenuTag) item.getTag();
315            tag.start = itemstart;
316            tag.sweep = sweep;
317            tag.inner = inner;
318            tag.outer = outer;
319            int state = canvas.save();
320            int[] topLeft = new int[2];
321            getLocationInWindow(topLeft);
322            topLeft[0] = mCenter.x - outer;
323            topLeft[1] = mCenter.y - outer;
324            Paint paint = item.isPressed() ? mActiveShaderPaint : mInactiveShaderPaint;
325            drawClipped(canvas, paint, slice, topLeft, item.isPressed());
326            canvas.restoreToCount(state);
327            state = canvas.save();
328            if (onTheLeft()) {
329                canvas.scale(-1, 1);
330            }
331            canvas.translate(item.getX(), item.getY());
332            item.draw(canvas);
333            canvas.restoreToCount(state);
334            if (mStack.contains(item)) {
335                // item is anchor for sub menu
336                newanchor = angle;
337            }
338            angle += sweep;
339        }
340        return newanchor;
341    }
342
343    private void drawClipped(Canvas canvas, Paint paint, Path clipPath, int[] pos,
344            boolean selected) {
345        // TODO: We should change the matrix/shader only when needed
346        final Matrix matrix = selected ? mActiveMatrix : mInactiveMatrix;
347        matrix.setTranslate(pos[0], pos[1]);
348        (selected ? mActiveShader : mInactiveShader).setLocalMatrix(matrix);
349        canvas.drawPath(clipPath, paint);
350    }
351
352
353    /**
354     * converts a
355     * @param angle from 0..PI to Android degrees (clockwise starting at 3 o'clock)
356     * @return skia angle
357     */
358    private float getDegrees(double angle) {
359        return (float) (270 - 180 * angle / Math.PI);
360    }
361
362    private Path makeSlice(float startangle, float endangle, int outerradius,
363            int innerradius, Point center) {
364        RectF bb = new RectF(center.x - outerradius, center.y - outerradius,
365                center.x + outerradius, center.y + outerradius);
366        RectF bbi = new RectF(center.x - innerradius, center.y - innerradius,
367                center.x + innerradius, center.y + innerradius);
368        Path path = new Path();
369        path.arcTo(bb, startangle, endangle - startangle, true);
370        path.arcTo(bbi, endangle, startangle - endangle);
371        path.close();
372        return path;
373    }
374
375    /**
376     * all angles are 0 .. MATH.PI where 0 points up, and rotate counterclockwise
377     * set the startangle and slice sweep in result
378     * @param anchorangle : angle at which the menu is anchored
379     * @param nslices
380     * @param result : x : start, y : sweep
381     */
382    private void setGeometry(float anchorangle, int nslices, PointF result) {
383        float span = (float) Math.min(anchorangle, Math.PI - anchorangle);
384        float sweep = 2 * span / (nslices + 1);
385        result.x = anchorangle - span + sweep / 2;
386        result.y = sweep;
387    }
388
389    // touch handling for pie
390
391    View mCurrentView;
392    Rect mHitRect = new Rect();
393
394    @Override
395    public boolean onTouchEvent(MotionEvent evt) {
396        float x = evt.getX();
397        float y = evt.getY();
398        int action = evt.getActionMasked();
399        int edges = evt.getEdgeFlags();
400        if (MotionEvent.ACTION_DOWN == action) {
401            if ((x > getWidth() - mSlop) || (x < mSlop)) {
402                setCenter((int) x, (int) y);
403                show(true);
404                return true;
405            }
406        } else if (MotionEvent.ACTION_UP == action) {
407            if (mOpen) {
408                View v = mCurrentView;
409                deselect();
410                if (v != null) {
411                    v.performClick();
412                }
413                show(false);
414                return true;
415            }
416        } else if (MotionEvent.ACTION_CANCEL == action) {
417            if (mOpen) {
418                show(false);
419            }
420            deselect();
421            return false;
422        } else if (MotionEvent.ACTION_MOVE == action) {
423            PointF polar = getPolar(x, y);
424            if (polar.y > mRadius + 2 * mRadiusInc) {
425                show(false);
426                deselect();
427                evt.setAction(MotionEvent.ACTION_DOWN);
428                if (getParent() != null) {
429                    ((ViewGroup) getParent()).dispatchTouchEvent(evt);
430                }
431                return false;
432            }
433            View v = findView(polar);
434            if (mCurrentView != v) {
435                onEnter(v);
436                invalidate();
437            }
438        }
439        // always re-dispatch event
440        return false;
441    }
442
443    /**
444     * enter a slice for a view
445     * updates model only
446     * @param view
447     */
448    private void onEnter(View view) {
449        // deselect
450        if (mCurrentView != null) {
451            if (getLevel(mCurrentView) >= getLevel(view)) {
452                mCurrentView.setPressed(false);
453            }
454        }
455        if (view != null) {
456            // clear up stack
457            playSoundEffect(SoundEffectConstants.CLICK);
458            MenuTag tag = (MenuTag) view.getTag();
459            int i = mStack.size() - 1;
460            while (i > 0) {
461                View v = mStack.get(i);
462                if (((MenuTag) v.getTag()).level >= tag.level) {
463                    v.setPressed(false);
464                    mStack.remove(i);
465                } else {
466                    break;
467                }
468                i--;
469            }
470            List<View> items = mMenu.get(view);
471            if (items != null) {
472                mStack.add(view);
473                mDirty = true;
474            }
475            view.setPressed(true);
476        }
477        mCurrentView = view;
478    }
479
480    private void deselect() {
481        if (mCurrentView != null) {
482            mCurrentView.setPressed(false);
483        }
484        mCurrentView = null;
485    }
486
487    private int getLevel(View v) {
488        if (v == null) return -1;
489        return ((MenuTag) v.getTag()).level;
490    }
491
492    private PointF getPolar(float x, float y) {
493        PointF res = new PointF();
494        // get angle and radius from x/y
495        res.x = (float) Math.PI / 2;
496        x = mCenter.x - x;
497        if (mCenter.x < mSlop) {
498            x = -x;
499        }
500        y = mCenter.y - y;
501        res.y = (float) Math.sqrt(x * x + y * y);
502        if (y > 0) {
503            res.x = (float) Math.asin(x / res.y);
504        } else if (y < 0) {
505            res.x = (float) (Math.PI - Math.asin(x / res.y ));
506        }
507        return res;
508    }
509
510    /**
511     *
512     * @param polar x: angle, y: dist
513     * @return
514     */
515    private View findView(PointF polar) {
516        // find the matching item:
517        for (View parent : mStack) {
518            List<View> subs = mMenu.get(parent);
519            if (subs != null) {
520                for (View item : subs) {
521                    MenuTag tag = (MenuTag) item.getTag();
522                    if ((tag.inner < polar.y)
523                            && (tag.outer > polar.y)
524                            && (tag.start < polar.x)
525                            && (tag.start + tag.sweep > polar.x)) {
526                        return item;
527                    }
528                }
529            }
530        }
531        return null;
532    }
533
534    class MenuTag {
535
536        int level;
537        float start;
538        float sweep;
539        int inner;
540        int outer;
541
542        public MenuTag(int l) {
543            level = l;
544        }
545
546    }
547
548}
549