我在我的项目中使用了Android弹簧动画(参见此处)。然而,这些动画妨碍了我的Espresso测试。
我已经尝试使用手机中的开发者选项禁用这些动画,但它们似乎不受这些设置的影响。
是否有任何方法可以仅在测试期间禁用它们?
我在我的项目中使用了Android弹簧动画(参见此处)。然而,这些动画妨碍了我的Espresso测试。
我已经尝试使用手机中的开发者选项禁用这些动画,但它们似乎不受这些设置的影响。
是否有任何方法可以仅在测试期间禁用它们?
在处理由于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() }
}
}
}
SpringAnimation
和FlingAnimation
都继承自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来解决这个问题,但我觉得这种情况很少见,不需要复杂化事情。
另一个基于反射的解决方案,但这个解决方案完全禁用了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)
。