自定义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要绘制的图形。
实战一下吧,实战过程中不懂的再回来反刍着看。之后你就昂~~~懂了。真爽,懂了懂了。
按步骤实战
就是给你把步骤说清楚,完整代码在最后面。
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主要通过以下两个步骤来生成圆形头像:
- 创建一个新的Bitmap对象,并通过BitmapShader设置一个圆形的渲染器,将原始图片以圆形的方式绘制到新的Bitmap上。
- 将新生成的圆形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)
}
}