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

news2024/12/23 5:18:46

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用Redis实现缓存及对应问题解决

一、为什么需要Redis作缓存&#xff1f; 在业务场景中&#xff0c;如果有些数据需要极高频的存取&#xff0c;每次都要在mysql中查询的话代价太大&#xff0c;假如有一个存在于客户端和mysql之间的存储空间&#xff0c;每次可以在这空间中进行存取操作&#xff0c;就会减轻mys…

Docker配置Nginx反向代理

文章目录 1.部署微程序到docker中1.1 dockerfile文件1.2 依据自定义的dockerfile文件创建docker镜像1.3 创建容器1.4 测试 2.在docker中安装Nginx2.1 安装Nginx镜像2.2 获取Nginx配置文件并将其同步到宿主电脑指定位置中安装nginx容器删除nginx容器 2.3 安装Nginx容器并数据挂载…

C++: 类和对象(中) (构造函数, 析构函数, 拷贝构造函数, 赋值重载, 取地址重载)

文章目录 1. 类的6个默认成员函数2. 构造函数构造函数概念构造函数特性特性1,2,3,4特性5特性6特性7 3. 析构函数析构函数概念析构函数特性特性1,2,3,4特性5特性6 4. 拷贝构造函数拷贝构造函数概念拷贝构造函数特性特性1,2特性3特性4特性5 5. 运算符重载一般运算符重载赋值运算符…

mysql安装成功

先在官网下载 地址&#xff1a;MySQL :: Download MySQL Community Server下载的 下载的这个 解压后 zip格式是自己解压&#xff0c;解压缩之后其实MySQL就可以使用了&#xff0c;但是要进行环境变量配置 我的电脑->属性->高级->环境变量->系统变量 选择Path,在其…

java版直播商城免费搭建平台规划及常见的营销模式+电商源码+小程序+三级分销+二次开发

1. 涉及平台 平台管理、商家端&#xff08;PC端、手机端&#xff09;、买家平台&#xff08;H5/公众号、小程序、APP端&#xff08;IOS/Android&#xff09;、微服务平台&#xff08;业务服务&#xff09; 2. 核心架构 Spring Cloud、Spring Boot、Mybatis、Redis 3. 前端框架…

二十、泛型(3)

本章概要 构建复杂模型泛型擦除 C 的方式迁移兼容性擦除的问题边界处的动作 构建复杂模型 泛型的一个重要好处是能够简单安全地创建复杂模型。例如&#xff0c;我们可以轻松地创建一个元组列表&#xff1a; TupleList.java import java.util.ArrayList;public class TupleL…

深入理解强化学习——多臂赌博机:乐观初始值

分类目录&#xff1a;《深入理解强化学习》总目录 目前为止我们讨论的所有方法都在一定程度上依赖于初始动作值 Q 1 ( a ) Q_1(a) Q1​(a)的选择。从统计学角度来说&#xff0c;这些方法&#xff08;由于初始估计值&#xff09;是有偏的。对于采样平均法来说&#xff0c;当所有…

软件专业毕业生的如何找工作?——加速度jsudo

据统计&#xff0c;2023届全国高校毕业生预计达到1158万人&#xff0c;同比增长82万人。根据某大学的统计数据&#xff0c;IT专业的就业率在过去五年中保持了稳定增长的趋势&#xff0c;平均超过90%。 IT行业的薪资水平相对较高&#xff0c;也让很多高校和培训机构愿意投入更多…

Sybase连接详解

Sybase连接详解 Sybase连接详解摘要一、JDBC基础1.1 JDBC简介1.2 JDBC驱动程序 二、配置Sybase JDBC连接2.1 连接Sybase数据库2.2 验证Sybase JDBC连接2.3 获取Sybase数据库表信息和注释2.4 根据表名获取Sybase字段信息和注释2.5 执行SQL查询2.6 插入数据2.7 执行Sybase存储过程…

ElasticSearch离线安装

