Explanation of Android Point-9 Diagram Mechanism and Its Application in Chat Bubbles

Keywords: Android Mobile Java iOS

A Brief Introduction to Point Nine Diagrams

In order to use the same image as the background of different quantities of text, Android designed an image format ".9.png" which can specify the area stretching. This image format is point nine.

Note: This image format can only be used in Android development. In ios development, you can specify a point in the code for stretching, but not in Android, so in Android to achieve this effect, you can only use point nine diagrams (the following will crack your face, in fact, it is possible, but very few people use it, compatibility does not know how. Click jump)

The essence of point nine graph

The essence of the point nine graph is to add 1 PX pixels around the image and mark it with pure black ( FF000000) lines. The others are no different from the original image. You can refer to the following pictures:

Mark position Meaning
Left black spot Longitudinal Tensile Zone
Upper black spot Transverse Tensile Zone
Right black line Longitudinal display area
Lower black line Horizontal display area

Application of Point Nine Graph in Android

There are three main applications of Point Nine Graph in Android

  1. Place it directly in the drawable or mipmap directory in the res directory
  2. Put it in the assert directory
  3. Download from the Internet

The first is our most commonly used method, which calls setBackgroundResource or setImageResource directly, so that the image and can be stretched automatically.

For the second or third way, if we load. 9.png directly, you will find that the background of the picture or picture can not be stretched at all. Nanny, why is that? Next, listen to the old lady slowly.

Android does not use point nine diagrams directly, but converts them to another format at compilation time. This format saves the black pixels around it into byte [] named mNinePatchChunk in the Bitmap class, and erases the width of the pixel around it; then, when using this format, if Bitmap's If mNinePatch Chunk is not empty and 9patch chunk, it is constructed as NinePatch Drawable, otherwise it will be constructed as Bitmap Drawable and finally set to view.

Therefore, in Android, if we want to dynamically use the dot-nine graph downloaded from the network, we usually need to go through the following steps:

  1. Using aapt tool in sdk directory to convert point nine to png
  2. When parsing a picture, determine whether it contains NinePatch Chunk, or, in some cases, NinePatch Drawable.
public static void setNineImagePatch(View view, File file, String url) {
    if (file.exists()) {
        Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
        byte[] chunk = bitmap.getNinePatchChunk();
        if (NinePatch.isNinePatchChunk(chunk)) {
            NinePatchDrawable patchy = new NinePatchDrawable(view.getResources(), bitmap, chunk, new Rect(), null);
            view.setBackground(patchy);
        }
        
    }
}

Point 9 Map Upload Server Flow

aapt conversion command

Single Picture File Conversion

./aapt s -i xxx.9.png -o xxx.png

Batch conversion

# Batch conversion
./aapt c -S inputDir -C outputDir
# inputDir is the original. 9 figure folder and outputDir is the output folder.

Successful execution examples

jundeMacBook-Pro: Phase I Bubble Junxu$. / AAPT c-S/Users/junxu/Desktop/Phase I Bubble/Bubble Demand Finishing-C/Users/junxu/Desktop/Phase I Bubble/output 
Crunching PNG Files in source dir: /Users/junxu/Desktop/Phase I Bubble/Bubble Demand Arrangement
 To destination dir: /Users/junxu/Desktop / Phase I bubble / output

Be careful:

If it is not a standard point-nine diagram, there will be an error in the conversion process. At this time, please redesign and provide a new point-nine diagram.

Problems encountered in actual development

Adaptation of Small Screen Mobile Phone

At first, we cut the image twice, so that the bubble height of the cell phone will be too high on the small screen mobile phone.

Reason analysis:

The essence of this phenomenon is that the height of the point nine picture is higher than that of the single line text message.

Solution 1 (not desirable for the time being):

  1. I tried to compress Point Nine, but eventually some of the mobile phones showed confusion, I do not know if the method of compressing Point Nine is wrong.

Solution 2

For low-resolution mobile phones and high-resolution mobile phones, we try to send different picture url s. The best solution is to use double image when density < 2, and double image when density >= 2.

Solution 3

Some people may wonder why we should adopt the solution of double graph and double graph. Let UI designers directly give a set of pictures, the height of the nine-point pictures is moderate to solve the problem. Yes, we think so, but they say that they are not very good at cutting some decorative patterns if the height is reduced. For example, the stars in the picture below.

Summary

In the final analysis, scheme 2 and scheme 3 are actually a compromise solution. If we can directly zoom and zoom in point 9, it will be perfectly solved. And the drawable or mipmap in the res directory in Android can really do it. Looking at the relevant code, there is no good solution at present. If you have a good solution, please leave a message to exchange.

padding of Point Nine Graph Fails on Some Mobile Phones

