Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Animated Vector Drawable using compat library even on API 22 device

I wrote an animated vector drawable using path morphing (which is available only on API 21 and above). I have a fallback animation using a simple rotation for API below 21. I'm using the animated vector drawable support library (com.android.support:animated-vector-drawable:25.3.1).

Here is how I start the animation:

mBinding.switchIcon.setImageResource(R.drawable.ic_animated_vertical_arrow_down_to_up_32dp);

final Drawable animation = mBinding.switchIcon.getDrawable();
if (animation instanceof Animatable) {
    ((Animatable) animation).start();
}

This works fine on API 19 and 24, but doesn't work on API 22 nor 23 (I don't have an API 21 device to test).

The API 19 case is logical: the animation is simple, is handled by the support library perfectly, it works. Great.

I expected any API 21 and above devices to pick the built-in vector drawable implementation. However, when debugging, I can see that animation is in fact an instance of AnimatedVectorDrawableCompat: hence, it doesn't support path morphing, and the animation doesn't work.

So why does it work on API 24? Well, animation is an instance of AnimatedVectorDrawable. Hence, path morphing works fine.

So my question is: why doesn't API 21-23 devices pick up the built-in implementation, and rely on the support library, while an API 24 device does pick it up?


  • As a side note, forcing the device to pick the built-in implementation does obviously works:

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        AnimatedVectorDrawable drawable = (AnimatedVectorDrawable) getDrawable(R.drawable.ic_animated_vertical_arrow_down_to_up_32dp);
        mBinding.switchIcon.setImageDrawable(drawable);
    } else {
        mBinding.switchIcon.setImageResource(R.drawable.ic_animated_vertical_arrow_down_to_up_32dp);
    }
    
    final Drawable animation = mBinding.switchIcon.getDrawable();
    if (animation instanceof Animatable) {
        ((Animatable) animation).start();
    }
  • I also found this (probably) related issue on the Google bug-tracker: https://issuetracker.google.com/issues/37116940

  • Using a debugger, I can confirm that on API 22 (and probably 23), the support libraries are indeed delegating the work to the SDK's AnimatorSet. I really don't understand the behavior change.


About what follows

These are some notes I thought could be interesting to share, which I took while investigating in the technical explanation of this issue. The intersting, less technical bits are summarized in the accepted answer.


Here is the AVD I'm using, for reference:

<animated-vector
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt">
    <aapt:attr name="android:drawable">
        <vector
            xmlns:android="http://schemas.android.com/apk/res/android"
            android:width="32dp"
            android:height="32dp"
            android:viewportWidth="24"
            android:viewportHeight="24"
            android:alpha="1">
            <group android:name="group">
                <path
                    android:name="path"
                    android:pathData="@string/vertical_arrow_up_path"
                    android:strokeColor="#000000"
                    android:strokeWidth="2"
                    android:strokeLineCap="square"/>
            </group>
        </vector>
    </aapt:attr>
    <target android:name="path">
        <aapt:attr name="android:animation">
            <objectAnimator
                xmlns:android="http://schemas.android.com/apk/res/android"
                android:name="path"
                android:propertyName="pathData"
                android:duration="300"
                android:valueFrom="@string/vertical_arrow_up_path"
                android:valueTo="@string/vertical_arrow_down_path"
                android:valueType="pathType"
                android:interpolator="@android:anim/accelerate_decelerate_interpolator"/>
        </aapt:attr>
    </target>
</animated-vector>

And both path resources:

<string name="vertical_arrow_up_path" translatable="false">M 7.41 10 L 12 14.585 L 16.59 10</string>
<string name="vertical_arrow_down_path" translatable="false">M 7.41 14.585 L 12 10 L 16.59 14.585</string>

On an API 22 device, both the built-in and the support version (25.3.1) seems to inflate the same Animator from my AVD above, albeit with a different hierarchy.

With the support version (25.3.1), the AnimatorSet has only one node: an AnimatorSet containing itself a single animation, seemingly matching the ObjectAnimator described in the AVD's XML. Its referent is set to the VectorDrawableCompat, the property name is rightfully pathData, and the values list is containing a single PropertyValuesHolder with two keyframes, matching my start and end paths. Result: doesn't work.

With the build-in version (SDK 22), it's not exactly the same (but the AnimatorSet isn't exactly in the same place, so…): in the AnimatedVectorDrawableState, the mAnimators list has 1 element, which is directly the ObjectAnimator (with the same values as with the support version). Result: works.

The only relevant difference I can see is the ValueAnimator in the PropertyValuesHolder. As it has some reference to the drawable, I guess it may have some typecheck ignoring the support library version of the VectorDrawable class. But that's pure guesswork at that point. I'll keep digging…


