三种方式实现圆角或圆形图片的自定义View

前言
实现圆角或圆形图片显示,我们开发中除了把原图直接做成圆角外,常见有三种方式实现:
使用Xfermode混合图层;
使用BitmapShader;
通过裁剪画布区域实现指定形状的图形(ClipPath)。
今天我就来带大家通过上面三种方式实现圆角或圆形的自定义View。

先来张效果图:
在这里插入图片描述

实现
1. 自定义属性

<attr name="borderRadius" format="dimension" />
    <attr name="type">
        <enum name="circle" value="0" />
        <enum name="round" value="1" />
    </attr>
    <attr name="src" format="reference"/>
    <declare-styleable name="XfermodeCircleView">
        <attr name="borderRadius" />
        <attr name="type" />
        <attr name="src" />
    </declare-styleable>

属性说明:
type: 圆角或圆形
src: 图片资源Id(这里使用的资源图片作为案例)
borderRadius: 圆角半径大小

2. 自定义View
我这里自定义了三个View, 大部分代码一致的,就是在onDraw方法实现不一样, 所以我这里就不重复贴出来了。
XfermodeCircleView、ClipPathCircleView 继承的View
BitmapShaderView 继承的ImageView

属性

private static final int TYPE_CIRCLE = 0;
    private static final int TYPE_ROUND = 1;

    private int mRadius;                //半径大小
    private int mBorderRadius;          //圆角大小
    private int mWidth, mHeight;        //控件宽高
    private int mType;                  //圆角还是圆
    private RectF mRoundRect;           // 圆角矩阵
    private BitmapShader mBitmapShader; //Shader
    private Paint mBitmapPaint;         //图片画笔
    private Matrix mMatrix;             //Bitmap缩放矩阵

构造函数

public XfermodeCircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.XfermodeCircleView, defStyleAttr, 0);
        this.mSrc = BitmapFactory.decodeResource(getResources(), a.getResourceId(R.styleable.XfermodeCircleView_src, 0));
        this.mType = a.getInt(R.styleable.XfermodeCircleView_type, 0);
        this.mBorderRadius = a.getDimensionPixelSize(R.styleable.XfermodeCircleView_borderRadius, 10);
        a.recycle();
        init();
    }

onMeasure方法

XfermodeCircleView、ClipPathCircleView

int specMode = MeasureSpec.getMode(widthMeasureSpec);
        int specSize = MeasureSpec.getSize(widthMeasureSpec);
        //设置宽度
        if (specMode == MeasureSpec.EXACTLY) {       //match_parent
            this.mWidth = specSize;
        } else {
            int imgWidth = this.getPaddingLeft() + this.mSrc.getWidth() + this.getPaddingRight();
            if (specMode == MeasureSpec.AT_MOST) {   //wrap_content
                this.mWidth = this.mHeight = Math.min(imgWidth, specSize);
            } else {
                this.mWidth = imgWidth;
            }
        }
        specMode = MeasureSpec.getMode(heightMeasureSpec);
        specSize = MeasureSpec.getSize(heightMeasureSpec);

        //设置高度
        if (specMode == MeasureSpec.EXACTLY) {
            this.mHeight = specSize;
        } else {
            int imgHeight = this.getPaddingLeft() + this.mSrc.getHeight() + this.getPaddingRight();
            if (specMode == MeasureSpec.AT_MOST) {
                this.mHeight = Math.min(imgHeight, specSize);
            } else {
                this.mHeight = imgHeight;
            }
        }
        this.setMeasuredDimension(this.mWidth, this.mHeight);

BitmapShaderView

super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (TYPE_CIRCLE == this.mType) {  //如果宽高不一致,强制使用较小边
            this.mWidth = this.mHeight = Math.min(this.getMeasuredWidth(), this.getMeasuredHeight());
            this.mRadius = this.mWidth / 2;
            this.setMeasuredDimension(this.mWidth, this.mWidth);
        }

