从String(路径)创建VectorDrawable?

时间:2015-10-23 22:12:06

标签: android android-vectordrawable

有没有办法从API获取SVG路径字符串并动态创建VectorDrawable

我一直试图这样做几个小时没有成功。更重要的是,Internet上的所有(!)示例都解释了如何从XML资源创建VectorDrawable

就我而言,XML资源文件毫无意义,因为我试图从Internet API获取SVG路径。

1 个答案:

答案 0 :(得分:3)

从XML文件而不是从资源中扩展drawable实际上是不可能的,因为drawable会尝试将XmlPullParser强制转换为XmlResourceParser,这只能由私有类XmlBlock.Parser实现。即使该解析器仅用于解析二进制XML文件。我尝试了一切可能的做法,没有反思,这是不可能的。

所以我找到了documentation on binary XML个文件并了解了它们是如何制作的,并帮助了我编写的一些二进制XML矢量可绘制文件。文档可以追溯到2011年并且仍然有效,我想它很可能会保持这种状态,因此未来的兼容性不是问题。

以前的版本测试了超过一千条路径,没有问题。这里发布的新版本也应该可以正常工作。 (answer history中提供了以前的版本)与直接从资源加载drawable相比,我发现平均有14微秒左右的额外加载,并不明显。

以下是代码:

public class VectorDrawableCreator {

    private static final byte[][] BIN_XML_STRINGS = {
            "width".getBytes(),
            "height".getBytes(),
            "viewportWidth".getBytes(),
            "viewportHeight".getBytes(),
            "fillColor".getBytes(),
            "pathData".getBytes(),
            "path".getBytes(),
            "vector".getBytes(),
            "http://schemas.android.com/apk/res/android".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
    };

    private static final short CHUNK_TYPE_XML = 0x0003;
    private static final short CHUNK_TYPE_STR_POOL = 0x0001;
    private static final short CHUNK_TYPE_START_TAG = 0x0102;
    private static final short CHUNK_TYPE_END_TAG = 0x0103;
    private static final short CHUNK_TYPE_RES_MAP = 0x0180;

    private static final short VALUE_TYPE_DIMENSION = 0x0500;
    private static final short VALUE_TYPE_STRING = 0x0300;
    private static final short VALUE_TYPE_COLOR = 0x1D00;
    private static final short VALUE_TYPE_FLOAT = 0x0400;


