28

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

 5 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzI1NjEwMTM4OA%3D%3D&%3Bmid=2651232915&%3Bidx=1&%3Bsn=c6cd95dc86c3c96548884e9d84ef273a&%3Bchksm=f1d9e7f0c6ae6ee64364c1af9198708fe6dc0d6966f482efcd63939cbbfeb59cadf68ab
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

1. 点九图介绍

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

1.1 点九图出现的原因

首先简单介绍下点九图出现的原因吧,Android为了使用同一张图作为不同数量文字的背景,设计了一种 可以指定区域拉伸 的图片格式“.9.png”,这种图片格式就是点九图。

注意:这种图片格式只能被使用于Android开发。在ios开发中,可以在代码中指定某个点进行拉伸,而在Android中不行,所以在Android中想要达到这个效果,只能使用点九图。(对大多数时候来说是这样,实际上可以自己构造,后面会稍微提一下,见3.2)

1.2 点九图的本质

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

FRBbuay.png!web

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

标记位置 含义 左-黑点 纵向拉伸区域 上-黑点 横向拉伸区域 右-黑线 纵向显示区域 下-黑线 横向显示区域

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文件夹中就可以直接使用,所以就将点九图直接上传到服务器上,这时从网上拉取的图片数据是带黑线的图,那么就会出错了。

这时候效果是这样的:

2iyyInu.jpg!web

emmmmm,很丑。

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

  1. 开发提供工具,产品或设计进行转换后再在配置平台上上传,问题是这个过程全是外包进行处理的,无法保证转换的质量和准确性,因为转换后的图和原图长一样。

  2. 将带黑线的点九图上传到配置平台,平台进行转换后再上传到服务器。这个暂时没有想到有什么大的问题。

  3. 客户端收到带黑线的点九图后,进行处理,问题是没有直接的方法进行转换,需要客户端通过像素级 + byte级的操作,来构造出NinePatchDrawable,过程比较耗时,影响性能和流畅度,并且涉及到的内容太细,后续维护困难。

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

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

2.2 最终确定的使用流程

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

aMnUrqn.jpg!web

接下来说说这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));

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

BZFzAzz.jpg!web

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

 * 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音乐团队诚聘测试、研发。有意者请发送简历至 [email protected] ,请注明来自公众号,我们将优先拜读。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK