当前位置: 首页 > news >正文

Android 使用.9图 NinePatchDrawable实现动态聊天气泡

最近一段时间,在做一个需求,需要实现一个聊天气泡的动画效果,如下图所示:

GitHub源码demo ,建议下载demo,运行查看。

动态聊天气泡动画

在这里插入图片描述

静态聊天气泡

在这里插入图片描述

经过一段时间调研,实现方案如下:

实现方案

  • 从服务端下载zip文件,文件中包含配置文件和多张png图片,配置文件定义了图片的横向拉伸拉伸区域、纵向拉伸区域、padding信息等。
  • 从本地加载配置文件,加载多张png图片为bitmap。
  • 将bitmap存储在内存里。LruCache,避免多次解析。
  • 根据配置文件,将png图片转换为.9图,NinePatchDrawable。
  • 使用多张NinePatchDrawable创建一个帧动画对象AnimationDrawable
  • 将AnimationDrawable设置为控件的背景,并让AnimationDrawable播放动画,执行一定的次数后停止动画。

其中的难点在于第3步,将png图片转换为.9图 NinePatchDrawable

NinePatchDrawable 的构造函数。

/*** Create drawable from raw nine-patch data, setting initial target density* based on the display metrics of the resources.*/
public NinePatchDrawable(Resources res,Bitmap bitmap,byte[]chunk,Rect padding,String srcName){this(new NinePatchState(new NinePatch(bitmap,chunk,srcName),padding),res);
}

其中最关键的点在于构建byte[] chunk参数。通过查看这个类NinePatchChunk.java,并参阅了许多博客,通过反向分析NinePatchChunk类的deserialize方法,得到了如何构建byte[] chunk的方法。

// See "frameworks/base/include/utils/ResourceTypes.h" for the format of
// NinePatch chunk.
class NinePatchChunk {public static final int NO_COLOR = 0x00000001;public static final int TRANSPARENT_COLOR = 0x00000000;public Rect mPaddings = new Rect();public int mDivX[];public int mDivY[];public int mColor[];private static void readIntArray(int[] data, ByteBuffer buffer) {for (int i = 0, n = data.length; i < n; ++i) {data[i] = buffer.getInt();}}private static void checkDivCount(int length) {if (length == 0 || (length & 0x01) != 0) {throw new RuntimeException("invalid nine-patch: " + length);}}//注释1处,解析byte[]数据,构建NinePatchChunk对象public static NinePatchChunk deserialize(byte[] data) {ByteBuffer byteBuffer =ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());byte wasSerialized = byteBuffer.get();if (wasSerialized == 0)//第一个字节不能为0return null;NinePatchChunk chunk = new NinePatchChunk();chunk.mDivX = new int[byteBuffer.get()];//第二个字节为x方向上的切割线的个数chunk.mDivY = new int[byteBuffer.get()];//第三个字节为y方向上的切割线的个数chunk.mColor = new int[byteBuffer.get()];//第四个字节为颜色的个数checkDivCount(chunk.mDivX.length);//判断x方向上的切割线的个数是否为偶数checkDivCount(chunk.mDivY.length);//判断y方向上的切割线的个数是否为偶数// skip 8 bytes,跳过8个字节byteBuffer.getInt();byteBuffer.getInt();//注释2处,处理padding,发现都设置为0也可以。chunk.mPaddings.left = byteBuffer.getInt();//左边的paddingchunk.mPaddings.right = byteBuffer.getInt();//右边的paddingchunk.mPaddings.top = byteBuffer.getInt();//上边的paddingchunk.mPaddings.bottom = byteBuffer.getInt();//下边的padding// skip 4 bytesbyteBuffer.getInt();//跳过4个字节readIntArray(chunk.mDivX, byteBuffer);//读取x方向上的切割线的位置readIntArray(chunk.mDivY, byteBuffer);//读取y方向上的切割线的位置readIntArray(chunk.mColor, byteBuffer);//读取颜色return chunk;}
}

