PieMenu.java revision 0860d99a463f7645bcc9aaa246fd8852e90dbb5d
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.Point;
25import android.graphics.PointF;
26import android.graphics.drawable.Drawable;
27import android.util.AttributeSet;
28import android.view.MotionEvent;
29import android.view.SoundEffectConstants;
30import android.view.View;
31import android.view.ViewGroup;
32import android.widget.FrameLayout;
33
34import java.util.ArrayList;
35import java.util.List;
36
37public class PieMenu extends FrameLayout {
38
39    private static final int MAX_LEVELS = 5;
40
41    public interface PieController {
42        /**
43         * called before menu opens to customize menu
44         * returns if pie state has been changed
45         */
46        public boolean onOpen();
47    }
48
49    private Point mCenter;
50    private int mRadius;
51    private int mRadiusInc;
52    private int mSlop;
53
54    private boolean mOpen;
55    private PieController mController;
56
57    private List<PieItem> mItems;
58    private int mLevels;
59    private int[] mCounts;
60
61    private Drawable mBackground;
62
63    // touch handling
64    PieItem mCurrentItem;
65
66    /**
67     * @param context
68     * @param attrs
69     * @param defStyle
70     */
71    public PieMenu(Context context, AttributeSet attrs, int defStyle) {
72        super(context, attrs, defStyle);
73        init(context);
74    }
75
76    /**
77     * @param context
78     * @param attrs
79     */
80    public PieMenu(Context context, AttributeSet attrs) {
81        super(context, attrs);
82        init(context);
83    }
84
85    /**
86     * @param context
87     */
88    public PieMenu(Context context) {
89        super(context);
90        init(context);
91    }
92
93    private void init(Context ctx) {
94        mItems = new ArrayList<PieItem>();
95        mLevels = 0;
96        mCounts = new int[MAX_LEVELS];
97        Resources res = ctx.getResources();
98        mRadius = (int) res.getDimension(R.dimen.qc_radius_start);
99        mRadiusInc = (int) res.getDimension(R.dimen.qc_radius_increment);
100        mSlop = (int) res.getDimension(R.dimen.qc_slop);
101        mOpen = false;
102        setWillNotDraw(false);
103        setDrawingCacheEnabled(false);
104        mCenter = new Point(0,0);
105        mBackground = res.getDrawable(R.drawable.qc_background_normal);
106    }
107
108    public void setController(PieController ctl) {
109        mController = ctl;
110    }
111
112    public void addItem(PieItem item) {
113        // add the item to the pie itself
114        mItems.add(item);
115        int l = item.getLevel();
116        mLevels = Math.max(mLevels, l);
117        mCounts[l]++;
118    }
119
120    public void removeItem(PieItem item) {
121        mItems.remove(item);
122    }
123
124    public void clearItems() {
125        mItems.clear();
126    }
127
128    private boolean onTheLeft() {
129        return mCenter.x < mSlop;
130    }
131
132    /**
133     * guaranteed has center set
134     * @param show
135     */
136    private void show(boolean show) {
137        mOpen = show;
138        if (mOpen) {
139            if (mController != null) {
140                boolean changed = mController.onOpen();
141            }
142            layoutPie();
143        }
144        if (!show) {
145            mCurrentItem = null;
146        }
147        invalidate();
148    }
149
150    private void setCenter(int x, int y) {
151        if (x < mSlop) {
152            mCenter.x = 0;
153        } else {
154            mCenter.x = getWidth();
155        }
156        mCenter.y = y;
157    }
158
159    private void layoutPie() {
160        int inner = mRadius;
161        int outer = mRadius + mRadiusInc;
162        for (int i = 0; i < mLevels; i++) {
163            int level = i + 1;
164            float sweep = (float) Math.PI / (mCounts[level] + 1);
165            float angle = sweep;
166            for (PieItem item : mItems) {
167                if (item.getLevel() == level) {
168                    View view = item.getView();
169                    view.measure(view.getLayoutParams().width,
170                            view.getLayoutParams().height);
171                    int w = view.getMeasuredWidth();
172                    int h = view.getMeasuredHeight();
173                    int x = (int) (outer * Math.sin(angle));
174                    int y = mCenter.y - (int) (outer * Math.cos(angle)) - h / 2;
175                    if (onTheLeft()) {
176                        x = mCenter.x + x - w;
177                    } else {
178                        x = mCenter.x - x;
179                    }
180                    view.layout(x, y, x + w, y + h);
181                    float itemstart = angle - sweep / 2;
182                    item.setGeometry(itemstart, sweep, inner, outer);
183                    angle += sweep;
184                }
185            }
186            inner += mRadiusInc;
187            outer += mRadiusInc;
188        }
189    }
190
191    @Override
192    protected void onDraw(Canvas canvas) {
193        if (mOpen) {
194            int w = mBackground.getIntrinsicWidth();
195            int h = mBackground.getIntrinsicHeight();
196            int left = mCenter.x - w;
197            int top = mCenter.y - h / 2;
198            mBackground.setBounds(left, top, left + w, top + h);
199            int state = canvas.save();
200            if (onTheLeft()) {
201                canvas.scale(-1, 1);
202            }
203            mBackground.draw(canvas);
204            canvas.restoreToCount(state);
205            for (PieItem item : mItems) {
206                drawItem(canvas, item);
207            }
208        }
209    }
210
211    private void drawItem(Canvas canvas, PieItem item) {
212        int outer = item.getOuterRadius();
213        int left = mCenter.x - outer;
214        int top = mCenter.y - outer;
215        // draw the item view
216        View view = item.getView();
217        int state = canvas.save();
218        canvas.translate(view.getX(), view.getY());
219        view.draw(canvas);
220        canvas.restoreToCount(state);
221    }
222
223    // touch handling for pie
224
225    @Override
226    public boolean onTouchEvent(MotionEvent evt) {
227        float x = evt.getX();
228        float y = evt.getY();
229        int action = evt.getActionMasked();
230        int edges = evt.getEdgeFlags();
231        if (MotionEvent.ACTION_DOWN == action) {
232            if ((x > getWidth() - mSlop) || (x < mSlop)) {
233                setCenter((int) x, (int) y);
234                show(true);
235                return true;
236            }
237        } else if (MotionEvent.ACTION_UP == action) {
238            if (mOpen) {
239                PieItem item = mCurrentItem;
240                deselect();
241                show(false);
242                if (item != null) {
243                    item.getView().performClick();
244                }
245                return true;
246            }
247        } else if (MotionEvent.ACTION_CANCEL == action) {
248            if (mOpen) {
249                show(false);
250            }
251            deselect();
252            return false;
253        } else if (MotionEvent.ACTION_MOVE == action) {
254            boolean handled = false;
255            PointF polar = getPolar(x, y);
256            int maxr = mRadius + mLevels * mRadiusInc + 50;
257            if (polar.y > maxr) {
258                deselect();
259                show(false);
260                evt.setAction(MotionEvent.ACTION_DOWN);
261                if (getParent() != null) {
262                    ((ViewGroup) getParent()).dispatchTouchEvent(evt);
263                }
264                return false;
265            }
266            PieItem item = findItem(polar);
267            if (mCurrentItem != item) {
268                onEnter(item);
269                invalidate();
270            }
271        }
272        // always re-dispatch event
273        return false;
274    }
275
276    /**
277     * enter a slice for a view
278     * updates model only
279     * @param item
280     */
281    private void onEnter(PieItem item) {
282        // deselect
283        if (mCurrentItem != null) {
284            mCurrentItem.setSelected(false);
285        }
286        if (item != null) {
287            // clear up stack
288            playSoundEffect(SoundEffectConstants.CLICK);
289            item.setSelected(true);
290        }
291        mCurrentItem = item;
292    }
293
294    private void deselect() {
295        if (mCurrentItem != null) {
296            mCurrentItem.setSelected(false);
297        }
298        mCurrentItem = null;
299    }
300
301    private PointF getPolar(float x, float y) {
302        PointF res = new PointF();
303        // get angle and radius from x/y
304        res.x = (float) Math.PI / 2;
305        x = mCenter.x - x;
306        if (mCenter.x < mSlop) {
307            x = -x;
308        }
309        y = mCenter.y - y;
310        res.y = (float) Math.sqrt(x * x + y * y);
311        if (y > 0) {
312            res.x = (float) Math.asin(x / res.y);
313        } else if (y < 0) {
314            res.x = (float) (Math.PI - Math.asin(x / res.y ));
315        }
316        return res;
317    }
318
319    /**
320     *
321     * @param polar x: angle, y: dist
322     * @return the item at angle/dist or null
323     */
324    private PieItem findItem(PointF polar) {
325        // find the matching item:
326        for (PieItem item : mItems) {
327            if ((item.getInnerRadius() < polar.y)
328                    && (item.getOuterRadius() > polar.y)
329                    && (item.getStartAngle() < polar.x)
330                    && (item.getStartAngle() + item.getSweep() > polar.x)) {
331                return item;
332            }
333        }
334        return null;
335    }
336
337}
338