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