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