“我报名参加金石计划1期应战——分割10万奖池,这是我的第3篇文章,点击查看活动概况”

你拿手机刷着刷着,忽然手滑点开一张图,

这图向上无限高,向下无限深,向左无限远,向右无限远,

这图是什么?

——是点9图。

对嘛!这才是从网络加载点9图的正确姿势!

我们好,我是来推翻你对点9图固有认知的星际码仔。

点9图几乎在每个Android工程中都或多或少地有用到,而切点9图也能够说是每个Android开发者必备的传统艺能了,但今日咱们要共享的主题估计各位平常比较少接触到,便是——从网络加载点9图

为了讲好这个主题,咱们会从点9图的基础知识出发,比较网络加载方式与惯例用法的差异,然后别离给出一个次优级和更优级的处理思路,能够依据你们当时项目的实践情况自由选取。

按例,先给出一张思维导图,方便复习:

对嘛!这才是从网络加载点9图的正确姿势!

点9图的基础知识

点9图,官方的正式名称为9-patch,是一种可拉伸的位图图画格局,因其必须以.9.png为扩展名进行保存而得名,一般被用作各类视图控件的布景

其典型的一个运用便是IM中的谈天气泡框,气泡框的宽高会随着咱们输入文本的长短而自适应拉伸,但气泡框资源自身并不会因拉伸而失真。

对嘛!这才是从网络加载点9图的正确姿势!

这么奇特的作用是怎样完成的呢?

答案是:四条黑线。

疏忽掉.9.png的扩展名,点9图的本质其实便是一张规范的PNG格局图片,而与其他一般PNG格局图片的不同之处在于,点9图在其图片的四周额定包括了1像素宽的黑色边框,用于界说图片的可拉伸的区域与可制造的区域,以完成依据视图内容主动调整图片大小的作用

可拉伸区域的界说

可拉伸区域由左侧及顶部一条或多条黑线来界说,左侧的黑色边框界说了纵向拉伸的区域,顶部的黑色边框界说了横向拉伸的区域,拉伸的作用是经过仿制区域内图片的像素来完成的。

对嘛!这才是从网络加载点9图的正确姿势!

能够看到,由于可拉伸区域选择的都是比较平坦的区域,而没有掩盖到四周的圆角,因而图片无论怎样纵向或横向拉伸,四周的圆角都不会因而而变形失真。

可制造区域的界说

可制造区域右侧及底部的各一条黑线来界说,称为内边距线。假如没有增加内边距线,视图内容将默许填满整个视图区域。

对嘛!这才是从网络加载点9图的正确姿势!

而假如增加了内边距线,则视图内容仅会在右侧及底部的黑线所界说的区域内显现,假如视图内容显现不下,则图片会拉伸至合适的尺度。

对嘛!这才是从网络加载点9图的正确姿势!

Glide能处理点9图吗

点九图的惯例用法,便是以.9.png为扩展名保存在项目的 res/drawable/ 目录下,并随着项目一同打包到 *.apk 文件中,然后跟其他一般的PNG格局图片相同正常运用即可。

但这种情况在改成了从网络加载点9图之后有所改变。

问题在于,即使强壮如Glide,关于从网络加载点9图的这种场景,也没有做很好的适配,以至于咱们加载完图片之后会发现…

完!全!没!有!拉!伸!效!果!

对嘛!这才是从网络加载点9图的正确姿势!

要了解这背后的原因,咱们需求把目光转移到一个原本在打包进程中常常被咱们忽视的角色——AAPT

AAPT是什么?

AAPT即Android Asset Packaging Tool,是用于构建*.apk文件的Android资源打包东西,默许存放在Android SDKbuild-tools目录下。

虽然咱们很少直接运用AAPT东西,但其却是*.apk文件打包流程中不可或缺的重要一环,具体可参照下面的*.apk文件具体构建流程图。

对嘛!这才是从网络加载点9图的正确姿势!

流程里,AAPT东西最重要的功用,便是获取并编译咱们运用的资源文件,例如AndroidManifest.xml清单文件和Activity的XML布局文件。 还有便是生成了一个R.java,以便咱们从 Java 代码中依据id索引到对应的资源

而惯例用法下的点9图之所以能正常工作,也离不开打包时,AAPT关于包括点9图在内的PNG格局图片的预处理

那么,AAPT的预处理具体都做了哪些事情呢?

AAPT对点九图做的预处理

首先,咱们要了解的是,在Android的国际里,存在着两种不同方式的点9图文件,别离是“源类型(source)”和“已编译类型(compiled)”。

源类型便是前面所说到的,运用了包括Draw 9-patch在内的点9图制造东西所创立的、四周带有1像素宽黑色边框的PNG图片。

对嘛!这才是从网络加载点9图的正确姿势!

