自定义View实战《圆形头像》

前言

来个经典的自定义View圆形头像。
本文分为三个部分
1、PorterDuffXfermode讲解
2、步骤讲解和分步实战
3、Glide实现圆形头像(当然带整个快速实现的方案喽)
4、完整代码送给你

PorterDuffXfermode

定义:

1、PorterDuffXfermode是Android中强大而灵活的图像合成工具之一。它可以让开发者通过不同的合成模式来操作和混合图像,从而创建出各种独特的效果。
2、Xfermode是Android中用于指定绘制模式的接口,通过该接口可以定义源图像和目标图像如何进行混合或合成。
3、Paint.xfermode是用于设置绘制模式的属性,而PorterDuffXfermode是其中一种实现了Xfermode接口的具体绘制模式。

ok了,上面是关于Xfermode和PorterDuffXfermode的理论介绍。往下看。
对了,先看看代码中是怎么使用的再往下讲:(先看一下代码怎么写,往下看会有讲解,不然干讲不会懂的)

//创建porterDuffXfermode
private val porterDuffXfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
//初始化paint.xfermode  等下去画的时候使用
paint.xfermode = porterDuffXfermode

解释:

1、当我们使用paint.xfermode属性设置绘制模式时,可以将PorterDuffXfermode的实例作为参数传递给该属性,从而实现特定的图像合成效果。
2、通过调用paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XXX))的方式,可以将PorterDuffXfermode的实例与paint对象进行绑定,然后在绘制时使用该paint对象实现所需的图像合成效果。
3、因此,paint.xfermode和PorterDuffXfermode之间的关系是,通过paint.xfermode属性可以设置绘制模式,并使用PorterDuffXfermode的实例实现具体的图像合成效果。
ok了吧!!!! 知道他俩啥关系了吧,挺铁。

Mode种类:

那么问题来了上面说了可以通过设置多种的Mode来决定绘画的方式,那么都有哪些mode呢都是什么意思呢?
在这里插入图片描述
PorterDuff.Mode mode,而Android给我们提供了18种图片混排模式,简单点可以 理解为两个图层按照不同模式,可以组合成不同的结果显示出来!

这里两个图层:先绘制的图是目标图(DST),后绘制的图是源图(SRC)

之后就是顾名思义了:

Mode 解释
SrcOver 源图覆盖
SrcIn 源图交集
SrcOut 源图的非交集部分
SrcATop 源图和目标图相交处绘制源图,不相交的地方绘制目标图
Xor 不相交的地方按原样绘制源图和目标图
Darken 取两图层全部区域,交集部分颜色加深
Lighten 取两图层全部区域,点亮交集部分颜色
Multiply 取两图层交集部分叠加后颜色
Screen 取两图层全部区域,交集部分变为透明色

没有全部列举,但是按照上面表中的相信你也能理解相关的DstOver、DstIn等等。具体效果如下图所示:
在这里插入图片描述
注意:使用这种模式去绘制的时候一定要开启离屏缓冲,不然结果可能不是你所预期的,离屏缓冲相当于拿出一块透明的View来绘制,Canvas要绘制的图形。
实战一下吧,实战过程中不懂的再回来反刍着看。之后你就昂~~~懂了。真爽,懂了懂了。

按步骤实战

就是给你把步骤说清楚,完整代码在最后面。

扫描二维码关注公众号,回复: 16478092 查看本文章

1、初始化

初始化porterDuffXfermode模式为PorterDuff.Mode.SRC_IN。也就是源图的交集部分。
为什么用这个呢? 可以结合上面来看。我们完全可以搞一个目标图为圆形。长方形的我们的图片作为源图。
因为我们取的是源图的交集部分。那么不就剩下圆了吗。。。哈哈 简单吧

//初始化porterDuffXfermode
private val porterDuffXfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
//初始化参数
val at = context.obtainStyledAttributes(attrs, R.styleable.CircularImageView)
val width = SizeUtil.dpToPx(context, at.getInt(R.styleable.CircularImageView_layout_width, 0).toFloat())
val height = SizeUtil.dpToPx(context, at.getInt(R.styleable.CircularImageView_layout_height, 0).toFloat())
val image = R.drawable.image