onDraw方法

XfermodeCircleView

protected void onDraw(Canvas canvas) {
        if (null == this.mSrc) return;
        switch (this.mType) {
            case TYPE_CIRCLE:
                int size = Math.min(this.mWidth, this.mHeight);
                this.mSrc = Bitmap.createScaledBitmap(this.mSrc, size, size, false);
                canvas.drawBitmap(createCircleImageBitmap(this.mSrc, size), 0, 0, null);
                break;
            case TYPE_ROUND:
                canvas.drawBitmap(createRoundCornerImage(this.mSrc), 0, 0, null);
                break;
        }
//        super.onDraw(canvas);
    }
    private Bitmap createCircleImageBitmap(Bitmap source, int size) {
        final Paint paint = new Paint();
        paint.setAntiAlias(true);
        Bitmap target = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(target);
        canvas.drawCircle(size / 2, size / 2, size / 2, paint);
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        canvas.drawBitmap(source, 0, 0, paint);
        return target;
    }
    private Bitmap createRoundCornerImage(Bitmap source) {
        final Paint paint = new Paint();
        paint.setAntiAlias(true);
        Bitmap target = Bitmap.createBitmap(this.mWidth, this.mHeight, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(target);
        canvas.drawRoundRect(this.mRoundRect, this.mBorderRadius, this.mBorderRadius, paint);
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        canvas.drawBitmap(source, 0, 0, paint);
        return target;
    }

ClipPathCircleView

protected void onDraw(Canvas canvas) {
        if (null == this.mSrc) return;
        switch (this.mType) {
            case TYPE_CIRCLE:
                int size = Math.min(this.mWidth, this.mHeight);
                this.mPath.reset();
                this.mPath.addCircle(size / 2, size / 2, size / 2, Path.Direction.CCW);
                canvas.clipPath(this.mPath);
                this.mSrc = Bitmap.createScaledBitmap(this.mSrc, size, size, false);
                canvas.drawBitmap(this.mSrc, 0, 0, this.mBitmapPaint);
                break;
            case TYPE_ROUND:
                this.mPath.reset();
                this.mPath.addRoundRect(this.mRectF, this.mBorderRadius, this.mBorderRadius, Path.Direction.CCW);
                canvas.clipPath(this.mPath);
                this.mSrc = Bitmap.createScaledBitmap(this.mSrc, this.getWidth(), this.getHeight(), false);
                canvas.drawBitmap(this.mSrc, 0, 0, this.mBitmapPaint);
                break;
        }
//        super.onDraw(canvas);
    }

BitmapShaderView

protected void onDraw(Canvas canvas) {
//        super.onDraw(canvas);
        if (null == this.getDrawable()) return;
        this.setShader();
        if (TYPE_CIRCLE == this.mType) {
            canvas.drawCircle(this.mRadius, this.mRadius, this.mRadius, this.mBitmapPaint);
        } else {
            canvas.drawRoundRect(this.mRoundRect, this.mBorderRadius, this.mBorderRadius, this.mBitmapPaint);
        }
    }
    private void setShader() {
        Drawable drawable = this.getDrawable();
        if (null == drawable) return;
        Bitmap bmp = drawable2Bitmap(drawable);
        this.mBitmapShader = new BitmapShader(bmp, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
        float scale = 1.0f;
        if (TYPE_CIRCLE == this.mType) {
            int bmpSize = Math.min(bmp.getWidth(), bmp.getHeight());
            scale = this.mWidth * 1.0f / bmpSize;
        } else if (TYPE_ROUND == this.mType) {
            scale = Math.max(this.getWidth() * 1.0f / bmp.getWidth(), this.getHeight() * 1.0f / bmp.getHeight());
        }
        this.mMatrix.setScale(scale, scale);
        this.mBitmapShader.setLocalMatrix(this.mMatrix);
        this.mBitmapPaint.setShader(this.mBitmapShader);
    }
    /**
     * Drawable 转 Bitmap
     *
     * @param drawable
     * @return
     */
    private Bitmap drawable2Bitmap(Drawable drawable) {
        if (drawable instanceof BitmapDrawable) {
            BitmapDrawable bd = (BitmapDrawable) drawable;
            return bd.getBitmap();
        }
        int w = drawable.getIntrinsicWidth();
        int h = drawable.getIntrinsicHeight();
        Bitmap bmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bmp);
        drawable.setBounds(0, 0, w, h); //这里必须设置Bounds,否则无效果
        drawable.draw(canvas);
        return bmp;
    }

onSizeChange

@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (TYPE_ROUND == this.mType) {
            this.mRoundRect = new RectF(0, 0, this.getWidth(), this.getHeight());
        }
    }

