Is there a way to get SVG path string from the API and create VectorDrawable
dynamically?
I have been trying to do this for hours without success. Even more, all (!) examples on the Internet explain creating VectorDrawable
from XML resources.
In my case, XML resource file is pointless as I am trying to fetch SVG path from the Internet API.
Inflating a drawable from a XML file instead of from resources is actually impossible, because the drawable will try to cast the XmlPullParser
to XmlResourceParser
which is only implemented by private class XmlBlock.Parser
. Even that parser is only used for parsing binary XML files. I tried every possible way of doing this without reflection, it's impossible.
So I found documentation on binary XML files and learned how they were made, helped with some compiled binary XML vector drawable files I had. The documentation dates back to 2011 and is still valid, I guess it will most likely remain this way, so future compatibility isn't an issue.
A previous version was tested for more than a thousand paths, without problem. The new version posted here should work just as well. (Previous versions are available in the answer history) Compared with loading a drawable directly from resources, I found that there's an average of 14 microseconds or so of extra loading, not noticeable.
Here's the code:
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);
}
}
}
A call to getVectorDrawable
returns a VectorDrawable
from a list of paths. The drawable can contain multiple paths with different colors. There are also parameters for the drawable and viewport size.
Here's an example:
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")));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With