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 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 
    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 
    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 
    138         protected void updateWidth(final String time) {
    139             mEm = mPaint.measureText(mWidest);
    140             mLabelWidth = mSpacingRatio * mEm;
    141             mWidth = time.length() * mEm;
    142         }
    143 
    144         protected void resetWidth() {
    145             mWidth = mLabelWidth = 0;
    146         }
    147 
    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 
    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          */
    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          */
    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 {
    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
    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 
    219         public SignedTime (UnsignedTime unsignedTime, float spacingRatio) {
    220             super(unsignedTime, spacingRatio);
    221         }
    222 
    223         @Override
    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
    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
    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")
    261     public CountingTimerView(Context context) {
    262         this(context, null);
    263     }
    264 
    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 
    304     protected void resetTextSize() {
    305         mTextHeight = mBigFontSize;
    306         mPaintBigThin.setTextSize(mBigFontSize);
    307         mPaintMed.setTextSize(mSmallFontSize);
    308     }
    309 
    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
    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 
    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 
    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      */
    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      */
    466     private float getHypotenuseSquared() {
    467         return mTotalTextWidth * mTotalTextWidth + mTextHeight * mTextHeight;
    468     }
    469 
    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 
    480     public void showTime(boolean visible) {
    481         mShowTimeStr = visible;
    482         invalidate();
    483     }
    484 
    485     public void setTimeStrTextColor(boolean active, boolean forceUpdate) {
    486         mDefaultColor = active ? mAccentColor : mWhiteColor;
    487         setTextColor(mDefaultColor);
    488         if (forceUpdate) {
    489             invalidate();
    490         }
    491     }
    492 
    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 
    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 
    545     public void setVirtualButtonEnabled(boolean enabled) {
    546         mVirtualButtonEnabled = enabled;
    547     }
    548 
    549     private void virtualButtonPressed(boolean pressedOn) {
    550         mVirtualButtonPressedOn = pressedOn;
    551         mStopStartTextView.setTextColor(pressedOn ? mPressedColor : mWhiteColor);
    552         invalidate();
    553     }
    554 
    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 
    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
    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
    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 
    661     public void registerStopTextView(TextView stopStartTextView) {
    662         mStopStartTextView = stopStartTextView;
    663     }
    664 }
    665