CountingTimerView.java revision 78b8e1513e24c58ffea6ee4edbebdce85c248f6f
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
17package com.android.deskclock.timer;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.graphics.Canvas;
22import android.graphics.Paint;
23import android.graphics.Typeface;
24import android.util.AttributeSet;
25import android.view.MotionEvent;
26import android.view.View;
27import android.view.accessibility.AccessibilityManager;
28import android.widget.TextView;
29
30import com.android.deskclock.DeskClock;
31import com.android.deskclock.R;
32import com.android.deskclock.Utils;
33
34
35public 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