您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 Code iProcess 课程 认证 咨询 工具 火云堂 讲座吧   成长之路  
会员   
 
   
 
  
每天15篇文章
不仅获得谋生技能
更可以追随信仰
 
     
   
 订阅
  捐助
Android点九图总结以及在聊天气泡中的使用
 
161 次浏览     评价:  
 2018-8-9
 
编辑推荐:
本文来自于腾讯云,介绍了点九图的本质,聊天气泡中使用点九图,其它问题等。

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无法识别。

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

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 ( Nine Patch .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图片的辅助数据块中了。

那么,这个数据块是什么样的,java的Bitmap又是如何解析出这个数据块的呢?通过追查,可以找到这块代码,其中mPatch最终将被构造到Bitmap中去。

// frameworks \base\core \jni\android \graphics \ NinePatch Peeker .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,存放到pn g的一个数据块中,然后j将tag设置为“npTc”,接着在使用时,通过遍历png 的数据块,找到tag为“npTc”的数据块,如果这个数据块没有问题,这被用作参数构造 Bitmap,最终成为 mNinePatchChunk。

判断一个经过tag和长度筛选后的chunk信息是否是点9chunk信息,是直接通过Res_png_ 9patch. wasDeserialized 判断的,可以看看NinePatch 的isNinePatchChun k的代码,如果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;
}
   
161 次浏览  评价: 差  订阅 捐助
相关文章

深度解析:清理烂代码
如何编写出拥抱变化的代码
重构-使代码更简洁优美
团队项目开发"编码规范"系列文章
相关文档

重构-改善既有代码的设计
软件重构v2
代码整洁之道
高质量编程规范
相关课程

基于HTML5客户端、Web端的应用开发
HTML 5+CSS 开发
嵌入式C高质量编程
C++高级编程
每天2个文档/视频
扫描微信二维码订阅
订阅技术月刊
获得每月300个技术资源
 
 

关于我们 | 联系我们 | 京ICP备10020922号 京公海网安备110108001071号