一张启动图引发的思考--探索.9图原理和应用场景
引子
小u啊,我们应用启动的时候有一段白屏,不雅观,你给整个启动图上去,给,这里是资源图片
换好了
嗯,不错不错,咦,这个小米fold怎么显示了两个logo?
啊?这。。。我来看看
是这样的,activity启动图和启动背景图标一起显示了,但是启动图片又不适配fold这种狭长的屏幕,而且下半部分由于没有背景,是透明的,所以就显示了两个图标,一个启动图标,一个启动图图片图标,请看示意图
那怎么解决呢?
有几个办法,1.让启动图拉伸,覆盖下部分区域,但是会有形变,不够优雅,2.给imageview设置一个白色背景,让它不透明,3.使用android studio的。9图片制作工具,让空白部分拉伸,内容不拉伸,非常优雅。
好,那你先试试.9图片吧
.9简介
我知道看这篇文章的朋友肯定对.9图或多或少有点了解,就简单介绍下就行
.9图可以通过设置来让图片某一部分拉伸 其他部分不拉伸 还可以控制图片里面显示内容的区域
这就是.9图的简介 够简了吧 如果想看更详细的说明可以移步谷歌官方的说明 大体上也是这意思
这个是官方的文档https://developer.android.com/studio/write/draw9patch?hl=zh-cn
这是.9图在三种拉伸情况下的例子,纵向,横向,横纵双向。
.9图制作
网上很多文章说最简单是用as内置工具来做 但是网上文章好多都是很早的 都是老版本工具 让我们来看看这工具现在啥样
好像也不是很简单啊 这些东西做什么用的第一次看确实会很蒙,这里介绍下这个工具
Zoom:调整图形在绘制区域中的缩放级别。
Patch scale:调整图像在预览区域中的比例。
Show lock:当鼠标悬停在图形的不可绘制区域上时以直观方式呈现。
Show patches:预览绘制区域中的可拉伸图块(粉色为可拉伸图块),如上面的图 2 所示。
Show content:突出显示预览图像中的内容区域(紫色为允许绘制内容的区域),如图 2 所示。
Show bad patches:在拉伸时可能会在图形中产生伪影的图块区域周围添加红色边框,如图 2 所示。如果您消除所有不良图块,已拉伸图像的视觉连贯性将得以保持。
怎么生成.9图不是重点,网上文章不要太多,最主要就是绘制左边和上面的黑线,左边黑线控制的是哪里可以被上下拉伸,上面黑线控制哪里可以被水平拉伸,可以有多条,右边和下面的黑线控制了哪里可以放内容,不在这个区域的内容不会被显示。
当然 还有很多办法生成.9图 单独工具或者在线工具都行 看个人喜好了。
.9图原理
上面的介绍都很大众化 那么为啥.9图这么神奇呢?它是什么原理呢,这个好像没什么人说过,这里也简单阐述下。
主要是四条黑线 分为两组
左边和上面的黑线 负责判定图片哪个部分可以被拉伸
右边和下面的黑线 负责确定图片内部展示内容的区域 比如这个图是个聊天气泡 内容是一堆文字
大概就是下面这个图的样子 分成了九个区域
我们把他们编号成1-9,这几个区域对应情况如下
- 1 3 7 9 号区域,不在两条黑线区域内,不会被拉伸
- 4 6 号区域,只在左侧黑线范围内,可以被上下拉伸
- 2 8 号区域,只在上面黑线区域内,会被左右拉伸
- 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之后加了些什么内容。
原图数据如下
.9数据如下
可以看到,变成.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;
}
我们看到这里处理了npTc
、npLb
、npOl
三个数据块,当判断有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轴上这些可以拉伸和不可拉伸的部分分别放在xdiv
和ydivs
数组里,同时内容的显示区域也由几个padding
来存放,到这里,就把.9图的额外信息都给读出来了。
既然已经获得了这些额外的信息,那么绘制的时候,系统就可以根据这些信息判断怎么拉伸一张.9图了,绘制调用了NinePatchDrawable
,最终也会走到native层去处理这些数据。
到此为止,制作.9和解析.9的过程就分析完毕了,一张.9图背后居然这么复杂,还是挺让人意外的,果然是学无止境啊。
.9图应用以及一个小坑
看完了怎么生成和它的原理 该我们去使用它了 回到开头 用我们新做的酷炫.9图解决问题吧!
代码张这样
为什么我们的.9图没有按照预期的拉伸,而是把内容也给拉伸了?因为.9图的特性问题,它只支持拉伸,如果一个图片本身就比imageview大了,那就不会去拉伸,所以我们的.9图本身太大了,也就不存在拉伸的说法了,那肯定是无效的。
那我们再来尝试把图片尺寸改小,这下可以拉伸了吧,来看看效果。
可以看到,下面图标被遮挡住了,但是内容也太小了吧,所以用.9图来做splash screen背景不是个好主意。
那么,到底哪些场景可以用它呢?总结一下就是,一张图片,角落不能拉伸,其他部分可以随着内容的增多随便拉伸的情况,像这样。
简单来说,就是聊天气泡一类的,如果一张图片的主体是内容要放缩的话,是不适合的,因为.9图只会拉伸特定部分,主体部分会维持原大小。
比如我们上面做背景图这种情况。
总结
- .9图并不适合作为全屏展示页面,要不太大了没有拉伸效果,要不内容太小了不美观
- .9图适合做一些纯色拉伸不形变的背景,比如聊天气泡和按钮背景这种
- 在android中,.9图得放到sr/drawable目录才行,系统才会去生成对应数据块,才可以被解析,否则就要自己去生成
- 普通图片由于没有额外的数据,是不能达到.9图这样的效果的