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)//第一个字节不能为0
            return 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();//左边的padding
        chunk.mPaddings.right = byteBuffer.getInt();//右边的padding
        chunk.mPaddings.top = byteBuffer.getInt();//上边的padding
        chunk.mPaddings.bottom = byteBuffer.getInt();//下边的padding
        // skip 4 bytes
        byteBuffer.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,这里只有一个线段,所以都是2
    val horizontalEndpointsSize = 2
    val verticalEndpointsSize = 2

    //这里计算的 arraySize 是 int 值,最终占用的字节数是 arraySize * 4
    val 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 bytes
    byteBuffer.putInt(0)
    byteBuffer.putInt(0)

    //Note: 目前还没搞清楚,发现都 byteBuffer.putInt(0),也没问题。
    //左右padding
    byteBuffer.putInt(mRectPadding.left)
    byteBuffer.putInt(mRectPadding.right)
    //上下padding
    byteBuffer.putInt(mRectPadding.top)
    byteBuffer.putInt(mRectPadding.bottom)

    //byteBuffer.putInt(0)
    //byteBuffer.putInt(0)
    //上下padding
    //byteBuffer.putInt(0)
    //byteBuffer.putInt(0)

    //skip 4 bytes
    byteBuffer.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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/132871.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

FM3793A-高性能PWM控制芯片 超低成本18W-20W 恒功率PD快充

产品描述&#xff1a; FM3793A是一款应用于离线反激式转换器中的高性能电流模式PWM控制器。在 FM3793A中&#xff0c;PWM开关频率最大为65KHz。在轻载和空载条件下&#xff0c;该FM3793A启动间歇模式从而降低开关频率。FM3793A具有丰富的芯片异常状况保护功能&#xff0c;如欠压…

力扣:160. 相交链表(Python3)

题目&#xff1a; 给你两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点&#xff0c;返回 null 。 图示两个链表在节点 c1 开始相交&#xff1a; 题目数据 保证 整个链式结构中不存在环。 注意&#xff0c;…

基于insightface实现的人脸检测,人脸识别,insightface源码讲解。

目录 1.搭建insightface需要的环境 2.下载insightface工程 3.代码工程文件讲解 3.1 python-package 3.2 进行测试 3.3 examples 4. 人脸识别 5.代码理解&#xff1a; 1.搭建insightface需要的环境 埋个坑&#xff0c;后续再写&#xff0c;笔者在安装过程中遇到了一些问题。…

人工智能基础——Python:运行效率与时间复杂度

人工智能的学习之路非常漫长&#xff0c;不少人因为学习路线不对或者学习内容不够专业而举步难行。不过别担心&#xff0c;我为大家整理了一份600多G的学习资源&#xff0c;基本上涵盖了人工智能学习的所有内容。点击下方链接,0元进群领取学习资源,让你的学习之路更加顺畅!记得…

浅析CC中的点云配准为什么效果好于PCL?

公众号致力于分享点云处理&#xff0c;SLAM&#xff0c;三维视觉&#xff0c;高精地图相关的文章与技术&#xff0c;欢迎各位加入我们&#xff0c;一起交流一起进步,有兴趣的可联系微信&#xff1a;cloudpoint9527。本文来自点云PCL博主的分享&#xff0c;未经作者允许请勿转载…

最新大麦订单生成器 大麦订单图一键生成

1、8.6全新版 本次更新了四种订单模板生成 多模板自由切换 2、在软件中输入生成的信息&#xff0c;这里输入的是商品信息&#xff0c;选择生成的商品图片&#xff0c;最后生成即可 新版大麦订单生成 四种模板图样式展示 这个样式图就是在大麦生成完的一个订单截图&#xff…

大数据毕业设计选题推荐-生产大数据平台-Hadoop-Spark-Hive

✨作者主页&#xff1a;IT毕设梦工厂✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Py…

内网安全-基础设施构建-cobaltstrike远控工具beacon使用

kali在CS文件目录下&#xff0c;打开终端,运行命令&#xff1a; /teamserver 192.168.77.128 123456 在windows中双击bat文件&#xff1a; 填写图下信息&#xff1a; 双击运行&#xff0c;CS上线 自查方法&#xff1a;1、kali与物理机可互通 2、物理机与windows10跳板…

[SHCTF]web方向wp

[SHCTF]web方向wp [WEEK1]babyRCE题目源码wp [WEEK1]1zzphp题目源码wp [WEEK1]ez_serialize题目源码wp [WEEK1]登录就给flag题目wp [WEEK1]生成你的邀请函吧~题目源码wp [WEEK1]飞机大战题目wp [WEEK1]ezphp题目源码wp [WEEK2]no_wake_up题目源码wp [WEEK2]MD5的事就拜托了题目…

