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.
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 VectorDrawable
s…
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).
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.
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