android中多态的应用_Android点9图机制及在聊天气泡中的应用

点九图简介

Android为了使用同一张图作为不同数量文字的背景,设计了一种可以指定区域拉伸的图片格式“.9.png”,这种图片格式就是点九图。

注意:这种图片格式只能被使用于Android开发。在ios开发中,可以在代码中指定某个点进行拉伸,而在Android中不行,所以在Android中想要达到这个效果,只能使用点九图(下文会啪啪打脸,其实是可以的,只是很少人这样使用,兼容性不知道怎么样,点击跳转)

1e598cb7bfc85cf869975390f9ad2a6a.gif

点九图实质

点九图的本质实际上是在图片的四周各增加了1px的像素,并使用纯黑(#FF000000)的线进行标记,其它的与原图没有任何区别。可以参考以下图片:

c49d2891fe49312bc22f2b7b0998a8a2.png
a8ba196ad975fcf0b81491fe43e311a4.png
标记位置含义
左-黑点纵向拉伸区域
上-黑点横向拉伸区域
右-黑线纵向显示区域
下-黑线横向显示区域

点九图在 Android 中的应用

点九图在 Android 中主要有三种应用方式

  1. 直接放在 res 目录中的 drawable 或者 mipmap 目录中

  2. 放在 assert 目录中

  3. 从网络下载

第一种方式是我们最常用的,直接调用 setBackgroundResource 或者 setImageResource 方法,这样的话图片及可以做到自动拉伸。

而对于第二种或者第三种方式,如果我们直接去加载 .9.png,你会发现图片或者图片背景根本无法拉伸。纳尼,这是为甚么呢。下面,且听老衲慢慢道来。

Android 并不是直接使用点九图,而是在编译时将其转换为另外一种格式,这种格式是将其四周的黑色像素保存至Bitmap类中的一个名为 mNinePatchChunk 的 byte[] 中,并抹除掉四周的这一个像素的宽度;接着在使用时,如果 Bitmap 的这个 mNinePatchChunk 不为空,且为 9patch chunk,则将其构造为 NinePatchDrawable,否则将会被构造为 BitmapDrawable,最终设置给 view。

因此,在 Android 中,我们如果想动态使用网络下载的点九图,一般需要经过以下步骤:

  1. 使用 sdk 目录下的 aapt 工具将点九图转化为 png 图片

  2. 解析图片的时候,判断是否含有 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}

点九图上传服务器流程

3bdc9751abcbe77f18106f86817a637d.png

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 倍图切的,这样在小屏幕手机上会手机气泡高度过大的问题。

ef2df50d1f1c1417026a29ab1b24cb4b.png

原因分析:

该现象的本质是点九图图片的高度大于单行文本消息的高度。

解决方案一(暂时不可取):

  1. 我尝试去压缩点九图,但最终再部分手机上面显示错乱,不知道是不是压缩点九图的方法错了。

解决方案二

对于低分辨率的手机和高分辨的手机分别下发不同的图片 url,我们尝试过得方案是当 density < 2 的时候,采用一倍图图片,density >= 2 采用二倍图图片。

解决方案三

可能有人会有这样的疑问呢,为什么要采用一倍图,两倍图的解决方案呢?直接让 UI 设计师给一套图,点九图图片的高度适中不就解决了。是啊,我们也是这样想得,但他们说对于有一些装饰的点九图,如果缩小高度,一些装饰图案他们不太好切。比如下面图片中的星星。

c49d2891fe49312bc22f2b7b0998a8a2.png

小结

说到底,方案二,方案三其实都是折中的一种方案,如果直接能够做到点九图缩放,那就完美解决了。而 Android 中 res 目录中的 drawable 或者 mipmap 的点九图确实能做到,去看了相关的代码,目前也没有发现什么好的解决方案,如果你有好的解决方案话,欢迎留言交流。

点九图的 padding 在部分手机上面失效

这个是部分 Android 手机的 bug,解决方法见:https://stackoverflow.com/questions/11065996/ninepatchdrawable-does-not-get-padding-from-chunk

 1public class NinePatchChunk {
2
3    private static final String TAG = "NinePatchChunk";
4
5    public final Rect mPaddings = new Rect();
6
7    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 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);

动态下载点九图会导致聊天气泡闪烁

  1. 这里我们采取的方案是预下载(预下载 10 个)

  2. 聊天气泡采用内存缓存,磁盘缓存,确保 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 for
3 * scaling.
4 *
5 * There are J horizontal and K vertical segments.  These segments divide
6 * the image into J*K regions as follows (where J=4 and K=3):
7 *
8 *      F0   S0    F1     S1
9 *   +-----+----+------+-------+
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;
7
8ByteBuffer 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 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 xRegions = new ArrayList(); 6    private ArrayList yRegions = new ArrayList(); 7 8    public NinePatchBuilder(Resources resources, Bitmap bitmap) { 9        width = bitmap.getWidth(); 10        height = bitmap.getHeight(); 11        this.bitmap = bitmap; 12        this.resources = resources; 13    } 14 15    public NinePatchBuilder(int width, int height) { 16        this.width = width; 17        this.height = height; 18    } 19 20    public NinePatchBuilder addXRegion(int x, int width) { 21        xRegions.add(x); 22        xRegions.add(x + width); 23        return this; 24    } 25 26    public NinePatchBuilder addXRegionPoints(int x1, int x2) { 27        xRegions.add(x1); 28        xRegions.add(x2); 29        return this; 30    } 31 32    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    } 38 39    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    } 44 45    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    } 51 52    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    } 59 60    public NinePatchBuilder addYRegion(int y, int height) { 61        yRegions.add(y); 62        yRegions.add(y + height); 63        return this; 64    } 65 66    public NinePatchBuilder addYRegionPoints(int y1, int y2) { 67        yRegions.add(y1); 68        yRegions.add(y2); 69        return this; 70    } 71 72    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    } 78 79    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    } 84 85    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    } 91 92    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    } 99100    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        }109110        int NO_COLOR = 1;//0x00000001;111        int COLOR_SIZE = 9;//could change, may be 2 or 6 or 15 - but has no effect on output112        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 translated115        byteBuffer.put((byte) xRegions.size());//divisions x116        byteBuffer.put((byte) yRegions.size());//divisions y117        byteBuffer.put((byte) COLOR_SIZE);//color size118119        //skip120        byteBuffer.putInt(0);121        byteBuffer.putInt(0);122123        //padding -- always 0 -- left right top bottom124        byteBuffer.putInt(0);125        byteBuffer.putInt(0);126        byteBuffer.putInt(0);127        byteBuffer.putInt(0);128129        //skip130        byteBuffer.putInt(0);131132        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 ...136137        for (int i = 0; i 138            byteBuffer.putInt(NO_COLOR);139140        return byteBuffer.array();141    }142143    public NinePatch buildNinePatch() {144        byte[] chunk = buildChunk();145        if (bitmap != null)146            return new NinePatch(bitmap, chunk, null);147        return null;148    }149150    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}
3d5f183d05641adf1c779e9a4207c405.png

可以看到,我们的图片完美拉伸


参考文章

  1. https://cloud.tencent.com/developer/article/1168755?

  2. https://mp.weixin.qq.com/s/rZyt9ECypfwvNnXKt5xtqg

推荐阅读早就是优势!我每年得忽悠10万程序员上车为什么说80%的程序员都缺乏基本功?

编程·思维·职场
欢迎扫码关注

9cbbcb6e73350512ed2c92da4d64589c.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值