时间: 2020-09-03 00:08:26 人气: 2281 评论: 0
这一块是对点九图的简单介绍,如果对这块已经有了解的话,可以直接跳到2,看看聊天气泡中如何使用点九图。
首先简单介绍下点九图出现的原因吧,Android为了使用同一张图作为不同数量文字的背景,设计了一种可以指定区域拉伸的图片格式“.9.png”,这种图片格式就是点九图。 注意:这种图片格式只能被使用于Android开发。在ios开发中,可以在代码中指定某个点进行拉伸,而在Android中不行,所以在Android中想要达到这个效果,只能使用点九图。(对大多数时候来说是这样,实际上可以自己构造,后面会稍微提一下,见3.2)
点九图的本质实际上是在图片的四周各增加了1px的像素,并使用纯黑(#FF000000)的线进行标记,其它的与原图没有任何区别。可以参考以下图片:
可以看到在该图的四周,均有黑色像素标记,这些标记的作用分别是:
标记位置 | 含义 |
|---|---|
左-黑点 | 纵向拉伸区域 |
上-黑点 | 横向拉伸区域 |
右-黑线 | 纵向显示区域 |
下-黑线 | 横向显示区域 |
由于点九图的本质也是个图片,只是在周围加了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. 便于开发人员直接使用 |
具体如何操作,这里就不多赘述了。
Android中使用点九图,主要有三种形式,使用res文件夹中的点九图,使用assets文件夹中的点九图以及使用网上拉取的点九图,下面分别看看它们如何使用。
Android并不是直接使用点九图,而是在编译时将其转换为另外一种格式(见3.1),这种格式是将其四周的黑色像素保存至Bitmap类中的一个名为mNinePatchChunk的byte[]中,并抹除掉四周的这一个像素的宽度;接着在使用时,如果Bitmap的这个mNinePatchChunk不为空,且为9patch chunk(见3.3),则将其构造为NinePatchDrawable,否则将会被构造为BitmapDrawable,最终设置给view,NinePatchDrawable的拉伸主要是通过其draw方法实现的。总而言之,最后打出的包中的点九图,已经不是原来的带黑线的点九图了。
先简单说下从网上拉取点九图的过程,首先使用url请求网络数据,并将结果缓存为本地文件,再使用文件流创建Bitmap,接着使用Bitmap创建drawable再交给view使用,最后由view的draw方法调用drawable的draw方法将图片绘制出来。 再看看上面1.5的解析原理,它会带来一个坑,由于聊天气泡需求需要使用url从网络上拉取点九图,如果这个点九图没有经过编译的过程,将其周围的黑线标记放入到png中的一个辅助chunk中,那么在使用这个图作为背景时,会显示出黑线,且不会拉伸。而根据以往的经验,Android是可以直接使用点九图的,因为放到res文件夹中就可以直接使用,所以就将点九图直接上传到服务器上,这时从网上拉取的图片数据是带黑线的图,那么就会出错了。 这时候效果是这样的:
emmmmm,很丑。
当初发现这个问题时,考虑了三个方案来处理
pass: 其实客户端还有一个解决方法,就是自己根据拉伸区域构造mNinePatchChunk,然后将普通的Bitmap创建为NinePatchDrawable,因为ios的特性,设计会指定一个拉伸点,以及文字显示区域,这两个数据是固定的,也就是说,每个点九图上的黑线是固定的,所以可以根据这些数据来构造一个固定的mNinePatchChunk。这样可以做出一个跟ios实现方式相同的控件。(见3.2)
最后是通过联系手q参考并采用了他们的方案,也就是上面的第一种方法实现的。 (为了避免外包同学出错后无法发现问题,这里如果不是点九图,则上报,用于发现问题)
最终确定的实现流程如下图所示:
接下来说说这9个步骤中的遇到问题:
aapt c -v -S . -C .\9out,其中.表示当前目录,.\9out表示目标目录,即将当前目录中的带黑线的点九图转换后放到当前目录下的9out文件夹中,9out文件夹该命令会自动创建。为了让外包自动化这个过程,可以将其做成一个工具,用于批量转换。先来一小段分析: 根据之前的讨论我们知道,画黑线的点九图与普通图片的区别主要在于四周多了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。
最后来看看之前的几个问题:
将png图片中四周黑线所代表的信息解析成Res_png_9patch,存放到png的一个数据块中,然后把黑线抹去,黑线所表示的信息就保存在了如上的Res_png_9patch结构中。
理论上是可行的,可以根据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,有兴趣的可以跳转看看。
这里的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 技术沙龙 教程文章 热点综合