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.Paint;
23 import android.graphics.Typeface;
24 import android.text.TextUtils;
25 import android.util.AttributeSet;
26 import android.view.MotionEvent;
27 import android.view.View;
28 import android.view.accessibility.AccessibilityManager;
29 import android.widget.TextView;
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         mWhiteColor = r.getColor(R.color.clock_white);
270         mDefaultColor = mWhiteColor;
271         mPressedColor = r.getColor(R.color.hot_pink);
272         mAccentColor = r.getColor(R.color.hot_pink);
273         mBigFontSize = r.getDimension(R.dimen.big_font_size);
274         mSmallFontSize = r.getDimension(R.dimen.small_font_size);
275 
276         Typeface androidClockMonoThin = Typeface.
277                 createFromAsset(context.getAssets(), "fonts/AndroidClockMono-Thin.ttf");
278         mPaintBigThin.setAntiAlias(true);
279         mPaintBigThin.setStyle(Paint.Style.STROKE);
280         mPaintBigThin.setTextAlign(Paint.Align.CENTER);
281         mPaintBigThin.setTypeface(androidClockMonoThin);
282 
283         Typeface androidClockMonoLight = Typeface.
284                 createFromAsset(context.getAssets(), "fonts/AndroidClockMono-Light.ttf");
285         mPaintMed.setAntiAlias(true);
286         mPaintMed.setStyle(Paint.Style.STROKE);
287         mPaintMed.setTextAlign(Paint.Align.CENTER);
288         mPaintMed.setTypeface(androidClockMonoLight);
289 
290         resetTextSize();
291         setTextColor(mDefaultColor);
292 
293         // allDigits will contain ten digits: "0123456789" in the default locale
294         final String allDigits = String.format("%010d", 123456789);
295         mBigSeconds = new UnsignedTime(mPaintBigThin, 0.f, allDigits);
296         mBigHours = new SignedTime(mBigSeconds, HOURS_MINUTES_SPACING);
297         mBigMinutes = new SignedTime(mBigSeconds, HOURS_MINUTES_SPACING);
298         mMedHundredths = new Hundredths(mPaintMed, HUNDREDTHS_SPACING, allDigits);
299 
300         mRadiusOffset = Utils.calculateRadiusOffset(r);
301     }
302 
resetTextSize()303     protected void resetTextSize() {
304         mTextHeight = mBigFontSize;
305         mPaintBigThin.setTextSize(mBigFontSize);
306         mPaintMed.setTextSize(mSmallFontSize);
307     }
308 
setTextColor(int textColor)309     protected void setTextColor(int textColor) {
310         mPaintBigThin.setColor(textColor);
311         mPaintMed.setColor(textColor);
312     }
313 
314     /**
315      * Update the time to display. Separates that time into the hours, minutes, seconds and
316      * hundredths. If update is true, the view is invalidated so that it will draw again.
317      *
318      * @param time new time to display - in milliseconds
319      * @param showHundredths flag to show hundredths resolution
320      * @param update to invalidate the view - otherwise the time is examined to see if it is within
321      *               100 milliseconds of zero seconds and when so, invalidate the view.
322      */
323     // TODO:showHundredths S/B attribute or setter - i.e. unchanging over object life
setTime(long time, boolean showHundredths, boolean update)324     public void setTime(long time, boolean showHundredths, boolean update) {
325         int oldLength = getDigitsLength();
326         boolean neg = false, showNeg = false;
327         String format;
328         if (time < 0) {
329             time = -time;
330             neg = showNeg = true;
331         }
332         long hundreds, seconds, minutes, hours;
333         seconds = time / 1000;
334         hundreds = (time - seconds * 1000) / 10;
335         minutes = seconds / 60;
336         seconds = seconds - minutes * 60;
337         hours = minutes / 60;
338         minutes = minutes - hours * 60;
339         if (hours > 999) {
340             hours = 0;
341         }
342         // The time  can be between 0 and -1 seconds, but the "truncated" equivalent time of hours
343         // and minutes and seconds could be zero, so since we do not show fractions of seconds
344         // when counting down, do not show the minus sign.
345         // TODO:does it matter that we do not look at showHundredths?
346         if (hours == 0 && minutes == 0 && seconds == 0) {
347             showNeg = false;
348         }
349 
350         // Normalize and check if it is 'time' to invalidate
351         if (!showHundredths) {
352             if (!neg && hundreds != 0) {
353                 seconds++;
354                 if (seconds == 60) {
355                     seconds = 0;
356                     minutes++;
357                     if (minutes == 60) {
358                         minutes = 0;
359                         hours++;
360                     }
361                 }
362             }
363             if (hundreds < 10 || hundreds > 90) {
364                 update = true;
365             }
366         }
367 
368         // Hours may be empty
369         if (hours >= 10) {
370             format = showNeg ? NEG_TWO_DIGITS : TWO_DIGITS;
371             mHours = String.format(format, hours);
372         } else if (hours > 0) {
373             format = showNeg ? NEG_ONE_DIGIT : ONE_DIGIT;
374             mHours = String.format(format, hours);
375         } else {
376             mHours = null;
377         }
378 
379         // Minutes are never empty and when hours are non-empty, must be two digits
380         if (minutes >= 10 || hours > 0) {
381             format = (showNeg && hours == 0) ? NEG_TWO_DIGITS : TWO_DIGITS;
382             mMinutes = String.format(format, minutes);
383         } else {
384             format = (showNeg && hours == 0) ? NEG_ONE_DIGIT : ONE_DIGIT;
385             mMinutes = String.format(format, minutes);
386         }
387 
388         // Seconds are always two digits
389         mSeconds = String.format(TWO_DIGITS, seconds);
390 
391         // Hundredths are optional and then two digits
392         if (showHundredths) {
393             mHundredths = String.format(TWO_DIGITS, hundreds);
394         } else {
395             mHundredths = null;
396         }
397 
398         int newLength = getDigitsLength();
399         if (oldLength != newLength) {
400             if (oldLength > newLength) {
401                 resetTextSize();
402             }
403             mRemeasureText = true;
404         }
405 
406         if (update) {
407             setContentDescription(getTimeStringForAccessibility((int) hours, (int) minutes,
408                     (int) seconds, showNeg, getResources()));
409             invalidate();
410         }
411     }
412 
getDigitsLength()413     private int getDigitsLength() {
414         return ((mHours == null) ? 0 : mHours.length())
415                 + ((mMinutes == null) ? 0 : mMinutes.length())
416                 + ((mSeconds == null) ? 0 : mSeconds.length())
417                 + ((mHundredths == null) ? 0 : mHundredths.length());
418     }
419 
calcTotalTextWidth()420     private void calcTotalTextWidth() {
421         mTotalTextWidth = mBigHours.calcTotalWidth(mHours) + mBigMinutes.calcTotalWidth(mMinutes)
422                 + mBigSeconds.calcTotalWidth(mSeconds)
423                 + mMedHundredths.calcTotalWidth(mHundredths);
424     }
425 
426     /**
427      * Adjust the size of the fonts to fit within the the circle and painted object in
428      * {@link com.android.deskclock.CircleTimerView#onDraw(android.graphics.Canvas)}
429      */
setTotalTextWidth()430     private void setTotalTextWidth() {
431         calcTotalTextWidth();
432         // To determine the maximum width, we find the minimum of the height and width (since the
433         // circle we are trying to fit the text into has its radius sized to the smaller of the
434         // two.
435         int width = Math.min(getWidth(), getHeight());
436         if (width != 0) {
437             // Shrink 'width' to account for circle stroke and other painted objects.
438             // Note on the "4 *": (1) To reduce divisions, using the diameter instead of the radius.
439             // (2) The radius of the enclosing circle is reduced by mRadiusOffset and the
440             // text needs to fit within a circle further reduced by mRadiusOffset.
441             width -= (int) (4 * mRadiusOffset + 0.5f);
442 
443             final float wantDiameter2 = TEXT_SIZE_TO_WIDTH_RATIO * width * width;
444             float totalDiameter2 = getHypotenuseSquared();
445 
446             // If the hypotenuse of the bounding box is too large, reduce all the paint text sizes
447             while (totalDiameter2 > wantDiameter2) {
448                 // Convergence is slightly difficult due to quantization in the mTotalTextWidth
449                 // calculation. Reducing the ratio by 1% converges more quickly without excessive
450                 // loss of quality.
451                 float sizeRatio = 0.99f * (float) Math.sqrt(wantDiameter2/totalDiameter2);
452                 mPaintBigThin.setTextSize(mPaintBigThin.getTextSize() * sizeRatio);
453                 mPaintMed.setTextSize(mPaintMed.getTextSize() * sizeRatio);
454                 // Recalculate the new total text height and half-width
455                 mTextHeight = mPaintBigThin.getTextSize();
456                 calcTotalTextWidth();
457                 totalDiameter2 = getHypotenuseSquared();
458             }
459         }
460     }
461 
462     /**
463      * Calculate the square of the diameter to use in {@link CountingTimerView#setTotalTextWidth()}
464      */
getHypotenuseSquared()465     private float getHypotenuseSquared() {
466         return mTotalTextWidth * mTotalTextWidth + mTextHeight * mTextHeight;
467     }
468 
blinkTimeStr(boolean blink)469     public void blinkTimeStr(boolean blink) {
470         if (blink) {
471             removeCallbacks(mBlinkThread);
472             post(mBlinkThread);
473         } else {
474             removeCallbacks(mBlinkThread);
475             showTime(true);
476         }
477     }
478 
showTime(boolean visible)479     public void showTime(boolean visible) {
480         mShowTimeStr = visible;
481         invalidate();
482     }
483 
setTimeStrTextColor(boolean active, boolean forceUpdate)484     public void setTimeStrTextColor(boolean active, boolean forceUpdate) {
485         mDefaultColor = active ? mAccentColor : mWhiteColor;
486         setTextColor(mDefaultColor);
487         if (forceUpdate) {
488             invalidate();
489         }
490     }
491 
getTimeString()492     public String getTimeString() {
493         // Though only called from Stopwatch Share, so hundredth are never null,
494         // protect the future and check for null mHundredths
495         if (mHundredths == null) {
496             if (mHours == null) {
497                 return String.format("%s:%s", mMinutes, mSeconds);
498             }
499             return String.format("%s:%s:%s", mHours, mMinutes, mSeconds);
500         } else if (mHours == null) {
501             return String.format("%s:%s.%s", mMinutes, mSeconds, mHundredths);
502         }
503         return String.format("%s:%s:%s.%s", mHours, mMinutes, mSeconds, mHundredths);
504     }
505 
getTimeStringForAccessibility(int hours, int minutes, int seconds, boolean showNeg, Resources r)506     private static String getTimeStringForAccessibility(int hours, int minutes, int seconds,
507             boolean showNeg, Resources r) {
508         StringBuilder s = new StringBuilder();
509         if (showNeg) {
510             // This must be followed by a non-zero number or it will be audible as "hyphen"
511             // instead of "minus".
512             s.append("-");
513         }
514         if (showNeg && hours == 0 && minutes == 0) {
515             // Non-negative time will always have minutes, eg. "0 minutes 7 seconds", but negative
516             // time must start with non-zero digit, eg. -0m7s will be audible as just "-7 seconds"
517             s.append(String.format(
518                     r.getQuantityText(R.plurals.Nseconds_description, seconds).toString(),
519                     seconds));
520         } else if (hours == 0) {
521             s.append(String.format(
522                     r.getQuantityText(R.plurals.Nminutes_description, minutes).toString(),
523                     minutes));
524             s.append(" ");
525             s.append(String.format(
526                     r.getQuantityText(R.plurals.Nseconds_description, seconds).toString(),
527                     seconds));
528         } else {
529             s.append(String.format(
530                     r.getQuantityText(R.plurals.Nhours_description, hours).toString(),
531                     hours));
532             s.append(" ");
533             s.append(String.format(
534                     r.getQuantityText(R.plurals.Nminutes_description, minutes).toString(),
535                     minutes));
536             s.append(" ");
537             s.append(String.format(
538                     r.getQuantityText(R.plurals.Nseconds_description, seconds).toString(),
539                     seconds));
540         }
541         return s.toString();
542     }
543 
setVirtualButtonEnabled(boolean enabled)544     public void setVirtualButtonEnabled(boolean enabled) {
545         mVirtualButtonEnabled = enabled;
546     }
547 
virtualButtonPressed(boolean pressedOn)548     private void virtualButtonPressed(boolean pressedOn) {
549         mVirtualButtonPressedOn = pressedOn;
550         invalidate();
551     }
552 
withinVirtualButtonBounds(float x, float y)553     private boolean withinVirtualButtonBounds(float x, float y) {
554         int width = getWidth();
555         int height = getHeight();
556         float centerX = width / 2;
557         float centerY = height / 2;
558         float radius = Math.min(width, height) / 2;
559 
560         // Within the circle button if distance to the center is less than the radius.
561         double distance = Math.sqrt(Math.pow(centerX - x, 2) + Math.pow(centerY - y, 2));
562         return distance < radius;
563     }
564 
registerVirtualButtonAction(final Runnable runnable)565     public void registerVirtualButtonAction(final Runnable runnable) {
566         if (!mAccessibilityManager.isEnabled()) {
567             this.setOnTouchListener(new OnTouchListener() {
568                 @Override
569                 public boolean onTouch(View v, MotionEvent event) {
570                     if (mVirtualButtonEnabled) {
571                         switch (event.getAction()) {
572                             case MotionEvent.ACTION_DOWN:
573                                 if (withinVirtualButtonBounds(event.getX(), event.getY())) {
574                                     virtualButtonPressed(true);
575                                     return true;
576                                 } else {
577                                     virtualButtonPressed(false);
578                                     return false;
579                                 }
580                             case MotionEvent.ACTION_CANCEL:
581                                 virtualButtonPressed(false);
582                                 return true;
583                             case MotionEvent.ACTION_OUTSIDE:
584                                 virtualButtonPressed(false);
585                                 return false;
586                             case MotionEvent.ACTION_UP:
587                                 virtualButtonPressed(false);
588                                 if (withinVirtualButtonBounds(event.getX(), event.getY())) {
589                                     runnable.run();
590                                 }
591                                 return true;
592                         }
593                     }
594                     return false;
595                 }
596             });
597         } else {
598             this.setOnClickListener(new OnClickListener() {
599                 @Override
600                 public void onClick(View v) {
601                     runnable.run();
602                 }
603             });
604         }
605     }
606 
607     @Override
onDraw(Canvas canvas)608     public void onDraw(Canvas canvas) {
609         // Blink functionality.
610         if (!mShowTimeStr && !mVirtualButtonPressedOn) {
611             return;
612         }
613 
614         int width = getWidth();
615         if (mRemeasureText && width != 0) {
616             setTotalTextWidth();
617             width = getWidth();
618             mRemeasureText = false;
619         }
620 
621         int xCenter = width / 2;
622         int yCenter = getHeight() / 2;
623 
624         float xTextStart = xCenter - mTotalTextWidth / 2;
625         float yTextStart = yCenter + mTextHeight/2 - (mTextHeight * FONT_VERTICAL_OFFSET);
626 
627         // Text color differs based on pressed state.
628         final int textColor = mVirtualButtonPressedOn ? mPressedColor : mDefaultColor;
629         mPaintBigThin.setColor(textColor);
630         mPaintMed.setColor(textColor);
631 
632         if (mHours != null) {
633             xTextStart = mBigHours.draw(canvas, mHours, xTextStart, yTextStart);
634         }
635         if (mMinutes != null) {
636             xTextStart = mBigMinutes.draw(canvas, mMinutes, xTextStart, yTextStart);
637         }
638         if (mSeconds != null) {
639             xTextStart = mBigSeconds.draw(canvas, mSeconds, xTextStart, yTextStart);
640         }
641         if (mHundredths != null) {
642             mMedHundredths.draw(canvas, mHundredths, xTextStart, yTextStart);
643         }
644     }
645 
646     @Override
onSizeChanged(int w, int h, int oldw, int oldh)647     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
648         super.onSizeChanged(w, h, oldw, oldh);
649         mRemeasureText = true;
650         resetTextSize();
651     }
652 }
653