前言

最近公司要做自定义的聊天气泡,需要可以从服务器配置,并且有底图和边缘的动效

边缘的动效到没什么难度,直接四个角对齐就好了

但是从服务端配置的类似.9图可拉伸的效果就有点麻烦了

所以下文尝试解决动态实现.9图

思路

首先做安卓开发的都知道.9图的特性:四个边有四条1像素的多余像素,用来表示可拉伸区域(左,上)和可展示内容的区域(右,下)(其实就是加了padding)

最开始想着将一个服务端png转成.9特性的png,后来查了下发现项目内的.9图是会经过编译变成其他东西,所以此条pass

然后就是可以自行绘制,实现Drawable将某一个像素数据在超过图片原始大小后重复绘制,但是比较麻烦

于是获取了一个.9图的Drawable对象,发现其是NinePatchDrawable对象,系统已经实现好了,为啥不用对吧

实现

看了下构造:

public NinePatchDrawable(Resources res, Bitmap bitmap, byte[] chunk, Rect padding, String srcName)

发现除了chunk其他的都很好理解,于是去看了看源码chunk是干嘛的,最后跟到了一个native方法里......

/**
     * Validates the 9-patch chunk and throws an exception if the chunk is invalid.
     * If validation is successful, this method returns a native Res_png_9patch*
     * object used by the renderers.
     */
    private static native long validateNinePatchChunk(byte[] chunk);


static jlong validateNinePatchChunk(JNIEnv* env, jobject, jbyteArray obj) {
        size_t chunkSize = env->GetArrayLength(obj);
        if (chunkSize < (int) (sizeof(Res_png_9patch))) {
            jniThrowRuntimeException(env, "Array too small for chunk.");
            return NULL;
        }

        int8_t* storage = new int8_t[chunkSize];
        // This call copies the content of the jbyteArray
        env->GetByteArrayRegion(obj, 0, chunkSize, reinterpret_cast<jbyte*>(storage));
        // Deserialize in place, return the array we just allocated
        return reinterpret_cast<jlong>(Res_png_9patch::deserialize(storage));
    }

ps:由于不是很懂c/c++,所以全靠瞎猜2333,有大佬能看懂的请指出错误!!!

可以看到这个函数是先检查了字节数组的长度,然后创建并copy了一个长度和数据相同的字节数组,并调用Res_png_9patch::deserialize方法将数组转成了long,按理说我们可以通过该方法找到字节数组的规则,继续往下看

Res_png_9patch* Res_png_9patch::deserialize(void* inData)
{

    Res_png_9patch* patch = reinterpret_cast<Res_png_9patch*>(inData);
    patch->wasDeserialized = true;
    fill9patchOffsets(patch);

    return patch;
}

看下fill9patchOffsets方法

static void fill9patchOffsets(Res_png_9patch* patch) {
    patch->xDivsOffset = sizeof(Res_png_9patch);
    patch->yDivsOffset = patch->xDivsOffset + (patch->numXDivs * sizeof(int32_t));
    patch->colorsOffset = patch->yDivsOffset + (patch->numYDivs * sizeof(int32_t));
}

看这个代码貌似就是设置了几条数据,那在看看Res_png_9patch的类型

/** ********************************************************************
 *  PNG Extensions
 *
 *  New private chunks that may be placed in PNG images.
 *
 *********************************************************************** */

