第一种方式是我们最常用的,直接调用 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 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/一期气泡/output
Crunching 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,解决方法见:https://stackoverflow.com/questions/11065996/ninepatchdrawable-does-not-get-padding-from-chunk
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 bytes
byteBuffer.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 bytes
byteBuffer.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 bytes
byteBuffer.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 bytes
byteBuffer.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):
-
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.
-
/
正如源码中,注释的一样,这个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,要不等于0
byteBuffer.put((byte) 1);
//mDivX length
byteBuffer.put((byte) 2);
//mDivY length
byteBuffer.put((byte) 2);
//mColors length
byteBuffer.put((byte) colorSize);
//skip
byteBuffer.putInt(0);
byteBuffer.putInt(0);
//padding 先设为0
byteBuffer.putInt(0);
byteBuffer.putInt(0);
byteBuffer.putInt(0);
byteBuffer.putInt(0);
//skip
byteBuffer.putInt(0);
// mDivX
byteBuffer.putInt(xRegions[0]);
byteBuffer.putInt(xRegions[1]);
// mDivY
byteBuffer.putInt(yRegions[0]);
byteBuffer.putInt(yRegions[1]);
// mColors
for (int i = 0; i < colorSize; i++) {
byteBuffer.putInt(NO_COLOR);
}
return byteBuffer.array();
create-a-ninepatch-ninepatchdrawable-in-runtime
在 stackoverflow 上面也找到牛逼的类,可以动态创建点九图,并拉伸图片,啪啪打脸,刚开始说到 android 中无法想 ios 一样动态指定图片拉伸区域。
public class NinePatchBuilder {
int width, height;
Bitmap bitmap;
Resources resources;
private ArrayList xRegions = new ArrayList();
private ArrayList yRegions = new ArrayList();
public NinePatchBuilder(Resources resources, Bitmap bitmap) {
width = bitmap.getWidth();
height = bitmap.getHeight();
this.bitmap = bitmap;
this.resources = resources;
}
public NinePatchBuilder(int width, int height) {
this.width = width;
this.height = height;
}
public NinePatchBuilder addXRegion(int x, int width) {
xRegions.add(x);
xRegions.add(x + width);
return this;
}
public NinePatchBuilder addXRegionPoints(int x1, int x2) {
xRegions.add(x1);
xRegions.add(x2);
return this;
}
public NinePatchBuilder addXRegion(float xPercent, float widthPercent) {
int xtmp = (int) (xPercent * this.width);
xRegions.add(xtmp);
xRegions.add(xtmp + (int) (widthPercent * this.width));
return this;
}
public NinePatchBuilder addXRegionPoints(float x1Percent, float x2Percent) {
xRegions.add((int) (x1Percent * this.width));
xRegions.add((int) (x2Percent * this.width));
return this;
}
public NinePatchBuilder addXCenteredRegion(int width) {
int x = (int) ((this.width - width) / 2);
xRegions.add(x);
xRegions.add(x + width);
return this;
}
public NinePatchBuilder addXCenteredRegion(float widthPercent) {
int width = (int) (widthPercent * this.width);
int x = (int) ((this.width - width) / 2);
xRegions.add(x);
xRegions.add(x + width);
return this;
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
![](https://img-blog.csdnimg.cn/img_convert/1d37c07ac5180929336109dd94042018.jpeg)
最后我想说
为什么很多程序员做不了架构师?
1、良好健康的职业规划很重要,但大多数人都忽略了
2、学习的习惯很重要,持之以恒才是正解。
3、编程思维没能提升一个台阶,局限在了编码,业务,没考虑过选型、扩展
4、身边没有好的架构师引导、培养。所处的圈子对程序员的成长影响巨大。
金九银十面试季,跳槽季,整理面试题已经成了我多年的习惯!在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。
附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
点,真正体系化!**
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
![](https://img-blog.csdnimg.cn/img_convert/1d37c07ac5180929336109dd94042018.jpeg)
最后我想说
为什么很多程序员做不了架构师?
1、良好健康的职业规划很重要,但大多数人都忽略了
2、学习的习惯很重要,持之以恒才是正解。
3、编程思维没能提升一个台阶,局限在了编码,业务,没考虑过选型、扩展
4、身边没有好的架构师引导、培养。所处的圈子对程序员的成长影响巨大。
金九银十面试季,跳槽季,整理面试题已经成了我多年的习惯!在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。
附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
[外链图片转存中…(img-NPDmwzeF-1712686262163)]
里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!