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 */ 98 void onTimerFinished(CircularProgressLayout layout); 99 } 100 101 public CircularProgressLayout(Context context) { 102 this(context, null); 103 } 104 105 public CircularProgressLayout(Context context, AttributeSet attrs) { 106 this(context, attrs, 0); 107 } 108 109 public CircularProgressLayout(Context context, AttributeSet attrs, int defStyleAttr) { 110 this(context, attrs, defStyleAttr, 0); 111 } 112 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 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 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 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 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 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 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 */ 370 public void setOnTimerFinishedListener(@Nullable OnTimerFinishedListener listener) { 371 mController.setOnTimerFinishedListener(listener); 372 } 373 } 374