如何在Espresso测试中禁用Spring动画?

4

我在我的项目中使用了Android弹簧动画(参见此处)。然而,这些动画妨碍了我的Espresso测试。

我已经尝试使用手机中的开发者选项禁用这些动画,但它们似乎不受这些设置的影响。

是否有任何方法可以仅在测试期间禁用它们?

1个回答

0

在处理由于SpringAnimations而出现问题的不稳定测试后,我想出了三个解决方案:

解决方案1:添加一个函数来包装创建SpringAnimations

在现有代码中进行更改最具侵入性,但是遵循的复杂度最低:

您可以在运行时检查动画是否已禁用:

 fun animationsDisabled() =
    Settings.Global.getFloat(
            contentResolver,
            Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f,
    ) == 0.0f

然后有选择性地返回一个虚拟动画,该动画立即完成并将值设置为其最终状态:

 fun <K : View?> createAnimation(
    target: K,
    property: FloatPropertyCompat<K>,
    finalValue: Float
) = if (animationsDisabled() == false) {
        SpringAnimation(target, property, finalValue).apply {
            spring.dampingRatio = dampingRatio
            spring.stiffness = stiffness
        }
    } else {
        property.setValue(target, finalValue)
        SpringAnimation(FloatValueHolder(0f)).apply{
            spring = SpringForce(100f)
            spring.dampingRatio = dampingRatio
            spring.stiffness = stiffness
            addUpdateListener { _, _, _ -> skipToEnd() }
        }
   }
}

解决方案2:创建一个IdlingResource,告诉Espresso是否正在运行DynamicAnimation

SpringAnimationFlingAnimation都继承自DynamicAnimation类,该类忽略系统动画比例并在此处引起问题。

这个解决方案并不是最漂亮的,因为它使用了反射,但它所依赖的实现细节自从DynamicAnimation被引入以来就没有改变过。

基于DataBindingIdlingResource

import android.view.View
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.test.espresso.IdlingResource
import androidx.test.ext.junit.rules.ActivityScenarioRule
import java.util.UUID


 // An espresso idling resource implementation that reports idle status for all DynamicAnimation instances
class DynamicAnimationIdlingResource(private val activityScenarioRule: ActivityScenarioRule<*>) :
    IdlingResource {
    // list of registered callbacks
    private val idlingCallbacks = mutableListOf<IdlingResource.ResourceCallback>()

    // give it a unique id to workaround an espresso bug where you cannot register/unregister
    // an idling resource w/ the same name.
    private val id = UUID.randomUUID().toString()

    // holds whether isIdle is called and the result was false. We track this to avoid calling
    // onTransitionToIdle callbacks if Espresso never thought we were idle in the first place.
    private var wasNotIdle = false

    override fun getName() = "DynamicAnimation $id"

    override fun isIdleNow(): Boolean {
        val idle = !getDynamicAnimations().any { it.isRunning }
        @Suppress("LiftReturnOrAssignment")
        if (idle) {
            if (wasNotIdle) {
                // notify observers to avoid espresso race detector
                idlingCallbacks.forEach { it.onTransitionToIdle() }
            }
            wasNotIdle = false
        } else {
            wasNotIdle = true
            activityScenarioRule.scenario.onActivity {
                it.findViewById<View>(android.R.id.content)
                        .postDelayed({ isIdleNow }, 16)
            }
        }

        return idle
    }

    override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
        idlingCallbacks.add(callback)
    }

    /**
     * Find all binding classes in all currently available fragments.
     */
    private fun getDynamicAnimations(): List<DynamicAnimation<*>> {
        val dynamicAnimations = mutableListOf<DynamicAnimation<*>>()
        val animationHandlerClass = Class
                .forName("androidx.dynamicanimation.animation.AnimationHandler")
        val animationHandler =
                animationHandlerClass
                        .getDeclaredMethod("getInstance")
                        .invoke(null)
        val animationCallbacksField =
                animationHandlerClass
                        .getDeclaredField("mAnimationCallbacks").apply {
                            isAccessible = true
                        }

        val animationCallbacks =
                animationCallbacksField.get(animationHandler) as ArrayList<*>
        animationCallbacks.forEach {
            if (it is DynamicAnimation<*>) {
                dynamicAnimations.add(it)
            }
        }
        return dynamicAnimations
    }
}

为方便起见,提供一个匹配测试规则:

/**
 * A JUnit rule that registers an idling resource for all animations that use DynamicAnimations.
 */
class DynamicAnimationIdlingResourceRule(activityScenarioRule: ActivityScenarioRule<*>) : TestWatcher() {
    private val idlingResource = DynamicAnimationIdlingResource(activityScenarioRule)

    override fun finished(description: Description?) {
        IdlingRegistry.getInstance().unregister(idlingResource)
        super.finished(description)
    }

    override fun starting(description: Description?) {
        IdlingRegistry.getInstance().register(idlingResource)
        super.starting(description)
    }
}

这不是完美的解决方案,因为即使全局改变动画比例,它仍然会导致您的测试等待动画。

如果您有基于SpringAnimations(通过将Damping设置为零)的无限动画,则此方法无法正常工作,因为它始终会向Espresso报告动画正在运行。您可以通过将DynamicAnimation转换为SpringAnimation并检查是否设置了Damping来解决这个问题,但我觉得这种情况很少见,不需要复杂化事情。

解决方案3:强制所有SpringAnimations跳到它们的最后一帧

另一个基于反射的解决方案,但这个解决方案完全禁用了SpringAnimations。权衡是理论上Espresso仍然可以在要求SpringAnimation结束和实际结束之间的1帧窗口内尝试交互。

实际上,我不得不连续重新运行测试数百次才能发生这种情况,此时动画可能甚至不是导致不稳定性的原因。因此,如果动画拖慢了测试完成所需的时间,这种权衡可能是值得的:

private fun disableSpringAnimations() {
    val animationHandlerClass = Class
            .forName("androidx.dynamicanimation.animation.AnimationHandler")
    val animationHandler =
            animationHandlerClass
                    .getDeclaredMethod("getInstance")
                    .invoke(null)
    val animationCallbacksField =
            animationHandlerClass
                    .getDeclaredField("mAnimationCallbacks").apply {
                        isAccessible = true
                    }

    CoroutineScope(Dispatchers.IO).launch {
        while (true) {
            withContext(Dispatchers.Main) {
                val animationCallbacks =
                        animationCallbacksField.get(animationHandler) as ArrayList<*>
                animationCallbacks.forEach {
                    val animation = it as? SpringAnimation
                    if (animation?.isRunning == true && animation.canSkipToEnd()) {
                        animation.skipToEnd()
                        animation.doAnimationFrame(100000L)
                    }
                }
            }

            delay(16L)
        }
    }
}

在你的@Before注释函数中调用此方法,以便在每个测试之前运行它。

SpringAnimation实现中,skipToEnd设置了一个标志,直到下一次调用doAnimationFrame才会检查该标志,因此需要调用animation.doAnimationFrame(100000L)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接