• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2012 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.deskclock.timer;
18 
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.graphics.Canvas;
22 import android.graphics.Color;
23 import android.graphics.Paint;
24 import android.graphics.Typeface;
25 import android.text.TextUtils;
26 import android.util.AttributeSet;
27 import android.view.MotionEvent;
28 import android.view.View;
29 import android.view.accessibility.AccessibilityManager;
30 
31 import com.android.deskclock.LogUtils;
32 import com.android.deskclock.R;
33 import com.android.deskclock.Utils;
34 
35 
36 /**
37  * Class to measure and draw the time in the {@link com.android.deskclock.CircleTimerView}.
38  * This class manages and sums the work of the four members mBigHours, mBigMinutes,
39  * mBigSeconds and mMedHundredths. Those members are each tasked with measuring, sizing and
40  * drawing digits (and optional label) of the time set in {@link #setTime(long, boolean, boolean)}
41  */
42 public class CountingTimerView extends View {
43     private static final String TWO_DIGITS = "%02d";
44     private static final String ONE_DIGIT = "%01d";
45     private static final String NEG_TWO_DIGITS = "-%02d";
46     private static final String NEG_ONE_DIGIT = "-%01d";
47     private static final float TEXT_SIZE_TO_WIDTH_RATIO = 0.85f;
48     // This is the ratio of the font height needed to vertically offset the font for alignment
49     // from the center.
50     private static final float FONT_VERTICAL_OFFSET = 0.14f;
51     // Ratio of the space trailing the Hours and Minutes
52     private static final float HOURS_MINUTES_SPACING = 0.4f;
53     // Ratio of the space leading the Hundredths
54     private static final float HUNDREDTHS_SPACING = 0.5f;
55     // Radial offset of the enclosing circle
56     private final float mRadiusOffset;
57 
58     private String mHours, mMinutes, mSeconds, mHundredths;
59 
60     private boolean mShowTimeStr = true;
61     private final Paint mPaintBigThin = new Paint();
62     private final Paint mPaintMed = new Paint();
63     private final float mBigFontSize, mSmallFontSize;
64     // Hours and minutes are signed for when a timer goes past the set time and thus negative
65     private final SignedTime mBigHours, mBigMinutes;
66     // Seconds are always shown with minutes, so are never signed
67     private final UnsignedTime mBigSeconds;
68     private final Hundredths mMedHundredths;
69     private float mTextHeight = 0;
70     private float mTotalTextWidth;
71     private boolean mRemeasureText = true;
72 
73     private int mDefaultColor;
74     private final int mPressedColor;
75     private final int mWhiteColor;
76     private final int mAccentColor;
77     private final AccessibilityManager mAccessibilityManager;
78 
79     // Fields for the text serving as a virtual button.
80     private boolean mVirtualButtonEnabled = false;
81     private boolean mVirtualButtonPressedOn = false;
82 
83     Runnable mBlinkThread = new Runnable() {
84         private boolean mVisible = true;
85         @Override
86         public void run() {
87             mVisible = !mVisible;
88             CountingTimerView.this.showTime(mVisible);
89             postDelayed(mBlinkThread, 500);
90         }
91 
92     };
93 
94     /**
95      * Class to measure and draw the digit pairs of hours, minutes, seconds or hundredths. Digits
96      * may have an optional label. for hours, minutes and seconds, this label trails the digits
97      * and for seconds, precedes the digits.
98      */
99     static class UnsignedTime {
100         protected Paint mPaint;
101         protected float mEm;
102         protected float mWidth = 0;
103         private final String mWidest;
104         protected final float mSpacingRatio;
105         private float mLabelWidth = 0;
106 
UnsignedTime(Paint paint, float spacingRatio, String allDigits)107         public UnsignedTime(Paint paint, float spacingRatio, String allDigits) {
108             mPaint = paint;
109             mSpacingRatio = spacingRatio;
110 
111             if (TextUtils.isEmpty(allDigits)) {
112                 LogUtils.wtf("Locale digits missing - using English");
113                 allDigits = "0123456789";
114             }
115 
116             float widths[] = new float[allDigits.length()];
117             int ll = mPaint.getTextWidths(allDigits, widths);
118             int largest = 0;
119             for (int ii = 1; ii < ll; ii++) {
120                 if (widths[ii] > widths[largest]) {
121                     largest = ii;
122                 }
123             }
124 
125             mEm = widths[largest];
126             mWidest = allDigits.substring(largest, largest + 1);
127         }
128 
UnsignedTime(UnsignedTime unsignedTime, float spacingRatio)129         public UnsignedTime(UnsignedTime unsignedTime, float spacingRatio) {
130             this.mPaint = unsignedTime.mPaint;
131             this.mEm = unsignedTime.mEm;
132             this.mWidth = unsignedTime.mWidth;
133             this.mWidest = unsignedTime.mWidest;
134             this.mSpacingRatio = spacingRatio;
135         }
136 
updateWidth(final String time)137         protected void updateWidth(final String time) {
138             mEm = mPaint.measureText(mWidest);
139             mLabelWidth = mSpacingRatio * mEm;
140             mWidth = time.length() * mEm;
141         }
142 
resetWidth()143         protected void resetWidth() {
144             mWidth = mLabelWidth = 0;
145         }
146 
calcTotalWidth(final String time)147         public float calcTotalWidth(final String time) {
148             if (time != null) {
149                 updateWidth(time);
150                 return mWidth + mLabelWidth;
151             } else {
152                 resetWidth();
153                 return 0;
154             }
155         }
156 
getLabelWidth()157         public float getLabelWidth() {
158             return mLabelWidth;
159         }
160 
161         /**
162          * Draws each character with a fixed spacing from time starting at ii.
163          * @param canvas the canvas on which the time segment will be drawn
164          * @param time time segment
165          * @param ii what character to start the draw
166          * @param x offset
167          * @param y offset
168          * @return X location for the next segment
169          */
drawTime(Canvas canvas, final String time, int ii, float x, float y)170         protected float drawTime(Canvas canvas, final String time, int ii, float x, float y) {
171             float textEm  = mEm / 2f;
172             while (ii < time.length()) {
173                 x += textEm;
174                 canvas.drawText(time.substring(ii, ii + 1), x, y, mPaint);
175                 x += textEm;
176                 ii++;
177             }
178             return x;
179         }
180 
181         /**
182          * Draw this time segment and append the intra-segment spacing to the x
183          * @param canvas the canvas on which the time segment will be drawn
184          * @param time time segment
185          * @param x offset
186          * @param y offset
187          * @return X location for the next segment
188          */
draw(Canvas canvas, final String time, float x, float y)189         public float draw(Canvas canvas, final String time, float x, float y) {
190             return drawTime(canvas, time, 0, x, y) + getLabelWidth();
191         }
192     }
193 
194     /**
195      * Special derivation to handle the hundredths painting with the label in front.
196      */
197     static class Hundredths extends UnsignedTime {
Hundredths(Paint paint, float spacingRatio, final String allDigits)198         public Hundredths(Paint paint, float spacingRatio, final String allDigits) {
199             super(paint, spacingRatio, allDigits);
200         }
201 
202         /**
203          * Draw this time segment after prepending the intra-segment spacing to the x location.
204          * {@link UnsignedTime#draw(android.graphics.Canvas, String, float, float)}
205          */
206         @Override
draw(Canvas canvas, final String time, float x, float y)207         public float draw(Canvas canvas, final String time, float x, float y) {
208             return drawTime(canvas, time, 0, x + getLabelWidth(), y);
209         }
210     }
211 
212     /**
213      * Special derivation to handle a negative number
214      */
215     static class SignedTime extends UnsignedTime {
216         private float mMinusWidth = 0;
217 
SignedTime(UnsignedTime unsignedTime, float spacingRatio)218         public SignedTime (UnsignedTime unsignedTime, float spacingRatio) {
219             super(unsignedTime, spacingRatio);
220         }
221 
222         @Override
updateWidth(final String time)223         protected void updateWidth(final String time) {
224             super.updateWidth(time);
225             if (time.contains("-")) {
226                 mMinusWidth = mPaint.measureText("-");
227                 mWidth += (mMinusWidth - mEm);
228             } else {
229                 mMinusWidth = 0;
230             }
231         }
232 
233         @Override
resetWidth()234         protected void resetWidth() {
235             super.resetWidth();
236             mMinusWidth = 0;
237         }
238 
239         /**
240          * Draws each character with a fixed spacing from time, handling the special negative
241          * number case.
242          * {@link UnsignedTime#draw(android.graphics.Canvas, String, float, float)}
243          */
244         @Override
draw(Canvas canvas, final String time, float x, float y)245         public float draw(Canvas canvas, final String time, float x, float y) {
246             int ii = 0;
247             if (mMinusWidth != 0f) {
248                 float minusWidth = mMinusWidth / 2;
249                 x += minusWidth;
250                 //TODO:hyphen is too thick when painted
251                 canvas.drawText(time.substring(0, 1), x, y, mPaint);
252                 x += minusWidth;
253                 ii++;
254             }
255             return drawTime(canvas, time, ii, x, y) + getLabelWidth();
256         }
257     }
258 
259     @SuppressWarnings("unused")
CountingTimerView(Context context)260     public CountingTimerView(Context context) {
261         this(context, null);
262     }
263 
CountingTimerView(Context context, AttributeSet attrs)264     public CountingTimerView(Context context, AttributeSet attrs) {
265         super(context, attrs);
266         mAccessibilityManager =
267                 (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
268         Resources r = context.getResources();
269         mDefaultColor = mWhiteColor = r.getColor(R.color.clock_white);
270         mPressedColor = mAccentColor = Utils.obtainStyledColor(
271                 context, R.attr.colorAccent, Color.RED);
272         mBigFontSize = r.getDimension(R.dimen.big_font_size);
273         mSmallFontSize = r.getDimension(R.dimen.small_font_size);
274 
275         Typeface androidClockMonoThin = Typeface.
276                 createFromAsset(context.getAssets(), "fonts/AndroidClockMono-Thin.ttf");
277         mPaintBigThin.setAntiAlias(true);
278         mPaintBigThin.setStyle(Paint.Style.STROKE);
279         mPaintBigThin.setTextAlign(Paint.Align.CENTER);
280         mPaintBigThin.setTypeface(androidClockMonoThin);
281 
282         Typeface androidClockMonoLight = Typeface.
283                 createFromAsset(context.getAssets(), "fonts/AndroidClockMono-Light.ttf");
284         mPaintMed.setAntiAlias(true);
285         mPaintMed.setStyle(Paint.Style.STROKE);
286         mPaintMed.setTextAlign(Paint.Align.CENTER);
287         mPaintMed.setTypeface(androidClockMonoLight);
288 
289         resetTextSize();
290         setTextColor(mDefaultColor);
291 
292         // allDigits will contain ten digits: "0123456789" in the default locale
293         final String allDigits = String.format("%010d", 123456789);
294         mBigSeconds = new UnsignedTime(mPaintBigThin, 0.f, allDigits);
295         mBigHours = new SignedTime(mBigSeconds, HOURS_MINUTES_SPACING);
296         mBigMinutes = new SignedTime(mBigSeconds, HOURS_MINUTES_SPACING);
297         mMedHundredths = new Hundredths(mPaintMed, HUNDREDTHS_SPACING, allDigits);
298 
299         mRadiusOffset = Utils.calculateRadiusOffset(r);
300     }
301 
resetTextSize()302     protected void resetTextSize() {
303         mTextHeight = mBigFontSize;
304         mPaintBigThin.setTextSize(mBigFontSize);
305         mPaintMed.setTextSize(mSmallFontSize);
306     }
307 
setTextColor(int textColor)308     protected void setTextColor(int textColor) {
309         mPaintBigThin.setColor(textColor);
310         mPaintMed.setColor(textColor);
311     }
312 
313     /**
314      * Update the time to display. Separates that time into the hours, minutes, seconds and
315      * hundredths. If update is true, the view is invalidated so that it will draw again.
316      *
317      * @param time new time to display - in milliseconds
318      * @param showHundredths flag to show hundredths resolution
319      * @param update to invalidate the view - otherwise the time is examined to see if it is within
320      *               100 milliseconds of zero seconds and when so, invalidate the view.
321      */
322     // TODO:showHundredths S/B attribute or setter - i.e. unchanging over object life
setTime(long time, boolean showHundredths, boolean update)323     public void setTime(long time, boolean showHundredths, boolean update) {
324         int oldLength = getDigitsLength();
325         boolean neg = false, showNeg = false;
326         String format;
327         if (time < 0) {
328             time = -time;
329             neg = showNeg = true;
330         }
331         long hundreds, seconds, minutes, hours;
332         seconds = time / 1000;
333         hundreds = (time - seconds * 1000) / 10;
334         minutes = seconds / 60;
335         seconds = seconds - minutes * 60;
336         hours = minutes / 60;
337         minutes = minutes - hours * 60;
338         if (hours > 999) {
339             hours = 0;
340         }
341         // The time  can be between 0 and -1 seconds, but the "truncated" equivalent time of hours
342         // and minutes and seconds could be zero, so since we do not show fractions of seconds
343         // when counting down, do not show the minus sign.
344         // TODO:does it matter that we do not look at showHundredths?
345         if (hours == 0 && minutes == 0 && seconds == 0) {
346             showNeg = false;
347         }
348 
349         // Normalize and check if it is 'time' to invalidate
350         if (!showHundredths) {
351             if (!neg && hundreds != 0) {
352                 seconds++;
353                 if (seconds == 60) {
354                     seconds = 0;
355                     minutes++;
356                     if (minutes == 60) {
357                         minutes = 0;
358                         hours++;
359                     }
360                 }
361             }
362             if (hundreds < 10 || hundreds > 90) {
363                 update = true;
364             }
365         }
366 
367         // Hours may be empty
368         if (hours >= 10) {
369             format = showNeg ? NEG_TWO_DIGITS : TWO_DIGITS;
370             mHours = String.format(format, hours);
371         } else if (hours > 0) {
372             format = showNeg ? NEG_ONE_DIGIT : ONE_DIGIT;
373             mHours = String.format(format, hours);
374         } else {
375             mHours = null;
376         }
377 
378         // Minutes are never empty and when hours are non-empty, must be two digits
379         if (minutes >= 10 || hours > 0) {
380             format = (showNeg && hours == 0) ? NEG_TWO_DIGITS : TWO_DIGITS;
381             mMinutes = String.format(format, minutes);
382         } else {
383             format = (showNeg && hours == 0) ? NEG_ONE_DIGIT : ONE_DIGIT;
384             mMinutes = String.format(format, minutes);
385         }
386 
387         // Seconds are always two digits
388         mSeconds = String.format(TWO_DIGITS, seconds);
389 
390         // Hundredths are optional and then two digits
391         if (showHundredths) {
392             mHundredths = String.format(TWO_DIGITS, hundreds);
393         } else {
394             mHundredths = null;
395         }
396 
397         int newLength = getDigitsLength();
398         if (oldLength != newLength) {
399             if (oldLength > newLength) {
400                 resetTextSize();
401             }
402             mRemeasureText = true;
403         }
404 
405         if (update) {
406             setContentDescription(getTimeStringForAccessibility((int) hours, (int) minutes,
407                     (int) seconds, showNeg, getResources()));
408             postInvalidateOnAnimation();
409         }
410     }
411 
getDigitsLength()412     private int getDigitsLength() {
413         return ((mHours == null) ? 0 : mHours.length())
414                 + ((mMinutes == null) ? 0 : mMinutes.length())
415                 + ((mSeconds == null) ? 0 : mSeconds.length())
416                 + ((mHundredths == null) ? 0 : mHundredths.length());
417     }
418 
calcTotalTextWidth()419     private void calcTotalTextWidth() {
420         mTotalTextWidth = mBigHours.calcTotalWidth(mHours) + mBigMinutes.calcTotalWidth(mMinutes)
421                 + mBigSeconds.calcTotalWidth(mSeconds)
422                 + mMedHundredths.calcTotalWidth(mHundredths);
423     }
424 
425     /**
426      * Adjust the size of the fonts to fit within the the circle and painted object in
427      * {@link com.android.deskclock.CircleTimerView#onDraw(android.graphics.Canvas)}
428      */
setTotalTextWidth()429     private void setTotalTextWidth() {
430         calcTotalTextWidth();
431         // To determine the maximum width, we find the minimum of the height and width (since the
432         // circle we are trying to fit the text into has its radius sized to the smaller of the
433         // two.
434         int width = Math.min(getWidth(), getHeight());
435         if (width != 0) {
436             // Shrink 'width' to account for circle stroke and other painted objects.
437             // Note on the "4 *": (1) To reduce divisions, using the diameter instead of the radius.
438             // (2) The radius of the enclosing circle is reduced by mRadiusOffset and the
439             // text needs to fit within a circle further reduced by mRadiusOffset.
440             width -= (int) (4 * mRadiusOffset + 0.5f);
441 
442             final float wantDiameter2 = TEXT_SIZE_TO_WIDTH_RATIO * width * width;
443             float totalDiameter2 = getHypotenuseSquared();
444 
445             // If the hypotenuse of the bounding box is too large, reduce all the paint text sizes
446             while (totalDiameter2 > wantDiameter2) {
447                 // Convergence is slightly difficult due to quantization in the mTotalTextWidth
448                 // calculation. Reducing the ratio by 1% converges more quickly without excessive
449                 // loss of quality.
450                 float sizeRatio = 0.99f * (float) Math.sqrt(wantDiameter2/totalDiameter2);
451                 mPaintBigThin.setTextSize(mPaintBigThin.getTextSize() * sizeRatio);
452                 mPaintMed.setTextSize(mPaintMed.getTextSize() * sizeRatio);
453                 // Recalculate the new total text height and half-width
454                 mTextHeight = mPaintBigThin.getTextSize();
455                 calcTotalTextWidth();
456                 totalDiameter2 = getHypotenuseSquared();
457             }
458         }
459     }
460 
461     /**
462      * Calculate the square of the diameter to use in {@link CountingTimerView#setTotalTextWidth()}
463      */
getHypotenuseSquared()464     private float getHypotenuseSquared() {
465         return mTotalTextWidth * mTotalTextWidth + mTextHeight * mTextHeight;
466     }
467 
blinkTimeStr(boolean blink)468     public void blinkTimeStr(boolean blink) {
469         if (blink) {
470             removeCallbacks(mBlinkThread);
471             post(mBlinkThread);
472         } else {
473             removeCallbacks(mBlinkThread);
474             showTime(true);
475         }
476     }
477 
showTime(boolean visible)478     public void showTime(boolean visible) {
479         mShowTimeStr = visible;
480         invalidate();
481     }
482 
setTimeStrTextColor(boolean active, boolean forceUpdate)483     public void setTimeStrTextColor(boolean active, boolean forceUpdate) {
484         mDefaultColor = active ? mAccentColor : mWhiteColor;
485         setTextColor(mDefaultColor);
486         if (forceUpdate) {
487             invalidate();
488         }
489     }
490 
getTimeString()491     public String getTimeString() {
492         // Though only called from Stopwatch Share, so hundredth are never null,
493         // protect the future and check for null mHundredths
494         if (mHundredths == null) {
495             if (mHours == null) {
496                 return String.format("%s:%s", mMinutes, mSeconds);
497             }
498             return String.format("%s:%s:%s", mHours, mMinutes, mSeconds);
499         } else if (mHours == null) {
500             return String.format("%s:%s.%s", mMinutes, mSeconds, mHundredths);
501         }
502         return String.format("%s:%s:%s.%s", mHours, mMinutes, mSeconds, mHundredths);
503     }
504 
getTimeStringForAccessibility(int hours, int minutes, int seconds, boolean showNeg, Resources r)505     private static String getTimeStringForAccessibility(int hours, int minutes, int seconds,
506             boolean showNeg, Resources r) {
507         StringBuilder s = new StringBuilder();
508         if (showNeg) {
509             // This must be followed by a non-zero number or it will be audible as "hyphen"
510             // instead of "minus".
511             s.append("-");
512         }
513         if (showNeg && hours == 0 && minutes == 0) {
514             // Non-negative time will always have minutes, eg. "0 minutes 7 seconds", but negative
515             // time must start with non-zero digit, eg. -0m7s will be audible as just "-7 seconds"
516             s.append(String.format(
517                     r.getQuantityText(R.plurals.Nseconds_description, seconds).toString(),
518                     seconds));
519         } else if (hours == 0) {
520             s.append(String.format(
521                     r.getQuantityText(R.plurals.Nminutes_description, minutes).toString(),
522                     minutes));
523             s.append(" ");
524             s.append(String.format(
525                     r.getQuantityText(R.plurals.Nseconds_description, seconds).toString(),
526                     seconds));
527         } else {
528             s.append(String.format(
529                     r.getQuantityText(R.plurals.Nhours_description, hours).toString(),
530                     hours));
531             s.append(" ");
532             s.append(String.format(
533                     r.getQuantityText(R.plurals.Nminutes_description, minutes).toString(),
534                     minutes));
535             s.append(" ");
536             s.append(String.format(
537                     r.getQuantityText(R.plurals.Nseconds_description, seconds).toString(),
538                     seconds));
539         }
540         return s.toString();
541     }
542 
setVirtualButtonEnabled(boolean enabled)543     public void setVirtualButtonEnabled(boolean enabled) {
544         mVirtualButtonEnabled = enabled;
545     }
546 
virtualButtonPressed(boolean pressedOn)547     private void virtualButtonPressed(boolean pressedOn) {
548         mVirtualButtonPressedOn = pressedOn;
549         invalidate();
550     }
551 
withinVirtualButtonBounds(float x, float y)552     private boolean withinVirtualButtonBounds(float x, float y) {
553         int width = getWidth();
554         int height = getHeight();
555         float centerX = width / 2;
556         float centerY = height / 2;
557         float radius = Math.min(width, height) / 2;
558 
559         // Within the circle button if distance to the center is less than the radius.
560         double distance = Math.sqrt(Math.pow(centerX - x, 2) + Math.pow(centerY - y, 2));
561         return distance < radius;
562     }
563 
registerVirtualButtonAction(final Runnable runnable)564     public void registerVirtualButtonAction(final Runnable runnable) {
565         if (!mAccessibilityManager.isEnabled()) {
566             this.setOnTouchListener(new OnTouchListener() {
567                 @Override
568                 public boolean onTouch(View v, MotionEvent event) {
569                     if (mVirtualButtonEnabled) {
570                         switch (event.getAction()) {
571                             case MotionEvent.ACTION_DOWN:
572                                 if (withinVirtualButtonBounds(event.getX(), event.getY())) {
573                                     virtualButtonPressed(true);
574                                     return true;
575                                 } else {
576                                     virtualButtonPressed(false);
577                                     return false;
578                                 }
579                             case MotionEvent.ACTION_CANCEL:
580                                 virtualButtonPressed(false);
581                                 return true;
582                             case MotionEvent.ACTION_OUTSIDE:
583                                 virtualButtonPressed(false);
584                                 return false;
585                             case MotionEvent.ACTION_UP:
586                                 virtualButtonPressed(false);
587                                 if (withinVirtualButtonBounds(event.getX(), event.getY())) {
588                                     runnable.run();
589                                 }
590                                 return true;
591                         }
592                     }
593                     return false;
594                 }
595             });
596         } else {
597             this.setOnClickListener(new OnClickListener() {
598                 @Override
599                 public void onClick(View v) {
600                     runnable.run();
601                 }
602             });
603         }
604     }
605 
606     @Override
onDraw(Canvas canvas)607     public void onDraw(Canvas canvas) {
608         // Blink functionality.
609         if (!mShowTimeStr && !mVirtualButtonPressedOn) {
610             return;
611         }
612 
613         int width = getWidth();
614         if (mRemeasureText && width != 0) {
615             setTotalTextWidth();
616             width = getWidth();
617             mRemeasureText = false;
618         }
619 
620         int xCenter = width / 2;
621         int yCenter = getHeight() / 2;
622 
623         float xTextStart = xCenter - mTotalTextWidth / 2;
624         float yTextStart = yCenter + mTextHeight/2 - (mTextHeight * FONT_VERTICAL_OFFSET);
625 
626         // Text color differs based on pressed state.
627         final int textColor = mVirtualButtonPressedOn ? mPressedColor : mDefaultColor;
628         mPaintBigThin.setColor(textColor);
629         mPaintMed.setColor(textColor);
630 
631         if (mHours != null) {
632             xTextStart = mBigHours.draw(canvas, mHours, xTextStart, yTextStart);
633         }
634         if (mMinutes != null) {
635             xTextStart = mBigMinutes.draw(canvas, mMinutes, xTextStart, yTextStart);
636         }
637         if (mSeconds != null) {
638             xTextStart = mBigSeconds.draw(canvas, mSeconds, xTextStart, yTextStart);
639         }
640         if (mHundredths != null) {
641             mMedHundredths.draw(canvas, mHundredths, xTextStart, yTextStart);
642         }
643     }
644 
645     @Override
onSizeChanged(int w, int h, int oldw, int oldh)646     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
647         super.onSizeChanged(w, h, oldw, oldh);
648         mRemeasureText = true;
649         resetTextSize();
650     }
651 }
652