前言
最近公司要做自定义的聊天气泡,需要可以从服务器配置,并且有底图和边缘的动效
边缘的动效到没什么难度,直接四个角对齐就好了
但是从服务端配置的类似.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)