一、前言

在网上搜索相关主题,大家会发现答案千篇一律地使用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文字与气泡整体更好融合?

android背景气泡,仿Android微信消息气泡相关推荐

  1. android气泡聊天消息背景,Android使用贝塞尔曲线仿QQ聊天消息气泡拖拽效果

    本文实例为大家分享了Android仿QQ聊天消息气泡拖拽效果展示的具体代码,供大家参考,具体内容如下 先画圆,都会吧.代码如下: public class Bezier extends View { ...

  2. android 图片气泡,关于实现微信聊天气泡里显示图片解决方案

    关于实现微信聊天气泡里显示图片 这是微信的效果,气泡中的图片没有边距 这是我的效果,背景气泡是用.9.png图片组成的一个selector,气泡中的图片有边距  如何才能像微信那样没有边矩呢? --- ...

  3. android auto 能微信_OPPO手环用上Android Auto,可回复微信消息

    现在的绝大部分智能手环产品,都是支持显示手机上的通知功能的,比如说手机上来微信消息了,手环是能够同步进行提醒的,只要你安装了相应的App并且给了它读取通知的权限.但是,就目前的产品来说,基本都是不支持 ...

  4. android 消息列表,[Android]用LinearLayout 实现类微信消息列表项

    实现效果: 微信参照界面: 一个关键点: "早晚报"与"下午19:05"之间要用空白TextView隔开,来造成一个靠左,一个靠右的效果 LinearLayou ...

  5. android背景图拉伸,Android使背景图像不拉伸它指定的视图_android_开发99编程知识库...

    图片的最大值在将图片分配到背景时,它看起来像是什么,而不是. 如果图像大于表,我非常喜欢它不伸展顶部 TableView . 我已经包含了一个空的"视图",以便为表背景提供一些额外 ...

  6. android背景不填充,(Android Studio)应用程序背景图像不填充屏幕

    我认为它正在发生,因为顶层容器中存在填充...这就是你的情况下的相对布局 如果您的相对布局看起来像下面的代码 xmlns:tools="http://schemas.android.com/ ...

  7. Android自定义控件----高仿Android酷狗播放条(YluoSeekBar)

    发一个高仿Android版本的酷狗进度条. 是继承SeekBar实现的,代码比较简单就不详细说.用法和原生的SeekBar一样.项目放在我的github上.github地址 效果如下图所示:

  8. android背景图加蒙版,Android实现蒙板效果

    本文实例为大家分享了Android实现蒙板效果的相关代码,供大家参考,具体内容如下 1.不保留标题栏蒙板的实现 效果: 原理: 1.弹窗时,设置背景窗体的透明度 2.取消弹窗时,恢复背景窗体的透明度 ...

  9. android 背景图缩放,android背景图按比例缩放方法

    直接在你的layout文件的开头加一个FrameLayout ,里面放一个ImageView,因为只有android:src可以设置android:scaleType,android:backgrou ...

最新文章

  1. python base64库介绍
  2. VTK:Utilities之Timer
  3. 如何实现Spark on Kubernetes?
  4. 路由协议和路由算法的不同
  5. python paramiko长连接_【Python】 SSH连接的paramiko
  6. 我学习的第一个uiautomator从创建到运行结束
  7. 使用严苛模式打破Android4.0以上平台应用中UI主线程的“独断专行”
  8. linux c语言画太极,利用C语言的Cairo图形库绘制太极图实例教程
  9. 【转】Linux 的启动流程
  10. 江苏计算机二级msoffice高级应用,计算机二级考试MSOffice高级应用
  11. 机器学习-随机森林之回归
  12. 计算机考试中的移动,WPSOffice2002中表格的移动方法
  13. Jetpack:Cannot create an instance of class myViewModel...解决方法
  14. c语言flappy bird编程,C语言实现Flappy Bird小游戏
  15. JAVA公司网站系统毕业设计 开题报告
  16. 习题:一圆型游泳池如图所示,现在需在其周围建一圆型过道,并在其四周围上栅栏。栅栏价格为35元/米,过道造价为20元/平方米。过道宽度为3米,游泳池半径由键盘输入。要求编程计算并输出过道和栅栏的造价。
  17. 中石油大学22春季《大学英语(四)#》第一阶段在线作业
  18. 数据可视化之小提琴图(原理+Python代码)
  19. Shell监控jvm发短信
  20. 4.2 图书借阅系统数据库设计 --MySQL

热门文章

  1. MIT 入梦实验室 梦相关新闻
  2. 微信小程序中的图片处理
  3. XML与Json解析
  4. 警惕 CONFIG+=ordered
  5. idea 全局查找快捷键
  6. v4l2及gstreamer使用指南
  7. ubuntu大于4T分区 12T硬盘分区
  8. python mongodb_Python操作MongoDB文档数据库
  9. 光标自动定位到输入框
  10. 用 canvas 做一个 DVD 待机动画