1. 上传和解压软件 将elasticsearch-7.11.2-linux-x86_64.tar.gz和kibana-7.11.2-linux-x86_64.tar.gz 上传到/data/es目录 解压文件 tar -zxvf elasticsearch-7.11.2-linux-x86_64.tar.gz tar -zxvf kibana-7.11.2-linux-x86_64.tar.gz 2. 创建es用户 因为安全问题&#xff…

windows好玩的cmd命令

颜色 后边的数字查表吧,反正我是喜欢一个随机的数字 color 01MAC getmac /v更新主机IP地址 通过DHCP更新 ipconfig /release ipconfig /renew改标题 title code with 你想要的标题

如何实现Word文档中的书签双向定位

工作中&#xff0c;经常需要拟定合同&#xff0c;一般都有固定的模板&#xff0c;在特定的位置填写内容。通过zOffice编辑合同文件时&#xff0c;可以在模板需要填写的位置预设书签&#xff0c;配合zOffice SDK使用&#xff0c;利用zOffice书签双向定位的特性&#xff0c;更方便…

3.5、Linux:命令行git的使用

个人主页&#xff1a;Lei宝啊 愿所有美好如期而遇 在Linux Centos7.6下安装git yum -y install git 注册一个gitee账号 进去注册就好&#xff0c;记住自己的用户名和密码。 创建一个仓库 点击复制&#xff0c;接着就可以在Linux上使用了 git clone git clone 刚才复制的地…

《UML和模式应用(原书第3版)》2024新修订译本部分截图

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 机械工业出版社即将在2024春节前后推出《UML和模式应用&#xff08;原书第3版&#xff09;》的典藏版。 受出版社委托&#xff0c;UMLChina审校了原中译本并做了一些修订。同比来说&a…

Qwt QwtThermo绘制温度计

1.简介 QwtThermo 是一个基于 Qt 框架的类库&#xff0c;用于创建温度计控件。它提供了一些方便的功能来展示和处理温度计相关的数据。 QwtThermo 添加了特定于温度计的功能。 使用 QwtThermo&#xff0c;可以实现以下功能&#xff1a; 设置温度范围&#xff1a;可以通过设置…

golang正则获取中括号中的内容

reg : regexp.MustCompile("【(.*?)】") //userInfo姓名:【AAA姓名】证件类型:【BBB身份证】证件号码:【122456789458】tempData reg.FindAllStringSubmatch(userInfo, -1)for k, v : range tempData {if k 0 {tempReleaseUser.Name v[1]//AAA姓名} else if k 1…

选择企业云盘?品牌推荐和评价解析

企业云盘是如今热门的企业协作工具&#xff0c;为企业提供了文件存储、文件共享服务。市面上的企业云盘千千万&#xff0c;到底哪个企业云盘好用&#xff1f;哪些品牌值得信赖呢&#xff1f; 好用的企业云盘&#xff0c;不能不提&#xff0c;Zoho Workdrive企业云盘为企业提供…

养老院展示服务预约小程序的作用是什么

养老院无论在哪个城市都有很高需求度&#xff0c;不少银发人群会因为种种原因而前往&#xff0c;而养老院近些年来各种服务也比较完善&#xff0c;增加了客户信任度及接受度&#xff0c;但对院方来说&#xff0c;也存在着一些痛点&#xff1a; 1、品牌传播服务呈现难 养老院也…

自己实现一个自动检测网卡状态,并设置ip地址

阅读本文前&#xff0c;请先学习下面几篇文章 《搞懂进程组、会话、控制终端关系&#xff0c;才能明白守护进程干嘛的&#xff1f;》 《简简单单教你如何用C语言列举当前所有网口&#xff01;》 《Linux下C语言操作网卡的几个代码实例&#xff01;特别实用》 《安卓如何设置…

使用 Python 从头开始​​编写 Canny 边缘检测算法。

原始图像&#xff08;左&#xff09;和检测到的边缘&#xff08;右&#xff09;| 图片由作者提供 一、说明 在本文中&#xff0c;我将解释有关 Canny 边缘检测的所有内容&#xff0c;以及在不使用一些预先编写的库的情况下对算法进行编码&#xff0c;以便您能够了解真正发生的情…