已编译类型指的是,把之前界说好的点九图数据(可拉伸区域&可制造区域等)写入原先格局的辅佐数据块后,把四周的黑色边框抹除了的PNG图片。

对嘛!这才是从网络加载点9图的正确姿势!

这里稍微提一下PNG图片的文件格局。

对嘛!这才是从网络加载点9图的正确姿势!

在文件头之外,PNG图片运用了基于“块(chunk)”的存储结构,每个块负责传达有关图画的某些信息

块有要害块辅佐块两种类型,要害块包括了读取和烘托PNG文件所需的信息,必不可少。而辅佐数据块则是可选的,程序在遇到它不了解的辅佐块时,能够安全地疏忽它,这种规划能够保持与旧版本的兼容性

点九图数据所放入的,正是一个tag为“npTc”的辅佐数据块。

AAPT在打包进程中对点9图的预处理,其实便是将点9图从源类型转换为已编译类型的进程,也只有已编译类型的点9图才能被Android体系辨认并处理,然后到达依据视图内容主动调整图片大小的作用。

而直接从网络加载的点9图则缺少这个进程,咱们实践拿到的是没有经过AAPT预处理的源类型,Android体系就只会把它当一般的PNG格局图片相同处理,因而展现时会有残留在四周的黑色边框,而且当视图内容过大时,图片就会由于不合理拉伸而产生显着的失真。

对嘛!这才是从网络加载点9图的正确姿势!

明白了这一层的原理之后,咱们也就有了一个次优等级的处理思路,也即:

用AAPT指令行复原对点9图的预处理

AAPT同时也是一个指令行东西,其在打包进程中参加的多项工作都能够经过指令行来完成。

其中就包括对PNG格局图片的预处理。

所以,具体可操作的进程也很清晰了:

进程1:规划组产出源类型的点9图后,即运用AAPT东西转换为已编译类型

这样做还有一个优点便是,AAPT指令行东西会校验源类型点9图的标准,假如不合规就会报错并给出原因提示,这样就能够在生产端时就确保产出点9图的合规性,而不是比及展现的时分才发现有问题。

指令行如下:

 aapt s[ingleCrunch] [-v] -i inputfile -o outputfile

[]表示是可选的完好指令或参数。

进程2:交付到资源上传平台后,后端改由下发这种已编译类型的点9图

这个进程还需确保不会因流量紧缩而将图片转为Webp格局,或者造成“npTc”的辅佐数据块丢掉。

进程3:客户端拿到后还需一些额定的处理,以正常辨认和展现点9图

这里主要涉及到2个问题:

  1. 咱们怎样知道下发的资源是已编译类型的点9图?
  2. 咱们怎样告知体系以点9图的方式正确处理这张图?

这2个问题都能够从Android SDK源码中找到答案。

关于问题1,咱们能够从点9图的常见运用场景,即设为视图控件布景的API入手,从View#setBackground办法一路深入直至BitmapFactory#setDensityFromOptions办法,就能够看到:

    private static void setDensityFromOptions(Bitmap outputBitmap, Options opts) {
            ...
            byte[] np = outputBitmap.getNinePatchChunk();
            final boolean isNinePatch = np != null && NinePatch.isNinePatchChunk(np);
           ...
    }

Bitmap#getNinePatchChunk办法返回的是一个byte数组类型的数据,从办法名就能够看出其正是关于点九图标准的辅佐块数据

    public byte[] getNinePatchChunk() {
        return mNinePatchChunk;
    }

NinePatch#isNinePatchChunk办法是一个Native函数,咱们比及后面深入点九图Native层结构体时再展开讲:

    public native static boolean isNinePatchChunk(byte[] chunk);

而关于问题2,咱们能够经过查找对Bitmap#getNinePatchChunk办法的引用,在Drawable#createFromResourceStream办法中找到一个参阅比如:

    public static Drawable createFromResourceStream(@Nullable Resources res,
            @Nullable TypedValue value, @Nullable InputStream is, @Nullable String srcName,
            @Nullable BitmapFactory.Options opts) {
        ...
        Rect pad = new Rect();
        ...
        Bitmap  bm = BitmapFactory.decodeResourceStream(res, value, is, pad, opts);
        if (bm != null) {
            byte[] np = bm.getNinePatchChunk();
            if (np == null || !NinePatch.isNinePatchChunk(np)) {
                np = null;
                pad = null;
            }
            final Rect opticalInsets = new Rect();
            bm.getOpticalInsets(opticalInsets);
            return drawableFromBitmap(res, bm, np, pad, opticalInsets, srcName);
        }
        return null;
    }
    private static Drawable drawableFromBitmap(Resources res, Bitmap bm, byte[] np,
            Rect pad, Rect layoutBounds, String srcName) {
        if (np != null) {
            return new NinePatchDrawable(res, bm, np, pad, layoutBounds, srcName);
        }
        return new BitmapDrawable(res, bm);
    }

