Chronometer.java revision 8a78fd4d9572dff95432fcc4ba0e87563415b728
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.graphics.Canvas; 22import android.os.Handler; 23import android.os.Message; 24import android.os.SystemClock; 25import android.text.format.DateUtils; 26import android.util.AttributeSet; 27import android.util.Log; 28import android.view.accessibility.AccessibilityEvent; 29import android.view.accessibility.AccessibilityNodeInfo; 30import android.widget.RemoteViews.RemoteView; 31 32import java.util.Formatter; 33import java.util.IllegalFormatException; 34import java.util.Locale; 35 36/** 37 * Class that implements a simple timer. 38 * <p> 39 * You can give it a start time in the {@link SystemClock#elapsedRealtime} timebase, 40 * and it counts up from that, or if you don't give it a base time, it will use the 41 * time at which you call {@link #start}. By default it will display the current 42 * timer value in the form "MM:SS" or "H:MM:SS", or you can use {@link #setFormat} 43 * to format the timer value into an arbitrary string. 44 * 45 * @attr ref android.R.styleable#Chronometer_format 46 */ 47@RemoteView 48public class Chronometer extends TextView { 49 private static final String TAG = "Chronometer"; 50 51 /** 52 * A callback that notifies when the chronometer has incremented on its own. 53 */ 54 public interface OnChronometerTickListener { 55 56 /** 57 * Notification that the chronometer has changed. 58 */ 59 void onChronometerTick(Chronometer chronometer); 60 61 } 62 63 private long mBase; 64 private boolean mVisible; 65 private boolean mStarted; 66 private boolean mRunning; 67 private boolean mLogged; 68 private String mFormat; 69 private Formatter mFormatter; 70 private Locale mFormatterLocale; 71 private Object[] mFormatterArgs = new Object[1]; 72 private StringBuilder mFormatBuilder; 73 private OnChronometerTickListener mOnChronometerTickListener; 74 private StringBuilder mRecycle = new StringBuilder(8); 75 76 private static final int TICK_WHAT = 2; 77 78 /** 79 * Initialize this Chronometer object. 80 * Sets the base to the current time. 81 */ 82 public Chronometer(Context context) { 83 this(context, null, 0); 84 } 85 86 /** 87 * Initialize with standard view layout information. 88 * Sets the base to the current time. 89 */ 90 public Chronometer(Context context, AttributeSet attrs) { 91 this(context, attrs, 0); 92 } 93 94 /** 95 * Initialize with standard view layout information and style. 96 * Sets the base to the current time. 97 */ 98 public Chronometer(Context context, AttributeSet attrs, int defStyle) { 99 super(context, attrs, defStyle); 100 101 TypedArray a = context.obtainStyledAttributes( 102 attrs, 103 com.android.internal.R.styleable.Chronometer, defStyle, 0); 104 setFormat(a.getString(com.android.internal.R.styleable.Chronometer_format)); 105 a.recycle(); 106 107 init(); 108 } 109 110 private void init() { 111 mBase = SystemClock.elapsedRealtime(); 112 updateText(mBase); 113 } 114 115 /** 116 * Set the time that the count-up timer is in reference to. 117 * 118 * @param base Use the {@link SystemClock#elapsedRealtime} time base. 119 */ 120 @android.view.RemotableViewMethod 121 public void setBase(long base) { 122 mBase = base; 123 dispatchChronometerTick(); 124 updateText(SystemClock.elapsedRealtime()); 125 } 126 127 /** 128 * Return the base time as set through {@link #setBase}. 129 */ 130 public long getBase() { 131 return mBase; 132 } 133 134 /** 135 * Sets the format string used for display. The Chronometer will display 136 * this string, with the first "%s" replaced by the current timer value in 137 * "MM:SS" or "H:MM:SS" form. 138 * 139 * If the format string is null, or if you never call setFormat(), the 140 * Chronometer will simply display the timer value in "MM:SS" or "H:MM:SS" 141 * form. 142 * 143 * @param format the format string. 144 */ 145 @android.view.RemotableViewMethod 146 public void setFormat(String format) { 147 mFormat = format; 148 if (format != null && mFormatBuilder == null) { 149 mFormatBuilder = new StringBuilder(format.length() * 2); 150 } 151 } 152 153 /** 154 * Returns the current format string as set through {@link #setFormat}. 155 */ 156 public String getFormat() { 157 return mFormat; 158 } 159 160 /** 161 * Sets the listener to be called when the chronometer changes. 162 * 163 * @param listener The listener. 164 */ 165 public void setOnChronometerTickListener(OnChronometerTickListener listener) { 166 mOnChronometerTickListener = listener; 167 } 168 169 /** 170 * @return The listener (may be null) that is listening for chronometer change 171 * events. 172 */ 173 public OnChronometerTickListener getOnChronometerTickListener() { 174 return mOnChronometerTickListener; 175 } 176 177 /** 178 * Start counting up. This does not affect the base as set from {@link #setBase}, just 179 * the view display. 180 * 181 * Chronometer works by regularly scheduling messages to the handler, even when the 182 * Widget is not visible. To make sure resource leaks do not occur, the user should 183 * make sure that each start() call has a reciprocal call to {@link #stop}. 184 */ 185 public void start() { 186 mStarted = true; 187 updateRunning(); 188 } 189 190 /** 191 * Stop counting up. This does not affect the base as set from {@link #setBase}, just 192 * the view display. 193 * 194 * This stops the messages to the handler, effectively releasing resources that would 195 * be held as the chronometer is running, via {@link #start}. 196 */ 197 public void stop() { 198 mStarted = false; 199 updateRunning(); 200 } 201 202 /** 203 * The same as calling {@link #start} or {@link #stop}. 204 * @hide pending API council approval 205 */ 206 @android.view.RemotableViewMethod 207 public void setStarted(boolean started) { 208 mStarted = started; 209 updateRunning(); 210 } 211 212 @Override 213 protected void onDetachedFromWindow() { 214 super.onDetachedFromWindow(); 215 mVisible = false; 216 updateRunning(); 217 } 218 219 @Override 220 protected void onWindowVisibilityChanged(int visibility) { 221 super.onWindowVisibilityChanged(visibility); 222 mVisible = visibility == VISIBLE; 223 updateRunning(); 224 } 225 226 private synchronized void updateText(long now) { 227 long seconds = now - mBase; 228 seconds /= 1000; 229 String text = DateUtils.formatElapsedTime(mRecycle, seconds); 230 231 if (mFormat != null) { 232 Locale loc = Locale.getDefault(); 233 if (mFormatter == null || !loc.equals(mFormatterLocale)) { 234 mFormatterLocale = loc; 235 mFormatter = new Formatter(mFormatBuilder, loc); 236 } 237 mFormatBuilder.setLength(0); 238 mFormatterArgs[0] = text; 239 try { 240 mFormatter.format(mFormat, mFormatterArgs); 241 text = mFormatBuilder.toString(); 242 } catch (IllegalFormatException ex) { 243 if (!mLogged) { 244 Log.w(TAG, "Illegal format string: " + mFormat); 245 mLogged = true; 246 } 247 } 248 } 249 setText(text); 250 } 251 252 private void updateRunning() { 253 boolean running = mVisible && mStarted; 254 if (running != mRunning) { 255 if (running) { 256 updateText(SystemClock.elapsedRealtime()); 257 dispatchChronometerTick(); 258 mHandler.sendMessageDelayed(Message.obtain(mHandler, TICK_WHAT), 1000); 259 } else { 260 mHandler.removeMessages(TICK_WHAT); 261 } 262 mRunning = running; 263 } 264 } 265 266 private Handler mHandler = new Handler() { 267 public void handleMessage(Message m) { 268 if (mRunning) { 269 updateText(SystemClock.elapsedRealtime()); 270 dispatchChronometerTick(); 271 sendMessageDelayed(Message.obtain(this, TICK_WHAT), 1000); 272 } 273 } 274 }; 275 276 void dispatchChronometerTick() { 277 if (mOnChronometerTickListener != null) { 278 mOnChronometerTickListener.onChronometerTick(this); 279 } 280 } 281 282 @Override 283 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 284 super.onInitializeAccessibilityEvent(event); 285 event.setClassName(Chronometer.class.getName()); 286 } 287 288 @Override 289 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 290 super.onInitializeAccessibilityNodeInfo(info); 291 info.setClassName(Chronometer.class.getName()); 292 } 293} 294