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    private boolean mUseBackground;
93
94    /**
95     * @param context
96     * @param attrs
97     * @param defStyle
98     */
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     */
108    public PieMenu(Context context, AttributeSet attrs) {
109        super(context, attrs);
110        init(context);
111    }
112
113    /**
114     * @param context
115     */
116    public PieMenu(Context context) {
117        super(context);
118        init(context);
119    }
120
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
143    public void setController(PieController ctl) {
144        mController = ctl;
145    }
146
147    public void setUseBackground(boolean useBackground) {
148        mUseBackground = useBackground;
149    }
150
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
159    public void removeItem(PieItem item) {
160        mItems.remove(item);
161    }
162
163    public void clearItems() {
164        mItems.clear();
165    }
166
167    private boolean onTheLeft() {
168        return mCenter.x < mSlop;
169    }
170
171    /**
172     * guaranteed has center set
173     * @param show
174     */
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
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
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     */
247    private float getDegrees(double angle) {
248        return (float) (270 - 180 * angle / Math.PI);
249    }
250
251    @Override
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
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
296    private void drawPath(Canvas canvas, Path path, Paint paint) {
297        canvas.drawPath(path, paint);
298    }
299
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
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
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     */
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
408    private void deselect() {
409        if (mCurrentItem != null) {
410            mCurrentItem.setSelected(false);
411        }
412        mCurrentItem = null;
413        mPieView = null;
414    }
415
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     */
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