能够看到,它是经过在判别NinePatchChunk数据不为空后,构建了一个NinePatchDrawable来告知体系以点9图的方式正确处理这张图的。

所以咱们能够得出结论,客户端要做的额定处理,便是在拿到已编译类型的点9图并构建为Bitmap后:

  1. 先调用Bitmap#getNinePatchChunk办法尝试获取点9图数据

  2. 再经过NinePatch#isNinePatchChunk办法判别是不是点9图数据。

  3. 假如是点9图数据,则运用这个点9图数据构建一个NinePatchDrawable

  4. 假如不是,则构建一个BitmapDrawable

示例代码如下:

        Glide.with(context).asBitmap().load(url)
            .into(object : CustomTarget<Bitmap>(){
                override fun onResourceReady(bitmap: Bitmap, transition: Transition<in Bitmap>?) {
                    try {
                        val chunk = bitmap.ninePatchChunk
                        val drawable = if (NinePatch.isNinePatchChunk(chunk)) {
                            NinePatchDrawable(context.resources, bitmap, chunk, Rect(), null)
                        } else {
                            BitmapDrawable(context.resources, bitmap);
                        }
                        view.background = drawable;
                    } catch (e: Exception) {
                        e.printStackTrace();
                    }
                }
                override fun onLoadCleared(placeholder: Drawable?) {
                }
            })

这样就满足了吗?并没有。计划自身虽然可行,但让一向习气可视化界面操作的规划组同事执行指令行,实在是有点太尴尬他们了,而且每次产出资源后都要用AAPT东西处理一遍,也确实有点麻烦。

话说回来,指令行东西的底层必定仍是依靠代码来完成的,那有没有可能在客户端侧完成一套与AAPT东西相同的逻辑呢?这就引出了咱们一个更次优等级的处理思路,也即:

在客户端侧复原对点9图的预处理

透过上一个计划咱们能够了解到,最要害的当地仍是那个byte数组类型的点九图数据块(NineChunk),假如咱们能知道这个数据块里面实践包括什么内容,就有时机在在客户端侧构造出一份相似的数据。

上一个计划中说到的NinePatch#isNinePatchChunk办法便是咱们的突破点。

接下来,就让咱们进入Native层查看isNinePatchChunk办法的源码完成吧:

    static jboolean isNinePatchChunk(JNIEnv* env, jobject, jbyteArray obj) {
        if (NULL == obj) {
            return JNI_FALSE;
        }
        if (env->GetArrayLength(obj) < (int)sizeof(Res_png_9patch)) {
            return JNI_FALSE;
        }
        const jbyte* array = env->GetByteArrayElements(obj, 0);
        if (array != NULL) {
            const Res_png_9patch* chunk = reinterpret_cast<const Res_png_9patch*>(array);
            int8_t wasDeserialized = chunk->wasDeserialized;
            env->ReleaseByteArrayElements(obj, const_cast<jbyte*>(array), JNI_ABORT);
            return (wasDeserialized != -1) ? JNI_TRUE : JNI_FALSE;
        }
        return JNI_FALSE;
    }

能够看到,在isNinePatchChunk办法内部实践是将传入的byte数组类型的点9图数据转为一个Res_png_9patch类型的结构体,再经过一个wasDeserialized的结构变量来判别是不是点9图数据的。

这个Res_png_9patch类型的结构体内部是这样的:

 * 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.
 * /
struct alignas(uintptr_t) Res_png_9patch
{
    Res_png_9patch() : wasDeserialized(false), xDivsOffset(0),
                       yDivsOffset(0), colorsOffset(0) { }
    int8_t wasDeserialized;
    uint8_t numXDivs;
    uint8_t numYDivs;
    uint8_t numColors;
    // The offset (from the start of this structure) to the xDivs & yDivs
    // array for this 9patch. To get a pointer to this array, call
    // getXDivs or getYDivs. Note that the serialized form for 9patches places
    // the xDivs, yDivs and colors arrays immediately after the location
    // of the Res_png_9patch struct.
    uint32_t xDivsOffset;
    uint32_t yDivsOffset;
    int32_t paddingLeft, paddingRight;
    int32_t paddingTop, paddingBottom;
    enum {
        // The 9 patch segment is not a solid color.
        NO_COLOR = 0x00000001,
        // The 9 patch segment is completely transparent.
        TRANSPARENT_COLOR = 0x00000000
    };
    // The offset (from the start of this structure) to the colors array
    // for this 9patch.
    uint32_t colorsOffset;
    ...
    inline int32_t* getXDivs() const {
        return reinterpret_cast<int32_t*>(reinterpret_cast<uintptr_t>(this) + xDivsOffset);
    }
    inline int32_t* getYDivs() const {
        return reinterpret_cast<int32_t*>(reinterpret_cast<uintptr_t>(this) + yDivsOffset);
    }
    inline uint32_t* getColors() const {
        return reinterpret_cast<uint32_t*>(reinterpret_cast<uintptr_t>(this) + colorsOffset);
    }
} __attribute__((packed));

