Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 137 additions & 2 deletions lib/java/com/google/android/material/button/MaterialButton.java
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,11 @@ interface OnPressedChangeListener {

private int orientation = UNSET;
private float originalWidth = UNSET;
private float originalHeight = UNSET;
@Px private int originalPaddingStart = UNSET;
@Px private int originalPaddingTop = UNSET;
@Px private int originalPaddingEnd = UNSET;
@Px private int originalPaddingBottom = UNSET;

@Nullable private LayoutParams originalLayoutParams;

Expand All @@ -258,11 +261,16 @@ interface OnPressedChangeListener {

// Fields for size morphing.
@Px int allowedWidthDecrease = UNSET;
@Px int allowedHeightDecrease = UNSET;
@Nullable StateListSizeChange sizeChange;
@Px int widthChangeMax;
@Px int heightChangeMax;
private float displayedWidthIncrease;
private float displayedWidthDecrease;
private float displayedHeightIncrease;
private float displayedHeightDecrease;
@Nullable private SpringAnimation widthIncreaseSpringAnimation;
@Nullable private SpringAnimation heightIncreaseSpringAnimation;

public MaterialButton(@NonNull Context context) {
this(context, null /* attrs */);
Expand Down Expand Up @@ -324,11 +332,16 @@ public MaterialButton(@NonNull Context context, @Nullable AttributeSet attrs, in
updateIcon(/* needsIconReset= */ icon != null);
}

private void initializeSizeAnimation() {
private void initializeWidthAnimation() {
widthIncreaseSpringAnimation = new SpringAnimation(this, WIDTH_INCREASE);
widthIncreaseSpringAnimation.setSpring(createSpringForce());
}

private void initializeHeightAnimation() {
heightIncreaseSpringAnimation = new SpringAnimation(this, HEIGHT_INCREASE);
heightIncreaseSpringAnimation.setSpring(createSpringForce());
}

private SpringForce createSpringForce() {
return MotionUtils.resolveThemeSpringForce(
getContext(),
Expand Down Expand Up @@ -544,6 +557,7 @@ protected void onLayout(boolean changed, int left, int top, int right, int botto
if (orientation != curOrientation) {
orientation = curOrientation;
originalWidth = UNSET;
originalHeight = UNSET;
}
if (originalWidth == UNSET) {
originalWidth = getMeasuredWidth();
Expand All @@ -560,6 +574,21 @@ && getParent() instanceof MaterialButtonGroup
setLayoutParams(newLayoutParams);
}
}
if (originalHeight == UNSET) {
originalHeight = getMeasuredHeight();
// The height morph leverage the height of the layout params. However, it's not available if
// layout_weight is used. We need to hardcode the height here. The original layout params will
// be preserved for the correctness of distribution when buttons are added or removed into the
// group programmatically.
if (originalLayoutParams == null
&& getParent() instanceof MaterialButtonGroup
&& ((MaterialButtonGroup) getParent()).getButtonSizeChange() != null) {
originalLayoutParams = (LayoutParams) getLayoutParams();
LayoutParams newLayoutParams = new LayoutParams(originalLayoutParams);
newLayoutParams.height = (int) originalHeight;
setLayoutParams(newLayoutParams);
}
}

if (allowedWidthDecrease == UNSET) {
int localIconSizeAndPadding =
Expand All @@ -568,13 +597,26 @@ && getParent() instanceof MaterialButtonGroup
: getIconPadding() + (iconSize == 0 ? icon.getIntrinsicWidth() : iconSize);
allowedWidthDecrease = getMeasuredWidth() - getTextLayoutWidth() - localIconSizeAndPadding;
}
if (allowedHeightDecrease == UNSET) {
int localIconSizeAndPadding =
icon == null
? 0
: getIconPadding() + (iconSize == 0 ? icon.getIntrinsicHeight() : iconSize);
allowedHeightDecrease = getMeasuredHeight() - getTextLayoutHeight() - localIconSizeAndPadding;
}

if (originalPaddingStart == UNSET) {
originalPaddingStart = getPaddingStart();
}
if (originalPaddingTop == UNSET) {
originalPaddingTop = getPaddingTop();
}
if (originalPaddingEnd == UNSET) {
originalPaddingEnd = getPaddingEnd();
}
if (originalPaddingBottom == UNSET) {
originalPaddingBottom = getPaddingBottom();
}
isInHorizontalButtonGroup = isInHorizontalButtonGroup();
}

Expand All @@ -583,6 +625,7 @@ void recoverOriginalLayoutParams() {
setLayoutParams(originalLayoutParams);
originalLayoutParams = null;
originalWidth = UNSET;
originalHeight = UNSET;
}
}

Expand All @@ -592,6 +635,12 @@ public void setWidth(@Px int pixels) {
super.setWidth(pixels);
}

@Override
public void setHeight(@Px int pixels) {
originalHeight = UNSET;
super.setHeight(pixels);
}

@Override
protected void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
super.onTextChanged(charSequence, i, i1, i2);
Expand Down Expand Up @@ -756,6 +805,16 @@ private int getTextLayoutWidth() {
return (int) ceil(maxWidth);
}

private int getTextLayoutHeight() {
float maxHeight = 0;
int lineCount = getLineCount();
for (int line = 0; line < lineCount; line++) {
int height = getLayout().getLineBottom(line) - getLayout().getLineTop(line);
maxHeight = max(maxHeight, height);
}
return (int) ceil(maxHeight);
}

private int getTextHeight() {
if (getLineCount() > 1) {
// If it's multi-line, return the internal text layout's height.
Expand Down Expand Up @@ -1520,9 +1579,14 @@ private void maybeAnimateSize(boolean skipAnimation) {
if (sizeChange == null) {
return;
}

if (widthIncreaseSpringAnimation == null) {
initializeSizeAnimation();
initializeWidthAnimation();
}
if (heightIncreaseSpringAnimation == null) {
initializeHeightAnimation();
}

if (isInHorizontalButtonGroup) {
// Animate width.
int widthChange =
Expand All @@ -1536,6 +1600,19 @@ private void maybeAnimateSize(boolean skipAnimation) {
if (skipAnimation) {
widthIncreaseSpringAnimation.skipToEnd();
}
} else {
// Animate height.
int heightChange =
min(
heightChangeMax,
sizeChange
.getSizeChangeForState(getDrawableState())
.heightChange
.getChange(getHeight()));
heightIncreaseSpringAnimation.animateToFinalPosition(heightChange);
if (skipAnimation) {
heightIncreaseSpringAnimation.skipToEnd();
}
}
}

Expand Down Expand Up @@ -1586,6 +1663,41 @@ void setDisplayedWidthDecrease(int widthDecrease) {
invalidate();
}

void setHeightChangeMax(@Px int heightChangeMax) {
if (this.heightChangeMax != heightChangeMax) {
this.heightChangeMax = heightChangeMax;
maybeAnimateSize(/* skipAnimation= */ true);
}
}

@Px
int getAllowedHeightDecrease() {
return allowedHeightDecrease;
}

private float getDisplayedHeightIncrease() {
return displayedHeightIncrease;
}

private void setDisplayedHeightIncrease(float heightIncrease) {
if (displayedHeightIncrease != heightIncrease) {
displayedHeightIncrease = heightIncrease;
updatePaddingsAndSizeForHeightAnimation();
invalidate();
// Report width changed to the parent group.
if (getParent() instanceof MaterialButtonGroup) {
((MaterialButtonGroup) getParent())
.onButtonHeightChanged(this, (int) displayedHeightIncrease);
}
}
}

void setDisplayedHeightDecrease(int heightDecrease) {
displayedHeightDecrease = min(heightDecrease, allowedHeightDecrease);
updatePaddingsAndSizeForHeightAnimation();
invalidate();
}

/**
* Sets whether to enable the optical center feature.
*
Expand Down Expand Up @@ -1639,6 +1751,17 @@ private void updatePaddingsAndSizeForWidthAnimation() {
getPaddingBottom());
}

private void updatePaddingsAndSizeForHeightAnimation() {
int heightChange = (int) (displayedHeightIncrease - displayedHeightDecrease);
int paddingTopChange = heightChange / 2;
getLayoutParams().height = (int) (originalHeight + heightChange);
setPaddingRelative(
originalPaddingStart,
originalPaddingTop + paddingTopChange,
originalPaddingEnd,
originalPaddingBottom + heightChange - paddingTopChange);
}

private int getOpticalCenterShift() {
if (opticalCenterEnabled && isInHorizontalButtonGroup) {
MaterialShapeDrawable materialShapeDrawable = materialButtonHelper.getMaterialShapeDrawable();
Expand All @@ -1663,6 +1786,18 @@ public void setValue(MaterialButton button, float value) {
button.setDisplayedWidthIncrease(value);
}
};
private static final FloatPropertyCompat<MaterialButton> HEIGHT_INCREASE =
new FloatPropertyCompat<MaterialButton>("heightIncrease") {
@Override
public float getValue(MaterialButton button) {
return button.getDisplayedHeightIncrease();
}

@Override
public void setValue(MaterialButton button, float value) {
button.setDisplayedHeightIncrease(value);
}
};

static class SavedState extends AbsSavedState {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
import com.google.android.material.resources.MaterialAttributes;
import com.google.android.material.shape.AbsoluteCornerSize;
import com.google.android.material.shape.CornerSize;
import com.google.android.material.shape.RelativeCornerSize;
import com.google.android.material.shape.ShapeAppearance;
import com.google.android.material.shape.ShapeAppearanceModel;
import com.google.android.material.shape.StateListCornerSize;
Expand Down Expand Up @@ -720,6 +721,30 @@ void onButtonWidthChanged(@NonNull MaterialButton button, int increaseSize) {
}
}

void onButtonHeightChanged(@NonNull MaterialButton button, int increaseSize) {
int buttonIndex = indexOfChild(button);
if (buttonIndex < 0) {
return;
}
MaterialButton prevVisibleButton = getPrevVisibleChildButton(buttonIndex);
MaterialButton nextVisibleButton = getNextVisibleChildButton(buttonIndex);
if (prevVisibleButton == null && nextVisibleButton == null) {
return;
}
if (prevVisibleButton == null) {
nextVisibleButton.setDisplayedHeightDecrease(increaseSize);
}
if (nextVisibleButton == null) {
prevVisibleButton.setDisplayedHeightDecrease(increaseSize);
}
if (prevVisibleButton != null && nextVisibleButton != null) {
// If there are two neighbors, each neighbor will absorb half of the expanded amount.
prevVisibleButton.setDisplayedHeightDecrease(increaseSize / 2);
// We want to avoid one pixel missing due to the casting, when increaseSize is odd.
nextVisibleButton.setDisplayedHeightDecrease((increaseSize + 1) / 2);
}
}

/**
* Adjusts the max amount of size to expand for each child button. So that it won't squeeze its
* neighbors too much to cause text truncation; and the expansion amount per edge is same for all
Expand All @@ -732,36 +757,51 @@ private void adjustChildSizeChange() {
return;
}
int widthIncreaseOnSingleEdge = Integer.MAX_VALUE;
int heightIncreaseOnSingleEdge = Integer.MAX_VALUE;
for (int i = firstVisibleChildIndex; i <= lastVisibleChildIndex; i++) {
if (!isChildVisible(i)) {
continue;
}
// Calculates the allowed width increase for each child button with consideration of the max
// allowed width decrease of its neighbors.
// Calculates the allowed width and height increase for each child button with consideration
// of the max allowed width and height decrease of its neighbors.
int widthIncrease = getButtonAllowedWidthIncrease(i);
int heightIncrease = getButtonAllowedHeightIncrease(i);

// If the button expands on both edges, the width increase on each edge should be half of
// the total width increase. Calculates the minimum width increase on each edge, so that all
// buttons won't squeeze their neighbors too much.
// the total width increase. Calculates the minimum width and height increase on each edge, so
// that all buttons won't squeeze their neighbors too much.
widthIncreaseOnSingleEdge =
min(
widthIncreaseOnSingleEdge,
i != firstVisibleChildIndex && i != lastVisibleChildIndex
? widthIncrease / 2
: widthIncrease);
heightIncreaseOnSingleEdge =
min(
heightIncreaseOnSingleEdge,
i != firstVisibleChildIndex && i != lastVisibleChildIndex
? heightIncrease / 2
: heightIncrease);
}
for (int i = firstVisibleChildIndex; i <= lastVisibleChildIndex; i++) {
if (!isChildVisible(i)) {
continue;
}
getChildButton(i).setSizeChange(buttonSizeChange);
// If the button expands on both edges, the total width increase should be double of the
// width increase on each edge.
// If the button expands on both horizontal edges, the total width increase should be double
// of the width increase on each edge.
getChildButton(i)
.setWidthChangeMax(
i == firstVisibleChildIndex || i == lastVisibleChildIndex
? widthIncreaseOnSingleEdge
: widthIncreaseOnSingleEdge * 2);
// If the button expands on both vertical edges, the total height increase should be double of
// the height increase on each edge.
getChildButton(i)
.setHeightChangeMax(
i == firstVisibleChildIndex || i == lastVisibleChildIndex
? heightIncreaseOnSingleEdge
: heightIncreaseOnSingleEdge * 2);
}
}

Expand All @@ -787,6 +827,28 @@ private int getButtonAllowedWidthIncrease(int index) {
return min(widthIncrease, prevButtonAllowedWidthDecrease + nextButtonAllowedWidthDecrease);
}

/**
* Returns the allowed height increase for a child button.
*
* <p>The allowed height increase is the smaller amount of the max height increase of the button
* in all states and the total allowed height decrease of its neighbors.
*/
private int getButtonAllowedHeightIncrease(int index) {
if (!isChildVisible(index) || buttonSizeChange == null) {
return 0;
}
MaterialButton currentButton = getChildButton(index);
int heightIncrease = max(0, buttonSizeChange.getMaxHeightChange(currentButton.getHeight()));
// Checking neighbors' allowed height decrease.
MaterialButton prevVisibleButton = getPrevVisibleChildButton(index);
int prevButtonAllowedHeightDecrease =
prevVisibleButton == null ? 0 : prevVisibleButton.getAllowedHeightDecrease();
MaterialButton nextVisibleButton = getNextVisibleChildButton(index);
int nextButtonAllowedHeightDecrease =
nextVisibleButton == null ? 0 : nextVisibleButton.getAllowedHeightDecrease();
return min(heightIncrease, prevButtonAllowedHeightDecrease + nextButtonAllowedHeightDecrease);
}

// ================ Getters and setters ===================

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:state_pressed="true" app:widthChange="15%"/>
<item app:widthChange="0%"/>
<item android:state_pressed="true" app:widthChange="15%" app:heightChange="15%"/>
<item app:widthChange="0%" app:heightChange="0%"/>
</selector>
Loading