RadialTextsView.java revision f3b38bd61d583d31200c501f5a74392aac510657
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.graphics.Typeface; 29import android.graphics.Paint.Align; 30import android.util.Log; 31import android.view.View; 32 33import com.android.datetimepicker.R; 34 35/** 36 * A view to show a series of numbers in a circular pattern. 37 */ 38public 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 77 public RadialTextsView(Context context) { 78 super(context); 79 mIsInitialized = false; 80 } 81 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 146 /** 147 * Allows for smoother animation. 148 */ 149 @Override 150 public boolean hasOverlappingRendering() { 151 return false; 152 } 153 154 /** 155 * Used by the animation to move the numbers in and out. 156 */ 157 public void setAnimationRadiusMultiplier(float animationRadiusMultiplier) { 158 mAnimationRadiusMultiplier = animationRadiusMultiplier; 159 mTextGridValuesDirty = true; 160 } 161 162 @Override 163 public void onDraw(Canvas canvas) { 164 int viewWidth = getWidth(); 165 if (viewWidth == 0 || !mIsInitialized) { 166 return; 167 } 168 169 if (!mDrawValuesReady) { 170 mXCenter = getWidth() / 2; 171 mYCenter = getHeight() / 2; 172 mCircleRadius = Math.min(mXCenter, mYCenter) * mCircleRadiusMultiplier; 173 if (!mIs24HourMode) { 174 // We'll need to draw the AM/PM circles, so the main circle will need to have 175 // a slightly higher center. To keep the entire view centered vertically, we'll 176 // have to push it up by half the radius of the AM/PM circles. 177 float amPmCircleRadius = mCircleRadius * mAmPmCircleRadiusMultiplier; 178 mYCenter -= amPmCircleRadius / 2; 179 } 180 181 mTextSize = mCircleRadius * mTextSizeMultiplier; 182 if (mHasInnerCircle) { 183 mInnerTextSize = mCircleRadius * mInnerTextSizeMultiplier; 184 } 185 186 // Because the text positions will be static, pre-render the animations. 187 renderAnimations(); 188 189 mTextGridValuesDirty = true; 190 mDrawValuesReady = true; 191 } 192 193 // Calculate the text positions, but only if they've changed since the last onDraw. 194 if (mTextGridValuesDirty) { 195 float numbersRadius = 196 mCircleRadius * mNumbersRadiusMultiplier * mAnimationRadiusMultiplier; 197 198 // Calculate the positions for the 12 numbers in the main circle. 199 calculateGridSizes(numbersRadius, mXCenter, mYCenter, 200 mTextSize, mTextGridHeights, mTextGridWidths); 201 if (mHasInnerCircle) { 202 // If we have an inner circle, calculate those positions too. 203 float innerNumbersRadius = 204 mCircleRadius * mInnerNumbersRadiusMultiplier * mAnimationRadiusMultiplier; 205 calculateGridSizes(innerNumbersRadius, mXCenter, mYCenter, 206 mInnerTextSize, mInnerTextGridHeights, mInnerTextGridWidths); 207 } 208 mTextGridValuesDirty = false; 209 } 210 211 // Draw the texts in the pre-calculated positions. 212 drawTexts(canvas, mTextSize, mTypefaceLight, mTexts, mTextGridWidths, mTextGridHeights); 213 if (mHasInnerCircle) { 214 drawTexts(canvas, mInnerTextSize, mTypefaceRegular, mInnerTexts, 215 mInnerTextGridWidths, mInnerTextGridHeights); 216 } 217 } 218 219 /** 220 * Using the trigonometric Unit Circle, calculate the positions that the text will need to be 221 * drawn at based on the specified circle radius. Place the values in the textGridHeights and 222 * textGridWidths parameters. 223 */ 224 private void calculateGridSizes(float numbersRadius, float xCenter, float yCenter, 225 float textSize, float[] textGridHeights, float[] textGridWidths) { 226 /* 227 * The numbers need to be drawn in a 7x7 grid, representing the points on the Unit Circle. 228 */ 229 float offset1 = numbersRadius; 230 // cos(30) = a / r => r * cos(30) = a => r * √3/2 = a 231 float offset2 = numbersRadius * ((float) Math.sqrt(3)) / 2f; 232 // sin(30) = o / r => r * sin(30) = o => r / 2 = a 233 float offset3 = numbersRadius / 2f; 234 mPaint.setTextSize(textSize); 235 // We'll need yTextBase to be slightly lower to account for the text's baseline. 236 yCenter -= (mPaint.descent() + mPaint.ascent()) / 2; 237 238 textGridHeights[0] = yCenter - offset1; 239 textGridWidths[0] = xCenter - offset1; 240 textGridHeights[1] = yCenter - offset2; 241 textGridWidths[1] = xCenter - offset2; 242 textGridHeights[2] = yCenter - offset3; 243 textGridWidths[2] = xCenter - offset3; 244 textGridHeights[3] = yCenter; 245 textGridWidths[3] = xCenter; 246 textGridHeights[4] = yCenter + offset3; 247 textGridWidths[4] = xCenter + offset3; 248 textGridHeights[5] = yCenter + offset2; 249 textGridWidths[5] = xCenter + offset2; 250 textGridHeights[6] = yCenter + offset1; 251 textGridWidths[6] = xCenter + offset1; 252 } 253 254 /** 255 * Draw the 12 text values at the positions specified by the textGrid parameters. 256 */ 257 private void drawTexts(Canvas canvas, float textSize, Typeface typeface, String[] texts, 258 float[] textGridWidths, float[] textGridHeights) { 259 mPaint.setTextSize(textSize); 260 mPaint.setTypeface(typeface); 261 canvas.drawText(texts[0], textGridWidths[3], textGridHeights[0], mPaint); 262 canvas.drawText(texts[1], textGridWidths[4], textGridHeights[1], mPaint); 263 canvas.drawText(texts[2], textGridWidths[5], textGridHeights[2], mPaint); 264 canvas.drawText(texts[3], textGridWidths[6], textGridHeights[3], mPaint); 265 canvas.drawText(texts[4], textGridWidths[5], textGridHeights[4], mPaint); 266 canvas.drawText(texts[5], textGridWidths[4], textGridHeights[5], mPaint); 267 canvas.drawText(texts[6], textGridWidths[3], textGridHeights[6], mPaint); 268 canvas.drawText(texts[7], textGridWidths[2], textGridHeights[5], mPaint); 269 canvas.drawText(texts[8], textGridWidths[1], textGridHeights[4], mPaint); 270 canvas.drawText(texts[9], textGridWidths[0], textGridHeights[3], mPaint); 271 canvas.drawText(texts[10], textGridWidths[1], textGridHeights[2], mPaint); 272 canvas.drawText(texts[11], textGridWidths[2], textGridHeights[1], mPaint); 273 } 274 275 /** 276 * Render the animations for appearing and disappearing. 277 */ 278 private void renderAnimations() { 279 Keyframe kf0, kf1, kf2, kf3; 280 float midwayPoint = 0.2f; 281 int duration = 500; 282 283 // Set up animator for disappearing. 284 kf0 = Keyframe.ofFloat(0f, 1); 285 kf1 = Keyframe.ofFloat(midwayPoint, mTransitionMidRadiusMultiplier); 286 kf2 = Keyframe.ofFloat(1f, mTransitionEndRadiusMultiplier); 287 PropertyValuesHolder radiusDisappear = PropertyValuesHolder.ofKeyframe( 288 "animationRadiusMultiplier", kf0, kf1, kf2); 289 290 kf0 = Keyframe.ofFloat(0f, 1f); 291 kf1 = Keyframe.ofFloat(1f, 0f); 292 PropertyValuesHolder fadeOut = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1); 293 294 mDisappearAnimator = ObjectAnimator.ofPropertyValuesHolder( 295 this, radiusDisappear, fadeOut).setDuration(duration); 296 mDisappearAnimator.addUpdateListener(mInvalidateUpdateListener); 297 298 299 // Set up animator for reappearing. 300 float delayMultiplier = 0.25f; 301 float transitionDurationMultiplier = 1f; 302 float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier; 303 int totalDuration = (int) (duration * totalDurationMultiplier); 304 float delayPoint = (delayMultiplier * duration) / totalDuration; 305 midwayPoint = 1 - (midwayPoint * (1 - delayPoint)); 306 307 kf0 = Keyframe.ofFloat(0f, mTransitionEndRadiusMultiplier); 308 kf1 = Keyframe.ofFloat(delayPoint, mTransitionEndRadiusMultiplier); 309 kf2 = Keyframe.ofFloat(midwayPoint, mTransitionMidRadiusMultiplier); 310 kf3 = Keyframe.ofFloat(1f, 1); 311 PropertyValuesHolder radiusReappear = PropertyValuesHolder.ofKeyframe( 312 "animationRadiusMultiplier", kf0, kf1, kf2, kf3); 313 314 kf0 = Keyframe.ofFloat(0f, 0f); 315 kf1 = Keyframe.ofFloat(delayPoint, 0f); 316 kf2 = Keyframe.ofFloat(1f, 1f); 317 PropertyValuesHolder fadeIn = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1, kf2); 318 319 mReappearAnimator = ObjectAnimator.ofPropertyValuesHolder( 320 this, radiusReappear, fadeIn).setDuration(totalDuration); 321 mReappearAnimator.addUpdateListener(mInvalidateUpdateListener); 322 } 323 324 public ObjectAnimator getDisappearAnimator() { 325 if (!mIsInitialized || !mDrawValuesReady || mDisappearAnimator == null) { 326 Log.e(TAG, "RadialTextView was not ready for animation."); 327 return null; 328 } 329 330 return mDisappearAnimator; 331 } 332 333 public ObjectAnimator getReappearAnimator() { 334 if (!mIsInitialized || !mDrawValuesReady || mReappearAnimator == null) { 335 Log.e(TAG, "RadialTextView was not ready for animation."); 336 return null; 337 } 338 339 return mReappearAnimator; 340 } 341 342 private class InvalidateUpdateListener implements AnimatorUpdateListener { 343 @Override 344 public void onAnimationUpdate(ValueAnimator animation) { 345 RadialTextsView.this.invalidate(); 346 } 347 } 348} 349