很显着,这个结构体便是用来存储点9图标准数据的,咱们能够依据该结构体的源码和注释梳理出每个变量的意义:

对嘛!这才是从网络加载点9图的正确姿势!

依据该结构体注释中的描绘,这个结构体是用于指定如何将图画分割成多个部分以进行缩放的,其中:

  • Sx标签符号的是拉伸区域(stretchable),Fx标签符号的是固定区域(fixed)
  • mDivX描绘了所有S区域水平方向的起始方位和完毕方位
  • mDivY描绘了所有S区域笔直方向的起始方位和完毕方位
  • mColor描绘了每个小区域的颜色

以该结构体注释中的比如来说,mDivX,mDivY,mColor别离如下:

 *      F0   S0    F1     S1
 *   +-----+----+------+-------+
 * S2|  0  |  1 |  2   |   3   |
 *   +-----+----+------+-------+
 *   |     |    |      |       |
 *   |     |    |      |       |
 * F2|  4  |  5 |  6   |   7   |
 *   |     |    |      |       |
 *   |     |    |      |       |
 *   +-----+----+------+-------+
 * S3|  8  |  9 |  10  |   11  |
 *   +-----+----+------+-------+
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]]

我画了一张示意图,应该会更方便了解一点:

对嘛!这才是从网络加载点9图的正确姿势!

这几个结构体变量所描绘的,不正是咱们源类型的点9图四周所对应的那些黑色边框的方位吗?

那么,现在咱们只需求在Java层界说一个与Res_png_9patch结构体的数据结构一模相同的类,并在填充要害的变量数据后序列化为byte数组类型的数据,就能够作为NinePatchDrawable构造函数的参数了

怎样做呢?这部分有点复杂,Github上已经有一个大神开源出了计划,能够参阅下其源码完成:github.com/Anatolii/Ni…

这里只给出运用层的示例代码:

     Glide.with(context).asBitmap().load(url)
            .into(object : CustomTarget<Bitmap>(){
                override fun onResourceReady(bitmap: Bitmap, transition: Transition<in Bitmap>?) {
                    try {
                        val drawable = NinePatchChunk.create9PatchDrawable(textBackground.context, resource, null)
                        view.background = drawable;
                    } catch (e: Exception) {
                        e.printStackTrace();
                    }
                }
                override fun onLoadCleared(placeholder: Drawable?) {
                }
            })

NinePatchChunk类即为前面说的在Java层界说的类,并供给了几个静态办法用于创立NinePatchDrawable,其在内部会去检测传入的Bitmap实例属于哪种类型:

	public static BitmapType determineBitmapType(Bitmap bitmap) {
		if (bitmap == null) return NULL;
		byte[] ninePatchChunk = bitmap.getNinePatchChunk();
		if (ninePatchChunk != null && android.graphics.NinePatch.isNinePatchChunk(ninePatchChunk))
			return NinePatch;
		if (NinePatchChunk.isRawNinePatchBitmap(bitmap))
			return RawNinePatch;
		return PlainImage;
	}

NinePatch即为已编译类型的点9图,RawNinePatch即为源类型的点9图,RawNinePatch是经过PNG图片4个角像素是否为通明且是否包括黑色边框判其他。

	public static boolean isRawNinePatchBitmap(Bitmap bitmap) {
		if (bitmap == null) return false;
		if (bitmap.getWidth() < 3 || bitmap.getHeight() < 3)
			return false;
		if (!isCornerPixelsAreTrasperent(bitmap))
			return false;
		if (!hasNinePatchBorder(bitmap))
			return false;
		return true;
	}

这样,咱们就完成了网络加载点9图的功用了,关于源类型和已编译类型的点9图都能正确展现。

好了,这个便是今日要共享的内容。最终留给我们一个问题,你觉得.9.png的扩展名关于从网络加载点九图有影响吗?

少侠,请留步!若本文对你有所协助或启示,还请:

  1. 点赞,让更多的人能看到!
  2. 保藏⭐️,好文值得反复品味!
  3. 重视➕,不错过每一次更文!

===> 技能号:「星际码仔」

你的支持是我继续创造的动力,感谢!

参阅

  • NinePatch

  • Android动态布局入门及NinePatchChunk解密

  • Android点九图总结以及在谈天气泡中的运用