I finally got it (and accepted @LewisMcGeary's answer, as I didn't mention in this question that I was looking for the technical bits behind the issue). Here's what happens. As mentioned, on APIs 21-23, the support library is taking over the SDK's implementation, to avoid bugs in said implementations. So we're using AnimatedVectorDrawableCompat and other [whatever]Compat classes. Once the vector itself is loaded, it's the animation's turn.

The animation is delegated to the SDK's ObjectAnimator, whatever the API level we're on (at least on 21+, but I guess it's the same thing on 19 and below). To animate primitive types, the ObjectAnimator has an internal map of functions to call to change the values. However, on complex types, it's relying on a specific method signature which has to be present on the animated object. Here's the method mapping value type to corresponding method to call, from PropertyValuesHolder (SDK, API 22):

    private Method getPropertyFunction(Class targetClass, String prefix, Class valueType) {
        // TODO: faster implementation...
        Method returnVal = null;
        String methodName = getMethodName(prefix, mPropertyName);
        Class args[] = null;
        if (valueType == null) {
            try {
                returnVal = targetClass.getMethod(methodName, args);
            } catch (NoSuchMethodException e) {
                // Swallow the error, log it later
            }
        } else {
            args = new Class[1];
            Class typeVariants[];
            if (valueType.equals(Float.class)) {
                typeVariants = FLOAT_VARIANTS;
            } else if (valueType.equals(Integer.class)) {
                typeVariants = INTEGER_VARIANTS;
            } else if (valueType.equals(Double.class)) {
                typeVariants = DOUBLE_VARIANTS;
            } else {
                typeVariants = new Class[1];
                typeVariants[0] = valueType;
            }
            for (Class typeVariant : typeVariants) {
                args[0] = typeVariant;
                try {
                    returnVal = targetClass.getMethod(methodName, args);
                    if (mConverter == null) {
                        // change the value type to suit
                        mValueType = typeVariant;
                    }
                    return returnVal;
                } catch (NoSuchMethodException e) {
                    // Swallow the error and keep trying other variants
                }
            }
            // If we got here, then no appropriate function was found
        }

        if (returnVal == null) {
            Log.w("PropertyValuesHolder", "Method " +
                    getMethodName(prefix, mPropertyName) + "() with type " + valueType +
                    " not found on target class " + targetClass);
        }

        return returnVal;
    }

The interesting part is the for loop trying to match any potential typeVariants to our target class. On this specific case, typeVariants contains only one Class object: android.util.PathParser$PathDataNode. The class we're trying to call a method on (targetClass) is our Compat class: android.support.graphics.drawable.VectorDrawableCompat$VFullPath. And the method we're looking for (methodName) is setPathData.

Sadly, VectorDrawableCompat$VFullPath.setPathData's signature doesn't match: public void android.support.graphics.drawable.VectorDrawableCompat$VPath.setPathData(android.support.graphics.drawable.PathParser$PathDataNode[])

As we only have one item in the typeVariants array, returnVal ends being null, and in the end, the ObjectAnimator has absolutely no way to know how to update the path data of our VectorDrawableCompat.

So from where comes the typeVariants content? The android.util.PathParser$PathDataNode instead of the support one? It's because of the way the animation is inflated. AnimatedVectorDrawableCompat, as we saw, is delegating much of the work to the SDK, which is why some things doesn't work on APIs 19 and below. When reading the target node of its XML, the Animator is inflated by the SDK:

Animator objectAnimator = AnimatorInflater.loadAnimator(mContext, id);

The AnimatorInflater comes from the SDK, and is hence inflating a android.util.PathParser$PathDataNode instead of a android.support.graphics.drawable.PathParser$PathDataNode. I guess the only possible fix for this would be for Google to integrate the AnimatorInflater in the support libraries…


So we're in a hard position here. Google admits that the VectorDrawable implementation from SDKs 21-23 contains bugs (I noticed some drawing issues on API 22 on some SVGs), but we can't use everything from the support library either. So, keep in mind that testing on 19 (or below), 21, 22, 23 and 24 (or above) is just mandatory when it comes to VectorDrawables…


Edit: as of today (09/06/2017), Google released support libraries 25.4, which back-ports path-morphing on API 14+. I guess this issue is now automatically solved (I didn't tested it yet).

like image 612
Marc Plano-Lesay Avatar asked Oct 29 '22 08:10

Marc Plano-Lesay


1 Answers

AnimatedVectorDrawableCompat does a version check internally and delegates to the system implementation only if the version is API 24 or above (at time of writing).

As for the reasoning, it seems to be as mentioned in the issue you linked to, to avoid problems with the built in implementation for earlier APIs.

For the most recent one, here's the git commit, which refers to this issue in the issue tracker about rendering problems.

Unfortunately that does mean that fixing some things also removes other features (eg. path morphing). I think the type of approach you use in the question is really the only option at present to get around this.

like image 53
Lewis McGeary Avatar answered Nov 11 '22 14:11

Lewis McGeary