Skip to content

Commit 24693b1

Browse files
committed
[Focus Rings] Integrate FocusRingDrawable into components (disabled by default)
PiperOrigin-RevId: 893469825
1 parent db1cf64 commit 24693b1

31 files changed

Lines changed: 331 additions & 71 deletions

File tree

lib/java/com/google/android/material/checkbox/res/values-v23/styles.xml renamed to lib/java/com/google/android/material/appbar/res/values-v24/styles.xml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<!--
3-
~ Copyright (C) 2021 The Android Open Source Project
3+
~ Copyright (C) 2026 The Android Open Source Project
44
~
55
~ Licensed under the Apache License, Version 2.0 (the "License");
66
~ you may not use this file except in compliance with the License.
@@ -15,7 +15,10 @@
1515
~ limitations under the License.
1616
-->
1717
<resources>
18-
<style name="Widget.Material3.CompoundButton.CheckBox" parent="Base.Widget.Material3.CompoundButton.CheckBox">
19-
<item name="android:background">@drawable/m3_selection_control_ripple</item>
18+
19+
<!-- TODO: remove and update main style to use ?attr/controlBackground for android:background. -->
20+
<style name="Widget.Material3.Toolbar.Button.Navigation" parent="Widget.AppCompat.Toolbar.Button.Navigation">
21+
<item name="android:background">@drawable/m3_focus_ring_control_background</item>
2022
</style>
23+
2124
</resources>

lib/java/com/google/android/material/appbar/res/values/styles.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,10 @@
173173
<item name="toolbarNavigationButtonStyle">@style/Widget.Material3.Toolbar.Button.Navigation.Circle</item>
174174
</style>
175175

176-
<style name="Widget.Material3.Toolbar.Button.Navigation.Circle" parent="Widget.AppCompat.Toolbar.Button.Navigation">
176+
<!-- TODO: update to use ?attr/controlBackground for android:background and remove v24 override. -->
177+
<style name="Widget.Material3.Toolbar.Button.Navigation" parent="Widget.AppCompat.Toolbar.Button.Navigation" />
178+
179+
<style name="Widget.Material3.Toolbar.Button.Navigation.Circle">
177180
<item name="android:background">@drawable/m3_toolbar_navigation_circle_background</item>
178181
</style>
179182