注释1处,解析byte[]数据,构建NinePatchChunk对象。我们添加了一些注释,意思已经很清晰了。

然后我们根据这里类来构建byte[] chunk参数。

private fun buildChunk(): ByteArray {// 横向和竖向端点的数量 = 线段数量 * 2,这里只有一个线段,所以都是2val horizontalEndpointsSize = 2val verticalEndpointsSize = 2//这里计算的 arraySize 是 int 值,最终占用的字节数是 arraySize * 4val arraySize = 1 + 2 + 4 + 1 + horizontalEndpointsSize + verticalEndpointsSize + COLOR_SIZE//这里乘以4,是因为一个int占用4个字节val byteBuffer = ByteBuffer.allocate(arraySize * 4).order(ByteOrder.nativeOrder())byteBuffer.put(1.toByte()) //第一个字节无意义,不等于0就行byteBuffer.put(horizontalEndpointsSize.toByte()) //mDivX x数组的长度byteBuffer.put(verticalEndpointsSize.toByte()) //mDivY y数组的长度byteBuffer.put(COLOR_SIZE.toByte()) //mColor数组的长度// skip 8 bytesbyteBuffer.putInt(0)byteBuffer.putInt(0)//Note: 目前还没搞清楚,发现都 byteBuffer.putInt(0),也没问题。//左右paddingbyteBuffer.putInt(mRectPadding.left)byteBuffer.putInt(mRectPadding.right)//上下paddingbyteBuffer.putInt(mRectPadding.top)byteBuffer.putInt(mRectPadding.bottom)//byteBuffer.putInt(0)//byteBuffer.putInt(0)//上下padding//byteBuffer.putInt(0)//byteBuffer.putInt(0)//skip 4 bytesbyteBuffer.putInt(0)//mDivX数组,控制横向拉伸的线段数据,目前只支持一个线段patchRegionHorizontal.forEach {byteBuffer.putInt(it.start * width / originWidth)byteBuffer.putInt(it.end * width / originWidth)}//mDivY数组,控制竖向拉伸的线段数据,目前只支持一个线段patchRegionVertical.forEach {byteBuffer.putInt(it.start * height / originHeight)byteBuffer.putInt(it.end * height / originHeight)}//mColor数组for (i in 0 until COLOR_SIZE) {byteBuffer.putInt(NO_COLOR)}return byteBuffer.array()
}

完整的类请参考 AnimationDrawableFactory.kt

使用

完整的使用请查看 ChatAdapter 类。

AnimationDrawableFactory 支持从文件构建动画,也支持从Android的资源文件夹构建动画。

!!!注意,从文件构建动画,需要将请把工程下的bubbleframe文件夹拷贝到手机的Android/data/包名/files
目录下val fileDir = getExternalFilesDir(null),否则会报错。

从文件构建动画

 return AnimationDrawableFactory(context).setDrawableDir(pngsDir)//图片文件所在的目录.setHorizontalStretchBean(PatchStretchBean(60, 61))//水平拉伸区域.setVerticalStretchBean(PatchStretchBean(52, 53))//垂直拉伸区域.setOriginSize(128, 112)//原始图片大小.setPadding(Rect(31, 37, 90, 75))//padding区域.setHorizontalMirror(isSelf)//是否水平镜像,不是必须的.setScaleFromFile(true)//是否从文件中读取图片的缩放比例,不是必须的.setFinishCount(3)//动画播放次数.setFrameDuration(100)//每帧动画的播放时间.buildFromFile()

这里注意一下:因为文件中的图片是一倍图,所以这里需要放大,所以设置了setScaleFromFile(true)
如果文件中的图片是3倍图,就不需要设置这个参数了。如果需要更加精细的缩放控制,后面再增加支持。

从Android的资源文件夹构建动画


