Skip to content

Commit

Permalink
[ProgressIndicator] Added waggle animation (wave speed) to active ind…
Browse files Browse the repository at this point in the history
…icator in both Linear and Circular types.

PiperOrigin-RevId: 620305285
  • Loading branch information
pekingme authored and paulfthomas committed Apr 2, 2024
1 parent b32512a commit 0f47e78
Show file tree
Hide file tree
Showing 9 changed files with 122 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,26 @@ public void setWavelength(@Px int wavelength) {
}
}

/**
* Returns the speed of the indicator's waveform in pixels.
*
* @see #setSpeed(int)
*/
@Px
public int getSpeed() {
return spec.speed;
}

/**
* Sets the speed of the indicator's waveform in pixels.
*
* @param speed The new speed in pixels.
* @see #getSpeed()
*/
public void setSpeed(@Px int speed) {
spec.speed = speed;
}

/**
* Returns the show animation behavior used in this progress indicator.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ public abstract class BaseProgressIndicatorSpec {
/** The size of the amplitude, if a wave effect is configured. */
@Px public int amplitude;

/** The speed of the waveform, if a wave effect is configured. */
@Px public int speed;

/**
* Instantiates BaseProgressIndicatorSpec.
*
Expand Down Expand Up @@ -119,6 +122,7 @@ protected BaseProgressIndicatorSpec(

wavelength = abs(a.getDimensionPixelSize(R.styleable.BaseProgressIndicator_wavelength, 0));
amplitude = abs(a.getDimensionPixelSize(R.styleable.BaseProgressIndicator_amplitude, 0));
speed = a.getDimensionPixelSize(R.styleable.BaseProgressIndicator_speed, 0);

loadIndicatorColors(context, a);
loadTrackColor(context, a);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static androidx.core.math.MathUtils.clamp;
import static com.google.android.material.math.MathUtils.lerp;
import static com.google.android.material.progressindicator.BaseProgressIndicator.HIDE_ESCAPE;
import static java.lang.Math.PI;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.lang.Math.toDegrees;
Expand Down Expand Up @@ -172,6 +173,7 @@ void fillIndicator(
color,
activeIndicator.gapSize,
activeIndicator.gapSize,
activeIndicator.phaseFraction,
/* shouldDrawActiveIndicator= */ true);
}

Expand All @@ -193,6 +195,7 @@ void fillTrack(
color,
gapSize,
gapSize,
/* phaseFraction= */ 0f,
/* shouldDrawActiveIndicator= */ false);
}

