Android控件的Outline效果的实现方式有很多种,这里介绍一下另一种使用Canvas.drawPath()方法来绘制控件轮廓Path路径的实现方案(API28及以上)。
实现效果:
属性
添加Outline相关属性,主要包括颜色和Stroke宽度:
<declare-styleable name="shape_button">
...
<attr name="carbon_stroke" />
<attr name="carbon_strokeWidth" />
</declare-styleable>
StrokeView接口
创建一个StrokeView通用接口:
/**
* 外部轮廓相关
*/
public interface StrokeView {
ColorStateList getStroke();
void setStroke(ColorStateList color);
void setStroke(int color);
float getStrokeWidth();
void setStrokeWidth(float strokeWidth);
}
ShapeButton 实现这个StrokeView接口:
public class ShapeButton extends AppCompatButton
implements ShadowView,
ShapeModelView,
RippleView,
StrokeView {
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
};
private static int[] rippleIds = new int[]{
R.styleable.shape_button_carbon_rippleColor,
R.styleable.shape_button_carbon_rippleStyle,
R.styleable.shape_button_carbon_rippleHotspot,
R.styleable.shape_button_carbon_rippleRadius
};
private static int[] strokeIds = new int[]{
R.styleable.shape_button_carbon_stroke,
R.styleable.shape_button_carbon_strokeWidth
};
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);
Carbon.initRippleDrawable(this,a,rippleIds);
// 初始化Stroke相关属性
Carbon.initStroke(this,a,strokeIds);
a.recycle();
}
// -------------------------------
// shadow
// -------------------------------
// -------------------------------
// shape
// -------------------------------
// -------------------------------
// ripple
// -------------------------------
// -------------------------------
// stroke
// -------------------------------
private ColorStateList stroke;
private float strokeWidth;
private Paint strokePaint;
// 绘制轮廓
private void drawStroke(Canvas canvas) {
strokePaint.setStrokeWidth(strokeWidth * 2);
strokePaint.setColor(stroke.getColorForState(getDrawableState(), stroke.getDefaultColor()));
// cornersMask这是之前装载控件轮廓的path对象
cornersMask.setFillType(Path.FillType.WINDING);
canvas.drawPath(cornersMask, strokePaint);
}
// 设置轮廓颜色
@Override
public void setStroke(ColorStateList colorStateList) {
stroke = colorStateList;
if (stroke == null)
return;
if (strokePaint == null) {
strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
strokePaint.setStyle(Paint.Style.STROKE);
}
}
// 设置轮廓颜色
@Override
public void setStroke(int color) {
setStroke(ColorStateList.valueOf(color));
}
@Override
public ColorStateList getStroke() {
return stroke;
}
// 设置轮廓线条宽度
@Override
public void setStrokeWidth(float strokeWidth) {
this.strokeWidth = strokeWidth;
}
@Override
public float getStrokeWidth() {
return strokeWidth;
}
}
初始化Stroke属性
public static void initStroke(StrokeView strokeView, TypedArray a, int[] ids) {
int carbon_stroke = ids[0];
int carbon_strokeWidth = ids[1];
View view = (View) strokeView;
ColorStateList color = a.getColorStateList(carbon_stroke);
if (color != null)
strokeView.setStroke(color);
strokeView.setStrokeWidth(a.getDimension(carbon_strokeWidth, 0));
}
绘制轮廓
在onDraw()
方法的super.draw(canvas);
后面执行drawStroke()
方法:
public void drawInternal(@NonNull Canvas canvas) {
super.draw(canvas);
if(stroke!=null){
drawStroke(canvas);
}
}
如何使用
<com.chinatsp.demo1.shadow.ShapeButton
android:id="@+id/show_dialog_btn"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_margin="@dimen/carbon_padding"
android:background="#ffffff"
android:stateListAnimator="@null"
android:text="TOM"
app:carbon_cornerCut="4dp"
app:carbon_elevation="30dp"
app:carbon_elevationShadowColor="#40ff0000"
app:carbon_rippleColor="#40ff0000"
app:carbon_rippleStyle="borderless"
app:carbon_rippleRadius="30dp"
app:carbon_stroke="@color/carbon_green_400"
app:carbon_strokeWidth="1dp"/>
完整代码:
public class ShapeButton extends AppCompatButton
implements ShadowView,
ShapeModelView,
RippleView,
StrokeView {
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
};
private static int[] rippleIds = new int[]{
R.styleable.shape_button_carbon_rippleColor,
R.styleable.shape_button_carbon_rippleStyle,
R.styleable.shape_button_carbon_rippleHotspot,
R.styleable.shape_button_carbon_rippleRadius
};
private static int[] strokeIds = new int[]{
R.styleable.shape_button_carbon_stroke,
R.styleable.shape_button_carbon_strokeWidth
};
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);
Carbon.initRippleDrawable(this,a,rippleIds);
Carbon.initStroke(this,a,strokeIds);
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 boolean hasShadow() {
return false;
}
@Override
public void drawShadow(Canvas canvas) {
}
@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);
drawInternal(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{
drawInternal(canvas);
}
}
public void drawInternal(@NonNull Canvas canvas) {
super.draw(canvas);
if(stroke!=null){
drawStroke(canvas);
}
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Over)
rippleDrawable.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();
if (rippleDrawable != null)
rippleDrawable.setBounds(0, 0, getWidth(), getHeight());
}
// -------------------------------
// ripple
// -------------------------------
private RippleDrawable rippleDrawable;
@Override
public boolean dispatchTouchEvent(@NonNull MotionEvent event) {
if (rippleDrawable != null && event.getAction() == MotionEvent.ACTION_DOWN)
rippleDrawable.setHotspot(event.getX(),event.getY());
return super.dispatchTouchEvent(event);
}
@Override
public RippleDrawable getRippleDrawable() {
return rippleDrawable;
}
@Override
public void setRippleDrawable(RippleDrawable newRipple) {
if (rippleDrawable != null) {
rippleDrawable.setCallback(null);
if (rippleDrawable.getStyle() == RippleDrawable.Style.Background)
super.setBackgroundDrawable(rippleDrawable.getBackground());
}
if (newRipple != null) {
newRipple.setCallback(this);
newRipple.setBounds(0, 0, getWidth(), getHeight());
newRipple.setState(getDrawableState());
((Drawable) newRipple).setVisible(getVisibility() == VISIBLE, false);
if (newRipple.getStyle() == RippleDrawable.Style.Background)
super.setBackgroundDrawable((Drawable) newRipple);
}
rippleDrawable = newRipple;
}
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
if (rippleDrawable != null && rippleDrawable.getStyle() != RippleDrawable.Style.Background)
rippleDrawable.setState(getDrawableState());
}
@Override
protected boolean verifyDrawable(@NonNull Drawable who) {
return super.verifyDrawable(who) || rippleDrawable == who;
}
@Override
public void invalidateDrawable(@NonNull Drawable drawable) {
super.invalidateDrawable(drawable);
invalidateParentIfNeeded();
}
@Override
public void invalidate(@NonNull Rect dirty) {
super.invalidate(dirty);
invalidateParentIfNeeded();
}
@Override
public void invalidate(int l, int t, int r, int b) {
super.invalidate(l, t, r, b);
invalidateParentIfNeeded();
}
@Override
public void invalidate() {
super.invalidate();
invalidateParentIfNeeded();
}
private void invalidateParentIfNeeded() {
if (getParent() == null || !(getParent() instanceof View))
return;
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Borderless)
((View) getParent()).invalidate();
}
@Override
public void setBackground(Drawable background) {
setBackgroundDrawable(background);
}
@Override
public void setBackgroundDrawable(Drawable background) {
if (background instanceof RippleDrawable) {
setRippleDrawable((RippleDrawable) background);
return;
}
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Background) {
rippleDrawable.setCallback(null);
rippleDrawable = null;
}
super.setBackgroundDrawable(background);
}
// -------------------------------
// stroke
// -------------------------------
private ColorStateList stroke;
private float strokeWidth;
private Paint strokePaint;
private void drawStroke(Canvas canvas) {
strokePaint.setStrokeWidth(strokeWidth * 2);
strokePaint.setColor(stroke.getColorForState(getDrawableState(), stroke.getDefaultColor()));
cornersMask.setFillType(Path.FillType.WINDING);
canvas.drawPath(cornersMask, strokePaint);
}
@Override
public void setStroke(ColorStateList colorStateList) {
stroke = colorStateList;
if (stroke == null)
return;
if (strokePaint == null) {
strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
strokePaint.setStyle(Paint.Style.STROKE);
}
}
@Override
public void setStroke(int color) {
setStroke(ColorStateList.valueOf(color));
}
@Override
public ColorStateList getStroke() {
return stroke;
}
@Override
public void setStrokeWidth(float strokeWidth) {
this.strokeWidth = strokeWidth;
}
@Override
public float getStrokeWidth() {
return strokeWidth;
}
}