/**
 * 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.
 *
 * The relative sizes of the stretchy segments indicates the relative
 * amount of stretchiness of the regions bordered by the segments.  For
 * example, regions 3, 7 and 11 above will take up more horizontal space
 * than regions 1, 5 and 9 since the horizontal segment associated with
 * the first set of regions is larger than the other set of regions.  The
 * ratios of the amount of horizontal (or vertical) space taken by any
 * two stretchable slices is exactly the ratio of their corresponding
 * segment lengths.
 *
 * xDivs and yDivs are arrays of horizontal and vertical pixel
 * indices.  The first pair of Divs (in either array) indicate the
 * starting and ending points of the first stretchable segment in that
 * axis. The next pair specifies the next stretchable segment, etc. So
 * in the above example xDiv[0] and xDiv[1] specify the horizontal
 * coordinates for the regions labeled 1, 5 and 9.  xDiv[2] and
 * xDiv[3] specify the coordinates for regions 3, 7 and 11. Note that
 * the leftmost slices always start at x=0 and the rightmost slices
 * always end at the end of the image. So, for example, the regions 0,
 * 4 and 8 (which are fixed along the X axis) start at x value 0 and
 * go to xDiv[0] and slices 2, 6 and 10 start at xDiv[1] and end at
 * xDiv[2].
 *
 * The colors array contains hints for each of the regions. They are
 * ordered according left-to-right and top-to-bottom as indicated above.
 * For each segment that is a solid color the array entry will contain
 * that color value; otherwise it will contain NO_COLOR. Segments that
 * are completely transparent will always have the value TRANSPARENT_COLOR.
 *
 * The PNG chunk type is "npTc".
 */
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;

    // Convert data from device representation to PNG file representation.
    void deviceToFile();
    // Convert data from PNG file representation to device representation.
    void fileToDevice();

    // Serialize/Marshall the patch data into a newly malloc-ed block.
    static void* serialize(const Res_png_9patch& patchHeader, const int32_t* xDivs,
                           const int32_t* yDivs, const uint32_t* colors);
    // Serialize/Marshall the patch data into |outData|.
    static void serialize(const Res_png_9patch& patchHeader, const int32_t* xDivs,
                           const int32_t* yDivs, const uint32_t* colors, void* outData);
    // Deserialize/Unmarshall the patch data
    static Res_png_9patch* deserialize(void* data);
    // Compute the size of the serialized data structure
    size_t serializedSize() const;

    // These tell where the next section of a patch starts.
    // For example, the first patch includes the pixels from
    // 0 to xDivs[0]-1 and the second patch includes the pixels
    // from xDivs[0] to xDivs[1]-1.
    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));

根据注释来瞎猜,Res_png_9patch相当于一个简单的数据结构,xDivs和yDivs字段保存了可拉伸区域的x和y轴像素的起始位置,color保存的不知道是什么(不过也不重要),也就是我们只要知道数组的哪里是保存的x和y的起始位置,就可以设置气泡的拉伸了

于是我们找到一个png转成.9图并拿到chunk数组:

bitmap.ninePatchChunk//[1,2,2,9,32,0,0,0,40,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,48,0,0,0,52 宽开始,0,0,0,53 宽结束,0,0,0,41 高开始,0,0,0,42 高结束,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,-1,70,-88,-77,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0]

发现了这个数组我们关心的数据其实就在中间我标记的位置,所以我们可以创建一个这样的数组并修改中间的值来替换拉伸位置(只拉伸中间一个像素)

val bs = byteArrayOf(1, 2, 2, 9, 32, 0, 0, 0, 40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, -1, 70, -88, -77, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
            var byteArray = ByteUtils.int2Bytes(newBitmap.width / 2)
            bs[29] = byteArray[0]
            bs[30] = byteArray[1]
            bs[31] = byteArray[2]
            bs[32] = byteArray[3]
            byteArray = ByteUtils.int2Bytes(newBitmap.width / 2 + 1)
            bs[23] = byteArray[0]
            bs[34] = byteArray[1]
            bs[35] = byteArray[2]
            bs[36] = byteArray[3]
            byteArray = ByteUtils.int2Bytes(newBitmap.height / 2)
            bs[37] = byteArray[0]
            bs[38] = byteArray[1]
            bs[39] = byteArray[2]
            bs[40] = byteArray[3]
            byteArray = ByteUtils.int2Bytes(newBitmap.width / 2 + 1)
            bs[41] = byteArray[0]
            bs[42] = byteArray[1]
            bs[43] = byteArray[2]
            bs[44] = byteArray[3]
public class ByteUtils {

    public static int bytes2Int(byte[] bytes) {
        int int1 = (bytes[0] & 0xff) << 24;
        int int2 = (bytes[1] & 0xff) << 16;
        int int3 = (bytes[2] & 0xff) << 8;
        int int4 = bytes[3] & 0xff;

        return int1 | int2 | int3 | int4;
    }

    public static byte[] int2Bytes(int integer) {
        byte[] bytes = new byte[4];
        bytes[0] = (byte) (integer >> 24);
        bytes[1] = (byte) (integer >> 16);
        bytes[2] = (byte) (integer >> 8);
        bytes[3] = (byte) integer;

        return bytes;
    }
}

这样我们通过工具类将x范围和y范围将int转为字节(int4个字节),然后赋值到相应位置即可实现效果(其他数据没有修改,目前没有发现问题,如果发现问题请留言谢谢)

这样我们就拿到了chunk数组了,然后就可以调用NinePatchDrawable的构造来动态创建出.9图了

NinePatchDrawable(context.resources, newBitmap, bs, Rect(), null)