Android画不规则形状

时间:2022-03-08 20:33:30

经常会在移动应用中看到类似下图的各种图片:

Android画不规则形状

这样的图形在Android上要怎么实现呢?在Android系统中,目前主要有三种方式可以实现上图的形状,下面一一介绍。

一、PorterDuffXfermode方式

之前的博客曾经介绍过用这种方式画圆形头像,实际上,它不仅可以用来画圆形头像,还可以实现任意形状。首先来复习一下16中效果:

1.PorterDuff.Mode.CLEAR    所绘制不会提交到画布上
 2.PorterDuff.Mode.SRC     显示上层绘制图片
 3.PorterDuff.Mode.DST     显示下层绘制图片
 4.PorterDuff.Mode.SRC_OVER  正常绘制显示,上下层绘制叠盖。
 5.PorterDuff.Mode.DST_OVER  上下层都显示。下层居上显示。
 6.PorterDuff.Mode.SRC_IN    取两层绘制交集。显示上层。
 7.PorterDuff.Mode.DST_IN    取两层绘制交集。显示下层。
 8.PorterDuff.Mode.SRC_OUT   取上层绘制非交集部分。
 9.PorterDuff.Mode.DST_OUT   取下层绘制非交集部分。
 10.PorterDuff.Mode.SRC_ATOP  取下层非交集部分与上层交集部分
 11.PorterDuff.Mode.DST_ATOP  取上层非交集部分与下层交集部分
 12.PorterDuff.Mode.XOR     异或:去除两图层交集部分
 13.PorterDuff.Mode.DARKEN   取两图层全部区域,交集部分颜色加深
 14.PorterDuff.Mode.LIGHTEN   取两图层全部,点亮交集部分颜色
 15.PorterDuff.Mode.MULTIPLY  取两图层交集部分叠加后颜色
 16.PorterDuff.Mode.SCREEN    取两图层全部区域,交集部分变为透明色

我们的实现思路就是绘制两个图层,利用蒙版效果,取两个图形的交集,就可以实现上图的各种形状。所以,这里我们要用到属性SRC_IN或DST_IN,并需要两个图层,一个是形状图层,一个是显示图层,并且显示图层完全覆盖形状图层。那么,如果形状图层在,显示图层在上,就应该选择SRC_IN属性。

采用PorterDuffXfermode方式的源码如下:

public class IrregularShapeImageView extends android.support.v7.widget.AppCompatImageView {

    private Bitmap backgroundBitmap;

    private Bitmap mBitmap;

    private int viewWidth;

    private int viewHeight;


    public IrregularShapeImageView(Context context) {
        this(context, null, 0);
    }

    public IrregularShapeImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public IrregularShapeImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        viewWidth = MeasureSpec.getSize(widthMeasureSpec);
        viewHeight = MeasureSpec.getSize(heightMeasureSpec);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (mBitmap != null && backgroundBitmap != null) {

            int min = Math.min(viewWidth, viewHeight);
            backgroundBitmap = Bitmap.createScaledBitmap(backgroundBitmap, min, min, false);
            mBitmap = Bitmap.createScaledBitmap(mBitmap, min, min, false);

            canvas.drawBitmap(createImage(), 0, 0, null);
        }
    }

    private Bitmap createImage() {
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        Bitmap finalBmp = Bitmap.createBitmap(viewWidth, viewHeight, Bitmap.Config.ARGB_8888);

        Canvas canvas = new Canvas(finalBmp);

        canvas.drawBitmap(backgroundBitmap, 0, 0, paint);

        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

        canvas.drawBitmap(mBitmap, 0, 0, paint);
        return finalBmp;
    }

    @Override
    public void setImageBitmap(Bitmap bm) {
        super.setImageBitmap(bm);
        mBitmap = bm;
        setBitmaps();
    }

    @Override
    public void setImageDrawable(Drawable drawable) {
        super.setImageDrawable(drawable);
        mBitmap = getBitmapFromDrawable(drawable);
        setBitmaps();
    }

    @Override
    public void setImageResource(int resId) {
        super.setImageResource(resId);
        mBitmap = getBitmapFromDrawable(getDrawable());
        setBitmaps();
    }

    @Override
    public void setImageURI(Uri uri) {
        super.setImageURI(uri);
        mBitmap = getBitmapFromDrawable(getDrawable());
        setBitmaps();
    }

    private void setBitmaps() {
        if (null == getBackground()) {
            throw new IllegalArgumentException(String.format("background is null."));
        } else {
            backgroundBitmap = getBitmapFromDrawable(getBackground());
            invalidate();
        }
    }

    private Bitmap getBitmapFromDrawable(Drawable drawable) {
        super.setScaleType(ScaleType.CENTER_CROP);
        if (drawable == null) {
            return null;
        }
        if (drawable instanceof BitmapDrawable) {
            return ((BitmapDrawable) drawable).getBitmap();
        }
        try {
            Bitmap bitmap;
            bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
            Canvas canvas = new Canvas(bitmap);
            drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
            drawable.draw(canvas);
            return bitmap;
        } catch (OutOfMemoryError e) {
            return null;
        }
    }

