20

I am trying to use VectorDrawables in my Android App.
I want to load an xml File from the File System and get an Instance of android.graphics.Drawable to display it in an ImageView. If i add the xml File to the Resources directory it works. But when i try to load it from the filesystem i always get a NullPointer.

I am currently trying to load the File via Drawable.createFromPath(*Path to File*) or VectorDrawable.createFromPath(*Path to File*), but i keep getting a NullPointer. The File exists and is a valid xml File (see below).

In adb log i always get :

SkImageDecoder::Factory returned null

When i use mContext.getFilesDir() the Path looks something like

/data/data/*packagename*/files/*filename*.xml

I also tried some public Folders like "Downloads". When i check the File with the File.io Api it exists(), canRead() and canWrite()

Update This ist the XML Code taken from the android Dev Pages

 <vector xmlns:android="http://schemas.android.com/apk/res/android"
 android:height="64dp"
 android:width="64dp"
 android:viewportHeight="600"
 android:viewportWidth="600" >
 <group
     android:name="rotationGroup"
     android:pivotX="300.0"
     android:pivotY="300.0"
     android:rotation="45.0" >
     <path
         android:name="v"
         android:fillColor="#000000"
         android:pathData="M300,70 l 0,-70 70,70 0,0 -70,70z" />
 </group>

Jay_DK
  • 213
  • 2
  • 5

3 Answers3

12

Actually, yes you can, and I've done it in my own projects.

As mentioned in the previous answer though, you indeed need the compiled version of the XML drawable instead of the one you get from, say, Android Studio. AFAIK, the VectorDrawable API does not yet support loading from raw XML.

To do this, simply place your images temporarily under the res directory so that they get compiled as normal resources during the build process, then find your generated APK and extract them from there (you'll notice they're no longer text files, but binary files). Now put them again wherever you want under your assets dir and use code similar to this to load them as drawables:

XmlResourceParser parser = context.getAssets()
    .openXmlResourceParser("assets/folder/image.xml");
drawable = VectorDrawableCompat.createFromXml(context.getResources(), parser);

Notice the 'assets/' part of the path. That's required, afaik.

n00bmind
  • 346
  • 4
  • 11
  • Is there a way to load the files from the normal XML files, as we see them in the IDE ? – android developer Jun 08 '17 at 07:50
  • It's brilliant! I've spent ~2 days trying different approaches, and your worked! :) – thorin86 Nov 19 '18 at 15:03
  • What a hack, it works but worry if there is any compatibility issue with this approach. – Brian Chu Apr 12 '20 at 22:36
  • I don't see the outcome of moving the image into the assets directory. It still can't be placed there at runtime, so it can't be used from the file system :/ – Arkadiusz Cieśliński May 29 '20 at 14:24
  • 3
    Is it possible to do it from the file system too? For some reason I get `java.lang.ClassCastException: android.util.XmlPullAttributes cannot be cast to android.content.res.XmlBlock$Parser` when trying to parse "ic_launcher_foreground.xml" which I've put into a file on the file system – android developer May 31 '20 at 09:09
8

Unfortunately, no. Not unless you somehow compile the XML source file in the way that resources are compiled. VectorDrawable currently assumes a compiled image source.

Otherwise you could probably have written something like this:

VectorDrawable d = new VectorDrawable();
try( InputStream in = new FileInputStream( *Path to File* ); )
{
    XmlPullParser p = Xml.newPullParser();
    p.setInput( in, /*encoding, self detect*/null );
    d.inflate( getResources(), p, Xml.asAttributeSet(p) ); // FAILS
}

Currently that fails (API level 23, Marshmallow 6.0) because the inflate call attempts to cast the attribute set to an XmlBlock.Parser, which throws a ClassCastException. The cause is documented in the source; it will “only work with compiled XML files”, such as resources packaged by the aapt tool.

Michael Allan
  • 3,731
  • 23
  • 31
  • 2
    Added a request for this: https://issuetracker.google.com/issues/62435069 – android developer Jun 08 '17 at 07:58
  • 1
    I tried to run this code on API 29, on "abc_vector_test.xml" file which I've extracted from the APK to a normal file (meaning it's not an XML file but a binary compiled one). Still it got me `java.lang.ClassCastException: android.util.XmlPullAttributes cannot be cast to android.content.res.XmlBlock$Parser` . How come? – android developer May 31 '20 at 09:01
2

Yes, it is possible but sadly according to what someone shown me, it has multiple disadvantages:

  1. Requires reflection, so might not work some day
  2. The reflection is on private API, which is not recommended
  3. Works only on compiled VectorDrawable XML files.

That being said, maybe you could create your own XmlPullParser by looking at the code of other libraries, such as here (or my fork of it, here). The interesting class name there is "BinaryXmlParser", which I think might help. I tried using other methods, but it caused an exception of java.lang.ClassCastException: android.util.XmlPullAttributes cannot be cast to android.content.res.XmlBlock$Parser , so maybe it wants to use only the reflected XmlBlock class, which means even if you do implement it, I think there is a good chance it won't work.

Here's the code (based on here) :

object VectorDrawableParser {
    /**
     * Create a vector drawable from a binary XML byte array.
     *
     * @param context Any context.
     * @param binXml  Byte array containing the binary XML.
     * @return The vector drawable or null it couldn't be created.
     */
    @SuppressLint("PrivateApi", "DiscouragedPrivateApi")
    fun getVectorDrawable(context: Context, binXml: ByteArray): Drawable? {
        try {
            // Get the binary XML parser (XmlBlock.Parser) and use it to create the drawable
            // This is the equivalent of what AssetManager#getXml() does
            val xmlBlock = Class.forName("android.content.res.XmlBlock")
            val xmlBlockConstr = xmlBlock.getConstructor(ByteArray::class.java)
            val xmlParserNew = xmlBlock.getDeclaredMethod("newParser")
            xmlBlockConstr.isAccessible = true
            xmlParserNew.isAccessible = true
            val parser = xmlParserNew.invoke(xmlBlockConstr.newInstance(binXml)) as XmlPullParser
            return if (Build.VERSION.SDK_INT >= 24) {
                Drawable.createFromXml(context.resources, parser)
            } else {
                // Before API 24, vector drawables aren't rendered correctly without compat lib
                val attrs = Xml.asAttributeSet(parser)
                var type = parser.next()
                while (type != XmlPullParser.START_TAG) {
                    type = parser.next()
                }
                VectorDrawableCompat.createFromXmlInner(context.resources, parser, attrs, null)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return null
    }
}

And how to use:

val filePath = "/storage/emulated/0/abc_vector_test.xml"
val readBytes = FileInputStream(File(filePath)).readBytes()
val vectorDrawable = VectorDrawableParser.getVectorDrawable(this, readBytes)
Log.d("AppLog", "got it?${vectorDrawable != null}")
android developer
  • 114,585
  • 152
  • 739
  • 1,270