Android画圆形图片,clippath方式/Xfermode方式

1.利用canvas.clipPath方法,按照自定义的Path图形去切割控件

ImageView显示图片,底层是通过Canvas将我们的图片资源画到View控件上实现的;
因此,要让其显示圆形图片,只需要对Canvas进行相应的变化,比如切割圆形、绘制圆形。

方法1:

描述:定义一个控件,包含ImageView,定义圆形Path路径,然后调用canvas.clipPath,将图片切割成圆形
缺点:锯齿

代码

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;

import android.util.AttributeSet;
import android.view.View;
import android.widget.AbsoluteLayout;

public class CircleImage extends AbsoluteLayout { // 也可以继承其他的父类
    private int mWidth = 0;
    private int mHeight = 0;
    private Paint mPaint;
    private Path mPath;

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

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

    public CircleImage(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public CircleImage(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initView();
      /*
      ViewGroup默认情况下,出于性能考虑,会被设置成WILL_NOT_DROW,这样,ondraw就不会被执行了。
      如果需要重写一个ViewGroup的onDraw方法,有两种方法:
      1. 构造函数中,给ViewGroup设置一个颜色。
      2. 构造函数中,调用setWillNotDraw(false),去掉其WILL_NOT_DRAW flag。
      */
        setWillNotDraw(false);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        mWidth = getWidth();
        mHeight = getHeight();
    }


    private void initView() {
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setAntiAlias(true);
        mPath = new Path();
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.addCircle(mWidth / 2, mHeight / 2, mWidth / 2, Path.Direction.CCW);
        canvas.clipPath(mPath);
    }
}

xml中定义

<xxx.CircleImage
    android:layout_width="500px"
    android:layout_height="500px"
    android:layout_x="0px"
    android:layout_y="0px"
>
    <ImageView
    android:layout_x="0px"
    android:layout_y="0px"
    android:layout_width="500px"
    android:layout_height="500px"
    android:background="@drawable/xxxpic"
    >
    </ImageView>

</xxx.CircleImage>

// ImageView中还可以设置src
// android:src="@drawable/xxxpic",但需要设置android:scaleType="centerCrop"
// 不然图片不会跟着窗口大小变化

方法2:

继承AppCompatImageView/ImageView自定义圆形控件
* canvas.clipPath:不支持硬件加速,所以在使用前需要禁止硬件加速
* setLayerType(View.LAYER_TYPE_SOFTWARE, null)
* clipPath要在super.onDraw方法前,调用,否则无效(canvas已经被设置给View了)
* 在onSizeChanged方法中,获取宽高
* 该这方法剪裁的是Canvas图形,View的实际形状是不变的,因此只能对src属性有效,对background属性是无效的

代码:

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Path;

import android.util.AttributeSet;
import android.content.res.TypedArray;

import android.support.v7.widget.AppCompatImageView;
import android.graphics.RectF;

public class CircleImage extends AppCompatImageView { // 也可以继承ImageView做

    private RectF mRect;
    private Path mPath;
    private float mRoundRadius;

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

    public CircleImage(Context context, AttributeSet attrs) {
        this(context, attrs, -1);
    }

    public CircleImage(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        getAttributes(context, attrs);
        initView(context);
    }

    private void initView(Context context) {
        mRect = new RectF();
        mPath = new Path();
        setLayerType(LAYER_TYPE_SOFTWARE, null);        // 禁用硬件加速
    }

    private void getAttributes(Context context, AttributeSet attrs) {
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CircleImage);
        mRoundRadius = ta.getInteger(R.styleable.CircleImage_roundRadius, -1);
        ta.recycle();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (mRoundRadius < 0) {
            clipCircle(w, h);
        } else {
            clipRoundRect(w, h);
        }
    }

    /**
     * 圆角
     */
    private void clipRoundRect(int width, int height) {
        mRect.left = 0;
        mRect.top = 0;
        mRect.right = width;
        mRect.bottom = height;
        mPath.addRoundRect(mRect, mRoundRadius, mRoundRadius, Path.Direction.CW);
    }