Expand All @@ -207,6 +210,7 @@ void fillTrack(
* @param paintColor The color used to draw the indicator.
* @param startGapSize The gap size applied to the start (rotating behind) of the drawing part.
* @param endGapSize The gap size applied to the end (rotating ahead) of the drawing part.
* @param phaseFraction The fraction [0, 1] of initial phase in one cycle.
* @param shouldDrawActiveIndicator Whether this part should be drawn as an active indicator.
*/
private void drawArc(
Expand All @@ -217,6 +221,7 @@ private void drawArc(
@ColorInt int paintColor,
@Px int startGapSize,
@Px int endGapSize,
float phaseFraction,
boolean shouldDrawActiveIndicator) {
float arcFraction =
endFraction >= startFraction
Expand All @@ -237,6 +242,7 @@ private void drawArc(
paintColor,
startGapSize,
/* endGapSize= */ 0,
phaseFraction,
shouldDrawActiveIndicator);
drawArc(
canvas,
Expand All @@ -246,6 +252,7 @@ private void drawArc(
paintColor,
/* startGapSize= */ 0,
endGapSize,
phaseFraction,
shouldDrawActiveIndicator);
return;
}
Expand Down Expand Up @@ -322,7 +329,8 @@ private void drawArc(
activePathMeasure,
displayedActivePath,
startDegreeWithoutCorners / 360,
arcDegreeWithoutCorners / 360);
arcDegreeWithoutCorners / 360,
phaseFraction);
canvas.drawPath(displayedActivePath, paint);
}

Expand Down Expand Up @@ -471,7 +479,11 @@ private void appendCubicPerHalfCycle(

@NonNull
private Pair<PathPoint, PathPoint> getDisplayedPath(
@NonNull PathMeasure pathMeasure, @NonNull Path displayedPath, float start, float span) {
@NonNull PathMeasure pathMeasure,
@NonNull Path displayedPath,
float start,
float span,
float phaseFraction) {
if (adjustedRadius != cachedRadius
|| (pathMeasure == activePathMeasure && displayedAmplitude != cachedAmplitude)) {
cachedAmplitude = displayedAmplitude;
Expand All @@ -481,6 +493,12 @@ private Pair<PathPoint, PathPoint> getDisplayedPath(
displayedPath.rewind();
span = clamp(span, 0, 1);
float resultRotation = 0;
if (spec.hasWavyEffect()) {
float cycleCount = (float) (2 * PI * adjustedRadius / adjustedWavelength);
float phaseFractionInOneCycle = phaseFraction / cycleCount;
start += phaseFractionInOneCycle;
resultRotation -= phaseFractionInOneCycle * 360;
}
start %= 1;
float startDistance = start * pathMeasure.getLength() / 2;
float endDistance = (start + span) * pathMeasure.getLength() / 2;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.google.android.material.progressindicator;

import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint.Style;
Expand All @@ -39,6 +40,8 @@ public final class DeterminateDrawable<S extends BaseProgressIndicatorSpec>
// If the progress is less than 1%, the gap will be proportional to the progress. So that, it
// draws a full track at 0%.
static final float GAP_RAMP_DOWN_THRESHOLD = 0.01f;
// The duration of repeated initial phase animation in ms. It can be any positive values.
private static final int PHASE_ANIMATION_DURATION_MS = 1000;

// Drawing delegate object.
private DrawingDelegate<S> drawingDelegate;
Expand All @@ -51,6 +54,8 @@ public final class DeterminateDrawable<S extends BaseProgressIndicatorSpec>
// Whether to skip the spring animation on level change event.
private boolean skipAnimationOnLevelChange = false;

@NonNull private final ValueAnimator phaseAnimator;

DeterminateDrawable(
@NonNull Context context,
@NonNull BaseProgressIndicatorSpec baseSpec,
Expand All @@ -60,6 +65,7 @@ public final class DeterminateDrawable<S extends BaseProgressIndicatorSpec>
setDrawingDelegate(drawingDelegate);
activeIndicator = new ActiveIndicator();

// Initializes a spring animator for progress animation.
springForce = new SpringForce();

springForce.setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY);
Expand All @@ -68,6 +74,19 @@ public final class DeterminateDrawable<S extends BaseProgressIndicatorSpec>
springAnimation = new SpringAnimation(this, INDICATOR_LENGTH_IN_LEVEL);
springAnimation.setSpring(springForce);

// Initializes a linear animator to enforce phase animation when progress is unchanged.
phaseAnimator = new ValueAnimator();
phaseAnimator.setDuration(PHASE_ANIMATION_DURATION_MS);
phaseAnimator.setFloatValues(0, 1);
phaseAnimator.setRepeatCount(ValueAnimator.INFINITE);
phaseAnimator.addUpdateListener(
animation -> {
if (baseSpec.speed != 0 && isVisible()) {
invalidateSelf();
}
});
phaseAnimator.start();

setGrowFraction(1f);
}

Expand Down Expand Up @@ -236,6 +255,8 @@ public void draw(@NonNull Canvas canvas) {
drawingDelegate.validateSpecAndAdjustCanvas(
canvas, getBounds(), getGrowFraction(), isShowing(), isHiding());

activeIndicator.phaseFraction = getPhaseFraction();

paint.setStyle(Style.FILL);
paint.setAntiAlias(true);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
*/
abstract class DrawableWithAnimatedVisibilityChange extends Drawable implements Animatable2Compat {

// Constant for mock values used in testing.
private static final float DEFAULT_MOCK_PHASE_FRACTION = -1f;

// Argument restart used in Drawable setVisible() doesn't matter in implementation.
private static final boolean DEFAULT_DRAWABLE_RESTART = false;

Expand All @@ -64,6 +67,7 @@ abstract class DrawableWithAnimatedVisibilityChange extends Drawable implements
private boolean mockShowAnimationRunning;
private boolean mockHideAnimationRunning;
private float mockGrowFraction;
private float mockPhaseFraction = DEFAULT_MOCK_PHASE_FRACTION;

// List of AnimationCallback to be called at the end of show/hide animation.
private List<AnimationCallback> animationCallbacks;
Expand Down Expand Up @@ -439,6 +443,29 @@ void setMockHideAnimationRunning(
mockGrowFraction = fraction;
}

@VisibleForTesting
void setMockPhaseFraction(@FloatRange(from = 0.0, to = 1.0) float fraction) {
mockPhaseFraction = fraction;
}

float getPhaseFraction() {
if (mockPhaseFraction > 0) {
return mockPhaseFraction;
}
float phaseFraction = 0f;
if (baseSpec.speed != 0) {
float durationScale =
animatorDurationScaleProvider.getSystemAnimatorDurationScale(
context.getContentResolver());
int cycleInMs = (int) (1000f * baseSpec.wavelength / baseSpec.speed * durationScale);
phaseFraction = (float) (System.currentTimeMillis() % cycleInMs) / cycleInMs;
if (phaseFraction < 0f) {
phaseFraction = (phaseFraction % 1) + 1f;
}
}
return phaseFraction;
}

// ******************* Properties *******************

private static final Property<DrawableWithAnimatedVisibilityChange, Float> GROW_FRACTION =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ protected static class ActiveIndicator {
// active indicator. But for linear contiguous indeterminate mode, the indicators are connecting
// to each other. Gaps are needed in this case.
@Px int gapSize;

// The fraction [0, 1] of the initial phase [0, 2 * PI] on indicator.
@FloatRange(from = 0.0, to = 1.0)
float phaseFraction;
}

/** An entity class for a point on a path, with the support of fundamental operations. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ public void draw(@NonNull Canvas canvas) {
indicatorIndex < animatorDelegate.activeIndicators.size();
indicatorIndex++) {
ActiveIndicator curIndicator = animatorDelegate.activeIndicators.get(indicatorIndex);
curIndicator.phaseFraction = getPhaseFraction();
// Draws indicators.
drawingDelegate.fillIndicator(canvas, paint, curIndicator, getAlpha());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ void fillIndicator(
color,
activeIndicator.gapSize,
activeIndicator.gapSize,
activeIndicator.phaseFraction,
/* drawingActiveIndicator= */ true);
}

Expand All @@ -179,6 +180,7 @@ void fillTrack(
color,
gapSize,
gapSize,
/* phaseFraction= */ 0f,
/* drawingActiveIndicator= */ false);
}

Expand All @@ -192,6 +194,7 @@ void fillTrack(
* @param paintColor The color used to draw the indicator.
* @param startGapSize The gap size applied to the start (left) of the drawing part.
* @param endGapSize The gap size applied to the end (right) of the drawing part.
* @param phaseFraction The fraction [0, 1] of initial phase in one cycle.
* @param drawingActiveIndicator Whether this part should be drawn as an active indicator.
*/
private void drawLine(
Expand All @@ -202,6 +205,7 @@ private void drawLine(
@ColorInt int paintColor,
@Px int startGapSize,
@Px int endGapSize,
float phaseFraction,
boolean drawingActiveIndicator) {
startFraction = clamp(startFraction, 0f, 1f);
endFraction = clamp(endFraction, 0f, 1f);
Expand Down Expand Up @@ -270,7 +274,8 @@ private void drawLine(
activePathMeasure,
displayedActivePath,
startBlockCenterX / trackLength,
endBlockCenterX / trackLength);
endBlockCenterX / trackLength,
phaseFraction);
canvas.drawPath(displayedActivePath, paint);
}
if (!useStrokeCap && displayedCornerRadius > 0) {
Expand Down Expand Up @@ -354,7 +359,7 @@ void invalidateCachedPaths() {
int cycleCount = (int) (trackLength / spec.wavelength);
adjustedWavelength = trackLength / cycleCount;
float smoothness = SINE_WAVE_FORM_SMOOTHNESS;
for (int i = 0; i < cycleCount; i++) {
for (int i = 0; i <= cycleCount; i++) {
cachedActivePath.cubicTo(2 * i + smoothness, 0, 2 * i + 1 - smoothness, 1, 2 * i + 1, 1);
cachedActivePath.cubicTo(
2 * i + 1 + smoothness, 1, 2 * i + 2 - smoothness, 0, 2 * i + 2, 0);
Expand All @@ -373,9 +378,21 @@ void invalidateCachedPaths() {

@NonNull
private Pair<PathPoint, PathPoint> getDisplayedPath(
@NonNull PathMeasure pathMeasure, @NonNull Path displayedPath, float start, float end) {
@NonNull PathMeasure pathMeasure,
@NonNull Path displayedPath,
float start,
float end,
float phaseFraction) {
displayedPath.rewind();
float resultTranslationX = -trackLength / 2;
if (spec.hasWavyEffect()) {
float cycleCount = trackLength / adjustedWavelength;
float phaseFractionInPath = phaseFraction / cycleCount;
float ratio = cycleCount / (cycleCount + 1);
start = (start + phaseFractionInPath) * ratio;
end = (end + phaseFractionInPath) * ratio;
resultTranslationX -= phaseFraction * adjustedWavelength;
}
float startDistance = start * pathMeasure.getLength();
float endDistance = end * pathMeasure.getLength();
pathMeasure.getSegment(startDistance, endDistance, displayedPath, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@
Defines the amplitude (in dp) of the wave effect.
-->
<attr name="amplitude" format="dimension"/>
<!--
Defines the wave speed (in dp/s) of the wavy effect. If positive, wave moves towards 100%; if
negative, wave moves towards 0%.
-->
<attr name="speed" format="dimension"/>
</declare-styleable>

<declare-styleable name="LinearProgressIndicator">
Expand Down

0 comments on commit 0f47e78

Please sign in to comment.