二、BitmapShader方式

Google官方的说法:Shader used to draw a bitmap as a texture. The bitmap can be repeated or mirrored by setting the tiling mode.

意思就是把bitmap当做纹理来用,可以重复,镜像,其实还有一种,拉伸。

BitmapShader 的构造函数

public BitmapShader(@NonNull Bitmap bitmap, TileMode tileX, TileMode tileY)
三个参数:bitmap 指的是要作为纹理的图片,tileX 指的是在x方向纹理的绘制模式,tileY 指的是Y方向上的绘制模式。

使用BitmapShader实现不规则图形的源码如下:

public class MultiView extends android.support.v7.widget.AppCompatImageView {
    /**
     * 图片的类型,圆形or圆角or多边形
     */
    private Context mContext;

    /**
     * 传输类型
     */
    private int type;

    /**
     * 圆形
     */
    public static final int TYPE_CIRCLE = 0;

    /**
     * 圆角
     */
    public static final int TYPE_ROUND = 1;

    /**
     * 多边形
     */
    public static final int TYPE_MULTI = 3;

    /*
     * 外多边形
     */
    public static final int TYPE_MULTI2 = 4;

    /**
     * 默认多边形角的个数
     */
    public static final int ANGLECOUNT = 5;

    /**
     * 默认开始绘制的角度
     */
    public static final int CURRENTANGLE = 180;

    /**
     * 多边形的半径
     */
    private int startRadius;

    /**
     * 多边形角的个数
     */
    private int angleCount;

    private int[] angles;

    /**
     * 开始绘制的角度
     */
    private int currentAngle;

    /**
     * 存储角位置的集合
     */
    private List<PointF> pointFList = new ArrayList<>();

    /**
     * 圆角大小的默认值
     */
    private static final int BODER_RADIUS_DEFAULT = 10;

    /**
     * 圆角的大小
     */
    private int mBorderRadius;

    /**
     * 绘图的Paint
     */
    private Paint mBitmapPaint;

    /**
     * 圆角的半径
     */
    private int mRadius;

    /**
     * 3x3 矩阵,主要用于缩小放大
     */
    private Matrix mMatrix;

    /**
     * 渲染图像,使用图像为绘制图形着色
     */
    private BitmapShader mBitmapShader;

    /**
     * view的宽度
     */
    private int mWidth;
    private RectF mRoundRect;

    public MultiView(Context context) {
        this(context, null);
    }

    public MultiView(Context context, AttributeSet attrs) {

        this(context, attrs, 0);

    }

    public MultiView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        this.mContext = context;