    /**
     * 圆形
     */
    private void clipCircle(int width, int height) {
        int radius = Math.min(width, height)/2;
        mPath.addCircle(width/2, height/2, radius, Path.Direction.CW);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.clipPath(mPath);
        super.onDraw(canvas);
    }
}

attrs.xml

<declare-styleable name="CircleImage">
    <attr name="roundRadius" format="integer"/>
</declare-styleable>

画面xml

<CircleImage
    android:layout_width="500px"
    android:layout_height="500px"
    android:layout_x="0px"
    android:layout_y="0px"
    android:src="@drawable/image_car"
    android:scaleType="centerCrop"
    app:roundRadius="10"
>

</CircleImage>

解决锯齿的方法

1、通过Paint设置 
Paint mPaint = new Paint(); 
mPaint.setFilterBitmap(true);

2、通过Canvas设置
canvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG|Paint.FILTER_BITMAP_FLAG)); 

不过这种方式不能完全解决圆形图片的锯齿

2.通过Xfermode方式实现

方法1:

自定义控件,包含一个ImageView对象,只对src属性设置的图片有效,background无效
该方法的注意事项就是:控件的大小和子child(ImageView)大小必须一致,可以不是正方形,原因是由于画完圆后,mImageView.setImageBitmap(roundBitmap);也需要设置一个圆盖上去,如果大小不一致,两个圆位置不一样,该问题待解决

代码:

package xxx;

import android.content.Context;

import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.graphics.Paint;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.PorterDuffXfermode;
import android.graphics.PorterDuff;
import android.graphics.Rect;

import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.widget.AbsoluteLayout;
import android.widget.ImageView;

public class CircleImage extends AbsoluteLayout {
    private int mWidth = 100;
    private int mHeight = 100;
    private Paint mPaint;
    private String TAG = "PKG_UICTRL_CircleImage";
    private ImageView mImageView = null;

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

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

    public CircleImage(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public CircleImage(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setFilterBitmap(true);
        mPaint.setDither(true);
        setWillNotDraw(false);
    }


    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        Log.d(TAG, "onFinishInflate: ");
        getView();

    }

    private void getView() {
        Log.d(TAG, " getView iCount="+getChildCount());
        for (int iCount = 0; iCount < getChildCount(); ++iCount) {
            View child = this.getChildAt(iCount);
            if (child instanceof ImageView) {
                Log.d(TAG, " ImageView");
                mImageView=(ImageView)child;
            }
        }
    }

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

