在我们的APP中通常会遇到,展示圆形头像的需求,一般通过Glide就能实现,但是让我们做一个圆形头像,如果让我们自定义实现这种效果,该怎样做呢?
好,接下来本文通过三种方式来实现这种效果!
注意:这是一个练手的Demo
1.通过本文可以学到的知识点
canvas.clipPath API的使用Xfermode的使用Paint的Xfermode和ShaderAPIMatrix的平移和Canvans的平移(源码中,为了在一个View同时展示三种效果,所以对Canvas坐标进行了平移)总结三种实现方式的优缺点2.通过自定义View制作圆形头像
通过三种方式来实现这种效果,是哪三种方式呢?
通过canvas.clipPath()通过paint.xfermode= porterDuffXfermode通过paint.shader= bitmapShader
3.第一种实现方式
利用 canvas的 clip方法
private val AVATAR_SIZE = 240.dp //图片的大小private val RADIUS = AVATAR_SIZE / 2 //裁剪圆形的半径class CircleAvatarView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {private val paint = Paint()private var avatar = getAvatar(R.drawable.my_avatar, AVATAR_SIZE.toInt())//图片的bitmapprivate val circlePath = Path()//圆形的路径override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {/*** 在圆心 x:View宽度的一半 y:View高度的一半 半径为图片尺寸的一半 的位置上画圆*/circlePath.addCircle(width / 2f, height / 2f, RADIUS, Path.Direction.CW)}override fun onDraw(canvas: Canvas) {super.onDraw(canvas)//ktx的扩展方法,会自动保存恢复Canvascanvas.withSave {//重置画笔paint.reset()//利用Canvas来裁切去要画的范围,也就是那个圆形canvas.clipPath(circlePath)//在裁切后画上声明的Bitmapcanvas.drawBitmap(avatar, width / 2 - RADIUS, height / 2 - RADIUS, paint)}}}
解释一下代码,当然看代码的注释也是一样的
定义包级别的图片的宽度为240dp和圆形的半径获取一个要展示的图片的Bitmap声明一个要裁剪的圆形的PathonSizeChanged方法中添加一个圆形利用Canvas来裁切去要画的范围,也就是那个圆形在裁切后画上声明的Bitmap
以上就是绘制圆形头像的第一种方法,主要使用的是canvas.clipPath(circlePath)
方法,注意Canvas的保存和恢复
4.第二种实现方式
通过paint.xfermode= porterDuffXfermode
PorterDuff.Mode
private val AVATAR_SIZE = 240.dp //图片的大小private val RADIUS = AVATAR_SIZE / 2 //裁剪圆形的半径class CircleAvatarView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {private val paint = Paint()private var avatar = getAvatar(R.drawable.my_avatar, AVATAR_SIZE.toInt())//图片的bitmapprivate val circlePath = Path()//圆形的路径private val bounds = RectF()//离屏缓冲的 boundsprivate val porterDuffXfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {/*** 在圆心 x:View宽度的一半 y:View高度的一半 半径为图片尺寸的一半 的位置上画圆*/circlePath.addCircle(width / 2f, height / 2f, RADIUS, Path.Direction.CW)/*** 设置离屏缓冲的 bounds最好不要太大,影响性能*/bounds.set(width / 2f - RADIUS, height / 2f - RADIUS,width / 2f + RADIUS, height / 2f + RADIUS)}override fun onDraw(canvas: Canvas) {super.onDraw(canvas) //开启离屏缓冲val count = canvas.saveLayer(bounds, null)paint.reset()canvas.drawPath(circlePath, paint)//设置paint的 xfermode 为PorterDuff.Mode.SRC_INpaint.xfermode = porterDuffXfermode//以当前的Paint来画Bitmapcanvas.drawBitmap(avatar, width / 2 - RADIUS, height / 2 - RADIUS, paint)//清空paint 的 xfermodepaint.xfermode = null//把离屏缓冲的内容,绘制到View上去canvas.restoreToCount(count)}}
解释一下代码,当然看代码的注释也是一样的
在onSizeChanged方法中设置离屏缓冲的范围,是圆形头像的外切矩形的范围onDraw方法中开启离屏缓冲Canvas先画了一个圆形(相当于PorterDuff.Mode中的Destination image)设置paint的 xfermode 为PorterDuff.Mode.SRC_IN以当前的Paint来画Bitmap(注意此时相当于PorterDuff.Mode中的Source image),因为我们选择的模式为PorterDuff.Mode.SRC_IN所以就画出我们想要的效果把离屏缓冲的内容,绘制到View上去
注意:一定要开启离屏缓冲,不然结果可能不是你所预期的,离屏缓冲相当于拿出一块透明的View来绘制,Canvas要绘制的图形
5.第三种实现方式
通过paint.shader= bitmapShader
private val AVATAR_SIZE = 240.dp //图片的大小private val RADIUS = AVATAR_SIZE / 2 //裁剪圆形的半径class CircleAvatarView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {private val paint = Paint()private var avatar = getAvatar(R.drawable.my_avatar, AVATAR_SIZE.toInt())//图片的bitmapprivate val circlePath = Path()//圆形的路径private val bitmapShader = BitmapShader(avatar, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {/*** 在圆心 x:View宽度的一半 y:View高度的一半 半径为图片尺寸的一半 的位置上画圆*/circlePath.addCircle(width / 2f, height / 2f, RADIUS, Path.Direction.CW)}override fun onDraw(canvas: Canvas) {super.onDraw(canvas)paint.reset()//给paint设置Shaderpaint.shader = bitmapShader//以Shader的形式来画圆形canvas.drawPath(circlePath, paint)}}
解释一下代码,当然看代码的注释也是一样的
这种方式就比较简单了
声明一个BitmapShader,把头像的bitmap填入BitmapShader的构造中,并填入参数Shader.TileMode的值,这个具体看这篇文章对Paint做了详细解释在onDraw方法中给paint设置Shader以Shader的形式来画圆形,结果就是一个圆角头像了
6.总结
总结一下三种方式的优缺点
1.canvas.clipPath():
利用 canvas的 clip方法没有抗锯齿效果,会有毛边,因为是精确的切像素点
2.paint.xfermode= porterDuffXfermode
利用 paint的 xfermode 方法有抗锯齿效果,没有毛边,效果好,做了抗锯齿的处理,填充和周边类似的半透明色等
3.paint.shader= bitmapShader
利用 设置 paint 的 shader 方法有抗锯齿效果,没有毛边,效果好但是图片结果会受到Shader.TileMode影响,可能结果不是你所预期的
综上总结:最好使用paint.xfermode= porterDuffXfermode这种方式,因为我们需要的是一个显示完美的头像
此外还要注意使用离屏缓冲
7.源码地址
CircleAvatarView.kt
8.原文地址
注意:我的源码是把三种方式统一画在了一个View中,并通过Matrix的平移或者Canvans的平移来实现向下排列的效果,顺便先练了Matrix的平移和Canvans的平移
Android自定义View实现圆形头像效果
9.参考文章
hencoder
PorterDuff.Mode
推荐一下我开源的项目 WanAndroid 客户端
WanAndroidJetpack 架构图
一个纯 Android 学习项目,WanAndroid 客户端。项目采用MVVM
架构,用Kotlin
语音编写。Android Jetpack 的大量使用包括但不限于Lifecycle
、LiveData
、ViewModel
、Databinding
、Room
、ConstraintLayout
等,未来可能会更多。采用Retrofit
和Kotlin-Coroutine
协程进行网络交互。加载图片Glide
主流加载图片框架。数据存储主要用到了Room
和腾讯的MMKV
。Kotlin + MVVM + Jetpack + Retrofit + Glide 的综合运用,是学习 MMVM 架构的不错的项目。此项目本身也是一个专门学习 Android 相关知识的 APP,欢迎下载体验!
源码地址(附带下载链接)
WanAndroidJetpack
APP 整体概览
喜欢的点个 Stars,有问题的请提 Issues。
如果觉得《android 清空canvas部分内容_Android自定义View实现圆形头像效果》对你有帮助,请点赞、收藏,并留下你的观点哦!