        init(context, attrs);

    }

    public void init(Context context, AttributeSet attrs) {
        mMatrix = new Matrix();
        mBitmapPaint = new Paint();
        mBitmapPaint.setAntiAlias(true);

        TypedArray typedArray = context.obtainStyledAttributes(attrs,
                R.styleable.RoundImageView);

        mBorderRadius = typedArray.getDimensionPixelSize(
                R.styleable.RoundImageView_borderRadius, (int) TypedValue
                        .applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                                BODER_RADIUS_DEFAULT, getResources()
                                        .getDisplayMetrics()));// 默认为10dp
        type = typedArray.getInt(R.styleable.RoundImageView_type, TYPE_CIRCLE);// 默认为Circle
        angleCount = typedArray.getInt(R.styleable.RoundImageView_angleCount, ANGLECOUNT);
        currentAngle = typedArray.getInt(R.styleable.RoundImageView_currentAngle, currentAngle);

        typedArray.recycle(); //回收之后对象可以重用
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        /**
         * 如果类型是圆形或多边形,则强制改变view的宽高一致,以小值为准
         */
        if (type == TYPE_CIRCLE) {
            mWidth = Math.min(getMeasuredWidth(), getMeasuredHeight());
            mRadius = mWidth / 2;
            setMeasuredDimension(mWidth, mWidth);
        }

        if (type == TYPE_MULTI || type == TYPE_MULTI2) {
            mWidth = Math.min(getMeasuredWidth(), getMeasuredHeight());

            setMeasuredDimension(mWidth, mWidth);

            angles = new int[angleCount];

            for (int i = 0; i < angleCount; i++) {
                int partOfAngle = 360 / angleCount; //每个顶点的角度
                angles[i] = currentAngle + partOfAngle * i;

                startRadius = mWidth / 2;
                float x = (float) (Math.sin(Math.toRadians(angles[i])) * startRadius);
                float y = (float) (Math.cos(Math.toRadians(angles[i])) * startRadius);
                pointFList.add(new PointF(x, y));
            }
        }

    }

    /**
     * 初始化BitmapShader
     */
    private void setUpShader() {
        Drawable drawable = getDrawable();
        if (drawable == null) {
            return;
        }

        Bitmap bmp = drawableToBitamp(drawable);
        // 将bmp作为着色器,就是在指定区域内绘制bmp
        mBitmapShader = new BitmapShader(bmp, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
        float scale = 1.0f;
        if (type == TYPE_CIRCLE) {
            // 拿到bitmap宽或高的小值
            int bSize = Math.min(bmp.getWidth(), bmp.getHeight());
            scale = mWidth * 1.0f / bSize;

        } else if (type == TYPE_ROUND) {
            if (!(bmp.getWidth() == getWidth() && bmp.getHeight() == getHeight())) {
                // 如果图片的宽或者高与view的宽高不匹配,计算出需要缩放的比例;缩放后的图片的宽高,一定要大于我们view的宽高;所以我们这里取大值;
                scale = Math.max(getWidth() * 1.0f / bmp.getWidth(), getHeight() * 1.0f / bmp.getHeight());
            }

        } else if (type == TYPE_MULTI || type == TYPE_MULTI2) {
            // 拿到bitmap宽或高的小值
            int bSize = Math.min(bmp.getWidth(), bmp.getHeight());
            scale = mWidth * 1.0f / bSize;
        }
        // shader的变换矩阵,我们这里主要用于放大或者缩小
        mMatrix.setScale(scale, scale);

        // 设置变换矩阵
        mBitmapShader.setLocalMatrix(mMatrix);
        // 设置shader
        mBitmapPaint.setShader(mBitmapShader);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (getDrawable() == null) {
            return;
        }
        setUpShader();

        if (type == TYPE_ROUND) {
            canvas.drawRoundRect(mRoundRect, mBorderRadius, mBorderRadius,
                    mBitmapPaint);
        } else if (type == TYPE_MULTI) {
            //canvas.translate(startRadius,startRadius);

            Path mPath = drawPath();

            canvas.drawPath(mPath, mBitmapPaint);
        } else if (type == TYPE_MULTI2) {
            Path mPath = drawPath2();

            canvas.drawPath(mPath, mBitmapPaint);
        } else {
            canvas.drawCircle(mRadius, mRadius, mRadius, mBitmapPaint);
        }
    }

    /**
     * @return 多边形路径
     */
    private Path drawPath2() {
        Path mPath = new Path();
        mPath.moveTo(pointFList.get(0).x, pointFList.get(0).y);
        for (int i = 1; i < angleCount; i++) {
            mPath.lineTo(pointFList.get(i).x, pointFList.get(i).y);
        }

        mPath.offset(startRadius, startRadius);
        return mPath;
    }

    /**
     * @return 多边形路径
     */
    private Path drawPath() {
        Path mPath = new Path();
        mPath.moveTo(pointFList.get(0).x, pointFList.get(0).y);
        for (int i = 2; i < angleCount; i++) {
            if (i % 2 == 0) {// 除以二取余数,余数为0则为偶数,否则奇数
                mPath.lineTo(pointFList.get(i).x, pointFList.get(i).y);
            }

        }

        if (angleCount % 2 == 0) {  //偶数,moveTo
            mPath.moveTo(pointFList.get(1).x, pointFList.get(1).y);
        } else {                    //奇数,lineTo
            mPath.lineTo(pointFList.get(1).x, pointFList.get(1).y);
        }

        for (int i = 3; i < angleCount; i++) {
            if (i % 2 != 0) {
                mPath.lineTo(pointFList.get(i).x, pointFList.get(i).y);
            }
        }

        mPath.offset(startRadius, startRadius);
        return mPath;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        // 圆角图片的范围
        if (type == TYPE_ROUND)
            mRoundRect = new RectF(0, 0, w, h);
    }

    /**
     * drawable转bitmap
     *
     * @param drawable
     * @return
     */
    private Bitmap drawableToBitamp(Drawable drawable) {
        if (drawable instanceof BitmapDrawable) {
            BitmapDrawable bd = (BitmapDrawable) drawable;
            return bd.getBitmap();
        }
        int w = drawable.getIntrinsicWidth();
        int h = drawable.getIntrinsicHeight();
        Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, w, h);
        drawable.draw(canvas);
        return bitmap;
    }

    private static final String STATE_INSTANCE = "state_instance";
    private static final String STATE_TYPE = "state_type";
    private static final String STATE_BORDER_RADIUS = "state_border_radius";

    @Override
    protected Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();
        bundle.putParcelable(STATE_INSTANCE, super.onSaveInstanceState());
        bundle.putInt(STATE_TYPE, type);
        bundle.putInt(STATE_BORDER_RADIUS, mBorderRadius);
        return bundle;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (state instanceof Bundle) {
            Bundle bundle = (Bundle) state;
            super.onRestoreInstanceState(((Bundle) state)
                    .getParcelable(STATE_INSTANCE));
            this.type = bundle.getInt(STATE_TYPE);
            this.mBorderRadius = bundle.getInt(STATE_BORDER_RADIUS);
        } else {
            super.onRestoreInstanceState(state);
        }

    }

    public void setType(int type) {
        if (this.type != type) {
            this.type = type;
            if (this.type != TYPE_ROUND && this.type != TYPE_CIRCLE && this.type != TYPE_MULTI && this.type != TYPE_MULTI2) {
                this.type = TYPE_CIRCLE;
            }
            requestLayout();
        }

    }

}