        Drawable drawable = mImageView.getDrawable();
        if (null != drawable && (drawable instanceof BitmapDrawable)) {
            Bitmap b = ((BitmapDrawable) drawable).getBitmap();
            Bitmap bitmap = b.copy(Bitmap.Config.ARGB_8888, true);

            mWidth = getWidth();
            mHeight = getHeight();
             // 计算显示圆形的半径,为保证圆形,取图片的长宽小的一半作为圆形
            int radius = (mWidth < mHeight ? mWidth : mHeight) / 2;
            // 获取我们处理后的圆形图片
            Bitmap roundBitmap = getCroppedCircleBitmap(bitmap, radius);
            // 绘制图片进行显示
            canvas.drawBitmap(roundBitmap, mWidth / 2 - radius, mHeight / 2 - radius, null);

            mImageView.setImageBitmap(roundBitmap);
        }
        else {
            super.onDraw(canvas);
        }
    }

    public Bitmap getCroppedCircleBitmap(Bitmap bmp, int radius) {
        Bitmap scaledSrcBmp;
        int diameter = radius * 2;
        // 对图片进行处理,获取我们需要的中央部分
        Bitmap squareBitmap = getCenterBitmap(bmp);

        // 将图片缩放到需要的圆形比例大小
        if (squareBitmap.getWidth() != diameter || squareBitmap.getHeight() != diameter) {
            scaledSrcBmp = Bitmap.createScaledBitmap(squareBitmap, diameter, diameter, true);
        } else {
            scaledSrcBmp = squareBitmap;
        }
        // 创建一个我们输出的对应
        Bitmap output = Bitmap.createBitmap(scaledSrcBmp.getWidth(),
                scaledSrcBmp.getHeight(),
                Bitmap.Config.ARGB_8888);
        // 在output上进行绘画
        Canvas canvas = new Canvas(output);
        // 创建裁剪的矩形
        Rect rect = new Rect(0, 0, scaledSrcBmp.getWidth(),scaledSrcBmp.getHeight());

        // 绘制dest目标区域
        canvas.drawCircle(scaledSrcBmp.getWidth() / 2,
                scaledSrcBmp.getHeight() / 2,
                scaledSrcBmp.getWidth() / 2,
                mPaint);

        // 设置xfermode模式
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

        // 绘制src源区域
        canvas.drawBitmap(scaledSrcBmp, rect, rect, mPaint);

        bmp.recycle();
        squareBitmap.recycle();
        scaledSrcBmp.recycle();
        return output;
    }

    // 注意这里必须只能用bitmap.getWidth的图片,去算尺寸,不然外面的框,大于bitmap的大小,超出的就变成了黑色的
    private Bitmap getCenterBitmap(Bitmap bitmap){
        // 为了防止图片宽高不相等,造成圆形图片变形,因此截取长方形中处于中间位置最大的正方形图片  
        int bmpWidth = bitmap.getWidth();
        int bmpHeight = bitmap.getHeight();
        Bitmap squareBitmap;
        if (bmpHeight > bmpWidth) {
            // 截取正方形图片  ,从(bmpHeight - bmpWidth) / 2处开始截取
            squareBitmap = Bitmap.createBitmap(bitmap, 0, (bmpHeight - bmpWidth) / 2, bmpWidth, bmpWidth);
        } else if (bmpHeight < bmpWidth) {
            squareBitmap = Bitmap.createBitmap(bitmap, (bmpWidth - bmpHeight) / 2, 0, bmpHeight, bmpHeight);
        } else {
            squareBitmap = bitmap;
        }
        return squareBitmap;
    }
}

画面xml:

<CircleImage
    android:layout_width="500px"
    android:layout_height="500px"
    android:layout_x="0px"
    android:layout_y="0px"
>
    <ImageView
    android:layout_x="0px"
    android:layout_y="0px"
    android:layout_width="500px"
    android:layout_height="500px"
    android:src="@drawable/image_car_n"
    >
    </ImageView>

</CircleImage>

方法2:

自定义控件,继承ImageView,该控件本身就可以获得Drawable(this.getDrawable()),不需要像方法1,拿到mImageView,利用mImageView拿到Drawable(mImageView.getDrawable())

注意:

这两种只适合src设置的图片,原因是src设置的图片getDrawable()才不为空,不是点9图片拿到的是BitmapDrawable可以获取Bitmap图片,((BitmapDrawable) drawable).getBitmap()

点9图片和背景图片想要达到圆形图片,只需在方法1的代码中改onDraw

    @Override
    protected void onDraw(Canvas canvas) {
        if (null == mImageView){
            return;
        }
        mWidth = getWidth();
        mHeight = getHeight();
        Bitmap bitmap = initBitmap();

        if (null != bitmap) {
            // Calculate radius of the circle. To ensure the circle, take half of the length or width as radius
            int radius = (mWidth < mHeight ? mWidth : mHeight) / 2;
            // get round Bitmap
            Bitmap roundBitmap = getCroppedCircleBitmap(bitmap, radius);
            // Draw Bitmap for display
            canvas.drawBitmap(roundBitmap, mWidth / 2 - radius, mHeight / 2 - radius, null);

            mImageView.setImageBitmap(roundBitmap);
        }
        else {
            super.onDraw(canvas);
        }
    }

    private Bitmap initBitmap()
    {
        Bitmap output = null;
        Drawable drawable1 = mImageView.getDrawable();
        Drawable drawable2 = mImageView.getBackground();
        Drawable drawable = drawable1==null ? drawable2 : drawable1;          // 不能在构造方法中获取drawable,为null
        if (null != drawable && drawable instanceof BitmapDrawable) {
            output = ((BitmapDrawable) drawable).getBitmap();
        } else if (null != drawable && drawable instanceof NinePatchDrawable) { //处理点9图片,利用canvas
            int width = drawable.getIntrinsicWidth(); // image original width
            int height = drawable.getIntrinsicHeight(); // image original height
            output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
            Canvas canvas = new Canvas(output);
            drawable.draw(canvas);
        } else if (null != drawable && drawable instanceof ColorDrawable){// 处理background设置的图片,没效果,待解决
            output = Bitmap.createBitmap(mWidth,
                    mHeight, Bitmap.Config.ARGB_8888);
            Canvas canvas = new Canvas(output);
            drawable.setBounds(0, 0, mWidth, mHeight);
            drawable.draw(canvas);
        }
        return output;
    }

