Target SDK 升级到 29 AnimatorSet 动画不执行了

525 阅读4分钟

起因

由于项目调整升级 Target Sdk 升级到 30,测试回归没有发现问题正常上线。上线几天后用户反馈,进入直播间看不到红包了,但是点击原有红包的位置还是能弹出红包。这边我们看下伪代码

fun <T> ValueAnimator.getValueAnimatorAndExecute(call: (T) -> Unit) {
    (animatedValue as? T)?.let {
        call.invoke(it)
    }
}

fun doRedPacketAnimatorSet(view:View){
    AnimatorSet().apply {
        playTogether(
            ValueAnimator.ofFloat(0f, 1.1f, 0.8f, 1f).apply {
                duration = 800L
            },ValueAnimator.ofFloat(0f, 1f).apply {
                duration = 800L
                addUpdateListener { animation: ValueAnimator ->
                    animation.getValueAnimatorAndExecute<Float> {
                       // 监听更新做一个渐现动画
                       view.alpha = it
                    }
                }
            })
        doOnStart {
            view.run {
                // 动画执行开始,透明度降为 1
                isVisible = false
                alpha = 0f
            }
        }
        doOnEnd {
            
        }
    }
}

按道理只要执行 update 监听 View 肯定会显示出来,为什么会有用户反馈无法显示?看日志发现 update 没有执行,立马就回调了 doOnEnd

为什么 update 不执行?最近也没做改动,只有 Target Sdk 进行升级为啥动画给干没了呢?

至此我人处于懵逼状态,这是什么情况为啥直接就 onEnd 了,奇怪的是为什么只有小部分手机有这个问题,而大部分手机都是好的?要出问题应该全部都得出问题才对啊?

尝试去复现,debug 环境还复现不出来

c8eeac56-1391-48ec-a1bb-459d39f209c0.jpg

分析原因

没得法子了看源码吧,AnimatorSet 部分源码

public final class AnimatorSet extends Animator implements AnimationHandler.AnimationFrameCallback {
    @Override
    public void start() {
        start(false, true);
    }

    private void start(boolean inReverse, boolean selfPulse) {
        // 省略部分代码
        boolean isEmptySet = isEmptySet(this);
        if (!isEmptySet) {
            startAnimation();
        }
        // 省略部分代码
    }


    private void startAnimation() {
        addAnimationEndListener();

        // Register animation callback
        addAnimationCallback(0);

        // 省略部分代码
    }

    private void addAnimationCallback(long delay) {
        if (!mSelfPulse) {
            return;
        }
        AnimationHandler handler = AnimationHandler.getInstance();
        handler.addAnimationFrameCallback(this, delay);
    }
}

这边是简化的调用链,一目了然。最终 AnimatorSet 调用 AnimationHandler 这个类把自己 add 进去了

public class AnimationHandler {
    private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
        @Override
        public void doFrame(long frameTimeNanos) {
            doAnimationFrame(getProvider().getFrameTime());
            if (mAnimationCallbacks.size() > 0) {
                getProvider().postFrameCallback(this);
            }
        }
    };

    public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
        if (mAnimationCallbacks.size() == 0) {
            getProvider().postFrameCallback(mFrameCallback);
        }
        if (!mAnimationCallbacks.contains(callback)) {
            mAnimationCallbacks.add(callback);
        }
        // 省略部分代码
    }

    private void doAnimationFrame(long frameTime) {
        long currentTime = SystemClock.uptimeMillis();
        final int size = mAnimationCallbacks.size();
        for (int i = 0; i < size; i++) {
            final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
            if (callback == null) {
                continue;
            }
            if (isCallbackDue(callback, currentTime)) {
                // 最终会回调到 AnimatorSet 
                callback.doAnimationFrame(frameTime);
                if (mCommitCallbacks.contains(callback)) {
                    getProvider().postCommitCallback(new Runnable() {
                        @Override
                        public void run() {
                            commitAnimationFrame(callback, getProvider().getFrameTime());
                        }
                    });
                }
            }
        }
        cleanUpList();
    }
}

AnimationHandler 简化后就看起来很清晰了,AnimatorSet 调用 addAnimationFrameCallback 后,AnimationHandlerAnimatorSet 放到了 mAnimationCallbacks 进行维护,并且调用了 getProvider().postFrameCallback(mFrameCallback) 监听屏幕刷新,当屏幕刷新时,执行doAnimationFrame 最终就会回调到 AnimatorSetdoAnimationFrame

重点来了

public final class AnimatorSet extends Animator implements AnimationHandler.AnimationFrameCallback {
    @Override
    public boolean doAnimationFrame(long frameTime) {
        float durationScale = ValueAnimator.getDurationScale();
        if (durationScale == 0f) {
            // Duration scale is 0, end the animation right away.
            forceToEnd();
            return true;
        }
        // 省略部分代码
    }
}


public class ValueAnimator extends Animator implements AnimationHandler.AnimationFrameCallback {
    /**
     * 系统范围的动画比例。
     *
     * 要检查是否启用了 areAnimatorsEnabled()动画,请使用 。
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
    private static float sDurationScale = 1.0f;

    /**
     * 返回基于动画器的动画的系统范围的比例因子。这会影响所有此类动画的开始延迟和持续时间。设置为 0 
     *
     * 将导致动画立即结束。默认值为 1.0f。
     *
     * 返回:持续时间刻度
     */
    @FloatRange(from = 0)
    public static float getDurationScale() {
        return sDurationScale;
    }

