I'm trying to convert a standard XML file
Like
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="30dp"
android:height="30dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#5CDD06"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
</vector>
to binary formated xml file so that I can inflate Views and drawables at runtime
But I can't find the right way. I tried to get the bytes from the file
From
FileInputStream is = new FileInputStream("file.xml");
byte[] arr = new byte[is.available];
is.read(arr)
and try to parse it to View or Drawlable but Xml$Block.Parser can't handle it
@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) arr2)); //throws invocationTargetException
Drawable.createFromXml(context.getResources, parser);
it throws InvocationTargetException
But when i use this method
public 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;
}
And try to invoke the newParser method
List<PathData> pathList = Arrays.asList(new PathData("M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z", Color.parseColor("#5CDD06")));
byte[] arr2 = createBinaryDrawableXml(30, 30, 24, 24, pathList);
XmlPullParser parser = (XmlPullParser) xmlParserNew.invoke(xmlBlockConstr.newInstance((Object) arr2));
Drawable.createFromXml(context.getResources, parser);
It works like charm and the Drawable shows
And i tried aapt but its packging the hole resources and put into apk
I found this question but no answer
How to use Android aapt to compile a specific layout file to binary?
I tried to use https://github.com/hzw1199/xml2axml
But im getting
xmlpullparser exception at line 7 height must be > 0
So any solutions?
CodePudding user response:
Solved
After a lot of work and tracing android java source code i found very acceptable solution
The real question was how to inflate views at run time
We all know that you cant
Important For performance reasons, view inflation relies heavily on pre-processing of XML files that is done at build time. Therefore, it is not currently possible to use LayoutInflater with an XmlPullParser over a plain XML file at runtime.
Because android compiling resources at compile time then convert it to bytes then use XmlBlock Class
That class only accept Android xml format (Axml)
And LayoutInflater class casting XmlBlock to XmlPullParser
So when you try to inflate views with
LayoutInflater.inflate(XmlPullParser,root);
You got cast Exception
Anyway here is the Solution
First i tried to convert xml to axml using Aapt tool and getting XmlBlock private class by reflection and inflate the xml file
That works but!
If i add attribute like
android:background="@drawable/resource"
He couldn't find the resource cuz i only compiled the layout xml file and inflate it
I tried a lot solutions and wasted a lot of time but nothing work
Then!
While tracing android source code found how are resource made programmatically
First you have to get ContextImpl private Class
Class contextImpl = Class.forName("android.app.ContextImpl");
Then create new Context or in other words clone it!
Dont worry we can clone the Context just keep up with me :>
To create new context we have to call createApplicationContext
This method takes ApplicationInfo argument and flags
Method createContext = contextImpl.getMethod("createApplicationContext", new Class<?>[]{ApplicationInfo.class, int.class});
And getting newInstance of ContextImpl
Method getImpl = contextImpl.getDeclaredMethod("getImpl",Context.class);
getImpl.setAccessible(true);
Object cImpl = getImpl.invoke(null,context);//orignal app context
Then excute method to get the cloned Context
But we have to send the orignal app context to get clone instance of it
Context newContext = (Context)createContext.invoke(cImpl,new Object[]{context.getApplicationInfo(),0});// my application info
Done we have cloned context
Now we nees to change the Resources in cloned context to our resources
So you have to create your full /res directory and put your resources in
Then we use Aapt to get compiled resources
Here is download link Aapt
Let's back to work!
Now compile our resources
String[] cmd2 = {context.getFilesDir().getAbsolutePath() "/aapt","p","-m","-f","-v","-M",
"/storage/emulated/0/AppProjects/XmlToView/app/src/main/AndroidManifest.xml","-I"
,"/storage/emulated/0/Download/android.jar",
"-S","/storage/emulated/0/UI Projects/Test/res","-S","/storage/emulated/0/appcompat-v7-28.0.0/res","-F",
"/storage/emulated/0/UI Projects/Test/compiledRes.apk"
,"-J",path.substring(0,path.length() - "res".length()),
"--extra-packages","android.support.v7.appcompat","--auto-add-overlay"};
Runtime.getRuntime().exec(cmd2).waitFor();
You can see documentation too learn how aapt works
Now we have occurred apk file but it doesn't matter we just need the resources
So now we can make our custom resources instance
First we have to get private Class ResourcesManager To create new rssources class
Class resourceManager = Class.forName("android.app.ResourcesManager");
And get new instance using getInstace method
Method getInstance = resourceManager.getDeclaredMethod("getInstance");
Object reso = getInstance.invoke(null);
Now we are ready to create new Resouces
Now we have to find getResources method
I found it using loop because it takes 11 argument
Now you have to get compiled res apk file path that we created using aapt
And path it in get resources method
Method[] methods = resourceManager.getDeclaredMethods();
Resources newResss = null;
for (Method mthd: methods)
{
String mthdName = mthd.getName();
if (mthdName.equals("getResources") && mthd.getParameterCount() == 11)
{
newResss =(Resources) mthd.invoke(reso , new Object[]{
null,
"Compiled res path /storage/resources.apk",
null,
null,
null,
0,
null,
null,
null,
null,
0
});
break;
}
}
Now we have a new Resources thet contains all your resources you make
All what left is to change the cloned context resources to the new Resources
Getting setResources method
if(newResss != null){
Method setResources = newContext.getClass().getDeclaredMethod("setResources",Resources.class);
setResources.setAccessible(true);
setResources.invoke(newContext,newResss);//passing new Context and new Resources
}
Now you have new context of your app with deffrent Resources
Now all you have to do to inflate the views is creating new LayoutInflater with the new context and calling inflate
But you have to get id of layout you need to inflate and of course you will not found it in R class
So take this method
public int getJavaFileResource(File f, String name){
String[] result = new String[3];
try
{
Reader r = new FileReader(f);
BufferedReader br = new BufferedReader(r);
String s;
while ((s = br.readLine()) != null)
{
if (s.contains("class") && s.contains("public"))
{
int index = s.indexOf("class") "class".length() 1;
String cls = s.substring(index, s.indexOf(" ", index));
result[0] = cls;
}
else if (s.contains("public") && s.contains("int"))
{
int index = s.indexOf("int") "int".length() 1;
String name2 = s.substring(index, s.indexOf("="));
String value = s.substring(s.indexOf("=") 1, s.length() - 1);
int vvalue = 0;
try{
vvalue = Integer.decode(value);
}
catch(NumberFormatException e){
continue;
}
if (name.equals(name2))
{
return vvalue;
}
}
}
}
catch (IOException e)
{}
return 0;
}
Aapt tool will generate new R java file in your choosed path
You have to read the documentation of aapt
So you excute this method getJavaResource and path R.java file and resource name like main ..
Now all you have to do calling that
LayoutInflater.from(newContext).inflate(resid,root);
Congratulations now you have inflated layout at runtime.
Hope this works for all devices
Bye.