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