PieRenderer.java revision c605826581f2ef1640828af82dbf26a70d4c7c78
1/*
2 * Copyright (C) 2012 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.camera.ui;
18
19import android.annotation.TargetApi;
20import android.content.Context;
21import android.content.res.Resources;
22import android.graphics.Canvas;
23import android.graphics.Color;
24import android.graphics.Paint;
25import android.graphics.Path;
26import android.graphics.Point;
27import android.graphics.PointF;
28import android.graphics.RectF;
29import android.os.Handler;
30import android.os.Message;
31import android.util.Log;
32import android.view.MotionEvent;
33import android.view.View;
34import android.view.animation.Animation;
35import android.view.animation.Animation.AnimationListener;
36import android.view.animation.Transformation;
37
38import com.android.camera.R;
39import com.android.gallery3d.common.ApiHelper;
40
41import java.util.ArrayList;
42import java.util.List;
43
44public class PieRenderer extends OverlayRenderer {
45
46    private static final String TAG = "CAM Pie";
47
48    private static final long PIE_OPEN_DELAY = 200;
49
50    private static final int MSG_OPEN = 2;
51    private static final int MSG_CLOSE = 3;
52    private static final int MSG_SUBMENU = 4;
53    private static final float PIE_SWEEP = (float)(Math.PI * 2 / 3);
54    // geometry
55    private Point mCenter;
56    private int mRadius;
57    private int mRadiusInc;
58    private int mSlop;
59    // the detection if touch is inside a slice is offset
60    // inbounds by this amount to allow the selection to show before the
61    // finger covers it
62    private int mTouchOffset;
63
64    private List<PieItem> mItems;
65
66    private PieItem mOpenItem;
67
68    private Paint mNormalPaint;
69    private Paint mSelectedPaint;
70    private Paint mSubPaint;
71
72    // touch handling
73    private PieItem mCurrentItem;
74
75    private boolean mAnimating;
76    private float mAlpha;
77
78    private Handler mHandler = new Handler() {
79        public void handleMessage(Message msg) {
80            switch(msg.what) {
81            case MSG_OPEN:
82                if (mListener != null && !mAnimating) {
83                    mListener.onPieOpened(mCenter.x, mCenter.y);
84                }
85                break;
86            case MSG_CLOSE:
87                if (mListener != null && !mAnimating) {
88                    mListener.onPieClosed();
89                }
90                break;
91            case MSG_SUBMENU:
92                openCurrentItem();
93                break;
94            }
95        }
96    };
97
98    private PieListener mListener;
99
100    static public interface PieListener {
101        public void onPieOpened(int centerX, int centerY);
102        public void onPieClosed();
103    }
104
105    public void setPieListener(PieListener pl) {
106        mListener = pl;
107    }
108
109    public PieRenderer(Context context) {
110        init(context);
111    }
112    private void init(Context ctx) {
113        setVisible(false);
114        mItems = new ArrayList<PieItem>();
115        Resources res = ctx.getResources();
116        mRadius = (int) res.getDimensionPixelSize(R.dimen.pie_radius_start);
117        mRadiusInc =  (int) res.getDimensionPixelSize(R.dimen.pie_radius_increment);
118        mSlop = (int) res.getDimensionPixelSize(R.dimen.pie_touch_slop);
119        mTouchOffset = (int) res.getDimensionPixelSize(R.dimen.pie_touch_offset);
120        mCenter = new Point(0,0);
121        mNormalPaint = new Paint();
122        mNormalPaint.setColor(Color.argb(0, 0, 0, 0));
123        mNormalPaint.setAntiAlias(true);
124        mSelectedPaint = new Paint();
125        mSelectedPaint.setColor(Color.argb(128, 0, 0, 0)); //res.getColor(R.color.qc_selected));
126        mSelectedPaint.setAntiAlias(true);
127        mSubPaint = new Paint();
128        mSubPaint.setAntiAlias(true);
129        mSubPaint.setColor(Color.argb(200, 250, 230, 128)); //res.getColor(R.color.qc_sub));
130    }
131
132    public void addItem(PieItem item) {
133        // add the item to the pie itself
134        mItems.add(item);
135    }
136
137    public void removeItem(PieItem item) {
138        mItems.remove(item);
139    }
140
141    public void clearItems() {
142        mItems.clear();
143    }
144
145    public void fade() {
146        Animation anim = new AlphaAnimation();
147        anim.setFillAfter(true);
148        anim.setAnimationListener(new AnimationListener() {
149            @Override
150            public void onAnimationStart(Animation animation) {
151                mAnimating = true;
152                update();
153            }
154            @Override
155            public void onAnimationEnd(Animation animation) {
156                show(false);
157                mAlpha = 0f;
158                mAnimating = false;
159                setViewAlpha(mOverlay, 1);
160            }
161            @Override
162            public void onAnimationRepeat(Animation animation) {
163            }
164        });
165        anim.reset();
166        anim.setDuration(500);
167        show(true);
168        mOverlay.startAnimation(anim);
169    }
170
171    /**
172     * guaranteed has center set
173     * @param show
174     */
175    private void show(boolean show) {
176        if (show) {
177            // ensure clean state
178            mAnimating = false;
179            mCurrentItem = null;
180            mOpenItem = null;
181            for (PieItem item : mItems) {
182                item.setSelected(false);
183            }
184            layoutPie();
185        }
186        setVisible(show);
187        mHandler.sendEmptyMessage(show ? MSG_OPEN : MSG_CLOSE);
188    }
189
190    public void setCenter(int x, int y) {
191        mCenter.x = x;
192        mCenter.y = y;
193    }
194
195    private void layoutPie() {
196        int rgap = 2;
197        int inner = mRadius + rgap;
198        int outer = mRadius + mRadiusInc - rgap;
199        int gap = 1;
200        layoutItems(mItems, (float) (Math.PI / 2), inner, outer, gap);
201    }
202
203    private void layoutItems(List<PieItem> items, float centerAngle, int inner,
204            int outer, int gap) {
205        float emptyangle = PIE_SWEEP / 16;
206        float sweep = (float) (PIE_SWEEP - 2 * emptyangle) / items.size();
207        float angle = centerAngle - PIE_SWEEP / 2 + emptyangle + sweep / 2;
208        // check if we have custom geometry
209        // first item we find triggers custom sweep for all
210        // this allows us to re-use the path
211        for (PieItem item : items) {
212            if (item.getCenter() >= 0) {
213                sweep = item.getSweep();
214                break;
215            }
216        }
217        Path path = makeSlice(getDegrees(0) - gap, getDegrees(sweep) + gap,
218                outer, inner, mCenter);
219        for (PieItem item : items) {
220            // shared between items
221            item.setPath(path);
222            View view = item.getView();
223            if (item.getCenter() >= 0) {
224                angle = item.getCenter();
225            }
226            if (view != null) {
227                view.measure(view.getLayoutParams().width,
228                        view.getLayoutParams().height);
229                int w = view.getMeasuredWidth();
230                int h = view.getMeasuredHeight();
231                // move views to outer border
232                int r = inner + (outer - inner) * 2 / 3;
233                int x = (int) (r * Math.cos(angle));
234                int y = mCenter.y - (int) (r * Math.sin(angle)) - h / 2;
235                x = mCenter.x + x - w / 2;
236                view.layout(x, y, x + w, y + h);
237            }
238            float itemstart = angle - sweep / 2;
239            item.setGeometry(itemstart, sweep, inner, outer);
240            if (item.hasItems()) {
241                layoutItems(item.getItems(), angle, inner,
242                        outer + mRadiusInc / 2, gap);
243            }
244            angle += sweep;
245        }
246    }
247
248    private Path makeSlice(float start, float end, int outer, int inner, Point center) {
249        outer = inner + (outer - inner) * 2 / 3;
250        RectF bb =
251                new RectF(center.x - outer, center.y - outer, center.x + outer,
252                        center.y + outer);
253        RectF bbi =
254                new RectF(center.x - inner, center.y - inner, center.x + inner,
255                        center.y + inner);
256        Path path = new Path();
257        path.arcTo(bb, start, end - start, true);
258        path.arcTo(bbi, end, start - end);
259        path.close();
260        return path;
261    }
262
263    /**
264     * converts a
265     * @param angle from 0..PI to Android degrees (clockwise starting at 3 o'clock)
266     * @return skia angle
267     */
268    private float getDegrees(double angle) {
269        return (float) (360 - 180 * angle / Math.PI);
270    }
271
272    @Override
273    public void onDraw(Canvas canvas) {
274        if (mAnimating) {
275            setViewAlpha(mOverlay, mAlpha);
276        }
277        if (mOpenItem == null) {
278            // draw base menu
279            for (PieItem item : mItems) {
280                drawItem(canvas, item);
281            }
282        } else {
283            for (PieItem inner : mOpenItem.getItems()) {
284                drawItem(canvas, inner);
285            }
286        }
287    }
288
289    private void drawItem(Canvas canvas, PieItem item) {
290        if (item.getView() != null) {
291            Paint p = item.isSelected() ? mSelectedPaint : mNormalPaint;
292            int state = canvas.save();
293            float r = getDegrees(item.getStartAngle());
294            canvas.rotate(r, mCenter.x, mCenter.y);
295            canvas.drawPath(item.getPath(), p);
296            canvas.restoreToCount(state);
297            // draw the item view
298            View view = item.getView();
299            state = canvas.save();
300            canvas.translate(view.getX(), view.getY());
301            view.draw(canvas);
302            canvas.restoreToCount(state);
303        }
304    }
305
306    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
307    private void setViewAlpha(View v, float alpha) {
308        if (ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES) {
309            v.setAlpha(alpha);
310        }
311    }
312
313    // touch handling for pie
314
315    @Override
316    public boolean onTouchEvent(MotionEvent evt) {
317        float x = evt.getX();
318        float y = evt.getY();
319        int action = evt.getActionMasked();
320        if (MotionEvent.ACTION_DOWN == action) {
321            setCenter((int) x, (int) y);
322            show(true);
323            return true;
324        } else if (MotionEvent.ACTION_UP == action) {
325            if (isVisible()) {
326                PieItem item = mCurrentItem;
327                if (!mAnimating) {
328                    deselect();
329                }
330                show(false);
331                if ((item != null) && (item.getView() != null)) {
332                    if ((item == mOpenItem) || !mAnimating) {
333                        item.getView().performClick();
334                    }
335                }
336                return true;
337            }
338        } else if (MotionEvent.ACTION_CANCEL == action) {
339            if (isVisible()) {
340                show(false);
341            }
342            if (!mAnimating) {
343                deselect();
344            }
345            return false;
346        } else if (MotionEvent.ACTION_MOVE == action) {
347            if (mAnimating) return false;
348            PointF polar = getPolar(x, y);
349            int maxr = mRadius + mRadiusInc + 50;
350            if (polar.y < mRadius) {
351                if (mOpenItem != null) {
352                    mOpenItem = null;
353                } else if (!mAnimating) {
354                    deselect();
355                }
356                return false;
357            }
358            if (polar.y > maxr) {
359                deselect();
360                show(false);
361                evt.setAction(MotionEvent.ACTION_DOWN);
362                return false;
363            }
364            PieItem item = findItem(polar);
365            if (item == null) {
366            } else if (mCurrentItem != item) {
367                onEnter(item);
368            }
369        }
370        return false;
371    }
372
373    /**
374     * enter a slice for a view
375     * updates model only
376     * @param item
377     */
378    private void onEnter(PieItem item) {
379        if (mCurrentItem != null) {
380            mCurrentItem.setSelected(false);
381        }
382        if (item != null && item.isEnabled()) {
383            item.setSelected(true);
384            mCurrentItem = item;
385            if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) {
386                mHandler.sendEmptyMessageDelayed(MSG_SUBMENU, PIE_OPEN_DELAY);
387            }
388        } else {
389            mCurrentItem = null;
390        }
391    }
392
393    private void deselect() {
394        if (mCurrentItem != null) {
395            mCurrentItem.setSelected(false);
396            mHandler.removeMessages(MSG_SUBMENU);
397        }
398        if (mOpenItem != null) {
399            mOpenItem = null;
400        }
401        mCurrentItem = null;
402    }
403
404    private void openCurrentItem() {
405        if ((mCurrentItem != null) && mCurrentItem.hasItems()) {
406            mOpenItem = mCurrentItem;
407        }
408    }
409
410    private PointF getPolar(float x, float y) {
411        PointF res = new PointF();
412        // get angle and radius from x/y
413        res.x = (float) Math.PI / 2;
414        x = x - mCenter.x;
415        y = mCenter.y - y;
416        res.y = (float) Math.sqrt(x * x + y * y);
417        if (x != 0) {
418            res.x = (float) Math.atan2(y,  x);
419            if (res.x < 0) {
420                res.x = (float) (2 * Math.PI + res.x);
421            }
422        }
423        res.y = res.y + mTouchOffset;
424        return res;
425    }
426
427    /**
428     * @param polar x: angle, y: dist
429     * @return the item at angle/dist or null
430     */
431    private PieItem findItem(PointF polar) {
432        // find the matching item:
433        List<PieItem> items = (mOpenItem != null) ? mOpenItem.getItems() : mItems;
434        for (PieItem item : items) {
435            if (inside(polar, item)) {
436                return item;
437            }
438        }
439        return null;
440    }
441
442    private boolean inside(PointF polar, PieItem item) {
443        return (item.getInnerRadius() < polar.y)
444        && (item.getOuterRadius() > polar.y)
445        && (item.getStartAngle() < polar.x)
446        && (item.getStartAngle() + item.getSweep() > polar.x);
447    }
448
449    @Override
450    public boolean handlesTouch() {
451        return true;
452    }
453
454    @Override
455    public void layout(int l, int t, int r, int b) {
456        super.layout(l, t, r, b);
457    }
458
459    private class AlphaAnimation extends Animation {
460        @Override
461        protected void applyTransformation(float interpolatedTime, Transformation t) {
462            mAlpha = 1 - interpolatedTime;
463        }
464    }
465
466}
467