Wire up animation scale in Dev Options with Compose animation
This CL introduced a MotionDurationScale CoroutineContext.Element. This
allows the duration scale to be put in the coroutine context used
by Recomposer. At the same time, user code can define their own
duration scale and add it to a coroutine context. It will impact
all the animations in that coroutine context.
Fixes: 161675988
Test: Included. Also added a benchmark.
RelNote: "Horray! Compose animation now supports
'Animator duration scale' setting from Developer Options."
Change-Id: I31c2425ff560949f24ef873559057e14f6916a39
diff --git a/compose/animation/animation-core/build.gradle b/compose/animation/animation-core/build.gradle
index 4ebb279..465cb92 100644
--- a/compose/animation/animation-core/build.gradle
+++ b/compose/animation/animation-core/build.gradle
@@ -39,7 +39,7 @@
api("androidx.annotation:annotation:1.1.0")
implementation("androidx.compose.runtime:runtime:1.1.0-rc01")
- implementation("androidx.compose.ui:ui:1.0.0")
+ implementation(project(":compose:ui:ui"))
implementation("androidx.compose.ui:ui-unit:1.0.0")
implementation("androidx.compose.ui:ui-util:1.0.0")
implementation(libs.kotlinStdlib)
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/InfiniteTransition.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/InfiniteTransition.kt
index d7b2ae9..065ab85 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/InfiniteTransition.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/InfiniteTransition.kt
@@ -26,8 +26,10 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.unit.Dp
+import kotlinx.coroutines.flow.first
/**
* Creates a [InfiniteTransition] that runs infinite child animations. Child animations can be
@@ -109,6 +111,15 @@
value = animation.getValueFromNanos(playTime)
isFinished = animation.isFinishedFromNanos(playTime)
}
+
+ fun skipToEnd() {
+ value = animation.targetValue
+ startOnTheNextFrame = true
+ }
+
+ fun reset() {
+ startOnTheNextFrame = true
+ }
}
internal val animations = mutableVectorOf<TransitionAnimationState<*, *>>()
@@ -130,18 +141,41 @@
internal fun run() {
if (isRunning || refreshChildNeeded) {
LaunchedEffect(this) {
+ var durationScale = 1f
+ // Restart every time duration scale changes
while (true) {
- withInfiniteAnimationFrameNanos(::onFrame)
+ withInfiniteAnimationFrameNanos {
+ if (startTimeNanos == AnimationConstants.UnspecifiedTime ||
+ durationScale != coroutineContext.durationScale
+ ) {
+ startTimeNanos = it
+ animations.forEach {
+ it.reset()
+ }
+ durationScale = coroutineContext.durationScale
+ }
+ if (durationScale == 0f) {
+ // Finish right away
+ animations.forEach {
+ it.skipToEnd()
+ }
+ } else {
+ val playTimeNanos = ((it - startTimeNanos) / durationScale).toLong()
+ onFrame(playTimeNanos)
+ }
+ }
+ // Suspend until duration scale is non-zero
+ if (durationScale == 0f) {
+ snapshotFlow { coroutineContext.durationScale }.first {
+ it > 0f
+ }
+ }
}
}
}
}
- private fun onFrame(frameTimeNanos: Long) {
- if (startTimeNanos == AnimationConstants.UnspecifiedTime) {
- startTimeNanos = frameTimeNanos
- }
- val playTimeNanos = frameTimeNanos - startTimeNanos
+ private fun onFrame(playTimeNanos: Long) {
var allFinished = true
// Pulse new playtime
animations.forEach {
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/SuspendAnimation.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/SuspendAnimation.kt
index 4ba8a52..32cc7ea 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/SuspendAnimation.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/SuspendAnimation.kt
@@ -17,6 +17,9 @@
package androidx.compose.animation.core
import androidx.compose.runtime.withFrameNanos
+import androidx.compose.ui.MotionDurationScale
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.coroutineContext
import kotlinx.coroutines.CancellationException
/**
@@ -224,6 +227,7 @@
val initialValue = animation.getValueFromNanos(0)
val initialVelocityVector = animation.getVelocityVectorFromNanos(0)
var lateInitScope: AnimationScope<T, V>? = null
+ val durationScale = coroutineContext.durationScale
try {
if (startTimeNanos == AnimationConstants.UnspecifiedTime) {
animation.callWithFrameNanos {
@@ -238,7 +242,7 @@
onCancel = { isRunning = false }
).apply {
// First frame
- doAnimationFrame(it, animation, this@animate, block)
+ doAnimationFrameWithScale(it, durationScale, animation, this@animate, block)
}
}
} else {
@@ -253,13 +257,19 @@
onCancel = { isRunning = false }
).apply {
// First frame
- doAnimationFrame(startTimeNanos, animation, this@animate, block)
+ doAnimationFrameWithScale(
+ startTimeNanos,
+ durationScale,
+ animation,
+ this@animate,
+ block
+ )
}
}
// Subsequent frames
while (lateInitScope!!.isRunning) {
animation.callWithFrameNanos {
- lateInitScope!!.doAnimationFrame(it, animation, this, block)
+ lateInitScope!!.doAnimationFrameWithScale(it, durationScale, animation, this, block)
}
}
// End of animation
@@ -289,6 +299,13 @@
}
}
+internal val CoroutineContext.durationScale: Float
+ get() {
+ val scale = this[MotionDurationScale]?.scaleFactor ?: 1f
+ check(scale >= 0f)
+ return scale
+ }
+
internal fun <T, V : AnimationVector> AnimationScope<T, V>.updateState(
state: AnimationState<T, V>
) {
@@ -299,15 +316,31 @@
state.isRunning = isRunning
}
+private fun <T, V : AnimationVector> AnimationScope<T, V>.doAnimationFrameWithScale(
+ frameTimeNanos: Long,
+ durationScale: Float,
+ anim: Animation<T, V>,
+ state: AnimationState<T, V>,
+ block: AnimationScope<T, V>.() -> Unit
+) {
+ val playTimeNanos =
+ if (durationScale == 0f) {
+ anim.durationNanos
+ } else {
+ ((frameTimeNanos - startTimeNanos) / durationScale).toLong()
+ }
+ doAnimationFrame(frameTimeNanos, playTimeNanos, anim, state, block)
+}
+
// Impl detail, invoked every frame.
private fun <T, V : AnimationVector> AnimationScope<T, V>.doAnimationFrame(
frameTimeNanos: Long,
+ playTimeNanos: Long,
anim: Animation<T, V>,
state: AnimationState<T, V>,
block: AnimationScope<T, V>.() -> Unit
) {
lastFrameTimeNanos = frameTimeNanos
- val playTimeNanos = frameTimeNanos - startTimeNanos
value = anim.getValueFromNanos(playTimeNanos)
velocityVector = anim.getVelocityVectorFromNanos(playTimeNanos)
val isLastFrame = anim.isFinishedFromNanos(playTimeNanos)
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
index 9d038dd..4fbdfb3 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
@@ -280,7 +280,7 @@
maxDurationNanos
}
- internal fun onFrame(frameTimeNanos: Long) {
+ internal fun onFrame(frameTimeNanos: Long, durationScale: Float) {
if (startTimeNanos == AnimationConstants.UnspecifiedTime) {
onTransitionStart(frameTimeNanos)
}
@@ -292,7 +292,7 @@
// Pulse new playtime
_animations.forEach {
if (!it.isFinished) {
- it.onPlayTimeChanged(playTimeNanos)
+ it.onPlayTimeChanged(playTimeNanos, durationScale)
}
// Check isFinished flag again after the animation pulse
if (!it.isFinished) {
@@ -301,7 +301,7 @@
}
_transitions.forEach {
if (it.targetState != it.currentState) {
- it.onFrame(playTimeNanos)
+ it.onFrame(playTimeNanos, durationScale)
}
if (it.targetState != it.currentState) {
allFinished = false
@@ -429,12 +429,13 @@
if (targetState != currentState || isRunning || updateChildrenNeeded) {
LaunchedEffect(this) {
while (true) {
+ val durationScale = coroutineContext.durationScale
withFrameNanos {
// This check is very important, as isSeeking may be changed off-band
// between the last check in composition and this callback which
// happens in the animation callback the next frame.
if (!isSeeking) {
- onFrame(it / AnimationDebugDurationScale)
+ onFrame(it / AnimationDebugDurationScale, durationScale)
}
}
}
@@ -503,8 +504,13 @@
internal val durationNanos
get() = animation.durationNanos
- internal fun onPlayTimeChanged(playTimeNanos: Long) {
- val playTime = playTimeNanos - offsetTimeNanos
+ internal fun onPlayTimeChanged(playTimeNanos: Long, durationScale: Float) {
+ val playTime =
+ if (durationScale == 0f) {
+ animation.durationNanos
+ } else {
+ ((playTimeNanos - offsetTimeNanos) / durationScale).toLong()
+ }
value = animation.getValueFromNanos(playTime)
velocityVector = animation.getVelocityVectorFromNanos(playTime)
if (animation.isFinishedFromNanos(playTime)) {
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/DurationScaleTest.kt b/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/DurationScaleTest.kt
new file mode 100644
index 0000000..f4421da9
--- /dev/null
+++ b/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/DurationScaleTest.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation.core
+
+import androidx.compose.ui.MotionDurationScale
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class DurationScaleTest {
+ @Test
+ fun testAnimatable() = runBlocking {
+ val clock = SuspendAnimationTest.TestFrameClock()
+ withContext(clock + object : MotionDurationScale {
+ override val scaleFactor: Float = 5f
+ }) {
+ var playTime = 0
+ clock.frame(0L)
+ animate(0f, 500f, animationSpec = tween(100, easing = LinearEasing)) { value, _ ->
+ assertEquals(playTime.toFloat(), value)
+ launch {
+ playTime += 10
+ clock.frame(playTime * 1_000_000L)
+ }
+ }
+ }
+
+ withContext(clock + object : MotionDurationScale {
+ override val scaleFactor: Float = 0f
+ }) {
+ clock.frame(0L)
+ animate(0f, 500f, animationSpec = tween(100, easing = LinearEasing)) { value, _ ->
+ // This should finish right away
+ assertEquals(500f, value)
+ }
+ clock.frame(0L)
+ animate(0f, 100f, animationSpec = infiniteRepeatable(tween(100))) { value, _ ->
+ // This should finish right away
+ assertEquals(100f, value)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index d099141..3734d4b 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -139,6 +139,17 @@
method public default <R> R! foldOut(R? initial, kotlin.jvm.functions.Function2<? super androidx.compose.ui.Modifier.Element,? super R,? extends R> operation);
}
+ @androidx.compose.runtime.Stable public interface MotionDurationScale extends kotlin.coroutines.CoroutineContext.Element {
+ method public default kotlin.coroutines.CoroutineContext.Key<?> getKey();
+ method public float getScaleFactor();
+ property public default kotlin.coroutines.CoroutineContext.Key<?> key;
+ property public abstract float scaleFactor;
+ field public static final androidx.compose.ui.MotionDurationScale.Key Key;
+ }
+
+ public static final class MotionDurationScale.Key implements kotlin.coroutines.CoroutineContext.Key<androidx.compose.ui.MotionDurationScale> {
+ }
+
public final class TempListUtilsKt {
}
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index ad70e37..a44bdf2 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -149,6 +149,17 @@
method public default <R> R! foldOut(R? initial, kotlin.jvm.functions.Function2<? super androidx.compose.ui.Modifier.Element,? super R,? extends R> operation);
}
+ @androidx.compose.runtime.Stable public interface MotionDurationScale extends kotlin.coroutines.CoroutineContext.Element {
+ method public default kotlin.coroutines.CoroutineContext.Key<?> getKey();
+ method public float getScaleFactor();
+ property public default kotlin.coroutines.CoroutineContext.Key<?> key;
+ property public abstract float scaleFactor;
+ field public static final androidx.compose.ui.MotionDurationScale.Key Key;
+ }
+
+ public static final class MotionDurationScale.Key implements kotlin.coroutines.CoroutineContext.Key<androidx.compose.ui.MotionDurationScale> {
+ }
+
public final class TempListUtilsKt {
}
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index bbd3b88..1d57f2d 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -139,6 +139,17 @@
method public default <R> R! foldOut(R? initial, kotlin.jvm.functions.Function2<? super androidx.compose.ui.Modifier.Element,? super R,? extends R> operation);
}
+ @androidx.compose.runtime.Stable public interface MotionDurationScale extends kotlin.coroutines.CoroutineContext.Element {
+ method public default kotlin.coroutines.CoroutineContext.Key<?> getKey();
+ method public float getScaleFactor();
+ property public default kotlin.coroutines.CoroutineContext.Key<?> key;
+ property public abstract float scaleFactor;
+ field public static final androidx.compose.ui.MotionDurationScale.Key Key;
+ }
+
+ public static final class MotionDurationScale.Key implements kotlin.coroutines.CoroutineContext.Key<androidx.compose.ui.MotionDurationScale> {
+ }
+
public final class TempListUtilsKt {
}
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/LifecycleAwareWindowRecomposerBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/LifecycleAwareWindowRecomposerBenchmark.kt
new file mode 100644
index 0000000..ef060ab
--- /dev/null
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/LifecycleAwareWindowRecomposerBenchmark.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.benchmark
+
+import android.view.View
+import android.view.ViewGroup
+import androidx.activity.ComponentActivity
+import androidx.benchmark.junit4.BenchmarkRule
+import androidx.benchmark.junit4.measureRepeated
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.platform.createLifecycleAwareWindowRecomposer
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.LifecycleObserver
+import androidx.test.annotation.UiThreadTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.RuleChain
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runner.RunWith
+import org.junit.runners.model.Statement
+
+@LargeTest
+@OptIn(ExperimentalComposeUiApi::class)
+@RunWith(AndroidJUnit4::class)
+class LifecycleAwareWindowRecomposerBenchmark {
+
+ @get:Rule
+ val rule = CombinedActivityBenchmarkRule()
+
+ @Test
+ @UiThreadTest
+ fun createRecomposer() {
+ val rootView = rule.activityTestRule.activity.window.decorView.rootView
+ val lifecycle = object : Lifecycle() {
+ override fun addObserver(observer: LifecycleObserver) {
+ if (observer is LifecycleEventObserver) {
+ observer.onStateChanged({ this }, Event.ON_CREATE)
+ }
+ }
+
+ override fun removeObserver(observer: LifecycleObserver) {}
+ override fun getCurrentState(): State = State.CREATED
+ }
+ var view: View? = null
+ rule.benchmarkRule.measureRepeated {
+ runWithTimingDisabled {
+ view = View(rule.activityTestRule.activity)
+ (rootView as ViewGroup).addView(view)
+ }
+ view!!.createLifecycleAwareWindowRecomposer(lifecycle = lifecycle)
+ runWithTimingDisabled {
+ (rootView as ViewGroup).removeAllViews()
+ view = null
+ }
+ }
+ }
+
+ class CombinedActivityBenchmarkRule() : TestRule {
+ @Suppress("DEPRECATION")
+ val activityTestRule =
+ androidx.test.rule.ActivityTestRule(ComponentActivity::class.java)
+
+ val benchmarkRule = BenchmarkRule()
+
+ override fun apply(base: Statement, description: Description?): Statement {
+ return RuleChain.outerRule(benchmarkRule)
+ .around(activityTestRule)
+ .apply(base, description)
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/WindowRecomposer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/WindowRecomposer.android.kt
index aa910b5..2b9c372 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/WindowRecomposer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/WindowRecomposer.android.kt
@@ -16,29 +16,36 @@
package androidx.compose.ui.platform
+import android.database.ContentObserver
+import android.net.Uri
+import android.provider.Settings
import android.view.View
import android.view.ViewParent
import androidx.compose.runtime.CompositionContext
import androidx.compose.runtime.MonotonicFrameClock
import androidx.compose.runtime.PausableMonotonicFrameClock
import androidx.compose.runtime.Recomposer
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.InternalComposeUiApi
+import androidx.compose.ui.MotionDurationScale
import androidx.compose.ui.R
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewTreeLifecycleOwner
+import java.util.concurrent.atomic.AtomicReference
+import kotlin.coroutines.ContinuationInterceptor
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.launch
-import java.util.concurrent.atomic.AtomicReference
-import kotlin.coroutines.ContinuationInterceptor
-import kotlin.coroutines.CoroutineContext
-import kotlin.coroutines.EmptyCoroutineContext
/**
* The [CompositionContext] that should be used as a parent for compositions at or below
@@ -263,18 +270,49 @@
// Only access AndroidUiDispatcher.CurrentThread if we would use an element from it,
// otherwise prevent lazy initialization.
val baseContext = if (coroutineContext[ContinuationInterceptor] == null ||
- coroutineContext[MonotonicFrameClock] == null) {
- AndroidUiDispatcher.CurrentThread + coroutineContext
+ coroutineContext[MonotonicFrameClock] == null
+ ) {
+ AndroidUiDispatcher.CurrentThread + coroutineContext
} else coroutineContext
val pausableClock = baseContext[MonotonicFrameClock]?.let {
PausableMonotonicFrameClock(it).apply { pause() }
}
- val contextWithClock = baseContext + (pausableClock ?: EmptyCoroutineContext)
- val recomposer = Recomposer(contextWithClock)
- val runRecomposeScope = CoroutineScope(contextWithClock)
- val viewTreeLifecycle = checkNotNull(lifecycle ?: ViewTreeLifecycleOwner.get(this)?.lifecycle) {
- "ViewTreeLifecycleOwner not found from $this"
+
+ var systemDurationScaleConsumer: MotionDurationScaleImpl? = null
+ val motionDurationScale = baseContext[MotionDurationScale] ?: MotionDurationScaleImpl().also {
+ systemDurationScaleConsumer = it
}
+ val animationScaleUri =
+ Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE)
+ val contentObserver = systemDurationScaleConsumer?.run {
+ object : ContentObserver(handler) {
+ override fun onChange(selfChange: Boolean, uri: Uri?) {
+ if (animationScaleUri.equals(uri)) {
+ scaleFactor = Settings.Global.getFloat(
+ context.contentResolver,
+ Settings.Global.ANIMATOR_DURATION_SCALE,
+ 1f
+ )
+ }
+ }
+ }
+ }
+ systemDurationScaleConsumer?.run {
+ scaleFactor = Settings.Global.getFloat(
+ context.contentResolver,
+ Settings.Global.ANIMATOR_DURATION_SCALE,
+ 1f
+ )
+ }
+
+ val contextWithClockAndMotionScale =
+ baseContext + (pausableClock ?: EmptyCoroutineContext) + motionDurationScale
+ val recomposer = Recomposer(contextWithClockAndMotionScale)
+ val runRecomposeScope = CoroutineScope(contextWithClockAndMotionScale)
+ val viewTreeLifecycle =
+ checkNotNull(lifecycle ?: ViewTreeLifecycleOwner.get(this)?.lifecycle) {
+ "ViewTreeLifecycleOwner not found from $this"
+ }
// Removing the view holding the ViewTreeRecomposer means we may never be reattached again.
// Since this factory function is used to create a new recomposer for each invocation and
// doesn't reuse a single instance like other factories might, shut it down whenever it
@@ -285,15 +323,26 @@
override fun onViewDetachedFromWindow(v: View?) {
removeOnAttachStateChangeListener(this)
recomposer.cancel()
+ contentObserver?.run {
+ context.contentResolver.unregisterContentObserver(this)
+ }
}
}
)
viewTreeLifecycle.addObserver(
object : LifecycleEventObserver {
- override fun onStateChanged(lifecycleOwner: LifecycleOwner, event: Lifecycle.Event) {
+ override fun onStateChanged(
+ lifecycleOwner: LifecycleOwner,
+ event: Lifecycle.Event
+ ) {
val self = this
when (event) {
- Lifecycle.Event.ON_CREATE ->
+ Lifecycle.Event.ON_CREATE -> {
+ contentObserver?.let {
+ context.contentResolver.registerContentObserver(
+ animationScaleUri, false, it
+ )
+ }
// Undispatched launch since we've configured this scope
// to be on the UI thread
runRecomposeScope.launch(start = CoroutineStart.UNDISPATCHED) {
@@ -306,10 +355,14 @@
lifecycleOwner.lifecycle.removeObserver(self)
}
}
+ }
Lifecycle.Event.ON_START -> pausableClock?.resume()
Lifecycle.Event.ON_STOP -> pausableClock?.pause()
Lifecycle.Event.ON_DESTROY -> {
recomposer.cancel()
+ contentObserver?.run {
+ context.contentResolver.unregisterContentObserver(this)
+ }
}
Lifecycle.Event.ON_PAUSE -> {
// Nothing
@@ -326,3 +379,7 @@
)
return recomposer
}
+
+private class MotionDurationScaleImpl : MotionDurationScale {
+ override var scaleFactor by mutableStateOf(1f)
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/MotionDurationScale.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/MotionDurationScale.kt
new file mode 100644
index 0000000..1820f8f
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/MotionDurationScale.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui
+
+import androidx.compose.runtime.Stable
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * Provides a duration scale for motion such as animations. When the duration [scaleFactor] is 0,
+ * the motion will end in the next frame callback. Otherwise, the duration [scaleFactor] will be
+ * used as a multiplier to scale the duration of the motion. The larger the scale, the longer the
+ * motion will take to finish, and therefore the slower it will be perceived.
+ */
+@Stable
+interface MotionDurationScale : CoroutineContext.Element {
+ /**
+ * Defines the multiplier for the duration of the motion. This value should be non-negative.
+ *
+ * 0f would cause motion to finish in the next frame callback. Larger [scaleFactor] will result
+ * in longer durations for the motion/animation (i.e. slower animation).
+ */
+ val scaleFactor: Float
+
+ override val key: CoroutineContext.Key<*> get() = Key
+
+ companion object Key : CoroutineContext.Key<MotionDurationScale>
+}