自定义属性如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="RoundImageView">
        <attr name="borderRadius" format="dimension" />
        <attr name="type">
            <enum name="circle" value="0" />
            <enum name="round" value="1" />
            <enum name="multi" value="3" />
            <enum name="multi2" value="4" />
        </attr>
        <attr name="angleCount" format="integer" />
        <attr name="currentAngle" format="integer" />
    </declare-styleable>
</resources>

三、ClipPath方式

Canvas类的ClipPath方法:

boolean    clipPath(Path path)
Intersect the current clip with the specified path.

clipPath方法的作用是“切割画布”。利用这个特性,也可以实现不规则形状。比如,实现一个三角形的代码如下:

public class TriangleView extends android.support.v7.widget.AppCompatImageView {
    private Context mContext;
    private int mWidth;
    private int mHeight;

    // 三角形的模式以直角点所在的点位置为模式名
    public static final int TYPE_LEFT_TOP = 0;
    public static final int TYPE_RIGHT_BOTTOM = 1;
    public static final int TYPE_RIGHT_TOP = 2;
    public static final int TYPE_RIGHT_MIDDLE = 3;
    public static final int TYPE_LEFT_BOTTOM = 4;
    public static final int TYPE_RIGHT_BOTTOM_SMAILL = 5;

    private int mMode;
    private int mColor;
    private String mText;
    // 三个点的顺序,从leftTop开始计算,顺时针数过去,依次三个点
    private int mPointOne[];
    private int mPointTwo[];
    private int mPointThree[];

    private Path mPath;
    private Path mTextPath;
    private Paint mPaint;

    private boolean isClicked = false; //是否被按下

