Skip to content

Commit

Permalink
[MaterialButton] Align icon on top
Browse files Browse the repository at this point in the history
Resolves #1129

GIT_ORIGIN_REV_ID=8d663b1ca7f7bf62e1e5aa8cfc887a33bdbaf9a2
PiperOrigin-RevId: 314715132
  • Loading branch information
rewgoes authored and ymarian committed Jun 5, 2020
1 parent ef3f09d commit a284edd
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,23 @@
app:iconGravity="textStart"
app:iconPadding="0dp"/>

<TextView
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:text="@string/cat_top_icon_btn_text"
android:textSize="16sp"/>

<Button
style="@style/Widget.MaterialComponents.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/cat_button_top_icon_btn_text"
android:text="@string/cat_button_label_enabled"
app:icon="@drawable/ic_dialogs_24px"
app:iconGravity="textTop" />

<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/cat_button_enabled_switch"
android:layout_width="wrap_content"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
<string name="cat_filled_buttons_section_title">Filled buttons</string>
<string name="cat_outlined_btn_text">Outlined buttons</string>
<string name="cat_outlined_icon_btn_text">Icon only buttons</string>
<string name="cat_top_icon_btn_text" description="Section header label for buttons with a top icon">Top icon buttons</string>
<string name="cat_button_top_icon_btn_text" description="Content description for a button with a top icon">Top icon buttons</string>
<string name="cat_button_clicked">Button clicked</string>
<string name="cat_snackbar_action_button_text">Done</string>

Expand Down
179 changes: 130 additions & 49 deletions lib/java/com/google/android/material/button/MaterialButton.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuff.Mode;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
Expand Down Expand Up @@ -160,8 +161,31 @@ interface OnPressedChangeListener {
*/
public static final int ICON_GRAVITY_TEXT_END = 0x4;

/**
* Gravity used to position the icon at the top of the view.
*
* @see #setIconGravity(int)
* @see #getIconGravity()
*/
public static final int ICON_GRAVITY_TOP = 0x10;

/**
* Gravity used to position the icon in the center of the view at the top of the text
*
* @see #setIconGravity(int)
* @see #getIconGravity()
*/
public static final int ICON_GRAVITY_TEXT_TOP = 0x20;

/** Positions the icon can be set to. */
@IntDef({ICON_GRAVITY_START, ICON_GRAVITY_TEXT_START, ICON_GRAVITY_END, ICON_GRAVITY_TEXT_END})
@IntDef({
ICON_GRAVITY_START,
ICON_GRAVITY_TEXT_START,
ICON_GRAVITY_END,
ICON_GRAVITY_TEXT_END,
ICON_GRAVITY_TOP,
ICON_GRAVITY_TEXT_TOP
})
@Retention(RetentionPolicy.SOURCE)
public @interface IconGravity {}

Expand All @@ -180,6 +204,7 @@ interface OnPressedChangeListener {

@Px private int iconSize;
@Px private int iconLeft;
@Px private int iconTop;
@Px private int iconPadding;

private boolean checked = false;
Expand Down Expand Up @@ -225,7 +250,7 @@ public MaterialButton(@NonNull Context context, @Nullable AttributeSet attrs, in
attributes.recycle();

setCompoundDrawablePadding(iconPadding);
updateIcon(/*needsIconUpdate=*/icon != null);
updateIcon(/*needsIconReset=*/icon != null);
}

@NonNull
Expand Down Expand Up @@ -422,15 +447,15 @@ protected void onLayout(boolean changed, int left, int top, int right, int botto
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
updateIconPosition();
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
updateIconPosition(w, h);
}

@Override
protected void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
super.onTextChanged(charSequence, i, i1, i2);
updateIconPosition();
updateIconPosition(getMeasuredWidth(), getMeasuredHeight());
}

@Override
Expand All @@ -452,17 +477,64 @@ public void setElevation(float elevation) {
}
}