lib/java/com/google/android/material/button/MaterialButtonHelper.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import androidx.annotation.RestrictTo;
3838
import androidx.dynamicanimation.animation.SpringForce;
3939
import com.google.android.material.color.MaterialColors;
40+
import com.google.android.material.focus.FocusRingDrawable;
4041
import com.google.android.material.internal.ViewUtils;
4142
import com.google.android.material.resources.MaterialResources;
4243
import com.google.android.material.ripple.RippleUtils;
@@ -74,7 +75,7 @@ class MaterialButtonHelper {
7475
private boolean cornerRadiusSet = false;
7576
private boolean checkable;
7677
private boolean toggleCheckedStateOnClick = true;
77-
private LayerDrawable rippleDrawable;
78+
private RippleDrawable rippleDrawable;
7879
private int elevation;
7980

8081
MaterialButtonHelper(MaterialButton button, @NonNull ShapeAppearance shapeAppearance) {
@@ -147,6 +148,13 @@ private void updateBackground() {
147148
// RippleDrawable lose their states, we need to reset the state here.
148149
materialShapeDrawable.setState(materialButton.getDrawableState());
149150
}
151+
152+
// Similar to the comment above, we need to set up the focus ring -> shape drawable connection
153+
// here, because the ripple's child drawables will be recreated when the background is set.
154+
FocusRingDrawable focusRingDrawable = FocusRingDrawable.find(materialButton.getBackground());
155+
if (focusRingDrawable != null) {
156+
focusRingDrawable.setFocusRingMaterialShapeDrawable(materialShapeDrawable);
157+
}
150158
}
151159

152160
/**
@@ -246,6 +254,7 @@ private Drawable createBackground() {
246254
new LayerDrawable(
247255
new Drawable[] {surfaceColorStrokeDrawable, backgroundDrawable})),
248256
maskDrawable);
257+
FocusRingDrawable.layer(context, rippleDrawable);
249258
return rippleDrawable;
250259
}
251260

@@ -420,15 +429,12 @@ private void updateButtonShape() {
420429

421430
@Nullable
422431
public Shapeable getMaskDrawable() {
423-
if (rippleDrawable != null && rippleDrawable.getNumberOfLayers() > 1) {
424-
if (rippleDrawable.getNumberOfLayers() > 2) {
425-
// This is a LayerDrawable with 3 layers, so return the mask layer
426-
return (Shapeable) rippleDrawable.getDrawable(2);
432+
if (rippleDrawable != null) {
433+
Drawable mask = rippleDrawable.findDrawableByLayerId(android.R.id.mask);
434+
if (mask instanceof Shapeable) {
435+
return (Shapeable) mask;
427436
}
428-
// This is a RippleDrawable, so return the mask layer
429-
return (Shapeable) rippleDrawable.getDrawable(1);
430437
}
431-
432438
return null;
433439
}
434440

lib/java/com/google/android/material/card/MaterialCardViewHelper.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import com.google.android.material.animation.AnimationUtils;
5353
import com.google.android.material.card.MaterialCardView.CheckedIconGravity;
5454
import com.google.android.material.color.MaterialColors;
55+
import com.google.android.material.focus.FocusRingDrawable;
5556
import com.google.android.material.motion.MotionUtils;
5657
import com.google.android.material.resources.MaterialResources;
5758
import com.google.android.material.shape.CornerTreatment;
@@ -729,10 +730,13 @@ private Drawable getClickableForeground() {
729730
}
730731

731732
if (clickableForegroundDrawable == null) {
732-
clickableForegroundDrawable =
733+
LayerDrawable layerDrawable =
733734
new LayerDrawable(
734735
new Drawable[] {rippleDrawable, foregroundContentDrawable, checkedIcon});
735-
clickableForegroundDrawable.setId(CHECKED_ICON_LAYER_INDEX, R.id.mtrl_card_checked_layer_id);
736+
FocusRingDrawable.layer(
737+
materialCardView.getContext(), layerDrawable, foregroundShapeDrawable);
738+
layerDrawable.setId(CHECKED_ICON_LAYER_INDEX, R.id.mtrl_card_checked_layer_id);
739+
clickableForegroundDrawable = layerDrawable;
736740
}
737741

738742
return clickableForegroundDrawable;

lib/java/com/google/android/material/carousel/MaskableFrameLayout.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import androidx.annotation.VisibleForTesting;
3434
import androidx.core.math.MathUtils;
3535
import com.google.android.material.animation.AnimationUtils;
36+
import com.google.android.material.focus.FocusRingDrawable;
3637
import com.google.android.material.shape.AbsoluteCornerSize;
3738
import com.google.android.material.shape.ClampedCornerSize;
3839
import com.google.android.material.shape.ShapeAppearanceModel;
@@ -117,6 +118,18 @@ public void setShapeAppearanceModel(@NonNull ShapeAppearanceModel shapeAppearanc
117118
}
118119
});
119120
shapeableDelegate.onShapeAppearanceChanged(this, this.shapeAppearanceModel);
121+
122+
FocusRingDrawable focusRingBackground = FocusRingDrawable.find(getBackground());
123+
if (focusRingBackground != null) {
124+
focusRingBackground.mutate();
125+
focusRingBackground.setFocusRingShapeAppearance(this.shapeAppearanceModel);
126+
}
127+
128+
FocusRingDrawable focusRingForeground = FocusRingDrawable.find(getForeground());
129+
if (focusRingForeground != null) {
130+
focusRingForeground.mutate();
131+
focusRingForeground.setFocusRingShapeAppearance(this.shapeAppearanceModel);
132+
}
120133
}
121134

122135
@NonNull

lib/java/com/google/android/material/checkbox/MaterialCheckBox.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import android.graphics.Rect;
3232
import android.graphics.drawable.AnimatedStateListDrawable;
3333
import android.graphics.drawable.Drawable;
34+
import android.graphics.drawable.DrawableWrapper;
35+
import android.graphics.drawable.RippleDrawable;
3436
import android.os.Build.VERSION;
3537
import android.os.Build.VERSION_CODES;
3638
import android.os.Parcel;
@@ -273,6 +275,12 @@ && isButtonDrawableLegacy(attributes)) {
273275
attributes.getInt(R.styleable.MaterialCheckBox_checkedState, STATE_UNCHECKED));
274276
}
275277

278+
if (attributes.hasValue(R.styleable.MaterialCheckBox_rippleColor)) {
279+
setRippleColor(
280+
MaterialResources.getColorStateList(
281+
context, attributes, R.styleable.MaterialCheckBox_rippleColor));
282+
}
283+
276284
attributes.recycle();
277285

278286
refreshButtonDrawable();
@@ -832,6 +840,19 @@ private ColorStateList getMaterialThemeColorsTintList() {
832840
return materialThemeColorsTintList;
833841
}
834842

843+
private void setRippleColor(@Nullable ColorStateList rippleColor) {
844+
if (rippleColor == null) {
845+
return;
846+
}
847+
Drawable background = getBackground();
848+
if (background instanceof DrawableWrapper) {
849+
background = ((DrawableWrapper) background).getDrawable();
850+
}
851+
if (background instanceof RippleDrawable) {
852+
((RippleDrawable) background).setColor(rippleColor);
853+
}
854+
}
855+
835856
@Override
836857
@Nullable
837858
public Parcelable onSaveInstanceState() {

lib/java/com/google/android/material/checkbox/res/values/attrs.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@
7070
<!-- The indeterminate state of the checkbox. -->
7171
<enum name="indeterminate" value="2"/>
7272
</attr>
73+
<!-- Ripple color for the checkbox. This may be a color state list, if the
74+
desired ripple color should be stateful. Attribute type definition is
75+
in ripple package. -->
76+
<attr name="rippleColor" />
7377
</declare-styleable>
7478

7579
<declare-styleable name="MaterialCheckBoxStates">

lib/java/com/google/android/material/checkbox/res/values/styles.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@
3333
didn't have the concept of an icon drawable separate from the button
3434
drawable. The default icon drawable will be set in MaterialCheckBox. -->
3535
<item name="buttonIcon">@null</item>
36+
<item name="android:background">?attr/controlBackground</item>
37+
<item name="rippleColor">@color/m3_selection_control_ripple_color_selector</item>
38+
<item name="materialThemeOverlay">@style/ThemeOverlay.Material3.CheckBox</item>
39+
</style>
40+
41+
<style name="ThemeOverlay.Material3.CheckBox" parent="">
42+
<item name="focusRingsRadius">4dp</item>
43+
<item name="focusRingsInset">2dp</item>
3644
</style>
3745

3846
<style name="Widget.Material3.CompoundButton.CheckBox" parent="Base.Widget.Material3.CompoundButton.CheckBox" />

lib/java/com/google/android/material/chip/Chip.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
import androidx.customview.widget.ExploreByTouchHelper;
7777
import com.google.android.material.animation.MotionSpec;
7878
import com.google.android.material.chip.ChipDrawable.Delegate;
79+
import com.google.android.material.focus.FocusRingDrawable;
7980
import com.google.android.material.internal.MaterialCheckable;
8081
import com.google.android.material.internal.ThemeEnforcement;
8182
import com.google.android.material.resources.MaterialAttributes;
@@ -456,11 +457,13 @@ public Drawable getBackgroundDrawable() {
456457

457458
private void updateFrameworkRippleBackground() {
458459
//noinspection NewApi
459-
ripple =
460+
RippleDrawable rippleDrawable =
460461
new RippleDrawable(
461462
RippleUtils.sanitizeRippleDrawableColor(chipDrawable.getRippleColor()),
462463
getBackgroundDrawable(),
463464
null);
465+
FocusRingDrawable.layer(getContext(), rippleDrawable, chipDrawable);
466+
ripple = rippleDrawable;
464467
chipDrawable.setUseCompatRipple(false);
465468
//noinspection NewApi
466469
setBackground(ripple);

lib/java/com/google/android/material/chip/ChipDrawable.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
import com.google.android.material.canvas.CanvasCompat;
7171
import com.google.android.material.color.MaterialColors;
7272
import com.google.android.material.drawable.DrawableUtils;
73+
import com.google.android.material.focus.FocusRingDrawable;
7374
import com.google.android.material.internal.TextDrawableHelper;
7475
import com.google.android.material.internal.TextDrawableHelper.TextDrawableDelegate;
7576
import com.google.android.material.internal.ThemeEnforcement;
@@ -1924,13 +1925,15 @@ public void setCloseIcon(@Nullable Drawable closeIcon) {
19241925
}
19251926

19261927
private void updateFrameworkCloseIconRipple() {
1927-
closeIconRipple =
1928+
RippleDrawable rippleDrawable =
19281929
new RippleDrawable(
19291930
RippleUtils.sanitizeRippleDrawableColor(getRippleColor()),
19301931
closeIcon,
19311932
// A separate drawable with a solid background is needed for the mask because by
19321933
// default, the close icon has a transparent background.
19331934
closeIconRippleMask);
1935+
FocusRingDrawable.layer(context, rippleDrawable);
1936+
closeIconRipple = rippleDrawable;
19341937
}
19351938

19361939
@Nullable

0 commit comments

Comments
 (0)