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