    /**
     * Create a vector drawable from a list of paths and colors
     * @param width drawable width
     * @param height drawable height
     * @param viewportWidth vector image width
     * @param viewportHeight vector image height
     * @param paths list of path data and colors
     * @return the vector drawable or null it couldn't be created.
     */
    public static Drawable getVectorDrawable(@NonNull Context context,
                                             int width, int height,
                                             float viewportWidth, float viewportHeight,
                                             List<PathData> paths) {
        byte[] binXml = createBinaryDrawableXml(width, height, viewportWidth, viewportHeight, paths);

        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));

            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) {
            Log.e(VectorDrawableCreator.class.getSimpleName(), "Vector creation failed", e);
        }
        return null;
    }

    private static byte[] createBinaryDrawableXml(int width, int height,
                                                  float viewportWidth, float viewportHeight,
                                                  List<PathData> paths) {
        List<byte[]> stringPool = new ArrayList<>(Arrays.asList(BIN_XML_STRINGS));
        for (PathData path : paths) {
            stringPool.add(path.data);
        }

        ByteBuffer bb = ByteBuffer.allocate(8192);  // Capacity might have to be greater.
        bb.order(ByteOrder.LITTLE_ENDIAN);

        int posBefore;

        // ==== XML chunk ====
        // https://justanapplication.wordpress.com/2011/09/22/android-internals-binary-xml-part-two-the-xml-chunk/
        bb.putShort(CHUNK_TYPE_XML);  // Type
        bb.putShort((short) 8);  // Header size
        int xmlSizePos = bb.position();
        bb.position(bb.position() + 4);

        // ==== String pool chunk ====
        // https://justanapplication.wordpress.com/2011/09/15/android-internals-resources-part-four-the-stringpool-chunk/
        int spStartPos = bb.position();
        bb.putShort(CHUNK_TYPE_STR_POOL);  // Type
        bb.putShort((short) 28);  // Header size
        int spSizePos = bb.position();
        bb.position(bb.position() + 4);
        bb.putInt(stringPool.size());  // String count
        bb.putInt(0);  // Style count
        bb.putInt(1 << 8);  // Flags set: encoding is UTF-8
        int spStringsStartPos = bb.position();
        bb.position(bb.position() + 4);
        bb.putInt(0);  // Styles start

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

        posBefore = bb.position();
        bb.putInt(spStringsStartPos, bb.position() - spStartPos);
        bb.position(posBefore);

        // String pool
        for (byte[] str : stringPool) {
            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
        posBefore = bb.position();
        bb.putInt(spSizePos, bb.position() - spStartPos);
        bb.position(posBefore);

        // ==== Resource map chunk ====
        // https://justanapplication.wordpress.com/2011/09/23/android-internals-binary-xml-part-four-the-xml-resource-map-chunk/
        bb.putShort(CHUNK_TYPE_RES_MAP);  // Type
        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();
        int vstSizePos = putStartTag(bb, 7, 4);

        // Attributes
        // android:width="24dp", value type: dimension (dp)
        putAttribute(bb, 0, -1, VALUE_TYPE_DIMENSION, (width << 8) + 1);

        // android:height="24dp", value type: dimension (dp)
        putAttribute(bb, 1, -1, VALUE_TYPE_DIMENSION, (height << 8) + 1);

        // android:viewportWidth="24", value type: float
        putAttribute(bb, 2, -1, VALUE_TYPE_FLOAT, Float.floatToRawIntBits(viewportWidth));

        // android:viewportHeight="24", value type: float
        putAttribute(bb, 3, -1, VALUE_TYPE_FLOAT, Float.floatToRawIntBits(viewportHeight));

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

        for (int i = 0; i < paths.size(); i++) {
            // ==== Path start tag ====
            int pstStartPos = bb.position();
            int pstSizePos = putStartTag(bb, 6, 2);

            // android:fillColor="#aarrggbb", value type: #rgb.
            putAttribute(bb, 4, -1, VALUE_TYPE_COLOR, paths.get(i).color);

            // android:pathData="...", value type: string
            putAttribute(bb, 5, 9 + i, VALUE_TYPE_STRING, 9 + i);

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

            // ==== Path end tag ====
            putEndTag(bb, 6);
        }

        // ==== Vector end tag ====
        putEndTag(bb, 7);

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

        // Return binary XML byte array
        byte[] binXml = new byte[bb.position()];
        bb.rewind();
        bb.get(binXml);

        return binXml;
    }

    private static int putStartTag(ByteBuffer bb, int name, int attributeCount) {
        // https://justanapplication.wordpress.com/2011/09/25/android-internals-binary-xml-part-six-the-xml-start-element-chunk/
        bb.putShort(CHUNK_TYPE_START_TAG);
        bb.putShort((short) 16);  // Header size
        int sizePos = bb.position();
        bb.putInt(0); // Size, to be set later
        bb.putInt(0);  // Line number: None
        bb.putInt(-1);  // Comment: None

        bb.putInt(-1);  // Namespace: None
        bb.putInt(name);
        bb.putShort((short) 0x14);  // Attributes start offset
        bb.putShort((short) 0x14);  // Attributes size
        bb.putShort((short) attributeCount);  // Attribute count
        bb.putShort((short) 0);  // ID attr: none
        bb.putShort((short) 0);  // Class attr: none
        bb.putShort((short) 0);  // Style attr: none

        return sizePos;
    }

    private static void putEndTag(ByteBuffer bb, int name) {
        // https://justanapplication.wordpress.com/2011/09/26/android-internals-binary-xml-part-seven-the-xml-end-element-chunk/
        bb.putShort(CHUNK_TYPE_END_TAG);
        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(name);  // Name: vector
    }

    private static void putAttribute(ByteBuffer bb, int name,
                                     int rawValue, short valueType, int valueData) {
        // https://justanapplication.wordpress.com/2011/09/19/android-internals-resources-part-eight-resource-entries-and-values/#struct_Res_value
        bb.putInt(8);  // Namespace index in string pool (always the android namespace)
        bb.putInt(name);
        bb.putInt(rawValue);
        bb.putShort((short) 0x08);  // Value size
        bb.putShort(valueType);
        bb.putInt(valueData);
    }


    public static class PathData {

        public byte[] data;
        public int color;

        public PathData(byte[] data, int color) {
            this.data = data;
            this.color = color;
        }

        public PathData(String data, int color) {
            this(data.getBytes(StandardCharsets.UTF_8), color);
        }

    }

}

