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.animation.ObjectAnimator;
     20 import android.animation.TimeInterpolator;
     21 import android.animation.ValueAnimator;
     22 import android.graphics.Canvas;
     23 import android.graphics.ColorFilter;
     24 import android.graphics.Paint;
     25 import android.graphics.PixelFormat;
     26 import android.graphics.RectF;
     27 import android.graphics.drawable.Drawable;
     28 import android.util.Property;
     29 import android.view.animation.LinearInterpolator;
     30 
     31 import androidx.annotation.RestrictTo;
     32 import androidx.annotation.RestrictTo.Scope;
     33 
     34 /**
     35  * Drawable for showing an indeterminate progress indicator.
     36  *
     37  * @hide
     38  */
     39 @RestrictTo(Scope.LIBRARY)
     40 class ProgressDrawable extends Drawable {
     41 
     42     private static final Property<ProgressDrawable, Integer> LEVEL =
     43             new Property<ProgressDrawable, Integer>(Integer.class, "level") {
     44                 @Override
     45                 public Integer get(ProgressDrawable drawable) {
     46                     return drawable.getLevel();
     47                 }
     48 
     49                 @Override
     50                 public void set(ProgressDrawable drawable, Integer value) {
     51                     drawable.setLevel(value);
     52                     drawable.invalidateSelf();
     53                 }
     54             };
     55     /**
     56      * Max level for a level drawable, as specified in developer docs for {@link Drawable}.
     57      */
     58     private static final int MAX_LEVEL = 10000;
     59 
     60     /**
     61      * How many different sections are there, five gives us the material style star. *
     62      */
     63     private static final int NUMBER_OF_SEGMENTS = 5;
     64 
     65     private static final int LEVELS_PER_SEGMENT = MAX_LEVEL / NUMBER_OF_SEGMENTS;
     66     private static final float STARTING_ANGLE = -90f;
     67     private static final long ANIMATION_DURATION = 6000;
     68     private static final int FULL_CIRCLE = 360;
     69     private static final int MAX_SWEEP = 306;
     70     private static final int CORRECTION_ANGLE = FULL_CIRCLE - MAX_SWEEP;
     71     /**
     72      * How far through each cycle does the bar stop growing and start shrinking, half way. *
     73      */
     74     private static final float GROW_SHRINK_RATIO = 0.5f;
     75     // TODO: replace this with BakedBezierInterpolator when its available in support library.
     76     private static final TimeInterpolator sInterpolator = BezierSCurveInterpolator.INSTANCE;
     77 
     78     private final RectF mInnerCircleBounds = new RectF();
     79     private final Paint mPaint = new Paint();
     80     private final ObjectAnimator mAnimator;
     81     private float mCircleBorderWidth;
     82     private int mCircleBorderColor;
     83 
     84     ProgressDrawable() {
     85         mPaint.setAntiAlias(true);
     86         mPaint.setStyle(Paint.Style.STROKE);
     87         mAnimator = ObjectAnimator.ofInt(this, LEVEL, 0, MAX_LEVEL);
     88         mAnimator.setRepeatCount(ValueAnimator.INFINITE);
     89         mAnimator.setRepeatMode(ValueAnimator.RESTART);
     90         mAnimator.setDuration(ANIMATION_DURATION);
     91         mAnimator.setInterpolator(new LinearInterpolator());
     92     }
     93 
     94     /**
     95      * Returns the interpolation scalar (s) that satisfies the equation:
     96      * {@code value = }lerp(a, b, s)
     97      *
     98      * <p>If {@code a == b}, then this function will return 0.
     99      */
    100     private static float lerpInv(float a, float b, float value) {
    101         return a != b ? ((value - a) / (b - a)) : 0.0f;
    102     }
    103 
    104     public void setRingColor(int color) {
    105         mCircleBorderColor = color;
    106     }
    107 
    108     public void setRingWidth(float width) {
    109         mCircleBorderWidth = width;
    110     }
    111 
    112     public void startAnimation() {
    113         if (!mAnimator.isStarted()) {
    114             mAnimator.start();
    115         }
    116     }
    117 
    118     public void stopAnimation() {
    119         mAnimator.cancel();
    120     }
    121 
    122     @Override
    123     public void draw(Canvas canvas) {
    124         canvas.save();
    125         mInnerCircleBounds.set(getBounds());
    126         mInnerCircleBounds.inset(mCircleBorderWidth / 2.0f, mCircleBorderWidth / 2.0f);
    127         mPaint.setStrokeWidth(mCircleBorderWidth);
    128         mPaint.setColor(mCircleBorderColor);
    129 
    130         int level = getLevel();
    131         int currentSegment = level / LEVELS_PER_SEGMENT;
    132         int offset = currentSegment * LEVELS_PER_SEGMENT;
    133         float progress = (level - offset) / (float) LEVELS_PER_SEGMENT;
    134 
    135         boolean growing = progress < GROW_SHRINK_RATIO;
    136         float correctionAngle = CORRECTION_ANGLE * progress;
    137 
    138         float sweepAngle;
    139 
    140         if (growing) {
    141             sweepAngle = MAX_SWEEP
    142                     * sInterpolator.getInterpolation(lerpInv(0f, GROW_SHRINK_RATIO, progress));
    143         } else {
    144             sweepAngle =
    145                     MAX_SWEEP
    146                             * (1.0f - sInterpolator.getInterpolation(
    147                             lerpInv(GROW_SHRINK_RATIO, 1.0f, progress)));
    148         }
    149 
    150         sweepAngle = Math.max(1, sweepAngle);
    151 
    152         canvas.rotate(
    153                 level * (1.0f / MAX_LEVEL) * 2 * FULL_CIRCLE + STARTING_ANGLE + correctionAngle,
    154                 mInnerCircleBounds.centerX(),
    155                 mInnerCircleBounds.centerY());
    156         canvas.drawArc(
    157                 mInnerCircleBounds, growing ? 0 : MAX_SWEEP - sweepAngle, sweepAngle, false,
    158                 mPaint);
    159         canvas.restore();
    160     }
    161 
    162     @Override
    163     public void setAlpha(int i) {
    164         // Not supported.
    165     }
    166 
    167     @Override
    168     public void setColorFilter(ColorFilter colorFilter) {
    169         // Not supported.
    170     }
    171 
    172     @Override
    173     public int getOpacity() {
    174         return PixelFormat.OPAQUE;
    175     }
    176 
    177     @Override
    178     protected boolean onLevelChange(int level) {
    179         return true; // Changing the level of this drawable does change its appearance.
    180     }
    181 }
    182