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