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