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