private val resIdList = mutableListOf<Int>().apply {add(R.drawable.bubble_frame1)add(R.drawable.bubble_frame2)add(R.drawable.bubble_frame3)add(R.drawable.bubble_frame4)add(R.drawable.bubble_frame5)add(R.drawable.bubble_frame6)add(R.drawable.bubble_frame7)add(R.drawable.bubble_frame8)add(R.drawable.bubble_frame9)add(R.drawable.bubble_frame10)add(R.drawable.bubble_frame11)add(R.drawable.bubble_frame12)
}/*** 从正常的资源文件加载动态气泡*/
return AnimationDrawableFactory(context).setDrawableResIdList(resIdList)//图片资源id列表.setHorizontalStretchBean(PatchStretchBean(60, 61))//水平拉伸区域.setVerticalStretchBean(PatchStretchBean(52, 53))//垂直拉伸区域.setOriginSize(128, 112)//原始图片大小.setPadding(Rect(31, 37, 90, 75))//padding区域.setHorizontalMirror(isSelf)//是否水平镜像,不是必须的.setFinishCount(3)//动画播放次数,不是必须的.setFrameDuration(100)//每帧动画的播放时间,不是必须的.buildFromResource()

有时候可能我们只需要构建静态气泡,也就是只需要一张 NinepatchDrawable,我们提供了一个类来构建静态气泡,NinePatchDrawableFactory.kt

从文件加载

return NinePatchDrawableFactory(context).setDrawableFile(pngFile)//图片文件.setHorizontalStretchBean(PatchStretchBean(60, 61))//水平拉伸区域.setVerticalStretchBean(PatchStretchBean(52, 53))//垂直拉伸区域.setOriginSize(128, 112)//原始图片大小.setScaleFromFile(true)//是否从文件中读取图片的缩放比例,不是必须的.setPadding(Rect(31, 37, 90, 75))//padding区域.setHorizontalMirror(isSelf)//是否水平镜像,不是必须的.buildFromFile()

从资源加载

return NinePatchDrawableFactory(context).setDrawableResId(R.drawable.bubble_frame1)//图片资源id.setHorizontalStretchBean(PatchStretchBean(60, 61))//水平拉伸区域.setVerticalStretchBean(PatchStretchBean(52, 53))//垂直拉伸区域.setOriginSize(128, 112)//原始图片大小.setPadding(Rect(31, 37, 90, 75))//padding区域.setHorizontalMirror(isSelf)//是否水平镜像,不是必须的.buildFromResource()

padding 取值

如图所示:宽高是128*112。横向padding取值为31、90,纵向padding取值为37、75。

在这里插入图片描述

其他

在实现过程中发现Android 的 帧动画 AnimationDrawable无法控制动画执行的次数。最后自定义了一个类,CanStopAnimationDrawable.kt 解决。

参考链接:

  • Carson带你学Android:关于逐帧动画的使用都在这里了!-腾讯云开发者社区-腾讯云
  • 聊天气泡图片的动态拉伸、镜像与适配 - 掘金
  • Android 点九图机制讲解及在聊天气泡中的应用 - 掘金
  • Android动态布局入门及NinePatchChunk解密
  • Android点九图总结以及在聊天气泡中的使用-腾讯云开发者社区-腾讯云
  • https://developer.android.com/studio/write/draw9patch?utm_source=android-studio&hl=zh-cn

相关文章:

Android 使用.9图 NinePatchDrawable实现动态聊天气泡

最近一段时间&#xff0c;在做一个需求&#xff0c;需要实现一个聊天气泡的动画效果&#xff0c;如下图所示&#xff1a; GitHub源码demo &#xff0c;建议下载demo&#xff0c;运行查看。 动态聊天气泡动画 静态聊天气泡 经过一段时间调研&#xff0c;实现方案如下: 实现方…...

力扣 LCR 024. 反转链表两种解法

