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")
CircleTimerView(Context context)45     public CircleTimerView(Context context) {
46         this(context, null);
47     }
48 
CircleTimerView(Context context, AttributeSet attrs)49     public CircleTimerView(Context context, AttributeSet attrs) {
50         super(context, attrs);
51         init(context);
52     }
53 
setIntervalTime(long t)54     public void setIntervalTime(long t) {
55         mIntervalTime = t;
56         postInvalidate();
57     }
58 
setMarkerTime(long t)59     public void setMarkerTime(long t) {
60         mMarkerTime = t;
61         postInvalidate();
62     }
63 
reset()64     public void reset() {
65         mIntervalStartTime = -1;
66         mMarkerTime = -1;
67         postInvalidate();
68     }
startIntervalAnimation()69     public void startIntervalAnimation() {
70         mIntervalStartTime = Utils.getTimeNow();
71         mAnimate = true;
72         invalidate();
73         mPaused = false;
74     }
stopIntervalAnimation()75     public void stopIntervalAnimation() {
76         mAnimate = false;
77         mIntervalStartTime = -1;
78         mAccumulatedTime = 0;
79     }
80 
isAnimating()81     public boolean isAnimating() {
82         return (mIntervalStartTime != -1);
83     }
84 
pauseIntervalAnimation()85     public void pauseIntervalAnimation() {
86         mAnimate = false;
87         mAccumulatedTime += Utils.getTimeNow() - mIntervalStartTime;
88         mPaused = true;
89     }
90 
abortIntervalAnimation()91     public void abortIntervalAnimation() {
92         mAnimate = false;
93     }
94 
setPassedTime(long time, boolean drawRed)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 
init(Context c)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 
setTimerMode(boolean mode)132     public void setTimerMode(boolean mode) {
133         mTimerMode = mode;
134     }
135 
136     @Override
onDraw(Canvas canvas)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 
drawRedDot( Canvas canvas, float degrees, int xCenter, int yCenter, float radius)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
writeToSharedPref(SharedPreferences prefs, String key)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 
readFromSharedPref(SharedPreferences prefs, String key)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 
clearSharedPref(SharedPreferences prefs, String key)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