/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.deskclock.timer; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Typeface; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.accessibility.AccessibilityManager; import android.widget.TextView; import com.android.deskclock.DeskClock; import com.android.deskclock.R; import com.android.deskclock.Utils; public class CountingTimerView extends View { private static final String TWO_DIGITS = "%02d"; private static final String ONE_DIGIT = "%01d"; private static final String NEG_TWO_DIGITS = "-%02d"; private static final String NEG_ONE_DIGIT = "-%01d"; private static final float TEXT_SIZE_TO_WIDTH_RATIO = 0.75f; // This is the ratio of the font typeface we need to offset the font by vertically to align it // vertically center. private static final float FONT_VERTICAL_OFFSET = 0.14f; private String mHours, mMinutes, mSeconds, mHunderdths; private final String mHoursLabel, mMinutesLabel, mSecondsLabel; private float mHoursWidth, mMinutesWidth, mSecondsWidth, mHundredthsWidth; private float mHoursLabelWidth, mMinutesLabelWidth, mSecondsLabelWidth, mHundredthsSepWidth; private boolean mShowTimeStr = true; private final Typeface mAndroidClockMonoThin, mAndroidClockMonoBold, mRobotoLabel, mAndroidClockMonoLight; private final Paint mPaintBig = new Paint(); private final Paint mPaintBigThin = new Paint(); private final Paint mPaintMed = new Paint(); private final Paint mPaintLabel = new Paint(); private float mTextHeight = 0; private float mTotalTextWidth; private static final String HUNDREDTH_SEPERATOR = "."; private boolean mRemeasureText = true; private int mDefaultColor; private final int mPressedColor; private final int mWhiteColor; private final int mRedColor; private TextView mStopStartTextView; private final AccessibilityManager mAccessibilityManager; // Fields for the text serving as a virtual button. private boolean mVirtualButtonEnabled = false; private boolean mVirtualButtonPressedOn = false; Runnable mBlinkThread = new Runnable() { private boolean mVisible = true; @Override public void run() { mVisible = !mVisible; CountingTimerView.this.showTime(mVisible); postDelayed(mBlinkThread, 500); } }; public CountingTimerView(Context context) { this(context, null); } public CountingTimerView(Context context, AttributeSet attrs) { super(context, attrs); mAndroidClockMonoThin = Typeface.createFromAsset(context.getAssets(),"fonts/AndroidClockMono-Thin.ttf"); mAndroidClockMonoBold = Typeface.createFromAsset(context.getAssets(),"fonts/AndroidClockMono-Bold.ttf"); mAndroidClockMonoLight = Typeface.createFromAsset(context.getAssets(),"fonts/AndroidClockMono-Light.ttf"); mAccessibilityManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); mRobotoLabel= Typeface.create("sans-serif-condensed", Typeface.BOLD); Resources r = context.getResources(); mHoursLabel = r.getString(R.string.hours_label).toUpperCase(); mMinutesLabel = r.getString(R.string.minutes_label).toUpperCase(); mSecondsLabel = r.getString(R.string.seconds_label).toUpperCase(); mWhiteColor = r.getColor(R.color.clock_white); mDefaultColor = mWhiteColor; mPressedColor = r.getColor(Utils.getPressedColorId()); mRedColor = r.getColor(R.color.clock_red); mPaintBig.setAntiAlias(true); mPaintBig.setStyle(Paint.Style.STROKE); mPaintBig.setTextAlign(Paint.Align.LEFT); mPaintBig.setTypeface(mAndroidClockMonoBold); float bigFontSize = r.getDimension(R.dimen.big_font_size); mPaintBig.setTextSize(bigFontSize); mTextHeight = bigFontSize; mPaintBigThin.setAntiAlias(true); mPaintBigThin.setStyle(Paint.Style.STROKE); mPaintBigThin.setTextAlign(Paint.Align.LEFT); mPaintBigThin.setTypeface(mAndroidClockMonoThin); mPaintBigThin.setTextSize(r.getDimension(R.dimen.big_font_size)); mPaintMed.setAntiAlias(true); mPaintMed.setStyle(Paint.Style.STROKE); mPaintMed.setTextAlign(Paint.Align.LEFT); mPaintMed.setTypeface(mAndroidClockMonoLight); mPaintMed.setTextSize(r.getDimension(R.dimen.small_font_size)); mPaintLabel.setAntiAlias(true); mPaintLabel.setStyle(Paint.Style.STROKE); mPaintLabel.setTextAlign(Paint.Align.LEFT); mPaintLabel.setTypeface(mRobotoLabel); mPaintLabel.setTextSize(r.getDimension(R.dimen.label_font_size)); setTextColor(mDefaultColor); } protected void setTextColor(int textColor) { mPaintBig.setColor(textColor); mPaintBigThin.setColor(textColor); mPaintMed.setColor(textColor); mPaintLabel.setColor(textColor); } public void setTime(long time, boolean showHundredths, boolean update) { boolean neg = false, showNeg = false; String format = null; if (time < 0) { time = -time; neg = showNeg = true; } long hundreds, seconds, minutes, hours; seconds = time / 1000; hundreds = (time - seconds * 1000) / 10; minutes = seconds / 60; seconds = seconds - minutes * 60; hours = minutes / 60; minutes = minutes - hours * 60; if (hours > 99) { hours = 0; } // time may less than a second below zero, since we do not show fractions of seconds // when counting down, do not show the minus sign. if (hours ==0 && minutes == 0 && seconds == 0) { showNeg = false; } // TODO: must build to account for localization if (!showHundredths) { if (!neg && hundreds != 0) { seconds++; if (seconds == 60) { seconds = 0; minutes++; if (minutes == 60) { minutes = 0; hours++; } } } if (hundreds < 10 || hundreds > 90) { update = true; } } if (hours >= 10) { format = showNeg ? NEG_TWO_DIGITS : TWO_DIGITS; mHours = String.format(format, hours); } else if (hours > 0) { format = showNeg ? NEG_ONE_DIGIT : ONE_DIGIT; mHours = String.format(format, hours); } else { mHours = null; } if (minutes >= 10 || hours > 0) { format = (showNeg && hours == 0) ? NEG_TWO_DIGITS : TWO_DIGITS; mMinutes = String.format(format, minutes); } else { format = (showNeg && hours == 0) ? NEG_ONE_DIGIT : ONE_DIGIT; mMinutes = String.format(format, minutes); } mSeconds = String.format(TWO_DIGITS, seconds); if (showHundredths) { mHunderdths = String.format(TWO_DIGITS, hundreds); } else { mHunderdths = null; } mRemeasureText = true; if (update) { setContentDescription(getTimeStringForAccessibility((int) hours, (int) minutes, (int) seconds, showNeg, getResources())); invalidate(); } } private void setTotalTextWidth() { mTotalTextWidth = 0; if (mHours != null) { mHoursWidth = mPaintBig.measureText(mHours); mTotalTextWidth += mHoursWidth; mHoursLabelWidth = mPaintLabel.measureText(mHoursLabel); mTotalTextWidth += mHoursLabelWidth; } if (mMinutes != null) { mMinutesWidth = mPaintBig.measureText(mMinutes); mTotalTextWidth += mMinutesWidth; mMinutesLabelWidth = mPaintLabel.measureText(mMinutesLabel); mTotalTextWidth += mMinutesLabelWidth; } if (mSeconds != null) { mSecondsWidth = mPaintBigThin.measureText(mSeconds); mTotalTextWidth += mSecondsWidth; mSecondsLabelWidth = mPaintLabel.measureText(mSecondsLabel); mTotalTextWidth += mSecondsLabelWidth; } if (mHunderdths != null) { mHundredthsWidth = mPaintMed.measureText(mHunderdths); mTotalTextWidth += mHundredthsWidth; mHundredthsSepWidth = mPaintLabel.measureText(HUNDREDTH_SEPERATOR); mTotalTextWidth += mHundredthsSepWidth; } // This is a hack: if the text is too wide, reduce all the paint text sizes // To determine the maximum width, we find the minimum of the height and width (since the // circle we are trying to fit the text into has its radius sized to the smaller of the // two. int width = Math.min(getWidth(), getHeight()); if (width != 0) { float ratio = mTotalTextWidth / width; if (ratio > TEXT_SIZE_TO_WIDTH_RATIO) { float sizeRatio = (TEXT_SIZE_TO_WIDTH_RATIO / ratio); mPaintBig.setTextSize( mPaintBig.getTextSize() * sizeRatio); mPaintBigThin.setTextSize( mPaintBigThin.getTextSize() * sizeRatio); mPaintMed.setTextSize( mPaintMed.getTextSize() * sizeRatio); mTotalTextWidth *= sizeRatio; mMinutesWidth *= sizeRatio; mHoursWidth *= sizeRatio; mSecondsWidth *= sizeRatio; mHundredthsWidth *= sizeRatio; mHundredthsSepWidth *= sizeRatio; //recalculate the new total text width and half text height mTotalTextWidth = mHoursWidth + mMinutesWidth + mSecondsWidth + mHundredthsWidth + mHundredthsSepWidth + mHoursLabelWidth + mMinutesLabelWidth + mSecondsLabelWidth; mTextHeight = mPaintBig.getTextSize(); } } } public void blinkTimeStr(boolean blink) { if (blink) { removeCallbacks(mBlinkThread); postDelayed(mBlinkThread, 1000); } else { removeCallbacks(mBlinkThread); showTime(true); } } public void showTime(boolean visible) { mShowTimeStr = visible; invalidate(); mRemeasureText = true; } public void redTimeStr(boolean red, boolean forceUpdate) { mDefaultColor = red ? mRedColor : mWhiteColor; setTextColor(mDefaultColor); if (forceUpdate) { invalidate(); } } public String getTimeString() { if (mHours == null) { return String.format("%s:%s.%s",mMinutes, mSeconds, mHunderdths); } return String.format("%s:%s:%s.%s",mHours, mMinutes, mSeconds, mHunderdths); } private static String getTimeStringForAccessibility(int hours, int minutes, int seconds, boolean showNeg, Resources r) { StringBuilder s = new StringBuilder(); if (showNeg) { // This must be followed by a non-zero number or it will be audible as "hyphen" // instead of "minus". s.append("-"); } if (showNeg && hours == 0 && minutes == 0) { // Non-negative time will always have minutes, eg. "0 minutes 7 seconds", but negative // time must start with non-zero digit, eg. -0m7s will be audible as just "-7 seconds" s.append(String.format( r.getQuantityText(R.plurals.Nseconds_description, seconds).toString(), seconds)); } else if (hours == 0) { s.append(String.format( r.getQuantityText(R.plurals.Nminutes_description, minutes).toString(), minutes)); s.append(" "); s.append(String.format( r.getQuantityText(R.plurals.Nseconds_description, seconds).toString(), seconds)); } else { s.append(String.format( r.getQuantityText(R.plurals.Nhours_description, hours).toString(), hours)); s.append(" "); s.append(String.format( r.getQuantityText(R.plurals.Nminutes_description, minutes).toString(), minutes)); s.append(" "); s.append(String.format( r.getQuantityText(R.plurals.Nseconds_description, seconds).toString(), seconds)); } return s.toString(); } public void setVirtualButtonEnabled(boolean enabled) { mVirtualButtonEnabled = enabled; } private void virtualButtonPressed(boolean pressedOn) { mVirtualButtonPressedOn = pressedOn; mStopStartTextView.setTextColor(pressedOn ? mPressedColor : mWhiteColor); invalidate(); } private boolean withinVirtualButtonBounds(float x, float y) { int width = getWidth(); int height = getHeight(); float centerX = width / 2; float centerY = height / 2; float radius = Math.min(width, height) / 2; // Within the circle button if distance to the center is less than the radius. double distance = Math.sqrt(Math.pow(centerX - x, 2) + Math.pow(centerY - y, 2)); return distance < radius; } public void registerVirtualButtonAction(final Runnable runnable) { if (!mAccessibilityManager.isEnabled()) { this.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if (mVirtualButtonEnabled) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: if (withinVirtualButtonBounds(event.getX(), event.getY())) { virtualButtonPressed(true); return true; } else { virtualButtonPressed(false); return false; } case MotionEvent.ACTION_CANCEL: virtualButtonPressed(false); return true; case MotionEvent.ACTION_OUTSIDE: virtualButtonPressed(false); return false; case MotionEvent.ACTION_UP: virtualButtonPressed(false); if (withinVirtualButtonBounds(event.getX(), event.getY())) { runnable.run(); } return true; } } return false; } }); } else { this.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { runnable.run(); } }); } } @Override public void onDraw(Canvas canvas) { // Blink functionality. if (!mShowTimeStr && !mVirtualButtonPressedOn) { return; } int width = getWidth(); if (mRemeasureText && width != 0) { setTotalTextWidth(); width = getWidth(); mRemeasureText = false; } int xCenter = width / 2; int yCenter = getHeight() / 2; float textXstart = xCenter - mTotalTextWidth / 2; float textYstart = yCenter + mTextHeight/2 - (mTextHeight * FONT_VERTICAL_OFFSET); // align the labels vertically to the top of the rest of the text float labelYStart = textYstart - (mTextHeight * (1 - 2 * FONT_VERTICAL_OFFSET)) + (1 - 2 * FONT_VERTICAL_OFFSET) * mPaintLabel.getTextSize(); // Text color differs based on pressed state. int textColor; if (mVirtualButtonPressedOn) { textColor = mPressedColor; mStopStartTextView.setTextColor(mPressedColor); } else { textColor = mDefaultColor; } mPaintBig.setColor(textColor); mPaintBigThin.setColor(textColor); mPaintLabel.setColor(textColor); mPaintMed.setColor(textColor); if (mHours != null) { canvas.drawText(mHours, textXstart, textYstart, mPaintBig); textXstart += mHoursWidth; canvas.drawText(mHoursLabel, textXstart, labelYStart, mPaintLabel); textXstart += mHoursLabelWidth; } if (mMinutes != null) { canvas.drawText(mMinutes, textXstart, textYstart, mPaintBig); textXstart += mMinutesWidth; canvas.drawText(mMinutesLabel, textXstart, labelYStart, mPaintLabel); textXstart += mMinutesLabelWidth; } if (mSeconds != null) { canvas.drawText(mSeconds, textXstart, textYstart, mPaintBigThin); textXstart += mSecondsWidth; canvas.drawText(mSecondsLabel, textXstart, labelYStart, mPaintLabel); textXstart += mSecondsLabelWidth; } if (mHunderdths != null) { canvas.drawText(HUNDREDTH_SEPERATOR, textXstart, textYstart, mPaintLabel); textXstart += mHundredthsSepWidth; canvas.drawText(mHunderdths, textXstart, textYstart, mPaintMed); } } public void registerStopTextView(TextView stopStartTextView) { mStopStartTextView = stopStartTextView; } }