getVectorDrawable的调用会从路径列表中返回VectorDrawable。 drawable可以包含多个具有不同颜色的路径。还有drawable和视口大小的参数。

以下是一个例子:

List<PathData> pathList = Arrays.asList(new PathData("M128.09 5.02a110.08 110.08 0 0 0-110 110h220a109.89 109.89 0 0 0-110-110z", Color.parseColor("#7cb342")),
                    new PathData("M128.09 115.02h-110a110.08 110.08 0 0 0 110 110 110.08 110.08 0 0 0 110-110z", Color.parseColor("#8bc34a")),
                    new PathData("M207.4 115.2v-.18h-5.1l-61.43-61.43h-25.48v20.6h-6.5a11.57 11.57 0 0 0-11.53 11.53v26.09h.11c-.11.9.5 2 1.7 3.32.12.08.12.08.12.2l3.96 4-46.11 79.91c5.33 4.5 11.04 8.4 17 11.8a109.81 109.81 0 0 0 108.04 0 110.04 110.04 0 0 0 51.52-64.65c.38-1.28.68-2.57 1.1-3.78z", Color.parseColor("#30000000")),
                    new PathData("M216.28 230.24a6.27 6.27 0 0 0-.9-2.8l-31.99-55.57-10.58-18.48-19.85-34.21-15.08 15.12 18.6 32.28 10.2 17.73 30.92 53.37a5.6 5.6 0 0 0 1.97 2.12l15.42 10.5c.6.39 1.29.39 1.9.08.6-.37.9-.98.9-1.7z", Color.parseColor("#e1e1e1")),
                    new PathData("M186.98 115.02a58.9 58.9 0 0 1-30.5 51.6 58.4 58.4 0 0 1-56.7 0l18.6-32.28-15.13-15.12-62.48 108.22c-.5.9-.8 1.78-.9 2.8l-1.4 18.6c-.12.71.3 1.28.9 1.7.6.37 1.29.3 1.9-.12l15.41-10.4a7.87 7.87 0 0 0 1.97-2.07l30.92-53.53a78.74 78.74 0 0 0 77.23 0 76.65 76.65 0 0 0 16.6-12.4 79.3 79.3 0 0 0 24.07-56.89z", Color.parseColor("#f1f1f1")),
                    new PathData("M147.3 74.12h-6.43v-20.6h-25.48v20.6h-6.5a11.57 11.57 0 0 0-11.53 11.5v26.07h.11c-.11 1.02.5 2.12 1.82 3.4l23.05 23.14a8.3 8.3 0 0 0 5.75 2.38v-.07l.07.07c2.12 0 4.2-.75 5.71-2.38l23.1-23.1c1.32-1.32 1.81-2.53 1.81-3.4h.12V85.7a11.68 11.68 0 0 0-11.6-11.6zm-19.14 40.9h-.07a15.4 15.4 0 0 1 0-30.8v-.2l.07.2a15.46 15.46 0 0 1 15.31 15.38 15.46 15.46 0 0 1-15.3 15.42z", Color.parseColor("#646464")));