背景
Android开发中,降低本地静态资源,减小包大小。期间发现点9图不能放在远端然后加载直接用
理论
0.加载普通图片
要知道,加载普通网络图片很简单,使用工具库直接get
Glide.with(context)
.load(imgUrl)
.into(view)
1、 Android是如何加载点9图的?
当我们将点九图放在res目录下,Android不是直接去加载点九图的,而是在编译的时候将其转换成另一种格式,这种格式是将其四周的黑色像素保存在Bitmap类中的mNinePatchChunk的byte[]数组中,并去掉四周一像素的宽度;在使用的时候,当判断mNinePatchChunk不为空且为9patchchunk,则将其构造为NinePatchDrawable,否则构造为BitmapDrawable,最终设置给view。所以,最后打包后的点九图已经不是原来带黑线的点九图了。
2.前期处理
步骤(参考文章)
1.进入Android sdk的build-tools文件夹下
⚠️注意:可能会“zsh: command not found: aapt”,解决方案: 解决方案链接
2.操作点9图,(执行命令:aapt s -i [输入图片路径] -o [输出图片路径])
1.//单张图片转换,命令:aapt s -i 点9图路径 -o 转换后的图片输出路径
aapt s -i /Users/xxx/Desktop/test.9.png -o /Users/xxx/Desktop/test_out.png
2.//图片文件的批量转换,命令:aapt c -v -S 文件夹路径 -C 转换后的图片文件夹的输出路径
aapt c -v -S /Users/xxx/Desktop/files -C /Users/xxx/Desktop/filesout
3.示例
eg:aapt s -i /Users/xxx/Desktop/home_test_bottom.9.png -o /Users/xxx/Desktop/home_test_bottom_out.png
3.上云,得到链接
4.加载,过程看下一小节
解决过程(含代码)
//此方案无效,chunk一直是null
fun loadNinePatchImg(view: View?, imgUrl: String?) {
if (view == null || imgUrl.isNullOrBlank()) return
val context = view.context ?: return
try {
Glide.with(context)
.asBitmap()
.load(imgUrl)
.into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(
resource: Bitmap,
transition: Transition<in Bitmap>?
) {
val chunk = resource.ninePatchChunk
if (NinePatch.isNinePatchChunk(chunk)) {
imageView.background =
NinePatchDrawable(context.resources, resource, chunk, null, null)
} else {
imageView.background = BitmapDrawable(context.resources, resource)
}
}
override fun onLoadCleared(placeholder: Drawable?) {}
})
} catch (e: Exception) {
e.printStackTrace()
}
}
// 正常加载,但是padding太大
fun loadNinePatchImg(view: View?, imgUrl: String?) {
if (view == null || imgUrl.isNullOrBlank()) return
val context = view.context ?: return
try {
Glide.with(context)
.asFile()
.load(imgUrl)
.into(object : CustomTarget<File>() {
override fun onResourceReady(resource: File, transition: Transition<in File>?) {
FileInputStream(resource).use { inputStream ->
val bitmap = BitmapFactory.decodeStream(inputStream)
val chunk = bitmap?.ninePatchChunk
if (NinePatch.isNinePatchChunk(chunk)) {
view.background = NinePatchDrawable(context.resources, bitmap, chunk, NinePatchChunk.deserialize(chunk)?.mPaddings, null)
Log.e("zhhh", "NinePatchDrawable: " )
} else {
view.background = BitmapDrawable(context.resources, bitmap)
Log.e("zhhh", "BitmapDrawable: " )
}
}
}
override fun onLoadCleared(placeholder: Drawable?) {
}
})
} catch (e: Exception) {
e.printStackTrace()
}
}
class NinePatchChunk {
companion object {
private fun readIntArray(data: IntArray, buffer: ByteBuffer) {
for (i in data.indices) {
data[i] = buffer.getInt()
}
}
private fun checkDivCount(length: Int) {
if (length == 0 || (length and 0x01) != 0) {
throw RuntimeException("invalid nine-patch: $length")
}
}
fun deserialize(data: ByteArray): NinePatchChunk? {
val byteBuffer = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder())
if (byteBuffer.get() == 0.toByte()) return null // is not serialized
val chunk = NinePatchChunk()
chunk.mDivX = IntArray(byteBuffer.get().toInt())
chunk.mDivY = IntArray(byteBuffer.get().toInt())
chunk.mColor = IntArray(byteBuffer.get().toInt())
checkDivCount(chunk.mDivX.size)
checkDivCount(chunk.mDivY.size)
// skip 8 bytes
byteBuffer.int
byteBuffer.int
chunk.mPaddings.left = byteBuffer.int
chunk.mPaddings.right = byteBuffer.int
chunk.mPaddings.top = byteBuffer.int
chunk.mPaddings.bottom = byteBuffer.int
// skip 4 bytes
byteBuffer.int
readIntArray(chunk.mDivX, byteBuffer)
readIntArray(chunk.mDivY, byteBuffer)
readIntArray(chunk.mColor, byteBuffer)
return chunk
}
}
val mPaddings = Rect()
lateinit var mDivX: IntArray
lateinit var mDivY: IntArray
lateinit var mColor: IntArray
}
// 问题解决
fun loadNinePatchImg(view: View?, imgUrl: String?) {
if (view == null || imgUrl.isNullOrBlank()) return
val context = view.context ?: return
try {
Glide.with(view.context)
.asFile()
.load(imgUrl)
.into(object : CustomTarget<File>() {
override fun onResourceReady(resource: File, transition: Transition<in File>?) {
FileInputStream(resource).use { inputStream ->
val rect = Rect()
val bitmap = BitmapFactory.decodeStream(inputStream, rect, BitmapFactory.Options())
val chunk = bitmap?.ninePatchChunk
if (NinePatch.isNinePatchChunk(chunk)) {
view.background = NinePatchDrawable(context.resources, bitmap, chunk, rect, null)
} else {
view.background = BitmapDrawable(context.resources, bitmap)
}
}
}
override fun onLoadCleared(placeholder: Drawable?) {
}
})
} catch (e: Exception) {
e.printStackTrace()
}
}
// 加上了异常情况处理
fun loadNinePatchImgToView(view: View?, url: String?, success: (()-> Unit)? = null, failed: (()-> Unit)? = null) {
if (view == null || url.isNullOrBlank()) return
val context = view.context ?: return
try {
Glide.with(view.context)
.asFile()
.load(url)
.into(object : CustomTarget<File>() {
override fun onResourceReady(resource: File, transition: Transition<in File>?) {
try {
success?.invoke()
FileInputStream(resource).use { inputStream ->
val rect = Rect()
val bitmap = BitmapFactory.decodeStream(inputStream, rect, BitmapFactory.Options())
val chunk = bitmap?.ninePatchChunk
if (NinePatch.isNinePatchChunk(chunk)) {
view.background = NinePatchDrawable(context.resources, bitmap, chunk, rect, null)
} else {
view.background = BitmapDrawable(context.resources, bitmap)
}
}
}catch (e: Exception){
e.printStackTrace()
}
}
override fun onLoadCleared(placeholder: Drawable?) {
failed?.invoke()
}
})
} catch (e: Exception) {
e.printStackTrace()
}
}
注意事项
1.
如果没做任何处理,从服务端直接拉取点九的图设置到view时,图片并不会有拉伸拉伸效果,并且图片周围的黑线也会显示,不符合预期。从上文加载点九图的原理知,因为少了编译一步,所以才出现问题。知道错误的原因后,可以做几种方式处理:
- 点9图先进行转换后(转换工具由开发提供)再上传到云端,客户端再从服务端拉取到的就是编译处理后的点九图了
- 将原始点9图上传到一个转换平台,平台进行转换后再上传到服务器
- 客户端拿到原始的点9图后自行处理,根据加载的原理自行构造出NinePatchDrawable,这个过程放到客户端渲染的时候显然太耗时,不符合要求
2.
某些cdn因为省流量,或者其他原因,对图片进行压缩或者转码为webp格式,这样会导致最终拉取到的图片不是点9图。