一张启动图引发的思考--探索.9图原理和应用场景

引子

小u啊,我们应用启动的时候有一段白屏,不雅观,你给整个启动图上去,给,这里是资源图片

换好了

嗯,不错不错,咦,这个小米fold怎么显示了两个logo?

啊?这。。。我来看看

是这样的,activity启动图和启动背景图标一起显示了,但是启动图片又不适配fold这种狭长的屏幕,而且下半部分由于没有背景,是透明的,所以就显示了两个图标,一个启动图标,一个启动图图片图标,请看示意图

示意图.png

那怎么解决呢?

有几个办法,1.让启动图拉伸,覆盖下部分区域,但是会有形变,不够优雅,2.给imageview设置一个白色背景,让它不透明,3.使用android studio的。9图片制作工具,让空白部分拉伸,内容不拉伸,非常优雅。

图片.png

好,那你先试试.9图片吧

.9简介

我知道看这篇文章的朋友肯定对.9图或多或少有点了解,就简单介绍下就行 .9图可以通过设置来让图片某一部分拉伸 其他部分不拉伸 还可以控制图片里面显示内容的区域 这就是.9图的简介 够简了吧 如果想看更详细的说明可以移步谷歌官方的说明 大体上也是这意思

这个是官方的文档developer.android.com/studio/writ…

图片.png

图片.png

图片.png

这是.9图在三种拉伸情况下的例子,纵向,横向,横纵双向。

.9图制作

网上很多文章说最简单是用as内置工具来做 但是网上文章好多都是很早的 都是老版本工具 让我们来看看这工具现在啥样

图片.png 好像也不是很简单啊 这些东西做什么用的第一次看确实会很蒙,这里介绍下这个工具

  • Zoom:调整图形在绘制区域中的缩放级别。

  • Patch scale:调整图像在预览区域中的比例。

  • Show lock:当鼠标悬停在图形的不可绘制区域上时以直观方式呈现。

  • Show patches:预览绘制区域中的可拉伸图块(粉色为可拉伸图块),如上面的图 2 所示。

  • Show content:突出显示预览图像中的内容区域(紫色为允许绘制内容的区域),如图 2 所示。

  • Show bad patches:在拉伸时可能会在图形中产生伪影的图块区域周围添加红色边框,如图 2 所示。如果您消除所有不良图块,已拉伸图像的视觉连贯性将得以保持。 怎么生成.9图不是重点,网上文章不要太多,最主要就是绘制左边和上面的黑线,左边黑线控制的是哪里可以被上下拉伸,上面黑线控制哪里可以被水平拉伸,可以有多条,右边和下面的黑线控制了哪里可以放内容,不在这个区域的内容不会被显示。

当然 还有很多办法生成.9图 单独工具或者在线工具都行 看个人喜好了。

.9图原理

上面的介绍都很大众化 那么为啥.9图这么神奇呢?它是什么原理呢,这个好像没什么人说过,这里也简单阐述下。

主要是四条黑线 分为两组 左边和上面的黑线 负责判定图片哪个部分可以被拉伸 右边和下面的黑线 负责确定图片内部展示内容的区域 比如这个图是个聊天气泡 内容是一堆文字

大概就是下面这个图的样子 分成了九个区域

图片.png

我们把他们编号成1-9,这几个区域对应情况如下

  1. 1 3 7 9 号区域,不在两条黑线区域内,不会被拉伸
  2. 4 6 号区域,只在左侧黑线范围内,可以被上下拉伸
  3. 2 8 号区域,只在上面黑线区域内,会被左右拉伸
  4. 5号区域,同时在上和左区域内,会被上下左右拉伸

回到刚才的问题 为啥处理之后就能控制拉伸和内容了?

首先我们发现处理之后的图片后缀还是原来的 证明没有变成其他格式 但是名字里加上了.9

那猜测是不是在文件里加上了一些额外信息 用.9作为标识 图片系统处理拉伸的时候就去读这些信息

那么 到底加了什么信息呢 又是怎么判断和使用的呢 下面一一道来

首先 我们给图片加了什么信息

我们看看官方怎么描述.9图的

NinePatchDrawable 图形是一种可拉伸的位图,可用作视图的背景。Android 会自动调整图形的大小以适应视图的内容。NinePatch 图形是标准 PNG 图片,包含一个额外的 1 像素边框。必须使用 9.png 扩展名将其保存在项目的 res/drawable/ 目录下。

可以看到,.9图本质上还是png图片,但是加了1像素边框,且名字里加了个.9。

让我们来看看什么是png图片,以及它的数据构成

The PNG format provides a portable, legally unencumbered, well-compressed, well-specified standard for lossless bitmapped image files.

A PNG file consists of a PNG signature followed by a series of chunks. This chapter defines the signature and the basic properties of chunks.

png的签名块后面跟了两个数据块critical chunk和ancillary chunks,其中critical chunk包含关键数据,也是每个图片必须有的,ancillary chunks包含一些辅助信息,png如果不识别这些辅助块,可以忽略它打到向下兼容的目的。

签名块就是一个八个字节的十六进制码,用来标识图片。

重点来看看数据块的布局

名称 字节数 说明
长度(Length) 4字节 指定数据块中数据区域的长度,长度不可超过(231-1)个字节
数据块类型码(Chunk Type Code) 4字节 数据块类型码由ASCII字母(A-Z和a-z)组成的"数据块符号"
数据块数据(Chunk Data) 可变长度 存储数据块类型码指定的数据
循环冗余检测(CRC) 4字节 存储用来检测是否文件传输有误的循环冗余码

