内容简介:在网上搜索相关主题,大家会发现答案千篇一律地使用9-Patch图片作为TextView的背景。这个方法我也用过,效果不好不说,还存在一些问题。使用9-Patch文件,本质上是用图片作为背景,只是这张图片会在设置的某些像素点上重复绘制达到拉伸的效果。由于Android屏幕的碎片化,一张9-Patch并不能适配所有屏幕,至少需要两张分别适配虽然每张9-Patch不大,但是扩展到
一、前言
在网上搜索相关主题,大家会发现答案千篇一律地使用9-Patch图片作为TextView的背景。这个方法我也用过,效果不好不说,还存在一些问题。
使用9-Patch文件,本质上是用图片作为背景,只是这张图片会在设置的某些像素点上重复绘制达到拉伸的效果。由于Android屏幕的碎片化,一张9-Patch并不能适配所有屏幕,至少需要两张分别适配 xhdpi 和 xxhdpi 。
虽然每张9-Patch不大,但是扩展到 不同场景要求不同的圆角 、 点击气泡时填充交互色变化 、 更加精细的屏幕适配 ,最终需要多款类似而有各自差别的图片集,累计成可观的安装包大小。其次用9-Patch作为消息气泡,气泡描边的视觉效果相当糟糕,这点在我的实战中得到了充分验证。
效果最好,莫过于通过代码实现消息的背景。由于通过xml的 Shape 样式没法绘制箭头,仅能实现描边和填充颜色,所以还得通过代码绘制的方式实现。
二、方向枚举
设置箭头的朝向,默认定义两个方向: START 为箭头朝左, END 为箭头朝右。
enum class DIRECTION { START, END }
当然还可以根据需要增加朝上和朝下的方向。虽然在Android中不建议使用枚举类,但用Android官方推荐的方法时,需要依赖的注解和Kotlin存在兼容性问题,所以这里依然使用枚举类。(Java用户请放心食用)
三、构造方法
class BubbleShape constructor(var arrowDirection: DIRECTION, @ColorInt var solidColor: Int, @ColorInt var strokeColor: Int, var strokeWidth: Int, var cornerRadius: Int, var arrowWidth: Int, var arrowHeight: Int, var arrowMarginTop: Int) : Shape()
变量名已能清晰描述变量本身功能,除了这三个需额外解释:
- arrowWidth 是箭头的水平宽度;
- arrowHeight 箭头的垂直高度 ;
- arrowMarginTop 是箭头上角距离(左、右)上方圆角的垂直高度;
通过水平宽度和垂直高度形成的矩形,就能在矩形内画出固定宽高的三角形。
四、数据成员
// 气泡上部区域的path private val mUpperPath = Path() // 气泡下部区域的path private val mLowerPath = Path() // 修正绘制stroke的偏差 private var mStrokeOffset = (strokeWidth ushr 1).toFloat() // 修正绘制radius的偏差 private var mRadiusOffset = (cornerRadius ushr 1).toFloat() // 预先计算以减少计算量:箭头上角到气泡顶部高度,NA:NoneArrow private val mUpperHeightNA = cornerRadius + arrowMarginTop + mStrokeOffset // 预先计算以减少计算量:箭头上角到气泡顶部高度 + 半个箭头的高度,HA:HalfArrow private val mUpperHeightHA = mUpperHeightNA + (arrowHeight ushr 1).toFloat() // 预先计算以减少计算量:箭头上角到气泡顶部高度 + 整个箭头的高度,FA:FullArrow private val mUpperHeightFA = mUpperHeightNA + arrowHeight
五、重写resize()
5.1 如何绘制
为了绘制方便,此类把一个气泡分为三个部分进行绘制。
上图三个部分用不同的颜色填充,描边加粗并使用半透明的白色以便查看。
5.2 onResize()
由于宽度和气泡内部 TextView 文字长度高度有关,所以需要重写方法,实时计算宽高值。此方法中调用的,就是计算 气泡上部path 和 气泡下部path 。此外还有气泡中部,不过中部纯粹为一个的矩形,计算好高度直接绘制即可。
override fun onResize(width: Float, height: Float) { resizeTopPath(width) resizeBottomPath(width, height) }
5.3 resizeTopPath()
气泡上部path
private fun resizeTopPath(width: Float) { val cornerRadius = cornerRadius.toFloat() val arrowWidth = arrowWidth.toFloat() val upperHeightNA = mUpperHeightNA val upperHeightHA = mUpperHeightHA val upperHeightFA = mUpperHeightFA mUpperPath.reset() // 设置箭头path mUpperPath.moveTo(arrowWidth, upperHeightFA) mUpperPath.lineTo(0F, upperHeightHA) mUpperPath.lineTo(arrowWidth, upperHeightNA) // 设置箭头到左上角之间的竖线path mUpperPath.lineTo(arrowWidth, cornerRadius) // 设置左上角path val leftTop = RectF(arrowWidth, 0F, arrowWidth + cornerRadius, cornerRadius) mUpperPath.arcTo(leftTop, 180F, 90F) // 设置顶部横线path mUpperPath.lineTo(width - cornerRadius, 0F) // 设置右上角path val rightTop = RectF(width - cornerRadius, 0F, width, cornerRadius) mUpperPath.arcTo(rightTop, 270F, 90F) // 设置右边竖线path mUpperPath.lineTo(width, upperHeightFA) }
5.3 resizeBottomPath()
气泡下部path
private fun resizeBottomPath(width: Float, height: Float) { val cornerRadius = cornerRadius.toFloat() val arrowWidth = arrowWidth.toFloat() mLowerPath.reset() // 设置右下角path mLowerPath.moveTo(width, height - cornerRadius) val rightBottom = RectF(width - cornerRadius, height - cornerRadius, width, height) mLowerPath.arcTo(rightBottom, 0F, 90F) // 设置底部横线path mLowerPath.lineTo((arrowWidth + cornerRadius), height) // 设置左下角path val leftBottom = RectF(arrowWidth, height - cornerRadius, (arrowWidth + cornerRadius), height) mLowerPath.arcTo(leftBottom, 90F, 90F) // 设置箭头到底部的竖线path mLowerPath.lineTo(arrowWidth, height - cornerRadius) }
六、重写onDraw()
定义好气泡 气泡上部path 和 气泡下部path ,就轮到 onDraw() 进行绘制了
6.1 onDraw()方法
override fun draw(canvas: Canvas, paint: Paint) { paint.color = solidColor // 填充颜色 paint.style = Paint.Style.FILL // 样式为FILL paint.isAntiAlias = true // 抗锯齿 paint.isDither = true // 开启抖动模式 // 记录画布 canvas.save() // 箭头的方向,通过scale变换画布方向实现 if (arrowDirection == DIRECTION.END) { canvas.scale(-1F, 1F, width / 2, height / 2) } // 绘制顶部分区域 canvas.drawPath(mUpperPath, paint) // 绘制中部分区域(矩形) val rectF = RectF(arrowWidth.toFloat(), mUpperHeightFA, width, height - cornerRadius) canvas.drawRect(rectF, paint) // 绘制底部分区域 canvas.drawPath(mLowerPath, paint) // 绘制描边 drawStroke(canvas, paint) // 还原画布 canvas.restore() }
6.2 绘制描边
private fun drawStroke(canvas: Canvas, paint: Paint) { val strokeOffset = mStrokeOffset val radiusOffset = mRadiusOffset val cornerRadius = cornerRadius val arrowWidth = arrowWidth val upperHeightNA = mUpperHeightNA val upperHeightHA = mUpperHeightHA val upperHeightFA = mUpperHeightFA // 设置画笔 paint.color = strokeColor // 画笔颜色 paint.style = Paint.Style.STROKE // 画笔样式为STROKE paint.strokeCap = Paint.Cap.ROUND // 笔尖绘制样式为圆形 paint.strokeJoin = Paint.Join.ROUND // 拐角绘制样式为圆形 paint.strokeWidth = strokeWidth.toFloat() // 描边的宽度,单位px // 绘制左上角和顶部描边 val leftTop = RectF(arrowWidth + strokeOffset, strokeOffset, arrowWidth + cornerRadius - strokeOffset, cornerRadius - strokeOffset) canvas.drawArc(leftTop, 180F, 90F, false, paint) canvas.drawLine(arrowWidth + cornerRadius - radiusOffset, strokeOffset, width - cornerRadius + radiusOffset, strokeOffset, paint) // 绘制右上角和右边描边 val rightTop = RectF(width - cornerRadius + strokeOffset, strokeOffset, width - strokeOffset, cornerRadius - strokeOffset) canvas.drawArc(rightTop, 270F, 90F, false, paint) canvas.drawLine(width - strokeOffset, cornerRadius - radiusOffset, width - strokeOffset, height - cornerRadius + radiusOffset, paint) // 绘制右下角和底部描边 val rightBottom = RectF(width - cornerRadius + strokeOffset, height - cornerRadius + strokeOffset, width - strokeOffset, height - strokeOffset) canvas.drawArc(rightBottom, 0F, 90F, false, paint) canvas.drawLine(width - cornerRadius + radiusOffset, height - strokeOffset, arrowWidth + cornerRadius - radiusOffset, height - strokeOffset, paint) // 绘制右下角和左边箭头下的描边 val leftBottom = RectF(arrowWidth + strokeOffset, height - cornerRadius + strokeOffset, arrowWidth + cornerRadius - strokeOffset, height - strokeOffset) canvas.drawArc(leftBottom, 90F, 90F, false, paint) canvas.drawLine(arrowWidth + strokeOffset, height - cornerRadius + radiusOffset, arrowWidth + strokeOffset, upperHeightFA, paint) // 绘制箭头和箭头上面的描边 canvas.drawLine(arrowWidth + strokeOffset, upperHeightFA, strokeOffset, upperHeightHA, paint) canvas.drawLine(strokeOffset, upperHeightHA, arrowWidth + strokeOffset, upperHeightNA, paint) canvas.drawLine(arrowWidth + strokeOffset, mUpperHeightNA, arrowWidth + strokeOffset, cornerRadius - radiusOffset, paint) }
七、克隆
override fun clone(): BubbleShape = super.clone() as BubbleShape
八、运行效果
九、结语
最后,留下几个问题用于提高,有兴趣的读者可以自行探索:
-
绘制圆角的偏差是如何造成的?
-
如何用代码实现点击气泡时填充颜色变化的反馈?
-
如何用代码设置内边距,令内部的TextView文字与气泡整体更好融合?
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- H5说话气泡点击动画
- arcgis for js 通用气泡
- 雕虫晓技(十) Android超简单气泡效果
- 漂亮的动态气泡背景效果bubbly-bg.js
- 消息队列面试连环问:如何保证消息不丢失?处理重复消息?消息有序性?消息堆积处理?
- 消息队列(三)常见消息队列介绍
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。