This is part of the Android mobile phone bug, the solution is as follows: https://stackoverflow.com/que...

public class NinePatchChunk {

    private static final String TAG = "NinePatchChunk";

    public final Rect mPaddings = new Rect();

    public int mDivX[];
    public int mDivY[];
    public int mColor[];

    private static float density = IMO.getInstance().getResources().getDisplayMetrics().density;

    private static void readIntArray(final int[] data, final ByteBuffer buffer) {
        for (int i = 0, n = data.length; i < n; ++i)
            data[i] = buffer.getInt();
    }

    private static void checkDivCount(final int length) {
        if (length == 0 || (length & 0x01) != 0)
            throw new IllegalStateException("invalid nine-patch: " + length);
    }

    public static Rect getPaddingRect(final byte[] data) {
        NinePatchChunk deserialize = deserialize(data);
        if (deserialize == null) {
            return new Rect();
        }
    }

    public static NinePatchChunk deserialize(final byte[] data) {
        final ByteBuffer byteBuffer =
                ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());

        if (byteBuffer.get() == 0) {
            return null; // is not serialized
        }

        final NinePatchChunk chunk = new NinePatchChunk();
        chunk.mDivX = new int[byteBuffer.get()];
        chunk.mDivY = new int[byteBuffer.get()];
        chunk.mColor = new int[byteBuffer.get()];

        try {
            checkDivCount(chunk.mDivX.length);
            checkDivCount(chunk.mDivY.length);
        } catch (Exception e) {
            return null;
        }


        // skip 8 bytes
        byteBuffer.getInt();
        byteBuffer.getInt();


        chunk.mPaddings.left = byteBuffer.getInt();
        chunk.mPaddings.right = byteBuffer.getInt();
        chunk.mPaddings.top = byteBuffer.getInt();
        chunk.mPaddings.bottom = byteBuffer.getInt();


        // skip 4 bytes
        byteBuffer.getInt();

        readIntArray(chunk.mDivX, byteBuffer);
        readIntArray(chunk.mDivY, byteBuffer);
        readIntArray(chunk.mColor, byteBuffer);

        return chunk;
    }
}

NinePatchDrawable patchy = new NinePatchDrawable(view.getResources(), bitmap, chunk, NinePatchChunk.getPaddingRect(chunk), null);
view.setBackground(patchy);

Dynamic download point 9 will cause chat bubbles to flicker

  1. Here we take the plan of pre-downloading (pre-downloading 10)
  2. Chat bubbles use memory caching and disk caching to ensure that RecyclerView does not flicker when sliding quickly

Understanding Point Nine Graph

The following is a reference to Tencent Music Introduction to Android Dynamic Layout and NinePatch Chunk Decryption

Looking back at the third parameter bitmap.getNinePatchChunk() of NinePatchDrawable's construction method, the author assumes that the aapt command actually adds information of NinePatchChunk to the bitmap picture. So, if we can construct this thing ourselves, can we make any picture elevate in the way we want it to? ?

But after looking up a bunch of official documents, it seems that there is no way to get the byte [] type chunk parameter.

Since it is impossible to know how this chunk is generated, can we reverse the NinePatch Chunk generation method from an analytical point of view?

Next, we need to start with the source code.

NinePatchChunk.java

public static NinePatchChunk deserialize(byte[] data) {
    ByteBuffer byteBuffer =
            ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
    byte wasSerialized = byteBuffer.get();
    if (wasSerialized == 0) return null;
    NinePatchChunk chunk = new NinePatchChunk();
    chunk.mDivX = new int[byteBuffer.get()];
    chunk.mDivY = new int[byteBuffer.get()];
    chunk.mColor = new int[byteBuffer.get()];
    checkDivCount(chunk.mDivX.length);
    checkDivCount(chunk.mDivY.length);
    // skip 8 bytes
    byteBuffer.getInt();
    byteBuffer.getInt();
    chunk.mPaddings.left = byteBuffer.getInt();
    chunk.mPaddings.right = byteBuffer.getInt();
    chunk.mPaddings.top = byteBuffer.getInt();
    chunk.mPaddings.bottom = byteBuffer.getInt();
    // skip 4 bytes
    byteBuffer.getInt();
    readIntArray(chunk.mDivX, byteBuffer);
    readIntArray(chunk.mDivY, byteBuffer);
    readIntArray(chunk.mColor, byteBuffer);
    return chunk;
}

Actually, by parsing the source code of byte[] chunk from this part, we can deduce the structure of byte[] chunk. As follows,

According to the conjecture in the figure above and the understanding of. 9.png, intuition feels that mDivX,mDivY,mColor are the three most critical arrays, but what is the specific, we need to continue to look at the source code.

ResourceTypes.h

