Search code examples
androidandroid-vectordrawable

Create Vector Drawable from String with multiple path tags (Android)


I was working on sending vector drawables from server to my application. I wanted to implement a function which gets a list of path data and color and creates a vector drawable from the given list of String/Color pairs.

I used the answer Nicolas provided on a similar thread (Create VectorDrawable from String (path)?) But it fails when given multiple path/color pair items.

Can anyone help me with with finding the bug? the code seems OK to me.

Here is my code:

private static final byte[][] BIN_XML_STRINGS = {
        "height".getBytes(), "width".getBytes(), "viewportWidth".getBytes(),
        "viewportHeight".getBytes(), "fillColor".getBytes(), "pathData".getBytes(),
        "http://schemas.android.com/apk/res/android".getBytes(), "path".getBytes(), "vector".getBytes()
};
private static final int[] BIN_XML_ATTRS = {android.R.attr.height, android.R.attr.width, android.R.attr.viewportWidth,
        android.R.attr.viewportHeight, android.R.attr.fillColor, android.R.attr.pathData};

public static Drawable getVectorDrawable(@NonNull Context context,
                                         int width, int height,
                                         float viewportWidth, float viewportHeight,
                                         List<Pair<String, Integer>> pathColorList) {
    List<Pair<byte[], Integer>> pathBytes = new ArrayList<>();
    for(Pair<String, Integer> pathData: pathColorList){
        pathBytes.add(new Pair<>(pathData.first.getBytes(), pathData.second));
    }
    try {
        // Get the binary XML parser (XmlBlock.Parser) and use it to create the drawable
        // This is the equivalent of what AssetManager#getXml() does
        @SuppressLint("PrivateApi")
        Class<?> xmlBlock = Class.forName("android.content.res.XmlBlock");
        Constructor xmlBlockConstr = xmlBlock.getConstructor(byte[].class);
        Method xmlParserNew = xmlBlock.getDeclaredMethod("newParser");
        xmlBlockConstr.setAccessible(true);
        xmlParserNew.setAccessible(true);
        XmlPullParser parser = (XmlPullParser) xmlParserNew.invoke(
                xmlBlockConstr.newInstance((Object) binXml));

        KappaLogger.LogError(parser.toString());
        if (Build.VERSION.SDK_INT >= 24) {
            return Drawable.createFromXml(context.getResources(), parser);
        } else {
            // Before API 24, vector drawables aren't rendered correctly without compat lib
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            int type = parser.next();
            while (type != XmlPullParser.START_TAG) {
                type = parser.next();
            }
            return VectorDrawableCompat.createFromXmlInner(context.getResources(), parser, attrs, null);
        }

    } catch (Exception e) {
        KappaExceptionUtils.sendStackTraceToLog(e);
    }
    return null;
}

