即使在 API 22 设备上也使用兼容库的动画矢量绘图

Animated Vector Drawable using compat library even on API 22 device

我使用路径变形编写了一个动画矢量可绘制对象(仅适用于 API 21 及更高版本)。对于 21 以下的 API,我有一个使用简单旋转的后备动画。我正在使用动画矢量可绘制支持库 (com.android.support:animated-vector-drawable:25.3.1)。

这是我开始动画的方式:

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();
}

这在 API 19 和 24 上工作正常,但是 在 API 22 或 23 上 不起作用(我没有 API 21 台设备进行测试).

API 19 个案例是合乎逻辑的:动画很简单,由支持库完美处理,它可以工作。太好了。

我希望任何 API 21 及以上的设备都能选择 内置 矢量可绘制对象实现。但是,在调试时,我可以看到 animation 实际上是 AnimatedVectorDrawableCompat 的一个实例:因此,它不支持路径变形,动画也不起作用。

那么为什么它在 API 24 上有效?嗯,animationAnimatedVectorDrawable 的一个实例。因此,路径变形工作正常。

所以我的问题是:为什么API 21-23 设备不采用内置实现,并依赖于支持库,而一个API 24 台设备 可以接吗?



关于以下内容

这些是我在调查此问题的技术解释时记下的一些笔记,我认为可以分享这些笔记。接受的答案中总结了有趣的、技术含量较低的部分。


这是我使用的AVD,供参考:

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

以及两个路径资源:

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

在 API 22 设备上,内置版本和支持版本 (25.3.1) 似乎从我上面的 AVD 中膨胀相同的 Animator,尽管具有不同的层次结构。

在支持版本 (25.3.1) 中,AnimatorSet 只有一个节点:一个 AnimatorSet 本身包含一个动画,看起来与 AVD 中描述的 ObjectAnimator 相匹配XML。它的 referent 设置为 VectorDrawableCompat,属性 名称是正确的 pathData,值列表包含一个 PropertyValuesHolder 和两个关键帧,匹配我的开始和结束路径。结果:不起作用

对于内置版本(SDK 22),它并不完全相同(但是 AnimatorSet 不完全在同一个地方,所以......):在 AnimatedVectorDrawableStatemAnimators 列表有 1 个元素,直接是 ObjectAnimator(与支持版本具有相同的值)。结果:有效.

我能看到的唯一相关区别是 PropertyValuesHolder 中的 ValueAnimator。因为它有一些对可绘制对象的引用,我猜它 可能 有一些类型检查忽略 VectorDrawable class 的支持库版本。但这纯粹是猜测。我会继续挖掘…


我终于明白了(并接受了@LewisMcGeary 的回答,因为我在这个问题中没有提到我正在寻找问题背后的技术细节)。这是发生了什么。如前所述,在 APIs 21-23,支持库将接管 SDK 的实现,以避免上述实现中的错误。所以我们使用 AnimatedVectorDrawableCompat 和其他 [whatever]Compat classes。矢量本身加载完成后,就轮到动画了。

动画委托给 SDK 的 ObjectAnimator,无论我们处于 API 级别(至少在 21+,但我想在 19 及以下都是一样的)。为了给基本类型设置动画,ObjectAnimator 有一个内部函数映射来调用以更改值。但是,对于复杂类型,它依赖于特定的方法签名,必须 出现在动画对象上。这是将值类型映射到要调用的相应方法的方法,来自 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;
    }

有趣的部分是 for 循环试图将任何潜在的 typeVariants 与我们的目标 class 相匹配。在这种特定情况下,typeVariants 仅包含一个 Class 对象:android.util.PathParser$PathDataNode。我们试图在 (targetClass) 上调用方法的 class 是我们的 Compat class: android.support.graphics.drawable.VectorDrawableCompat$VFullPath。而我们正在寻找的方法 (methodName) 是 setPathData.

遗憾的是,VectorDrawableCompat$VFullPath.setPathData 的签名不匹配:public void android.support.graphics.drawable.VectorDrawableCompat$VPath.setPathData(android.support.graphics.drawable.PathParser$PathDataNode[])

因为我们在typeVariants数组中只有一项,所以returnVal最终是null,最后ObjectAnimator根本无从知晓如何更新我们的 VectorDrawableCompat.

的路径数据

那么 typeVariants 内容从何而来? android.util.PathParser$PathDataNode 而不是支持一个?这是因为动画膨胀的方式。正如我们所见,AnimatedVectorDrawableCompat 将大部分工作委派给 SDK,这就是为什么某些功能在 APIs 19 及以下版本上不起作用的原因。在读取其XML的target节点时,Animator被SDK膨胀:

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

AnimatorInflater 来自 SDK,因此膨胀 android.util.PathParser$PathDataNode 而不是 android.support.graphics.drawable.PathParser$PathDataNode。我想唯一可能的解决方法是 Google 将 AnimatorInflater 集成到支持库中……


所以我们在这里处境艰难。 Google 承认 SDK 21-23 的 VectorDrawable 实现包含错误(我注意到 API 22 在某些 SVG 上存在一些绘图问题),但我们不能使用支持库中的所有内容任何一个。因此,请记住,在 VectorDrawables 时,对 19(或以下)、21、22、23 24(或以上)的测试是强制性的……


编辑:截至今天 (09/06/2017),Google 发布了支持库 25.4,它向后移植了 API 14+ 上的路径变形。我想这个问题现在已经自动解决了(我还没有测试过)。

AnimatedVectorDrawableCompat 在内部进行版本检查,仅当版本为 API 24 或更高版本(在撰写本文时)时才委托给系统实现。

至于推理,它似乎与您链接到的问题中提到的一样,以避免早期 APIs 的内置实现出现问题。

对于最近的一个,here's the git commit, which refers to this issue in the issue tracker关于渲染问题。

不幸的是,这确实意味着修复某些东西也会删除其他功能(例如路径变形)。我认为您在问题中使用的方法类型确实是目前解决此问题的唯一选择。