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 17 package com.android.datetimepicker.time; 18 19 import android.animation.Keyframe; 20 import android.animation.ObjectAnimator; 21 import android.animation.PropertyValuesHolder; 22 import android.animation.ValueAnimator; 23 import android.animation.ValueAnimator.AnimatorUpdateListener; 24 import android.content.Context; 25 import android.content.res.Resources; 26 import android.graphics.Canvas; 27 import android.graphics.Paint; 28 import android.graphics.Typeface; 29 import android.graphics.Paint.Align; 30 import android.util.Log; 31 import android.view.View; 32 33 import com.android.datetimepicker.R; 34 35 /** 36 * A view to show a series of numbers in a circular pattern. 37 */ 38 class RadialTextsView extends View { 39 private final static String TAG = "RadialTextsView"; 40 41 private final Paint mPaint = new Paint(); 42 43 private boolean mDrawValuesReady; 44 private boolean mIsInitialized; 45 46 private Typeface mTypefaceLight; 47 private Typeface mTypefaceRegular; 48 private String[] mTexts; 49 private String[] mInnerTexts; 50 private boolean mIs24HourMode; 51 private boolean mHasInnerCircle; 52 private float mCircleRadiusMultiplier; 53 private float mAmPmCircleRadiusMultiplier; 54 private float mNumbersRadiusMultiplier; 55 private float mInnerNumbersRadiusMultiplier; 56 private float mTextSizeMultiplier; 57 private float mInnerTextSizeMultiplier; 58 59 private int mXCenter; 60 private int mYCenter; 61 private float mCircleRadius; 62 private boolean mTextGridValuesDirty; 63 private float mTextSize; 64 private float mInnerTextSize; 65 private float[] mTextGridHeights; 66 private float[] mTextGridWidths; 67 private float[] mInnerTextGridHeights; 68 private float[] mInnerTextGridWidths; 69 70 private float mAnimationRadiusMultiplier; 71 private float mTransitionMidRadiusMultiplier; 72 private float mTransitionEndRadiusMultiplier; 73 ObjectAnimator mDisappearAnimator; 74 ObjectAnimator mReappearAnimator; 75 private InvalidateUpdateListener mInvalidateUpdateListener; 76 RadialTextsView(Context context)77 public RadialTextsView(Context context) { 78 super(context); 79 mIsInitialized = false; 80 } 81 initialize(Resources res, String[] texts, String[] innerTexts, boolean is24HourMode, boolean disappearsOut)82 public void initialize(Resources res, String[] texts, String[] innerTexts, 83 boolean is24HourMode, boolean disappearsOut) { 84 if (mIsInitialized) { 85 Log.e(TAG, "This RadialTextsView may only be initialized once."); 86 return; 87 } 88 89 // Set up the paint. 90 int numbersTextColor = res.getColor(R.color.numbers_text_color); 91 mPaint.setColor(numbersTextColor); 92 String typefaceFamily = res.getString(R.string.radial_numbers_typeface); 93 mTypefaceLight = Typeface.create(typefaceFamily, Typeface.NORMAL); 94 String typefaceFamilyRegular = res.getString(R.string.sans_serif); 95 mTypefaceRegular = Typeface.create(typefaceFamilyRegular, Typeface.NORMAL); 96 mPaint.setAntiAlias(true); 97 mPaint.setTextAlign(Align.CENTER); 98 99 mTexts = texts; 100 mInnerTexts = innerTexts; 101 mIs24HourMode = is24HourMode; 102 mHasInnerCircle = (innerTexts != null); 103 104 // Calculate the radius for the main circle. 105 if (is24HourMode) { 106 mCircleRadiusMultiplier = Float.parseFloat( 107 res.getString(R.string.circle_radius_multiplier_24HourMode)); 108 } else { 109 mCircleRadiusMultiplier = Float.parseFloat( 110 res.getString(R.string.circle_radius_multiplier)); 111 mAmPmCircleRadiusMultiplier = 112 Float.parseFloat(res.getString(R.string.ampm_circle_radius_multiplier)); 113 } 114 115 // Initialize the widths and heights of the grid, and calculate the values for the numbers. 116 mTextGridHeights = new float[7]; 117 mTextGridWidths = new float[7]; 118 if (mHasInnerCircle) { 119 mNumbersRadiusMultiplier = Float.parseFloat( 120 res.getString(R.string.numbers_radius_multiplier_outer)); 121 mTextSizeMultiplier = Float.parseFloat( 122 res.getString(R.string.text_size_multiplier_outer)); 123 mInnerNumbersRadiusMultiplier = Float.parseFloat( 124 res.getString(R.string.numbers_radius_multiplier_inner)); 125 mInnerTextSizeMultiplier = Float.parseFloat( 126 res.getString(R.string.text_size_multiplier_inner)); 127 128 mInnerTextGridHeights = new float[7]; 129 mInnerTextGridWidths = new float[7]; 130 } else { 131 mNumbersRadiusMultiplier = Float.parseFloat( 132 res.getString(R.string.numbers_radius_multiplier_normal)); 133 mTextSizeMultiplier = Float.parseFloat( 134 res.getString(R.string.text_size_multiplier_normal)); 135 } 136 137 mAnimationRadiusMultiplier = 1; 138 mTransitionMidRadiusMultiplier = 1f + (0.05f * (disappearsOut? -1 : 1)); 139 mTransitionEndRadiusMultiplier = 1f + (0.3f * (disappearsOut? 1 : -1)); 140 mInvalidateUpdateListener = new InvalidateUpdateListener(); 141 142 mTextGridValuesDirty = true; 143 mIsInitialized = true; 144 } 145 setTheme(Context context, boolean themeDark)146 /* package */ void setTheme(Context context, boolean themeDark) { 147 Resources res = context.getResources(); 148 int textColor; 149 if (themeDark) { 150 textColor = res.getColor(android.R.color.white); 151 } else { 152 textColor = res.getColor(R.color.numbers_text_color); 153 } 154 mPaint.setColor(textColor); 155 } 156 157 /** 158 * Allows for smoother animation. 159 */ 160 @Override hasOverlappingRendering()161 public boolean hasOverlappingRendering() { 162 return false; 163 } 164 165 /** 166 * Used by the animation to move the numbers in and out. 167 */ setAnimationRadiusMultiplier(float animationRadiusMultiplier)168 public void setAnimationRadiusMultiplier(float animationRadiusMultiplier) { 169 mAnimationRadiusMultiplier = animationRadiusMultiplier; 170 mTextGridValuesDirty = true; 171 } 172 173 @Override onDraw(Canvas canvas)174 public void onDraw(Canvas canvas) { 175 int viewWidth = getWidth(); 176 if (viewWidth == 0 || !mIsInitialized) { 177 return; 178 } 179 180 if (!mDrawValuesReady) { 181 mXCenter = getWidth() / 2; 182 mYCenter = getHeight() / 2; 183 mCircleRadius = Math.min(mXCenter, mYCenter) * mCircleRadiusMultiplier; 184 if (!mIs24HourMode) { 185 // We'll need to draw the AM/PM circles, so the main circle will need to have 186 // a slightly higher center. To keep the entire view centered vertically, we'll 187 // have to push it up by half the radius of the AM/PM circles. 188 float amPmCircleRadius = mCircleRadius * mAmPmCircleRadiusMultiplier; 189 mYCenter -= amPmCircleRadius / 2; 190 } 191 192 mTextSize = mCircleRadius * mTextSizeMultiplier; 193 if (mHasInnerCircle) { 194 mInnerTextSize = mCircleRadius * mInnerTextSizeMultiplier; 195 } 196 197 // Because the text positions will be static, pre-render the animations. 198 renderAnimations(); 199 200 mTextGridValuesDirty = true; 201 mDrawValuesReady = true; 202 } 203 204 // Calculate the text positions, but only if they've changed since the last onDraw. 205 if (mTextGridValuesDirty) { 206 float numbersRadius = 207 mCircleRadius * mNumbersRadiusMultiplier * mAnimationRadiusMultiplier; 208 209 // Calculate the positions for the 12 numbers in the main circle. 210 calculateGridSizes(numbersRadius, mXCenter, mYCenter, 211 mTextSize, mTextGridHeights, mTextGridWidths); 212 if (mHasInnerCircle) { 213 // If we have an inner circle, calculate those positions too. 214 float innerNumbersRadius = 215 mCircleRadius * mInnerNumbersRadiusMultiplier * mAnimationRadiusMultiplier; 216 calculateGridSizes(innerNumbersRadius, mXCenter, mYCenter, 217 mInnerTextSize, mInnerTextGridHeights, mInnerTextGridWidths); 218 } 219 mTextGridValuesDirty = false; 220 } 221 222 // Draw the texts in the pre-calculated positions. 223 drawTexts(canvas, mTextSize, mTypefaceLight, mTexts, mTextGridWidths, mTextGridHeights); 224 if (mHasInnerCircle) { 225 drawTexts(canvas, mInnerTextSize, mTypefaceRegular, mInnerTexts, 226 mInnerTextGridWidths, mInnerTextGridHeights); 227 } 228 } 229 230 /** 231 * Using the trigonometric Unit Circle, calculate the positions that the text will need to be 232 * drawn at based on the specified circle radius. Place the values in the textGridHeights and 233 * textGridWidths parameters. 234 */ calculateGridSizes(float numbersRadius, float xCenter, float yCenter, float textSize, float[] textGridHeights, float[] textGridWidths)235 private void calculateGridSizes(float numbersRadius, float xCenter, float yCenter, 236 float textSize, float[] textGridHeights, float[] textGridWidths) { 237 /* 238 * The numbers need to be drawn in a 7x7 grid, representing the points on the Unit Circle. 239 */ 240 float offset1 = numbersRadius; 241 // cos(30) = a / r => r * cos(30) = a => r * √3/2 = a 242 float offset2 = numbersRadius * ((float) Math.sqrt(3)) / 2f; 243 // sin(30) = o / r => r * sin(30) = o => r / 2 = a 244 float offset3 = numbersRadius / 2f; 245 mPaint.setTextSize(textSize); 246 // We'll need yTextBase to be slightly lower to account for the text's baseline. 247 yCenter -= (mPaint.descent() + mPaint.ascent()) / 2; 248 249 textGridHeights[0] = yCenter - offset1; 250 textGridWidths[0] = xCenter - offset1; 251 textGridHeights[1] = yCenter - offset2; 252 textGridWidths[1] = xCenter - offset2; 253 textGridHeights[2] = yCenter - offset3; 254 textGridWidths[2] = xCenter - offset3; 255 textGridHeights[3] = yCenter; 256 textGridWidths[3] = xCenter; 257 textGridHeights[4] = yCenter + offset3; 258 textGridWidths[4] = xCenter + offset3; 259 textGridHeights[5] = yCenter + offset2; 260 textGridWidths[5] = xCenter + offset2; 261 textGridHeights[6] = yCenter + offset1; 262 textGridWidths[6] = xCenter + offset1; 263 } 264 265 /** 266 * Draw the 12 text values at the positions specified by the textGrid parameters. 267 */ drawTexts(Canvas canvas, float textSize, Typeface typeface, String[] texts, float[] textGridWidths, float[] textGridHeights)268 private void drawTexts(Canvas canvas, float textSize, Typeface typeface, String[] texts, 269 float[] textGridWidths, float[] textGridHeights) { 270 mPaint.setTextSize(textSize); 271 mPaint.setTypeface(typeface); 272 canvas.drawText(texts[0], textGridWidths[3], textGridHeights[0], mPaint); 273 canvas.drawText(texts[1], textGridWidths[4], textGridHeights[1], mPaint); 274 canvas.drawText(texts[2], textGridWidths[5], textGridHeights[2], mPaint); 275 canvas.drawText(texts[3], textGridWidths[6], textGridHeights[3], mPaint); 276 canvas.drawText(texts[4], textGridWidths[5], textGridHeights[4], mPaint); 277 canvas.drawText(texts[5], textGridWidths[4], textGridHeights[5], mPaint); 278 canvas.drawText(texts[6], textGridWidths[3], textGridHeights[6], mPaint); 279 canvas.drawText(texts[7], textGridWidths[2], textGridHeights[5], mPaint); 280 canvas.drawText(texts[8], textGridWidths[1], textGridHeights[4], mPaint); 281 canvas.drawText(texts[9], textGridWidths[0], textGridHeights[3], mPaint); 282 canvas.drawText(texts[10], textGridWidths[1], textGridHeights[2], mPaint); 283 canvas.drawText(texts[11], textGridWidths[2], textGridHeights[1], mPaint); 284 } 285 286 /** 287 * Render the animations for appearing and disappearing. 288 */ renderAnimations()289 private void renderAnimations() { 290 Keyframe kf0, kf1, kf2, kf3; 291 float midwayPoint = 0.2f; 292 int duration = 500; 293 294 // Set up animator for disappearing. 295 kf0 = Keyframe.ofFloat(0f, 1); 296 kf1 = Keyframe.ofFloat(midwayPoint, mTransitionMidRadiusMultiplier); 297 kf2 = Keyframe.ofFloat(1f, mTransitionEndRadiusMultiplier); 298 PropertyValuesHolder radiusDisappear = PropertyValuesHolder.ofKeyframe( 299 "animationRadiusMultiplier", kf0, kf1, kf2); 300 301 kf0 = Keyframe.ofFloat(0f, 1f); 302 kf1 = Keyframe.ofFloat(1f, 0f); 303 PropertyValuesHolder fadeOut = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1); 304 305 mDisappearAnimator = ObjectAnimator.ofPropertyValuesHolder( 306 this, radiusDisappear, fadeOut).setDuration(duration); 307 mDisappearAnimator.addUpdateListener(mInvalidateUpdateListener); 308 309 310 // Set up animator for reappearing. 311 float delayMultiplier = 0.25f; 312 float transitionDurationMultiplier = 1f; 313 float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier; 314 int totalDuration = (int) (duration * totalDurationMultiplier); 315 float delayPoint = (delayMultiplier * duration) / totalDuration; 316 midwayPoint = 1 - (midwayPoint * (1 - delayPoint)); 317 318 kf0 = Keyframe.ofFloat(0f, mTransitionEndRadiusMultiplier); 319 kf1 = Keyframe.ofFloat(delayPoint, mTransitionEndRadiusMultiplier); 320 kf2 = Keyframe.ofFloat(midwayPoint, mTransitionMidRadiusMultiplier); 321 kf3 = Keyframe.ofFloat(1f, 1); 322 PropertyValuesHolder radiusReappear = PropertyValuesHolder.ofKeyframe( 323 "animationRadiusMultiplier", kf0, kf1, kf2, kf3); 324 325 kf0 = Keyframe.ofFloat(0f, 0f); 326 kf1 = Keyframe.ofFloat(delayPoint, 0f); 327 kf2 = Keyframe.ofFloat(1f, 1f); 328 PropertyValuesHolder fadeIn = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1, kf2); 329 330 mReappearAnimator = ObjectAnimator.ofPropertyValuesHolder( 331 this, radiusReappear, fadeIn).setDuration(totalDuration); 332 mReappearAnimator.addUpdateListener(mInvalidateUpdateListener); 333 } 334 getDisappearAnimator()335 public ObjectAnimator getDisappearAnimator() { 336 if (!mIsInitialized || !mDrawValuesReady || mDisappearAnimator == null) { 337 Log.e(TAG, "RadialTextView was not ready for animation."); 338 return null; 339 } 340 341 return mDisappearAnimator; 342 } 343 getReappearAnimator()344 public ObjectAnimator getReappearAnimator() { 345 if (!mIsInitialized || !mDrawValuesReady || mReappearAnimator == null) { 346 Log.e(TAG, "RadialTextView was not ready for animation."); 347 return null; 348 } 349 350 return mReappearAnimator; 351 } 352 353 private class InvalidateUpdateListener implements AnimatorUpdateListener { 354 @Override onAnimationUpdate(ValueAnimator animation)355 public void onAnimationUpdate(ValueAnimator animation) { 356 RadialTextsView.this.invalidate(); 357 } 358 } 359 } 360