    public TriangleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        mPath = new Path();
        mTextPath = new Path();
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mColor = Color.parseColor("#FFFFFF");
        mPaint.setColor(mColor);
    }

    public void setMode(int model) {
        mMode = model;
        postInvalidate();
    }

    public void setColor(int color) {
        mColor = color;
        postInvalidate();
    }
    public void setText(String text) {
        mText = text;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.clipPath(mPath,INTERSECT);
        super.onDraw(canvas);
        if(!isClicked) {
            mPaint.setColor(Color.argb(180, 0, 0, 0));
            canvas.drawPath(mPath, mPaint);
        }
        if(mText != null) {
            mPaint.setFakeBoldText(true);
            mPaint.setTextSize(sp2px(mContext,13));
            mPaint.setColor(mColor);
            float tw = mPaint.measureText(mText);
            float th = mPaint.measureText(mText)/mText.length();
            switch (mMode) {
                case TYPE_LEFT_TOP:
                    canvas.drawText(mText, (mWidth - tw) / 5f, mHeight / 3f, mPaint);
                    break;
                case TYPE_RIGHT_BOTTOM:
                    canvas.drawText(mText, (mWidth - tw)*4/5f, mHeight*4/5f, mPaint);
                    break;
                case TYPE_RIGHT_TOP:
                    canvas.drawText(mText, (mWidth - tw)*3/4f, mHeight/3f, mPaint);
                    break;
                case TYPE_RIGHT_MIDDLE:
                    canvas.drawText(mText, (mWidth*0.92f - tw)/2f, (mHeight+th)/2f, mPaint);
                    break;
                case TYPE_LEFT_BOTTOM:
                    mTextPath.moveTo((1.4f*mHeight-tw-2*th)/2.8f,(1.4f*mHeight-tw+2*th)/2.8f);
                    mTextPath.lineTo((1.4f*mHeight+tw-2*th)/2.8f,(1.4f*mHeight+tw+2*th)/2.8f);
                    canvas.drawTextOnPath(mText,mTextPath,0, 0, mPaint);
                    break;
                case TYPE_RIGHT_BOTTOM_SMAILL:
                    mTextPath.moveTo((1.4f*mHeight-tw+2*th)/2.8f,(1.4f*mHeight+tw+2*th)/2.8f);
                    mTextPath.lineTo((1.4f*mHeight+tw+2*th)/2.8f,(1.4f*mHeight-tw+2*th)/2.8f);
                    canvas.drawTextOnPath(mText,mTextPath,0, 0, mPaint);
                    break;
            }
        }
    }

    @Override
    protected void onSizeChanged(int width, int height, int oldw, int oldh) {
        super.onSizeChanged(width, height, oldw, oldh);
        mWidth = width;
        mHeight = height;

        switch (mMode) {
            case TYPE_LEFT_TOP:
                mPointOne = new int[] { 0, 0 };
                mPointTwo = new int[] { width, 0 };
                mPointThree = new int[] { 0, height };
                break;
            case TYPE_RIGHT_BOTTOM:
                mPointOne = new int[] { width, 0 };
                mPointTwo = new int[] { width, height };
                mPointThree = new int[] { 0, height };
                break;
            case TYPE_RIGHT_TOP:
                mPointOne = new int[] { 0, 0 };
                mPointTwo = new int[] { width, 0 };
                mPointThree = new int[] { width, height };
                break;
            case TYPE_RIGHT_MIDDLE:
                mPointOne = new int[] { 0, 0 };
                mPointTwo = new int[] { width, height / 2 };
                mPointThree = new int[] { 0, height };
                break;
            case TYPE_LEFT_BOTTOM:
                mPointOne = new int[] { 0, 0 };
                mPointTwo = new int[] { width, height};
                mPointThree = new int[] { 0, height };
                break;
            case TYPE_RIGHT_BOTTOM_SMAILL:
                mPointOne = new int[] { width, 0 };
                mPointTwo = new int[] { width, height };
                mPointThree = new int[] { 0, height };
                break;
        }

        if (null != mPointOne) {
            mPath.moveTo(mPointOne[0], mPointOne[1]);
            mPath.lineTo(mPointTwo[0], mPointTwo[1]);
            mPath.lineTo(mPointThree[0], mPointThree[1]);
            mPath.close();
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        RectF bounds = new RectF();
        mPath.computeBounds(bounds, true);
        Region region = new Region();
        region.setPath(mPath, new Region((int)bounds.left, (int)bounds.top,(int)bounds.right, (int)bounds.bottom));
        boolean ct =  region.contains((int)event.getX(), (int)event.getY());
        if(event.getAction() == MotionEvent.ACTION_DOWN){
            if(ct){
                isClicked = true;
                invalidate();
                return true;
            }
            return false;
        }else if(event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL){
            if(isClicked){
                isClicked = false;
                invalidate();
                if (null != mOnClickListener && ct && event.getAction() != MotionEvent.ACTION_CANCEL) {
                    mOnClickListener.onClick(this);
                    return true;
                }
            }
            return false;
        }else if(event.getAction() == MotionEvent.ACTION_MOVE){
            return isClicked;
        }
        return super.onTouchEvent(event);
    }

    private OnClickListener mOnClickListener;
    public void setOnViewClickListener(OnClickListener clickListener){
        mOnClickListener = clickListener;
    }

    //计算两点的距离
    private int distance(PointF point1, PointF point2) {
        int disX = (int) Math.abs(point1.x - point2.x);
        int disY = (int) Math.abs(point1.y - point2.y);
        return (int) Math.sqrt(Math.pow(disX, 2) + Math.pow(disY, 2));
    }

    //将sp值转换为px值,保证文字大小不变
    public static int sp2px(Context context, float spValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (spValue * fontScale + 0.5f);
    }
}

以上三种方式的完整代码:https://github.com/gengqifu/Irregular-View