自定义镂空蒙版控件

自定义镂空蒙版控件

        • 蒙版控件
        • attrs.xml
        • 使用示例
        • 效果示例


蒙版控件

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、正方形镂空四角描边蒙版
自定义镂空蒙版控件_第1张图片
圆形镂空实线描边蒙版
自定义镂空蒙版控件_第2张图片

你可能感兴趣的:(自定义控件,canvas,裁剪,Android,镂空,自定义控件)