PieRenderer.java revision 8042e7045f3e11dd49d40eebe603eb5fe12d711a
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                Paint p = item.isSelected() ? mSelectedPaint : mNormalPaint;
380                int state = canvas.save();
381                float r = getDegrees(item.getStartAngle());
382                canvas.rotate(r, mCenter.x, mCenter.y);
383                canvas.drawPath(item.getPath(), p);
384                canvas.restoreToCount(state);
385                // draw the item view
386                View view = item.getView();
387                state = canvas.save();
388                canvas.translate(view.getX(), view.getY());
389                view.draw(canvas);
390                canvas.restoreToCount(state);
391            }
392        }
393    }
394
395    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
396    private void setViewAlpha(View v, float alpha) {
397        if (ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES) {
398            v.setAlpha(alpha);
399        }
400    }
401
402    // touch handling for pie
403
404    @Override
405    public boolean onTouchEvent(MotionEvent evt) {
406        float x = evt.getX();
407        float y = evt.getY();
408        int action = evt.getActionMasked();
409        if (MotionEvent.ACTION_DOWN == action) {
410            setCenter((int) x, (int) y);
411            show(true);
412            return true;
413        } else if (MotionEvent.ACTION_UP == action) {
414            if (isVisible()) {
415                PieItem item = mCurrentItem;
416                if (!mAnimating) {
417                    deselect();
418                }
419                show(false);
420                if ((item != null) && (item.getView() != null)) {
421                    if ((item == mOpenItem) || !mAnimating) {
422                        item.getView().performClick();
423                    }
424                }
425                return true;
426            }
427        } else if (MotionEvent.ACTION_CANCEL == action) {
428            if (isVisible()) {
429                show(false);
430            }
431            if (!mAnimating) {
432                deselect();
433            }
434            return false;
435        } else if (MotionEvent.ACTION_MOVE == action) {
436            if (mAnimating) return false;
437            PointF polar = getPolar(x, y);
438            int maxr = mRadius + mRadiusInc + 50;
439            if (polar.y < mRadius) {
440                if (mOpenItem != null) {
441                    mOpenItem = null;
442                } else if (!mAnimating) {
443                    deselect();
444                }
445                return false;
446            }
447            if (polar.y > maxr) {
448                deselect();
449                show(false);
450                evt.setAction(MotionEvent.ACTION_DOWN);
451                return false;
452            }
453            PieItem item = findItem(polar);
454            if (item == null) {
455            } else if (mCurrentItem != item) {
456                onEnter(item);
457            }
458        }
459        return false;
460    }
461
462    /**
463     * enter a slice for a view
464     * updates model only
465     * @param item
466     */
467    private void onEnter(PieItem item) {
468        if (mCurrentItem != null) {
469            mCurrentItem.setSelected(false);
470        }
471        if (item != null && item.isEnabled()) {
472            item.setSelected(true);
473            mCurrentItem = item;
474            if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) {
475                mHandler.sendEmptyMessageDelayed(MSG_SUBMENU, PIE_OPEN_DELAY);
476            }
477        } else {
478            mCurrentItem = null;
479        }
480    }
481
482    private void deselect() {
483        if (mCurrentItem != null) {
484            mCurrentItem.setSelected(false);
485            mHandler.removeMessages(MSG_SUBMENU);
486        }
487        if (mOpenItem != null) {
488            mOpenItem = null;
489        }
490        mCurrentItem = null;
491    }
492
493    private void openCurrentItem() {
494        if ((mCurrentItem != null) && mCurrentItem.hasItems()) {
495            mOpenItem = mCurrentItem;
496        }
497    }
498
499    private PointF getPolar(float x, float y) {
500        PointF res = new PointF();
501        // get angle and radius from x/y
502        res.x = (float) Math.PI / 2;
503        x = x - mCenter.x;
504        y = mCenter.y - y;
505        res.y = (float) Math.sqrt(x * x + y * y);
506        if (x != 0) {
507            res.x = (float) Math.atan2(y,  x);
508            if (res.x < 0) {
509                res.x = (float) (2 * Math.PI + res.x);
510            }
511        }
512        res.y = res.y + mTouchOffset;
513        return res;
514    }
515
516    /**
517     * @param polar x: angle, y: dist
518     * @return the item at angle/dist or null
519     */
520    private PieItem findItem(PointF polar) {
521        // find the matching item:
522        List<PieItem> items = (mOpenItem != null) ? mOpenItem.getItems() : mItems;
523        for (PieItem item : items) {
524            if (inside(polar, item)) {
525                return item;
526            }
527        }
528        return null;
529    }
530
531    private boolean inside(PointF polar, PieItem item) {
532        return (item.getInnerRadius() < polar.y)
533        && (item.getOuterRadius() > polar.y)
534        && (item.getStartAngle() < polar.x)
535        && (item.getStartAngle() + item.getSweep() > polar.x);
536    }
537
538    @Override
539    public boolean handlesTouch() {
540        return true;
541    }
542
543    private class AlphaAnimation extends Animation {
544        @Override
545        protected void applyTransformation(float interpolatedTime, Transformation t) {
546            mAlpha = 1 - interpolatedTime;
547        }
548    }
549
550    // focus specific code
551
552    public void setFocus(int x, int y) {
553        switch(mOverlay.getOrientation()) {
554        case 0:
555            mFocusX = x;
556            mFocusY = y;
557            break;
558        case 180:
559            mFocusX = getWidth() - x;
560            mFocusY = getHeight() - y;
561            break;
562        case 90:
563            mFocusX = getWidth() - y;
564            mFocusY = x;
565            break;
566        case 270:
567            mFocusX = y ;
568            mFocusY = getHeight() - x;
569            break;
570        }
571        setCircle(mFocusX, mFocusY);
572        setupPie(mFocusX, mFocusY);
573    }
574
575    public void alignFocus(int x, int y) {
576        mOverlay.removeCallbacks(mDisappear);
577        mAnimation.cancel();
578        mAnimation.reset();
579        mFocusX = x;
580        mFocusY = y;
581        mDialAngle = DIAL_HORIZONTAL;
582        setCircle(x, y);
583        mFocused = false;
584    }
585
586    public int getSize() {
587        return 2 * mCircleSize;
588    }
589
590    private int getRandomAngle() {
591        return (int)(90 * Math.random());
592    }
593
594    private int getRandomRange() {
595        return (int)(120 * Math.random());
596    }
597
598    @Override
599    public void layout(int l, int t, int r, int b) {
600        super.layout(l, t, r, b);
601        mCircleSize = Math.min(200, Math.min(getWidth(), getHeight()) / 5);
602        mCenterX = (r - l) / 2;
603        mCenterY = (b - t) / 2;
604        mFocusX = mCenterX;
605        mFocusY = mCenterY;
606        setCircle(mFocusX, mFocusY);
607        if (mFirstTime) {
608            fade();
609        }
610    }
611
612    private void setCircle(int cx, int cy) {
613        mCircle.set(cx - mCircleSize, cy - mCircleSize,
614                cx + mCircleSize, cy + mCircleSize);
615        mDial.set(cx - mCircleSize + mInnerOffset, cy - mCircleSize + mInnerOffset,
616                cx + mCircleSize - mInnerOffset, cy + mCircleSize - mInnerOffset);
617    }
618
619    public void drawFocus(Canvas canvas) {
620        mFocusPaint.setStrokeWidth(mOuterStroke);
621        canvas.drawCircle((float) mFocusX, (float) mFocusY, (float) mCircleSize, mFocusPaint);
622        Paint inner = (mFocused ? mSuccessPaint : mFocusPaint);
623        inner.setStrokeWidth(mInnerStroke);
624        canvas.drawArc(mDial, mDialAngle, 45, false, inner);
625        canvas.drawArc(mDial, mDialAngle + 180, 45, false, inner);
626        drawLine(canvas, mDialAngle, inner);
627        drawLine(canvas, mDialAngle + 45, inner);
628        drawLine(canvas, mDialAngle + 180, inner);
629        drawLine(canvas, mDialAngle + 225, inner);
630    }
631
632    private void drawLine(Canvas canvas, int angle, Paint p) {
633        convertCart(angle, mCircleSize - mInnerOffset, mPoint1);
634        convertCart(angle, mCircleSize - mInnerOffset + mInnerOffset / 3, mPoint2);
635        canvas.drawLine(mPoint1.x + mFocusX, mPoint1.y + mFocusY,
636                mPoint2.x + mFocusX, mPoint2.y + mFocusY, p);
637    }
638
639    private static void convertCart(int angle, int radius, Point out) {
640        double a = 2 * Math.PI * (angle % 360) / 360;
641        out.x = (int) (radius * Math.cos(a) + 0.5);
642        out.y = (int) (radius * Math.sin(a) + 0.5);
643    }
644
645    @Override
646    public void showStart() {
647        if (mState == STATE_IDLE) {
648            int angle = getRandomAngle();
649            int range = getRandomRange();
650            startAnimation(R.drawable.ic_focus_focusing, SCALING_UP_TIME,
651                    false, angle, angle + range);
652            mState = STATE_FOCUSING;
653            mStartAnimationAngle = angle;
654        }
655    }
656
657    @Override
658    public void showSuccess(boolean timeout) {
659        if (mState == STATE_FOCUSING) {
660            startAnimation(R.drawable.ic_focus_focused, SCALING_DOWN_TIME,
661                    timeout, mStartAnimationAngle);
662            mState = STATE_FINISHING;
663            mFocused = true;
664        }
665    }
666
667    @Override
668    public void showFail(boolean timeout) {
669        if (mState == STATE_FOCUSING) {
670            startAnimation(R.drawable.ic_focus_failed, SCALING_DOWN_TIME,
671                    timeout, mStartAnimationAngle);
672            mState = STATE_FINISHING;
673            mFocused = false;
674        }
675    }
676
677    @Override
678    public void clear() {
679        mAnimation.cancel();
680        mFocused = false;
681        mOverlay.removeCallbacks(mDisappear);
682        mDisappear.run();
683    }
684
685    private void startAnimation(int resid, long duration, boolean timeout,
686            float toScale) {
687        startAnimation(resid, duration, timeout, mDialAngle,
688                toScale);
689    }
690
691    private void startAnimation(int resid, long duration, boolean timeout,
692            float fromScale, float toScale) {
693        setVisible(true);
694        mAnimation.cancel();
695        mAnimation.reset();
696        mAnimation.setDuration(duration);
697        mAnimation.setScale(fromScale, toScale);
698        mAnimation.setAnimationListener(timeout ? mEndAction : null);
699        mOverlay.startAnimation(mAnimation);
700        update();
701    }
702
703    private class EndAction implements Animation.AnimationListener {
704        @Override
705        public void onAnimationEnd(Animation animation) {
706            // Keep the focus indicator for some time.
707            mOverlay.postDelayed(mDisappear, DISAPPEAR_TIMEOUT);
708        }
709
710        @Override
711        public void onAnimationRepeat(Animation animation) {
712        }
713
714        @Override
715        public void onAnimationStart(Animation animation) {
716        }
717    }
718
719    private class Disappear implements Runnable {
720        @Override
721        public void run() {
722            setVisible(false);
723            mFocusX = mCenterX;
724            mFocusY = mCenterY;
725            mState = STATE_IDLE;
726            setCircle(mFocusX, mFocusY);
727            setupPie(mFocusX, mFocusY);
728            mFocused = false;
729        }
730    }
731
732    private class ScaleAnimation extends Animation {
733        private float mFrom = 1f;
734        private float mTo = 1f;
735
736        public ScaleAnimation() {
737            setFillAfter(true);
738        }
739
740        public void setScale(float from, float to) {
741            mFrom = from;
742            mTo = to;
743        }
744
745        @Override
746        protected void applyTransformation(float interpolatedTime, Transformation t) {
747            mDialAngle = (int)(mFrom + (mTo - mFrom) * interpolatedTime);
748        }
749    }
750
751}
752