6262import androidx .annotation .RestrictTo ;
6363import androidx .core .graphics .drawable .DrawableCompat ;
6464import androidx .customview .view .AbsSavedState ;
65+ import androidx .dynamicanimation .animation .SpringForce ;
6566import com .google .android .material .internal .ThemeEnforcement ;
6667import com .google .android .material .internal .ViewUtils ;
6768import androidx .resourceinspection .annotation .Attribute ;
6869import com .google .android .material .resources .MaterialResources ;
6970import com .google .android .material .shape .MaterialShapeUtils ;
7071import com .google .android .material .shape .ShapeAppearanceModel ;
7172import com .google .android .material .shape .Shapeable ;
73+ import com .google .android .material .shape .StateListShapeAppearanceModel ;
7274import java .lang .annotation .Retention ;
7375import java .lang .annotation .RetentionPolicy ;
7476import java .util .LinkedHashSet ;
@@ -188,12 +190,12 @@ interface OnPressedChangeListener {
188190
189191 /** Positions the icon can be set to. */
190192 @ IntDef ({
191- ICON_GRAVITY_START ,
192- ICON_GRAVITY_TEXT_START ,
193- ICON_GRAVITY_END ,
194- ICON_GRAVITY_TEXT_END ,
195- ICON_GRAVITY_TOP ,
196- ICON_GRAVITY_TEXT_TOP
193+ ICON_GRAVITY_START ,
194+ ICON_GRAVITY_TEXT_START ,
195+ ICON_GRAVITY_END ,
196+ ICON_GRAVITY_TEXT_END ,
197+ ICON_GRAVITY_TOP ,
198+ ICON_GRAVITY_TEXT_TOP
197199 })
198200 @ Retention (RetentionPolicy .SOURCE )
199201 public @interface IconGravity {}
@@ -202,8 +204,14 @@ interface OnPressedChangeListener {
202204
203205 private static final int DEF_STYLE_RES = R .style .Widget_MaterialComponents_Button ;
204206
207+ private static final float TOGGLE_BUTTON_SPRING_DAMPING = 0.8f ;
208+ private static final float DEFAULT_BUTTON_CORNER_SPRING_DAMPING = 0.5f ;
209+ private static final float DEFAULT_BUTTON_SPRING_STIFFNESS = 800 ;
210+
205211 @ NonNull private final MaterialButtonHelper materialButtonHelper ;
206- @ NonNull private final LinkedHashSet <OnCheckedChangeListener > onCheckedChangeListeners =
212+
213+ @ NonNull
214+ private final LinkedHashSet <OnCheckedChangeListener > onCheckedChangeListeners =
207215 new LinkedHashSet <>();
208216
209217 @ Nullable private OnPressedChangeListener onPressedChangeListenerInternal ;
@@ -250,17 +258,34 @@ public MaterialButton(@NonNull Context context, @Nullable AttributeSet attrs, in
250258 iconGravity = attributes .getInteger (R .styleable .MaterialButton_iconGravity , ICON_GRAVITY_START );
251259
252260 iconSize = attributes .getDimensionPixelSize (R .styleable .MaterialButton_iconSize , 0 );
261+ StateListShapeAppearanceModel stateListShapeAppearanceModel =
262+ StateListShapeAppearanceModel .create (
263+ context , attributes , R .styleable .MaterialButton_shapeAppearance );
253264 ShapeAppearanceModel shapeAppearanceModel =
254- ShapeAppearanceModel .builder (context , attrs , defStyleAttr , DEF_STYLE_RES ).build ();
265+ stateListShapeAppearanceModel != null
266+ ? stateListShapeAppearanceModel .getDefaultShape (/* withCornerSizeOverrides= */ true )
267+ : ShapeAppearanceModel .builder (context , attrs , defStyleAttr , DEF_STYLE_RES ).build ();
255268
256269 // Loads and sets background drawable attributes
257270 materialButtonHelper = new MaterialButtonHelper (this , shapeAppearanceModel );
258271 materialButtonHelper .loadFromAttributes (attributes );
259272
273+ if (stateListShapeAppearanceModel != null ) {
274+ materialButtonHelper .setCornerSpringForce (createSpringForce ());
275+ materialButtonHelper .setStateListShapeAppearanceModel (stateListShapeAppearanceModel );
276+ }
277+
260278 attributes .recycle ();
261279
262280 setCompoundDrawablePadding (iconPadding );
263- updateIcon (/*needsIconReset=*/ icon != null );
281+ updateIcon (/* needsIconReset= */ icon != null );
282+ }
283+
284+ private SpringForce createSpringForce () {
285+ return new SpringForce ()
286+ .setDampingRatio (
287+ isCheckable () ? TOGGLE_BUTTON_SPRING_DAMPING : DEFAULT_BUTTON_CORNER_SPRING_DAMPING )
288+ .setStiffness (DEFAULT_BUTTON_SPRING_STIFFNESS );
264289 }
265290
266291 @ NonNull
@@ -536,8 +561,8 @@ private Alignment getGravityTextAlignment() {
536561
537562 /**
538563 * This method and {@link #getGravityTextAlignment()} is modified from Android framework
539- * TextView's private method getLayoutAlignment(). Please note that the logic here assumes
540- * the actual text direction is the same as the layout direction, which is not always the case,
564+ * TextView's private method getLayoutAlignment(). Please note that the logic here assumes the
565+ * actual text direction is the same as the layout direction, which is not always the case,
541566 * especially when the text mixes different languages. However, this is probably the best we can
542567 * do for now, unless we have a good way to detect the final text direction being used by
543568 * TextView.
@@ -573,17 +598,18 @@ private void updateIconPosition(int buttonWidth, int buttonHeight) {
573598 || (iconGravity == ICON_GRAVITY_TEXT_START && textAlignment == Alignment .ALIGN_NORMAL )
574599 || (iconGravity == ICON_GRAVITY_TEXT_END && textAlignment == Alignment .ALIGN_OPPOSITE )) {
575600 iconLeft = 0 ;
576- updateIcon (/* needsIconReset = */ false );
601+ updateIcon (/* needsIconReset= */ false );
577602 return ;
578603 }
579604
580605 int localIconSize = iconSize == 0 ? icon .getIntrinsicWidth () : iconSize ;
581- int availableWidth = buttonWidth
582- - getTextLayoutWidth ()
583- - getPaddingEnd ()
584- - localIconSize
585- - iconPadding
586- - getPaddingStart ();
606+ int availableWidth =
607+ buttonWidth
608+ - getTextLayoutWidth ()
609+ - getPaddingEnd ()
610+ - localIconSize
611+ - iconPadding
612+ - getPaddingStart ();
587613 int newIconLeft =
588614 textAlignment == Alignment .ALIGN_CENTER ? availableWidth / 2 : availableWidth ;
589615
@@ -594,13 +620,13 @@ private void updateIconPosition(int buttonWidth, int buttonHeight) {
594620
595621 if (iconLeft != newIconLeft ) {
596622 iconLeft = newIconLeft ;
597- updateIcon (/* needsIconReset = */ false );
623+ updateIcon (/* needsIconReset= */ false );
598624 }
599625 } else if (isIconTop ()) {
600626 iconLeft = 0 ;
601627 if (iconGravity == ICON_GRAVITY_TOP ) {
602628 iconTop = 0 ;
603- updateIcon (/* needsIconReset = */ false );
629+ updateIcon (/* needsIconReset= */ false );
604630 return ;
605631 }
606632
@@ -618,7 +644,7 @@ private void updateIconPosition(int buttonWidth, int buttonHeight) {
618644
619645 if (iconTop != newIconTop ) {
620646 iconTop = newIconTop ;
621- updateIcon (/* needsIconReset = */ false );
647+ updateIcon (/* needsIconReset= */ false );
622648 }
623649 }
624650 }
@@ -707,7 +733,7 @@ public void setIconSize(@Px int iconSize) {
707733
708734 if (this .iconSize != iconSize ) {
709735 this .iconSize = iconSize ;
710- updateIcon (/* needsIconReset = */ true );
736+ updateIcon (/* needsIconReset= */ true );
711737 }
712738 }
713739
@@ -735,10 +761,11 @@ public int getIconSize() {
735761 public void setIcon (@ Nullable Drawable icon ) {
736762 if (this .icon != icon ) {
737763 this .icon = icon ;
738- updateIcon (/* needsIconReset = */ true );
764+ updateIcon (/* needsIconReset= */ true );
739765 updateIconPosition (getMeasuredWidth (), getMeasuredHeight ());
740766 }
741767 }
768+
742769 /**
743770 * Sets the icon drawable resource to show for this button. By default, this icon will be shown on
744771 * the left side of the button.
@@ -779,7 +806,7 @@ public Drawable getIcon() {
779806 public void setIconTint (@ Nullable ColorStateList iconTint ) {
780807 if (this .iconTint != iconTint ) {
781808 this .iconTint = iconTint ;
782- updateIcon (/* needsIconReset = */ false );
809+ updateIcon (/* needsIconReset= */ false );
783810 }
784811 }
785812
@@ -817,7 +844,7 @@ public ColorStateList getIconTint() {
817844 public void setIconTintMode (Mode iconTintMode ) {
818845 if (this .iconTintMode != iconTintMode ) {
819846 this .iconTintMode = iconTintMode ;
820- updateIcon (/* needsIconReset = */ false );
847+ updateIcon (/* needsIconReset= */ false );
821848 }
822849 }
823850
@@ -834,6 +861,7 @@ public Mode getIconTintMode() {
834861
835862 /**
836863 * Updates the icon, icon tint, and icon tint mode for this button.
864+ *
837865 * @param needsIconReset Whether to force the drawable to be set
838866 */
839867 private void updateIcon (boolean needsIconReset ) {
@@ -1106,6 +1134,7 @@ public void setInsetBottom(@Dimension int insetBottom) {
11061134 public int getInsetBottom () {
11071135 return materialButtonHelper .getInsetBottom ();
11081136 }
1137+
11091138 /**
11101139 * Sets the button top inset
11111140 *
@@ -1174,6 +1203,7 @@ public void clearOnCheckedChangeListeners() {
11741203 public void setChecked (boolean checked ) {
11751204 if (isCheckable () && isEnabled () && this .checked != checked ) {
11761205 this .checked = checked ;
1206+
11771207 refreshDrawableState ();
11781208
11791209 // Report checked state change to the parent toggle group, if there is one
@@ -1256,7 +1286,8 @@ public void setCheckable(boolean checkable) {
12561286 }
12571287
12581288 /**
1259- * {@inheritDoc}
1289+ * Sets the {@link ShapeAppearanceModel} used for this {@link MaterialButton}'s original
1290+ * drawables.
12601291 *
12611292 * @throws IllegalStateException if the MaterialButton's background has been overwritten.
12621293 */
@@ -1272,7 +1303,8 @@ public void setShapeAppearanceModel(@NonNull ShapeAppearanceModel shapeAppearanc
12721303 }
12731304
12741305 /**
1275- * Returns the {@link ShapeAppearanceModel} used for this MaterialButton's shape definition.
1306+ * Returns the {@link ShapeAppearanceModel} used for this {@link MaterialButton}'s original
1307+ * drawables.
12761308 *
12771309 * <p>This {@link ShapeAppearanceModel} can be modified to change the component's shape.
12781310 *
@@ -1290,6 +1322,72 @@ public ShapeAppearanceModel getShapeAppearanceModel() {
12901322 }
12911323 }
12921324
1325+ /**
1326+ * Sets the {@link StateListShapeAppearanceModel} used for this {@link MaterialButton}'s original
1327+ * drawables.
1328+ *
1329+ * @throws IllegalStateException if the MaterialButton's background has been overwritten.
1330+ * @hide
1331+ */
1332+ @ RestrictTo (LIBRARY_GROUP )
1333+ public void setStateListShapeAppearanceModel (
1334+ @ NonNull StateListShapeAppearanceModel stateListShapeAppearanceModel ) {
1335+ if (isUsingOriginalBackground ()) {
1336+ if (materialButtonHelper .getCornerSpringForce () == null
1337+ && stateListShapeAppearanceModel .isStateful ()) {
1338+ materialButtonHelper .setCornerSpringForce (createSpringForce ());
1339+ }
1340+ materialButtonHelper .setStateListShapeAppearanceModel (stateListShapeAppearanceModel );
1341+ } else {
1342+ throw new IllegalStateException (
1343+ "Attempted to set StateListShapeAppearanceModel on a MaterialButton which has an"
1344+ + " overwritten background." );
1345+ }
1346+ }
1347+
1348+ /**
1349+ * Returns the {@link StateListShapeAppearanceModel} used for this {@link MaterialButton}'s
1350+ * original drawables.
1351+ *
1352+ * <p>This {@link StateListShapeAppearanceModel} can be modified to change the component's shape.
1353+ *
1354+ * @throws IllegalStateException if the MaterialButton's background has been overwritten.
1355+ * @hide
1356+ */
1357+ @ Nullable
1358+ @ RestrictTo (LIBRARY_GROUP )
1359+ public StateListShapeAppearanceModel getStateListShapeAppearanceModel () {
1360+ if (isUsingOriginalBackground ()) {
1361+ return materialButtonHelper .getStateListShapeAppearanceModel ();
1362+ } else {
1363+ throw new IllegalStateException (
1364+ "Attempted to get StateListShapeAppearanceModel from a MaterialButton which has an"
1365+ + " overwritten background." );
1366+ }
1367+ }
1368+
1369+ /**
1370+ * Sets the corner spring force for this {@link MaterialButton}.
1371+ *
1372+ * @param springForce The new {@link SpringForce} object.
1373+ * @hide
1374+ */
1375+ @ RestrictTo (LIBRARY_GROUP )
1376+ public void setCornerSpringForce (@ NonNull SpringForce springForce ) {
1377+ materialButtonHelper .setCornerSpringForce (springForce );
1378+ }
1379+
1380+ /**
1381+ * Returns the corner spring force for this {@link MaterialButton}.
1382+ *
1383+ * @hide
1384+ */
1385+ @ Nullable
1386+ @ RestrictTo (LIBRARY_GROUP )
1387+ public SpringForce getCornerSpringForce () {
1388+ return materialButtonHelper .getCornerSpringForce ();
1389+ }
1390+
12931391 /**
12941392 * Register a callback to be invoked when the pressed state of this button changes. This callback
12951393 * is used for internal purpose only.
0 commit comments