看到这个可以猜测,我们添加的黑色边框就是往辅助块里面加了内容,在展示的时候识别添加的信息,达到控制哪些地方伸缩的目的。

让我们看看一张图片被弄成.9之后加了些什么内容。

原图数据如下

原图.png

.9数据如下

.9图.png

可以看到,变成.9图片之后,多了5个IDAT块,而且参数里面的长宽都增加了2像素,而且图片大小也增加了不少从80kb增加到了180kb,我们可以猜测到,这几个块里面记录的数据就是我们生成.9图时画的那几条线生成的了。

其次 这些信息在图片发生拉伸时怎么被识别和使用的

这就涉及到android怎么加载一张图片的问题了,当然这些操作都是native层进行的,经过代码跟踪,我们发现有这样一个类 ** NinePatchPeeker.cpp**

我们来看看这个类里面的readChunk方法干了些什么

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 if (!strcmp("npLb", tag) && length == sizeof(int32_t) * 4) {

        mHasInsets = true;

        memcpy(&mOpticalInsets, data, sizeof(int32_t) * 4);

    } else if (!strcmp("npOl", tag) && length == 24) { // 4 int32_ts, 1 float, 1 int32_t sized byte

        mHasInsets = true;

        memcpy(&mOutlineInsets, data, sizeof(int32_t) * 4);

        mOutlineRadius = ((const float*)data)[4];

        mOutlineAlpha = ((const int32_t*)data)[5] & 0xff;

    }

    return true;

}

我们看到这里处理了npTcnpLbnpOl三个数据块,当判断有npTc这个数据块的时候,系统就认为这是.9图片,就会进行下一步处理。

npTc这个数据又是从哪来的呢?

从上面内容我们知道已经添加了一些额外信息,我们发现官方的说明里有一句,要把.9图放在src/drawable目录下,这是因为在编译的时候,aapt会在发现图片名字符合.9图规则的时候,把四周的黑色边框提取出来,放在npTc数据块里面。

接下来我们看看结构体Res_png_9patch里面有什么。

/**

 * 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.

 *

 * ...

 *

 * The colors array contains hints for each of the regions. They are

 * ordered according left-to-right and top-to-bottom as indicated above.

 * For each segment that is a solid color the array entry will contain

 * that color value; otherwise it will contain NO_COLOR. Segments that

 * are completely transparent will always have the value TRANSPARENT_COLOR.

 *

 * The PNG chunk type is "npTc".

 */

struct alignas(uintptr_t) Res_png_9patch

{

    int8_t wasDeserialized;

    uint8_t numXDivs, numYDivs, numColors;


    uint32_t xDivsOffset, yDivsOffset, colorsOffset;

    int32_t paddingLeft, paddingRight, 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

    };


    ...

      

    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);

    }

}

从注释我们看出,一张图片被分成了很多个部分,s开头的代表可以拉伸的,f开头的代表不能拉伸,还有一些颜色的数据,他们共同构成了这个结构体,x,y轴上这些可以拉伸和不可拉伸的部分分别放在xdivydivs数组里,同时内容的显示区域也由几个padding来存放,到这里,就把.9图的额外信息都给读出来了。

既然已经获得了这些额外的信息,那么绘制的时候,系统就可以根据这些信息判断怎么拉伸一张.9图了,绘制调用了NinePatchDrawable,最终也会走到native层去处理这些数据。

到此为止,制作.9和解析.9的过程就分析完毕了,一张.9图背后居然这么复杂,还是挺让人意外的,果然是学无止境啊。

.9图应用以及一个小坑

看完了怎么生成和它的原理 该我们去使用它了 回到开头 用我们新做的酷炫.9图解决问题吧! 代码张这样

图片.png 效果张这样

图片.png

为什么我们的.9图没有按照预期的拉伸,而是把内容也给拉伸了?因为.9图的特性问题,它只支持拉伸,如果一个图片本身就比imageview大了,那就不会去拉伸,所以我们的.9图本身太大了,也就不存在拉伸的说法了,那肯定是无效的。 那我们再来尝试把图片尺寸改小,这下可以拉伸了吧,来看看效果。

图片.png

可以看到,下面图标被遮挡住了,但是内容也太小了吧所以用.9图来做splash screen背景不是个好主意

那么,到底哪些场景可以用它呢?总结一下就是,一张图片,角落不能拉伸,其他部分可以随着内容的增多随便拉伸的情况,像这样。

图片.png

简单来说,就是聊天气泡一类的,如果一张图片的主体是内容要放缩的话,是不适合的,因为.9图只会拉伸特定部分,主体部分会维持原大小。

比如我们上面做背景图这种情况。

总结

  1. .9图并不适合作为全屏展示页面,要不太大了没有拉伸效果,要不内容太小了不美观
  2. .9图适合做一些纯色拉伸不形变的背景,比如聊天气泡和按钮背景这种
  3. 在android中,.9图得放到sr/drawable目录才行,系统才会去生成对应数据块,才可以被解析,否则就要自己去生成
  4. 普通图片由于没有额外的数据,是不能达到.9图这样的效果的

猜你喜欢

转载自juejin.im/post/7118282635972968484