• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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 
17 package androidx.wear.widget;
18 
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.content.res.TypedArray;
22 import android.graphics.Color;
23 import android.graphics.Paint;
24 import android.os.Build;
25 import android.util.AttributeSet;
26 import android.util.TypedValue;
27 import android.view.Gravity;
28 import android.view.View;
29 import android.widget.FrameLayout;
30 
31 import androidx.annotation.ColorInt;
32 import androidx.annotation.NonNull;
33 import androidx.annotation.Nullable;
34 import androidx.annotation.RequiresApi;
35 import androidx.core.content.ContextCompat;
36 import androidx.swiperefreshlayout.widget.CircularProgressDrawable;
37 import androidx.wear.R;
38 
39 /**
40  * {@link CircularProgressLayout} adds a circular countdown timer behind the view it contains,
41  * typically used to automatically confirm an operation after a short delay has elapsed.
42  *
43  * <p>The developer can specify a countdown interval via {@link #setTotalTime(long)} and a listener
44  * via {@link #setOnTimerFinishedListener(OnTimerFinishedListener)} to be called when the time has
45  * elapsed after {@link #startTimer()} has been called. Tap action can be received via {@link
46  * #setOnClickListener(OnClickListener)} and can be used to cancel the timer via {@link
47  * #stopTimer()} method.
48  *
49  * <p>Alternatively, this layout can be used to show indeterminate progress by calling {@link
50  * #setIndeterminate(boolean)} method.
51  */
52 @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
53 public class CircularProgressLayout extends FrameLayout {
54 
55     /**
56      * Update interval for 60 fps.
57      */
58     private static final long DEFAULT_UPDATE_INTERVAL = 1000 / 60;
59 
60     /**
61      * Starting rotation for the progress indicator. Geometric clockwise [0..360] degree range
62      * correspond to [0..1] range. 0.75 corresponds to 12 o'clock direction on a watch.
63      */
64     private static final float DEFAULT_ROTATION = 0.75f;
65 
66     /**
67      * Used as background of this layout.
68      */
69     private CircularProgressDrawable mProgressDrawable;
70 
71     /**
72      * Used to control this layout.
73      */
74     private CircularProgressLayoutController mController;
75 
76     /**
77      * Angle for the progress to start from.
78      */
79     private float mStartingRotation = DEFAULT_ROTATION;
80 
81     /**
82      * Duration of the timer in milliseconds.
83      */
84     private long mTotalTime;
85 
86 
87     /**
88      * Interface to implement for listening to {@link
89      * OnTimerFinishedListener#onTimerFinished(CircularProgressLayout)} event.
90      */
91     public interface OnTimerFinishedListener {
92 
93         /**
94          * Called when the timer started by {@link #startTimer()} method finishes.
95          *
96          * @param layout {@link CircularProgressLayout} that calls this method.
97          */
onTimerFinished(CircularProgressLayout layout)98         void onTimerFinished(CircularProgressLayout layout);
99     }
100 
CircularProgressLayout(Context context)101     public CircularProgressLayout(Context context) {
102         this(context, null);
103     }
104 
CircularProgressLayout(Context context, AttributeSet attrs)105     public CircularProgressLayout(Context context, AttributeSet attrs) {
106         this(context, attrs, 0);
107     }
108 
CircularProgressLayout(Context context, AttributeSet attrs, int defStyleAttr)109     public CircularProgressLayout(Context context, AttributeSet attrs, int defStyleAttr) {
110         this(context, attrs, defStyleAttr, 0);
111     }
112 
CircularProgressLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)113     public CircularProgressLayout(Context context, AttributeSet attrs, int defStyleAttr,
114             int defStyleRes) {
115         super(context, attrs, defStyleAttr, defStyleRes);
116 
117         mProgressDrawable = new CircularProgressDrawable(context);
118         mProgressDrawable.setProgressRotation(DEFAULT_ROTATION);
119         mProgressDrawable.setStrokeCap(Paint.Cap.BUTT);
120         setBackground(mProgressDrawable);
121 
122         // If a child view is added, make it center aligned so it fits in the progress drawable.
123         setOnHierarchyChangeListener(new OnHierarchyChangeListener() {
124             @Override
125             public void onChildViewAdded(View parent, View child) {
126                 // Ensure that child view is aligned in center
127                 LayoutParams params = (LayoutParams) child.getLayoutParams();
128                 params.gravity = Gravity.CENTER;
129                 child.setLayoutParams(params);
130             }
131 
132             @Override
133             public void onChildViewRemoved(View parent, View child) {
134 
135             }
136         });
137 
138         mController = new CircularProgressLayoutController(this);
139 
140         Resources r = context.getResources();
141         TypedArray a = r.obtainAttributes(attrs, R.styleable.CircularProgressLayout);
142 
143         if (a.getType(R.styleable.CircularProgressLayout_colorSchemeColors) == TypedValue
144                 .TYPE_REFERENCE || !a.hasValue(
145                 R.styleable.CircularProgressLayout_colorSchemeColors)) {
146             int arrayResId = a.getResourceId(R.styleable.CircularProgressLayout_colorSchemeColors,
147                     R.array.circular_progress_layout_color_scheme_colors);
148             setColorSchemeColors(getColorListFromResources(r, arrayResId));
149         } else {
150             setColorSchemeColors(a.getColor(R.styleable.CircularProgressLayout_colorSchemeColors,
151                     Color.BLACK));
152         }
153 
154         setStrokeWidth(a.getDimensionPixelSize(R.styleable.CircularProgressLayout_strokeWidth,
155                 r.getDimensionPixelSize(
156                         R.dimen.circular_progress_layout_stroke_width)));
157 
158         setBackgroundColor(a.getColor(R.styleable.CircularProgressLayout_backgroundColor,
159                 ContextCompat.getColor(context,
160                         R.color.circular_progress_layout_background_color)));
161 
162         setIndeterminate(a.getBoolean(R.styleable.CircularProgressLayout_indeterminate, false));
163 
164         a.recycle();
165     }
166 
getColorListFromResources(Resources resources, int arrayResId)167     private int[] getColorListFromResources(Resources resources, int arrayResId) {
168         TypedArray colorArray = resources.obtainTypedArray(arrayResId);
169         int[] colors = new int[colorArray.length()];
170         for (int i = 0; i < colorArray.length(); i++) {
171             colors[i] = colorArray.getColor(i, 0);
172         }
173         colorArray.recycle();
174         return colors;
175     }
176 
177     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)178     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
179         super.onLayout(changed, left, top, right, bottom);
180         if (getChildCount() != 0) {
181             View childView = getChildAt(0);
182             // Wrap the drawable around the child view
183             mProgressDrawable.setCenterRadius(
184                     Math.min(childView.getWidth(), childView.getHeight()) / 2f);
185         } else {
186             // Fill the bounds if no child view is present
187             mProgressDrawable.setCenterRadius(0f);
188         }
189     }
190 
191     @Override
onDetachedFromWindow()192     protected void onDetachedFromWindow() {
193         super.onDetachedFromWindow();
194         mController.reset();
195     }
196 
197     /**
198      * Sets the background color of the {@link CircularProgressDrawable}, which is drawn as a circle
199      * inside the progress drawable. Colors are in ARGB format defined in {@link Color}.
200      *
201      * @param color an ARGB color
202      */
203     @Override
setBackgroundColor(@olorInt int color)204     public void setBackgroundColor(@ColorInt int color) {
205         mProgressDrawable.setBackgroundColor(color);
206     }
207 
208     /**
209      * Returns the background color of the {@link CircularProgressDrawable}.
210      *
211      * @return an ARGB color
212      */
213     @ColorInt
getBackgroundColor()214     public int getBackgroundColor() {
215         return mProgressDrawable.getBackgroundColor();
216     }
217 
218     /**
219      * Returns the {@link CircularProgressDrawable} used as background of this layout.
220      *
221      * @return {@link CircularProgressDrawable}
222      */
223     @NonNull
getProgressDrawable()224     public CircularProgressDrawable getProgressDrawable() {
225         return mProgressDrawable;
226     }
227 
228     /**
229      * Sets if progress should be shown as an indeterminate spinner.
230      *
231      * @param indeterminate {@code true} if indeterminate spinner should be shown, {@code false}
232      *                      otherwise.
233      */
setIndeterminate(boolean indeterminate)234     public void setIndeterminate(boolean indeterminate) {
235         mController.setIndeterminate(indeterminate);
236     }
237 
238     /**
239      * Returns if progress is showing as an indeterminate spinner.
240      *
241      * @return {@code true} if indeterminate spinner is shown, {@code false} otherwise.
242      */
isIndeterminate()243     public boolean isIndeterminate() {
244         return mController.isIndeterminate();
245     }
246 
247     /**
248      * Sets the total time in milliseconds for the timer to countdown to. Calling this method while
249      * the timer is already running will not change the duration of the current timer.
250      *
251      * @param totalTime total time in milliseconds
252      */
setTotalTime(long totalTime)253     public void setTotalTime(long totalTime) {
254         if (totalTime <= 0) {
255             throw new IllegalArgumentException("Total time should be greater than zero.");
256         }
257         mTotalTime = totalTime;
258     }
259 
260     /**
261      * Returns the total time in milliseconds for the timer to countdown to.
262      *
263      * @return total time in milliseconds
264      */
getTotalTime()265     public long getTotalTime() {
266         return mTotalTime;
267     }
268 
269     /**
270      * Starts the timer countdown. Once the countdown is finished, if there is an {@link
271      * OnTimerFinishedListener} registered by {@link
272      * #setOnTimerFinishedListener(OnTimerFinishedListener)} method, its
273      * {@link OnTimerFinishedListener#onTimerFinished(CircularProgressLayout)} method is called. If
274      * this method is called while there is already a running timer, it will restart the timer.
275      */
startTimer()276     public void startTimer() {
277         mController.startTimer(mTotalTime, DEFAULT_UPDATE_INTERVAL);
278         mProgressDrawable.setProgressRotation(mStartingRotation);
279     }
280 
281     /**
282      * Stops the timer countdown. If there is no timer running, calling this method will not do
283      * anything.
284      */
stopTimer()285     public void stopTimer() {
286         mController.stopTimer();
287     }
288 
289     /**
290      * Returns if the timer is running.
291      *
292      * @return {@code true} if the timer is running, {@code false} otherwise
293      */
isTimerRunning()294     public boolean isTimerRunning() {
295         return mController.isTimerRunning();
296     }
297 
298     /**
299      * Sets the starting rotation for the progress drawable to start from. Default starting rotation
300      * is {@code 0.75} and it corresponds clockwise geometric 270 degrees (12 o'clock on a watch)
301      *
302      * @param rotation starting rotation from [0..1]
303      */
setStartingRotation(float rotation)304     public void setStartingRotation(float rotation) {
305         mStartingRotation = rotation;
306     }
307 
308     /**
309      * Returns the starting rotation of the progress drawable.
310      *
311      * @return starting rotation from [0..1]
312      */
getStartingRotation()313     public float getStartingRotation() {
314         return mStartingRotation;
315     }
316 
317     /**
318      * Sets the stroke width of the progress drawable in pixels.
319      *
320      * @param strokeWidth stroke width in pixels
321      */
setStrokeWidth(float strokeWidth)322     public void setStrokeWidth(float strokeWidth) {
323         mProgressDrawable.setStrokeWidth(strokeWidth);
324     }
325 
326     /**
327      * Returns the stroke width of the progress drawable in pixels.
328      *
329      * @return stroke width in pixels
330      */
getStrokeWidth()331     public float getStrokeWidth() {
332         return mProgressDrawable.getStrokeWidth();
333     }
334 
335     /**
336      * Sets the color scheme colors of the progress drawable, which is equivalent to calling {@link
337      * CircularProgressDrawable#setColorSchemeColors(int...)} method on background drawable of this
338      * layout.
339      *
340      * @param colors list of ARGB colors
341      */
setColorSchemeColors(int... colors)342     public void setColorSchemeColors(int... colors) {
343         mProgressDrawable.setColorSchemeColors(colors);
344     }
345 
346     /**
347      * Returns the color scheme colors of the progress drawable
348      *
349      * @return list of ARGB colors
350      */
getColorSchemeColors()351     public int[] getColorSchemeColors() {
352         return mProgressDrawable.getColorSchemeColors();
353     }
354 
355     /**
356      * Returns the {@link OnTimerFinishedListener} that is registered to this layout.
357      *
358      * @return registered {@link OnTimerFinishedListener}
359      */
360     @Nullable
getOnTimerFinishedListener()361     public OnTimerFinishedListener getOnTimerFinishedListener() {
362         return mController.getOnTimerFinishedListener();
363     }
364 
365     /**
366      * Sets the {@link OnTimerFinishedListener} to be notified when timer countdown is finished.
367      *
368      * @param listener {@link OnTimerFinishedListener} to be notified, or {@code null} to clear
369      */
setOnTimerFinishedListener(@ullable OnTimerFinishedListener listener)370     public void setOnTimerFinishedListener(@Nullable OnTimerFinishedListener listener) {
371         mController.setOnTimerFinishedListener(listener);
372     }
373 }
374