Skip to content

Commit f7e6715

Browse files
committed
[Focus Rings] Add animation to FocusRingDrawable
PiperOrigin-RevId: 895334381
1 parent a87e102 commit f7e6715

1 file changed

Lines changed: 86 additions & 4 deletions

File tree

lib/java/com/google/android/material/focus/FocusRingDrawable.java

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717

1818
import com.google.android.material.R;
1919

20+
import android.animation.Animator;
21+
import android.animation.AnimatorListenerAdapter;
22+
import android.animation.ObjectAnimator;
23+
import android.animation.TimeInterpolator;
2024
import android.content.Context;
2125
import android.content.res.Resources;
2226
import android.content.res.Resources.Theme;
@@ -37,8 +41,10 @@
3741
import android.os.Build.VERSION;
3842
import android.os.Build.VERSION_CODES;
3943
import android.util.AttributeSet;
44+
import android.util.FloatProperty;
4045
import android.util.StateSet;
4146
import android.util.TypedValue;
47+
import android.view.animation.OvershootInterpolator;
4248
import androidx.annotation.NonNull;
4349
import androidx.annotation.Nullable;
4450
import androidx.annotation.RequiresApi;
@@ -60,6 +66,24 @@ public class FocusRingDrawable extends DrawableWrapper {
6066
private static final Drawable EMPTY_DRAWABLE = new ColorDrawable(Color.TRANSPARENT);
6167
private static final int[] FOCUSED_STATE_SET = {android.R.attr.state_focused};
6268

69+
private static final TimeInterpolator INTERPOLATOR = new OvershootInterpolator(4f);
70+
private static final int ANIMATION_DURATION = 300;
71+
72+
@RequiresApi(VERSION_CODES.N)
73+
private static final FloatProperty<FocusRingDrawable> PROPERTY_INTERPOLATION =
74+
new FloatProperty<FocusRingDrawable>("interpolation") {
75+
@Override
76+
public void setValue(FocusRingDrawable drawable, float value) {
77+
drawable.interpolation = value;
78+
drawable.invalidateSelf();
79+
}
80+
81+
@Override
82+
public Float get(FocusRingDrawable drawable) {
83+
return drawable.interpolation;
84+
}
85+
};
86+
6387
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
6488
private final RectF tmpRectF = new RectF();
6589
private final Rect tmpRect = new Rect();
@@ -71,6 +95,9 @@ public class FocusRingDrawable extends DrawableWrapper {
7195

7296
@Nullable private WeakReference<MaterialShapeDrawable> materialShapeDrawable;
7397
private float shapeAppearanceCornerSize = -1;
98+
@Nullable private ObjectAnimator animator;
99+
private float interpolation = 1f;
100+
private boolean previousStateSetEmpty;
74101
private boolean focused = false;
75102
private boolean mutated = false;
76103

@@ -132,6 +159,9 @@ public static FocusRingDrawable layer(
132159
focusRingDrawable.setFocusRingMaterialShapeDrawable(materialShapeDrawable);
133160
}
134161
layerDrawable.addLayer(focusRingDrawable);
162+
// Needed when the FocusRingDrawable is not the view's overall background, to ensure that
163+
// invalidateSelf() calls during the animation work.
164+
focusRingDrawable.setCallback(layerDrawable);
135165
return focusRingDrawable;
136166
}
137167

@@ -378,9 +408,44 @@ protected boolean onStateChange(@NonNull int[] stateSet) {
378408
boolean focused = StateSet.stateSetMatches(FOCUSED_STATE_SET, stateSet);
379409
boolean changed = this.focused != focused;
380410
this.focused = focused;
411+
412+
// Don't cancel or start the animation if the current or previous state set is / was empty. This
413+
// is a workaround for MaterialButton which strangely has empty state sets come through on focus
414+
// and press, which causes the focus ring animation to be played multiple times.
415+
if (changed && stateSet.length > 0 && !previousStateSetEmpty) {
416+
maybeAnimate(focused);
417+
}
418+
419+
previousStateSetEmpty = stateSet.length == 0;
420+
381421
return super.onStateChange(stateSet) || changed;
382422
}
383423

424+
private void maybeAnimate(boolean focused) {
425+
if (animator != null) {
426+
animator.cancel();
427+
animator = null;
428+
}
429+
if (focused) {
430+
if (VERSION.SDK_INT >= VERSION_CODES.N) {
431+
animator = createAnimator();
432+
animator.start();
433+
}
434+
} else {
435+
interpolation = 1f;
436+
}
437+
}
438+
439+
@Override
440+
public void jumpToCurrentState() {
441+
super.jumpToCurrentState();
442+
443+
if (animator != null) {
444+
animator.end();
445+
animator = null;
446+
}
447+
}
448+
384449
@Override
385450
public boolean isStateful() {
386451
return super.isStateful() || state.ringEnabled;
@@ -447,7 +512,7 @@ private void drawPath(Canvas canvas, Path path, float inset, float strokeWidth,
447512
matrix.postScale(scaleX, scaleY, tmpRectF.centerX(), tmpRectF.centerY());
448513
path.transform(matrix, tmpPath);
449514

450-
paint.setStrokeWidth(strokeWidth);
515+
paint.setStrokeWidth(strokeWidth * interpolation);
451516
paint.setColor(color);
452517
canvas.drawPath(tmpPath, paint);
453518
}
@@ -457,7 +522,7 @@ private void drawRoundRect(
457522
calculateBounds(tmpRectF);
458523
tmpRectF.inset(inset, inset);
459524

460-
paint.setStrokeWidth(strokeWidth);
525+
paint.setStrokeWidth(strokeWidth * interpolation);
461526
paint.setColor(color);
462527
canvas.drawRoundRect(tmpRectF, radius, radius, paint);
463528
}
@@ -526,11 +591,11 @@ private void calculateBounds(RectF rectF) {
526591
}
527592

528593
private float calculateOuterInset() {
529-
return state.ringInset + state.ringOuterStrokeWidth / 2f;
594+
return state.ringInset + state.ringOuterStrokeWidth / 2f * interpolation;
530595
}
531596

532597
private float calculateInnerInset() {
533-
return state.ringInset + state.ringInnerInset + state.ringInnerStrokeWidth / 2f;
598+
return state.ringInset + state.ringInnerInset + state.ringInnerStrokeWidth / 2f * interpolation;
534599
}
535600

536601
private float calculateOuterRadius() {
@@ -584,6 +649,23 @@ private void calculateShapeAppearanceRoundRectOrPath() {
584649
}
585650
}
586651

652+
@RequiresApi(VERSION_CODES.N)
653+
private ObjectAnimator createAnimator() {
654+
ObjectAnimator animator = ObjectAnimator.ofFloat(this, PROPERTY_INTERPOLATION, 0f, 1f);
655+
animator.setDuration(ANIMATION_DURATION);
656+
animator.setInterpolator(INTERPOLATOR);
657+
animator.addListener(
658+
new AnimatorListenerAdapter() {
659+
@Override
660+
public void onAnimationCancel(Animator animation) {
661+
super.onAnimationCancel(animation);
662+
interpolation = 1f;
663+
invalidateSelf();
664+
}
665+
});
666+
return animator;
667+
}
668+
587669
@CanIgnoreReturnValue
588670
@NonNull
589671
@Override

0 commit comments

Comments
 (0)