Home | History | Annotate | Download | only in timer
      1 /*
      2  * Copyright (C) 2008 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.util.AttributeSet;
     25 import android.view.MotionEvent;
     26 import android.view.View;
     27 import android.view.accessibility.AccessibilityManager;
     28 import android.widget.TextView;
     29 
     30 import com.android.deskclock.DeskClock;
     31 import com.android.deskclock.R;
     32 import com.android.deskclock.Utils;
     33 
     34 
     35 public class CountingTimerView extends View {
     36     private static final String TWO_DIGITS = "%02d";
     37     private static final String ONE_DIGIT = "%01d";
     38     private static final String NEG_TWO_DIGITS = "-%02d";
     39     private static final String NEG_ONE_DIGIT = "-%01d";
     40     private static final float TEXT_SIZE_TO_WIDTH_RATIO = 0.75f;
     41     // This is the ratio of the font typeface we need to offset the font by vertically to align it
     42     // vertically center.
     43     private static final float FONT_VERTICAL_OFFSET = 0.14f;
     44 
     45     private String mHours, mMinutes, mSeconds, mHunderdths;
     46     private final String mHoursLabel, mMinutesLabel, mSecondsLabel;
     47     private float mHoursWidth, mMinutesWidth, mSecondsWidth, mHundredthsWidth;
     48     private float mHoursLabelWidth, mMinutesLabelWidth, mSecondsLabelWidth, mHundredthsSepWidth;
     49 
     50     private boolean mShowTimeStr = true;
     51     private final Typeface mAndroidClockMonoThin, mAndroidClockMonoBold, mRobotoLabel, mAndroidClockMonoLight;
     52     private final Paint mPaintBig = new Paint();
     53     private final Paint mPaintBigThin = new Paint();
     54     private final Paint mPaintMed = new Paint();
     55     private final Paint mPaintLabel = new Paint();
     56     private float mTextHeight = 0;
     57     private float mTotalTextWidth;
     58     private static final String HUNDREDTH_SEPERATOR = ".";
     59     private boolean mRemeasureText = true;
     60 
     61     private int mDefaultColor;
     62     private final int mPressedColor;
     63     private final int mWhiteColor;
     64     private final int mRedColor;
     65     private TextView mStopStartTextView;
     66     private final AccessibilityManager mAccessibilityManager;
     67 
     68     // Fields for the text serving as a virtual button.
     69     private boolean mVirtualButtonEnabled = false;
     70     private boolean mVirtualButtonPressedOn = false;
     71 
     72     Runnable mBlinkThread = new Runnable() {
     73         private boolean mVisible = true;
     74         @Override
     75         public void run() {
     76             mVisible = !mVisible;
     77             CountingTimerView.this.showTime(mVisible);
     78             postDelayed(mBlinkThread, 500);
     79         }
     80 
     81     };
     82 
     83 
     84     public CountingTimerView(Context context) {
     85         this(context, null);
     86     }
     87 
     88     public CountingTimerView(Context context, AttributeSet attrs) {
     89         super(context, attrs);
     90         mAndroidClockMonoThin = Typeface.createFromAsset(context.getAssets(),"fonts/AndroidClockMono-Thin.ttf");
     91         mAndroidClockMonoBold = Typeface.createFromAsset(context.getAssets(),"fonts/AndroidClockMono-Bold.ttf");
     92         mAndroidClockMonoLight = Typeface.createFromAsset(context.getAssets(),"fonts/AndroidClockMono-Light.ttf");
     93         mAccessibilityManager =
     94                 (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
     95         mRobotoLabel= Typeface.create("sans-serif-condensed", Typeface.BOLD);
     96         Resources r = context.getResources();
     97         mHoursLabel = r.getString(R.string.hours_label).toUpperCase();
     98         mMinutesLabel = r.getString(R.string.minutes_label).toUpperCase();
     99         mSecondsLabel = r.getString(R.string.seconds_label).toUpperCase();
    100         mWhiteColor = r.getColor(R.color.clock_white);
    101         mDefaultColor = mWhiteColor;
    102         mPressedColor = r.getColor(Utils.getPressedColorId());
    103         mRedColor = r.getColor(R.color.clock_red);
    104 
    105         mPaintBig.setAntiAlias(true);
    106         mPaintBig.setStyle(Paint.Style.STROKE);
    107         mPaintBig.setTextAlign(Paint.Align.LEFT);
    108         mPaintBig.setTypeface(mAndroidClockMonoBold);
    109         float bigFontSize = r.getDimension(R.dimen.big_font_size);
    110         mPaintBig.setTextSize(bigFontSize);
    111         mTextHeight = bigFontSize;
    112 
    113         mPaintBigThin.setAntiAlias(true);
    114         mPaintBigThin.setStyle(Paint.Style.STROKE);
    115         mPaintBigThin.setTextAlign(Paint.Align.LEFT);
    116         mPaintBigThin.setTypeface(mAndroidClockMonoThin);
    117         mPaintBigThin.setTextSize(r.getDimension(R.dimen.big_font_size));
    118 
    119         mPaintMed.setAntiAlias(true);
    120         mPaintMed.setStyle(Paint.Style.STROKE);
    121         mPaintMed.setTextAlign(Paint.Align.LEFT);
    122         mPaintMed.setTypeface(mAndroidClockMonoLight);
    123         mPaintMed.setTextSize(r.getDimension(R.dimen.small_font_size));
    124 
    125         mPaintLabel.setAntiAlias(true);
    126         mPaintLabel.setStyle(Paint.Style.STROKE);
    127         mPaintLabel.setTextAlign(Paint.Align.LEFT);
    128         mPaintLabel.setTypeface(mRobotoLabel);
    129         mPaintLabel.setTextSize(r.getDimension(R.dimen.label_font_size));
    130 
    131         setTextColor(mDefaultColor);
    132     }
    133 
    134     protected void setTextColor(int textColor) {
    135         mPaintBig.setColor(textColor);
    136         mPaintBigThin.setColor(textColor);
    137         mPaintMed.setColor(textColor);
    138         mPaintLabel.setColor(textColor);
    139     }
    140 
    141     public void setTime(long time, boolean showHundredths, boolean update) {
    142         boolean neg = false, showNeg = false;
    143         String format = null;
    144         if (time < 0) {
    145             time = -time;
    146             neg = showNeg = true;
    147         }
    148         long hundreds, seconds, minutes, hours;
    149         seconds = time / 1000;
    150         hundreds = (time - seconds * 1000) / 10;
    151         minutes = seconds / 60;
    152         seconds = seconds - minutes * 60;
    153         hours = minutes / 60;
    154         minutes = minutes - hours * 60;
    155         if (hours > 99) {
    156             hours = 0;
    157         }
    158         // time may less than a second below zero, since we do not show fractions of seconds
    159         // when counting down, do not show the minus sign.
    160         if (hours ==0 && minutes == 0 && seconds == 0) {
    161             showNeg = false;
    162         }
    163         // TODO: must build to account for localization
    164         if (!showHundredths) {
    165             if (!neg && hundreds != 0) {
    166                 seconds++;
    167                 if (seconds == 60) {
    168                     seconds = 0;
    169                     minutes++;
    170                     if (minutes == 60) {
    171                         minutes = 0;
    172                         hours++;
    173                     }
    174                 }
    175             }
    176             if (hundreds < 10 || hundreds > 90) {
    177                 update = true;
    178             }
    179         }
    180 
    181         if (hours >= 10) {
    182             format = showNeg ? NEG_TWO_DIGITS : TWO_DIGITS;
    183             mHours = String.format(format, hours);
    184         } else if (hours > 0) {
    185             format = showNeg ? NEG_ONE_DIGIT : ONE_DIGIT;
    186             mHours = String.format(format, hours);
    187         } else {
    188             mHours = null;
    189         }
    190 
    191         if (minutes >= 10 || hours > 0) {
    192             format = (showNeg && hours == 0) ? NEG_TWO_DIGITS : TWO_DIGITS;
    193             mMinutes = String.format(format, minutes);
    194         } else {
    195             format = (showNeg && hours == 0) ? NEG_ONE_DIGIT : ONE_DIGIT;
    196             mMinutes = String.format(format, minutes);
    197         }
    198 
    199         mSeconds = String.format(TWO_DIGITS, seconds);
    200         if (showHundredths) {
    201             mHunderdths = String.format(TWO_DIGITS, hundreds);
    202         } else {
    203             mHunderdths = null;
    204         }
    205         mRemeasureText = true;
    206 
    207         if (update) {
    208             setContentDescription(getTimeStringForAccessibility((int) hours, (int) minutes,
    209                     (int) seconds, showNeg, getResources()));
    210             invalidate();
    211         }
    212     }
    213     private void setTotalTextWidth() {
    214         mTotalTextWidth = 0;
    215         if (mHours != null) {
    216             mHoursWidth = mPaintBig.measureText(mHours);
    217             mTotalTextWidth += mHoursWidth;
    218             mHoursLabelWidth = mPaintLabel.measureText(mHoursLabel);
    219             mTotalTextWidth += mHoursLabelWidth;
    220         }
    221         if (mMinutes != null) {
    222             mMinutesWidth =  mPaintBig.measureText(mMinutes);
    223             mTotalTextWidth += mMinutesWidth;
    224             mMinutesLabelWidth = mPaintLabel.measureText(mMinutesLabel);
    225             mTotalTextWidth += mMinutesLabelWidth;
    226         }
    227         if (mSeconds != null) {
    228             mSecondsWidth = mPaintBigThin.measureText(mSeconds);
    229             mTotalTextWidth += mSecondsWidth;
    230             mSecondsLabelWidth = mPaintLabel.measureText(mSecondsLabel);
    231             mTotalTextWidth += mSecondsLabelWidth;
    232         }
    233         if (mHunderdths != null) {
    234             mHundredthsWidth = mPaintMed.measureText(mHunderdths);
    235             mTotalTextWidth += mHundredthsWidth;
    236             mHundredthsSepWidth = mPaintLabel.measureText(HUNDREDTH_SEPERATOR);
    237             mTotalTextWidth += mHundredthsSepWidth;
    238         }
    239 
    240         // This is a hack: if the text is too wide, reduce all the paint text sizes
    241         // To determine the maximum width, we find the minimum of the height and width (since the
    242         // circle we are trying to fit the text into has its radius sized to the smaller of the
    243         // two.
    244         int width = Math.min(getWidth(), getHeight());
    245         if (width != 0) {
    246             float ratio = mTotalTextWidth / width;
    247             if (ratio > TEXT_SIZE_TO_WIDTH_RATIO) {
    248                 float sizeRatio = (TEXT_SIZE_TO_WIDTH_RATIO / ratio);
    249                 mPaintBig.setTextSize( mPaintBig.getTextSize() * sizeRatio);
    250                 mPaintBigThin.setTextSize( mPaintBigThin.getTextSize() * sizeRatio);
    251                 mPaintMed.setTextSize( mPaintMed.getTextSize() * sizeRatio);
    252                 mTotalTextWidth *= sizeRatio;
    253                 mMinutesWidth *= sizeRatio;
    254                 mHoursWidth *= sizeRatio;
    255                 mSecondsWidth *= sizeRatio;
    256                 mHundredthsWidth *= sizeRatio;
    257                 mHundredthsSepWidth *= sizeRatio;
    258                 //recalculate the new total text width and half text height
    259                 mTotalTextWidth = mHoursWidth + mMinutesWidth + mSecondsWidth +
    260                         mHundredthsWidth + mHundredthsSepWidth + mHoursLabelWidth +
    261                         mMinutesLabelWidth + mSecondsLabelWidth;
    262                 mTextHeight = mPaintBig.getTextSize();
    263             }
    264         }
    265     }
    266 
    267     public void blinkTimeStr(boolean blink) {
    268         if (blink) {
    269             removeCallbacks(mBlinkThread);
    270             postDelayed(mBlinkThread, 1000);
    271         } else {
    272             removeCallbacks(mBlinkThread);
    273             showTime(true);
    274         }
    275     }
    276 
    277     public void showTime(boolean visible) {
    278         mShowTimeStr = visible;
    279         invalidate();
    280         mRemeasureText = true;
    281     }
    282 
    283     public void redTimeStr(boolean red, boolean forceUpdate) {
    284         mDefaultColor = red ? mRedColor : mWhiteColor;
    285         setTextColor(mDefaultColor);
    286         if (forceUpdate) {
    287             invalidate();
    288         }
    289     }
    290 
    291     public String getTimeString() {
    292         if (mHours == null) {
    293             return String.format("%s:%s.%s",mMinutes, mSeconds,  mHunderdths);
    294         }
    295         return String.format("%s:%s:%s.%s",mHours, mMinutes, mSeconds,  mHunderdths);
    296     }
    297 
    298     private static String getTimeStringForAccessibility(int hours, int minutes, int seconds,
    299             boolean showNeg, Resources r) {
    300         StringBuilder s = new StringBuilder();
    301         if (showNeg) {
    302             // This must be followed by a non-zero number or it will be audible as "hyphen"
    303             // instead of "minus".
    304             s.append("-");
    305         }
    306         if (showNeg && hours == 0 && minutes == 0) {
    307             // Non-negative time will always have minutes, eg. "0 minutes 7 seconds", but negative
    308             // time must start with non-zero digit, eg. -0m7s will be audible as just "-7 seconds"
    309             s.append(String.format(
    310                     r.getQuantityText(R.plurals.Nseconds_description, seconds).toString(),
    311                     seconds));
    312         } else if (hours == 0) {
    313             s.append(String.format(
    314                     r.getQuantityText(R.plurals.Nminutes_description, minutes).toString(),
    315                     minutes));
    316             s.append(" ");
    317             s.append(String.format(
    318                     r.getQuantityText(R.plurals.Nseconds_description, seconds).toString(),
    319                     seconds));
    320         } else {
    321             s.append(String.format(
    322                     r.getQuantityText(R.plurals.Nhours_description, hours).toString(),
    323                     hours));
    324             s.append(" ");
    325             s.append(String.format(
    326                     r.getQuantityText(R.plurals.Nminutes_description, minutes).toString(),
    327                     minutes));
    328             s.append(" ");
    329             s.append(String.format(
    330                     r.getQuantityText(R.plurals.Nseconds_description, seconds).toString(),
    331                     seconds));
    332         }
    333         return s.toString();
    334     }
    335 
    336     public void setVirtualButtonEnabled(boolean enabled) {
    337         mVirtualButtonEnabled = enabled;
    338     }
    339 
    340     private void virtualButtonPressed(boolean pressedOn) {
    341         mVirtualButtonPressedOn = pressedOn;
    342         mStopStartTextView.setTextColor(pressedOn ? mPressedColor : mWhiteColor);
    343         invalidate();
    344     }
    345 
    346     private boolean withinVirtualButtonBounds(float x, float y) {
    347         int width = getWidth();
    348         int height = getHeight();
    349         float centerX = width / 2;
    350         float centerY = height / 2;
    351         float radius = Math.min(width, height) / 2;
    352 
    353         // Within the circle button if distance to the center is less than the radius.
    354         double distance = Math.sqrt(Math.pow(centerX - x, 2) + Math.pow(centerY - y, 2));
    355         return distance < radius;
    356     }
    357 
    358     public void registerVirtualButtonAction(final Runnable runnable) {
    359         if (!mAccessibilityManager.isEnabled()) {
    360             this.setOnTouchListener(new OnTouchListener() {
    361                 @Override
    362                 public boolean onTouch(View v, MotionEvent event) {
    363                     if (mVirtualButtonEnabled) {
    364                         switch (event.getAction()) {
    365                             case MotionEvent.ACTION_DOWN:
    366                                 if (withinVirtualButtonBounds(event.getX(), event.getY())) {
    367                                     virtualButtonPressed(true);
    368                                     return true;
    369                                 } else {
    370                                     virtualButtonPressed(false);
    371                                     return false;
    372                                 }
    373                             case MotionEvent.ACTION_CANCEL:
    374                                 virtualButtonPressed(false);
    375                                 return true;
    376                             case MotionEvent.ACTION_OUTSIDE:
    377                                 virtualButtonPressed(false);
    378                                 return false;
    379                             case MotionEvent.ACTION_UP:
    380                                 virtualButtonPressed(false);
    381                                 if (withinVirtualButtonBounds(event.getX(), event.getY())) {
    382                                     runnable.run();
    383                                 }
    384                                 return true;
    385                         }
    386                     }
    387                     return false;
    388                 }
    389             });
    390         } else {
    391             this.setOnClickListener(new OnClickListener() {
    392                 @Override
    393                 public void onClick(View v) {
    394                     runnable.run();
    395                 }
    396             });
    397         }
    398     }
    399 
    400     @Override
    401     public void onDraw(Canvas canvas) {
    402         // Blink functionality.
    403         if (!mShowTimeStr && !mVirtualButtonPressedOn) {
    404             return;
    405         }
    406 
    407         int width = getWidth();
    408         if (mRemeasureText && width != 0) {
    409             setTotalTextWidth();
    410             width = getWidth();
    411             mRemeasureText = false;
    412         }
    413 
    414         int xCenter = width / 2;
    415         int yCenter = getHeight() / 2;
    416 
    417         float textXstart = xCenter - mTotalTextWidth / 2;
    418         float textYstart = yCenter + mTextHeight/2 - (mTextHeight * FONT_VERTICAL_OFFSET);
    419         // align the labels vertically to the top of the rest of the text
    420         float labelYStart = textYstart - (mTextHeight * (1 - 2 * FONT_VERTICAL_OFFSET))
    421                 + (1 - 2 * FONT_VERTICAL_OFFSET) * mPaintLabel.getTextSize();
    422 
    423         // Text color differs based on pressed state.
    424         int textColor;
    425         if (mVirtualButtonPressedOn) {
    426             textColor = mPressedColor;
    427             mStopStartTextView.setTextColor(mPressedColor);
    428         } else {
    429             textColor = mDefaultColor;
    430         }
    431         mPaintBig.setColor(textColor);
    432         mPaintBigThin.setColor(textColor);
    433         mPaintLabel.setColor(textColor);
    434         mPaintMed.setColor(textColor);
    435 
    436         if (mHours != null) {
    437             canvas.drawText(mHours, textXstart, textYstart, mPaintBig);
    438             textXstart += mHoursWidth;
    439             canvas.drawText(mHoursLabel, textXstart, labelYStart, mPaintLabel);
    440             textXstart += mHoursLabelWidth;
    441         }
    442         if (mMinutes != null) {
    443             canvas.drawText(mMinutes, textXstart, textYstart, mPaintBig);
    444             textXstart += mMinutesWidth;
    445             canvas.drawText(mMinutesLabel, textXstart, labelYStart, mPaintLabel);
    446             textXstart += mMinutesLabelWidth;
    447         }
    448         if (mSeconds != null) {
    449             canvas.drawText(mSeconds, textXstart, textYstart, mPaintBigThin);
    450             textXstart += mSecondsWidth;
    451             canvas.drawText(mSecondsLabel, textXstart, labelYStart, mPaintLabel);
    452             textXstart += mSecondsLabelWidth;
    453         }
    454         if (mHunderdths != null) {
    455             canvas.drawText(HUNDREDTH_SEPERATOR, textXstart, textYstart, mPaintLabel);
    456             textXstart += mHundredthsSepWidth;
    457             canvas.drawText(mHunderdths, textXstart, textYstart, mPaintMed);
    458         }
    459     }
    460 
    461     public void registerStopTextView(TextView stopStartTextView) {
    462         mStopStartTextView = stopStartTextView;
    463     }
    464 }
    465