private void updateIconPosition() {
private void updateIconPosition(int buttonWidth, int buttonHeight) {
if (icon == null || getLayout() == null) {
return;
}

if (iconGravity == ICON_GRAVITY_START || iconGravity == ICON_GRAVITY_END) {
if (isIconStart() || isIconEnd()) {
iconTop = 0;
if (iconGravity == ICON_GRAVITY_START || iconGravity == ICON_GRAVITY_END) {
iconLeft = 0;
updateIcon(/* needsIconReset = */ false);
return;
}

int localIconSize = iconSize == 0 ? icon.getIntrinsicWidth() : iconSize;
int newIconLeft =
(buttonWidth
- getTextWidth()
- ViewCompat.getPaddingEnd(this)
- localIconSize
- iconPadding
- ViewCompat.getPaddingStart(this))
/ 2;

// Only flip the bound value if either isLayoutRTL() or iconGravity is textEnd, but not both
if (isLayoutRTL() != (iconGravity == ICON_GRAVITY_TEXT_END)) {
newIconLeft = -newIconLeft;
}

if (iconLeft != newIconLeft) {
iconLeft = newIconLeft;
updateIcon(/* needsIconReset = */ false);
}
} else if (isIconTop()) {
iconLeft = 0;
updateIcon(/* needsIconUpdate = */ false);
return;
if (iconGravity == ICON_GRAVITY_TOP) {
iconTop = 0;
updateIcon(/* needsIconReset = */ false);
return;
}

int localIconSize = iconSize == 0 ? icon.getIntrinsicHeight() : iconSize;
int newIconTop =
(buttonHeight
- getTextHeight()
- getPaddingTop()
- localIconSize
- iconPadding
- getPaddingBottom())
/ 2;

if (iconTop != newIconTop) {
iconTop = newIconTop;
updateIcon(/* needsIconReset = */ false);
}
}
}

private int getTextWidth() {
Paint textPaint = getPaint();
String buttonText = getText().toString();
if (getTransformationMethod() != null) {
Expand All @@ -471,28 +543,22 @@ private void updateIconPosition() {
buttonText = getTransformationMethod().getTransformation(buttonText, this).toString();
}

int textWidth =
Math.min((int) textPaint.measureText(buttonText), getLayout().getEllipsizedWidth());

int localIconSize = iconSize == 0 ? icon.getIntrinsicWidth() : iconSize;
int newIconLeft =
(getMeasuredWidth()
- textWidth
- ViewCompat.getPaddingEnd(this)
- localIconSize
- iconPadding
- ViewCompat.getPaddingStart(this))
/ 2;

// Only flip the bound value if either isLayoutRTL() or iconGravity is textEnd, but not both
if (isLayoutRTL() != (iconGravity == ICON_GRAVITY_TEXT_END)) {
newIconLeft = -newIconLeft;
}
return Math.min((int) textPaint.measureText(buttonText), getLayout().getEllipsizedWidth());
}

if (iconLeft != newIconLeft) {
iconLeft = newIconLeft;
updateIcon(/* needsIconUpdate = */ false);
private int getTextHeight() {
Paint textPaint = getPaint();
String buttonText = getText().toString();
if (getTransformationMethod() != null) {
// if text is transformed, add that transformation to to ensure correct calculation
// of icon padding.
buttonText = getTransformationMethod().getTransformation(buttonText, this).toString();
}

Rect bounds = new Rect();
textPaint.getTextBounds(buttonText, 0, buttonText.length(), bounds);

return Math.min(bounds.height(), getLayout().getHeight());
}

private boolean isLayoutRTL() {
Expand Down Expand Up @@ -550,7 +616,7 @@ public void setIconSize(@Px int iconSize) {

if (this.iconSize != iconSize) {
this.iconSize = iconSize;
updateIcon(/* needsIconUpdate = */ true);
updateIcon(/* needsIconReset = */ true);
}
}

Expand Down Expand Up @@ -578,7 +644,8 @@ public int getIconSize() {
public void setIcon(@Nullable Drawable icon) {
if (this.icon != icon) {
this.icon = icon;
updateIcon(/* needsIconUpdate = */ true);
updateIcon(/* needsIconReset = */ true);
updateIconPosition(getMeasuredWidth(), getMeasuredHeight());
}
}
/**
Expand Down Expand Up @@ -621,7 +688,7 @@ public Drawable getIcon() {
public void setIconTint(@Nullable ColorStateList iconTint) {
if (this.iconTint != iconTint) {
this.iconTint = iconTint;
updateIcon(/* needsIconUpdate = */ false);
updateIcon(/* needsIconReset = */ false);
}
}

Expand Down Expand Up @@ -659,7 +726,7 @@ public ColorStateList getIconTint() {
public void setIconTintMode(Mode iconTintMode) {
if (this.iconTintMode != iconTintMode) {
this.iconTintMode = iconTintMode;
updateIcon(/* needsIconUpdate = */ false);
updateIcon(/* needsIconReset = */ false);
}
}

Expand All @@ -676,9 +743,9 @@ public Mode getIconTintMode() {

/**
* Updates the icon, icon tint, and icon tint mode for this button.
* @param needsIconUpdate Whether to force the drawable to be set
* @param needsIconReset Whether to force the drawable to be set
*/
private void updateIcon(boolean needsIconUpdate) {
private void updateIcon(boolean needsIconReset) {
if (icon != null) {
icon = DrawableCompat.wrap(icon).mutate();
DrawableCompat.setTintList(icon, iconTint);
Expand All @@ -688,38 +755,52 @@ private void updateIcon(boolean needsIconUpdate) {

int width = iconSize != 0 ? iconSize : icon.getIntrinsicWidth();
int height = iconSize != 0 ? iconSize : icon.getIntrinsicHeight();
icon.setBounds(iconLeft, 0, iconLeft + width, height);
icon.setBounds(iconLeft, iconTop, iconLeft + width, iconTop + height);
}

// Reset icon drawable if needed
boolean isIconStart =
iconGravity == ICON_GRAVITY_START || iconGravity == ICON_GRAVITY_TEXT_START;
// Forced icon update
if (needsIconUpdate) {
resetIconDrawable(isIconStart);
if (needsIconReset) {
resetIconDrawable();
return;
}

// Otherwise only update if the icon or the position has changed
Drawable[] existingDrawables = TextViewCompat.getCompoundDrawablesRelative(this);
Drawable[] existingDrawables = TextViewCompat.getCompoundDrawablesRelative(this);
Drawable drawableStart = existingDrawables[0];
Drawable drawableTop = existingDrawables[1];
Drawable drawableEnd = existingDrawables[2];
boolean hasIconChanged =
(isIconStart && drawableStart != icon) || (!isIconStart && drawableEnd != icon);
(isIconStart() && drawableStart != icon)
|| (isIconEnd() && drawableEnd != icon)
|| (isIconTop() && drawableTop != icon);

if (hasIconChanged) {
resetIconDrawable(isIconStart);
resetIconDrawable();
}
}

private void resetIconDrawable(boolean isIconStart) {
if (isIconStart) {
private void resetIconDrawable() {
if (isIconStart()) {
TextViewCompat.setCompoundDrawablesRelative(this, icon, null, null, null);
} else {
} else if (isIconEnd()) {
TextViewCompat.setCompoundDrawablesRelative(this, null, null, icon, null);
} else if (isIconTop()) {
TextViewCompat.setCompoundDrawablesRelative(this, null, icon, null, null);
}
}

private boolean isIconStart() {
return iconGravity == ICON_GRAVITY_START || iconGravity == ICON_GRAVITY_TEXT_START;
}

private boolean isIconEnd() {
return iconGravity == ICON_GRAVITY_END || iconGravity == ICON_GRAVITY_TEXT_END;
}

private boolean isIconTop() {
return iconGravity == ICON_GRAVITY_TOP || iconGravity == ICON_GRAVITY_TEXT_TOP;
}

/**
* Sets the ripple color for this button.
*
Expand Down Expand Up @@ -909,7 +990,7 @@ public int getIconGravity() {
public void setIconGravity(@IconGravity int iconGravity) {
if (this.iconGravity != iconGravity) {
this.iconGravity = iconGravity;
updateIconPosition();
updateIconPosition(getMeasuredWidth(), getMeasuredHeight());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@
<!-- Push the icon to the end of the text keeping a distance equal to
{@link R.attr#iconPadding} from the text. -->
<flag name="textEnd" value="0x4"/>
<!-- Push the icon to the top of the button. -->
<flag name="top" value="0x10"/>
<!-- Push the icon to the top of the text keeping a distance equal to
{@link R.attr#iconPadding} from the text. -->
<flag name="textTop" value="0x20"/>
</attr>
<!-- Tint for icon drawable to display. -->
<attr name="iconTint" format="color"/>
Expand Down

0 comments on commit a284edd

Please sign in to comment.