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