    /**
     * 返回系统范围的动画器当前是否已启用。默认情况下,所有动画器都处于启用状态。如果用户将开发人员选项设置为将动画器持续时间比例设置为 0,或者启用电池保存模式(禁用所有动画),则这可能会更改。
     * 开发人员通常不需要调用此方法,但如果应用希望在禁用动画器时显示不同的体验,则可以将此返回值用作要提供的体验的决策程序。
     * 返回:
     * 布尔值 当前是否启用动画器。默认值为 true。
     */
    public static boolean areAnimatorsEnabled() {
        return !(sDurationScale == 0);
    }
}

到这里我们看到 doAnimationFrame 如果从 ValueAnimator.getDurationScale() 就直接给我掉 onEnd 了,额。。。。

终于是找到原因了,sDurationScale 为 0 就直接给我结束了。尼玛

最终和用户确认后确实是有个用户确实是省电模式,但是也有些用户没开过???而且低电量模式有问题,为啥以前老版本没有用户反馈有这个问题???

UnsupportedAppUsage 注意这个,后面会提到

e88dce21-b18a-4b2d-bac0-bcc4f7f138c0.jpg

我又懵逼了

这个时候我同事发来一篇文章 targetSdkVersion 29,部分海外机型无法显示动效

刚好项目里面有用到 SVGA 这个库版本是 2.4.7 的,我们看看 SVGA 做了些啥

open class SVGAImageView : ImageView {
    fun startAnimation() {
        startAnimation(null, false)
    }

    fun startAnimation(range: SVGARange?, reverse: Boolean = false) {
        // 省略部分代码
        drawable.videoItem.let {
            // 省略部分代码
            try {
                val animatorClass = Class.forName("android.animation.ValueAnimator")
                animatorClass?.let {
                    it.getDeclaredField("sDurationScale")?.let {
                        it.isAccessible = true
                        it.getFloat(animatorClass).let {
                            durationScale = it.toDouble()
                        }
                        if (durationScale == 0.0) {
                            it.setFloat(animatorClass, 1.0f)
                            durationScale = 1.0
                            Log.e("SVGAPlayer", "The animation duration scale has been reset to 1.0x, because you closed it on developer options.")
                        }
                    }
                }
            } catch (e: Exception) {}
            // 省略部分代码
        }
    }
}

看到这里就解释为什么 AnimatorSet 老版本为什么用户开了省电模式也没有出问题,刚好也解释了为什么 Target sdk 升级后才出问题,因为 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) 这个注解 sDurationScale 加入到了 受限的灰名单 中,导致反射失效了

Landroid/animation/ValueAnimator;->sDurationScale:F   # Use ValueAnimator.areAnimatorsEnabled() (introduced in API 26) to query whether duration scale = 0. Otherwise, it is intended not to expose impl details such as the actual duration scales to devs. 

SVGA 2.5.14 版本修复了这个问题

open class SVGAImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0)
    : ImageView(context, attrs, defStyleAttr) {


    fun startAnimation() {
        startAnimation(null, false)
    }

    fun startAnimation(range: SVGARange?, reverse: Boolean = false) {
        stopAnimation(false)
        play(range, reverse)
    }

    private fun play(range: SVGARange?, reverse: Boolean) {
        // 省略部分代码
        animator.duration = ((mEndFrame - mStartFrame + 1) * (1000 / videoItem.FPS) / generateScale()).toLong()
        // 省略部分代码
    }
    
    @Suppress("UNNECESSARY_SAFE_CALL")
    private fun generateScale(): Double {
        var scale = 1.0
        try {
            val animatorClass = Class.forName("android.animation.ValueAnimator") ?: return scale
            val getMethod = animatorClass.getDeclaredMethod("getDurationScale") ?: return scale
            scale = (getMethod.invoke(animatorClass) as Float).toDouble()
            if (scale == 0.0) {
                val setMethod = animatorClass.getDeclaredMethod("setDurationScale",Float::class.java) ?: return scale
                setMethod.isAccessible = true
                setMethod.invoke(animatorClass,1.0f)
                scale = 1.0
                LogUtils.info(TAG,
                        "The animation duration scale has been reset to" +
                                " 1.0x, because you closed it on developer options.")
            }
        } catch (ignore: Exception) {
            ignore.printStackTrace()
        }
        return scale
    }

}

可以看到 svga 都是 startAnimation 的时候才会去反射修改 sDurationScale,但是有些时候是不会使用到 svga 就还是会有问题。

解决问题

由于有些时候我们并不会使用 SVGAImageView 只是想使用 AnimatorSet,那么我们就手动反射修改 sDurationScale

fun setAnimatorsEnabled() {
    // 代码就不贴了,`SVGAImageView` 文章里面都有,自己拷贝下
}

OK,我们在 application 中处理下

public abstract class BaseApplication extends Application {

    @SuppressLint("MissingSuperCall")
    @Override
    public void onCreate() {
        setAnimatorsEnabled()
    }
}

心里美滋滋,这次完美了。运行。。。安装。。。测试。。。好,没效果???

我的剧本???怎么肥事???那放到 activity 里面执行下吧

public abstract class BaseActivity extends Application {
    @Override  
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        setAnimatorsEnabled()
    }
}

运行。。。安装。。。测试。。。好了。

到此问题解决了!!!

但是。。。等下一篇吧,累了不写了。