1 package com.android.deskclock; 2 3 import android.content.Context; 4 import android.content.SharedPreferences; 5 import android.content.res.Resources; 6 import android.graphics.Canvas; 7 import android.graphics.Paint; 8 import android.graphics.RectF; 9 import android.util.AttributeSet; 10 import android.view.View; 11 12 import com.android.deskclock.stopwatch.Stopwatches; 13 14 /** 15 * Class to draw a circle for timers and stopwatches. 16 * These two usages require two different animation modes: 17 * Timer counts down. In this mode the animation is counter-clockwise and stops at 0. 18 * Stopwatch counts up. In this mode the animation is clockwise and will run until stopped. 19 */ 20 public class CircleTimerView extends View { 21 22 23 private int mAccentColor; 24 private int mWhiteColor; 25 private long mIntervalTime = 0; 26 private long mIntervalStartTime = -1; 27 private long mMarkerTime = -1; 28 private long mCurrentIntervalTime = 0; 29 private long mAccumulatedTime = 0; 30 private boolean mPaused = false; 31 private boolean mAnimate = false; 32 private static float mStrokeSize = 4; 33 private static float mDotRadius = 6; 34 private static float mMarkerStrokeSize = 2; 35 private final Paint mPaint = new Paint(); 36 private final Paint mFill = new Paint(); 37 private final RectF mArcRect = new RectF(); 38 private float mRadiusOffset; // amount to remove from radius to account for markers on circle 39 private float mScreenDensity; 40 41 // Stopwatch mode is the default. 42 private boolean mTimerMode = false; 43 44 @SuppressWarnings("unused") 45 public CircleTimerView(Context context) { 46 this(context, null); 47 } 48 49 public CircleTimerView(Context context, AttributeSet attrs) { 50 super(context, attrs); 51 init(context); 52 } 53 54 public void setIntervalTime(long t) { 55 mIntervalTime = t; 56 postInvalidate(); 57 } 58 59 public void setMarkerTime(long t) { 60 mMarkerTime = t; 61 postInvalidate(); 62 } 63 64 public void reset() { 65 mIntervalStartTime = -1; 66 mMarkerTime = -1; 67 postInvalidate(); 68 } 69 public void startIntervalAnimation() { 70 mIntervalStartTime = Utils.getTimeNow(); 71 mAnimate = true; 72 invalidate(); 73 mPaused = false; 74 } 75 public void stopIntervalAnimation() { 76 mAnimate = false; 77 mIntervalStartTime = -1; 78 mAccumulatedTime = 0; 79 } 80 81 public boolean isAnimating() { 82 return (mIntervalStartTime != -1); 83 } 84 85 public void pauseIntervalAnimation() { 86 mAnimate = false; 87 mAccumulatedTime += Utils.getTimeNow() - mIntervalStartTime; 88 mPaused = true; 89 } 90 91 public void abortIntervalAnimation() { 92 mAnimate = false; 93 } 94 95 public void setPassedTime(long time, boolean drawRed) { 96 // The onDraw() method checks if mIntervalStartTime has been set before drawing any red. 97 // Without drawRed, mIntervalStartTime should not be set here at all, and would remain at -1 98 // when the state is reconfigured after exiting and re-entering the application. 99 // If the timer is currently running, this drawRed will not be set, and will have no effect 100 // because mIntervalStartTime will be set when the thread next runs. 101 // When the timer is not running, mIntervalStartTime will not be set upon loading the state, 102 // and no red will be drawn, so drawRed is used to force onDraw() to draw the red portion, 103 // despite the timer not running. 104 mCurrentIntervalTime = mAccumulatedTime = time; 105 if (drawRed) { 106 mIntervalStartTime = Utils.getTimeNow(); 107 } 108 postInvalidate(); 109 } 110 111 112 113 private void init(Context c) { 114 115 Resources resources = c.getResources(); 116 mStrokeSize = resources.getDimension(R.dimen.circletimer_circle_size); 117 float dotDiameter = resources.getDimension(R.dimen.circletimer_dot_size); 118 mMarkerStrokeSize = resources.getDimension(R.dimen.circletimer_marker_size); 119 mRadiusOffset = Utils.calculateRadiusOffset( 120 mStrokeSize, dotDiameter, mMarkerStrokeSize); 121 mPaint.setAntiAlias(true); 122 mPaint.setStyle(Paint.Style.STROKE); 123 mWhiteColor = resources.getColor(R.color.clock_white); 124 mAccentColor = resources.getColor(R.color.hot_pink); 125 mScreenDensity = resources.getDisplayMetrics().density; 126 mFill.setAntiAlias(true); 127 mFill.setStyle(Paint.Style.FILL); 128 mFill.setColor(mAccentColor); 129 mDotRadius = dotDiameter / 2f; 130 } 131 132 public void setTimerMode(boolean mode) { 133 mTimerMode = mode; 134 } 135 136 @Override 137 public void onDraw(Canvas canvas) { 138 int xCenter = getWidth() / 2 + 1; 139 int yCenter = getHeight() / 2; 140 141 mPaint.setStrokeWidth(mStrokeSize); 142 float radius = Math.min(xCenter, yCenter) - mRadiusOffset; 143 144 if (mIntervalStartTime == -1) { 145 // just draw a complete white circle, no red arc needed 146 mPaint.setColor(mWhiteColor); 147 canvas.drawCircle (xCenter, yCenter, radius, mPaint); 148 if (mTimerMode) { 149 drawRedDot(canvas, 0f, xCenter, yCenter, radius); 150 } 151 } else { 152 if (mAnimate) { 153 mCurrentIntervalTime = Utils.getTimeNow() - mIntervalStartTime + mAccumulatedTime; 154 } 155 //draw a combination of red and white arcs to create a circle 156 mArcRect.top = yCenter - radius; 157 mArcRect.bottom = yCenter + radius; 158 mArcRect.left = xCenter - radius; 159 mArcRect.right = xCenter + radius; 160 float redPercent = (float)mCurrentIntervalTime / (float)mIntervalTime; 161 // prevent timer from doing more than one full circle 162 redPercent = (redPercent > 1 && mTimerMode) ? 1 : redPercent; 163 164 float whitePercent = 1 - (redPercent > 1 ? 1 : redPercent); 165 // draw red arc here 166 mPaint.setColor(mAccentColor); 167 if (mTimerMode){ 168 canvas.drawArc (mArcRect, 270, - redPercent * 360 , false, mPaint); 169 } else { 170 canvas.drawArc (mArcRect, 270, + redPercent * 360 , false, mPaint); 171 } 172 173 // draw white arc here 174 mPaint.setStrokeWidth(mStrokeSize); 175 mPaint.setColor(mWhiteColor); 176 if (mTimerMode) { 177 canvas.drawArc(mArcRect, 270, + whitePercent * 360, false, mPaint); 178 } else { 179 canvas.drawArc(mArcRect, 270 + (1 - whitePercent) * 360, 180 whitePercent * 360, false, mPaint); 181 } 182 183 if (mMarkerTime != -1 && radius > 0 && mIntervalTime != 0) { 184 mPaint.setStrokeWidth(mMarkerStrokeSize); 185 float angle = (float)(mMarkerTime % mIntervalTime) / (float)mIntervalTime * 360; 186 // draw 2dips thick marker 187 // the formula to draw the marker 1 unit thick is: 188 // 180 / (radius * Math.PI) 189 // after that we have to scale it by the screen density 190 canvas.drawArc (mArcRect, 270 + angle, mScreenDensity * 191 (float) (360 / (radius * Math.PI)) , false, mPaint); 192 } 193 drawRedDot(canvas, redPercent, xCenter, yCenter, radius); 194 } 195 if (mAnimate) { 196 invalidate(); 197 } 198 } 199 200 protected void drawRedDot( 201 Canvas canvas, float degrees, int xCenter, int yCenter, float radius) { 202 mPaint.setColor(mAccentColor); 203 float dotPercent; 204 if (mTimerMode) { 205 dotPercent = 270 - degrees * 360; 206 } else { 207 dotPercent = 270 + degrees * 360; 208 } 209 210 final double dotRadians = Math.toRadians(dotPercent); 211 canvas.drawCircle(xCenter + (float) (radius * Math.cos(dotRadians)), 212 yCenter + (float) (radius * Math.sin(dotRadians)), mDotRadius, mFill); 213 } 214 215 public static final String PREF_CTV_PAUSED = "_ctv_paused"; 216 public static final String PREF_CTV_INTERVAL = "_ctv_interval"; 217 public static final String PREF_CTV_INTERVAL_START = "_ctv_interval_start"; 218 public static final String PREF_CTV_CURRENT_INTERVAL = "_ctv_current_interval"; 219 public static final String PREF_CTV_ACCUM_TIME = "_ctv_accum_time"; 220 public static final String PREF_CTV_TIMER_MODE = "_ctv_timer_mode"; 221 public static final String PREF_CTV_MARKER_TIME = "_ctv_marker_time"; 222 223 // Since this view is used in multiple places, use the key to save different instances 224 public void writeToSharedPref(SharedPreferences prefs, String key) { 225 SharedPreferences.Editor editor = prefs.edit(); 226 editor.putBoolean (key + PREF_CTV_PAUSED, mPaused); 227 editor.putLong (key + PREF_CTV_INTERVAL, mIntervalTime); 228 editor.putLong (key + PREF_CTV_INTERVAL_START, mIntervalStartTime); 229 editor.putLong (key + PREF_CTV_CURRENT_INTERVAL, mCurrentIntervalTime); 230 editor.putLong (key + PREF_CTV_ACCUM_TIME, mAccumulatedTime); 231 editor.putLong (key + PREF_CTV_MARKER_TIME, mMarkerTime); 232 editor.putBoolean (key + PREF_CTV_TIMER_MODE, mTimerMode); 233 editor.apply(); 234 } 235 236 public void readFromSharedPref(SharedPreferences prefs, String key) { 237 mPaused = prefs.getBoolean(key + PREF_CTV_PAUSED, false); 238 mIntervalTime = prefs.getLong(key + PREF_CTV_INTERVAL, 0); 239 mIntervalStartTime = prefs.getLong(key + PREF_CTV_INTERVAL_START, -1); 240 mCurrentIntervalTime = prefs.getLong(key + PREF_CTV_CURRENT_INTERVAL, 0); 241 mAccumulatedTime = prefs.getLong(key + PREF_CTV_ACCUM_TIME, 0); 242 mMarkerTime = prefs.getLong(key + PREF_CTV_MARKER_TIME, -1); 243 mTimerMode = prefs.getBoolean(key + PREF_CTV_TIMER_MODE, false); 244 mAnimate = (mIntervalStartTime != -1 && !mPaused); 245 } 246 247 public void clearSharedPref(SharedPreferences prefs, String key) { 248 SharedPreferences.Editor editor = prefs.edit(); 249 editor.remove (Stopwatches.PREF_START_TIME); 250 editor.remove (Stopwatches.PREF_ACCUM_TIME); 251 editor.remove (Stopwatches.PREF_STATE); 252 editor.remove (key + PREF_CTV_PAUSED); 253 editor.remove (key + PREF_CTV_INTERVAL); 254 editor.remove (key + PREF_CTV_INTERVAL_START); 255 editor.remove (key + PREF_CTV_CURRENT_INTERVAL); 256 editor.remove (key + PREF_CTV_ACCUM_TIME); 257 editor.remove (key + PREF_CTV_MARKER_TIME); 258 editor.remove (key + PREF_CTV_TIMER_MODE); 259 editor.apply(); 260 } 261 } 262