Chronometer.java revision 08c7116ab9cd04ad6dd3c04aa1017237e7f409ac
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.os.Handler; 22import android.os.Message; 23import android.os.SystemClock; 24import android.text.format.DateUtils; 25import android.util.AttributeSet; 26import android.util.Log; 27import android.widget.RemoteViews.RemoteView; 28 29import java.util.Formatter; 30import java.util.IllegalFormatException; 31import java.util.Locale; 32 33/** 34 * Class that implements a simple timer. 35 * <p> 36 * You can give it a start time in the {@link SystemClock#elapsedRealtime} timebase, 37 * and it counts up from that, or if you don't give it a base time, it will use the 38 * time at which you call {@link #start}. By default it will display the current 39 * timer value in the form "MM:SS" or "H:MM:SS", or you can use {@link #setFormat} 40 * to format the timer value into an arbitrary string. 41 * 42 * @attr ref android.R.styleable#Chronometer_format 43 */ 44@RemoteView 45public class Chronometer extends TextView { 46 private static final String TAG = "Chronometer"; 47 48 /** 49 * A callback that notifies when the chronometer has incremented on its own. 50 */ 51 public interface OnChronometerTickListener { 52 53 /** 54 * Notification that the chronometer has changed. 55 */ 56 void onChronometerTick(Chronometer chronometer); 57 58 } 59 60 private long mBase; 61 private boolean mVisible; 62 private boolean mStarted; 63 private boolean mRunning; 64 private boolean mLogged; 65 private String mFormat; 66 private Formatter mFormatter; 67 private Locale mFormatterLocale; 68 private Object[] mFormatterArgs = new Object[1]; 69 private StringBuilder mFormatBuilder; 70 private OnChronometerTickListener mOnChronometerTickListener; 71 private StringBuilder mRecycle = new StringBuilder(8); 72 73 private static final int TICK_WHAT = 2; 74 75 /** 76 * Initialize this Chronometer object. 77 * Sets the base to the current time. 78 */ 79 public Chronometer(Context context) { 80 this(context, null, 0); 81 } 82 83 /** 84 * Initialize with standard view layout information. 85 * Sets the base to the current time. 86 */ 87 public Chronometer(Context context, AttributeSet attrs) { 88 this(context, attrs, 0); 89 } 90 91 /** 92 * Initialize with standard view layout information and style. 93 * Sets the base to the current time. 94 */ 95 public Chronometer(Context context, AttributeSet attrs, int defStyleAttr) { 96 this(context, attrs, defStyleAttr, 0); 97 } 98 99 public Chronometer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 100 super(context, attrs, defStyleAttr, defStyleRes); 101 102 final TypedArray a = context.obtainStyledAttributes( 103 attrs, com.android.internal.R.styleable.Chronometer, defStyleAttr, defStyleRes); 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 CharSequence getAccessibilityClassName() { 284 return Chronometer.class.getName(); 285 } 286} 287