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