人工智能在汽车业应用的五项挑战

在汽车行业扩展人工智能应用时需要注意的问题 随着更多企业投资于汽车人工智能 (AI) 解决方案&#xff0c;我们也愈加接近大规模部署 5 级全自动驾驶汽车。汽车行业的组织如果希望加入这场 AI 带来的颠覆性变革&#xff0c;就应该已提前考虑如何成功和大规模地将人工智能部署到…

C语言每日一题(28) 反转链表

牛客网 BM1 反转链表 题目描述 描述 给定一个单链表的头结点pHead(该头节点是有值的&#xff0c;比如在下图&#xff0c;它的val是1)&#xff0c;长度为n&#xff0c;反转该链表后&#xff0c;返回新链表的表头。 数据范围&#xff1a; 0≤n≤1000 要求&#xff1a;空间复…

LCD1602设计(1)

本文为博主 日月同辉&#xff0c;与我共生&#xff0c;csdn原创首发。希望看完后能对你有所帮助&#xff0c;不足之处请指正&#xff01;一起交流学习&#xff0c;共同进步&#xff01; > 发布人&#xff1a;日月同辉,与我共生_单片机-CSDN博客 > 欢迎你为独创博主日月同…

stm32超声波测距不准的解决方法(STM32 delay_us()产生1us)

首先要说明一下原理&#xff1a;使用stm32无法准确产生1us的时间&#xff0c;但是超声波测距一定要依赖时间&#xff0c;时间不准&#xff0c;距离一定不准&#xff0c;这是要肯定的&#xff0c;但是在不准确的情况下&#xff0c;要测量一个比较准确的时间&#xff0c;那么只能…

JavaScript从入门到精通系列第三十三篇:详解正则表达式语法(二)

文章目录 一&#xff1a;正则表达式 1&#xff1a; 检查一个字符串中是否有. 2&#xff1a;第二种关键表达 3&#xff1a;第三种关键表达 ​编辑4&#xff1a;第四种关键表达 5&#xff1a;第五种关键表达 6&#xff1a;第六种关键表达 二&#xff1a;核心表达二 1&am…

在程序中链接静态库

现在我们把上面src目录中的add.cpp、div.cpp、mult.cpp、sub.cpp编译成一个静态库文件libcalc.a。 add_library(库名称 STATIC 源文件1 [源文件2] ...) link_libraries(<static lib> [<static lib>...]) 参数1&#xff1a;指定出要链接的静态库的名字 可以是全…

Postgres的级数生成函数generate_series应用

Postgres的级数生成函数generate_series应用 引用&#xff1a;http://postgres.cn/docs/12/functions-srf.html 函数文档 函数 参数类型 返回类型 描述 generate_series(start, stop) int、bigint或者numeric setof int、setof bigint或者setof numeric&#xff08;与参数类型相…

公司注册股东选择几个人合适?

创业初期很多创业者都会选择有注册有限责任公司&#xff0c;有限责任由五十个以下的股东出资设立&#xff0c;每个股东以其所认缴的出资额为限对公司承担有限责任。那么问题来了股东人数选择几个最合适呢&#xff0c;下面上海注册公司网&#xff08;www.91kaiye.cn&#xff09;…

AMD64内存属性详解

本文参考文档为AMD64 Architecture Programmer’s Manual Volume 2: System Programming&#xff0c;版本号3.41&#xff0c;这不是对原英文文档的翻译&#xff0c;但是所有内容的排版都是根据原手册的排版来的&#xff0c;如有与官方文档冲突的内容&#xff0c;以官方文档为准…

【C++破局】C++内存管理之new与deleted剖析

​作者主页 &#x1f4da;lovewold少个r博客主页 ⚠️本文重点&#xff1a;c内存管理部分知识点梳理 &#x1f449;【C-C入门系列专栏】&#xff1a;博客文章专栏传送门 &#x1f604;每日一言&#xff1a;花有重开日&#xff0c;人无再少年&#xff01; 目录 C/C的内存分配机…

向量的点积和外积

参考&#xff1a;https://www.cnblogs.com/gxcdream/p/7597865.html 一、向量的内积&#xff08;点乘&#xff09; 定义&#xff1a; 两个向量a与b的内积为 ab |a||b|cos∠(a, b)&#xff0c;特别地&#xff0c;0a a0 0&#xff1b;若a&#xff0c;b是非零向量&#xff0c;…