RadialSelectorView.java revision 3d5a23b698cb8c59f43914ea2f9bb4fb36575f88
1/*
2 * Copyright (C) 2013 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.datetimepicker;
18
19import android.animation.Keyframe;
20import android.animation.ObjectAnimator;
21import android.animation.PropertyValuesHolder;
22import android.animation.ValueAnimator;
23import android.animation.ValueAnimator.AnimatorUpdateListener;
24import android.app.Service;
25import android.content.Context;
26import android.content.res.Resources;
27import android.graphics.Canvas;
28import android.graphics.Paint;
29import android.graphics.Typeface;
30import android.os.SystemClock;
31import android.os.Vibrator;
32import android.util.AttributeSet;
33import android.util.Log;
34import android.view.MotionEvent;
35import android.view.View;
36import android.view.View.OnTouchListener;
37
38import com.android.datetimepicker.R;
39
40public class RadialSelectorView extends View {
41    private static final String TAG = "RadialSelectorView";
42
43    private final Paint mPaint = new Paint();
44
45    private boolean mIsInitialized;
46    private boolean mDrawValuesReady;
47
48    private float mCircleRadiusMultiplier;
49    private float mAmPmCircleRadiusMultiplier;
50    private float mInnerNumbersRadiusMultiplier;
51    private float mOuterNumbersRadiusMultiplier;
52    private float mNumbersRadiusMultiplier;
53    private float mSelectionRadiusMultiplier;
54    private float mAnimationRadiusMultiplier;
55    private boolean mIs24HourMode;
56    private boolean mHasInnerCircle;
57
58    private int mXCenter;
59    private int mYCenter;
60    private int mCircleRadius;
61    private float mTransitionMidRadiusMultiplier;
62    private float mTransitionEndRadiusMultiplier;
63    private int mLineLength;
64    private int mSelectionRadius;
65    private InvalidateUpdateListener mInvalidateUpdateListener;
66
67    private int mSelectionDegrees;
68    private double mSelectionRadians;
69    private boolean mDrawLine;
70    private boolean mForceDrawDot;
71
72    public RadialSelectorView(Context context) {
73        super(context);
74        mIsInitialized = false;
75    }
76
77    public void initialize(Context context, int selectionDegrees, boolean is24HourMode,
78            boolean hasInnerCircle, boolean isInnerCircle, boolean disappearsOut) {
79        if (mIsInitialized) {
80            Log.e(TAG, "This RadialSelectorView may only be initialized once.");
81            return;
82        }
83
84        Resources res = context.getResources();
85
86        int blue = res.getColor(R.color.blue);
87        mPaint.setColor(blue);
88        mPaint.setAntiAlias(true);
89
90        mIs24HourMode = is24HourMode;
91        if (is24HourMode) {
92            mCircleRadiusMultiplier = Float.parseFloat(
93                    res.getString(R.string.circle_radius_multiplier_24HourMode));
94        } else {
95            mCircleRadiusMultiplier = Float.parseFloat(
96                    res.getString(R.string.circle_radius_multiplier));
97            mAmPmCircleRadiusMultiplier =
98                    Float.parseFloat(res.getString(R.string.ampm_circle_radius_multiplier));
99        }
100
101        mHasInnerCircle = hasInnerCircle;
102        if (hasInnerCircle) {
103            mInnerNumbersRadiusMultiplier =
104                    Float.parseFloat(res.getString(R.string.numbers_radius_multiplier_inner));
105            mOuterNumbersRadiusMultiplier =
106                    Float.parseFloat(res.getString(R.string.numbers_radius_multiplier_outer));
107        } else {
108            mNumbersRadiusMultiplier =
109                    Float.parseFloat(res.getString(R.string.numbers_radius_multiplier_normal));
110        }
111        mSelectionRadiusMultiplier =
112                Float.parseFloat(res.getString(R.string.selection_radius_multiplier));
113
114        setSelection(selectionDegrees, isInnerCircle, false, false);
115
116        mAnimationRadiusMultiplier = 1;
117        mTransitionMidRadiusMultiplier = 1f + (0.05f * (disappearsOut? -1 : 1));
118        mTransitionEndRadiusMultiplier = 1f + (0.3f * (disappearsOut? 1 : -1));
119        mInvalidateUpdateListener = new InvalidateUpdateListener();
120
121        mIsInitialized = true;
122    }
123
124    public void setSelection(int selectionDegrees, boolean isInnerCircle,
125            boolean drawLine, boolean forceDrawDot) {
126        mSelectionDegrees = selectionDegrees;
127        mSelectionRadians = selectionDegrees * Math.PI / 180;
128        mDrawLine = drawLine;
129        mForceDrawDot = forceDrawDot;
130
131        if (mHasInnerCircle) {
132            if (isInnerCircle) {
133                mNumbersRadiusMultiplier = mInnerNumbersRadiusMultiplier;
134            } else {
135                mNumbersRadiusMultiplier = mOuterNumbersRadiusMultiplier;
136            }
137        }
138    }
139
140    public void setDrawLine(boolean drawLine) {
141        mDrawLine = drawLine;
142    }
143
144    @Override
145    public boolean hasOverlappingRendering() {
146        return false;
147    }
148
149    public void setAnimationRadiusMultiplier(float animationRadiusMultiplier) {
150        mAnimationRadiusMultiplier = animationRadiusMultiplier;
151    }
152
153    public int getDegreesFromCoords(float pointX, float pointY, boolean forceLegal,
154            final Boolean[] isInnerCircle) {
155        if (!mDrawValuesReady) {
156            return -1;
157        }
158
159        double hypotenuse = Math.sqrt(
160                (pointY - mYCenter)*(pointY - mYCenter) +
161                (pointX - mXCenter)*(pointX - mXCenter));
162        // Check if we're outside the range
163        if (mHasInnerCircle) {
164            if (forceLegal) {
165                // If we're told to force the coordinates to be legal, we'll set the isInnerCircle
166                // boolean based based off whichever number the coordinates are closer to.
167                int innerNumberRadius = (int) (mCircleRadius * mInnerNumbersRadiusMultiplier);
168                int distanceToInnerNumber = (int) Math.abs(hypotenuse - innerNumberRadius);
169                int outerNumberRadius = (int) (mCircleRadius * mOuterNumbersRadiusMultiplier);
170                int distanceToOuterNumber = (int) Math.abs(hypotenuse - outerNumberRadius);
171
172                isInnerCircle[0] = (distanceToInnerNumber <= distanceToOuterNumber);
173            } else {
174                // Otherwise, if we're close enough to either number (with the space between the
175                // two allotted equally), set the isInnerCircle boolean as the closer one.
176                // appropriately, but otherwise return -1.
177                int minAllowedHypotenuseForInnerNumber =
178                        (int) (mCircleRadius * mInnerNumbersRadiusMultiplier) - mSelectionRadius;
179                int maxAllowedHypotenuseForOuterNumber =
180                        (int) (mCircleRadius * mOuterNumbersRadiusMultiplier) + mSelectionRadius;
181                int halfwayHypotenusePoint = (int) (mCircleRadius *
182                        ((mOuterNumbersRadiusMultiplier + mInnerNumbersRadiusMultiplier) / 2));
183
184                if (hypotenuse >= minAllowedHypotenuseForInnerNumber &&
185                        hypotenuse <= halfwayHypotenusePoint) {
186                    isInnerCircle[0] = true;
187                } else if (hypotenuse <= maxAllowedHypotenuseForOuterNumber &&
188                        hypotenuse >= halfwayHypotenusePoint) {
189                    isInnerCircle[0] = false;
190                } else {
191                    return -1;
192                }
193            }
194        } else {
195            // If there's just one circle, we'll need to return -1 if:
196            // we're not told to force the coordinates to be legal, and
197            // the coordinates' distance to the number is within the allowed distance.
198            if (!forceLegal) {
199                int distanceToNumber = (int) Math.abs(hypotenuse - mLineLength);
200                // The max allowed distance will be defined as the distance from the center of the
201                // number to the edge of the circle.
202                int maxAllowedDistance = (int) (mCircleRadius * (1 - mNumbersRadiusMultiplier));
203                if (distanceToNumber > maxAllowedDistance) {
204                    return -1;
205                }
206            }
207        }
208
209
210        float opposite = Math.abs(pointY - mYCenter);
211        double radians = Math.asin(opposite / hypotenuse);
212        int degrees = (int) (radians * 180 / Math.PI);
213
214        // Now we have to translate to the correct quadrant.
215        boolean rightSide = (pointX > mXCenter);
216        boolean topSide = (pointY < mYCenter);
217        if (rightSide && topSide) {
218            degrees = 90 - degrees;
219        } else if (rightSide && !topSide) {
220            degrees = 90 + degrees;
221        } else if (!rightSide && !topSide) {
222            degrees = 270 - degrees;
223        } else if (!rightSide && topSide) {
224            degrees = 270 + degrees;
225        }
226        return degrees;
227    }
228
229    @Override
230    public void onDraw(Canvas canvas) {
231        int viewWidth = getWidth();
232        if (viewWidth == 0 || !mIsInitialized) {
233            return;
234        }
235
236        if (!mDrawValuesReady) {
237            mXCenter = getWidth() / 2;
238            mYCenter = getHeight() / 2;
239            mCircleRadius = (int) (Math.min(mXCenter, mYCenter) * mCircleRadiusMultiplier);
240
241            if (!mIs24HourMode) {
242                // We'll need to draw the AM/PM circles, so the main circle will need to have
243                // a slightly higher center. To keep the entire view centered vertically, we'll
244                // have to push it up by half the radius of the AM/PM circles.
245                int amPmCircleRadius = (int) (mCircleRadius * mAmPmCircleRadiusMultiplier);
246                mYCenter -= amPmCircleRadius / 2;
247            }
248
249            mSelectionRadius = (int) (mCircleRadius * mSelectionRadiusMultiplier);
250
251            mDrawValuesReady = true;
252        }
253
254        mLineLength = (int) (mCircleRadius * mNumbersRadiusMultiplier * mAnimationRadiusMultiplier);
255        int pointX = mXCenter + (int) (mLineLength * Math.sin(mSelectionRadians));
256        int pointY = mYCenter - (int) (mLineLength * Math.cos(mSelectionRadians));
257
258        mPaint.setAlpha(60);
259        canvas.drawCircle(pointX, pointY, mSelectionRadius, mPaint);
260
261        if (mForceDrawDot | mSelectionDegrees % 30 != 0) {
262            // We're not on a direct tick.
263            mPaint.setAlpha(255);
264            canvas.drawCircle(pointX, pointY, (mSelectionRadius * 2 / 7), mPaint);
265        } else {
266            int lineLength = mLineLength;
267            lineLength -= mSelectionRadius;
268            pointX = mXCenter + (int) (lineLength * Math.sin(mSelectionRadians));
269            pointY = mYCenter - (int) (lineLength * Math.cos(mSelectionRadians));
270        }
271
272        if (mDrawLine || true) {
273            mPaint.setAlpha(255);
274            mPaint.setStrokeWidth(1);
275            canvas.drawLine(mXCenter, mYCenter, pointX, pointY, mPaint);
276        }
277    }
278
279    public ObjectAnimator getDisappearAnimator() {
280        if (!mIsInitialized || !mDrawValuesReady) {
281            Log.e(TAG, "RadialSelectorView was not ready for animation.");
282            return null;
283        }
284
285        Keyframe kf0, kf1, kf2;
286        float midwayPoint = 0.2f;
287        int duration = 500;
288
289        kf0 = Keyframe.ofFloat(0f, 1);
290        kf1 = Keyframe.ofFloat(midwayPoint, mTransitionMidRadiusMultiplier);
291        kf2 = Keyframe.ofFloat(1f, mTransitionEndRadiusMultiplier);
292        PropertyValuesHolder radiusDisappear = PropertyValuesHolder.ofKeyframe(
293                "animationRadiusMultiplier", kf0, kf1, kf2);
294
295        kf0 = Keyframe.ofFloat(0f, 1f);
296        kf1 = Keyframe.ofFloat(1f, 0f);
297        PropertyValuesHolder fadeOut = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1);
298
299        ObjectAnimator disappearAnimator = ObjectAnimator.ofPropertyValuesHolder(
300                this, radiusDisappear, fadeOut).setDuration(duration);
301        disappearAnimator.addUpdateListener(mInvalidateUpdateListener);
302
303        return disappearAnimator;
304    }
305
306    public ObjectAnimator getReappearAnimator() {
307        if (!mIsInitialized || !mDrawValuesReady) {
308            Log.e(TAG, "RadialSelectorView was not ready for animation.");
309            return null;
310        }
311
312        Keyframe kf0, kf1, kf2, kf3;
313        float midwayPoint = 0.2f;
314        int duration = 500;
315
316        // The time points are half of what they would normally be, because this animation is
317        // staggered against the disappear so they happen seamlessly. The reappear starts
318        // halfway into the disappear.
319        float delayMultiplier = 0.5f;
320        float transitionDurationMultiplier = 0.75f;
321        float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier;
322        int totalDuration = (int) (duration * totalDurationMultiplier);
323        float delayPoint = (delayMultiplier * duration) / totalDuration;
324        midwayPoint = 1 - (midwayPoint * (1 - delayPoint));
325
326        kf0 = Keyframe.ofFloat(0f, mTransitionEndRadiusMultiplier);
327        kf1 = Keyframe.ofFloat(delayPoint, mTransitionEndRadiusMultiplier);
328        kf2 = Keyframe.ofFloat(midwayPoint, mTransitionMidRadiusMultiplier);
329        kf3 = Keyframe.ofFloat(1f, 1);
330        PropertyValuesHolder radiusReappear = PropertyValuesHolder.ofKeyframe(
331                "animationRadiusMultiplier", kf0, kf1, kf2, kf3);
332
333        kf0 = Keyframe.ofFloat(0f, 0f);
334        kf1 = Keyframe.ofFloat(delayPoint, 0f);
335        kf2 = Keyframe.ofFloat(1f, 1f);
336        PropertyValuesHolder fadeIn = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1, kf2);
337
338        ObjectAnimator reappearAnimator = ObjectAnimator.ofPropertyValuesHolder(
339                this, radiusReappear, fadeIn).setDuration(totalDuration);
340        reappearAnimator.addUpdateListener(mInvalidateUpdateListener);
341        return reappearAnimator;
342    }
343
344    private class InvalidateUpdateListener implements AnimatorUpdateListener {
345        @Override
346        public void onAnimationUpdate(ValueAnimator animation) {
347            RadialSelectorView.this.invalidate();
348        }
349    }
350}
351