1717
1818import 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 ;
2024import android .content .Context ;
2125import android .content .res .Resources ;
2226import android .content .res .Resources .Theme ;
3741import android .os .Build .VERSION ;
3842import android .os .Build .VERSION_CODES ;
3943import android .util .AttributeSet ;
44+ import android .util .FloatProperty ;
4045import android .util .StateSet ;
4146import android .util .TypedValue ;
47+ import android .view .animation .OvershootInterpolator ;
4248import androidx .annotation .NonNull ;
4349import androidx .annotation .Nullable ;
4450import 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