private static byte[] createBinaryDrawableXml(int width, int height,
                                              float viewportWidth, float viewportHeight,
                                              List<Pair<byte[], Integer>> pathBytes) {
    List<byte[]> binXmlStrings = new ArrayList<>(Arrays.asList(BIN_XML_STRINGS));
    for(Pair<byte[], Integer> pathItem: pathBytes){
        binXmlStrings.add(pathItem.first);
    }

    ByteBuffer bb = ByteBuffer.allocate(8192);
    bb.order(ByteOrder.LITTLE_ENDIAN);

    // ==== XML header ====
    bb.putShort((short) 0x0003);  // Type: XML
    bb.putShort((short) 8);  // Header size
    int xmlSizePos = bb.position();
    bb.position(bb.position() + 4);

    // ==== String pool chunk ====
    int spStartPos = bb.position();
    bb.putShort((short) 0x0001);  // Type: String pool
    bb.putShort((short) 28);  // Header size
    int spSizePos = bb.position();
    bb.position(bb.position() + 4);
    bb.putInt(binXmlStrings.size());  // String count
    bb.putInt(0);  // Style count
    bb.putInt(256);  // Flags set: encoding is UTF-8
    bb.putInt(0x44);  // Strings start
    bb.putInt(0);  // Styles start

    // String offsets
    int offset = 0;
    for (byte[] str : binXmlStrings) {
        bb.putInt(offset);
        offset += str.length + (str.length > 127 ? 5 : 3);
    }

    // String pool
    for (byte[] str : binXmlStrings) {
        if (str.length > 127) {
            byte high = (byte) ((str.length & 0xFF00 | 0x8000) >>> 8);
            byte low = (byte) (str.length & 0xFF);
            bb.put(high);
            bb.put(low);
            bb.put(high);
            bb.put(low);
        } else {
            byte len = (byte) str.length;
            bb.put(len);
            bb.put(len);
        }
        bb.put(str);
        bb.put((byte) 0);
    }

    if (bb.position() % 4 != 0) {
        // Padding to align on 32-bit
        bb.put(new byte[4 - (bb.position() % 4)]);
    }

    // Write string pool chunk size
    int posBefore = bb.position();
    bb.putInt(spSizePos, bb.position() - spStartPos);
    bb.position(posBefore);

    // ==== Resource map chunk ====
    bb.putShort((short) 0x0180);  // Type: Resource map
    bb.putShort((short) 8);  // Header size
    bb.putInt(8 + BIN_XML_ATTRS.length * 4);  // Chunk size
    for (int attr : BIN_XML_ATTRS) {
        bb.putInt(attr);
    }

    // ==== Vector start tag ====
    int vstStartPos = bb.position();
    bb.putShort((short) 0x0102);  // Type: Start tag
    bb.putShort((short) 16);  // Header size
    int vstSizePos = bb.position();
    bb.position(bb.position() + 4);
    bb.putInt(0);  // Line number: None
    bb.putInt(-1);  // Comment: None

    bb.putInt(-1);  // Namespace: None
    bb.putInt(8); // Name: vector (index 9)
    bb.putShort((short) 0x14);
    bb.putShort((short) 0x14);
    bb.putShort((short) 4);  // Attribute count
    bb.putShort((short) 0);
    bb.putShort((short) 0);
    bb.putShort((short) 0);

    // Attributes
    bb.putInt(6);  // Namespace: android
    bb.putInt(0);  // Name: height
    bb.putInt(-1);  // Raw value: none
    bb.putShort((short) 0x08);  // Value size
    bb.putShort((short) 0x0500);  // value type: dimension
    bb.putInt(height * 256 + 1);  // Value data: 0x01 for dp, 0x18 for 24

    bb.putInt(6);  // Namespace: android
    bb.putInt(1);  // Name: width
    bb.putInt(-1);  // Raw value: none
    bb.putShort((short) 0x08);  // Value size
    bb.putShort((short) 0x0500);  // value type: dimension
    bb.putInt(width * 256 + 1);  // Value data: 0x01 for dp, 0x18 for 24

    bb.putInt(6);  // Namespace: android
    bb.putInt(2);  // Name: viewportWidth
    bb.putInt(-1);  // Raw value: none
    bb.putShort((short) 0x08);  // Value size
    bb.putShort((short) 0x0400);  // value type: float
    bb.putInt(Float.floatToRawIntBits(viewportWidth));  // Value data: 24.0

    bb.putInt(6);  // Namespace: android
    bb.putInt(3);  // Name: viewportHeight
    bb.putInt(-1);  // Raw value: none
    bb.putShort((short) 0x08);  // Value size
    bb.putShort((short) 0x0400);  // value type: float
    bb.putInt(Float.floatToRawIntBits(viewportHeight));  // Value data: 24.0

    // Write vector start tag chunk size
    posBefore = bb.position();
    bb.putInt(vstSizePos, bb.position() - vstStartPos);
    bb.position(posBefore);

    for(int i=0; i<pathBytes.size(); i++){
        // ==== Path start tag ====
        int pstStartPos = bb.position();
        bb.putShort((short) 0x0102);  // Type: Start tag
        bb.putShort((short) 16);  // Header size
        int pstSizePos = bb.position();
        bb.position(bb.position() + 4);
        bb.putInt(0);  // Line number: None
        bb.putInt(-1);  // Comment: None

        bb.putInt(-1);  // Namespace: None
        bb.putInt(7); // Name: path (index 8)
        bb.putShort((short) 0x14);
        bb.putShort((short) 0x14);
        bb.putShort((short) 2);  // Attribute count
        bb.putShort((short) 0);
        bb.putShort((short) 0);
        bb.putShort((short) 0);

        bb.putInt(6);  // Namespace: android
        bb.putInt(4);  // Name: fillColor
        bb.putInt(-1);  // Raw value: none
        bb.putShort((short) 0x08);  // Value size
        bb.putShort((short) 0x1D00);  // value type: color #rgb
        bb.putInt(pathBytes.get(i).second);  // Value data: color

        bb.putInt(6);  // Namespace: android
        bb.putInt(5);  // Name: pathData
        bb.putInt(i+9);  // Raw value: index 9 in string pool (path data)
        bb.putShort((short) 0x08);  // Value size
        bb.putShort((short) 0x0300);  // value type: string
        bb.putInt(i+9);  // Value data: same as raw data

        // Write path start tag chunk size
        posBefore = bb.position();
        bb.putInt(pstSizePos, bb.position() - pstStartPos);
        bb.position(posBefore);

        // ==== Path end tag ====
        bb.putShort((short) 0x0103);
        bb.putShort((short) 16);  // Header size
        bb.putInt(24);  // Chunk size
        bb.putInt(0);  // Line number: none
        bb.putInt(-1);  // Comment: none
        bb.putInt(-1);  // Namespace: none
        bb.putInt(7);  // Name: path
    }

    // ==== Vector end tag ====
    bb.putShort((short) 0x0103);
    bb.putShort((short) 16);  // Header size
    bb.putInt(24);  // Chunk size
    bb.putInt(0);  // Line number: none
    bb.putInt(-1);  // Comment: none
    bb.putInt(-1);  // Namespace: none
    bb.putInt(8);  // Name: vector

    // Write XML chunk size
    posBefore = bb.position();
    bb.putInt(xmlSizePos, bb.position());
    bb.position(posBefore);

    byte[] binXml = new byte[bb.position()];
    bb.rewind();
    bb.get(binXml);

    StringBuilder sb = new StringBuilder();
    for (byte b : binXml) {
        String hex = Integer.toHexString(b & 0xFF);
        if (hex.length() == 1) sb.append('0');
        sb.append(hex.toUpperCase());
        sb.append(' ');
    }
    String str = sb.toString();

    return binXml;
}

It works for a single path in vector, but when I use the following pair/color list, I get android.view.InflateException: Class not found h error.

input:

[
    new Pair<>("M177.230469 329.847656v167.382813c0 8.15625 6.613281 14.769531 14.769531 14.769531l9.855469-99.9375-9.855469-96.984375c-8.15625 0-14.769531 6.613281-14.769531 14.769531zm0 0",Color.BLACK),
    new Pair<>("M192 315.078125v196.921875c8.15625 0 14.769531-6.613281 14.769531-14.769531v-167.382813c0-8.15625-6.613281-14.769531-14.769531-14.769531zm0 0", Color.RED)
]

I pass 512 for width and height, and 512.0f as viewportWidth and viewportHeight input variables.


Solution

  • I cleaned up and fixed the code in my answer: https://stackoverflow.com/a/49920860/5288316

    You can now pass a list of paths with different colors and change the drawable size and the viewport size.

    The problem was with the line:

    bb.putInt(0x44);  // Strings start
    

    This parameter of the string pool chunk is the number of bytes between the start of the chunk and the start of the list of strings. In between there's the list of string offsets, which was longer when there was more than one path, hence the 0x44 value wasn't right anymore.