Home | History | Annotate | Download | only in timer
      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 
    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 
    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 
    137         protected void updateWidth(final String time) {
    138             mEm = mPaint.measureText(mWidest);
    139             mLabelWidth = mSpacingRatio * mEm;
    140             mWidth = time.length() * mEm;
    141         }
    142 
    143         protected void resetWidth() {
    144             mWidth = mLabelWidth = 0;
    145         }
    146 
    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 
    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          */
    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          */
    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 {
    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
    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 
    218         public SignedTime (UnsignedTime unsignedTime, float spacingRatio) {
    219             super(unsignedTime, spacingRatio);
    220         }
    221 
    222         @Override
    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
    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
    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")
    260     public CountingTimerView(Context context) {
    261         this(context, null);
    262     }
    263 
    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 
    303     protected void resetTextSize() {
    304         mTextHeight = mBigFontSize;
    305         mPaintBigThin.setTextSize(mBigFontSize);
    306         mPaintMed.setTextSize(mSmallFontSize);
    307     }
    308 
    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
    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 
    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 
    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      */
    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      */
    465     private float getHypotenuseSquared() {
    466         return mTotalTextWidth * mTotalTextWidth + mTextHeight * mTextHeight;
    467     }
    468 
    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 
    479     public void showTime(boolean visible) {
    480         mShowTimeStr = visible;
    481         invalidate();
    482     }
    483 
    484     public void setTimeStrTextColor(boolean active, boolean forceUpdate) {
    485         mDefaultColor = active ? mAccentColor : mWhiteColor;
    486         setTextColor(mDefaultColor);
    487         if (forceUpdate) {
    488             invalidate();
    489         }
    490     }
    491 
    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 
    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 
    544     public void setVirtualButtonEnabled(boolean enabled) {
    545         mVirtualButtonEnabled = enabled;
    546     }
    547 
    548     private void virtualButtonPressed(boolean pressedOn) {
    549         mVirtualButtonPressedOn = pressedOn;
    550         invalidate();
    551     }
    552 
    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 
    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
    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
    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