Home | History | Annotate | Download | only in widget
      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