方法3:

方法1,在ImageView是动态设置的情况下,调用完imageView的onDraw,圆形图片会失效
方法3,通过设置的bitmap画圆,自定义控件中不包含ImageView

代码示例:

package xxx;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.graphics.Paint;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.PorterDuffXfermode;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.widget.AbsoluteLayout;
import android.view.ViewGroup;

public class CircleImage extends AbsoluteLayout { // 也可以继承其他的父类
    private String TAG = "TAG_CircleImage";
    private int mWidth = 100;
    private int mHeight = 100;
    private int mRadius = 0;
    private Paint mPaint;
    private Bitmap mCircleImage;

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

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

    public CircleImage(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public CircleImage(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setFilterBitmap(true);
        mPaint.setDither(true);
        setWillNotDraw(false);
	
	// 关闭硬件加速
	// 参考网站:https://juejin.im/post/6844903893189525512
	// 参考网站:https://developer.android.com/guide/topics/graphics/hardware-accel
        this.setLayerType(LAYER_TYPE_SOFTWARE, mPaint);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        Log.d(TAG, "onFinishInflate: ");
        // getView();
        if (null != mCircleImage) {
            Log.d(TAG, "onFinishInflate: mCircleImage != null");
            setCircleImageBitmap(mCircleImage);
        }
    }

    public void setCircleImageBitmap(Bitmap bmp) {
        Log.d(TAG, "setCircleImageBitmap");

        mCircleImage = bmp;
	
	/*刷新,会触发调用onDraw*/
        invalidate();
    }

    private Bitmap updateImageBitmap(Bitmap bmp) {
        Log.d(TAG, "updateImageBitmap bmp = " + bmp);

        ViewGroup.LayoutParams layoutParams = this.getLayoutParams();
        if (null != layoutParams && null != bmp) {
            mWidth = layoutParams.width;
            mHeight = layoutParams.height;
            Log.d(TAG, "updateImageBitmap layoutParams != null, mWidth = " + mWidth + ", mHeight = " + mHeight);

            // 计算显示圆形的半径,为保证圆形,取图片的长宽小的一半作为圆形
            mRadius = (mWidth < mHeight ? mWidth : mHeight) / 2;
            Log.d(TAG, "updateImageBitmap mRadius = " + mRadius);
            // 获取我们处理后的圆形图片
            if (mRadius > 0) {
                Log.d(TAG, "updateImageBitmap mRadius > 0");
                Bitmap bitmap = getCroppedCircleBitmap(bmp, mRadius);
                return bitmap;
            }
        }
        return null;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        Log.d(TAG, "onDraw");
        if (null != mCircleImage) {
            Log.d(TAG, "onDraw, mCircleImage != null");
            Bitmap bitmap = updateImageBitmap(mCircleImage);
	
	    /*
	    view.isHardwareAccelerated():
	    只会受到Application、Activity的影响,如果Activity本身不支持硬件加速,那么返回false,反之返回true
            canvas.isHardwareAccelerated():
	    会受到Application、Activity的影响,如果设置了setLayerType,那么会被setLayerType直接影响
	    */
            Log.d(TAG, "onDraw, view HardwareAccelerated = " + this.isHardwareAccelerated() + ", Canvas HardwareAccelera = " + canvas.isHardwareAccelerated());
            if (null != bitmap) {
                Log.d(TAG, "onDraw bitmap != null, bitmap width = " + bitmap.getWidth() + ", height = " + bitmap.getHeight());
		// 绘制图片进行显示
                canvas.drawBitmap(bitmap, mWidth / 2 - mRadius, mHeight / 2 - mRadius, null);
		/* 还原混合模式  */
                mPaint.setXfermode(null);
            }
        }
    }

    public Bitmap getCroppedCircleBitmap(Bitmap bmp, int radius) {
        Bitmap scaledSrcBmp;
        int diameter = radius * 2;
        // 对图片进行处理,获取我们需要的中央部分
        Bitmap squareBitmap = getCenterBitmap(bmp);

        // 将图片缩放到需要的圆形比例大小
        if (squareBitmap.getWidth() != diameter || squareBitmap.getHeight() != diameter) {
            scaledSrcBmp = Bitmap.createScaledBitmap(squareBitmap, diameter, diameter, true);
        } else {
            scaledSrcBmp = squareBitmap;
        }
        // 创建一个我们输出的对应
        Bitmap output = Bitmap.createBitmap(scaledSrcBmp.getWidth(),
                scaledSrcBmp.getHeight(),
                Bitmap.Config.ARGB_8888);
        // 在output上进行绘画
        Canvas canvas = new Canvas(output);
        // 创建裁剪的矩形
        Rect rect = new Rect(0, 0, scaledSrcBmp.getWidth(),scaledSrcBmp.getHeight());
        // 绘制dest目标区域
        canvas.drawCircle(scaledSrcBmp.getWidth() / 2,
                scaledSrcBmp.getHeight() / 2,
                scaledSrcBmp.getWidth() / 2,
                mPaint);

        // 设置xfermode模式
	// 参考:https://www.jianshu.com/p/d11892bbe055
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

        // 绘制src源区域
        canvas.drawBitmap(scaledSrcBmp, rect, rect, mPaint);
	/*注意:Bitmap.recycle 方法被弃用
	在Android2.3之后,整个Bitmap,包括数据和引用,都放在了堆中,这样,整个Bitmap的回收就全部交给GC了,这个recycle方法就再也不需要使用了。
	*/
	// bmp.recycle();
        // squareBitmap.recycle();
        //scaledSrcBmp.recycle();
        return output;
    }

    // 注意这里必须只能用bitmap.getWidth的图片,去算尺寸,不然外面的框,大于bitmap的大小,超出的就变成了黑色的
    private Bitmap getCenterBitmap(Bitmap bitmap) {
        // 为了防止图片宽高不相等,造成圆形图片变形,因此截取长方形中处于中间位置最大的正方形图片  
        int bmpWidth = bitmap.getWidth();
        int bmpHeight = bitmap.getHeight();
        Log.d(TAG, "getCenterBitmap bmpWidth = "+bmpWidth+", bmpHeight = "+bmpHeight);
        Bitmap squareBitmap;
        if (bmpHeight > bmpWidth) {
            // 截取正方形图片  ,从(bmpHeight - bmpWidth) / 2处开始截取
            squareBitmap = Bitmap.createBitmap(bitmap, 0, (bmpHeight - bmpWidth) / 2, bmpWidth, bmpWidth);
        } else if (bmpHeight < bmpWidth) {
            squareBitmap = Bitmap.createBitmap(bitmap, (bmpWidth - bmpHeight) / 2, 0, bmpHeight, bmpHeight);
        } else {
            squareBitmap = bitmap;
        }
        return squareBitmap;
    }
}

其他方法参考
https://segmentfault.com/a/1190000012253911
https://blog.csdn.net/Mr_dsw/article/details/48629177

根据资源拿到bitmap图片的一个方法为

Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.pic);
posted on 2020-05-22 18:19  JJ_S  阅读(1487)  评论(0编辑  收藏  举报