/**
 * 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.
 * /

As noted in the source code, this NinePatch Chunk divides the image from the X and y axes into several regions, the F region represents fixation, and the S region represents stretching. mDivX,mDivY describes the starting position of all S regions, while mColor describes the color of each Segment, usually assigning NO_COLOR = 0x00000001 as defined in the source code. For example, in source annotations, mDivX,mDivY,mColor are as follows:

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]]

For the mColor array, the length is equal to the number of areas divided, which is used to describe the color of each area. If we only describe the stretching mode of a bitmap, we do not need the color, that is, NO_COLOR = 0x00000001 in the source code.

Having said all this, let's illustrate how to construct an inePatch Drawable that stretches according to the center point with a simple example.

Bitmap bitmap = BitmapFactory.decodeFile(filepath);
int[] xRegions = new int[]{bitmap.getWidth() / 2, bitmap.getWidth() / 2 + 1};
int[] yRegions = new int[]{bitmap.getWidth() / 2, bitmap.getWidth() / 2 + 1};
int NO_COLOR = 0x00000001;
int colorSize = 9;
int bufferSize = xRegions.length * 4 + yRegions.length * 4 + colorSize * 4 + 32;

ByteBuffer byteBuffer = ByteBuffer.allocate(bufferSize).order(ByteOrder.nativeOrder());
// The first byte, or not equal to 0
byteBuffer.put((byte) 1);

//mDivX length
byteBuffer.put((byte) 2);
//mDivY length
byteBuffer.put((byte) 2);
//mColors length
byteBuffer.put((byte) colorSize);

//skip
byteBuffer.putInt(0);
byteBuffer.putInt(0);

//padding is set to 0
byteBuffer.putInt(0);
byteBuffer.putInt(0);
byteBuffer.putInt(0);
byteBuffer.putInt(0);

//skip
byteBuffer.putInt(0);

// mDivX
byteBuffer.putInt(xRegions[0]);
byteBuffer.putInt(xRegions[1]);

// mDivY
byteBuffer.putInt(yRegions[0]);
byteBuffer.putInt(yRegions[1]);

// mColors
for (int i = 0; i < colorSize; i++) {
    byteBuffer.putInt(NO_COLOR);
}

return byteBuffer.array();

create-a-ninepatch-ninepatchdrawable-in-runtime

Also found on the stack overflow class, you can dynamically create a point nine graph, and stretch the picture, slap face, just started to say that android can not dynamically specify the image stretch area like ios.

public class NinePatchBuilder {
    int width, height;
    Bitmap bitmap;
    Resources resources;
    private ArrayList<Integer> xRegions = new ArrayList<Integer>();
    private ArrayList<Integer> yRegions = new ArrayList<Integer>();

    public NinePatchBuilder(Resources resources, Bitmap bitmap) {
        width = bitmap.getWidth();
        height = bitmap.getHeight();
        this.bitmap = bitmap;
        this.resources = resources;
    }

    public NinePatchBuilder(int width, int height) {
        this.width = width;
        this.height = height;
    }

    public NinePatchBuilder addXRegion(int x, int width) {
        xRegions.add(x);
        xRegions.add(x + width);
        return this;
    }

    public NinePatchBuilder addXRegionPoints(int x1, int x2) {
        xRegions.add(x1);
        xRegions.add(x2);
        return this;
    }

    public NinePatchBuilder addXRegion(float xPercent, float widthPercent) {
        int xtmp = (int) (xPercent * this.width);
        xRegions.add(xtmp);
        xRegions.add(xtmp + (int) (widthPercent * this.width));
        return this;
    }

    public NinePatchBuilder addXRegionPoints(float x1Percent, float x2Percent) {
        xRegions.add((int) (x1Percent * this.width));
        xRegions.add((int) (x2Percent * this.width));
        return this;
    }

    public NinePatchBuilder addXCenteredRegion(int width) {
        int x = (int) ((this.width - width) / 2);
        xRegions.add(x);
        xRegions.add(x + width);
        return this;
    }

    public NinePatchBuilder addXCenteredRegion(float widthPercent) {
        int width = (int) (widthPercent * this.width);
        int x = (int) ((this.width - width) / 2);
        xRegions.add(x);
        xRegions.add(x + width);
        return this;
    }

    public NinePatchBuilder addYRegion(int y, int height) {
        yRegions.add(y);
        yRegions.add(y + height);
        return this;
    }

    public NinePatchBuilder addYRegionPoints(int y1, int y2) {
        yRegions.add(y1);
        yRegions.add(y2);
        return this;
    }

    public NinePatchBuilder addYRegion(float yPercent, float heightPercent) {
        int ytmp = (int) (yPercent * this.height);
        yRegions.add(ytmp);
        yRegions.add(ytmp + (int) (heightPercent * this.height));
        return this;
    }

    public NinePatchBuilder addYRegionPoints(float y1Percent, float y2Percent) {
        yRegions.add((int) (y1Percent * this.height));
        yRegions.add((int) (y2Percent * this.height));
        return this;
    }

    public NinePatchBuilder addYCenteredRegion(int height) {
        int y = (int) ((this.height - height) / 2);
        yRegions.add(y);
        yRegions.add(y + height);
        return this;
    }

    public NinePatchBuilder addYCenteredRegion(float heightPercent) {
        int height = (int) (heightPercent * this.height);
        int y = (int) ((this.height - height) / 2);
        yRegions.add(y);
        yRegions.add(y + height);
        return this;
    }

    public byte[] buildChunk() {
        if (xRegions.size() == 0) {
            xRegions.add(0);
            xRegions.add(width);
        }
        if (yRegions.size() == 0) {
            yRegions.add(0);
            yRegions.add(height);
        }
     
        int NO_COLOR = 1;//0x00000001;
        int COLOR_SIZE = 9;//could change, may be 2 or 6 or 15 - but has no effect on output
        int arraySize = 1 + 2 + 4 + 1 + xRegions.size() + yRegions.size() + COLOR_SIZE;
        ByteBuffer byteBuffer = ByteBuffer.allocate(arraySize * 4).order(ByteOrder.nativeOrder());
        byteBuffer.put((byte) 1);//was translated
        byteBuffer.put((byte) xRegions.size());//divisions x
        byteBuffer.put((byte) yRegions.size());//divisions y
        byteBuffer.put((byte) COLOR_SIZE);//color size

        //skip
        byteBuffer.putInt(0);
        byteBuffer.putInt(0);

        //padding -- always 0 -- left right top bottom
        byteBuffer.putInt(0);
        byteBuffer.putInt(0);
        byteBuffer.putInt(0);
        byteBuffer.putInt(0);

        //skip
        byteBuffer.putInt(0);

        for (int rx : xRegions)
            byteBuffer.putInt(rx); // regions left right left right ...
        for (int ry : yRegions)
            byteBuffer.putInt(ry);// regions top bottom top bottom ...

        for (int i = 0; i < COLOR_SIZE; i++)
            byteBuffer.putInt(NO_COLOR);

        return byteBuffer.array();
    }

    public NinePatch buildNinePatch() {
        byte[] chunk = buildChunk();
        if (bitmap != null)
            return new NinePatch(bitmap, chunk, null);
        return null;
    }

    public NinePatchDrawable build() {
        NinePatch ninePatch = buildNinePatch();
        if (ninePatch != null)
            return new NinePatchDrawable(resources, ninePatch);
        return null;
    }
}

Run the test code

mLlRoot = findViewById(R.id.ll_root);
try {
    InputStream is = getAssets().open("sea.png");
    Bitmap bitmap = BitmapFactory.decodeStream(is);
    for (int i = 0; i < 5; i++) {
        NinePatchDrawable ninePatchDrawable = NinePatchHelper.buildMulti(this, bitmap);
        TextView textView = new TextView(this);
        textView.setTextSize(25);
        textView.setPadding(20, 10, 20, 10);
        textView.setText(strArray[i]);
        textView.setGravity(Gravity.CENTER_VERTICAL);
        LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
        layoutParams.leftMargin = 20;
        layoutParams.rightMargin = 20;
        textView.setLayoutParams(layoutParams);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            textView.setBackground(ninePatchDrawable);
        }
        mLlRoot.addView(textView);
    }
} catch (IOException e) {
    e.printStackTrace();
}

As you can see, our pictures stretch perfectly.

Reference articles

  1. https://cloud.tencent.com/dev...
  2. https://mp.weixin.qq.com/s?__...

Recommended reading

Responsibility Chain Model and Its Application in Android

Observer design pattern Vs event delegation (java)

Decorator Model and Its Application

Builder Model and Its Application

Third-party Framework for Secondary Packaging Pictures: Application of Simple Factory Model

Android Secondary Packaging Network Loading Framework

Detailed explanation of java proxy pattern

Rxjava 2.x Source Series-Basic Framework Analysis

Rxjava 2.x Source Series - Thread Switching (Part I)

Rxjava 2.x Source Series - Thread Switching (Part 2)

Rxjava 2.x Source Series - Transform Operator Map (Part 1)

butterknife source code analysis

Step by step disassemble LeakCanary

java Source Series - Read Reference and Reference Queue

Sweep it out. Welcome to my Wechat public number stormjun94 (Xugong Code Word). Now I am a programmer. I not only share the knowledge of Android development, but also share the growth process of technicians, including personal summary, career experience, interview experience, etc. I hope you can take less detours.

Posted by doofystyle on Thu, 19 Sep 2019 05:49:21 -0700