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