CountingTimerView.java revision 78b8e1513e24c58ffea6ee4edbebdce85c248f6f
1/* 2 * Copyright (C) 2008 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.deskclock.timer; 18 19import android.content.Context; 20import android.content.res.Resources; 21import android.graphics.Canvas; 22import android.graphics.Paint; 23import android.graphics.Typeface; 24import android.util.AttributeSet; 25import android.view.MotionEvent; 26import android.view.View; 27import android.view.accessibility.AccessibilityManager; 28import android.widget.TextView; 29 30import com.android.deskclock.DeskClock; 31import com.android.deskclock.R; 32import com.android.deskclock.Utils; 33 34 35public class CountingTimerView extends View { 36 private static final String TWO_DIGITS = "%02d"; 37 private static final String ONE_DIGIT = "%01d"; 38 private static final String NEG_TWO_DIGITS = "-%02d"; 39 private static final String NEG_ONE_DIGIT = "-%01d"; 40 private static final float TEXT_SIZE_TO_WIDTH_RATIO = 0.75f; 41 // This is the ratio of the font typeface we need to offset the font by vertically to align it 42 // vertically center. 43 private static final float FONT_VERTICAL_OFFSET = 0.14f; 44 45 private String mHours, mMinutes, mSeconds, mHunderdths; 46 private final String mHoursLabel, mMinutesLabel, mSecondsLabel; 47 private float mHoursWidth, mMinutesWidth, mSecondsWidth, mHundredthsWidth; 48 private float mHoursLabelWidth, mMinutesLabelWidth, mSecondsLabelWidth, mHundredthsSepWidth; 49 50 private boolean mShowTimeStr = true; 51 private final Typeface mAndroidClockMonoThin, mAndroidClockMonoBold, mRobotoLabel, mAndroidClockMonoLight; 52 private final Paint mPaintBig = new Paint(); 53 private final Paint mPaintBigThin = new Paint(); 54 private final Paint mPaintMed = new Paint(); 55 private final Paint mPaintLabel = new Paint(); 56 private float mTextHeight = 0; 57 private float mTotalTextWidth; 58 private static final String HUNDREDTH_SEPERATOR = "."; 59 private boolean mRemeasureText = true; 60 61 private int mDefaultColor; 62 private final int mPressedColor; 63 private final int mWhiteColor; 64 private final int mRedColor; 65 private TextView mStopStartTextView; 66 private final AccessibilityManager mAccessibilityManager; 67 68 // Fields for the text serving as a virtual button. 69 private boolean mVirtualButtonEnabled = false; 70 private boolean mVirtualButtonPressedOn = false; 71 72 Runnable mBlinkThread = new Runnable() { 73 private boolean mVisible = true; 74 @Override 75 public void run() { 76 mVisible = !mVisible; 77 CountingTimerView.this.showTime(mVisible); 78 postDelayed(mBlinkThread, 500); 79 } 80 81 }; 82 83 84 public CountingTimerView(Context context) { 85 this(context, null); 86 } 87 88 public CountingTimerView(Context context, AttributeSet attrs) { 89 super(context, attrs); 90 mAndroidClockMonoThin = Typeface.createFromAsset(context.getAssets(),"fonts/AndroidClockMono-Thin.ttf"); 91 mAndroidClockMonoBold = Typeface.createFromAsset(context.getAssets(),"fonts/AndroidClockMono-Bold.ttf"); 92 mAndroidClockMonoLight = Typeface.createFromAsset(context.getAssets(),"fonts/AndroidClockMono-Light.ttf"); 93 mAccessibilityManager = 94 (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); 95 mRobotoLabel= Typeface.create("sans-serif-condensed", Typeface.BOLD); 96 Resources r = context.getResources(); 97 mHoursLabel = r.getString(R.string.hours_label).toUpperCase(); 98 mMinutesLabel = r.getString(R.string.minutes_label).toUpperCase(); 99 mSecondsLabel = r.getString(R.string.seconds_label).toUpperCase(); 100 mWhiteColor = r.getColor(R.color.clock_white); 101 mDefaultColor = mWhiteColor; 102 mPressedColor = r.getColor(Utils.getPressedColorId()); 103 mRedColor = r.getColor(R.color.clock_red); 104 105 mPaintBig.setAntiAlias(true); 106 mPaintBig.setStyle(Paint.Style.STROKE); 107 mPaintBig.setTextAlign(Paint.Align.LEFT); 108 mPaintBig.setTypeface(mAndroidClockMonoBold); 109 float bigFontSize = r.getDimension(R.dimen.big_font_size); 110 mPaintBig.setTextSize(bigFontSize); 111 mTextHeight = bigFontSize; 112 113 mPaintBigThin.setAntiAlias(true); 114 mPaintBigThin.setStyle(Paint.Style.STROKE); 115 mPaintBigThin.setTextAlign(Paint.Align.LEFT); 116 mPaintBigThin.setTypeface(mAndroidClockMonoThin); 117 mPaintBigThin.setTextSize(r.getDimension(R.dimen.big_font_size)); 118 119 mPaintMed.setAntiAlias(true); 120 mPaintMed.setStyle(Paint.Style.STROKE); 121 mPaintMed.setTextAlign(Paint.Align.LEFT); 122 mPaintMed.setTypeface(mAndroidClockMonoLight); 123 mPaintMed.setTextSize(r.getDimension(R.dimen.small_font_size)); 124 125 mPaintLabel.setAntiAlias(true); 126 mPaintLabel.setStyle(Paint.Style.STROKE); 127 mPaintLabel.setTextAlign(Paint.Align.LEFT); 128 mPaintLabel.setTypeface(mRobotoLabel); 129 mPaintLabel.setTextSize(r.getDimension(R.dimen.label_font_size)); 130 131 setTextColor(mDefaultColor); 132 } 133 134 protected void setTextColor(int textColor) { 135 mPaintBig.setColor(textColor); 136 mPaintBigThin.setColor(textColor); 137 mPaintMed.setColor(textColor); 138 mPaintLabel.setColor(textColor); 139 } 140 141 public void setTime(long time, boolean showHundredths, boolean update) { 142 boolean neg = false, showNeg = false; 143 String format = null; 144 if (time < 0) { 145 time = -time; 146 neg = showNeg = true; 147 } 148 long hundreds, seconds, minutes, hours; 149 seconds = time / 1000; 150 hundreds = (time - seconds * 1000) / 10; 151 minutes = seconds / 60; 152 seconds = seconds - minutes * 60; 153 hours = minutes / 60; 154 minutes = minutes - hours * 60; 155 if (hours > 99) { 156 hours = 0; 157 } 158 // time may less than a second below zero, since we do not show fractions of seconds 159 // when counting down, do not show the minus sign. 160 if (hours ==0 && minutes == 0 && seconds == 0) { 161 showNeg = false; 162 } 163 // TODO: must build to account for localization 164 if (!showHundredths) { 165 if (!neg && hundreds != 0) { 166 seconds++; 167 if (seconds == 60) { 168 seconds = 0; 169 minutes++; 170 if (minutes == 60) { 171 minutes = 0; 172 hours++; 173 } 174 } 175 } 176 if (hundreds < 10 || hundreds > 90) { 177 update = true; 178 } 179 } 180 181 if (hours >= 10) { 182 format = showNeg ? NEG_TWO_DIGITS : TWO_DIGITS; 183 mHours = String.format(format, hours); 184 } else if (hours > 0) { 185 format = showNeg ? NEG_ONE_DIGIT : ONE_DIGIT; 186 mHours = String.format(format, hours); 187 } else { 188 mHours = null; 189 } 190 191 if (minutes >= 10 || hours > 0) { 192 format = (showNeg && hours == 0) ? NEG_TWO_DIGITS : TWO_DIGITS; 193 mMinutes = String.format(format, minutes); 194 } else { 195 format = (showNeg && hours == 0) ? NEG_ONE_DIGIT : ONE_DIGIT; 196 mMinutes = String.format(format, minutes); 197 } 198 199 mSeconds = String.format(TWO_DIGITS, seconds); 200 if (showHundredths) { 201 mHunderdths = String.format(TWO_DIGITS, hundreds); 202 } else { 203 mHunderdths = null; 204 } 205 mRemeasureText = true; 206 207 if (update) { 208 setContentDescription(getTimeStringForAccessibility((int) hours, (int) minutes, 209 (int) seconds, showNeg, getResources())); 210 invalidate(); 211 } 212 } 213 private void setTotalTextWidth() { 214 mTotalTextWidth = 0; 215 if (mHours != null) { 216 mHoursWidth = mPaintBig.measureText(mHours); 217 mTotalTextWidth += mHoursWidth; 218 mHoursLabelWidth = mPaintLabel.measureText(mHoursLabel); 219 mTotalTextWidth += mHoursLabelWidth; 220 } 221 if (mMinutes != null) { 222 mMinutesWidth = mPaintBig.measureText(mMinutes); 223 mTotalTextWidth += mMinutesWidth; 224 mMinutesLabelWidth = mPaintLabel.measureText(mMinutesLabel); 225 mTotalTextWidth += mMinutesLabelWidth; 226 } 227 if (mSeconds != null) { 228 mSecondsWidth = mPaintBigThin.measureText(mSeconds); 229 mTotalTextWidth += mSecondsWidth; 230 mSecondsLabelWidth = mPaintLabel.measureText(mSecondsLabel); 231 mTotalTextWidth += mSecondsLabelWidth; 232 } 233 if (mHunderdths != null) { 234 mHundredthsWidth = mPaintMed.measureText(mHunderdths); 235 mTotalTextWidth += mHundredthsWidth; 236 mHundredthsSepWidth = mPaintLabel.measureText(HUNDREDTH_SEPERATOR); 237 mTotalTextWidth += mHundredthsSepWidth; 238 } 239 240 // This is a hack: if the text is too wide, reduce all the paint text sizes 241 // To determine the maximum width, we find the minimum of the height and width (since the 242 // circle we are trying to fit the text into has its radius sized to the smaller of the 243 // two. 244 int width = Math.min(getWidth(), getHeight()); 245 if (width != 0) { 246 float ratio = mTotalTextWidth / width; 247 if (ratio > TEXT_SIZE_TO_WIDTH_RATIO) { 248 float sizeRatio = (TEXT_SIZE_TO_WIDTH_RATIO / ratio); 249 mPaintBig.setTextSize( mPaintBig.getTextSize() * sizeRatio); 250 mPaintBigThin.setTextSize( mPaintBigThin.getTextSize() * sizeRatio); 251 mPaintMed.setTextSize( mPaintMed.getTextSize() * sizeRatio); 252 mTotalTextWidth *= sizeRatio; 253 mMinutesWidth *= sizeRatio; 254 mHoursWidth *= sizeRatio; 255 mSecondsWidth *= sizeRatio; 256 mHundredthsWidth *= sizeRatio; 257 mHundredthsSepWidth *= sizeRatio; 258 //recalculate the new total text width and half text height 259 mTotalTextWidth = mHoursWidth + mMinutesWidth + mSecondsWidth + 260 mHundredthsWidth + mHundredthsSepWidth + mHoursLabelWidth + 261 mMinutesLabelWidth + mSecondsLabelWidth; 262 mTextHeight = mPaintBig.getTextSize(); 263 } 264 } 265 } 266 267 public void blinkTimeStr(boolean blink) { 268 if (blink) { 269 removeCallbacks(mBlinkThread); 270 postDelayed(mBlinkThread, 1000); 271 } else { 272 removeCallbacks(mBlinkThread); 273 showTime(true); 274 } 275 } 276 277 public void showTime(boolean visible) { 278 mShowTimeStr = visible; 279 invalidate(); 280 mRemeasureText = true; 281 } 282 283 public void redTimeStr(boolean red, boolean forceUpdate) { 284 mDefaultColor = red ? mRedColor : mWhiteColor; 285 setTextColor(mDefaultColor); 286 if (forceUpdate) { 287 invalidate(); 288 } 289 } 290 291 public String getTimeString() { 292 if (mHours == null) { 293 return String.format("%s:%s.%s",mMinutes, mSeconds, mHunderdths); 294 } 295 return String.format("%s:%s:%s.%s",mHours, mMinutes, mSeconds, mHunderdths); 296 } 297 298 private static String getTimeStringForAccessibility(int hours, int minutes, int seconds, 299 boolean showNeg, Resources r) { 300 StringBuilder s = new StringBuilder(); 301 if (showNeg) { 302 // This must be followed by a non-zero number or it will be audible as "hyphen" 303 // instead of "minus". 304 s.append("-"); 305 } 306 if (showNeg && hours == 0 && minutes == 0) { 307 // Non-negative time will always have minutes, eg. "0 minutes 7 seconds", but negative 308 // time must start with non-zero digit, eg. -0m7s will be audible as just "-7 seconds" 309 s.append(String.format( 310 r.getQuantityText(R.plurals.Nseconds_description, seconds).toString(), 311 seconds)); 312 } else if (hours == 0) { 313 s.append(String.format( 314 r.getQuantityText(R.plurals.Nminutes_description, minutes).toString(), 315 minutes)); 316 s.append(" "); 317 s.append(String.format( 318 r.getQuantityText(R.plurals.Nseconds_description, seconds).toString(), 319 seconds)); 320 } else { 321 s.append(String.format( 322 r.getQuantityText(R.plurals.Nhours_description, hours).toString(), 323 hours)); 324 s.append(" "); 325 s.append(String.format( 326 r.getQuantityText(R.plurals.Nminutes_description, minutes).toString(), 327 minutes)); 328 s.append(" "); 329 s.append(String.format( 330 r.getQuantityText(R.plurals.Nseconds_description, seconds).toString(), 331 seconds)); 332 } 333 return s.toString(); 334 } 335 336 public void setVirtualButtonEnabled(boolean enabled) { 337 mVirtualButtonEnabled = enabled; 338 } 339 340 private void virtualButtonPressed(boolean pressedOn) { 341 mVirtualButtonPressedOn = pressedOn; 342 mStopStartTextView.setTextColor(pressedOn ? mPressedColor : mWhiteColor); 343 invalidate(); 344 } 345 346 private boolean withinVirtualButtonBounds(float x, float y) { 347 int width = getWidth(); 348 int height = getHeight(); 349 float centerX = width / 2; 350 float centerY = height / 2; 351 float radius = Math.min(width, height) / 2; 352 353 // Within the circle button if distance to the center is less than the radius. 354 double distance = Math.sqrt(Math.pow(centerX - x, 2) + Math.pow(centerY - y, 2)); 355 return distance < radius; 356 } 357 358 public void registerVirtualButtonAction(final Runnable runnable) { 359 if (!mAccessibilityManager.isEnabled()) { 360 this.setOnTouchListener(new OnTouchListener() { 361 @Override 362 public boolean onTouch(View v, MotionEvent event) { 363 if (mVirtualButtonEnabled) { 364 switch (event.getAction()) { 365 case MotionEvent.ACTION_DOWN: 366 if (withinVirtualButtonBounds(event.getX(), event.getY())) { 367 virtualButtonPressed(true); 368 return true; 369 } else { 370 virtualButtonPressed(false); 371 return false; 372 } 373 case MotionEvent.ACTION_CANCEL: 374 virtualButtonPressed(false); 375 return true; 376 case MotionEvent.ACTION_OUTSIDE: 377 virtualButtonPressed(false); 378 return false; 379 case MotionEvent.ACTION_UP: 380 virtualButtonPressed(false); 381 if (withinVirtualButtonBounds(event.getX(), event.getY())) { 382 runnable.run(); 383 } 384 return true; 385 } 386 } 387 return false; 388 } 389 }); 390 } else { 391 this.setOnClickListener(new OnClickListener() { 392 @Override 393 public void onClick(View v) { 394 runnable.run(); 395 } 396 }); 397 } 398 } 399 400 @Override 401 public void onDraw(Canvas canvas) { 402 // Blink functionality. 403 if (!mShowTimeStr && !mVirtualButtonPressedOn) { 404 return; 405 } 406 407 int width = getWidth(); 408 if (mRemeasureText && width != 0) { 409 setTotalTextWidth(); 410 width = getWidth(); 411 mRemeasureText = false; 412 } 413 414 int xCenter = width / 2; 415 int yCenter = getHeight() / 2; 416 417 float textXstart = xCenter - mTotalTextWidth / 2; 418 float textYstart = yCenter + mTextHeight/2 - (mTextHeight * FONT_VERTICAL_OFFSET); 419 // align the labels vertically to the top of the rest of the text 420 float labelYStart = textYstart - (mTextHeight * (1 - 2 * FONT_VERTICAL_OFFSET)) 421 + (1 - 2 * FONT_VERTICAL_OFFSET) * mPaintLabel.getTextSize(); 422 423 // Text color differs based on pressed state. 424 int textColor; 425 if (mVirtualButtonPressedOn) { 426 textColor = mPressedColor; 427 mStopStartTextView.setTextColor(mPressedColor); 428 } else { 429 textColor = mDefaultColor; 430 } 431 mPaintBig.setColor(textColor); 432 mPaintBigThin.setColor(textColor); 433 mPaintLabel.setColor(textColor); 434 mPaintMed.setColor(textColor); 435 436 if (mHours != null) { 437 canvas.drawText(mHours, textXstart, textYstart, mPaintBig); 438 textXstart += mHoursWidth; 439 canvas.drawText(mHoursLabel, textXstart, labelYStart, mPaintLabel); 440 textXstart += mHoursLabelWidth; 441 } 442 if (mMinutes != null) { 443 canvas.drawText(mMinutes, textXstart, textYstart, mPaintBig); 444 textXstart += mMinutesWidth; 445 canvas.drawText(mMinutesLabel, textXstart, labelYStart, mPaintLabel); 446 textXstart += mMinutesLabelWidth; 447 } 448 if (mSeconds != null) { 449 canvas.drawText(mSeconds, textXstart, textYstart, mPaintBigThin); 450 textXstart += mSecondsWidth; 451 canvas.drawText(mSecondsLabel, textXstart, labelYStart, mPaintLabel); 452 textXstart += mSecondsLabelWidth; 453 } 454 if (mHunderdths != null) { 455 canvas.drawText(HUNDREDTH_SEPERATOR, textXstart, textYstart, mPaintLabel); 456 textXstart += mHundredthsSepWidth; 457 canvas.drawText(mHunderdths, textXstart, textYstart, mPaintMed); 458 } 459 } 460 461 public void registerStopTextView(TextView stopStartTextView) { 462 mStopStartTextView = stopStartTextView; 463 } 464} 465