自定义控件-阳光沙滩用户头像
开发需求
- 在阳光沙滩客户端中会有大量的显示用户头像的地方,例如文章列表、摸鱼列表、相关评论中都会用到,并且在不同地方他们的大小和样式可能也不一样,还有就是Vip和普通用户也有不同标志,因此需要用到自定义控件,并且控件应该自适应大小和通过其属性控制控件的样式
- 控件自适应
- 属性控制控件的形状
- 属性控制控件的VIP和非VIP的样式
开发知识预热
-
Paint的xfermode的应用
由于图片的形状是矩形,而在自定义过程中我们要将头像设置成圆形或圆角矩形,我们在这里使用
Paint
的xfermode
实现, 当取不同的值是可以将得到两张图片的并集、合集等等,具体知识可以参看文章:各个击破搞明白PorterDuff.Mode。在当前需求中我们使用PorterDuff.Mode.SRC_IN
,在此模式下: 1. 在两者相交的地方绘制源图像 2. 绘制的效果会受到目标图像对应地方透明度的影响重点在于第一点,第二点由于需求没有涉及到透明度的问题所以用不到这个属性。而实现圆形和圆角矩形就是在canvs上绘制需要的图像形状,然后设置
xfermode
,最终绘制原图像,即可获取到所需形状的图像绘制圆角矩形图片的代码示例:
private fun createRoundImage(bitmap: Bitmap, width: Int, height: Int): Bitmap{ paint.isAntiAlias = true val target = Bitmap.createBitmap(bitmap.width, bitmap.width, Bitmap.Config.ARGB_8888) val canvas = Canvas(target) val min = Math.min(bitmap.width, bitmap.height) val dstRect = Rect(0, 0, width, height) val srcRect = Rect(0, 0, min, min) val backgroundRect = RectF(0.0f + borderWidth,0.0f + borderWidth, width.toFloat() - borderWidth, height.toFloat() - borderWidth) val roundR = ScreenUtils.dip2px(context, 8f).toFloat() canvas.drawRoundRect(backgroundRect, roundR - borderWidth, roundR - borderWidth, paint) // 核心代码取两个图片的交集部分 paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) canvas.drawBitmap(bitmap, srcRect, dstRect, paint) paint.xfermode = null return target }
-
Glide加载图片的监听 在glide的使用中最简单和最频繁的使用的代码是:
Glide.with(context).load(url).into(iv)
但是在本次需求中我们要再设置的图片后去创建指定形状的图片和绘制其他VIP相关的样式,所以我们要让控件何时被设置了图片然后去调用postInvalidate()重新绘制图片
设置glide加载图片的代码示例:
//使用了ViewDataBing中的自定义属性 @BindingAdapter("avatar") fun setAvatarImage(iv: SobAvatarView, avatar:String){ Glide.with(iv.context) .asBitmap() .load(avatar) .listener(object:RequestListener<Bitmap>{ override fun onLoadFailed( e: GlideException?, model: Any?, target: Target<Bitmap>?, isFirstResource: Boolean ): Boolean { return true } override fun onResourceReady( resource: Bitmap?, model: Any?, target: Target<Bitmap>?, dataSource: DataSource?, isFirstResource: Boolean ): Boolean { //资源准备完毕,设置图片并刷新控件 iv.setImageBitmap(resource) iv.update() return true } }).into(iv) }
补充小知识:使用glide直接设置图片圆角
//使用了ViewDataBing中的自定义属性 @BindingAdapter(value=["pic_url", "pic_corner"], requireAll = false) fun setImageViewCorner(iv: ImageView, pic_url:String?, pic_corner: Int?){ pic_url?.let { var override:RequestOptions? = null pic_corner?.let { if(pic_corner > 0){ val corners = RoundedCorners(ScreenUtils.dp2px(pic_corner)) override = RequestOptions.bitmapTransform(corners) } } if (override == null) { Glide.with(iv.context).load(pic_url).into(iv) }else{ Glide.with(iv.context).load(pic_url).apply(override!!).into(iv) } } }
-
DataBing在为自定义属性绑定赋值中遇到的问题
在此次需求中isVip属性是通过DataBing绑定给控件的,使用
{data.isVip}
后编译会报错,说找不这个接口,因此需要在控件中创建函数isVip(vip:Boolean)解决该问题fun isVip(vip:Boolean){ isVip = vip //参数置为0,绘画时重新新计算参数 viewHeight = 0 viewWidth = 0 postInvalidate() }
开始敲代码
-
根据需求自定义属性如下:
<declare-styleable name="SobAvatarView"> <attr name="isCircle" format="boolean"/> <attr name="isVip" format="boolean"/> </declare-styleable>
-
编写控件代码
-
初始化获取自定义属性的值
init { val obtainStyledAttributes = context.obtainStyledAttributes(attrs, R.styleable.SobAvatarView) isCircle = obtainStyledAttributes.getBoolean(R.styleable.SobAvatarView_isCircle, isCircle) isVip = obtainStyledAttributes.getBoolean(R.styleable.SobAvatarView_isVip, isVip) }
-
编写onDraw代码
override fun onDraw(canvas: Canvas?) { canvas?.let { //检查需要重新初始化相关属性 if(viewHeight == 0 || viewWidth == 0){ //计算相关属性的值 calcValues() } //判断画那种样式 if(isCircle){ drawCircleStyle(it) }else{ drawRoundStyle(it) } //画字母V drawLetterV(it) } if (canvas == null) { super.draw(canvas) } }
- 相关属性的值的计算
/** * 计算相关参数 */ private fun calcValues() { //计算控件的宽高 viewHeight = height - paddingBottom - paddingTop viewWidth = width - paddingStart - paddingEnd //将设置的图片转化为控件大小的Bitmap val image: Bitmap = drawableToBitmap(drawable) val resSizeBitmap = resizeBitmap(image, viewWidth, viewHeight) //VIP broad宽度 borderWidth = (defBorderWidth * 1.0 * (viewWidth * 1.0 / ScreenUtils.dip2px(context, 60f))).toInt() //VIP图标半径 smallR = viewWidth / 2.0f - ((viewWidth - borderWidth) / 2.0f) * (sqrt2 / 2.0f).toFloat() if (isCircle) { //圆形头像 circleBitmap = createCircleImage(resSizeBitmap, viewWidth, viewHeight) } else { //圆角矩形头像 roundBitmap = createRoundImage(resSizeBitmap, viewWidth, viewHeight) } vipIconRect = RectF( viewWidth - 2 * smallR, viewWidth - 2 * smallR, viewWidth.toFloat(), viewHeight.toFloat() ) }
- 刷新头像数据的接口
/** * 提供给外部设置头像后用来刷新数据的接口 */ fun update() { //参数置为0,绘画时重新新计算参数 viewHeight = 0 viewWidth = 0 postInvalidate() }
-
小结
-
阳光沙滩用户头像的自定义View的代码分享如上,除了以上方法为还有其余方法都是简单的功能函数,避免篇幅过大,暂不做分享,详细代码可以在这里阅读。希望xdjmm能阅读阅读代码,指出不足的地方,相互学习,哈哈哈。
-
总结
-
好久都没写文章了,肚子里也没什么墨水了,因此文章大部分都是代码,但是关键注释还是有的,希望大家通过文章能够学习到关于自定义View的相关知识,如果代码阅读过程中遇到不足的地方欢迎在评论区指出纠正,大家一起学习,共同进步;一直觉得程序猿(媛)写代码的时间占据了大部分,在大部分时间效率较低,而对代码的商量与思考时间较少,所以希望阳光沙滩能越来越热闹,xdjmm能在这里愉快的交流分享知识。至少现在对于我来说代码能使感到愉快,希望以后也如此!
-
最后是关于阳光沙客户端文章详情的预告,因为文章的样式已经优化的不错了(在我看来是?),争取这周日分享出来,敬请期待。。。