mirror of
https://github.com/material-components/material-components-android.git
synced 2026-01-16 18:01:42 +08:00
389 lines
15 KiB
Java
389 lines
15 KiB
Java
/*
|
|
* Copyright (C) 2017 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.google.android.material.button;
|
|
|
|
import com.google.android.material.R;
|
|
|
|
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
|
|
|
|
import android.annotation.TargetApi;
|
|
import android.content.res.ColorStateList;
|
|
import android.content.res.TypedArray;
|
|
import android.graphics.Canvas;
|
|
import android.graphics.Color;
|
|
import android.graphics.Paint;
|
|
import android.graphics.Paint.Style;
|
|
import android.graphics.PorterDuff.Mode;
|
|
import android.graphics.Rect;
|
|
import android.graphics.RectF;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.graphics.drawable.GradientDrawable;
|
|
import android.graphics.drawable.InsetDrawable;
|
|
import android.graphics.drawable.LayerDrawable;
|
|
import android.graphics.drawable.RippleDrawable;
|
|
import android.os.Build.VERSION;
|
|
import android.os.Build.VERSION_CODES;
|
|
import android.support.annotation.Nullable;
|
|
import android.support.annotation.RestrictTo;
|
|
import com.google.android.material.internal.ViewUtils;
|
|
import com.google.android.material.resources.MaterialResources;
|
|
import com.google.android.material.ripple.RippleUtils;
|
|
import android.support.v4.graphics.drawable.DrawableCompat;
|
|
|
|
/** @hide */
|
|
@RestrictTo(LIBRARY_GROUP)
|
|
class MaterialButtonHelper {
|
|
|
|
// This is a hacky workaround. Currently on certain devices/versions,
|
|
// LayerDrawable will draw a black background underneath any layer with a non-opaque color,
|
|
// unless we set the shape to be something that's not a perfect rectangle.
|
|
private static final float CORNER_RADIUS_ADJUSTMENT = 0.00001F;
|
|
private static final int DEFAULT_BACKGROUND_COLOR = Color.WHITE;
|
|
private static final boolean IS_LOLLIPOP = VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP;
|
|
|
|
private final MaterialButton materialButton;
|
|
|
|
private int insetLeft;
|
|
private int insetRight;
|
|
private int insetTop;
|
|
private int insetBottom;
|
|
|
|
private int cornerRadius;
|
|
private int strokeWidth;
|
|
|
|
@Nullable private Mode backgroundTintMode;
|
|
@Nullable private ColorStateList backgroundTint;
|
|
@Nullable private ColorStateList strokeColor;
|
|
@Nullable private ColorStateList rippleColor;
|
|
|
|
private final Paint buttonStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
private final Rect bounds = new Rect();
|
|
private final RectF rectF = new RectF();
|
|
|
|
@Nullable private GradientDrawable colorableBackgroundDrawableCompat;
|
|
@Nullable private Drawable tintableBackgroundDrawableCompat;
|
|
@Nullable private GradientDrawable rippleDrawableCompat;
|
|
@Nullable private Drawable tintableRippleDrawableCompat;
|
|
|
|
@Nullable private GradientDrawable backgroundDrawableLollipop;
|
|
@Nullable private GradientDrawable strokeDrawableLollipop;
|
|
@Nullable private GradientDrawable maskDrawableLollipop;
|
|
|
|
private boolean backgroundOverwritten = false;
|
|
|
|
public MaterialButtonHelper(MaterialButton button) {
|
|
materialButton = button;
|
|
}
|
|
|
|
public void loadFromAttributes(TypedArray attributes) {
|
|
insetLeft = attributes.getDimensionPixelOffset(R.styleable.MaterialButton_android_insetLeft, 0);
|
|
insetRight =
|
|
attributes.getDimensionPixelOffset(R.styleable.MaterialButton_android_insetRight, 0);
|
|
insetTop = attributes.getDimensionPixelOffset(R.styleable.MaterialButton_android_insetTop, 0);
|
|
insetBottom =
|
|
attributes.getDimensionPixelOffset(R.styleable.MaterialButton_android_insetBottom, 0);
|
|
|
|
cornerRadius = attributes.getDimensionPixelSize(R.styleable.MaterialButton_cornerRadius, 0);
|
|
strokeWidth = attributes.getDimensionPixelSize(R.styleable.MaterialButton_strokeWidth, 0);
|
|
|
|
backgroundTintMode =
|
|
ViewUtils.parseTintMode(
|
|
attributes.getInt(R.styleable.MaterialButton_backgroundTintMode, -1), Mode.SRC_IN);
|
|
backgroundTint =
|
|
MaterialResources.getColorStateList(
|
|
materialButton.getContext(), attributes, R.styleable.MaterialButton_backgroundTint);
|
|
strokeColor =
|
|
MaterialResources.getColorStateList(
|
|
materialButton.getContext(), attributes, R.styleable.MaterialButton_strokeColor);
|
|
rippleColor =
|
|
MaterialResources.getColorStateList(
|
|
materialButton.getContext(), attributes, R.styleable.MaterialButton_rippleColor);
|
|
|
|
buttonStrokePaint.setStyle(Style.STROKE);
|
|
buttonStrokePaint.setStrokeWidth(strokeWidth);
|
|
buttonStrokePaint.setColor(
|
|
strokeColor != null
|
|
? strokeColor.getColorForState(materialButton.getDrawableState(), Color.TRANSPARENT)
|
|
: Color.TRANSPARENT);
|
|
|
|
// Update materialButton's background without triggering setBackgroundOverwritten()
|
|
materialButton.setInternalBackground(
|
|
IS_LOLLIPOP ? createBackgroundLollipop() : createBackgroundCompat());
|
|
}
|
|
|
|
/**
|
|
* Method that is triggered when our initial background, created by {@link
|
|
* #createBackgroundCompat()} or {@link #createBackgroundLollipop()}, has been overwritten with a
|
|
* new background. Sets the {@link #backgroundOverwritten} flag, which disables some of the
|
|
* functionality tied to our custom background.
|
|
*/
|
|
void setBackgroundOverwritten() {
|
|
backgroundOverwritten = true;
|
|
// AppCompatButton re-applies any tint that was set when background is changed, so we must
|
|
// pass our tints to AppCompatButton when background is overwritten.
|
|
materialButton.setSupportBackgroundTintList(backgroundTint);
|
|
materialButton.setSupportBackgroundTintMode(backgroundTintMode);
|
|
}
|
|
|
|
boolean isBackgroundOverwritten() {
|
|
return backgroundOverwritten;
|
|
}
|
|
|
|
/** Manually draw stroke on top of background for Kit Kat (API 19) and earlier versions */
|
|
void drawStroke(@Nullable Canvas canvas) {
|
|
if (canvas != null && strokeColor != null && strokeWidth > 0) {
|
|
bounds.set(materialButton.getBackground().getBounds());
|
|
rectF.set(
|
|
bounds.left + (strokeWidth / 2f) + insetLeft,
|
|
bounds.top + (strokeWidth / 2f) + insetTop,
|
|
bounds.right - (strokeWidth / 2f) - insetRight,
|
|
bounds.bottom - (strokeWidth / 2f) - insetBottom);
|
|
// We need to adjust stroke's corner radius so that the corners of the background are not
|
|
// drawn outside stroke
|
|
float strokeCornerRadius = cornerRadius - strokeWidth / 2f;
|
|
canvas.drawRoundRect(rectF, strokeCornerRadius, strokeCornerRadius, buttonStrokePaint);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create background for KitKat (API 19) and earlier API versions
|
|
*
|
|
* @return Drawable representing background for this button.
|
|
*/
|
|
private Drawable createBackgroundCompat() {
|
|
colorableBackgroundDrawableCompat = new GradientDrawable();
|
|
colorableBackgroundDrawableCompat.setCornerRadius(cornerRadius + CORNER_RADIUS_ADJUSTMENT);
|
|
colorableBackgroundDrawableCompat.setColor(DEFAULT_BACKGROUND_COLOR);
|
|
|
|
tintableBackgroundDrawableCompat = DrawableCompat.wrap(colorableBackgroundDrawableCompat);
|
|
DrawableCompat.setTintList(tintableBackgroundDrawableCompat, backgroundTint);
|
|
if (backgroundTintMode != null) {
|
|
DrawableCompat.setTintMode(tintableBackgroundDrawableCompat, backgroundTintMode);
|
|
}
|
|
|
|
rippleDrawableCompat = new GradientDrawable();
|
|
rippleDrawableCompat.setCornerRadius(cornerRadius + CORNER_RADIUS_ADJUSTMENT);
|
|
rippleDrawableCompat.setColor(Color.WHITE);
|
|
|
|
tintableRippleDrawableCompat = DrawableCompat.wrap(rippleDrawableCompat);
|
|
DrawableCompat.setTintList(tintableRippleDrawableCompat, rippleColor);
|
|
|
|
return wrapDrawableWithInset(
|
|
new LayerDrawable(
|
|
new Drawable[] {tintableBackgroundDrawableCompat, tintableRippleDrawableCompat}));
|
|
}
|
|
|
|
private InsetDrawable wrapDrawableWithInset(Drawable drawable) {
|
|
return new InsetDrawable(drawable, insetLeft, insetTop, insetRight, insetBottom);
|
|
}
|
|
|
|
void setSupportBackgroundTintList(@Nullable ColorStateList tintList) {
|
|
if (backgroundTint != tintList) {
|
|
backgroundTint = tintList;
|
|
if (IS_LOLLIPOP) {
|
|
updateTintAndTintModeLollipop();
|
|
} else if (tintableBackgroundDrawableCompat != null) {
|
|
DrawableCompat.setTintList(tintableBackgroundDrawableCompat, backgroundTint);
|
|
}
|
|
}
|
|
}
|
|
|
|
ColorStateList getSupportBackgroundTintList() {
|
|
return backgroundTint;
|
|
}
|
|
|
|
void setSupportBackgroundTintMode(@Nullable Mode mode) {
|
|
if (backgroundTintMode != mode) {
|
|
backgroundTintMode = mode;
|
|
if (IS_LOLLIPOP) {
|
|
updateTintAndTintModeLollipop();
|
|
} else if (tintableBackgroundDrawableCompat != null && backgroundTintMode != null) {
|
|
DrawableCompat.setTintMode(tintableBackgroundDrawableCompat, backgroundTintMode);
|
|
}
|
|
}
|
|
}
|
|
|
|
Mode getSupportBackgroundTintMode() {
|
|
return backgroundTintMode;
|
|
}
|
|
|
|
private void updateTintAndTintModeLollipop() {
|
|
if (backgroundDrawableLollipop != null) {
|
|
DrawableCompat.setTintList(backgroundDrawableLollipop, backgroundTint);
|
|
if (backgroundTintMode != null) {
|
|
DrawableCompat.setTintMode(backgroundDrawableLollipop, backgroundTintMode);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create RippleDrawable background for Lollipop (API 21) and later API versions
|
|
*
|
|
* @return Drawable representing background for this button.
|
|
*/
|
|
@TargetApi(VERSION_CODES.LOLLIPOP)
|
|
private Drawable createBackgroundLollipop() {
|
|
backgroundDrawableLollipop = new GradientDrawable();
|
|
backgroundDrawableLollipop.setCornerRadius(cornerRadius + CORNER_RADIUS_ADJUSTMENT);
|
|
backgroundDrawableLollipop.setColor(DEFAULT_BACKGROUND_COLOR);
|
|
|
|
updateTintAndTintModeLollipop();
|
|
|
|
strokeDrawableLollipop = new GradientDrawable();
|
|
strokeDrawableLollipop.setCornerRadius(cornerRadius + CORNER_RADIUS_ADJUSTMENT);
|
|
strokeDrawableLollipop.setColor(Color.TRANSPARENT);
|
|
strokeDrawableLollipop.setStroke(strokeWidth, strokeColor);
|
|
|
|
LayerDrawable layerDrawable =
|
|
new LayerDrawable(new Drawable[] {backgroundDrawableLollipop, strokeDrawableLollipop});
|
|
|
|
InsetDrawable bgInsetDrawable = wrapDrawableWithInset(layerDrawable);
|
|
|
|
maskDrawableLollipop = new GradientDrawable();
|
|
maskDrawableLollipop.setCornerRadius(cornerRadius + CORNER_RADIUS_ADJUSTMENT);
|
|
maskDrawableLollipop.setColor(Color.WHITE);
|
|
|
|
return new MaterialButtonBackgroundDrawable(
|
|
RippleUtils.convertToRippleDrawableColor(rippleColor),
|
|
bgInsetDrawable,
|
|
maskDrawableLollipop);
|
|
}
|
|
|
|
void updateMaskBounds(int height, int width) {
|
|
if (maskDrawableLollipop != null) {
|
|
maskDrawableLollipop.setBounds(insetLeft, insetTop, width - insetRight, height - insetBottom);
|
|
}
|
|
}
|
|
|
|
void setBackgroundColor(int color) {
|
|
if (IS_LOLLIPOP && backgroundDrawableLollipop != null) {
|
|
backgroundDrawableLollipop.setColor(color);
|
|
} else if (!IS_LOLLIPOP && colorableBackgroundDrawableCompat != null) {
|
|
colorableBackgroundDrawableCompat.setColor(color);
|
|
}
|
|
}
|
|
|
|
void setRippleColor(@Nullable ColorStateList rippleColor) {
|
|
if (this.rippleColor != rippleColor) {
|
|
this.rippleColor = rippleColor;
|
|
if (IS_LOLLIPOP && materialButton.getBackground() instanceof RippleDrawable) {
|
|
((RippleDrawable) materialButton.getBackground()).setColor(rippleColor);
|
|
} else if (!IS_LOLLIPOP && tintableRippleDrawableCompat != null) {
|
|
DrawableCompat.setTintList(tintableRippleDrawableCompat, rippleColor);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Nullable
|
|
ColorStateList getRippleColor() {
|
|
return rippleColor;
|
|
}
|
|
|
|
void setStrokeColor(@Nullable ColorStateList strokeColor) {
|
|
if (this.strokeColor != strokeColor) {
|
|
this.strokeColor = strokeColor;
|
|
buttonStrokePaint.setColor(
|
|
strokeColor != null
|
|
? strokeColor.getColorForState(materialButton.getDrawableState(), Color.TRANSPARENT)
|
|
: Color.TRANSPARENT);
|
|
updateStroke();
|
|
}
|
|
}
|
|
|
|
@Nullable
|
|
ColorStateList getStrokeColor() {
|
|
return strokeColor;
|
|
}
|
|
|
|
void setStrokeWidth(int strokeWidth) {
|
|
if (this.strokeWidth != strokeWidth) {
|
|
this.strokeWidth = strokeWidth;
|
|
buttonStrokePaint.setStrokeWidth(strokeWidth);
|
|
updateStroke();
|
|
}
|
|
}
|
|
|
|
int getStrokeWidth() {
|
|
return strokeWidth;
|
|
}
|
|
|
|
private void updateStroke() {
|
|
if (IS_LOLLIPOP && strokeDrawableLollipop != null) {
|
|
// TODO: Stroke on API 21 results in strange width, even after unwrapping stroke drawable
|
|
// TODO: Changing stroke width on strokeDrawableLollipop results in stroke being clipped
|
|
materialButton.setInternalBackground(createBackgroundLollipop());
|
|
} else if (!IS_LOLLIPOP) {
|
|
// Force redraw of stroke
|
|
materialButton.invalidate();
|
|
}
|
|
}
|
|
|
|
void setCornerRadius(int cornerRadius) {
|
|
if (this.cornerRadius != cornerRadius) {
|
|
this.cornerRadius = cornerRadius;
|
|
if (IS_LOLLIPOP
|
|
&& backgroundDrawableLollipop != null
|
|
&& strokeDrawableLollipop != null
|
|
&& maskDrawableLollipop != null) {
|
|
// TODO: Setting corner radius on API 21 does not work without unwrapping drawables
|
|
if (VERSION.SDK_INT == VERSION_CODES.LOLLIPOP) {
|
|
unwrapBackgroundDrawable().setCornerRadius(cornerRadius + CORNER_RADIUS_ADJUSTMENT);
|
|
unwrapStrokeDrawable().setCornerRadius(cornerRadius + CORNER_RADIUS_ADJUSTMENT);
|
|
}
|
|
backgroundDrawableLollipop.setCornerRadius(cornerRadius + CORNER_RADIUS_ADJUSTMENT);
|
|
strokeDrawableLollipop.setCornerRadius(cornerRadius + CORNER_RADIUS_ADJUSTMENT);
|
|
maskDrawableLollipop.setCornerRadius(cornerRadius + CORNER_RADIUS_ADJUSTMENT);
|
|
} else if (!IS_LOLLIPOP
|
|
&& colorableBackgroundDrawableCompat != null
|
|
&& rippleDrawableCompat != null) {
|
|
colorableBackgroundDrawableCompat.setCornerRadius(cornerRadius + CORNER_RADIUS_ADJUSTMENT);
|
|
rippleDrawableCompat.setCornerRadius(cornerRadius + CORNER_RADIUS_ADJUSTMENT);
|
|
// Force redraw of stroke
|
|
materialButton.invalidate();
|
|
}
|
|
}
|
|
}
|
|
|
|
int getCornerRadius() {
|
|
return cornerRadius;
|
|
}
|
|
|
|
@Nullable
|
|
private GradientDrawable unwrapStrokeDrawable() {
|
|
if (IS_LOLLIPOP && materialButton.getBackground() != null) {
|
|
RippleDrawable background = (RippleDrawable) materialButton.getBackground();
|
|
InsetDrawable insetDrawable = (InsetDrawable) background.getDrawable(0);
|
|
LayerDrawable layerDrawable = (LayerDrawable) insetDrawable.getDrawable();
|
|
return (GradientDrawable) layerDrawable.getDrawable(1);
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
@Nullable
|
|
private GradientDrawable unwrapBackgroundDrawable() {
|
|
if (IS_LOLLIPOP && materialButton.getBackground() != null) {
|
|
RippleDrawable background = (RippleDrawable) materialButton.getBackground();
|
|
InsetDrawable insetDrawable = (InsetDrawable) background.getDrawable(0);
|
|
LayerDrawable layerDrawable = (LayerDrawable) insetDrawable.getDrawable();
|
|
return (GradientDrawable) layerDrawable.getDrawable(0);
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
}
|