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