Android 点九图机制讲解及在聊天气泡中的应用,android 音视频开发何俊林 pdf
注意:这种图片格式只能被使用于 Android 开发。在 ios 开发中,可以在代码中指定某个点进行拉伸,而在 Android 中不行,所以在 Android 中想要达到这个效果,只能使用点九图(下文会啪啪打脸,其实是可以的,只是很少人这样使用,兼容性不知道怎么样,点击跳转)
点九图实质
点九图的本质实际上是在图片的四周各增加了 1px 的像素,并使用纯黑(#FF000000)的线进行标记,其它的与原图没有任何区别。可以参考以下图片:
点九图在 Android 中的应用
点九图在 Android 中主要有三种应用方式
直接放在 res 目录中的 drawable 或者 mipmap 目录中
放在 assert 目录中
从网络下载
第一种方式是我们最常用的,直接调用 setBackgroundResource
或者 setImageResource
方法,这样的话图片及可以做到自动拉伸。
而对于第二种或者第三种方式,如果我们直接去加载 .9.png,你会发现图片或者图片背景根本无法拉伸。纳尼,这是为甚么呢。下面,且听老衲慢慢道来。
Android 并不是直接使用点九图,而是在编译时将其转换为另外一种格式,这种格式是将其四周的黑色像素保存至 Bitmap 类中的一个名为 mNinePatchChunk
的 byte[] 中,并抹除掉四周的这一个像素的宽度;接着在使用时,如果 Bitmap 的这个 mNinePatchChunk
不为空,且为 9patch chunk,则将其构造为 NinePatchDrawable
,否则将会被构造为 BitmapDrawable,最终设置给 view。
因此,在 Android 中,我们如果想动态使用网络下载的点九图,一般需要经过以下步骤:
使用 sdk 目录下的 aapt 工具将点九图转化为 png 图片
解析图片的时候,判断是否含有 NinePatchChunk,有的话,转化为 NinePatchDrawable
public static void setNineImagePatch(View view, File file, String url) {if (file.exists()) {Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());byte[] chunk = bitmap.getNinePatchChunk();if (NinePatch.isNinePatchChunk(chunk)) {NinePatchDrawable patchy = new NinePatchDrawable(view.getResources(), bitmap, chunk, new Rect(), null);view.setBackground(patchy);}
}}
点九图上传服务器流程
aapt 转换命令
单个图片文件转换
./aapt s -i xxx.9.png -o xxx.png
批量转换
批量转换
./aapt c -S
inputDir -C outputDir
inputDir 为原始.9 图文件夹,outputDir 为输出文件夹
执行成功实例
jundeMacBook-Pro:一期气泡 junxu$ ./aapt c -S /Users/junxu/Desktop/一期气泡/气泡需求整理 -C /Users/junxu/Desktop/一期气泡/outputCrunching PNG Files in source dir: /Users/junxu/Desktop/一期气泡/气泡需求整理 To destination dir: /Users/junxu/Desktop/一期气泡/output
注意:
若不是标准的点九图,在转换的过程会报错,这时候请设计重新提供新的点九图
实际开发当中遇到的问题
小屏手机适配问题
刚开始,我们的切图是按照 2 倍图切的,这样在小屏幕手机上会手机气泡高度过大的问题。
原因分析:
该现象的本质是点九图图片的高度大于单行文本消息的高度。
解决方案一(暂时不可取):
我尝试去压缩点九图,但最终再部分手机上面显示错乱,不知道是不是压缩点九图的方法错了。
解决方案二
对于低分辨率的手机和高分辨的手机分别下发不同的图片 url,我们尝试过得方案是当 density < 2
的时候,采用一倍图图片,density >= 2
采用二倍图图片。
解决方案三
可能有人会有这样的疑问呢,为什么要采用一倍图,两倍图的解决方案呢?直接让 UI 设计师给一套图,点九图图片的高度适中不就解决了。是啊,我们也是这样想得,但他们说对于有一些装饰的点九图,如果缩小高度,一些装饰图案他们不太好切。比如下面图片中的星星。
小结
说到底,方案二,方案三其实都是折中的一种方案,如果直接能够做到点九图缩放,那就完美解决了。而 Android 中 res 目录中的 drawable 或者 mipmap 的点九图确实能做到,去看了相关的代码,目前也没有发现什么好的解决方案,如果你有好的解决方案话,欢迎留言交流。
点九图的 padding 在部分手机上面失效
这个是部分 Android 手机的 bug,解决方法见:[stackoverflow.com/questions/1…](
)
public class NinePatchChunk {
private static final String TAG = "NinePatchChunk";
public final Rect mPaddings = new Rect();
public int mDivX[];public int mDivY[];public int mColor[];
private static float density = IMO.getInstance().getResources().getDisplayMetrics().density;
private static void readIntArray(final int[] data, final ByteBuffer buffer) {for (int i = 0, n = data.length; i < n; ++i)data[i] = buffer.getInt();}
private static void checkDivCount(final int length) {if (length == 0 || (length & 0x01) != 0)throw new IllegalStateException("invalid nine-patch: " + length);}
public static Rect getPaddingRect(final byte[] data) {NinePatchChunk deserialize = deserialize(data);if (deserialize == null) {return new Rect();}}
public static NinePatchChunk deserialize(final byte[] data) {final ByteBuffer byteBuffer =ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
if (byteBuffer.get() == 0) {return null; // is not serialized}
final NinePatchChunk chunk = new NinePatchChunk();chunk.mDivX = new int[byteBuffer.get()];chunk.mDivY = new int[byteBuffer.get()];chunk.mColor = new int[byteBuffer.get()];
try {checkDivCount(chunk.mDivX.length);checkDivCount(chunk.mDivY.length);} catch (Exception e) {return null;}
// skip 8 bytesbyteBuffer.getInt();byteBuffer.getInt();
chunk.mPaddings.left = byteBuffer.getInt();chunk.mPaddings.right = byteBuffer.getInt();chunk.mPaddings.top = byteBuffer.getInt();chunk.mPaddings.bottom = byteBuffer.getInt();
// skip 4 bytesbyteBuffer.getInt();
readIntArray(chunk.mDivX, byteBuffer);readIntArray(chunk.mDivY, byteBuffer);readIntArray(chunk.mColor, byteBuffer);
return chunk;}}
NinePatchDrawable patchy = new NinePatchDrawable(view.getResources(), bitmap, chunk, NinePatchChunk.getPaddingRect(chunk), null);view.setBackground(patchy);
动态下载点九图会导致聊天气泡闪烁
这里我们采取的方案是预下载(预下载 10 个)
聊天气泡采用内存缓存,磁盘缓存,确保 RecyclerView 快速滑动的时候不会闪烁
理解点九图
以下内容参考腾讯音乐的 [Android 动态布局入门及 NinePatchChunk 解密](
)
回顾 NinePatchDrawable 的构造方法第三个参数 bitmap.getNinePatchChunk(),作者猜想,aapt 命令其实就是在 bitmap 图片中,加入了 NinePatchChunk 的信息,那么我们是不是只要能自己构造出这个东西,就可以让任何图片按照我们想要的方式拉升了呢?
可是查了一堆官方文档,似乎并找不到相应的方法来获得这个 byte[]类型的 chunk 参数。
既然无法知道这个 chunk 如何生成,那么能不能从解析的角度逆向得出这个 NinePatchChunk 的生成方法呢?
下面就需要从源码入手了。
NinePatchChunk.java
public static NinePatchChunk deserialize(byte[] data) {ByteBuffer byteBuffer =ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());byte wasSerialized = byteBuffer.get();if (wasSerialized == 0) return null;NinePatchChunk chunk = new NinePatchChunk();chunk.mDivX = new int[byteBuffer.get()];chunk.mDivY = new int[byteBuffer.get()];chunk.mColor = new int[byteBuffer.get()];checkDivCount(chunk.mDivX.length);checkDivCount(chunk.mDivY.length);// skip 8 bytesbyteBuffer.getInt();byteBuffer.getInt();chunk.mPaddings.left = byteBuffer.getInt();chunk.mPaddings.right = byteBuffer.getInt();chunk.mPaddings.top = byteBuffer.getInt();chunk.mPaddings.bottom = byteBuffer.getInt();// skip 4 bytesbyteBuffer.getInt();readIntArray(chunk.mDivX, byteBuffer);readIntArray(chunk.mDivY, byteBuffer);readIntArray(chunk.mColor, byteBuffer);return chunk;}
其实从这部分解析 byte[] chunk 的源码,我们已经可以反推出来大概的结构了。如下图,
按照上图中的猜想以及对.9.png 的认识,直觉感受到,mDivX,mDivY,mColor 这三个数组是最关键的,但是具体是什么,就要继续看源码了。
ResourceTypes.h
/**
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):
+-----+----+------+-------+
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.
/
正如源码中,注释的一样,这个 NinePatch Chunk 把图片从 x 轴和 y 轴分成若干个区域,F 区域代表了固定,S 区域代表了拉伸。mDivX,mDivY 描述了所有 S 区域的位置起始,而 mColor 描述了,各个 Segment 的颜色,通常情况下,赋值为源码中定义的 NO_COLOR = 0x00000001 就行了。就以源码注释中的例子来说,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]]
对于 mColor 这个数组,长度等于划分的区域数,是用来描述各个区域的颜色的,而如果我们这个只是描述了一个 bitmap 的拉伸方式的话,是不需要颜色的,即源码中 NO_COLOR = 0x00000001
说了这么多,我们还是通过一个简单例子来说明如何构造一个按中心点拉伸的 NinePatchDrawable 吧,
Bitmap bitmap = BitmapFactory.decodeFile(filepath);int[] xRegions = new int[]{bitmap.getWidth() / 2, bitmap.getWidth() / 2 + 1};int[] yRegions = new int[]{bitmap.getWidth() / 2, bitmap.getWidth() / 2 + 1};int NO_COLOR = 0x00000001;int colorSize = 9;int bufferSize = xRegions.length * 4 + yRegions.length * 4 + colorSize * 4 + 32;
ByteBuffer byteBuffer = ByteBuffer.allocate(bufferSize).order(ByteOrder.nativeOrder());// 第一个 byte,要不等于 0byteBuffer.put((byte) 1);
//mDivX lengthbyteBuffer.put((byte) 2);//mDivY lengthbyteBuffer.put((byte) 2);//mColors lengthbyteBuffer.put((byte) colorSize);
//skipbyteBuffer.putInt(0);byteBuffer.putInt(0);
//padding 先设为 0byteBuffer.putInt(0);byteBuffer.putInt(0);byteBuffer.putInt(0);byteBuffer.putInt(0);
//skipbyteBuffer.putInt(0);
// mDivXbyteBuffer.putInt(xRegions[0]);byteBuffer.putInt(xRegions[1]);
评论