Android中如何切圆角

大家平时在工作中经常有对一些图片切圆角的需求,一般都是用前人已经封装好的控件,如果遇到问题了(比如性能问题),如果不了解底层原理以及其他的一些方案,可能就不知道怎么优化。现在来整理下现有的切圆角方案。

方案一 通过PorterDuff实现

首先说说PorterDuff是什么意思。其实他不是一个单词,而是因为ProterDuff是两个人名的组合,Tomas Proter和 Tom Duff. 他们是最早在SIGGRAPH上提出图形混合概念的大神级人物。

那我们能用PorterDuff做什么?可以完成任意二位图形的绘制。

Android中对应的类叫XferMode,实际的代码很简单,啥也不做,就定义了两个参数:

public class Xfermode {
    static final int DEFAULT = PorterDuff.Mode.SRC_OVER.nativeInt;
    int porterDuffMode = DEFAULT;
}

它有三个子类,分别是:

PorterDuffXfermode 这是一个非常强大的转换模式,使用它,可以使用图像合成的16条Porter-Duff规则的任意一条来控制Paint如何与已有的Canvas图像进行交互,具体的规则可以去官方文档查看 官方文档会给你丢这么一张图:

PixelXorXfermode 当覆盖已有的颜色时,应用一个简单的像素异或操作。

AvoidXfermode 指定了一个颜色和容差,强制Paint避免在它上面绘图(或者只在它上面绘图)。

b68f15dd13627acd5194c2d4ac6c6152.png

看了下,好像看能看明白,但是哪里又有点小问题,比如Source是什么,Destination又是什么?

Destination实际上是先画的图,也就是底下的图,而Source是后画上去的图,这两个概念要搞清楚,否则套公式的时候如果搞错。另外,官方给的图是不涉及透明度变化的,所以有透明度变化的图,需要自己对照生成公式去看,这里就不讨论了。

那我们大概明白了,我们如果要绘制一个圆形的用户图像,可以先绘制ImageView实际用户头像的Bitmap,再绘制一个圆形,再让他们取交集不就行了吗?谁先画后画没关系,只要你用对模式就行。另外这里要注意的是,由于我们要用Canvas操作Bitmap,所以需要先从网络加载Bitmap,这里应该由控件处理,这里就不展开了。关键代码如下:

class RoundImageView @JvmOverloads constructor(context: Context, attributeSet: AttributeSet? = null,
                                               defStyle : Int = 0) : ImageView(context, attributeSet, defStyle){
​
    var paint:Paint? = null
    var paint2:Paint? = null
    init {
        paint = Paint()
        paint?.apply {
            isAntiAlias = true
            color = Color.BLACK
            style = Paint.Style.FILL
        }
​
        paint2 = Paint()
        paint2?.apply {
            isAntiAlias = true
            color = Color.BLACK
            style = Paint.Style.FILL
        }
    }
    val cat = BitmapFactory.decodeResource(resources, R.drawable.cat)
    val bitmapWidth: Int = cat.width
    val bitmapHeight : Int = cat.height
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        val count = canvas.saveLayer(RectF((width/2 - cat.width/2).toFloat(),
            (height/2 - cat.height/2).toFloat(), (width/2 + cat.width/2).toFloat(), (height/2 + cat.height/2).toFloat()
        ), paint, Canvas.ALL_SAVE_FLAG)
        canvas.drawBitmap(getCatBitmap() , Rect(0, 0, cat.width, cat.height),
            Rect(width/2 - cat.width/2, height/2 - cat.height/2, width/2 + cat.width/2, height/2 + cat.height/2), paint)
        paint?.setXfermode(PorterDuffXfermode(PorterDuff.Mode.DST_IN))
        canvas.drawBitmap(getCircleBitmap(),Rect(0, 0, cat.width, cat.height),
            Rect(width/2 - cat.width/2, height/2 - cat.height/2, width/2 + cat.width/2, height/2 + cat.height/2), paint)
        paint?.setXfermode(null)
        canvas.restoreToCount(count)
    }
​
    fun getCircleBitmap() : Bitmap{
        val bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bitmap)
        paint2?.reset()
        canvas.drawCircle(bitmapWidth/2.toFloat(), bitmapHeight/2.toFloat(), bitmapWidth/2.toFloat(), paint2)
        return bitmap
    }
​
    fun getCatBitmap(): Bitmap {
        val bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bitmap)
        paint2?.reset()
        canvas.drawBitmap(cat, 0.toFloat(), 0.toFloat(), paint2)
        return bitmap
    }
​
}

这里要注意的是,Paint.setXfermode()要结合Canvas.saveLayer()和Paint.restoreToCount()一起使用,原因也很简单,Canvas.saveLayer()这个方法,它的作用是生成一个新的层,这个层是透明的,之后的所有drawxx的方法都是在这个层上进行的,再通过restoreToCount()将新的层与底下的Canvas相结合就形成一个完整的图像,也就是说在画源图像时,会把之前画布上的所有内容当做 dst.而在saveLayer新生成的bitmap上,只有dst对应的圆形,所以除了与圆形相交之外的位置都是空像素。在画图完成之后,会把saveLayer所生成的bitmap盖在原来的canvas上面。saveLayer详细介绍的博文可以看这条Canvas的恢复和保存

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