Android点九图总结以及在聊天气泡中的使用

1. 点九图介绍

这一块是对点九图的简单介绍,如果对这块已经有了解的话,可以直接跳到2,看看聊天气泡中如何使用点九图。

1.1 点九图出现的原因

首先简单介绍下点九图出现的原因吧,Android为了使用同一张图作为不同数量文字的背景,设计了一种可以指定区域拉伸的图片格式“.9.png”,这种图片格式就是点九图。 注意:这种图片格式只能被使用于Android开发。在ios开发中,可以在代码中指定某个点进行拉伸,而在Android中不行,所以在Android中想要达到这个效果,只能使用点九图。(对大多数时候来说是这样,实际上可以自己构造,后面会稍微提一下,见3.2)

1.2 点九图的本质

点九图的本质实际上是在图片的四周各增加了1px的像素,并使用纯黑(#FF000000)的线进行标记,其它的与原图没有任何区别。可以参考以下图片:

可以看到在该图的四周,均有黑色像素标记,这些标记的作用分别是:

标记位置

含义

左-黑点

纵向拉伸区域

上-黑点

横向拉伸区域

右-黑线

纵向显示区域

下-黑线

横向显示区域

1.3 创建点九图的几个方法

由于点九图的本质也是个图片,只是在周围加了1px的像素,所以你可以使用ps或其它任意支持像素操作的p图工具来将一个普通图片转换为点九图,但是就易用性和可视性来看,推荐使用Draw9patch工具,该工具存在于早期的Android SDK中,如今被集成到了Android studio中,它实际上也是在图片边缘画线,但是在工具中只能在边缘画,且只能画黑线,这样便减少了误操作的可能性。并且在Draw9patch中可以预览结果。 注意:图片四个角的像素点不要画上黑线,否则Android无法识别。

边缘黑线绘制方法

优缺点

ps等p图工具

1. 设计人员可以直接出图2. 不需要安装额外的环境和工具3. 可能会误操作,比如颜色不是纯黑等,导致输出了错误的点九图

Draw9patch工具(推荐)

1. 需要安装jre环境并下载Draw9patch工具,最新的SDK中已经没有了但是在网上可以找到2. 直观方便,不会有误操作

Android Studio

1. 需要设计或者产品同学安装as,并熟悉其操作2. 便于开发人员直接使用

具体如何操作,这里就不多赘述了。

1.4 Android 点九图的基本使用

Android中使用点九图,主要有三种形式,使用res文件夹中的点九图,使用assets文件夹中的点九图以及使用网上拉取的点九图,下面分别看看它们如何使用。

  1. 使用res文件夹中的点九图比较简单,直接将带黑线的点九图放到res文件夹中,就可以按照正常使用res的方法使用了。一般为设置为TextView的背景,便可以根据TextView的内容大小进行拉伸了。
  2. 使用assets文件夹中的点九图稍微复杂一些,这里不能直接放入带黑线的点九图,而是放入一种转换后的点九图,然后在使用时,再由开发主动构造成NinePatchDrawable然后使用。(是不是看不懂,往后看就对了。)
  3. 使用网上拉取的点九图就更复杂了,本篇文章大部分都在讲这一块,有兴趣的就请往下看~。

1.5 Android点九图的解析原理

Android并不是直接使用点九图,而是在编译时将其转换为另外一种格式(见3.1),这种格式是将其四周的黑色像素保存至Bitmap类中的一个名为mNinePatchChunk的byte[]中,并抹除掉四周的这一个像素的宽度;接着在使用时,如果Bitmap的这个mNinePatchChunk不为空,且为9patch chunk(见3.3),则将其构造为NinePatchDrawable,否则将会被构造为BitmapDrawable,最终设置给view,NinePatchDrawable的拉伸主要是通过其draw方法实现的。总而言之,最后打出的包中的点九图,已经不是原来的带黑线的点九图了。

2. 聊天气泡中使用点九图

2.1 遇到的问题和解决方案

先简单说下从网上拉取点九图的过程,首先使用url请求网络数据,并将结果缓存为本地文件,再使用文件流创建Bitmap,接着使用Bitmap创建drawable再交给view使用,最后由view的draw方法调用drawable的draw方法将图片绘制出来。 再看看上面1.5的解析原理,它会带来一个坑,由于聊天气泡需求需要使用url从网络上拉取点九图,如果这个点九图没有经过编译的过程,将其周围的黑线标记放入到png中的一个辅助chunk中,那么在使用这个图作为背景时,会显示出黑线,且不会拉伸。而根据以往的经验,Android是可以直接使用点九图的,因为放到res文件夹中就可以直接使用,所以就将点九图直接上传到服务器上,这时从网上拉取的图片数据是带黑线的图,那么就会出错了。 这时候效果是这样的:

emmmmm,很丑。

当初发现这个问题时,考虑了三个方案来处理

  1. 开发提供工具,产品或设计进行转换后再在配置平台上上传,问题是这个过程全是外包进行处理的,无法保证转换的质量和准确性,因为转换后的图和原图长一样。
  2. 将带黑线的点九图上传到配置平台,平台进行转换后再上传到服务器。这个暂时没有想到有什么大的问题。
  3. 客户端收到带黑线的点九图后,进行处理,问题是没有直接的方法进行转换,需要客户端通过像素级 + byte级的操作,来构造出NinePatchDrawable,过程比较耗时,影响性能和流畅度,并且涉及到的内容太细,后续维护困难。

pass: 其实客户端还有一个解决方法,就是自己根据拉伸区域构造mNinePatchChunk,然后将普通的Bitmap创建为NinePatchDrawable,因为ios的特性,设计会指定一个拉伸点,以及文字显示区域,这两个数据是固定的,也就是说,每个点九图上的黑线是固定的,所以可以根据这些数据来构造一个固定的mNinePatchChunk。这样可以做出一个跟ios实现方式相同的控件。(见3.2)

最后是通过联系手q参考并采用了他们的方案,也就是上面的第一种方法实现的。 (为了避免外包同学出错后无法发现问题,这里如果不是点九图,则上报,用于发现问题)

2.2 最终确定的使用流程

最终确定的实现流程如下图所示:

接下来说说这9个步骤中的遇到问题:

  1. 步骤2中,给9点图画黑线,必须是纯黑色像素,且图片的四个角必须为透明像素点,否则Android会无法识别,且在步骤3中将无法转换。
  2. 步骤3中,将带黑线的点九图转换,可以使用Android SDK自带的aapt工具进行转换,使用命令aapt c -v -S  . -C .\9out,其中.表示当前目录,.\9out表示目标目录,即将当前目录中的带黑线的点九图转换后放到当前目录下的9out文件夹中,9out文件夹该命令会自动创建。为了让外包自动化这个过程,可以将其做成一个工具,用于批量转换。
  3. 步骤4中,上传的过程中不能对转换后的点九图进行压缩(某些配置平台会默认对上传的图片进行压缩),因为转换后的点九图的黑线信息被保存到了png图片的辅助数据块中,这部分数据在压缩过程中会消失,导致最终客户端通过url拉到的图片不是点九图,从而显示错误。
  4. 步骤4中,某些cdn因为省流量,或者其它原因,对图片进行压缩或者转码为webp格式,这样会导致最终通过url拉取的图片不是想要的点九图,从而显示错误。这里要针对不同业务采取不同的处理方式,这里简单说说K歌这里的处理方式,用于借鉴。 首先介绍下目前K歌使用webp的方案: 1. 客户端http请求如果带了accept:image/webp,则服务器认为需要webp,此时会转一份webp格式图片出来,后续请求给客户端的是webp格式图片。 2. 如果http请求里不带webp参数,且图片url是/0(表示原图)结尾,则服务器不会压缩。 所以要保证最终url拉到的图片不是webp格式,且不被压缩,有两个条件: 1. 在这类拉点九图url请求的请求头里不带上accept:image/webp。 2. 拉点九图的url的末尾以/0结尾。
  5. 步骤8中,需要通过Bitmap创建drawable,如果是使用的res文件,Android系统自己会完成这个过程,而如果是网上拉取的图片,则需要自己创建,这部分代码如下: byte[] chunk = bitmap.getNinePatchChunk();if (NinePatch.isNinePatchChunk(chunk)) { NinePatchDrawable ninePatchDrawable = new NinePatchDrawable(bitmap, chunk, new Rect(), null); } else { BitmapDrawable bitmapDrawable = new BitmapDrawable(bitmap) } 这里要看看这个chunk信息是怎么被构造的,以及如何判断这个chunk是不是点9chunk的。这个后面再讲。
  6. 步骤9中,一定要使用缓存,不然异步加载的过程中,在list中显示会有问题,跳变很严重。有的图片加载组件不支持NinePatchDrawable缓存的记得要补上。
  7. 步骤8或9中,为了避免外包同学出错后无法发现问题,或者出现问题4中所说的压缩和格式转换导致出错,所以这里如果不是点九图,则进行上报,用于发现问题。

3. 其它问题

先来一小段分析: 根据之前的讨论我们知道,画黑线的点九图与普通图片的区别主要在于四周多了1px的黑线,而转换后的点九图则没有这1px的黑线,但是它却包含了用于拉伸的信息,那么这个信息是被包含在哪里呢?这里就要看看png图片的文件格式了。 png图片是由一个png文件标志和三个以上的数据块(chunk)按照特性的顺序组成,它含有两种类型的数据块,关键数据块和辅助数据块,关键数据块只包含文件头、尾数据块和图像数据块,是必须要有的,而辅助数据块则是可选的。包含了一些额外的信息,每个数据块包含哪些信息可以参考文章PNG文件结构分析,这里就不多说了。 PNG文件结构如下

PNG文件标志

PNG数据块

……

PNG数据块

现在可以知道,点九图的黑线,在编译时,被转换成了某些数据,保存在了png图片的辅助数据块中了。 那么,这个数据块是什么样的,java的Bitmap又是如何解析出这个数据块的呢?通过追查,可以找到这块代码,其中mPatch最终将被构造到Bitmap中去。

// frameworks\base\core\jni\android\graphics\NinePatchPeeker.cpp
bool NinePatchPeeker::readChunk(const char tag[], const void* data, size_t length) {
    if (!strcmp("npTc", tag) && length >= sizeof(Res_png_9patch)) {
        Res_png_9patch* patch = (Res_png_9patch*) data;
        size_t patchSize = patch->serializedSize();
        if (length != patchSize) {
            return false;
        }
        // You have to copy the data because it is owned by the png reader
        Res_png_9patch* patchNew = (Res_png_9patch*) malloc(patchSize);
        memcpy(patchNew, patch, patchSize);
        Res_png_9patch::deserialize(patchNew);
        patchNew->fileToDevice();
        free(mPatch);
        mPatch = patchNew;
        mPatchSize = patchSize;
    } else {
        ...
    }
    return true;    // keep on decoding
}

通过这块代码可以知道,系统是找到tag为“npTc”的数据块,如果这个数据块没有异常的话,就将这个数据块的数据复制给mPatch,最终被装入到Bitmap中。

这里有个Res_png_9patch结构,所以Bitmap的mNinePatchChunk的数据结构实际上为Res_png_9patch,第一个字节用来表示这个png图片是否是点九图,上述的NinePatch.isNinePatchChunk()方法也是通过这个字节判断的,接着就是一些拉伸点的位置和padding信息,用于最后的渲染流程。

//frameworks\base\libs\androidfw\include\androidfw\ResourceTypes.h
struct alignas(uintptr_t) Res_png_9patch
{
    Res_png_9patch() : wasDeserialized(false), xDivsOffset(0),
                       yDivsOffset(0), colorsOffset(0) { }

    int8_t wasDeserialized;
    uint8_t numXDivs;
    uint8_t numYDivs;
    uint8_t numColors;

    // The offset (from the start of this structure) to the xDivs & yDivs
    // array for this 9patch. To get a pointer to this array, call
    // getXDivs or getYDivs. Note that the serialized form for 9patches places
    // the xDivs, yDivs and colors arrays immediately after the location
    // of the Res_png_9patch struct.
    uint32_t xDivsOffset;
    uint32_t yDivsOffset;

    int32_t paddingLeft, paddingRight;
    int32_t paddingTop, paddingBottom;

    enum {
        // The 9 patch segment is not a solid color.
        NO_COLOR = 0x00000001,

        // The 9 patch segment is completely transparent.
        TRANSPARENT_COLOR = 0x00000000
    };

    // The offset (from the start of this structure) to the colors array
    // for this 9patch.
    uint32_t colorsOffset;
    ...

    inline int32_t* getXDivs() const {
        return reinterpret_cast<int32_t*>(reinterpret_cast<uintptr_t>(this) + xDivsOffset);
    }
    inline int32_t* getYDivs() const {
        return reinterpret_cast<int32_t*>(reinterpret_cast<uintptr_t>(this) + yDivsOffset);
    }
    inline uint32_t* getColors() const {
        return reinterpret_cast<uint32_t*>(reinterpret_cast<uintptr_t>(this) + colorsOffset);
    }
} __attribute__((packed));

这里简单讲下这个结构中每个字段代表的含义:

再看看这些字段是如何生效的,首先看看一段源码中的注释:

 * This chunk specifies how to split an image into segments for
 * scaling.
 *
 * There are J horizontal and K vertical segments.  These segments divide
 * the image into J*K regions as follows (where J=4 and K=3):
 *
 *      F0   S0    F1     S1
 *   +-----+----+------+-------+
 * S2|  0  |  1 |  2   |   3   |
 *   +-----+----+------+-------+
 *   |     |    |      |       |
 *   |     |    |      |       |
 * F2|  4  |  5 |  6   |   7   |
 *   |     |    |      |       |
 *   |     |    |      |       |
 *   +-----+----+------+-------+
 * S3|  8  |  9 |  10  |   11  |
 *   +-----+----+------+-------+
 *
 * Each horizontal and vertical segment is considered to by either
 * stretchable (marked by the Sx labels) or fixed (marked by the Fy
 * labels), in the horizontal or vertical axis, respectively. In the
 * above example, the first is horizontal segment (F0) is fixed, the
 * next is stretchable and then they continue to alternate. Note that
 * the segment list for each axis can begin or end with a stretchable
 * or fixed segment.
 * /

正如源码注释中所示,点九图将图片虚拟地划分成了n个模块,其中F区域代表固定,S区域代表拉伸,而mDivX,mDivY描述了所有S区域的位置起始位置和结束位置,mColor描述了各个小模块的颜色,大小为n,通常情况下,赋值为Res_png_9patch.NO_COLOR。就以源码注释中的例子来说,mDivX,mDivY,mColor如下:

mDivX = [ S0.start, S0.end, S1.start, S1.end];
mDivY = [ S2.start, S2.end, S3.start, S3.end];
mColor = [c[0],c[1],...,c[11]]

这时之前的问题就解决了,这个数据块就是tag为”npTc“的数据块,数据内容为 Res_png_9patch。Java的Bitmap通过遍历png的数据块,找出tag为”npTc“且长度无误的数据块,就是点九图的数据块,这个数据块保存了点九图的拉伸信息,主要是定义了拉伸区域以及padding。

最后来看看之前的几个问题:

3.1 画黑线的点九图在编译时经历了什么?

将png图片中四周黑线所代表的信息解析成Res_png_9patch,存放到png的一个数据块中,然后把黑线抹去,黑线所表示的信息就保存在了如上的Res_png_9patch结构中。

3.2 可否不用点九图,而是指定位置拉伸达到点九图的效果?

理论上是可行的,可以根据Res_png_patch的结构,构造一个chunk[],将所需要的拉伸信息和padding填入到需要的位置上,接着在构造NinePatchDrawable的时候,将这个chunk[]信息传入进去即可。 其中拉伸信息因为ios端也需要,所以后台会传,或者设计定好一个位置写死,而padding也是设计给的,实际上这个padding会被view本身设置的padding所覆盖。

NinePatchDrawable的构造方法为NinePatchDrawable ninePatchDrawable = new NinePatchDrawable(bitmap, chunk, new Rect(), null);,其中bitmap直接用解析出来的bitmap,chunk则是从bitmap.getNinePatchChunk()取出的一个chunk,或者是客户端自己构造的一个byte[],allocate一个ByteBuffer,然后根据Res_png_9patch的结构,依次填入数据即可。参考文章2有一个小demo,有兴趣的可以跳转看看。

3.3 mNinePatchChunk信息是如何被构造的,又是如何判断一个chunk信息是不是点9chunk信息的?

这里的mNinePatchChunk信息,实际上是在编译时,编译器将png图片中四周黑线所代表的信息解析成Res_png_9patch,存放到png的一个数据块中,然后j将tag设置为“npTc”,接着在使用时,通过遍历png的数据块,找到tag为“npTc”的数据块,如果这个数据块没有问题,这被用作参数构造Bitmap,最终成为mNinePatchChunk。 判断一个经过tag和长度筛选后的chunk信息是否是点9chunk信息,是直接通过Res_png_9patch.wasDeserialized判断的,可以看看NinePatch的isNinePatchChunk的代码,如果wasDeserialized不为-1,则表示这个信息是点9chunk信息。

 static jboolean isNinePatchChunk(JNIEnv* env, jobject, jbyteArray obj) {
        if (NULL == obj) {
            return JNI_FALSE;
        }
        if (env->GetArrayLength(obj) < (int)sizeof(Res_png_9patch)) {
            return JNI_FALSE;
        }
        const jbyte* array = env->GetByteArrayElements(obj, 0);
        if (array != NULL) {
            const Res_png_9patch* chunk = reinterpret_cast<const Res_png_9patch*>(array);
            int8_t wasDeserialized = chunk->wasDeserialized;
            env->ReleaseByteArrayElements(obj, const_cast<jbyte*>(array), JNI_ABORT);
            return (wasDeserialized != -1) ? JNI_TRUE : JNI_FALSE;
        }
        return JNI_FALSE;
    }

参考文章

  1. PNG文件结构分析    http://www.360doc.com/content/11/0428/12/1016783_112894280.shtml
  2. Android动态布局入门及NinePatchChunk解密  https://mp.weixin.qq.com/s?__biz=MzI1NjEwMTM4OA==&mid=2651232105&idx=1&sn=fcc4fa956f329f839f2a04793e7dd3b9&mpshare=1&scene=1&srcid=0719Nyt7J8hsr4iYwOjVPXQE#rd

QQ音乐团队诚聘测试、研发。有意者请发送简历至tmezp@tencent.com,请注明来自公众号,我们将优先拜读。

原文发布于微信公众号 - 腾讯音乐技术团队(gh_287053a877e6)

原文发表时间:2018-07-27

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏自由而无用的灵魂的碎碎念

启动Myeclipse报错“Failed to create the Java Virtual Machine”的解决办法

我安装的是Myeclipse 10.7.1。装上好久没用,今天启动突然报错:Failed to create the Java Virtual Machine。...

13530
来自专栏cmazxiaoma的架构师之路

FastDFS蛋疼的集群和负载均衡(十五)之lvs四层+Nginx七层负载均衡

16720
来自专栏安恒网络空间安全讲武堂

赛前福利①最新2018HITB国际赛writeup

FIRST 距离“西湖论剑杯”全国大学生网络空间安全技能大赛只有10天啦! 要拿大奖、赢offer,那必须得来点赛前练习定定心啊~这不,讲武堂就拿到了2018H...

47650
来自专栏菩提树下的杨过

spring集成kafka

一、添加依赖项 compile 'org.springframework.kafka:spring-kafka:1.2.2.RELEASE' 二、发消息(生产者...

23580
来自专栏进击的程序猿

swoole入门abc1. 入门abc

分析上面的代码,我们发现会有什么问题?如果两个请求同时进来,都读到了lastTime,没有被拒绝,但是这两个请求本身是已经请求过快了。

10020
来自专栏数据和云

监控工具:Oracle 12c Cluster Health Monitor 详解

? 戴明明(Dave) Oracle ACE-A,ACOUG核心成员,宝存科技数据库方案架构师 Dave也是CSDN 认证专家,超过7年的DBA经验,擅长O...

43290
来自专栏24K纯开源

OpenProcess打开进程返回错误的问题

问题描述       项目中需要做一个小功能:能够查看系统中当前正在运行的进程的内存信息,如内存块类型、分配状态、访问权限等。如下图所示: ?       需要...

475100
来自专栏Kubernetes

cluster-proportional-autoscaler源码分析及如何解决KubeDNS性能瓶颈

Author: xidianwangtao@gmail.com 工作机制 cluster-proportional-autoscaler是kubernetes的...

590100
来自专栏圣杰的专栏

Asp.net mvc 知多少(一)

本系列主要翻译自《ASP.NET MVC Interview Questions and Answers 》- By Shailendra Chauhan,想...

28470
来自专栏一只程序汪的自我修养

手把手教你用.NET Core写爬虫

自从上一个项目58HouseSearch从.NET迁移到.NET core之后,磕磕碰碰磨蹭了一个月才正式上线到新版本。

369120

扫码关注云+社区

领取腾讯云代金券