目录 1.解题思路Ⅰ2.代码实现Ⅰ3.解题思路Ⅱ4.代码实现Ⅱ 1.解题思路Ⅰ 利用头插法&#xff0c;遍历数组将后面的元素头插到前面的元素. 2.代码实现Ⅰ struct ListNode* reverseList(struct ListNode* head) { struct ListNode*curhead;;struct ListNode*newheadNULL;whil…...

掌握Capture One 23 Pro,打造专业级图片编辑体验!

作为一位摄影师&#xff0c;您是否曾经为自己的照片无法达到预期效果而烦恼&#xff1f;或者您是否在寻找一种能够让您轻松处理和编辑照片的工具&#xff1f;如果是&#xff0c;那么您一定不能错过Capture One 23 Pro这款图片编辑软件&#xff01; Capture One 23 Pro的特点 …...

MFC-TCP网络编程服务端-Socket

目录 1、通过Socket建立服务端&#xff1a; 2、UI设计&#xff1a; 3、代码的实现&#xff1a; &#xff08;1&#xff09;、CListenSocket类 &#xff08;2&#xff09;、CConnectSocket类 &#xff08;3&#xff09;、CTcpServerDlg类 1、通过Socket建立服务端&#xff…...

ChatGPT辅助下的小组学习

1 网上分享会-主题 1.9曾子曰&#xff1a;“慎终追远&#xff0c;民德归厚矣。” Master Zeng said:“Be circumspect in funerary services and continue sacrifices to the distant ancestors, and the virtue (de 德) of the common people will thrive.” 2 过程记录 听…...

Linux相关命令

切换root用户&#xff1a;sudo su 串口功能测试&#xff1a;cutecom 某某驱动查询&#xff1a;nvidia-smi #xxx-smi查询某某驱动 在线安装某某程序&#xff1a;apt install xxx 设置文件权限chmod 常用&#xff1a;chmod 777 sudo chmod 600 &#xff08;只有所有者…...

详解卷积神经网络结构

前言 卷积神经网络是以卷积层为主的深度网路结构&#xff0c;网络结构包括有卷积层、激活层、BN层、池化层、FC层、损失层等。卷积操作是对图像和滤波矩阵做内积&#xff08;元素相乘再求和&#xff09;的操作。 1. 卷积层 常见的卷积操作如下&#xff1a; 卷积操作解释图解…...

java读取pdf数据

目录 读取方式有两种: 方式一: 方式一所需要的maven依赖如下: 方式一读取的Java代码如下:<...

arcmap / arcgis 安装教程

ArcGIS 10.8 for Desktop 完整安装教程&#xff08;含win7/8/10 32/64位下载地址亲测可用汉化&#xff09; | 麻辣GIS (malagis.com) 关于GIS语言汉化包&#xff08;中文&#xff09;安装失败的解决办法_arcgis中文语言包_miumiuniya的博客-CSDN博客 检查安装路径&#xff1a;…...

CMake中的变量: 改变构建行为的变量

文章目录 变量名称描述BUILD_SHARED_LIBS全局标志&#xff0c;用于在启用时使add_library()创建共享库。 如果存在并且为true&#xff0c;则这将导致所有库被构建为共享库&#xff0c;除非该库被明确添加为静态库。这个变量通常作为option()添加到项目中&#xff0c;这样项目的…...

台式电脑怎么无损备份迁移系统到新硬盘(使用傲梅,免费的就可以)

文章目录 前言一、想要将源硬盘上的系统原封不动地迁移到新硬盘上二、准备工作2.具体步骤 总结 前言 半路接手公司一台台式电脑&#xff0c;C盘&#xff08;120g&#xff09;爆红&#xff0c;仅剩几个G&#xff0c;优化了几次&#xff0c;无果后。准备换一个大一点的增到500g。…...

【紫光同创国产FPGA教程】【PGC1/2KG第七章】7.数字钟实验例程

