[踩坑记录]Android加载网络点9图(含代码)

1,101 阅读4分钟

背景

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图。