62
62
import androidx .annotation .RestrictTo ;
63
63
import androidx .core .graphics .drawable .DrawableCompat ;
64
64
import androidx .customview .view .AbsSavedState ;
65
+ import androidx .dynamicanimation .animation .SpringForce ;
65
66
import com .google .android .material .internal .ThemeEnforcement ;
66
67
import com .google .android .material .internal .ViewUtils ;
67
68
import androidx .resourceinspection .annotation .Attribute ;
68
69
import com .google .android .material .resources .MaterialResources ;
69
70
import com .google .android .material .shape .MaterialShapeUtils ;
70
71
import com .google .android .material .shape .ShapeAppearanceModel ;
71
72
import com .google .android .material .shape .Shapeable ;
73
+ import com .google .android .material .shape .StateListShapeAppearanceModel ;
72
74
import java .lang .annotation .Retention ;
73
75
import java .lang .annotation .RetentionPolicy ;
74
76
import java .util .LinkedHashSet ;
@@ -188,12 +190,12 @@ interface OnPressedChangeListener {
188
190
189
191
/** Positions the icon can be set to. */
190
192
@ 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
197
199
})
198
200
@ Retention (RetentionPolicy .SOURCE )
199
201
public @interface IconGravity {}
@@ -202,8 +204,14 @@ interface OnPressedChangeListener {
202
204
203
205
private static final int DEF_STYLE_RES = R .style .Widget_MaterialComponents_Button ;
204
206
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
+
205
211
@ NonNull private final MaterialButtonHelper materialButtonHelper ;
206
- @ NonNull private final LinkedHashSet <OnCheckedChangeListener > onCheckedChangeListeners =
212
+
213
+ @ NonNull
214
+ private final LinkedHashSet <OnCheckedChangeListener > onCheckedChangeListeners =
207
215
new LinkedHashSet <>();
208
216
209
217
@ Nullable private OnPressedChangeListener onPressedChangeListenerInternal ;
@@ -250,17 +258,34 @@ public MaterialButton(@NonNull Context context, @Nullable AttributeSet attrs, in
250
258
iconGravity = attributes .getInteger (R .styleable .MaterialButton_iconGravity , ICON_GRAVITY_START );
251
259
252
260
iconSize = attributes .getDimensionPixelSize (R .styleable .MaterialButton_iconSize , 0 );
261
+ StateListShapeAppearanceModel stateListShapeAppearanceModel =
262
+ StateListShapeAppearanceModel .create (
263
+ context , attributes , R .styleable .MaterialButton_shapeAppearance );
253
264
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 ();
255
268
256
269
// Loads and sets background drawable attributes
257
270
materialButtonHelper = new MaterialButtonHelper (this , shapeAppearanceModel );
258
271
materialButtonHelper .loadFromAttributes (attributes );
259
272
273
+ if (stateListShapeAppearanceModel != null ) {
274
+ materialButtonHelper .setCornerSpringForce (createSpringForce ());
275
+ materialButtonHelper .setStateListShapeAppearanceModel (stateListShapeAppearanceModel );
276
+ }
277
+
260
278
attributes .recycle ();
261
279
262
280
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 );
264
289
}
265
290
266
291
@ NonNull
@@ -536,8 +561,8 @@ private Alignment getGravityTextAlignment() {
536
561
537
562
/**
538
563
* 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,
541
566
* especially when the text mixes different languages. However, this is probably the best we can
542
567
* do for now, unless we have a good way to detect the final text direction being used by
543
568
* TextView.
@@ -573,17 +598,18 @@ private void updateIconPosition(int buttonWidth, int buttonHeight) {
573
598
|| (iconGravity == ICON_GRAVITY_TEXT_START && textAlignment == Alignment .ALIGN_NORMAL )
574
599
|| (iconGravity == ICON_GRAVITY_TEXT_END && textAlignment == Alignment .ALIGN_OPPOSITE )) {
575
600
iconLeft = 0 ;
576
- updateIcon (/* needsIconReset = */ false );
601
+ updateIcon (/* needsIconReset= */ false );
577
602
return ;
578
603
}
579
604
580
605
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 ();
587
613
int newIconLeft =
588
614
textAlignment == Alignment .ALIGN_CENTER ? availableWidth / 2 : availableWidth ;
589
615
@@ -594,13 +620,13 @@ private void updateIconPosition(int buttonWidth, int buttonHeight) {
594
620
595
621
if (iconLeft != newIconLeft ) {
596
622
iconLeft = newIconLeft ;
597
- updateIcon (/* needsIconReset = */ false );
623
+ updateIcon (/* needsIconReset= */ false );
598
624
}
599
625
} else if (isIconTop ()) {
600
626
iconLeft = 0 ;
601
627
if (iconGravity == ICON_GRAVITY_TOP ) {
602
628
iconTop = 0 ;
603
- updateIcon (/* needsIconReset = */ false );
629
+ updateIcon (/* needsIconReset= */ false );
604
630
return ;
605
631
}
606
632
@@ -618,7 +644,7 @@ private void updateIconPosition(int buttonWidth, int buttonHeight) {
618
644
619
645
if (iconTop != newIconTop ) {
620
646
iconTop = newIconTop ;
621
- updateIcon (/* needsIconReset = */ false );
647
+ updateIcon (/* needsIconReset= */ false );
622
648
}
623
649
}
624
650
}
@@ -707,7 +733,7 @@ public void setIconSize(@Px int iconSize) {
707
733
708
734
if (this .iconSize != iconSize ) {
709
735
this .iconSize = iconSize ;
710
- updateIcon (/* needsIconReset = */ true );
736
+ updateIcon (/* needsIconReset= */ true );
711
737
}
712
738
}
713
739
@@ -735,10 +761,11 @@ public int getIconSize() {
735
761
public void setIcon (@ Nullable Drawable icon ) {
736
762
if (this .icon != icon ) {
737
763
this .icon = icon ;
738
- updateIcon (/* needsIconReset = */ true );
764
+ updateIcon (/* needsIconReset= */ true );
739
765
updateIconPosition (getMeasuredWidth (), getMeasuredHeight ());
740
766
}
741
767
}
768
+
742
769
/**
743
770
* Sets the icon drawable resource to show for this button. By default, this icon will be shown on
744
771
* the left side of the button.
@@ -779,7 +806,7 @@ public Drawable getIcon() {
779
806
public void setIconTint (@ Nullable ColorStateList iconTint ) {
780
807
if (this .iconTint != iconTint ) {
781
808
this .iconTint = iconTint ;
782
- updateIcon (/* needsIconReset = */ false );
809
+ updateIcon (/* needsIconReset= */ false );
783
810
}
784
811
}
785
812
@@ -817,7 +844,7 @@ public ColorStateList getIconTint() {
817
844
public void setIconTintMode (Mode iconTintMode ) {
818
845
if (this .iconTintMode != iconTintMode ) {
819
846
this .iconTintMode = iconTintMode ;
820
- updateIcon (/* needsIconReset = */ false );
847
+ updateIcon (/* needsIconReset= */ false );
821
848
}
822
849
}
823
850
@@ -834,6 +861,7 @@ public Mode getIconTintMode() {
834
861
835
862
/**
836
863
* Updates the icon, icon tint, and icon tint mode for this button.
864
+ *
837
865
* @param needsIconReset Whether to force the drawable to be set
838
866
*/
839
867
private void updateIcon (boolean needsIconReset ) {
@@ -1106,6 +1134,7 @@ public void setInsetBottom(@Dimension int insetBottom) {
1106
1134
public int getInsetBottom () {
1107
1135
return materialButtonHelper .getInsetBottom ();
1108
1136
}
1137
+
1109
1138
/**
1110
1139
* Sets the button top inset
1111
1140
*
@@ -1174,6 +1203,7 @@ public void clearOnCheckedChangeListeners() {
1174
1203
public void setChecked (boolean checked ) {
1175
1204
if (isCheckable () && isEnabled () && this .checked != checked ) {
1176
1205
this .checked = checked ;
1206
+
1177
1207
refreshDrawableState ();
1178
1208
1179
1209
// Report checked state change to the parent toggle group, if there is one
@@ -1256,7 +1286,8 @@ public void setCheckable(boolean checkable) {
1256
1286
}
1257
1287
1258
1288
/**
1259
- * {@inheritDoc}
1289
+ * Sets the {@link ShapeAppearanceModel} used for this {@link MaterialButton}'s original
1290
+ * drawables.
1260
1291
*
1261
1292
* @throws IllegalStateException if the MaterialButton's background has been overwritten.
1262
1293
*/
@@ -1272,7 +1303,8 @@ public void setShapeAppearanceModel(@NonNull ShapeAppearanceModel shapeAppearanc
1272
1303
}
1273
1304
1274
1305
/**
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.
1276
1308
*
1277
1309
* <p>This {@link ShapeAppearanceModel} can be modified to change the component's shape.
1278
1310
*
@@ -1290,6 +1322,72 @@ public ShapeAppearanceModel getShapeAppearanceModel() {
1290
1322
}
1291
1323
}
1292
1324
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
+
1293
1391
/**
1294
1392
* Register a callback to be invoked when the pressed state of this button changes. This callback
1295
1393
* is used for internal purpose only.
0 commit comments