Chronometer.java revision 241872a93beff5822f6ed5a23bba51862f63bf14
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 android.widget; 18 19import android.content.Context; 20import android.content.res.TypedArray; 21import android.icu.text.MeasureFormat; 22import android.icu.text.MeasureFormat.FormatWidth; 23import android.icu.util.Measure; 24import android.icu.util.MeasureUnit; 25import android.os.SystemClock; 26import android.text.format.DateUtils; 27import android.util.AttributeSet; 28import android.util.Log; 29import android.view.View; 30import android.widget.RemoteViews.RemoteView; 31 32import com.android.internal.R; 33 34import java.util.ArrayList; 35import java.util.Formatter; 36import java.util.IllegalFormatException; 37import java.util.Locale; 38 39/** 40 * Class that implements a simple timer. 41 * <p> 42 * You can give it a start time in the {@link SystemClock#elapsedRealtime} timebase, 43 * and it counts up from that, or if you don't give it a base time, it will use the 44 * time at which you call {@link #start}. 45 * 46 * <p>The timer can also count downward towards the base time by 47 * setting {@link #setCountDown(boolean)} to true. 48 * 49 * <p>By default it will display the current 50 * timer value in the form "MM:SS" or "H:MM:SS", or you can use {@link #setFormat} 51 * to format the timer value into an arbitrary string. 52 * 53 * @attr ref android.R.styleable#Chronometer_format 54 * @attr ref android.R.styleable#Chronometer_countDown 55 */ 56@RemoteView 57public class Chronometer extends TextView { 58 private static final String TAG = "Chronometer"; 59 60 /** 61 * A callback that notifies when the chronometer has incremented on its own. 62 */ 63 public interface OnChronometerTickListener { 64 65 /** 66 * Notification that the chronometer has changed. 67 */ 68 void onChronometerTick(Chronometer chronometer); 69 70 } 71 72 private long mBase; 73 private long mNow; // the currently displayed time 74 private boolean mVisible; 75 private boolean mStarted; 76 private boolean mRunning; 77 private boolean mLogged; 78 private String mFormat; 79 private Formatter mFormatter; 80 private Locale mFormatterLocale; 81 private Object[] mFormatterArgs = new Object[1]; 82 private StringBuilder mFormatBuilder; 83 private OnChronometerTickListener mOnChronometerTickListener; 84 private StringBuilder mRecycle = new StringBuilder(8); 85 private boolean mCountDown; 86 87 /** 88 * Initialize this Chronometer object. 89 * Sets the base to the current time. 90 */ 91 public Chronometer(Context context) { 92 this(context, null, 0); 93 } 94 95 /** 96 * Initialize with standard view layout information. 97 * Sets the base to the current time. 98 */ 99 public Chronometer(Context context, AttributeSet attrs) { 100 this(context, attrs, 0); 101 } 102 103 /** 104 * Initialize with standard view layout information and style. 105 * Sets the base to the current time. 106 */ 107 public Chronometer(Context context, AttributeSet attrs, int defStyleAttr) { 108 this(context, attrs, defStyleAttr, 0); 109 } 110 111 public Chronometer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 112 super(context, attrs, defStyleAttr, defStyleRes); 113 114 final TypedArray a = context.obtainStyledAttributes( 115 attrs, com.android.internal.R.styleable.Chronometer, defStyleAttr, defStyleRes); 116 setFormat(a.getString(R.styleable.Chronometer_format)); 117 setCountDown(a.getBoolean(R.styleable.Chronometer_countDown, false)); 118 a.recycle(); 119 120 init(); 121 } 122 123 private void init() { 124 mBase = SystemClock.elapsedRealtime(); 125 updateText(mBase); 126 } 127 128 /** 129 * Set this view to count down to the base instead of counting up from it. 130 * 131 * @param countDown whether this view should count down 132 * 133 * @see #setBase(long) 134 */ 135 @android.view.RemotableViewMethod 136 public void setCountDown(boolean countDown) { 137 mCountDown = countDown; 138 updateText(SystemClock.elapsedRealtime()); 139 } 140 141 /** 142 * @return whether this view counts down 143 * 144 * @see #setCountDown(boolean) 145 */ 146 public boolean isCountDown() { 147 return mCountDown; 148 } 149 150 /** 151 * Set the time that the count-up timer is in reference to. 152 * 153 * @param base Use the {@link SystemClock#elapsedRealtime} time base. 154 */ 155 @android.view.RemotableViewMethod 156 public void setBase(long base) { 157 mBase = base; 158 dispatchChronometerTick(); 159 updateText(SystemClock.elapsedRealtime()); 160 } 161 162 /** 163 * Return the base time as set through {@link #setBase}. 164 */ 165 public long getBase() { 166 return mBase; 167 } 168 169 /** 170 * Sets the format string used for display. The Chronometer will display 171 * this string, with the first "%s" replaced by the current timer value in 172 * "MM:SS" or "H:MM:SS" form. 173 * 174 * If the format string is null, or if you never call setFormat(), the 175 * Chronometer will simply display the timer value in "MM:SS" or "H:MM:SS" 176 * form. 177 * 178 * @param format the format string. 179 */ 180 @android.view.RemotableViewMethod 181 public void setFormat(String format) { 182 mFormat = format; 183 if (format != null && mFormatBuilder == null) { 184 mFormatBuilder = new StringBuilder(format.length() * 2); 185 } 186 } 187 188 /** 189 * Returns the current format string as set through {@link #setFormat}. 190 */ 191 public String getFormat() { 192 return mFormat; 193 } 194 195 /** 196 * Sets the listener to be called when the chronometer changes. 197 * 198 * @param listener The listener. 199 */ 200 public void setOnChronometerTickListener(OnChronometerTickListener listener) { 201 mOnChronometerTickListener = listener; 202 } 203 204 /** 205 * @return The listener (may be null) that is listening for chronometer change 206 * events. 207 */ 208 public OnChronometerTickListener getOnChronometerTickListener() { 209 return mOnChronometerTickListener; 210 } 211 212 /** 213 * Start counting up. This does not affect the base as set from {@link #setBase}, just 214 * the view display. 215 * 216 * Chronometer works by regularly scheduling messages to the handler, even when the 217 * Widget is not visible. To make sure resource leaks do not occur, the user should 218 * make sure that each start() call has a reciprocal call to {@link #stop}. 219 */ 220 public void start() { 221 mStarted = true; 222 updateRunning(); 223 } 224 225 /** 226 * Stop counting up. This does not affect the base as set from {@link #setBase}, just 227 * the view display. 228 * 229 * This stops the messages to the handler, effectively releasing resources that would 230 * be held as the chronometer is running, via {@link #start}. 231 */ 232 public void stop() { 233 mStarted = false; 234 updateRunning(); 235 } 236 237 /** 238 * The same as calling {@link #start} or {@link #stop}. 239 * @hide pending API council approval 240 */ 241 @android.view.RemotableViewMethod 242 public void setStarted(boolean started) { 243 mStarted = started; 244 updateRunning(); 245 } 246 247 @Override 248 protected void onDetachedFromWindow() { 249 super.onDetachedFromWindow(); 250 mVisible = false; 251 updateRunning(); 252 } 253 254 @Override 255 protected void onWindowVisibilityChanged(int visibility) { 256 super.onWindowVisibilityChanged(visibility); 257 mVisible = visibility == VISIBLE; 258 updateRunning(); 259 } 260 261 @Override 262 protected void onVisibilityChanged(View changedView, int visibility) { 263 super.onVisibilityChanged(changedView, visibility); 264 updateRunning(); 265 } 266 267 private synchronized void updateText(long now) { 268 mNow = now; 269 long seconds = mCountDown ? mBase - now : now - mBase; 270 seconds /= 1000; 271 boolean negative = false; 272 if (seconds < 0) { 273 seconds = -seconds; 274 negative = true; 275 } 276 String text = DateUtils.formatElapsedTime(mRecycle, seconds); 277 if (negative) { 278 text = getResources().getString(R.string.negative_duration, text); 279 } 280 281 if (mFormat != null) { 282 Locale loc = Locale.getDefault(); 283 if (mFormatter == null || !loc.equals(mFormatterLocale)) { 284 mFormatterLocale = loc; 285 mFormatter = new Formatter(mFormatBuilder, loc); 286 } 287 mFormatBuilder.setLength(0); 288 mFormatterArgs[0] = text; 289 try { 290 mFormatter.format(mFormat, mFormatterArgs); 291 text = mFormatBuilder.toString(); 292 } catch (IllegalFormatException ex) { 293 if (!mLogged) { 294 Log.w(TAG, "Illegal format string: " + mFormat); 295 mLogged = true; 296 } 297 } 298 } 299 setText(text); 300 } 301 302 private void updateRunning() { 303 boolean running = mVisible && mStarted && isShown(); 304 if (running != mRunning) { 305 if (running) { 306 updateText(SystemClock.elapsedRealtime()); 307 dispatchChronometerTick(); 308 postDelayed(mTickRunnable, 1000); 309 } else { 310 removeCallbacks(mTickRunnable); 311 } 312 mRunning = running; 313 } 314 } 315 316 private final Runnable mTickRunnable = new Runnable() { 317 @Override 318 public void run() { 319 if (mRunning) { 320 updateText(SystemClock.elapsedRealtime()); 321 dispatchChronometerTick(); 322 postDelayed(mTickRunnable, 1000); 323 } 324 } 325 }; 326 327 void dispatchChronometerTick() { 328 if (mOnChronometerTickListener != null) { 329 mOnChronometerTickListener.onChronometerTick(this); 330 } 331 } 332 333 private static final int MIN_IN_SEC = 60; 334 private static final int HOUR_IN_SEC = MIN_IN_SEC*60; 335 private static String formatDuration(long ms) { 336 int duration = (int) (ms / DateUtils.SECOND_IN_MILLIS); 337 if (duration < 0) { 338 duration = -duration; 339 } 340 341 int h = 0; 342 int m = 0; 343 344 if (duration >= HOUR_IN_SEC) { 345 h = duration / HOUR_IN_SEC; 346 duration -= h * HOUR_IN_SEC; 347 } 348 if (duration >= MIN_IN_SEC) { 349 m = duration / MIN_IN_SEC; 350 duration -= m * MIN_IN_SEC; 351 } 352 final int s = duration; 353 354 final ArrayList<Measure> measures = new ArrayList<Measure>(); 355 if (h > 0) { 356 measures.add(new Measure(h, MeasureUnit.HOUR)); 357 } 358 if (m > 0) { 359 measures.add(new Measure(m, MeasureUnit.MINUTE)); 360 } 361 measures.add(new Measure(s, MeasureUnit.SECOND)); 362 363 return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE) 364 .formatMeasures((Measure[]) measures.toArray()); 365 } 366 367 @Override 368 public CharSequence getContentDescription() { 369 return formatDuration(mNow - mBase); 370 } 371 372 @Override 373 public CharSequence getAccessibilityClassName() { 374 return Chronometer.class.getName(); 375 } 376} 377