本原创教程由深圳市小眼睛科技有限公司创作&#xff0c;版权归本公司所有&#xff0c;如需转载&#xff0c;需授权并注明出处 适用于板卡型号&#xff1a; 紫光同创PGC1/2KG开发平台&#xff08;盘古1K/2K&#xff09; 一&#xff1a;盘古1K/2K开发板&#xff08;紫光同创PGC…...

【星海随笔】git的使用

1.在终端&#xff0c;检查git是否安装 git --version 2.没有安装的话去&#xff0c;官网&#xff0c;下载git 3.一直点下一步即可 4.安装后在终端检查git是否安装好 5.设置用户名和邮件地址(最好和GitHub的用户名/邮箱保持一致) git config --global user.name “自己的用户名”…...

安卓常见设计模式------装饰器模式(Kotlin版)

1. W1 是什么&#xff0c;什么是装饰器模式&#xff1f; 思想&#xff1a;动态地给对象添加额外的功能&#xff0c;通过将对象包装在一个装饰器类中&#xff0c;使装饰器类在不改变原始对象结构的情况下&#xff0c;扩展其功能。 2. W2 为什么&#xff0c;为什么需要使用装饰…...

将网站上的点击作为转化操作进行跟踪-官方指导文档

您可以使用转化跟踪功能&#xff0c;在用户点击您网站上的某个按钮或链接时进行跟踪。例如&#xff0c;您可以在用户点击“立即购买”按钮或点击您移动网站上的电话号码时进行跟踪。 本文介绍如何添加和修改转化跟踪代码&#xff0c;以便跟踪客户在您网站上的点击操作。如果希…...

Go相关命令说明

目录 Go相关命令说明go mod tidy &#xff1a;清理未使用依赖项&#xff0c;并更新模块文件主要功能好处 go clean -modcache &#xff1a;清除模块缓存go clean -testcache &#xff1a;清除测试缓存go test -v ./client &#xff1a;测试当前目录下client目录中的所有测试函数…...

3D全景技术,为我们打开全新宣传领域

随着科技的发展&#xff0c;3D全景技术正在融入我们的生活&#xff0c;这种全新视觉体验方式为我们打开了一扇全新的宣传领域&#xff0c;可以让我们多方位、多视角地探索各个行业&#xff0c;无论是对教育、商业、还是其他领域&#xff0c;都产生了深远的影响。 3D全景技术结合…...

【3D 图像分割】基于 Pytorch 的 VNet 3D 图像分割10(测试推理篇)

对于直接将裁剪的patch,一个个的放到训练好的模型中进行预测,这部分代码可以直接参考前面的训练部分就行了。其实说白了,就是验证部分。不使用dataloader的方法,也只需要修改少部分代码即可。 但是,这种方法是不end to end的。我们接下来要做的,就是将一个CT数组作为输入…...

PyCharm+Miniconda3安装配置教程

PyCharm是Python著名的Python集成开发环境&#xff08;IDE&#xff09; conda有Miniconda和Anaconda&#xff0c;前者应该是类似最小化版本&#xff0c;后者可能是功能更为强大的版本&#xff0c;我们这里安装Miniconda 按官方文档的说法conda相当于pip与virtualenv的结合&am…...

【慢SQL性能优化】 一条SQL的生命周期 | 京东物流技术团队

一、 一条简单SQL在MySQL执行过程 一张简单的图说明下&#xff0c;MySQL架构有哪些组件和组建间关系&#xff0c;接下来给大家用SQL语句分析 例如如下SQL语句 SELECT department_id FROM employee WHERE name Lucy AND age > 18 GROUP BY department_id其中name为索引&a…...

SD-PPP:终极Photoshop AI插件完整指南 - 让AI绘图与Photoshop无缝协作

SD-PPP&#xff1a;终极Photoshop AI插件完整指南 - 让AI绘图与Photoshop无缝协作 【免费下载链接】sd-ppp A Photoshop AI plugin 项目地址: https://gitcode.com/gh_mirrors/sd/sd-ppp 还在为AI绘图和Photoshop之间的繁琐切换而烦恼吗&#xff1f;SD-PPP这款革命性的P…...

