Android点9图机制及在聊天气泡中的应用
点九图简介
Android为了使用同一张图作为不同数量文字的背景,设计了一种可以指定区域拉伸的图片格式“.9.png”,这种图片格式就是点九图。
注意:这种图片格式只能被使用于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
1public static void setNineImagePatch(View view, File file, String url) {2 if (file.exists()) {3 Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());4 byte[] chunk = bitmap.getNinePatchChunk();5 if (NinePatch.isNinePatchChunk(chunk)) {6 NinePatchDrawable patchy = new NinePatchDrawable(view.getResources(), bitmap, chunk, new Rect(), null);7 view.setBackground(patchy);8 }9
10 }
11}
点九图上传服务器流程
aapt 转换命令
单个图片文件转换
1./aapt s -i xxx.9.png -o xxx.png
批量转换
1# 批量转换
2./aapt c -S inputDir -C outputDir
3# inputDir 为原始.9图文件夹,outputDir 为输出文件夹
执行成功实例
1jundeMacBook-Pro:一期气泡 junxu$ ./aapt c -S /Users/junxu/Desktop/一期气泡/气泡需求整理 -C /Users/junxu/Desktop/一期气泡/output
2Crunching PNG Files in source dir: /Users/junxu/Desktop/一期气泡/气泡需求整理
3To destination dir: /Users/junxu/Desktop/一期气泡/output
注意:
若不是标准的点九图,在转换的过程会报错,这时候请设计重新提供新的点九图
实际开发当中遇到的问题
小屏手机适配问题
刚开始,我们的切图是按照 2 倍图切的,这样在小屏幕手机上会手机气泡高度过大的问题。
原因分析:
该现象的本质是点九图图片的高度大于单行文本消息的高度。
解决方案一(暂时不可取):
我尝试去压缩点九图,但最终再部分手机上面显示错乱,不知道是不是压缩点九图的方法错了。
解决方案二
对于低分辨率的手机和高分辨的手机分别下发不同的图片 url,我们尝试过得方案是当 density < 2
的时候,采用一倍图图片,density >= 2
采用二倍图图片。
解决方案三
可能有人会有这样的疑问呢,为什么要采用一倍图,两倍图的解决方案呢?直接让 UI 设计师给一套图,点九图图片的高度适中不就解决了。是啊,我们也是这样想得,但他们说对于有一些装饰的点九图,如果缩小高度,一些装饰图案他们不太好切。比如下面图片中的星星。
小结
说到底,方案二,方案三其实都是折中的一种方案,如果直接能够做到点九图缩放,那就完美解决了。而 Android 中 res 目录中的 drawable 或者 mipmap 的点九图确实能做到,去看了相关的代码,目前也没有发现什么好的解决方案,如果你有好的解决方案话,欢迎留言交流。
点九图的 padding 在部分手机上面失效
这个是部分 Android 手机的 bug,解决方法见:https://stackoverflow.com/questions/11065996/ninepatchdrawable-does-not-get-padding-from-chunk
1public class NinePatchChunk {23 private static final String TAG = "NinePatchChunk";45 public final Rect mPaddings = new Rect();67 public int mDivX[];8 public int mDivY[];9 public int mColor[];
10
11 private static float density = IMO.getInstance().getResources().getDisplayMetrics().density;
12
13 private static void readIntArray(final int[] data, final ByteBuffer buffer) {
14 for (int i = 0, n = data.length; i < n; ++i)
15 data[i] = buffer.getInt();
16 }
17
18 private static void checkDivCount(final int length) {
19 if (length == 0 || (length & 0x01) != 0)
20 throw new IllegalStateException("invalid nine-patch: " + length);
21 }
22
23 public static Rect getPaddingRect(final byte[] data) {
24 NinePatchChunk deserialize = deserialize(data);
25 if (deserialize == null) {
26 return new Rect();
27 }
28 }
29
30 public static NinePatchChunk deserialize(final byte[] data) {
31 final ByteBuffer byteBuffer =
32 ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
33
34 if (byteBuffer.get() == 0) {
35 return null; // is not serialized
36 }
37
38 final NinePatchChunk chunk = new NinePatchChunk();
39 chunk.mDivX = new int[byteBuffer.get()];
40 chunk.mDivY = new int[byteBuffer.get()];
41 chunk.mColor = new int[byteBuffer.get()];
42
43 try {
44 checkDivCount(chunk.mDivX.length);
45 checkDivCount(chunk.mDivY.length);
46 } catch (Exception e) {
47 return null;
48 }
49
50
51 // skip 8 bytes
52 byteBuffer.getInt();
53 byteBuffer.getInt();
54
55
56 chunk.mPaddings.left = byteBuffer.getInt();
57 chunk.mPaddings.right = byteBuffer.getInt();
58 chunk.mPaddings.top = byteBuffer.getInt();
59 chunk.mPaddings.bottom = byteBuffer.getInt();
60
61
62 // skip 4 bytes
63 byteBuffer.getInt();
64
65 readIntArray(chunk.mDivX, byteBuffer);
66 readIntArray(chunk.mDivY, byteBuffer);
67 readIntArray(chunk.mColor, byteBuffer);
68
69 return chunk;
70 }
71}
72
73NinePatchDrawable patchy = new NinePatchDrawable(view.getResources(), bitmap, chunk, NinePatchChunk.getPaddingRect(chunk), null);
74view.setBackground(patchy);
动态下载点九图会导致聊天气泡闪烁
这里我们采取的方案是预下载(预下载 10 个)
聊天气泡采用内存缓存,磁盘缓存,确保 RecyclerView 快速滑动的时候不会闪烁
理解点九图
以下内容参考腾讯音乐的 Android动态布局入门及NinePatchChunk解密
回顾NinePatchDrawable的构造方法第三个参数bitmap.getNinePatchChunk(),作者猜想,aapt命令其实就是在bitmap图片中,加入了NinePatchChunk的信息,那么我们是不是只要能自己构造出这个东西,就可以让任何图片按照我们想要的方式拉升了呢?
可是查了一堆官方文档,似乎并找不到相应的方法来获得这个byte[]类型的chunk参数。
既然无法知道这个chunk如何生成,那么能不能从解析的角度逆向得出这个NinePatchChunk的生成方法呢?
下面就需要从源码入手了。
NinePatchChunk.java
1public static NinePatchChunk deserialize(byte[] data) {2 ByteBuffer byteBuffer =3 ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());4 byte wasSerialized = byteBuffer.get();5 if (wasSerialized == 0) return null;6 NinePatchChunk chunk = new NinePatchChunk();7 chunk.mDivX = new int[byteBuffer.get()];8 chunk.mDivY = new int[byteBuffer.get()];9 chunk.mColor = new int[byteBuffer.get()];
10 checkDivCount(chunk.mDivX.length);
11 checkDivCount(chunk.mDivY.length);
12 // skip 8 bytes
13 byteBuffer.getInt();
14 byteBuffer.getInt();
15 chunk.mPaddings.left = byteBuffer.getInt();
16 chunk.mPaddings.right = byteBuffer.getInt();
17 chunk.mPaddings.top = byteBuffer.getInt();
18 chunk.mPaddings.bottom = byteBuffer.getInt();
19 // skip 4 bytes
20 byteBuffer.getInt();
21 readIntArray(chunk.mDivX, byteBuffer);
22 readIntArray(chunk.mDivY, byteBuffer);
23 readIntArray(chunk.mColor, byteBuffer);
24 return chunk;
25}
其实从这部分解析byte[] chunk的源码,我们已经可以反推出来大概的结构了。如下图,
按照上图中的猜想以及对.9.png的认识,直觉感受到,mDivX,mDivY,mColor这三个数组是最关键的,但是具体是什么,就要继续看源码了。
ResourceTypes.h
1/**2 * This chunk specifies how to split an image into segments for3 * scaling.4 *5 * There are J horizontal and K vertical segments. These segments divide6 * the image into J*K regions as follows (where J=4 and K=3):7 *8 * F0 S0 F1 S19 * +-----+----+------+-------+
10 * S2| 0 | 1 | 2 | 3 |
11 * +-----+----+------+-------+
12 * | | | | |
13 * | | | | |
14 * F2| 4 | 5 | 6 | 7 |
15 * | | | | |
16 * | | | | |
17 * +-----+----+------+-------+
18 * S3| 8 | 9 | 10 | 11 |
19 * +-----+----+------+-------+
20 *
21 * Each horizontal and vertical segment is considered to by either
22 * stretchable (marked by the Sx labels) or fixed (marked by the Fy
23 * labels), in the horizontal or vertical axis, respectively. In the
24 * above example, the first is horizontal segment (F0) is fixed, the
25 * next is stretchable and then they continue to alternate. Note that
26 * the segment list for each axis can begin or end with a stretchable
27 * or fixed segment.
28 * /
正如源码中,注释的一样,这个NinePatch Chunk把图片从x轴和y轴分成若干个区域,F区域代表了固定,S区域代表了拉伸。mDivX,mDivY描述了所有S区域的位置起始,而mColor描述了,各个Segment的颜色,通常情况下,赋值为源码中定义的NO_COLOR = 0x00000001就行了。就以源码注释中的例子来说,mDivX,mDivY,mColor如下:
1mDivX = [ S0.start, S0.end, S1.start, S1.end];
2mDivY = [ S2.start, S2.end, S3.start, S3.end];
3mColor = [c[0],c[1],...,c[11]]
对于mColor这个数组,长度等于划分的区域数,是用来描述各个区域的颜色的,而如果我们这个只是描述了一个bitmap的拉伸方式的话,是不需要颜色的,即源码中NO_COLOR = 0x00000001
说了这么多,我们还是通过一个简单例子来说明如何构造一个按中心点拉伸的 NinePatchDrawable 吧,
1Bitmap bitmap = BitmapFactory.decodeFile(filepath);2int[] xRegions = new int[]{bitmap.getWidth() / 2, bitmap.getWidth() / 2 + 1};3int[] yRegions = new int[]{bitmap.getWidth() / 2, bitmap.getWidth() / 2 + 1};4int NO_COLOR = 0x00000001;5int colorSize = 9;6int bufferSize = xRegions.length * 4 + yRegions.length * 4 + colorSize * 4 + 32;78ByteBuffer byteBuffer = ByteBuffer.allocate(bufferSize).order(ByteOrder.nativeOrder());9// 第一个byte,要不等于0
10byteBuffer.put((byte) 1);
11
12//mDivX length
13byteBuffer.put((byte) 2);
14//mDivY length
15byteBuffer.put((byte) 2);
16//mColors length
17byteBuffer.put((byte) colorSize);
18
19//skip
20byteBuffer.putInt(0);
21byteBuffer.putInt(0);
22
23//padding 先设为0
24byteBuffer.putInt(0);
25byteBuffer.putInt(0);
26byteBuffer.putInt(0);
27byteBuffer.putInt(0);
28
29//skip
30byteBuffer.putInt(0);
31
32// mDivX
33byteBuffer.putInt(xRegions[0]);
34byteBuffer.putInt(xRegions[1]);
35
36// mDivY
37byteBuffer.putInt(yRegions[0]);
38byteBuffer.putInt(yRegions[1]);
39
40// mColors
41for (int i = 0; i < colorSize; i++) {
42 byteBuffer.putInt(NO_COLOR);
43}
44
45return byteBuffer.array();
create-a-ninepatch-ninepatchdrawable-in-runtime
在 stackoverflow 上面也找到牛逼的类,可以动态创建点九图,并拉伸图片,啪啪打脸,刚开始说到 android 中无法想 ios 一样动态指定图片拉伸区域。
1public class NinePatchBuilder {2 int width, height;3 Bitmap bitmap;4 Resources resources;5 private ArrayList<Integer> xRegions = new ArrayList<Integer>();6 private ArrayList<Integer> yRegions = new ArrayList<Integer>();78 public NinePatchBuilder(Resources resources, Bitmap bitmap) {9 width = bitmap.getWidth();10 height = bitmap.getHeight();11 this.bitmap = bitmap;12 this.resources = resources;13 }1415 public NinePatchBuilder(int width, int height) {16 this.width = width;17 this.height = height;18 }1920 public NinePatchBuilder addXRegion(int x, int width) {21 xRegions.add(x);22 xRegions.add(x + width);23 return this;24 }2526 public NinePatchBuilder addXRegionPoints(int x1, int x2) {27 xRegions.add(x1);28 xRegions.add(x2);29 return this;30 }3132 public NinePatchBuilder addXRegion(float xPercent, float widthPercent) {33 int xtmp = (int) (xPercent * this.width);34 xRegions.add(xtmp);35 xRegions.add(xtmp + (int) (widthPercent * this.width));36 return this;37 }3839 public NinePatchBuilder addXRegionPoints(float x1Percent, float x2Percent) {40 xRegions.add((int) (x1Percent * this.width));41 xRegions.add((int) (x2Percent * this.width));42 return this;43 }4445 public NinePatchBuilder addXCenteredRegion(int width) {46 int x = (int) ((this.width - width) / 2);47 xRegions.add(x);48 xRegions.add(x + width);49 return this;50 }5152 public NinePatchBuilder addXCenteredRegion(float widthPercent) {53 int width = (int) (widthPercent * this.width);54 int x = (int) ((this.width - width) / 2);55 xRegions.add(x);56 xRegions.add(x + width);57 return this;58 }5960 public NinePatchBuilder addYRegion(int y, int height) {61 yRegions.add(y);62 yRegions.add(y + height);63 return this;64 }6566 public NinePatchBuilder addYRegionPoints(int y1, int y2) {67 yRegions.add(y1);68 yRegions.add(y2);69 return this;70 }7172 public NinePatchBuilder addYRegion(float yPercent, float heightPercent) {73 int ytmp = (int) (yPercent * this.height);74 yRegions.add(ytmp);75 yRegions.add(ytmp + (int) (heightPercent * this.height));76 return this;77 }7879 public NinePatchBuilder addYRegionPoints(float y1Percent, float y2Percent) {80 yRegions.add((int) (y1Percent * this.height));81 yRegions.add((int) (y2Percent * this.height));82 return this;83 }8485 public NinePatchBuilder addYCenteredRegion(int height) {86 int y = (int) ((this.height - height) / 2);87 yRegions.add(y);88 yRegions.add(y + height);89 return this;90 }9192 public NinePatchBuilder addYCenteredRegion(float heightPercent) {93 int height = (int) (heightPercent * this.height);94 int y = (int) ((this.height - height) / 2);95 yRegions.add(y);96 yRegions.add(y + height);97 return this;98 }99
100 public byte[] buildChunk() {
101 if (xRegions.size() == 0) {
102 xRegions.add(0);
103 xRegions.add(width);
104 }
105 if (yRegions.size() == 0) {
106 yRegions.add(0);
107 yRegions.add(height);
108 }
109
110 int NO_COLOR = 1;//0x00000001;
111 int COLOR_SIZE = 9;//could change, may be 2 or 6 or 15 - but has no effect on output
112 int arraySize = 1 + 2 + 4 + 1 + xRegions.size() + yRegions.size() + COLOR_SIZE;
113 ByteBuffer byteBuffer = ByteBuffer.allocate(arraySize * 4).order(ByteOrder.nativeOrder());
114 byteBuffer.put((byte) 1);//was translated
115 byteBuffer.put((byte) xRegions.size());//divisions x
116 byteBuffer.put((byte) yRegions.size());//divisions y
117 byteBuffer.put((byte) COLOR_SIZE);//color size
118
119 //skip
120 byteBuffer.putInt(0);
121 byteBuffer.putInt(0);
122
123 //padding -- always 0 -- left right top bottom
124 byteBuffer.putInt(0);
125 byteBuffer.putInt(0);
126 byteBuffer.putInt(0);
127 byteBuffer.putInt(0);
128
129 //skip
130 byteBuffer.putInt(0);
131
132 for (int rx : xRegions)
133 byteBuffer.putInt(rx); // regions left right left right ...
134 for (int ry : yRegions)
135 byteBuffer.putInt(ry);// regions top bottom top bottom ...
136
137 for (int i = 0; i < COLOR_SIZE; i++)
138 byteBuffer.putInt(NO_COLOR);
139
140 return byteBuffer.array();
141 }
142
143 public NinePatch buildNinePatch() {
144 byte[] chunk = buildChunk();
145 if (bitmap != null)
146 return new NinePatch(bitmap, chunk, null);
147 return null;
148 }
149
150 public NinePatchDrawable build() {
151 NinePatch ninePatch = buildNinePatch();
152 if (ninePatch != null)
153 return new NinePatchDrawable(resources, ninePatch);
154 return null;
155 }
156}
运行一下测试代码
1mLlRoot = findViewById(R.id.ll_root);2try {3 InputStream is = getAssets().open("sea.png");4 Bitmap bitmap = BitmapFactory.decodeStream(is);5 for (int i = 0; i < 5; i++) {6 NinePatchDrawable ninePatchDrawable = NinePatchHelper.buildMulti(this, bitmap);7 TextView textView = new TextView(this);8 textView.setTextSize(25);9 textView.setPadding(20, 10, 20, 10);
10 textView.setText(strArray[i]);
11 textView.setGravity(Gravity.CENTER_VERTICAL);
12 LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
13 layoutParams.leftMargin = 20;
14 layoutParams.rightMargin = 20;
15 textView.setLayoutParams(layoutParams);
16 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
17 textView.setBackground(ninePatchDrawable);
18 }
19 mLlRoot.addView(textView);
20 }
21} catch (IOException e) {
22 e.printStackTrace();
23}
可以看到,我们的图片完美拉伸
参考文章
https://cloud.tencent.com/developer/article/1168755?
https://mp.weixin.qq.com/s/rZyt9ECypfwvNnXKt5xtqg
推荐阅读
编程·思维·职场
Android点9图机制及在聊天气泡中的应用相关推荐
- android .9图片在代码中的设置,Android点九图总结以及在聊天气泡中的使用
编辑推荐: 本文来自于腾讯云,介绍了点九图的本质,聊天气泡中使用点九图,其它问题等. 1. 点九图介绍 这一块是对点九图的简单介绍,如果对这块已经有了解的话,可以直接跳到2,看看聊天气泡中如何使用点九 ...
- Android 宫格图控件MultiImageView(RecyclerView适配器中展示不定数量的图片)
Android 宫格图控件MultiImageView(RecyclerView适配器中展示不定数量的图片) 最近的github上的996ICU在IT界应该很多人听说了,不知道CSDN上什么是最受大家 ...
- Android 点九图机制讲解及在聊天气泡中的应用
点九图简介 Android为了使用同一张图作为不同数量文字的背景,设计了一种可以指定区域拉伸的图片格式".9.png",这种图片格式就是点九图. 注意:这种图片格式只能被使用于An ...
- 聊天气泡图片的动态拉伸、适配与镜像
聊天气泡图片的动态拉伸.适配与镜像 前情提要 创建.9.png格式的图片 从资源文件夹加载.9.png图片 从本地文件加载".9.png"图片 项目痛点 进阶探索 iOS中的方式 ...
- android气泡组件,Android 聊天气泡
网上搜到的只有一篇是自定义的TextView,其使用比较麻烦,所以采用大众化的方法--使用9.png来实现. 这里主要介绍sdk tool的draw9patch.bat的使用. 这个bat执行文件打开 ...
- android .9png聊天气泡,Android 关于点9图在气泡评论里使用的调研
需求是这样的 气泡 1. 点9图简单介绍 Android为了使用同一张图作为大小区域背景,设计了一种可以指定区域拉伸的图片格式".9.png",这种图片格式就是点九图.Androi ...
- android 屏幕密度350 是xh xxh,Android-绘图机制总结
这里你将了解到以下内容:Android屏幕相关知识 Android绘图技巧 Android图像处理技巧 SurfaceView的使用 1.屏幕尺寸信息 1.屏幕参数 一块屏幕通常具备以下的几个参数 屏 ...
- 反思 | Android 音视频缓存机制的系统性设计
反思 系列博客是我的一种新学习方式的尝试,该系列起源和目录请参考 这里 . 背景 在自媒体的时代,音视频播放 俨然已成为内容类型 APP 最基础的能力,对于 Android 开发者而言,无论是 Goo ...
- Android Touch事件传递机制 二:单纯的(伪生命周期) 这个清楚一点
转载于:http://blog.csdn.net/yuanzeyao/article/details/38025165 在前一篇文章中,我主要讲解了Android源码中的Touch事件的传递过程,现在 ...
最新文章
- 后台开发经典书籍--计算机网络
- Eclipse | 使用
- Unity 中使用Async-Await替代 coroutines
- android 标题图标,android 中 actionbar 常用方法。设置标题,隐藏图标等
- 【移动开发】EditText输入字数限制总结(包括中文输入内存溢出的解决方法)...
- iPhone走马灯控件实现
- Python练习:星号三角形 I
- 解决实例化Servlet类[com.mu.servlet.HelloServlet]异常
- oracle 自动关闭 数据库连接
- Android:图解四种启动模式 及 实际应用场景解说
- SpringCloud项目接入Nacos服务治理
- 金融企业如何构建有效的数据分析体系?1000+案例经验汇总
- android 指纹比对方法,指纹识别功能方面对比_手机Android频道-中关村在线
- Excel文档误删的4种恢复方法,1秒就可以还原所有内容,你用过吗
- spring定时任务总结
- python名片管理系统的总结_名片管理系统(Python)
- 【雷达与对抗】【2006.09】多普勒模式匹配对雷达探测性能的改进
- mysql连接数据了的dep_MySQL数据库基础
- windows无法启动此程序,因为计算机中丢失rtl70.bpl
- 4、Linux用户和用户组管理