onSaveInstanceState、onRestoreInstanceState

@Override
    protected Parcelable onSaveInstanceState() {
        Parcelable superData = super.onSaveInstanceState();
        Bundle bundle = new Bundle();
        bundle.putParcelable("super_data", superData);
        bundle.putInt("type", this.mType);
        bundle.putInt("border_radius", this.mBorderRadius);
        return bundle;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (state instanceof Bundle) {
            Bundle bundle = (Bundle) state;
            Parcelable superData = bundle.getParcelable("super_data");
            this.mType = bundle.getInt("type", TYPE_CIRCLE);
            this.mBorderRadius = bundle.getInt("border_radius", this.dp2px(10));
            super.onRestoreInstanceState(superData);
        } else
            super.onRestoreInstanceState(state);
    }

设置圆角半径和类型

/**
 * 动态设置圆角半径
 *
 * @param borderRadius
 */
public void setBorderRadius(int borderRadius) {
    int pxVal = this.dp2px(borderRadius);
    if (this.mBorderRadius != pxVal) {
        this.mBorderRadius = pxVal;
        this.invalidate();
    }
}

/**
 * 设置圆角类型
 *
 * @param type
 */
public void setType(int type) {
    if (this.mType != type) {
        this.mType = type;
        if (this.mType != TYPE_ROUND && this.mType != TYPE_CIRCLE) {
            this.mType = TYPE_CIRCLE;
        }
        this.requestLayout();
    }
}

private int dp2px(int dp) {
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, this.getResources().getDisplayMetrics());
}

结束完成…

补充:最后贴一下Glide4.+ 实现方式配置:

/**
     * 加载圆角图片
     *
     * @param image
     * @param view
     */
    public static void loadRound(String image, ImageView view, int borderRadius) {
        //设置图片圆角角度
        RoundedCorners roundedCorners = new RoundedCorners(borderRadius);
        //通过RequestOptions扩展功能,override:采样率,因为ImageView就这么大,可以压缩图片,降低内存消耗
        RequestOptions options = RequestOptions.bitmapTransform(roundedCorners).override(300, 300);
        Glide.with(BaseApp.getInstance()).load(image).apply(options).into(view);
    }
/**
     * 加载圆形图片
     *
     * @param image
     * @param view
     */
    public static void loadRound(String image, ImageView view, int width, int height, int borderRadius) {
        //设置图片圆角角度
        RoundedCorners roundedCorners = new RoundedCorners(borderRadius);
        //通过RequestOptions扩展功能,override:采样率,因为ImageView就这么大,可以压缩图片,降低内存消耗
        RequestOptions options = RequestOptions.bitmapTransform(roundedCorners).override(width, height);
        Glide.with(BaseApp.getInstance()).load(image).apply(options).into(view);
    }

总结 三种方式实现自定义圆角或圆形View基本代码在这里了,把上面的属性、方法、自定义属性组合起来就是三种自定义View,代码还需要很多优化的(如: 怎么加载网络图片, onDetachedFromWindow 中Bitmap的回收等等),这里主要通过这三个案例熟悉自定义基本过程。