Jasminum终极指南:3步解决Zotero中文文献管理的核心痛点

Jasminum终极指南&#xff1a;3步解决Zotero中文文献管理的核心痛点 【免费下载链接】jasminum A Zotero add-on to retrive CNKI meta data. 一个简单的Zotero 插件&#xff0c;用于识别中文元数据 项目地址: https://gitcode.com/gh_mirrors/ja/jasminum 你是否曾为中…...

GLM-4.1V-9B-Base赋能前端设计:基于VSCode的智能UI/UX原型生成工具

GLM-4.1V-9B-Base赋能前端设计&#xff1a;基于VSCode的智能UI/UX原型生成工具 1. 设计师与开发者的效率困境 想象一下这样的场景&#xff1a;设计师小王刚刚完成了一个精美的移动端界面设计稿&#xff0c;兴奋地发给开发团队。三天后&#xff0c;他看到实现效果时差点没认出…...

告别点灯!用STM32F407的SPI DMA驱动ST7735S TFT屏,让你的UI刷新快人一步

STM32F407 SPI DMA驱动ST7735S TFT屏性能优化实战 在嵌入式UI开发中&#xff0c;流畅的显示效果往往直接影响用户体验。当我们需要在ST7735S这类小型TFT屏上实现动态波形显示或菜单动画时&#xff0c;传统的SPI轮询方式常会遇到帧率低、MCU资源占用高等瓶颈。本文将深入探讨如何…...

企业网盘,基于 .NET 技术开发,用于构建安全高效的文件云存储和云管理平台。

内容目录一、详细介绍二、效果展示1.部分代码2.效果图展示一、详细介绍 企业网盘&#xff0c;基于 .NET 技术开发&#xff0c;用于构建安全高效的文件云存储和云管理平台。 自动同步提供智能化的文件上传、下载及版本更替功能&#xff0c;实现便捷的文件云备份和云共享解决方…...

Cosmos-Reason1-7B参数详解:Top-P=0.95在开放性物理问题中的平衡表现

Cosmos-Reason1-7B参数详解&#xff1a;Top-P0.95在开放性物理问题中的平衡表现 1. 引言 当你让一个AI模型去分析一张图片&#xff0c;判断“这个机器人手臂能安全地拿起那个玻璃杯吗&#xff1f;”&#xff0c;你期望的答案是什么&#xff1f;是一个简单的是或否&#xff0c…...

ANI3DHUMAN:3D人体动画技术的自引导随机采样解析

1. ANI3DHUMAN&#xff1a;基于自引导随机采样的3D人体动画技术解析在数字内容创作领域&#xff0c;3D人体动画一直面临着逼真度与可控性难以兼得的困境。传统运动学方法能精确控制骨骼动作&#xff0c;却无法模拟衣物飘动等自然动态&#xff1b;而基于物理模拟的方案虽能呈现逼…...

抖音视频批量下载工具:免费去水印,轻松保存合集与主页作品

抖音视频批量下载工具&#xff1a;免费去水印&#xff0c;轻松保存合集与主页作品 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser…...

计算机视觉算法优化方法

计算机视觉算法优化方法&#xff1a;提升效率与精度的关键路径 计算机视觉作为人工智能的核心领域之一&#xff0c;广泛应用于自动驾驶、医疗影像、安防监控等场景。随着任务复杂度的提升&#xff0c;算法的计算效率、精度和泛化能力面临巨大挑战。如何优化算法成为研究者关注…...

LangChain与LangGraph实战:从零构建智能体应用与RAG系统

1. 项目概述&#xff1a;从零构建你的第一个智能体应用如果你对AI应用开发感兴趣&#xff0c;尤其是想亲手打造一个能调用工具、有记忆、能自主决策的智能体&#xff08;Agent&#xff09;&#xff0c;那么LangChain和LangGraph这两个框架是你绕不开的利器。我最近花了大量时间…...