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