PieRenderer.java revision 1404be918ef3e5e5150c13c5a89a66b83a816846
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        implements FocusIndicator {
46
47    private static final String TAG = "CAM Pie";
48
49    // Sometimes continuous autofocus starts and stops several times quickly.
50    // These states are used to make sure the animation is run for at least some
51    // time.
52    private int mState;
53    private ScaleAnimation mAnimation = new ScaleAnimation();
54    private static final int STATE_IDLE = 0;
55    private static final int STATE_FOCUSING = 1;
56    private static final int STATE_FINISHING = 2;
57    private static final int STATE_PIE = 3;
58
59    private Runnable mDisappear = new Disappear();
60    private Animation.AnimationListener mEndAction = new EndAction();
61    private static final int SCALING_UP_TIME = 1000;
62    private static final int SCALING_DOWN_TIME = 200;
63    private static final int DISAPPEAR_TIMEOUT = 200;
64    private static final int DIAL_HORIZONTAL = 157;
65
66    private static final long PIE_OPEN_DELAY = 200;
67
68    private static final int MSG_OPEN = 2;
69    private static final int MSG_CLOSE = 3;
70    private static final int MSG_SUBMENU = 4;
71    private static final float PIE_SWEEP = (float)(Math.PI * 2 / 3);
72    // geometry
73    private Point mCenter;
74    private int mRadius;
75    private int mRadiusInc;
76    private int mSlop;
77
78    // the detection if touch is inside a slice is offset
79    // inbounds by this amount to allow the selection to show before the
80    // finger covers it
81    private int mTouchOffset;
82
83    private List<PieItem> mItems;
84
85    private PieItem mOpenItem;
86
87    private Paint mNormalPaint;
88    private Paint mSelectedPaint;
89    private Paint mSubPaint;
90
91    // touch handling
92    private PieItem mCurrentItem;
93
94    private boolean mAnimating;
95    private float mAlpha;
96
97    private Paint mFocusPaint;
98    private Paint mSuccessPaint;
99    private Paint mDotPaint;
100    private int mCircleSize;
101    private int mDotRadius;
102    private int mFocusX;
103    private int mFocusY;
104    private int mCenterX;
105    private int mCenterY;
106
107    private int mDialAngle;
108    private RectF mCircle;
109    private RectF mDial;
110    private Point mPoint1;
111    private Point mPoint2;
112    private int mStartAnimationAngle;
113    private boolean mFocused;
114    private int mInnerOffset;
115    private int mOuterStroke;
116    private int mInnerStroke;
117    private boolean mFirstTime = true;
118
119
120    private Handler mHandler = new Handler() {
121        public void handleMessage(Message msg) {
122            switch(msg.what) {
123            case MSG_OPEN:
124                if (mListener != null && !mAnimating) {
125                    mListener.onPieOpened(mCenter.x, mCenter.y);
126                }
127                break;
128            case MSG_CLOSE:
129                if (mListener != null && !mAnimating) {
130                    mListener.onPieClosed();
131                }
132                break;
133            case MSG_SUBMENU:
134                openCurrentItem();
135                break;
136            }
137        }
138    };
139
140    private PieListener mListener;
141
142    static public interface PieListener {
143        public void onPieOpened(int centerX, int centerY);
144        public void onPieClosed();
145    }
146
147    public void setPieListener(PieListener pl) {
148        mListener = pl;
149    }
150
151    public PieRenderer(Context context) {
152        init(context);
153    }
154    private void init(Context ctx) {
155        setVisible(false);
156        mItems = new ArrayList<PieItem>();
157        Resources res = ctx.getResources();
158        mRadius = (int) res.getDimensionPixelSize(R.dimen.pie_radius_start);
159        mRadiusInc =  (int) res.getDimensionPixelSize(R.dimen.pie_radius_increment);
160        mSlop = (int) res.getDimensionPixelSize(R.dimen.pie_touch_slop);
161        mTouchOffset = (int) res.getDimensionPixelSize(R.dimen.pie_touch_offset);
162        mCenter = new Point(0,0);
163        mNormalPaint = new Paint();
164        mNormalPaint.setColor(Color.argb(0, 0, 0, 0));
165        mNormalPaint.setAntiAlias(true);
166        mSelectedPaint = new Paint();
167        mSelectedPaint.setColor(Color.argb(128, 0, 0, 0)); //res.getColor(R.color.qc_selected));
168        mSelectedPaint.setAntiAlias(true);
169        mSubPaint = new Paint();
170        mSubPaint.setAntiAlias(true);
171        mSubPaint.setColor(Color.argb(200, 250, 230, 128)); //res.getColor(R.color.qc_sub));
172        mFocusPaint = new Paint();
173        mFocusPaint.setAntiAlias(true);
174        mFocusPaint.setColor(Color.WHITE);
175        mFocusPaint.setStyle(Paint.Style.STROKE);
176        mSuccessPaint = new Paint(mFocusPaint);
177        mSuccessPaint.setColor(Color.GREEN);
178        mDotPaint = new Paint();
179        mDotPaint.setAntiAlias(true);
180        mDotPaint.setColor(Color.argb(80, 255, 255, 255));
181        mDotPaint.setStyle(Paint.Style.FILL);
182        mCircle = new RectF();
183        mDial = new RectF();
184        mPoint1 = new Point();
185        mPoint2 = new Point();
186        mInnerOffset = res.getDimensionPixelSize(R.dimen.focus_inner_offset);
187        mOuterStroke = res.getDimensionPixelSize(R.dimen.focus_outer_stroke);
188        mInnerStroke = res.getDimensionPixelSize(R.dimen.focus_inner_stroke);
189        mDotRadius = res.getDimensionPixelSize(R.dimen.focus_dot_radius);
190        setVisible(false);
191
192    }
193
194    public void addItem(PieItem item) {
195        // add the item to the pie itself
196        mItems.add(item);
197    }
198
199    public void removeItem(PieItem item) {
200        mItems.remove(item);
201    }
202
203    public void clearItems() {
204        mItems.clear();
205    }
206
207    public void fade() {
208        mFirstTime = false;
209        setCenter(mCenterX, mCenterY);
210        Animation anim = new AlphaAnimation();
211        anim.setFillAfter(true);
212        anim.setAnimationListener(new AnimationListener() {
213            @Override
214            public void onAnimationStart(Animation animation) {
215                mAnimating = true;
216                update();
217            }
218            @Override
219            public void onAnimationEnd(Animation animation) {
220                show(false);
221                mAlpha = 0f;
222                mAnimating = false;
223                setViewAlpha(mOverlay, 1);
224            }
225            @Override
226            public void onAnimationRepeat(Animation animation) {
227            }
228        });
229        anim.reset();
230        anim.setDuration(1000);
231        show(true);
232        mOverlay.startAnimation(anim);
233    }
234
235    /**
236     * guaranteed has center set
237     * @param show
238     */
239    private void show(boolean show) {
240        if (show) {
241            mState = STATE_PIE;
242            // ensure clean state
243            mAnimating = false;
244            mCurrentItem = null;
245            mOpenItem = null;
246            for (PieItem item : mItems) {
247                item.setSelected(false);
248            }
249            layoutPie();
250        }
251        setVisible(show);
252        mHandler.sendEmptyMessage(show ? MSG_OPEN : MSG_CLOSE);
253    }
254
255    public void setCenter(int x, int y) {
256        mCenter.x = x;
257        mCenter.y = y;
258        // when using the pie menu, align the focus ring
259        alignFocus(x, y);
260    }
261
262    private void setupPie(int x, int y) {
263        // when using the focus ring, align pie items
264        mCenter.x = x;
265        mCenter.y = y;
266        mAnimating = false;
267        mCurrentItem = null;
268        mOpenItem = null;
269        for (PieItem item : mItems) {
270            item.setSelected(false);
271        }
272        layoutPie();
273    }
274
275    private void layoutPie() {
276        int rgap = 2;
277        int inner = mRadius + rgap;
278        int outer = mRadius + mRadiusInc - rgap;
279        int gap = 1;
280        layoutItems(mItems, (float) (Math.PI / 2), inner, outer, gap);
281    }
282
283    private void layoutItems(List<PieItem> items, float centerAngle, int inner,
284            int outer, int gap) {
285        float emptyangle = PIE_SWEEP / 16;
286        float sweep = (float) (PIE_SWEEP - 2 * emptyangle) / items.size();
287        float angle = centerAngle - PIE_SWEEP / 2 + emptyangle + sweep / 2;
288        // check if we have custom geometry
289        // first item we find triggers custom sweep for all
290        // this allows us to re-use the path
291        for (PieItem item : items) {
292            if (item.getCenter() >= 0) {
293                sweep = item.getSweep();
294                break;
295            }
296        }
297        Path path = makeSlice(getDegrees(0) - gap, getDegrees(sweep) + gap,
298                outer, inner, mCenter);
299        for (PieItem item : items) {
300            // shared between items
301            item.setPath(path);
302            View view = item.getView();
303            if (item.getCenter() >= 0) {
304                angle = item.getCenter();
305            }
306            if (view != null) {
307                view.measure(view.getLayoutParams().width,
308                        view.getLayoutParams().height);
309                int w = view.getMeasuredWidth();
310                int h = view.getMeasuredHeight();
311                // move views to outer border
312                int r = inner + (outer - inner) * 2 / 3;
313                int x = (int) (r * Math.cos(angle));
314                int y = mCenter.y - (int) (r * Math.sin(angle)) - h / 2;
315                x = mCenter.x + x - w / 2;
316                view.layout(x, y, x + w, y + h);
317            }
318            float itemstart = angle - sweep / 2;
319            item.setGeometry(itemstart, sweep, inner, outer);
320            if (item.hasItems()) {
321                layoutItems(item.getItems(), angle, inner,
322                        outer + mRadiusInc / 2, gap);
323            }
324            angle += sweep;
325        }
326    }
327
328    private Path makeSlice(float start, float end, int outer, int inner, Point center) {
329        outer = inner + (outer - inner) * 2 / 3;
330        RectF bb =
331                new RectF(center.x - outer, center.y - outer, center.x + outer,
332                        center.y + outer);
333        RectF bbi =
334                new RectF(center.x - inner, center.y - inner, center.x + inner,
335                        center.y + inner);
336        Path path = new Path();
337        path.arcTo(bb, start, end - start, true);
338        path.arcTo(bbi, end, start - end);
339        path.close();
340        return path;
341    }
342
343    /**
344     * converts a
345     * @param angle from 0..PI to Android degrees (clockwise starting at 3 o'clock)
346     * @return skia angle
347     */
348    private float getDegrees(double angle) {
349        return (float) (360 - 180 * angle / Math.PI);
350    }
351
352    @Override
353    public void onDraw(Canvas canvas) {
354        drawFocus(canvas);
355        if (mState == STATE_FINISHING) return;
356        if (mAnimating) {
357            setViewAlpha(mOverlay, mAlpha);
358        }
359        if (mOpenItem == null) {
360            // draw base menu
361            for (PieItem item : mItems) {
362                drawItem(canvas, item);
363            }
364        } else {
365            for (PieItem inner : mOpenItem.getItems()) {
366                drawItem(canvas, inner);
367            }
368        }
369    }
370
371    private void drawItem(Canvas canvas, PieItem item) {
372        if (item.getView() != null) {
373            if (mState == STATE_FOCUSING) {
374                View view = item.getView();
375                canvas.drawCircle(view.getLeft() + view.getWidth() / 2,
376                        view.getTop() + view.getHeight() / 2,
377                        mDotRadius, mDotPaint);
378            } else {
379                if (item.getPath() != null) {
380                    Paint p = item.isSelected() ? mSelectedPaint : mNormalPaint;
381                    int state = canvas.save();
382                    float r = getDegrees(item.getStartAngle());
383                    canvas.rotate(r, mCenter.x, mCenter.y);
384                    canvas.drawPath(item.getPath(), p);
385                    canvas.restoreToCount(state);
386                    // draw the item view
387                    View view = item.getView();
388                    state = canvas.save();
389                    canvas.translate(view.getX(), view.getY());
390                    view.draw(canvas);
391                    canvas.restoreToCount(state);
392                }
393            }
394        }
395    }
396
397    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
398    private void setViewAlpha(View v, float alpha) {
399        if (ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES) {
400            v.setAlpha(alpha);
401        }
402    }
403
404    // touch handling for pie
405
406    @Override
407    public boolean onTouchEvent(MotionEvent evt) {
408        float x = evt.getX();
409        float y = evt.getY();
410        int action = evt.getActionMasked();
411        if (MotionEvent.ACTION_DOWN == action) {
412            setCenter((int) x, (int) y);
413            show(true);
414            return true;
415        } else if (MotionEvent.ACTION_UP == action) {
416            if (isVisible()) {
417                PieItem item = mCurrentItem;
418                if (!mAnimating) {
419                    deselect();
420                }
421                show(false);
422                if ((item != null) && (item.getView() != null)) {
423                    if ((item == mOpenItem) || !mAnimating) {
424                        item.getView().performClick();
425                    }
426                }
427                return true;
428            }
429        } else if (MotionEvent.ACTION_CANCEL == action) {
430            if (isVisible()) {
431                show(false);
432            }
433            if (!mAnimating) {
434                deselect();
435            }
436            return false;
437        } else if (MotionEvent.ACTION_MOVE == action) {
438            if (mAnimating) return false;
439            PointF polar = getPolar(x, y);
440            int maxr = mRadius + mRadiusInc + 50;
441            if (polar.y < mRadius) {
442                if (mOpenItem != null) {
443                    mOpenItem = null;
444                } else if (!mAnimating) {
445                    deselect();
446                }
447                return false;
448            }
449            if (polar.y > maxr) {
450                deselect();
451                show(false);
452                evt.setAction(MotionEvent.ACTION_DOWN);
453                return false;
454            }
455            PieItem item = findItem(polar);
456            if (item == null) {
457            } else if (mCurrentItem != item) {
458                onEnter(item);
459            }
460        }
461        return false;
462    }
463
464    /**
465     * enter a slice for a view
466     * updates model only
467     * @param item
468     */
469    private void onEnter(PieItem item) {
470        if (mCurrentItem != null) {
471            mCurrentItem.setSelected(false);
472        }
473        if (item != null && item.isEnabled()) {
474            item.setSelected(true);
475            mCurrentItem = item;
476            if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) {
477                mHandler.sendEmptyMessageDelayed(MSG_SUBMENU, PIE_OPEN_DELAY);
478            }
479        } else {
480            mCurrentItem = null;
481        }
482    }
483
484    private void deselect() {
485        if (mCurrentItem != null) {
486            mCurrentItem.setSelected(false);
487            mHandler.removeMessages(MSG_SUBMENU);
488        }
489        if (mOpenItem != null) {
490            mOpenItem = null;
491        }
492        mCurrentItem = null;
493    }
494
495    private void openCurrentItem() {
496        if ((mCurrentItem != null) && mCurrentItem.hasItems()) {
497            mOpenItem = mCurrentItem;
498        }
499    }
500
501    private PointF getPolar(float x, float y) {
502        PointF res = new PointF();
503        // get angle and radius from x/y
504        res.x = (float) Math.PI / 2;
505        x = x - mCenter.x;
506        y = mCenter.y - y;
507        res.y = (float) Math.sqrt(x * x + y * y);
508        if (x != 0) {
509            res.x = (float) Math.atan2(y,  x);
510            if (res.x < 0) {
511                res.x = (float) (2 * Math.PI + res.x);
512            }
513        }
514        res.y = res.y + mTouchOffset;
515        return res;
516    }
517
518    /**
519     * @param polar x: angle, y: dist
520     * @return the item at angle/dist or null
521     */
522    private PieItem findItem(PointF polar) {
523        // find the matching item:
524        List<PieItem> items = (mOpenItem != null) ? mOpenItem.getItems() : mItems;
525        for (PieItem item : items) {
526            if (inside(polar, item)) {
527                return item;
528            }
529        }
530        return null;
531    }
532
533    private boolean inside(PointF polar, PieItem item) {
534        return (item.getInnerRadius() < polar.y)
535        && (item.getOuterRadius() > polar.y)
536        && (item.getStartAngle() < polar.x)
537        && (item.getStartAngle() + item.getSweep() > polar.x);
538    }
539
540    @Override
541    public boolean handlesTouch() {
542        return true;
543    }
544
545    private class AlphaAnimation extends Animation {
546        @Override
547        protected void applyTransformation(float interpolatedTime, Transformation t) {
548            mAlpha = 1 - interpolatedTime;
549        }
550    }
551
552    // focus specific code
553
554    public void setFocus(int x, int y) {
555        switch(mOverlay.getOrientation()) {
556        case 0:
557            mFocusX = x;
558            mFocusY = y;
559            break;
560        case 180:
561            mFocusX = getWidth() - x;
562            mFocusY = getHeight() - y;
563            break;
564        case 90:
565            mFocusX = getWidth() - y;
566            mFocusY = x;
567            break;
568        case 270:
569            mFocusX = y ;
570            mFocusY = getHeight() - x;
571            break;
572        }
573        setCircle(mFocusX, mFocusY);
574        setupPie(mFocusX, mFocusY);
575    }
576
577    public void alignFocus(int x, int y) {
578        mOverlay.removeCallbacks(mDisappear);
579        mAnimation.cancel();
580        mAnimation.reset();
581        mFocusX = x;
582        mFocusY = y;
583        mDialAngle = DIAL_HORIZONTAL;
584        setCircle(x, y);
585        mFocused = false;
586    }
587
588    public int getSize() {
589        return 2 * mCircleSize;
590    }
591
592    private int getRandomAngle() {
593        return (int)(90 * Math.random());
594    }
595
596    private int getRandomRange() {
597        return (int)(120 * Math.random());
598    }
599
600    @Override
601    public void layout(int l, int t, int r, int b) {
602        super.layout(l, t, r, b);
603        mCircleSize = Math.min(200, Math.min(getWidth(), getHeight()) / 5);
604        mCenterX = (r - l) / 2;
605        mCenterY = (b - t) / 2;
606        mFocusX = mCenterX;
607        mFocusY = mCenterY;
608        setCircle(mFocusX, mFocusY);
609        if (mFirstTime) {
610            fade();
611        }
612    }
613
614    private void setCircle(int cx, int cy) {
615        mCircle.set(cx - mCircleSize, cy - mCircleSize,
616                cx + mCircleSize, cy + mCircleSize);
617        mDial.set(cx - mCircleSize + mInnerOffset, cy - mCircleSize + mInnerOffset,
618                cx + mCircleSize - mInnerOffset, cy + mCircleSize - mInnerOffset);
619    }
620
621    public void drawFocus(Canvas canvas) {
622        mFocusPaint.setStrokeWidth(mOuterStroke);
623        canvas.drawCircle((float) mFocusX, (float) mFocusY, (float) mCircleSize, mFocusPaint);
624        Paint inner = (mFocused ? mSuccessPaint : mFocusPaint);
625        inner.setStrokeWidth(mInnerStroke);
626        canvas.drawArc(mDial, mDialAngle, 45, false, inner);
627        canvas.drawArc(mDial, mDialAngle + 180, 45, false, inner);
628        drawLine(canvas, mDialAngle, inner);
629        drawLine(canvas, mDialAngle + 45, inner);
630        drawLine(canvas, mDialAngle + 180, inner);
631        drawLine(canvas, mDialAngle + 225, inner);
632    }
633
634    private void drawLine(Canvas canvas, int angle, Paint p) {
635        convertCart(angle, mCircleSize - mInnerOffset, mPoint1);
636        convertCart(angle, mCircleSize - mInnerOffset + mInnerOffset / 3, mPoint2);
637        canvas.drawLine(mPoint1.x + mFocusX, mPoint1.y + mFocusY,
638                mPoint2.x + mFocusX, mPoint2.y + mFocusY, p);
639    }
640
641    private static void convertCart(int angle, int radius, Point out) {
642        double a = 2 * Math.PI * (angle % 360) / 360;
643        out.x = (int) (radius * Math.cos(a) + 0.5);
644        out.y = (int) (radius * Math.sin(a) + 0.5);
645    }
646
647    @Override
648    public void showStart() {
649        if (mState == STATE_IDLE) {
650            int angle = getRandomAngle();
651            int range = getRandomRange();
652            startAnimation(R.drawable.ic_focus_focusing, SCALING_UP_TIME,
653                    false, angle, angle + range);
654            mState = STATE_FOCUSING;
655            mStartAnimationAngle = angle;
656        }
657    }
658
659    @Override
660    public void showSuccess(boolean timeout) {
661        if (mState == STATE_FOCUSING) {
662            startAnimation(R.drawable.ic_focus_focused, SCALING_DOWN_TIME,
663                    timeout, mStartAnimationAngle);
664            mState = STATE_FINISHING;
665            mFocused = true;
666        }
667    }
668
669    @Override
670    public void showFail(boolean timeout) {
671        if (mState == STATE_FOCUSING) {
672            startAnimation(R.drawable.ic_focus_failed, SCALING_DOWN_TIME,
673                    timeout, mStartAnimationAngle);
674            mState = STATE_FINISHING;
675            mFocused = false;
676        }
677    }
678
679    @Override
680    public void clear() {
681        mAnimation.cancel();
682        mFocused = false;
683        mOverlay.removeCallbacks(mDisappear);
684        mDisappear.run();
685    }
686
687    private void startAnimation(int resid, long duration, boolean timeout,
688            float toScale) {
689        startAnimation(resid, duration, timeout, mDialAngle,
690                toScale);
691    }
692
693    private void startAnimation(int resid, long duration, boolean timeout,
694            float fromScale, float toScale) {
695        setVisible(true);
696        mAnimation.cancel();
697        mAnimation.reset();
698        mAnimation.setDuration(duration);
699        mAnimation.setScale(fromScale, toScale);
700        mAnimation.setAnimationListener(timeout ? mEndAction : null);
701        mOverlay.startAnimation(mAnimation);
702        update();
703    }
704
705    private class EndAction implements Animation.AnimationListener {
706        @Override
707        public void onAnimationEnd(Animation animation) {
708            // Keep the focus indicator for some time.
709            mOverlay.postDelayed(mDisappear, DISAPPEAR_TIMEOUT);
710        }
711
712        @Override
713        public void onAnimationRepeat(Animation animation) {
714        }
715
716        @Override
717        public void onAnimationStart(Animation animation) {
718        }
719    }
720
721    private class Disappear implements Runnable {
722        @Override
723        public void run() {
724            setVisible(false);
725            mFocusX = mCenterX;
726            mFocusY = mCenterY;
727            mState = STATE_IDLE;
728            setCircle(mFocusX, mFocusY);
729            setupPie(mFocusX, mFocusY);
730            mFocused = false;
731        }
732    }
733
734    private class ScaleAnimation extends Animation {
735        private float mFrom = 1f;
736        private float mTo = 1f;
737
738        public ScaleAnimation() {
739            setFillAfter(true);
740        }
741
742        public void setScale(float from, float to) {
743            mFrom = from;
744            mTo = to;
745        }
746
747        @Override
748        protected void applyTransformation(float interpolatedTime, Transformation t) {
749            mDialAngle = (int)(mFrom + (mTo - mFrom) * interpolatedTime);
750        }
751    }
752
753}
754