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