蒙版控件
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.DiscretePathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.OnLifecycleEvent;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class MaskView extends View implements LifecycleObserver {
/**
* 四角圆,矩形
*/
public static final int TYPE_RECT = 1;
/**
* 椭圆形/圆
*/
public static final int TYPE_OVAL = 2;
public static final int TYPE_SOLID = 1;//实线
public static final int TYPE_DASHED = 2;//虚线
public static final int TYPE_ONLY_CORNERS = 3;//仅四角 仅矩形可用
public static final int TYPE_LACEWORK = 4;//花边
private PathEffect pathEffect;
private float innerBoundSectionWidth;
private int innerBoundStrokeType;
private boolean isAdjust;
private int width;
private int height;
private boolean isRecalculate = true;
@IntDef({TYPE_RECT, TYPE_OVAL})
@Retention(RetentionPolicy.SOURCE)
public @interface BoundsType {
}
@IntDef({TYPE_SOLID, TYPE_DASHED, TYPE_ONLY_CORNERS, TYPE_LACEWORK})
@Retention(RetentionPolicy.SOURCE)
public @interface BoundsStrokeType {
}
private float innerWidth;
private float innerHeight;
private float offsetX;
private float offsetY;
private Float offsetPercentX = null;
private Float offsetPercentY = null;
private boolean isOffsetX = false;
private boolean isOffsetY = false;
private int mType;//默认椭圆形
private float mRadius;
private boolean isBorder = false;
private boolean isClip = false;
private Paint boundPaint;
private RectF innerBounds;
private Path innerPath;
private float boundWidth;
private int backgroundColor;
private float offsetBound;
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void onDestroy() {
boundPaint = null;
offsetPercentX = null;
offsetPercentY = null;
innerBounds = null;
innerPath = null;
if (getContext() instanceof LifecycleOwner) {
((LifecycleOwner) getContext()).getLifecycle().removeObserver(this);
}
}
public MaskView(Context context) {
this(context, null);
}
public MaskView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MaskView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
if (context instanceof LifecycleOwner) {
((LifecycleOwner) context).getLifecycle().addObserver(this);
}
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MaskView);
mType = array.getInt(R.styleable.MaskView_innerBoundType, TYPE_OVAL);
if (array.hasValue(R.styleable.MaskView_innerBoundWidth)) {
float boundWidth = array.getDimensionPixelSize(R.styleable.MaskView_innerBoundWidth, 0);
setBoundPaintW(boundWidth);
}
if (array.hasValue(R.styleable.MaskView_innerBoundColor)) {
int boundColor = array.getColor(R.styleable.MaskView_innerBoundColor, Color.WHITE);
setBoundPaintC(boundColor);
}
innerBoundStrokeType = array.getInt(R.styleable.MaskView_innerBoundStrokeType, TYPE_SOLID);
innerBoundSectionWidth = array.getDimensionPixelSize(R.styleable.MaskView_innerBoundSectionWidth, 0);
try {
innerWidth = array.getLayoutDimension(R.styleable.MaskView_inner_Width, -1);
} catch (Exception e) {
e.printStackTrace();
innerWidth = -1;
}
try {
innerHeight = array.getLayoutDimension(R.styleable.MaskView_inner_height, (int) innerWidth);
} catch (Exception e) {
e.printStackTrace();
innerHeight = -1;
}
isAdjust = innerWidth == innerHeight;
backgroundColor = array.getColor(R.styleable.MaskView_backgroundColor, Color.TRANSPARENT);
mRadius = array.getDimensionPixelSize(R.styleable.MaskView_radius, 0);
if (array.hasValue(R.styleable.MaskView_offsetX)) {
initOffsetX(array.getDimensionPixelSize(R.styleable.MaskView_offsetX, 0));
}
if (array.hasValue(R.styleable.MaskView_offsetY)) {
initOffsetY(array.getDimensionPixelSize(R.styleable.MaskView_offsetY, 0));
}
if (array.hasValue(R.styleable.MaskView_offset_percentX)) {
offsetPercentX = array.getFloat(R.styleable.MaskView_offset_percentX, 0.0f);
isOffsetX = true;
}
if (array.hasValue(R.styleable.MaskView_offset_percentY)) {
offsetPercentY = array.getFloat(R.styleable.MaskView_offset_percentY, 0.0f);
isOffsetY = true;
}
if (array.hasValue(R.styleable.MaskView_paddingX)) {
int paddingX = array.getDimensionPixelSize(R.styleable.MaskView_paddingX, 0);
setPadding(paddingX, getPaddingTop(), paddingX, getPaddingBottom());
}
if (array.hasValue(R.styleable.MaskView_paddingY)) {
int paddingY = array.getDimensionPixelSize(R.styleable.MaskView_paddingY, 0);
setPadding(getPaddingLeft(), paddingY, getPaddingRight(), paddingY);
}
array.recycle();
setInnerBoundStrokeType();
innerBounds = new RectF();
innerPath = new Path();
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
public void setRadius(float mRadius) {
this.mRadius = mRadius;
isRecalculate = true;
}
public void setPadding(float dp) {
int px = BaseUtils.dp2px(dp);
setPadding(px, px, px, px);
isRecalculate = true;
}
/**
* @param marginDp 单位dp
*/
public void setPaddingX(float marginDp) {
int px = BaseUtils.dp2px(marginDp);
setPadding(px, getPaddingTop(), px, getPaddingBottom());
isRecalculate = true;
}
/**
* @param marginDp 单位dp
*/
public void setPaddingY(float marginDp) {
int px = BaseUtils.dp2px(marginDp);
setPadding(getPaddingLeft(), px, getPaddingRight(), px);
isRecalculate = true;
}
public RectF getBoundsRect() {
return innerBounds;
}
public void setInnerBoundsType(@BoundsType int type) {
mType = type;
if (type == TYPE_OVAL && innerBoundStrokeType == TYPE_ONLY_CORNERS) {
innerBoundStrokeType = TYPE_SOLID;
}
isRecalculate = true;
}
public void setInnerWidth(float widthDp) {
innerWidth = BaseUtils.dp2px(widthDp);
isRecalculate = true;
}
public void setInnerHeight(float heightDp) {
innerHeight = BaseUtils.dp2px(heightDp);
isAdjust = false;
isRecalculate = true;
}
public void adjustInnerBounds(boolean isAdjust) {
this.isAdjust = isAdjust;
isRecalculate = true;
}
public void setInnerWidthMatch() {
innerWidth = ViewGroup.LayoutParams.MATCH_PARENT;
isRecalculate = true;
}
public void setInnerHeightMatch() {
innerHeight = ViewGroup.LayoutParams.MATCH_PARENT;
isAdjust = false;
isRecalculate = true;
}
public void setOffsetX(int offset) {
initOffsetX(offset);
isRecalculate = true;
}
public void setOffsetY(int offset) {
initOffsetY(offset);
isRecalculate = true;
}
public void setOffsetX(float offsetPercent) {
offsetPercentX = offsetPercent;
isOffsetX = true;
isRecalculate = true;
}
public void setOffsetY(float offsetPercent) {
offsetPercentY = offsetPercent;
isOffsetY = true;
isRecalculate = true;
}
public void setInnerBoundsCenter() {
isOffsetY = false;
isOffsetX = false;
isRecalculate = true;
}
public void setBoundWidth(int widthDp) {
if (setBoundPaintW(BaseUtils.dp2px(widthDp))) {
isRecalculate = true;
}
}
public void setBoundColorRes(@ColorRes int color) {
setBoundColor(ResourcesUtils.getColor(color));
}
public void setBoundColor(@ColorInt int color) {
if (setBoundPaintC(color)) {
invalidate();
}
}
public void setInnerBoundStrokeType(@BoundsStrokeType int type) {
innerBoundStrokeType = type;
setInnerBoundStrokeType();
}
@Override
public void setBackgroundColor(int backgroundColor) {
this.backgroundColor = backgroundColor;
invalidate();
}
@Override
public void setBackgroundDrawable(Drawable background) {
super.setBackgroundDrawable(null);
}
@Override
public void setForeground(Drawable foreground) {
super.setForeground(null);
}
public void setInnerBoundSectionWidth(int dp) {
innerBoundSectionWidth = dp;
isRecalculate = true;
}
private void setInnerBoundStrokeType() {
if (innerBoundStrokeType == TYPE_SOLID) {
pathEffect = null;
} else if (innerBoundStrokeType == TYPE_DASHED) {
pathEffect = new DashPathEffect(new float[]{innerBoundSectionWidth, innerBoundSectionWidth}, innerBoundSectionWidth);
} else if (innerBoundStrokeType == TYPE_LACEWORK) {
pathEffect = new DiscretePathEffect(innerBoundSectionWidth, innerBoundSectionWidth);
} else {
pathEffect = null;
if (mType == TYPE_OVAL) {
mType = TYPE_RECT;
}
}
}
public void setPathEffect(PathEffect pathEffect) {
this.pathEffect = pathEffect;
postInvalidate();
}
private void initOffsetX(float offset) {
offsetX = offset;
isOffsetX = true;
}
private void initOffsetY(float offset) {
offsetY = offset;
isOffsetY = true;
}
private boolean setBoundPaintW(float width) {
boolean changeW = boundPaint == null || width != boundPaint.getStrokeWidth();
if (changeW) {
boundWidth = width;
initBoundPaint();
boundPaint.setStrokeWidth(width);
}
isBorder();
return changeW;
}
private boolean setBoundPaintC(int boundColor) {
boolean changeC = boundPaint == null || boundColor != boundPaint.getColor();
if (changeC) {
initBoundPaint();
boundPaint.setColor(boundColor);
}
isBorder();
return changeC;
}
private void isBorder() {
isBorder = boundPaint != null && (boundPaint.getStrokeWidth() > 0 || boundPaint.getColor() != Color.TRANSPARENT);
}
private void initBoundPaint() {
if (boundPaint == null) {
boundPaint = new Paint();
boundPaint.setAntiAlias(true);
boundPaint.setStyle(Paint.Style.STROKE);
}
}
/**
* 计算偏移
*/
/**
* 计算偏移
*/
private float calculateOffset(int view, float inner, float offset, Float percent, boolean isOffset) {
if (isOffset) {
if (percent != null) {
offset = (view - inner) * percent;
}
return offset;
} else {
return (view - offsetBound * 2 - inner) / 2f;
}
}
/**
* 计算内框宽/高
*/
private float calculateSize(float view, float inner, float marginStart, float marginEnd) {
if (inner == ViewGroup.LayoutParams.MATCH_PARENT) {
inner = view - offsetBound * 2 - marginStart - marginEnd;
}
return inner;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
isRecalculate = true;
recalculate();
}
private void recalculate() {
if (isRecalculate) {
isRecalculate = false;
width = getMeasuredWidth();
height = getMeasuredHeight();
offsetBound = isBorder ? boundWidth : 0;
innerWidth = calculateSize(width, innerWidth, getPaddingLeft(), getPaddingRight());
innerHeight = calculateSize(height, innerHeight, getPaddingTop(), getPaddingBottom());
if (isAdjust) {
innerWidth = Math.min(innerWidth, innerHeight);
innerHeight = innerWidth;
}
offsetX = calculateOffset(width, innerWidth, offsetX, offsetPercentX, isOffsetX);
offsetY = calculateOffset(height, innerHeight, offsetY, offsetPercentY, isOffsetY);
float offsetStart = offsetBound / 2f;
float offsetEnd = offsetBound * 1.5f;
innerBounds.set(offsetX + offsetStart,
offsetY + offsetStart,
offsetX + innerWidth + offsetEnd,
offsetY + innerHeight + offsetEnd);
setPath();
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
recalculate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
onDrawBackground(canvas);
onDrawBound(canvas);
}
private void onDrawBackground(Canvas canvas) {
canvas.save();
if (isClip) {
canvas.clipPath(innerPath, Region.Op.DIFFERENCE);
}
canvas.drawColor(backgroundColor);
canvas.restore();
}
private void onDrawBound(Canvas canvas) {
canvas.save();
if (isBorder && isClip) {
if (innerBoundStrokeType == TYPE_ONLY_CORNERS) {
float strokeWidth = boundPaint.getStrokeWidth();
canvas.clipRect(innerBounds.left - strokeWidth,
innerBounds.top + innerBoundSectionWidth,
innerBounds.right + strokeWidth,
innerBounds.bottom - innerBoundSectionWidth,
Region.Op.DIFFERENCE);
canvas.clipRect(innerBounds.left + innerBoundSectionWidth,
innerBounds.top - strokeWidth,
innerBounds.right - innerBoundSectionWidth,
innerBounds.bottom + strokeWidth,
Region.Op.DIFFERENCE);
}
if (pathEffect != null) {
boundPaint.setPathEffect(pathEffect);
}
canvas.drawPath(innerPath, boundPaint);
}
canvas.restore();
}
private void setPath() {
isClip = innerPath != null && innerBounds != null && !innerBounds.isEmpty();
if (isClip) {
innerPath.reset();
if (mType == TYPE_RECT) {// 绘制圆角矩形
innerPath.addRoundRect(innerBounds, mRadius, mRadius, Path.Direction.CW);
} else if (mType == TYPE_OVAL) {
// 绘制椭圆
innerPath.addOval(innerBounds, Path.Direction.CW);
}
}
}
}
attrs.xml
使用示例
效果示例
1、正方形镂空四角描边蒙版
圆形镂空实线描边蒙版