blob: 19790115701375a1709e050b0f8d822414442c40 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.wear.widget;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Build;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.widget.FrameLayout;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import androidx.swiperefreshlayout.widget.CircularProgressDrawable;
import androidx.wear.R;
/**
* {@link CircularProgressLayout} adds a circular countdown timer behind the view it contains,
* typically used to automatically confirm an operation after a short delay has elapsed.
*
* <p>The developer can specify a countdown interval via {@link #setTotalTime(long)} and a listener
* via {@link #setOnTimerFinishedListener(OnTimerFinishedListener)} to be called when the time has
* elapsed after {@link #startTimer()} has been called. Tap action can be received via {@link
* #setOnClickListener(OnClickListener)} and can be used to cancel the timer via {@link
* #stopTimer()} method.
*
* <p>Alternatively, this layout can be used to show indeterminate progress by calling {@link
* #setIndeterminate(boolean)} method.
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public class CircularProgressLayout extends FrameLayout {
/**
* Update interval for 60 fps.
*/
private static final long DEFAULT_UPDATE_INTERVAL = 1000 / 60;
/**
* Starting rotation for the progress indicator. Geometric clockwise [0..360] degree range
* correspond to [0..1] range. 0.75 corresponds to 12 o'clock direction on a watch.
*/
private static final float DEFAULT_ROTATION = 0.75f;
/**
* Used as background of this layout.
*/
private CircularProgressDrawable mProgressDrawable;
/**
* Used to control this layout.
*/
private CircularProgressLayoutController mController;
/**
* Angle for the progress to start from.
*/
private float mStartingRotation = DEFAULT_ROTATION;
/**
* Duration of the timer in milliseconds.
*/
private long mTotalTime;
/**
* Interface to implement for listening to {@link
* OnTimerFinishedListener#onTimerFinished(CircularProgressLayout)} event.
*/
public interface OnTimerFinishedListener {
/**
* Called when the timer started by {@link #startTimer()} method finishes.
*
* @param layout {@link CircularProgressLayout} that calls this method.
*/
void onTimerFinished(CircularProgressLayout layout);
}
public CircularProgressLayout(Context context) {
this(context, null);
}
public CircularProgressLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CircularProgressLayout(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public CircularProgressLayout(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mProgressDrawable = new CircularProgressDrawable(context);
mProgressDrawable.setProgressRotation(DEFAULT_ROTATION);
mProgressDrawable.setStrokeCap(Paint.Cap.BUTT);
setBackground(mProgressDrawable);
// If a child view is added, make it center aligned so it fits in the progress drawable.
setOnHierarchyChangeListener(new OnHierarchyChangeListener() {
@Override
public void onChildViewAdded(View parent, View child) {
// Ensure that child view is aligned in center
LayoutParams params = (LayoutParams) child.getLayoutParams();
params.gravity = Gravity.CENTER;
child.setLayoutParams(params);
}
@Override
public void onChildViewRemoved(View parent, View child) {
}
});
mController = new CircularProgressLayoutController(this);
Resources r = context.getResources();
TypedArray a = r.obtainAttributes(attrs, R.styleable.CircularProgressLayout);
if (a.getType(R.styleable.CircularProgressLayout_colorSchemeColors) == TypedValue
.TYPE_REFERENCE || !a.hasValue(
R.styleable.CircularProgressLayout_colorSchemeColors)) {
int arrayResId = a.getResourceId(R.styleable.CircularProgressLayout_colorSchemeColors,
R.array.circular_progress_layout_color_scheme_colors);
setColorSchemeColors(getColorListFromResources(r, arrayResId));
} else {
setColorSchemeColors(a.getColor(R.styleable.CircularProgressLayout_colorSchemeColors,
Color.BLACK));
}
setStrokeWidth(a.getDimensionPixelSize(R.styleable.CircularProgressLayout_strokeWidth,
r.getDimensionPixelSize(
R.dimen.circular_progress_layout_stroke_width)));
setBackgroundColor(a.getColor(R.styleable.CircularProgressLayout_backgroundColor,
ContextCompat.getColor(context,
R.color.circular_progress_layout_background_color)));
setIndeterminate(a.getBoolean(R.styleable.CircularProgressLayout_indeterminate, false));
a.recycle();
}
private int[] getColorListFromResources(Resources resources, int arrayResId) {
TypedArray colorArray = resources.obtainTypedArray(arrayResId);
int[] colors = new int[colorArray.length()];
for (int i = 0; i < colorArray.length(); i++) {
colors[i] = colorArray.getColor(i, 0);
}
colorArray.recycle();
return colors;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (getChildCount() != 0) {
View childView = getChildAt(0);
// Wrap the drawable around the child view
mProgressDrawable.setCenterRadius(
Math.min(childView.getWidth(), childView.getHeight()) / 2f);
} else {
// Fill the bounds if no child view is present
mProgressDrawable.setCenterRadius(0f);
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mController.reset();
}
/**
* Sets the background color of the {@link CircularProgressDrawable}, which is drawn as a circle
* inside the progress drawable. Colors are in ARGB format defined in {@link Color}.
*
* @param color an ARGB color
*/
@Override
public void setBackgroundColor(@ColorInt int color) {
mProgressDrawable.setBackgroundColor(color);
}
/**
* Returns the background color of the {@link CircularProgressDrawable}.
*
* @return an ARGB color
*/
@ColorInt
public int getBackgroundColor() {
return mProgressDrawable.getBackgroundColor();
}
/**
* Returns the {@link CircularProgressDrawable} used as background of this layout.
*
* @return {@link CircularProgressDrawable}
*/
@NonNull
public CircularProgressDrawable getProgressDrawable() {
return mProgressDrawable;
}
/**
* Sets if progress should be shown as an indeterminate spinner.
*
* @param indeterminate {@code true} if indeterminate spinner should be shown, {@code false}
* otherwise.
*/
public void setIndeterminate(boolean indeterminate) {
mController.setIndeterminate(indeterminate);
}
/**
* Returns if progress is showing as an indeterminate spinner.
*
* @return {@code true} if indeterminate spinner is shown, {@code false} otherwise.
*/
public boolean isIndeterminate() {
return mController.isIndeterminate();
}
/**
* Sets the total time in milliseconds for the timer to countdown to. Calling this method while
* the timer is already running will not change the duration of the current timer.
*
* @param totalTime total time in milliseconds
*/
public void setTotalTime(long totalTime) {
if (totalTime <= 0) {
throw new IllegalArgumentException("Total time should be greater than zero.");
}
mTotalTime = totalTime;
}
/**
* Returns the total time in milliseconds for the timer to countdown to.
*
* @return total time in milliseconds
*/
public long getTotalTime() {
return mTotalTime;
}
/**
* Starts the timer countdown. Once the countdown is finished, if there is an {@link
* OnTimerFinishedListener} registered by {@link
* #setOnTimerFinishedListener(OnTimerFinishedListener)} method, its
* {@link OnTimerFinishedListener#onTimerFinished(CircularProgressLayout)} method is called. If
* this method is called while there is already a running timer, it will restart the timer.
*/
public void startTimer() {
mController.startTimer(mTotalTime, DEFAULT_UPDATE_INTERVAL);
mProgressDrawable.setProgressRotation(mStartingRotation);
}
/**
* Stops the timer countdown. If there is no timer running, calling this method will not do
* anything.
*/
public void stopTimer() {
mController.stopTimer();
}
/**
* Returns if the timer is running.
*
* @return {@code true} if the timer is running, {@code false} otherwise
*/
public boolean isTimerRunning() {
return mController.isTimerRunning();
}
/**
* Sets the starting rotation for the progress drawable to start from. Default starting rotation
* is {@code 0.75} and it corresponds clockwise geometric 270 degrees (12 o'clock on a watch)
*
* @param rotation starting rotation from [0..1]
*/
public void setStartingRotation(float rotation) {
mStartingRotation = rotation;
}
/**
* Returns the starting rotation of the progress drawable.
*
* @return starting rotation from [0..1]
*/
public float getStartingRotation() {
return mStartingRotation;
}
/**
* Sets the stroke width of the progress drawable in pixels.
*
* @param strokeWidth stroke width in pixels
*/
public void setStrokeWidth(float strokeWidth) {
mProgressDrawable.setStrokeWidth(strokeWidth);
}
/**
* Returns the stroke width of the progress drawable in pixels.
*
* @return stroke width in pixels
*/
public float getStrokeWidth() {
return mProgressDrawable.getStrokeWidth();
}
/**
* Sets the color scheme colors of the progress drawable, which is equivalent to calling {@link
* CircularProgressDrawable#setColorSchemeColors(int...)} method on background drawable of this
* layout.
*
* @param colors list of ARGB colors
*/
public void setColorSchemeColors(int... colors) {
mProgressDrawable.setColorSchemeColors(colors);
}
/**
* Returns the color scheme colors of the progress drawable
*
* @return list of ARGB colors
*/
public int[] getColorSchemeColors() {
return mProgressDrawable.getColorSchemeColors();
}
/**
* Returns the {@link OnTimerFinishedListener} that is registered to this layout.
*
* @return registered {@link OnTimerFinishedListener}
*/
@Nullable
public OnTimerFinishedListener getOnTimerFinishedListener() {
return mController.getOnTimerFinishedListener();
}
/**
* Sets the {@link OnTimerFinishedListener} to be notified when timer countdown is finished.
*
* @param listener {@link OnTimerFinishedListener} to be notified, or {@code null} to clear
*/
public void setOnTimerFinishedListener(@Nullable OnTimerFinishedListener listener) {
mController.setOnTimerFinishedListener(listener);
}
}