• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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          */
onOpen()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 {
onLayout(int ax, int ay, boolean left)58             public void onLayout(int ax, int ay, boolean left);
59         }
60 
setLayoutListener(OnLayoutListener l)61         public void setLayoutListener(OnLayoutListener l);
62 
layout(int anchorX, int anchorY, boolean onleft, float angle)63         public void layout(int anchorX, int anchorY, boolean onleft, float angle);
64 
draw(Canvas c)65         public void draw(Canvas c);
66 
onTouchEvent(MotionEvent evt)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      */
PieMenu(Context context, AttributeSet attrs, int defStyle)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      */
PieMenu(Context context, AttributeSet attrs)108     public PieMenu(Context context, AttributeSet attrs) {
109         super(context, attrs);
110         init(context);
111     }
112 
113     /**
114      * @param context
115      */
PieMenu(Context context)116     public PieMenu(Context context) {
117         super(context);
118         init(context);
119     }
120 
init(Context ctx)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 
setController(PieController ctl)143     public void setController(PieController ctl) {
144         mController = ctl;
145     }
146 
setUseBackground(boolean useBackground)147     public void setUseBackground(boolean useBackground) {
148         mUseBackground = useBackground;
149     }
150 
addItem(PieItem item)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 
removeItem(PieItem item)159     public void removeItem(PieItem item) {
160         mItems.remove(item);
161     }
162 
clearItems()163     public void clearItems() {
164         mItems.clear();
165     }
166 
onTheLeft()167     private boolean onTheLeft() {
168         return mCenter.x < mSlop;
169     }
170 
171     /**
172      * guaranteed has center set
173      * @param show
174      */
show(boolean show)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 
setCenter(int x, int y)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 
layoutPie()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      */
getDegrees(double angle)247     private float getDegrees(double angle) {
248         return (float) (270 - 180 * angle / Math.PI);
249     }
250 
251     @Override
onDraw(Canvas canvas)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 
drawItem(Canvas canvas, PieItem item)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 
drawPath(Canvas canvas, Path path, Paint paint)296     private void drawPath(Canvas canvas, Path path, Paint paint) {
297         canvas.drawPath(path, paint);
298     }
299 
makeSlice(float start, float end, int outer, int inner, Point center)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
onTouchEvent(MotionEvent evt)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 
layoutPieView(PieView pv, int x, int y, float angle)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      */
onEnter(PieItem item)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 
deselect()408     private void deselect() {
409         if (mCurrentItem != null) {
410             mCurrentItem.setSelected(false);
411         }
412         mCurrentItem = null;
413         mPieView = null;
414     }
415 
getPolar(float x, float y)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      */
findItem(PointF polar)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