2、计算需要的绘画参数

圆一定是在长方形的图片内的。或者其他形状的,反正一定是能包含我们这个圆的。

//获取width和height较为短的一边作为圆的直径
val imageSize = minOf(width, height)
// 圆的半径
radius = imageSize / 2
//把图片转为Bitmap好操作
avatar = getAvatar(image, imageSize)//图片的bitmap

3、画圆作为目标图

先初始化圆的path和RectF

/**
 * 在圆心 x:View宽度的一半 y:View高度的一半 半径为图片尺寸的一半 的位置上画圆
 */
circlePath.addCircle(width / 2f, height / 2f, radius.toFloat(), Path.Direction.CW)
/**
 * 设置离屏缓冲的 bounds最好不要太大,影响性能
 */
bounds.set(
    width / 2f - radius, height / 2f - radius,
    width / 2f + radius, height / 2f + radius
)

利用drawPath画圆

canvas.save()
canvas.restore()
paint.reset()
canvas.drawPath(circlePath, paint)

4、画源图裁剪圆头

利用paint.xfermode来画我们的源图

//设置paint的 xfermode 为PorterDuff.Mode.SRC_IN
paint.xfermode = porterDuffXfermode
//以当前的Paint来画Bitmap
canvas.drawBitmap(avatar, ((width / 2 ) - radius).toFloat(), ((height / 2) - radius).toFloat(), paint)
//清空paint 的 xfermode
paint.xfermode = null

注意:记得开离屏加载,看完整代码看看放在哪里。

Glide简单实现

来点简单的,直接上框架:

步骤:

第一步,添加Glide依赖。在项目的build.gradle文件中添加以下代码:

dependencies {
    
    
    implementation ‘com.github.bumptech.glide:glide:4.12.0’
    annotationProcessor ‘com.github.bumptech.glide:compiler:4.12.0}

第二步,加载图片。在你的代码中,首先确保你有一个ImageView来显示头像。然后,使用Glide的with()方法来初始化一个Glide实例,指定上下文和需要加载图片的地址。接着,调用asBitmap()方法来告诉Glide我们需要加载一张bitmap图片。最后,使用into()方法将加载好的图片显示到ImageView上。以下是示例代码:

ImageView avatarImageView = findViewById(R.id.avatar_image_view);
String imageUrl = "https://example.com/your_image.jpg";

Glide.with(this) 
    .asBitmap() 
    .load(imageUrl) 
    .into(avatarImageView);

第三步,裁剪图片为圆形。默认情况下,Glide会将图片按照ImageView的尺寸进行缩放来适应显示。为了实现圆形头像,我们需要使用Glide的transform()方法来裁剪图片为圆形。Glide提供了许多内置的Transformations,其中包括一个叫做CircleCrop的Transformation,它可以将图片裁剪为圆形。以下是示例代码:

ImageView avatarImageView = findViewById(R.id.avatar_image_view);
String imageUrl = "https://example.com/your_image.jpg";

Glide.with(this)
    .asBitmap()
    .load(imageUrl)
    .transform(new CircleCrop()) 
    .into(avatarImageView);

以上就是用Glide实现圆形头像的步骤。通过添加Glide依赖、加载图片并裁剪为圆形,我们可以很容易地实现一个带有圆形头像的ImageView。

实现的内部原理:

Glide在底层实现圆形头像时,并没有直接使用离屏缓冲(off-screen buffer)这种方式。实际上,Glide主要通过以下两个步骤来生成圆形头像:

  1. 创建一个新的Bitmap对象,并通过BitmapShader设置一个圆形的渲染器,将原始图片以圆形的方式绘制到新的Bitmap上。
  2. 将新生成的圆形Bitmap设置到ImageView上进行展示。
    具体来说,Glide底层实现圆形头像的核心代码如下所示:
Bitmap sourceBitmap = ...; // 原始图片的Bitmap对象
Bitmap resultBitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(resultBitmap);
Paint paint = new Paint();
paint.setShader(new BitmapShader(sourceBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
paint.setAntiAlias(true);
float radius = size / 2f;
canvas.drawCircle(radius, radius, radius, paint);

上述代码中,通过Canvas的drawCircle()方法将圆形路径绘制在新的Bitmap上,并通过Paint的setShader()方法将原始图片设置为圆形渲染器。最后,将新生成的圆形Bitmap设置到ImageView上进行展示。

问题 :

为什么Glide最初没有使用离屏缓冲的方式呢?
1、这主要是出于性能和资源消耗的考虑。在过去的Android版本中,使用离屏缓冲可能会引起内存消耗和性能问题,尤其是对于较大尺寸的图片和频繁的圆形头像加载。
2、因此,Glide选择了一种相对简单和高效的方式来处理圆形头像,即通过绘制路径的方式进行裁剪,并避免了使用离屏缓冲所带来的潜在问题。
3、然而,随着Android系统的发展和改进,离屏缓冲的性能和资源消耗问题得到了一定程度的解决和优化。因此,Glide在某些版本中可能会采用离屏缓冲的方式来实现圆形头像,以提供更加高效和优化的圆形头像处理。

自定义圆形头像的完整代码

CircularImageView.kt

class CircularImageView : View {
    
    

    private val paint = Paint()
    private lateinit var avatar: Bitmap
    private var radius: Int = 0
    private val circlePath = Path()
    private val bounds = RectF()
    private val porterDuffXfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)

    //利用对象创建的时候
    constructor(context: Context) : super(context)

    //XML中正常使用时候
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
    
    
        //1、初始化获取相关参数
        val at = context.obtainStyledAttributes(attrs, R.styleable.CircularImageView)
        val width = SizeUtil.dpToPx(context, at.getInt(R.styleable.CircularImageView_layout_width, 0).toFloat())
        val height = SizeUtil.dpToPx(context, at.getInt(R.styleable.CircularImageView_layout_height, 0).toFloat())
        val image = R.drawable.image
        //2、获取width和height较为短的一边作为圆的直径
        val imageSize = minOf(width, height)
        // 圆的半径
        radius = imageSize / 2

        //3、把图片转为Bitmap好操作
        avatar = getAvatar(image, imageSize)//图片的bitmap

        //缩放1.2倍
        val scaledBitmap = Bitmap.createScaledBitmap(
            avatar, avatar.width * 1.2.toInt(),
            avatar.height * 1.2.toInt(), true
        )
        avatar = scaledBitmap
        at.recycle()
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    
    
        /**
         * 在圆心 x:View宽度的一半 y:View高度的一半 半径为图片尺寸的一半 的位置上画圆
         */
        circlePath.addCircle(width / 2f, height / 2f, radius.toFloat(), Path.Direction.CW)
        /**
         * 设置离屏缓冲的 bounds最好不要太大,影响性能
         */
        bounds.set(
            width / 2f - radius, height / 2f - radius,
            width / 2f + radius, height / 2f + radius
        )

    }

    override fun onDraw(canvas: Canvas) {
    
    
        canvas.save()
        canvas.restore()
        //开启离屏缓冲
        val count = canvas.saveLayer(bounds, null)
        paint.reset()
        canvas.drawPath(circlePath, paint)
        //设置paint的 xfermode 为PorterDuff.Mode.SRC_IN
        paint.xfermode = porterDuffXfermode
        //以当前的Paint来画Bitmap
        canvas.drawBitmap(avatar, ((width / 2 ) - radius).toFloat(), ((height / 2) - radius).toFloat(), paint)
        //清空paint 的 xfermode
        paint.xfermode = null
        //把离屏缓冲的内容,绘制到View上去
        canvas.restoreToCount(count)
    }

    private fun View.getAvatar(@DrawableRes resId: Int, width: Int): Bitmap {
    
    
        val options = BitmapFactory.Options()
        options.inJustDecodeBounds = true
        BitmapFactory.decodeResource(resources, resId, options)
        options.inJustDecodeBounds = false
        options.inDensity = options.outWidth
        options.inTargetDensity = width
        return BitmapFactory.decodeResource(resources, resId, options)
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_45112340/article/details/131634984