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