Skip to content

Commit ef1e1cd

Browse files
pekingmedrchen
authored andcommitted
[Button] Added corner morph.
PiperOrigin-RevId: 657325202
1 parent b184df6 commit ef1e1cd

File tree

8 files changed

+567
-228
lines changed

8 files changed

+567
-228
lines changed

docs/components/Button.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -751,8 +751,14 @@ Element | Attribute | Related method
751751
**Single selection** | `app:singleSelection` | `setSingleSelection`<br/>`isSingleSelection` | `false`
752752
**Selection required** | `app:selectionRequired` | `setSelectionRequired`<br/>`isSelectionRequired` | `false`
753753
**Enable the group and all children** | `android:enabled` | `setEnabled`<br/>`isEnabled` | `true`
754-
**Size of inner corners** | `app:innerCornerSize` | `setInnerCornerSize`<br/>`getInnerCornerSize` | `0dp`
755-
**Spacing between buttons** | `android:spacing` | `setSpacing`<br/>`getSpacing` | `0dp`
754+
755+
#### Container attributes
756+
757+
Element | Attribute | Related method(s) | Default value
758+
------------------------------- | --------------------- | --------------------------------------------- | -------------
759+
**Size of inner corners** | `app:innerCornerSize` | `setInnerCornerSize`<br/>`getInnerCornerSize` | `0dp`
760+
**Spacing between buttons** | `android:spacing` | `setSpacing`<br/>`getSpacing` | `0dp`
761+
**Group shape (outer corners)** | `app:shapeAppearance` | `setShapeAppearance`</br>`getShapeAppearance` | `none`
756762

757763
#### Styles
758764

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

Lines changed: 125 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,15 @@
6262
import androidx.annotation.RestrictTo;
6363
import androidx.core.graphics.drawable.DrawableCompat;
6464
import androidx.customview.view.AbsSavedState;
65+
import androidx.dynamicanimation.animation.SpringForce;
6566
import com.google.android.material.internal.ThemeEnforcement;
6667
import com.google.android.material.internal.ViewUtils;
6768
import androidx.resourceinspection.annotation.Attribute;
6869
import com.google.android.material.resources.MaterialResources;
6970
import com.google.android.material.shape.MaterialShapeUtils;
7071
import com.google.android.material.shape.ShapeAppearanceModel;
7172
import com.google.android.material.shape.Shapeable;
73+
import com.google.android.material.shape.StateListShapeAppearanceModel;
7274
import java.lang.annotation.Retention;
7375
import java.lang.annotation.RetentionPolicy;
7476
import 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

Comments
 (0)