Android控件的圆角效果的实现方式有很多种,这里介绍一下另一种Android原生的圆角实现方案(API28及以上)。
我们利用ShapeAppearanceModel、MaterialShapeDrawable来实现一个圆角/切角的Button。
实现效果如下图
我们在为控件添加阴影的基础上实现为控件添加shape的功能.
增加自定义属性:
创建通用的shape接口
public interface ShapeModelView {
void setShapeModel(ShapeAppearanceModel shapeModel);
ShapeAppearanceModel getShapeModel();
void setCornerCut(float cornerCut);
void setCornerRadius(float cornerRadius);
}
为ShapeButton 实现ShapeModeView接口:
public class ShapeButton extends AppCompatButton implements ShadowView, ShapeModelView {
//.....
private static int[] elevationIds = new int[]{
R.styleable.shape_button_carbon_elevation,
R.styleable.shape_button_carbon_elevationShadowColor,
R.styleable.shape_button_carbon_elevationAmbientShadowColor,
R.styleable.shape_button_carbon_elevationSpotShadowColor
};
private static int[] cornerCutRadiusIds = new int[]{
R.styleable.shape_button_carbon_cornerRadiusTopStart,
R.styleable.shape_button_carbon_cornerRadiusTopEnd,
R.styleable.shape_button_carbon_cornerRadiusBottomStart,
R.styleable.shape_button_carbon_cornerRadiusBottomEnd,
R.styleable.shape_button_carbon_cornerRadius,
R.styleable.shape_button_carbon_cornerCutTopStart,
R.styleable.shape_button_carbon_cornerCutTopEnd,
R.styleable.shape_button_carbon_cornerCutBottomStart,
R.styleable.shape_button_carbon_cornerCutBottomEnd,
R.styleable.shape_button_carbon_cornerCut
};
private void initButton(AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.shape_button, defStyleAttr, defStyleRes);
// 初始化阴影相关属性
Carbon.initElevation(this, a, elevationIds);
// 初始化圆角相关属性
Carbon.initCornerCutRadius(this,a,cornerCutRadiusIds);
a.recycle();
}
// -------------------------------
// shadow
// -------------------------------
.....
// -------------------------------
// shape
// -------------------------------
private ShapeAppearanceModel shapeModel = new ShapeAppearanceModel();
private MaterialShapeDrawable shadowDrawable = new MaterialShapeDrawable(shapeModel);
@Override
public void setShapeModel(ShapeAppearanceModel shapeModel) {
this.shapeModel = shapeModel;
shadowDrawable = new MaterialShapeDrawable(shapeModel);
if (getWidth() > 0 && getHeight() > 0)
updateCorners();
if (!Carbon.IS_LOLLIPOP_OR_HIGHER)
postInvalidate();
}
// View的轮廓形状
private RectF boundsRect = new RectF();
// View的轮廓形状形成的Path路径
private Path cornersMask = new Path();
/**
* 更新圆角
*/
private void updateCorners() {
if (Carbon.IS_LOLLIPOP_OR_HIGHER) {
// 如果不是矩形,裁剪View的轮廓
if (!Carbon.isShapeRect(shapeModel, boundsRect)){
setClipToOutline(true);
}
//该方法返回一个Outline对象,它描述了该视图的形状。
setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
if (Carbon.isShapeRect(shapeModel, boundsRect)) {
outline.setRect(0, 0, getWidth(), getHeight());
} else {
shadowDrawable.setBounds(0, 0, getWidth(), getHeight());
shadowDrawable.setShadowCompatibilityMode(MaterialShapeDrawable.SHADOW_COMPAT_MODE_NEVER);
shadowDrawable.getOutline(outline);
}
}
});
}
// 拿到圆角矩形的形状
boundsRect.set(shadowDrawable.getBounds());
// 拿到圆角矩形的Path
shadowDrawable.getPathForSize(getWidth(), getHeight(), cornersMask);
}
@Override
public ShapeAppearanceModel getShapeModel() {
return this.shapeModel;
}
@Override
public void setCornerCut(float cornerCut) {
shapeModel = ShapeAppearanceModel.builder().setAllCorners(new CutCornerTreatment(cornerCut)).build();
setShapeModel(shapeModel);
}
@Override
public void setCornerRadius(float cornerRadius) {
shapeModel = ShapeAppearanceModel.builder().setAllCorners(new RoundedCornerTreatment(cornerRadius)).build();
setShapeModel(shapeModel);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (!changed)
return;
if (getWidth() == 0 || getHeight() == 0)
return;
updateCorners();
}
}
@Override
public void draw(Canvas canvas) {
boolean c = !Carbon.isShapeRect(shapeModel, boundsRect);
if (Carbon.IS_PIE_OR_HIGHER) {
if (spotShadowColor != null)
super.setOutlineSpotShadowColor(spotShadowColor.getColorForState(getDrawableState(), spotShadowColor.getDefaultColor()));
if (ambientShadowColor != null)
super.setOutlineAmbientShadowColor(ambientShadowColor.getColorForState(getDrawableState(), ambientShadowColor.getDefaultColor()));
}
// 判断如果不是圆角矩形,需要使用轮廓Path,重新绘制一下Path,不然显示会很奇怪
if (getWidth() > 0 && getHeight() > 0 && ((c && !Carbon.IS_LOLLIPOP_OR_HIGHER) || !shapeModel.isRoundRect(boundsRect))) {
int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
super.draw(canvas);
paint.setXfermode(Carbon.CLEAR_MODE);
if (c) {
cornersMask.setFillType(Path.FillType.INVERSE_WINDING);
canvas.drawPath(cornersMask, paint);
}
canvas.restoreToCount(saveCount);
paint.setXfermode(null);
}else{
super.draw(canvas);
}
}
在Carbon中添加方法:
public static void initCornerCutRadius(ShapeModelView shapeModelView, TypedArray a, int[] ids) {
int carbon_cornerRadiusTopStart = ids[0];
int carbon_cornerRadiusTopEnd = ids[1];
int carbon_cornerRadiusBottomStart = ids[2];
int carbon_cornerRadiusBottomEnd = ids[3];
int carbon_cornerRadius = ids[4];
int carbon_cornerCutTopStart = ids[5];
int carbon_cornerCutTopEnd = ids[6];
int carbon_cornerCutBottomStart = ids[7];
int carbon_cornerCutBottomEnd = ids[8];
int carbon_cornerCut = ids[9];
float cornerRadius = Math.max(a.getDimension(carbon_cornerRadius, 0), 0.1f);
float cornerRadiusTopStart = a.getDimension(carbon_cornerRadiusTopStart, cornerRadius);
float cornerRadiusTopEnd = a.getDimension(carbon_cornerRadiusTopEnd, cornerRadius);
float cornerRadiusBottomStart = a.getDimension(carbon_cornerRadiusBottomStart, cornerRadius);
float cornerRadiusBottomEnd = a.getDimension(carbon_cornerRadiusBottomEnd, cornerRadius);
float cornerCut = a.getDimension(carbon_cornerCut, 0);
float cornerCutTopStart = a.getDimension(carbon_cornerCutTopStart, cornerCut);
float cornerCutTopEnd = a.getDimension(carbon_cornerCutTopEnd, cornerCut);
float cornerCutBottomStart = a.getDimension(carbon_cornerCutBottomStart, cornerCut);
float cornerCutBottomEnd = a.getDimension(carbon_cornerCutBottomEnd, cornerCut);
ShapeAppearanceModel model = ShapeAppearanceModel.builder()
.setTopLeftCorner(cornerCutTopStart >= cornerRadiusTopStart ? new CutCornerTreatment(cornerCutTopStart) : new RoundedCornerTreatment(cornerRadiusTopStart))
.setTopRightCorner(cornerCutTopEnd >= cornerRadiusTopEnd ? new CutCornerTreatment(cornerCutTopEnd) : new RoundedCornerTreatment(cornerRadiusTopEnd))
.setBottomLeftCorner(cornerCutBottomStart >= cornerRadiusBottomStart ? new CutCornerTreatment(cornerCutBottomStart) : new RoundedCornerTreatment(cornerRadiusBottomStart))
.setBottomRightCorner(cornerCutBottomEnd >= cornerRadiusBottomEnd ? new CutCornerTreatment(cornerCutBottomEnd) : new RoundedCornerTreatment(cornerRadiusBottomEnd))
.build();
shapeModelView.setShapeModel(model);
}
public static boolean isShapeRect(ShapeAppearanceModel model, RectF bounds) {
return model.getTopLeftCornerSize().getCornerSize(bounds) <= 0.2f &&
model.getTopRightCornerSize().getCornerSize(bounds) <= 0.2f &&
model.getBottomLeftCornerSize().getCornerSize(bounds) <= 0.2f &&
model.getBottomRightCornerSize().getCornerSize(bounds) <= 0.2f;
}
完整代码:
public class ShapeButton extends AppCompatButton implements ShadowView, ShapeModelView {
public ShapeButton(@NonNull Context context) {
super(context);
initButton(null, android.R.attr.buttonStyle, R.style.carbon_Button);
}
public ShapeButton(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initButton(attrs, android.R.attr.buttonStyle, R.style.carbon_Button);
}
public ShapeButton(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initButton(attrs, defStyleAttr, R.style.carbon_Button);
}
public ShapeButton(Context context, String text, OnClickListener listener) {
super(context);
initButton(null, android.R.attr.buttonStyle, R.style.carbon_Button);
setText(text);
setOnClickListener(listener);
}
private static int[] elevationIds = new int[]{
R.styleable.shape_button_carbon_elevation,
R.styleable.shape_button_carbon_elevationShadowColor,
R.styleable.shape_button_carbon_elevationAmbientShadowColor,
R.styleable.shape_button_carbon_elevationSpotShadowColor
};
private static int[] cornerCutRadiusIds = new int[]{
R.styleable.shape_button_carbon_cornerRadiusTopStart,
R.styleable.shape_button_carbon_cornerRadiusTopEnd,
R.styleable.shape_button_carbon_cornerRadiusBottomStart,
R.styleable.shape_button_carbon_cornerRadiusBottomEnd,
R.styleable.shape_button_carbon_cornerRadius,
R.styleable.shape_button_carbon_cornerCutTopStart,
R.styleable.shape_button_carbon_cornerCutTopEnd,
R.styleable.shape_button_carbon_cornerCutBottomStart,
R.styleable.shape_button_carbon_cornerCutBottomEnd,
R.styleable.shape_button_carbon_cornerCut
};
protected TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
private void initButton(AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.shape_button, defStyleAttr, defStyleRes);
Carbon.initElevation(this, a, elevationIds);
Carbon.initCornerCutRadius(this,a,cornerCutRadiusIds);
a.recycle();
}
// -------------------------------
// shadow
// -------------------------------
private float elevation = 0;
private float translationZ = 0;
private ColorStateList ambientShadowColor, spotShadowColor;
@Override
public float getElevation() {
return elevation;
}
@Override
public void setElevation(float elevation) {
if (Carbon.IS_PIE_OR_HIGHER) {
super.setElevation(elevation);
super.setTranslationZ(translationZ);
} else if (Carbon.IS_LOLLIPOP_OR_HIGHER) {
if (ambientShadowColor == null || spotShadowColor == null) {
super.setElevation(elevation);
super.setTranslationZ(translationZ);
} else {
super.setElevation(0);
super.setTranslationZ(0);
}
} else if (elevation != this.elevation && getParent() != null) {
((View) getParent()).postInvalidate();
}
this.elevation = elevation;
}
@Override
public float getTranslationZ() {
return translationZ;
}
public void setTranslationZ(float translationZ) {
if (translationZ == this.translationZ)
return;
if (Carbon.IS_PIE_OR_HIGHER) {
super.setTranslationZ(translationZ);
} else if (Carbon.IS_LOLLIPOP_OR_HIGHER) {
if (ambientShadowColor == null || spotShadowColor == null) {
super.setTranslationZ(translationZ);
} else {
super.setTranslationZ(0);
}
} else if (translationZ != this.translationZ && getParent() != null) {
((View) getParent()).postInvalidate();
}
this.translationZ = translationZ;
}
@Override
public ColorStateList getElevationShadowColor() {
return ambientShadowColor;
}
@Override
public void setElevationShadowColor(ColorStateList shadowColor) {
ambientShadowColor = spotShadowColor = shadowColor;
setElevation(elevation);
setTranslationZ(translationZ);
}
@Override
public void setElevationShadowColor(int color) {
ambientShadowColor = spotShadowColor = ColorStateList.valueOf(color);
setElevation(elevation);
setTranslationZ(translationZ);
}
@Override
public void setOutlineAmbientShadowColor(ColorStateList color) {
ambientShadowColor = color;
if (Carbon.IS_PIE_OR_HIGHER) {
super.setOutlineAmbientShadowColor(color.getColorForState(getDrawableState(), color.getDefaultColor()));
} else {
setElevation(elevation);
setTranslationZ(translationZ);
}
}
@Override
public void setOutlineAmbientShadowColor(int color) {
setOutlineAmbientShadowColor(ColorStateList.valueOf(color));
}
@Override
public int getOutlineAmbientShadowColor() {
return ambientShadowColor.getDefaultColor();
}
@Override
public void setOutlineSpotShadowColor(int color) {
setOutlineSpotShadowColor(ColorStateList.valueOf(color));
}
@Override
public void setOutlineSpotShadowColor(ColorStateList color) {
spotShadowColor = color;
if (Carbon.IS_PIE_OR_HIGHER) {
super.setOutlineSpotShadowColor(color.getColorForState(getDrawableState(), color.getDefaultColor()));
} else {
setElevation(elevation);
setTranslationZ(translationZ);
}
}
@Override
public int getOutlineSpotShadowColor() {
return ambientShadowColor.getDefaultColor();
}
@Override
public void draw(Canvas canvas) {
boolean c = !Carbon.isShapeRect(shapeModel, boundsRect);
if (Carbon.IS_PIE_OR_HIGHER) {
if (spotShadowColor != null)
super.setOutlineSpotShadowColor(spotShadowColor.getColorForState(getDrawableState(), spotShadowColor.getDefaultColor()));
if (ambientShadowColor != null)
super.setOutlineAmbientShadowColor(ambientShadowColor.getColorForState(getDrawableState(), ambientShadowColor.getDefaultColor()));
}
// 判断如果不是圆角矩形,需要使用轮廓Path,绘制一下Path,不然显示会很奇怪
if (getWidth() > 0 && getHeight() > 0 && ((c && !Carbon.IS_LOLLIPOP_OR_HIGHER) || !shapeModel.isRoundRect(boundsRect))) {
int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
super.draw(canvas);
paint.setXfermode(Carbon.CLEAR_MODE);
if (c) {
cornersMask.setFillType(Path.FillType.INVERSE_WINDING);
canvas.drawPath(cornersMask, paint);
}
canvas.restoreToCount(saveCount);
paint.setXfermode(null);
}else{
super.draw(canvas);
}
}
// -------------------------------
// shape
// -------------------------------
private ShapeAppearanceModel shapeModel = new ShapeAppearanceModel();
private MaterialShapeDrawable shadowDrawable = new MaterialShapeDrawable(shapeModel);
@Override
public void setShapeModel(ShapeAppearanceModel shapeModel) {
this.shapeModel = shapeModel;
shadowDrawable = new MaterialShapeDrawable(shapeModel);
if (getWidth() > 0 && getHeight() > 0)
updateCorners();
if (!Carbon.IS_LOLLIPOP_OR_HIGHER)
postInvalidate();
}
// View的轮廓形状
private RectF boundsRect = new RectF();
// View的轮廓形状形成的Path路径
private Path cornersMask = new Path();
/**
* 更新圆角
*/
private void updateCorners() {
if (Carbon.IS_LOLLIPOP_OR_HIGHER) {
// 如果不是矩形,裁剪View的轮廓
if (!Carbon.isShapeRect(shapeModel, boundsRect)){
setClipToOutline(true);
}
//该方法返回一个Outline对象,它描述了该视图的形状。
setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
if (Carbon.isShapeRect(shapeModel, boundsRect)) {
outline.setRect(0, 0, getWidth(), getHeight());
} else {
shadowDrawable.setBounds(0, 0, getWidth(), getHeight());
shadowDrawable.setShadowCompatibilityMode(MaterialShapeDrawable.SHADOW_COMPAT_MODE_NEVER);
shadowDrawable.getOutline(outline);
}
}
});
}
// 拿到圆角矩形的形状
boundsRect.set(shadowDrawable.getBounds());
// 拿到圆角矩形的Path
shadowDrawable.getPathForSize(getWidth(), getHeight(), cornersMask);
}
@Override
public ShapeAppearanceModel getShapeModel() {
return this.shapeModel;
}
@Override
public void setCornerCut(float cornerCut) {
shapeModel = ShapeAppearanceModel.builder().setAllCorners(new CutCornerTreatment(cornerCut)).build();
setShapeModel(shapeModel);
}
@Override
public void setCornerRadius(float cornerRadius) {
shapeModel = ShapeAppearanceModel.builder().setAllCorners(new RoundedCornerTreatment(cornerRadius)).build();
setShapeModel(shapeModel);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (!changed)
return;
if (getWidth() == 0 || getHeight() == 0)
return;
updateCorners();
}
}