diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 3971093..c299214 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -15,11 +15,6 @@ jobs: uses: actions/setup-java@v1 with: java-version: 1.8 - - uses: actions/cache@v1 - with: - path: ~/.gradle/caches - key: gradle-${{ runner.os }}-${{ hashFiles('buildSrc/**') }}-${{ hashFiles('**/*.gradle*') }} - restore-keys: gradle-${{ runner.os }}- - run: ./gradlew build ui-tests: @@ -40,11 +35,6 @@ jobs: uses: actions/setup-java@v1 with: java-version: 1.8 - - uses: actions/cache@v1 - with: - path: ~/.gradle/caches - key: gradle-${{ runner.os }}-${{ hashFiles('buildSrc/**') }}-${{ hashFiles('**/*.gradle*') }} - restore-keys: gradle-${{ runner.os }}- - uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} diff --git a/build.gradle.kts b/build.gradle.kts index 89fee16..4464a7e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -42,9 +42,7 @@ subprojects { apiVersion = "1.3" freeCompilerArgs = listOf( - "-Xopt-in=kotlin.RequiresOptIn", - "-Xallow-jvm-ir-dependencies", - "-Xskip-prerelease-check" + "-Xopt-in=kotlin.RequiresOptIn" ) } } diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index b440f22..876fbee 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -15,33 +15,21 @@ object Dependencies { val androidGradlePlugin = "com.android.tools.build:gradle:${Versions.agp}" object AndroidX { - const val activity = "androidx.activity:activity:1.1.0" - const val annotations = "androidx.annotation:annotation:1.1.0" - const val appcompat = "androidx.appcompat:appcompat:1.1.0" - const val constraintLayout = "androidx.constraintlayout:constraintlayout:1.1.3" - const val fragment = "androidx.fragment:fragment:1.2.2" + const val appcompat = "androidx.appcompat:appcompat:1.3.0-beta01" // Note that we're not using the actual androidx material dep yet, it's still alpha. const val material = "com.google.android.material:material:1.1.0" - const val recyclerview = "androidx.recyclerview:recyclerview:1.1.0" - - // Note that we are *not* using lifecycle-viewmodel-savedstate, which at this - // writing is still in beta and still fixing bad bugs. Probably we'll never bother to, - // it doesn't really add value for us. - const val savedstate = "androidx.savedstate:savedstate:1.0.0" - const val transition = "androidx.transition:transition:1.3.1" - const val viewbinding = "androidx.databinding:viewbinding:3.6.1" const val junitExt = "androidx.test.ext:junit:1.1.1" } object Compose { + const val activity = "androidx.activity:activity-compose:1.3.0-alpha02" val foundation = "androidx.compose.foundation:foundation:${Versions.compose}" val icons = "androidx.compose.material:material-icons-extended:${Versions.compose}" val material = "androidx.compose.material:material:${Versions.compose}" - val savedstate = "androidx.compose.runtime:runtime-saved-instance-state:${Versions.compose}" - val test = "androidx.ui:ui-test:${Versions.compose}" - val tooling = "androidx.ui:ui-tooling:${Versions.compose}" + val test = "androidx.compose.ui:ui-test-junit4:${Versions.compose}" + val tooling = "androidx.compose.ui:ui-tooling:${Versions.compose}" val util = "androidx.compose.ui:ui-util:${Versions.compose}" } @@ -50,13 +38,9 @@ object Dependencies { "org.jetbrains.kotlinx:binary-compatibility-validator:0.2.1" val dokka = "org.jetbrains.dokka:dokka-gradle-plugin:${Versions.dokka}" val gradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}" - val reflect = "org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlin}" object Test { const val common = "org.jetbrains.kotlin:kotlin-test-common" - const val annotations = "org.jetbrains.kotlin:kotlin-test-annotations-common" - const val jdk = "org.jetbrains.kotlin:kotlin-test-junit" - const val mockito = "com.nhaarman:mockito-kotlin-kt1.1:1.6.0" } } diff --git a/buildSrc/src/main/java/default-android-config.gradle.kts b/buildSrc/src/main/java/default-android-config.gradle.kts index 69dea79..2ddc85d 100644 --- a/buildSrc/src/main/java/default-android-config.gradle.kts +++ b/buildSrc/src/main/java/default-android-config.gradle.kts @@ -29,4 +29,9 @@ configure { kotlinCompilerVersion = Versions.kotlin kotlinCompilerExtensionVersion = Versions.compose } + + packagingOptions { + excludes += "META-INF/AL2.0" + excludes += "META-INF/LGPL2.1" + } } diff --git a/buildSrc/src/main/resources/versions.properties b/buildSrc/src/main/resources/versions.properties index 4bef4a4..4a2630c 100644 --- a/buildSrc/src/main/resources/versions.properties +++ b/buildSrc/src/main/resources/versions.properties @@ -2,6 +2,6 @@ releaseVersion=0.7.0 androidGradlePluginVersion=4.2.0-alpha15 -kotlinVersion=1.4.10 +kotlinVersion=1.4.31 dokkaVersion=1.4.0 -composeDevVersion=alpha05 +composeDevVersion=beta02 diff --git a/compose-backstack-viewer/build.gradle.kts b/compose-backstack-viewer/build.gradle.kts index 2e6bd4f..6e55df2 100644 --- a/compose-backstack-viewer/build.gradle.kts +++ b/compose-backstack-viewer/build.gradle.kts @@ -22,7 +22,6 @@ dependencies { implementation(Dependencies.Compose.icons) implementation(Dependencies.Compose.foundation) implementation(Dependencies.Compose.material) - implementation(Dependencies.Compose.savedstate) implementation(Dependencies.Compose.tooling) testImplementation(Dependencies.Test.junit) diff --git a/compose-backstack-viewer/src/androidTest/java/com/zachklipp/compose/backstack/viewer/BackstackViewerTest.kt b/compose-backstack-viewer/src/androidTest/java/com/zachklipp/compose/backstack/viewer/BackstackViewerTest.kt index 827217c..4a8b5bf 100644 --- a/compose-backstack-viewer/src/androidTest/java/com/zachklipp/compose/backstack/viewer/BackstackViewerTest.kt +++ b/compose-backstack-viewer/src/androidTest/java/com/zachklipp/compose/backstack/viewer/BackstackViewerTest.kt @@ -1,15 +1,14 @@ package com.zachklipp.compose.backstack.viewer +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotSelected +import androidx.compose.ui.test.assertIsSelected +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.ui.test.assertHasClickAction -import androidx.ui.test.assertIsDisplayed -import androidx.ui.test.assertIsNotSelected -import androidx.ui.test.assertIsSelected -import androidx.ui.test.createComposeRule -import androidx.ui.test.onNodeWithSubstring -import androidx.ui.test.onNodeWithTag -import androidx.ui.test.onNodeWithText -import androidx.ui.test.performClick import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -27,14 +26,14 @@ class BackstackViewerTest { } compose.onNodeWithText("Slide Transition").assertIsDisplayed() - compose.onNodeWithSubstring("Slow animations").assertIsDisplayed() + compose.onNodeWithText("Slow animations", substring = true).assertIsDisplayed() compose.onNodeWithText("one").assertIsSelected() compose.onNodeWithText("one, two").assertIsNotSelected() compose.onNodeWithText("one, two, three").assertIsNotSelected() compose.onNodeWithText("Screen one").assertIsDisplayed() - compose.onNodeWithSubstring("Counter:").assertIsDisplayed() + compose.onNodeWithText("Counter:", substring = true).assertIsDisplayed() } @Test diff --git a/compose-backstack-viewer/src/main/java/com/zachklipp/compose/backstack/viewer/AppModel.kt b/compose-backstack-viewer/src/main/java/com/zachklipp/compose/backstack/viewer/AppModel.kt index bb06d22..24f0f3f 100644 --- a/compose-backstack-viewer/src/main/java/com/zachklipp/compose/backstack/viewer/AppModel.kt +++ b/compose-backstack-viewer/src/main/java/com/zachklipp/compose/backstack/viewer/AppModel.kt @@ -4,8 +4,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.savedinstancestate.listSaver -import androidx.compose.runtime.savedinstancestate.rememberSavedInstanceState +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import com.zachklipp.compose.backstack.BackstackTransition @@ -51,14 +51,14 @@ internal class AppModel private constructor( companion object { /** - * Creates an instance of [AppModel] and saves it using [rememberSavedInstanceState]. + * Creates an instance of [AppModel] and saves it using [rememberSaveable]. */ @Composable fun create( namedTransitions: List>, prefabBackstacks: List> ): AppModel { - return rememberSavedInstanceState(saver = saver(namedTransitions, prefabBackstacks)) { + return rememberSaveable(saver = saver(namedTransitions, prefabBackstacks)) { AppModel(namedTransitions, prefabBackstacks) } } diff --git a/compose-backstack-viewer/src/main/java/com/zachklipp/compose/backstack/viewer/AppScreen.kt b/compose-backstack-viewer/src/main/java/com/zachklipp/compose/backstack/viewer/AppScreen.kt index 5871b00..b0d2669 100644 --- a/compose-backstack-viewer/src/main/java/com/zachklipp/compose/backstack/viewer/AppScreen.kt +++ b/compose-backstack-viewer/src/main/java/com/zachklipp/compose/backstack/viewer/AppScreen.kt @@ -3,24 +3,24 @@ package com.zachklipp.compose.backstack.viewer import android.os.Handler -import androidx.compose.foundation.Icon -import androidx.compose.foundation.Text import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material.FloatingActionButton +import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.Scaffold +import androidx.compose.material.Text import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Menu import androidx.compose.runtime.* -import androidx.compose.runtime.savedinstancestate.savedInstanceState +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag -import androidx.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.Preview internal fun addTestTag(screen: String) = "add screen to $screen" internal fun backTestTag(screen: String) = "go back from $screen" @@ -44,35 +44,36 @@ internal fun AppScreen( TopAppBar( navigationIcon = { IconButton(onClick = onBack, modifier = Modifier.testTag(backTestTag(name))) { - Icon(navigationIcon) + Icon(navigationIcon, contentDescription = "Back") } }, title = { Text("Screen $name") }) }, floatingActionButton = { FloatingActionButton(onClick = onAdd, modifier = Modifier.testTag(addTestTag(name))) { - Icon(Icons.Default.Add) + Icon(Icons.Default.Add, contentDescription = "Add screen") } } ) { Text( - text = "Counter: ${Counter(200)}", - modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center) + text = "Counter: ${rememberCounter(200)}", + modifier = Modifier + .fillMaxSize() + .wrapContentSize(Alignment.Center) ) } } @Suppress("SameParameterValue") @Composable -private fun Counter(periodMs: Long): Int = key(periodMs) { +private fun rememberCounter(periodMs: Long): Int = key(periodMs) { // If the screen is temporarily removed from the composition, the counter will effectively // be "paused": it will stop incrementing, but will resume from its last value when restored to // the composition. - var value by savedInstanceState { 0 } - onActive { + var value by rememberSaveable { mutableStateOf(0) } + DisposableEffect(periodMs) { val mainHandler = Handler() var disposed = false - onDispose { disposed = true } fun schedule() { mainHandler.postDelayed({ value++ @@ -80,6 +81,7 @@ private fun Counter(periodMs: Long): Int = key(periodMs) { }, periodMs) } schedule() + onDispose { disposed = true } } return@key value } diff --git a/compose-backstack-viewer/src/main/java/com/zachklipp/compose/backstack/viewer/BackstackViewerApp.kt b/compose-backstack-viewer/src/main/java/com/zachklipp/compose/backstack/viewer/BackstackViewerApp.kt index 9294199..c32331c 100644 --- a/compose-backstack-viewer/src/main/java/com/zachklipp/compose/backstack/viewer/BackstackViewerApp.kt +++ b/compose-backstack-viewer/src/main/java/com/zachklipp/compose/backstack/viewer/BackstackViewerApp.kt @@ -5,8 +5,6 @@ import android.content.ContextWrapper import androidx.activity.ComponentActivity import androidx.activity.OnBackPressedCallback import androidx.compose.animation.core.TweenSpec -import androidx.compose.foundation.AmbientTextStyle -import androidx.compose.foundation.Text import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -14,26 +12,32 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.preferredHeight import androidx.compose.foundation.selection.selectable +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ListItem +import androidx.compose.material.LocalTextStyle import androidx.compose.material.MaterialTheme import androidx.compose.material.RadioButton import androidx.compose.material.Surface import androidx.compose.material.Switch +import androidx.compose.material.Text import androidx.compose.material.darkColors import androidx.compose.material.lightColors import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.key -import androidx.compose.runtime.onCommit import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.ContextAmbient +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.text +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.ui.tooling.preview.Preview import com.zachklipp.compose.backstack.Backstack import com.zachklipp.compose.backstack.BackstackTransition import com.zachklipp.compose.backstack.BackstackTransition.Crossfade @@ -91,15 +95,20 @@ fun BackstackViewerApp( MaterialTheme(colors = darkColors()) { Surface { - Column(modifier = Modifier.padding(16.dp).fillMaxSize()) { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxSize() + ) { AppControls(model) - Spacer(Modifier.preferredHeight(24.dp)) + Spacer(Modifier.height(24.dp)) AppScreens(model) } } } } +@OptIn(ExperimentalMaterialApi::class) @Composable private fun AppControls(model: AppModel) { Spinner( @@ -142,20 +151,22 @@ private fun AppScreens(model: AppModel) { MaterialTheme(colors = lightColors()) { InspectionGestureDetector(enabled = model.inspectionEnabled) { inspectionParams -> Backstack( - backstack = model.currentBackstack, - transition = model.selectedTransition.second, - animationBuilder = animation, - modifier = Modifier.fillMaxSize().border(width = 3.dp, color = Color.Red), - inspectionParams = inspectionParams, - onTransitionStarting = { from, to, direction -> - println( - """ - Transitioning $direction: - from: $from - to: $to - """.trimIndent() - ) - }, + backstack = model.currentBackstack, + transition = model.selectedTransition.second, + animationBuilder = animation, + modifier = Modifier + .fillMaxSize() + .border(width = 3.dp, color = Color.Red), + inspectionParams = inspectionParams, + onTransitionStarting = { from, to, direction -> + println( + """ + Transitioning $direction: + from: $from + to: $to + """.trimIndent() + ) + }, onTransitionFinished = { println("Transition finished.") } ) { screen -> AppScreen( @@ -176,17 +187,25 @@ private fun RadioButton( onSelect: () -> Unit ) { Box( - modifier = Modifier.selectable( - selected = selected, - onClick = { if (!selected) onSelect() } - ), - children = { + modifier = Modifier + .selectable( + selected = selected, + onClick = { if (!selected) onSelect() } + ) + .semantics { + this.text = buildAnnotatedString { append(text) } + }, + content = { Box { - Row(Modifier.fillMaxWidth().padding(16.dp)) { + Row( + Modifier + .fillMaxWidth() + .padding(16.dp) + ) { RadioButton(selected = selected, onClick = onSelect) Text( text = text, - style = MaterialTheme.typography.body1.merge(other = AmbientTextStyle.current), + style = MaterialTheme.typography.body1.merge(other = LocalTextStyle.current), modifier = Modifier.padding(start = 16.dp) ) } @@ -197,9 +216,9 @@ private fun RadioButton( @Composable private fun OnBackPressed(onPressed: () -> Unit) { - val context = ContextAmbient.current - onCommit(context, onPressed) { - val activity = context.findComponentActivity() ?: return@onCommit + val context = LocalContext.current + DisposableEffect(context, onPressed) { + val activity = context.findComponentActivity() ?: return@DisposableEffect onDispose {} val callback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { onPressed() diff --git a/compose-backstack-viewer/src/main/java/com/zachklipp/compose/backstack/viewer/Spinner.kt b/compose-backstack-viewer/src/main/java/com/zachklipp/compose/backstack/viewer/Spinner.kt index 64339ed..92c95c0 100644 --- a/compose-backstack-viewer/src/main/java/com/zachklipp/compose/backstack/viewer/Spinner.kt +++ b/compose-backstack-viewer/src/main/java/com/zachklipp/compose/backstack/viewer/Spinner.kt @@ -3,16 +3,16 @@ package com.zachklipp.compose.backstack.viewer -import androidx.compose.foundation.Icon -import androidx.compose.foundation.Text import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.preferredWidth +import androidx.compose.foundation.layout.width +import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface +import androidx.compose.material.Text import androidx.compose.material.darkColors import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDropDown @@ -24,9 +24,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog -import androidx.ui.tooling.preview.Preview @Preview @Composable @@ -64,12 +64,17 @@ internal fun Spinner( // Always draw the selected item. Row(Modifier.clickable(onClick = { isOpen = !isOpen })) { - Box(modifier = Modifier.weight(1f).align(Alignment.CenterVertically)) { + Box(modifier = Modifier + .weight(1f) + .align(Alignment.CenterVertically)) { drawItem(selectedItem) } Icon( - Icons.Default.ArrowDropDown, - modifier = Modifier.preferredWidth(48.dp).aspectRatio(1f) + Icons.Default.ArrowDropDown, + contentDescription = "Open spinner", + modifier = Modifier + .width(48.dp) + .aspectRatio(1f) ) } diff --git a/compose-backstack/build.gradle.kts b/compose-backstack/build.gradle.kts index 90f4780..4f4a2bf 100644 --- a/compose-backstack/build.gradle.kts +++ b/compose-backstack/build.gradle.kts @@ -10,7 +10,6 @@ dependencies { compileOnly(Dependencies.Compose.tooling) implementation(Dependencies.Compose.foundation) - implementation(Dependencies.Compose.savedstate) testImplementation(Dependencies.Test.junit) testImplementation(Dependencies.Test.truth) diff --git a/compose-backstack/src/androidTest/java/com/zachklipp/compose/backstack/BackstackComposableTest.kt b/compose-backstack/src/androidTest/java/com/zachklipp/compose/backstack/BackstackComposableTest.kt index 0915a4a..fb329be 100644 --- a/compose-backstack/src/androidTest/java/com/zachklipp/compose/backstack/BackstackComposableTest.kt +++ b/compose-backstack/src/androidTest/java/com/zachklipp/compose/backstack/BackstackComposableTest.kt @@ -1,18 +1,14 @@ package com.zachklipp.compose.backstack -import androidx.compose.animation.core.ManualAnimationClock import androidx.compose.animation.core.TweenSpec -import androidx.compose.foundation.Text -import androidx.compose.runtime.Providers +import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.AnimationClockAmbient +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.ui.test.assertIsDisplayed -import androidx.ui.test.createComposeRule -import androidx.ui.test.onNodeWithText -import androidx.ui.test.runOnUiThread import com.zachklipp.compose.backstack.BackstackTransition.Crossfade import com.zachklipp.compose.backstack.BackstackTransition.Slide import org.junit.Rule @@ -25,8 +21,7 @@ class BackstackComposableTest { @get:Rule val compose = createComposeRule() - private val clock = ManualAnimationClock(0) - private val animation = TweenSpec(durationMillis = 100) + private val animation = TweenSpec(durationMillis = 1000) @Test fun initialStateWithSingleScreen_slide() { @@ -61,7 +56,7 @@ class BackstackComposableTest { private fun assertInitialStateWithSingleScreen(transition: BackstackTransition) { val originalBackstack = listOf("one") compose.setContent { - Backstack(originalBackstack, transition = transition) { Text(it) } + Backstack(originalBackstack, transition = transition) { BasicText(it) } } compose.onNodeWithText("one").assertIsDisplayed() @@ -70,7 +65,7 @@ class BackstackComposableTest { private fun assertInitialStateWithMultipleScreens(transition: BackstackTransition) { val originalBackstack = listOf("one", "two") compose.setContent { - Backstack(originalBackstack, transition = transition) { Text(it) } + Backstack(originalBackstack, transition = transition) { BasicText(it) } } compose.onNodeWithText("two").assertIsDisplayed() @@ -81,14 +76,13 @@ class BackstackComposableTest { val originalBackstack = listOf("one") val destinationBackstack = listOf("one", "two") var backstack by mutableStateOf(originalBackstack) + compose.mainClock.autoAdvance = false compose.setContent { - Providers(AnimationClockAmbient provides clock) { Backstack( - backstack, - animationBuilder = animation, - transition = transition - ) { Text(it) } - } + backstack, + animationBuilder = animation, + transition = transition + ) { BasicText(it) } } compose.onNodeWithText("one").assertIsDisplayed() @@ -101,25 +95,19 @@ class BackstackComposableTest { compose.onNodeWithText("one").assertIsDisplayed() compose.onNodeWithText("two").assertDoesNotExist() - setTransitionTime(25) + compose.mainClock.advanceTimeBy(250) compose.onNodeWithText("one").assertIsDisplayed() compose.onNodeWithText("two").assertIsDisplayed() - setTransitionTime(75) + compose.mainClock.advanceTimeBy(750) compose.onNodeWithText("one").assertIsDisplayed() compose.onNodeWithText("two").assertIsDisplayed() - setTransitionTime(100) + compose.mainClock.advanceTimeBy(1000) compose.onNodeWithText("one").assertDoesNotExist() compose.onNodeWithText("two").assertIsDisplayed() } - - private fun setTransitionTime(time: Long) { - compose.runOnUiThread { - clock.clockTimeMillis = time - } - } } diff --git a/compose-backstack/src/main/java/com/zachklipp/compose/backstack/Backstack.kt b/compose-backstack/src/main/java/com/zachklipp/compose/backstack/Backstack.kt index 3721e7f..861c653 100644 --- a/compose-backstack/src/main/java/com/zachklipp/compose/backstack/Backstack.kt +++ b/compose-backstack/src/main/java/com/zachklipp/compose/backstack/Backstack.kt @@ -2,25 +2,27 @@ package com.zachklipp.compose.backstack -import androidx.compose.animation.animatedFloat +import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationEndReason -import androidx.compose.animation.core.AnimationEndReason.TargetReached +import androidx.compose.animation.core.AnimationEndReason.Finished import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.TweenSpec import androidx.compose.foundation.layout.Box import androidx.compose.runtime.* -import androidx.compose.runtime.savedinstancestate.UiSavedStateRegistryAmbient +import androidx.compose.runtime.saveable.LocalSaveableStateRegistry import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawOpacity import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.platform.AnimationClockAmbient -import androidx.compose.ui.platform.ContextAmbient +import androidx.compose.ui.platform.LocalContext import com.zachklipp.compose.backstack.TransitionDirection.Backward import com.zachklipp.compose.backstack.TransitionDirection.Forward +import kotlinx.coroutines.CoroutineStart.UNDISPATCHED +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch /** Used to hide screens when not transitioning. */ -internal val HIDDEN_MODIFIER = Modifier.drawOpacity(0f) +internal val HIDDEN_MODIFIER = Modifier.alpha(0f) /** * Identifies which direction a transition is being performed in. @@ -44,12 +46,11 @@ internal data class ScreenProperties( val isVisible: Boolean ) -@Composable private val DefaultBackstackAnimation: AnimationSpec - get() { - val context = ContextAmbient.current + @Composable get() { + val context = LocalContext.current return TweenSpec( - durationMillis = context.resources.getInteger(android.R.integer.config_shortAnimTime) + durationMillis = context.resources.getInteger(android.R.integer.config_shortAnimTime) ) } @@ -126,6 +127,7 @@ private val DefaultBackstackAnimation: AnimationSpec * controlled by touch gestures. * @param drawScreen Called with each element of [backstack] to render it. */ +@OptIn(ExperimentalCoroutinesApi::class) @Composable fun Backstack( backstack: List, @@ -138,11 +140,10 @@ fun Backstack( drawScreen: @Composable (T) -> Unit ) { require(backstack.isNotEmpty()) { "Backstack must contain at least 1 screen." } - onCommit(backstack) { - require(backstack.distinct().size == backstack.size) { - "Backstack must not contain duplicates: $backstack" - } + require(backstack.distinct().size == backstack.size) { + "Backstack must not contain duplicates: $backstack" } + val scope = rememberCoroutineScope() // When transitioning, contains a stable cache of the screens actually being displayed. Will not // change even if backstack changes during the transition. @@ -156,22 +157,23 @@ fun Backstack( // Defines the progress of the current transition animation in terms of visibility of the top // screen. 1 means top screen is visible, 0 means top screen is entirely hidden. Must be 1 when // no transition in progress. - val transitionProgress = animatedFloat(1f) + val transitionProgress = remember { Animatable(1f) } // Null means not transitioning. var direction by remember { mutableStateOf(null) } // Callback passed to animations to cleanup after the transition is done. val onTransitionEnd = remember { { reason: AnimationEndReason, _: Float -> - if (reason == TargetReached) { + if (reason == Finished) { direction = null - transitionProgress.snapTo(1f) - onTransitionFinished?.invoke() + scope.launch { + transitionProgress.snapTo(1f) + onTransitionFinished?.invoke() + } } } } val animation = animationBuilder ?: DefaultBackstackAnimation - val clock = AnimationClockAmbient.current - val inspector = remember { BackstackInspector(clock) } + val inspector = remember { BackstackInspector(scope) } inspector.params = inspectionParams if (direction == null && activeKeys != backstack) { @@ -202,8 +204,12 @@ fun Backstack( newKeys += oldTop // When going back the top screen needs to start off as visible. - transitionProgress.snapTo(1f) - transitionProgress.animateTo(0f, anim = animation, onEnd = onTransitionEnd) + // Need to start the coroutine undispatched so the snap happens before the frame is drawn. + scope.launch(start = UNDISPATCHED) { + transitionProgress.snapTo(1f) + val result = transitionProgress.animateTo(0f, animationSpec = animation) + onTransitionEnd(result.endReason, result.endState.value) + } } else { // If the current screen is not the new second-last screen, we need to move it to that // position so it animates out when going forward. This is true whether or not the current @@ -214,8 +220,11 @@ fun Backstack( newKeys += targetTop // When going forward, the top screen needs to start off as invisible. - transitionProgress.snapTo(0f) - transitionProgress.animateTo(1f, anim = animation, onEnd = onTransitionEnd) + scope.launch(start = UNDISPATCHED) { + transitionProgress.snapTo(0f) + val result = transitionProgress.animateTo(1f, animationSpec = animation) + onTransitionEnd(result.endReason, result.endState.value) + } } onTransitionStarting?.invoke(activeKeys, backstack, direction!!) activeKeys = newKeys @@ -253,7 +262,7 @@ fun Backstack( return@ScreenWrapper } - Providers(UiSavedStateRegistryAmbient provides savedStateRegistry) { + CompositionLocalProvider(LocalSaveableStateRegistry provides savedStateRegistry) { Box(screenProperties.modifier) { children() } } } @@ -296,8 +305,8 @@ internal fun calculateRegularModifier( else -> transition.modifierForScreen(visibility, index == count - 1) } return ScreenProperties( - modifier = screenModifier, - isVisible = visibility != 0f + modifier = screenModifier, + isVisible = visibility != 0f ) } diff --git a/compose-backstack/src/main/java/com/zachklipp/compose/backstack/BackstackInspector.kt b/compose-backstack/src/main/java/com/zachklipp/compose/backstack/BackstackInspector.kt index 629bd8a..3306423 100644 --- a/compose-backstack/src/main/java/com/zachklipp/compose/backstack/BackstackInspector.kt +++ b/compose-backstack/src/main/java/com/zachklipp/compose/backstack/BackstackInspector.kt @@ -2,21 +2,23 @@ package com.zachklipp.compose.backstack -import androidx.compose.animation.AnimatedFloatModel -import androidx.compose.animation.animate -import androidx.compose.animation.core.AnimationClockObservable +import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.FloatSpringSpec import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.drawLayer -import androidx.compose.ui.platform.DensityAmbient +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch import java.lang.Math.toRadians import kotlin.math.sin @@ -60,7 +62,7 @@ fun InspectionParams.constrained() = InspectionParams( overlayOpacity = overlayOpacity.coerceIn(0f, 1f) ) -internal class BackstackInspector(clock: AnimationClockObservable) { +internal class BackstackInspector(private val scope: CoroutineScope) { private val animation = FloatSpringSpec(stiffness = Spring.StiffnessLow) @@ -91,24 +93,24 @@ internal class BackstackInspector(clock: AnimationClockObservable) { } } constrainedParams?.let { - offsetDpX.animateTo(it.offsetX.value, animation) - offsetDpY.animateTo(it.offsetY.value, animation) - rotationX.animateTo(it.rotationXDegrees, animation) - rotationY.animateTo(it.rotationYDegrees, animation) - scaleFactor.animateTo(it.scale, animation) - alpha.animateTo(it.opacity, animation) - overlayAlpha.animateTo(it.overlayOpacity) + scope.launch { offsetDpX.animateTo(it.offsetX.value, animation) } + scope.launch { offsetDpY.animateTo(it.offsetY.value, animation) } + scope.launch { rotationX.animateTo(it.rotationXDegrees, animation) } + scope.launch { rotationY.animateTo(it.rotationYDegrees, animation) } + scope.launch { scaleFactor.animateTo(it.scale, animation) } + scope.launch { alpha.animateTo(it.opacity, animation) } + scope.launch { overlayAlpha.animateTo(it.overlayOpacity) } } field = constrainedParams } - private val offsetDpX = AnimatedFloatModel(INITIAL_OFFSET_X, clock) - private val offsetDpY = AnimatedFloatModel(INITIAL_OFFSET_Y, clock) - private val rotationX = AnimatedFloatModel(INITIAL_ROTATION_X, clock) - private val rotationY = AnimatedFloatModel(INITIAL_ROTATION_Y, clock) - private val scaleFactor = AnimatedFloatModel(INITIAL_SCALE, clock) - private val alpha = AnimatedFloatModel(INITIAL_ALPHA, clock) - private val overlayAlpha = AnimatedFloatModel(INITIAL_OVERLAY_ALPHA, clock) + private val offsetDpX = Animatable(INITIAL_OFFSET_X) + private val offsetDpY = Animatable(INITIAL_OFFSET_Y) + private val rotationX = Animatable(INITIAL_ROTATION_X) + private val rotationY = Animatable(INITIAL_ROTATION_Y) + private val scaleFactor = Animatable(INITIAL_SCALE) + private val alpha = Animatable(INITIAL_ALPHA) + private val overlayAlpha = Animatable(INITIAL_OVERLAY_ALPHA) /** * Calculates a [Modifier] to apply to a screen when in inspection mode. @@ -117,6 +119,7 @@ internal class BackstackInspector(clock: AnimationClockObservable) { * [InspectionParams.overlayOpacity]. All other screens will be drawn as a 3D stack. * All transformations are animated. */ + @Suppress("ComposableModifierFactory", "ModifierFactoryExtensionFunction") @Composable internal fun inspectScreen( screenIndex: Int, @@ -127,53 +130,53 @@ internal class BackstackInspector(clock: AnimationClockObservable) { // compose supports transforming inputs as well as outputs, the top screen can // participate in scaling/rotation too. val isTop = screenIndex == screenCount - 1 - val density = DensityAmbient.current + val density = LocalDensity.current // drawLayer will scale around the center of the bounds, so we need to offset relative // to that so the entire stack stays centered. - val centerOffset = animate( - // Don't need to adjust the pivot point if there's only one screen. - if (screenCount == 1) 0f - // Add -1 + visibility so new screens animate "out of" the previous one. - else (screenIndex - 1f + visibility) - screenCount / 3f + val centerOffset by animateFloatAsState( + // Don't need to adjust the pivot point if there's only one screen. + if (screenCount == 1) 0f + // Add -1 + visibility so new screens animate "out of" the previous one. + else (screenIndex - 1f + visibility) - screenCount / 3f ) - val scale = animate(if (isTop) 1f else scaleFactor.value) + val scale by animateFloatAsState(if (isTop) 1f else scaleFactor.value) - val offsetDpX = animate( - if (isTop) 0f else { - // Adjust by screenCount to squeeze more in as the count increases. - val densityFactor = 10f / screenCount - // Adjust X offset by sin(rotation) so it looks 3D. - val xRotation = sin(toRadians(rotationY.value.toDouble())).toFloat() - (centerOffset * offsetDpX.value * scale * densityFactor * xRotation) - } + val offsetDpX by animateFloatAsState( + if (isTop) 0f else { + // Adjust by screenCount to squeeze more in as the count increases. + val densityFactor = 10f / screenCount + // Adjust X offset by sin(rotation) so it looks 3D. + val xRotation = sin(toRadians(rotationY.value.toDouble())).toFloat() + (centerOffset * offsetDpX.value * scale * densityFactor * xRotation) + } ) - val offsetDpY = animate(if (isTop) 0f else (centerOffset * offsetDpY.value * scale)) + val offsetDpY by animateFloatAsState(if (isTop) 0f else (centerOffset * offsetDpY.value * scale)) - val rotationX = animate(if (isTop) 0f else (rotationX.value)) - val rotationY = animate(if (isTop) 0f else (rotationY.value)) + val rotationX by animateFloatAsState(if (isTop) 0f else (rotationX.value)) + val rotationY by animateFloatAsState(if (isTop) 0f else (rotationY.value)) // This is the only transformation applied to the top screen, so it has some extra logic. - val alpha = animate( - when { - // If there's only one screen in the stack, don't transform it at all. - screenCount == 1 -> 1f - isTop -> overlayAlpha.value - else -> alpha.value - // Adjust alpha by visibility to make transition less jarring when adding/removing - // screens. - } * visibility + val alpha by animateFloatAsState( + when { + // If there's only one screen in the stack, don't transform it at all. + screenCount == 1 -> 1f + isTop -> overlayAlpha.value + else -> alpha.value + // Adjust alpha by visibility to make transition less jarring when adding/removing + // screens. + } * visibility ) - return Modifier.drawLayer( - scaleX = scale, - scaleY = scale, - rotationX = rotationX, - rotationY = rotationY, - translationX = with(density) { offsetDpX.dp.toPx() }, - translationY = with(density) { offsetDpY.dp.toPx() }, - alpha = alpha + return Modifier.graphicsLayer( + scaleX = scale, + scaleY = scale, + rotationX = rotationX, + rotationY = rotationY, + translationX = with(density) { offsetDpX.dp.toPx() }, + translationY = with(density) { offsetDpY.dp.toPx() }, + alpha = alpha ) } @@ -184,17 +187,20 @@ internal class BackstackInspector(clock: AnimationClockObservable) { /** Transition away from inspection mode. */ private fun stopInspecting() { - offsetDpX.animateTo(INITIAL_OFFSET_X, animation) - offsetDpY.animateTo(INITIAL_OFFSET_Y, animation) - rotationX.animateTo(INITIAL_ROTATION_X, animation) - rotationY.animateTo(INITIAL_ROTATION_Y, animation) - scaleFactor.animateTo(INITIAL_SCALE, animation) - alpha.animateTo(INITIAL_ALPHA, animation) - overlayAlpha.animateTo(INITIAL_OVERLAY_ALPHA, animation, onEnd = { _, _ -> - // Doesn't matter which one, but we need to listen to the end of one of the animations - // so we can tell the Backstack that we're done being in control. + scope.launch { + coroutineScope { + launch { offsetDpX.animateTo(INITIAL_OFFSET_X, animation) } + launch { offsetDpY.animateTo(INITIAL_OFFSET_Y, animation) } + launch { rotationX.animateTo(INITIAL_ROTATION_X, animation) } + launch { rotationY.animateTo(INITIAL_ROTATION_Y, animation) } + launch { scaleFactor.animateTo(INITIAL_SCALE, animation) } + launch { alpha.animateTo(INITIAL_ALPHA, animation) } + launch { overlayAlpha.animateTo(INITIAL_OVERLAY_ALPHA, animation) } + } + // Once all the animations are done we need to tell the Backstack that we're done being in + // control. isInspectionActive = false - }) + } } private companion object { diff --git a/compose-backstack/src/main/java/com/zachklipp/compose/backstack/BackstackTransition.kt b/compose-backstack/src/main/java/com/zachklipp/compose/backstack/BackstackTransition.kt index 558094a..bea08bb 100644 --- a/compose-backstack/src/main/java/com/zachklipp/compose/backstack/BackstackTransition.kt +++ b/compose-backstack/src/main/java/com/zachklipp/compose/backstack/BackstackTransition.kt @@ -3,7 +3,7 @@ package com.zachklipp.compose.backstack import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawOpacity +import androidx.compose.ui.draw.alpha import com.zachklipp.compose.backstack.BackstackTransition.Crossfade import com.zachklipp.compose.backstack.BackstackTransition.Slide @@ -26,6 +26,7 @@ interface BackstackTransition { * visible, then the top screen is always transitioning _out_, and non-top screens are either * transitioning out or invisible. */ + @Suppress("ModifierFactoryExtensionFunction") fun modifierForScreen( visibility: Float, isTop: Boolean @@ -35,6 +36,7 @@ interface BackstackTransition { * A simple transition that slides screens horizontally. */ object Slide : BackstackTransition { + @Suppress("ModifierFactoryExtensionFunction") override fun modifierForScreen( visibility: Float, isTop: Boolean @@ -47,9 +49,10 @@ interface BackstackTransition { * A simple transition that crossfades between screens. */ object Crossfade : BackstackTransition { + @Suppress("ModifierFactoryExtensionFunction") override fun modifierForScreen( visibility: Float, isTop: Boolean - ): Modifier = Modifier.drawOpacity(visibility) + ): Modifier = Modifier.alpha(visibility) } } diff --git a/compose-backstack/src/main/java/com/zachklipp/compose/backstack/ChildSavedStateRegistry.kt b/compose-backstack/src/main/java/com/zachklipp/compose/backstack/ChildSavedStateRegistry.kt index df85fa9..a59bc4d 100644 --- a/compose-backstack/src/main/java/com/zachklipp/compose/backstack/ChildSavedStateRegistry.kt +++ b/compose-backstack/src/main/java/com/zachklipp/compose/backstack/ChildSavedStateRegistry.kt @@ -1,32 +1,34 @@ package com.zachklipp.compose.backstack import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLifecycleObserver import androidx.compose.runtime.ExperimentalComposeApi -import androidx.compose.runtime.currentComposer +import androidx.compose.runtime.RememberObserver +import androidx.compose.runtime.currentCompositeKeyHash import androidx.compose.runtime.remember -import androidx.compose.runtime.savedinstancestate.UiSavedStateRegistry -import androidx.compose.runtime.savedinstancestate.UiSavedStateRegistryAmbient +import androidx.compose.runtime.saveable.LocalSaveableStateRegistry +import androidx.compose.runtime.saveable.SaveableStateRegistry +import androidx.compose.runtime.saveable.SaveableStateRegistry.Entry /** - * Returns a [UiSavedStateRegistry] that will automatically save values from all its registered + * Returns a [SaveableStateRegistry] that will automatically save values from all its registered * providers whenever [childWillBeComposed] transitions from true to false, and make those values available * to be restored when [childWillBeComposed] transitions from false to true. */ +@Suppress("ComposableNaming") @OptIn(ExperimentalComposeApi::class) @Composable -fun ChildSavedStateRegistry(childWillBeComposed: Boolean): UiSavedStateRegistry { - val parent = UiSavedStateRegistryAmbient.current - val key = currentComposer.currentCompoundKeyHash.toString() +fun ChildSavedStateRegistry(childWillBeComposed: Boolean): SaveableStateRegistry { + val parent = LocalSaveableStateRegistry.current + val key = currentCompositeKeyHash.toString() val holder = remember { SavedStateHolder(key) } return holder.updateAndReturnRegistry(parent, childWillBeComposed) } -internal class SavedStateHolder(private val key: String) : CompositionLifecycleObserver { - private var parent: UiSavedStateRegistry? = null +internal class SavedStateHolder(private val key: String) : RememberObserver { + private var parent: SaveableStateRegistry? = null private var isScreenVisible = false private var values: Map>? = null - private var registry: UiSavedStateRegistry = createRegistry() + private var registry: SaveableStateRegistry = createRegistry() private var valueProvider: () -> Any? = { if (isScreenVisible) { // Save the screen if it is visible right now. If it is invisible, then it's @@ -36,6 +38,8 @@ internal class SavedStateHolder(private val key: String) : CompositionLifecycleO values } + private var entryInParent: Entry? = null + /** * Manages the visibility of the screen and saves its state whenever [isVisible] transitions * from true to false, or whenever the Android OS triggers an onSaveInstanceState dispatch. @@ -44,9 +48,9 @@ internal class SavedStateHolder(private val key: String) : CompositionLifecycleO */ @Suppress("UNCHECKED_CAST") fun updateAndReturnRegistry( - parent: UiSavedStateRegistry?, + parent: SaveableStateRegistry?, isVisible: Boolean - ): UiSavedStateRegistry { + ): SaveableStateRegistry { // When values is null, try restore any previously saved values (or fallback to an empty // map). Once values is non-null, it'll hold the all the latest saved values for the screen. values = values ?: parent?.consumeRestored(key) as Map>? ?: emptyMap() @@ -59,8 +63,8 @@ internal class SavedStateHolder(private val key: String) : CompositionLifecycleO // call unregisterProvider on an UiSavedStateRegistry where `key` isn't already registered, // then it'll crash. if (parent !== oldParent) { - oldParent?.unregisterProvider(key, valueProvider) - parent?.registerProvider(key, valueProvider) + entryInParent?.unregister() + entryInParent = parent?.registerProvider(key, valueProvider) } if (isVisible == this.isScreenVisible) return registry @@ -79,18 +83,22 @@ internal class SavedStateHolder(private val key: String) : CompositionLifecycleO return registry } - override fun onEnter() { + override fun onAbandoned() { + entryInParent?.unregister() + } + + override fun onRemembered() { // No-op } - override fun onLeave() { - parent?.unregisterProvider(key, valueProvider) + override fun onForgotten() { + entryInParent?.unregister() } - private fun createRegistry(): UiSavedStateRegistry { + private fun createRegistry(): SaveableStateRegistry { // If there's no registry available, then we won't be restored anyway so there are no // serializability restrictions on saved values. val canBeSaved: (Any) -> Boolean = parent?.let { it::canBeSaved } ?: { true } - return UiSavedStateRegistry(values, canBeSaved) + return SaveableStateRegistry(values, canBeSaved) } } diff --git a/compose-backstack/src/main/java/com/zachklipp/compose/backstack/InspectionGestureDetector.kt b/compose-backstack/src/main/java/com/zachklipp/compose/backstack/InspectionGestureDetector.kt index 4c2daa2..b706559 100644 --- a/compose-backstack/src/main/java/com/zachklipp/compose/backstack/InspectionGestureDetector.kt +++ b/compose-backstack/src/main/java/com/zachklipp/compose/backstack/InspectionGestureDetector.kt @@ -2,6 +2,7 @@ package com.zachklipp.compose.backstack +import androidx.compose.foundation.gestures.detectTransformGestures import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -9,11 +10,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.gesture.DragObserver -import androidx.compose.ui.gesture.ScaleObserver -import androidx.compose.ui.gesture.dragGestureFilter -import androidx.compose.ui.gesture.scaleGestureFilter +import androidx.compose.ui.input.pointer.pointerInput /** * Wrap your [Backstack] with this composable to get convenient gesture-based control of the @@ -42,34 +39,19 @@ fun InspectionGestureDetector( ) { var inspectionParams: InspectionParams by remember { mutableStateOf(InspectionParams()) } - val scaleObserver = remember(enabled) { - object : ScaleObserver { - override fun onScale(scaleFactor: Float) { - if (!enabled) return + val controlModifier = if (!enabled) Modifier else { + Modifier.pointerInput(Unit) { + detectTransformGestures { _, pan, zoom, _ -> inspectionParams = inspectionParams.copy( - scale = inspectionParams.scale * scaleFactor - ).constrained() - } - } - } - val dragObserver = remember(enabled) { - object : DragObserver { - override fun onDrag(dragDistance: Offset): Offset { - if (!enabled) return Offset.Zero - inspectionParams = inspectionParams.copy( - // Dragging left-and-right rotates around the vertical Y axis. - rotationYDegrees = inspectionParams.rotationYDegrees + (dragDistance.x / 5f) - ).constrained() - return dragDistance + scale = inspectionParams.scale * zoom, + // Dragging left-and-right rotates around the vertical Y axis. + rotationYDegrees = inspectionParams.rotationYDegrees + (pan.x / 5f) + ) } } } - Box( - modifier = Modifier - .scaleGestureFilter(scaleObserver = scaleObserver) - .dragGestureFilter(dragObserver = dragObserver) - ) { + Box(modifier = controlModifier) { children(inspectionParams.takeIf { enabled }) } } diff --git a/compose-backstack/src/main/java/com/zachklipp/compose/backstack/PercentageLayoutOffset.kt b/compose-backstack/src/main/java/com/zachklipp/compose/backstack/PercentageLayoutOffset.kt index 7981e61..7fd1c48 100644 --- a/compose-backstack/src/main/java/com/zachklipp/compose/backstack/PercentageLayoutOffset.kt +++ b/compose-backstack/src/main/java/com/zachklipp/compose/backstack/PercentageLayoutOffset.kt @@ -2,9 +2,10 @@ package com.zachklipp.compose.backstack import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting.PRIVATE -import androidx.compose.ui.LayoutModifier -import androidx.compose.ui.Measurable -import androidx.compose.ui.MeasureScope +import androidx.compose.ui.layout.LayoutModifier +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize @@ -15,7 +16,7 @@ internal class PercentageLayoutOffset(offset: Float) : LayoutModifier { override fun MeasureScope.measure( measurable: Measurable, constraints: Constraints - ): MeasureScope.MeasureResult { + ): MeasureResult { val placeable = measurable.measure(constraints) return layout(placeable.width, placeable.height) { placeable.place(offsetPosition(IntSize(placeable.width, placeable.height))) diff --git a/compose-backstack/src/test/java/com/zachklipp/compose/backstack/BackstackTest.kt b/compose-backstack/src/test/java/com/zachklipp/compose/backstack/BackstackTest.kt index f241606..780cea5 100644 --- a/compose-backstack/src/test/java/com/zachklipp/compose/backstack/BackstackTest.kt +++ b/compose-backstack/src/test/java/com/zachklipp/compose/backstack/BackstackTest.kt @@ -152,6 +152,7 @@ class BackstackTest { } private object TestTransition : BackstackTransition { + @Suppress("ModifierFactoryExtensionFunction") override fun modifierForScreen( visibility: Float, isTop: Boolean diff --git a/compose-backstack/src/test/java/com/zachklipp/compose/backstack/SavedStateHolderTest.kt b/compose-backstack/src/test/java/com/zachklipp/compose/backstack/SavedStateHolderTest.kt index 27bf553..ca9ead6 100644 --- a/compose-backstack/src/test/java/com/zachklipp/compose/backstack/SavedStateHolderTest.kt +++ b/compose-backstack/src/test/java/com/zachklipp/compose/backstack/SavedStateHolderTest.kt @@ -1,6 +1,6 @@ package com.zachklipp.compose.backstack -import androidx.compose.runtime.savedinstancestate.UiSavedStateRegistry +import androidx.compose.runtime.saveable.SaveableStateRegistry import com.google.common.truth.Truth.assertThat import org.junit.Test @@ -8,14 +8,14 @@ class SavedStateHolderTest { @Test fun `saves and restores`() { - val parent = UiSavedStateRegistry(restoredValues = null, canBeSaved = { true }) + val parent = SaveableStateRegistry(restoredValues = null, canBeSaved = { true }) val holder = SavedStateHolder("pk") val valueProvider = { "value" } var registry = holder.updateAndReturnRegistry(parent, isVisible = true) - registry.registerProvider("ck", valueProvider) - registry = holder.updateAndReturnRegistry(parent, isVisible = false) - registry.unregisterProvider("ck", valueProvider) + val entry = registry.registerProvider("ck", valueProvider) + /*registry =*/ holder.updateAndReturnRegistry(parent, isVisible = false) + entry.unregister() registry = holder.updateAndReturnRegistry(parent = parent, isVisible = true) assertThat(registry.consumeRestored("ck")).isEqualTo("value") @@ -24,7 +24,7 @@ class SavedStateHolderTest { @Test fun `restores from initial values`() { val restoredValues = mutableMapOf("pk" to listOf(mapOf("ck" to listOf("value")))) - val parent = UiSavedStateRegistry(restoredValues = restoredValues, canBeSaved = { true }) + val parent = SaveableStateRegistry(restoredValues = restoredValues, canBeSaved = { true }) val holder = SavedStateHolder("pk") val registry = holder.updateAndReturnRegistry(parent = parent, isVisible = true) @@ -34,13 +34,14 @@ class SavedStateHolderTest { @Test fun `doesn't save unregistered providers`() { - val parent = UiSavedStateRegistry(restoredValues = null, canBeSaved = { true }) + val parent = SaveableStateRegistry(restoredValues = null, canBeSaved = { true }) val holder = SavedStateHolder("pk") val valueProvider = { "value" } var registry = holder.updateAndReturnRegistry(parent, isVisible = true) - registry.registerProvider("key", valueProvider) - registry.unregisterProvider("key", valueProvider) + registry.registerProvider("key", valueProvider).also { + it.unregister() + } holder.updateAndReturnRegistry(parent, isVisible = false) registry = holder.updateAndReturnRegistry(parent, isVisible = true) @@ -50,7 +51,7 @@ class SavedStateHolderTest { @Test fun `preserves unrestored values from previous save`() { val restoredValues = mutableMapOf("pk" to listOf(mapOf("old key" to listOf("old value")))) - val parent = UiSavedStateRegistry(restoredValues = restoredValues, canBeSaved = { true }) + val parent = SaveableStateRegistry(restoredValues = restoredValues, canBeSaved = { true }) val holder = SavedStateHolder("pk") holder.updateAndReturnRegistry(parent, isVisible = true) @@ -64,7 +65,7 @@ class SavedStateHolderTest { @Test fun `cleans up restored values from previous save`() { val restoredValues = mutableMapOf("pk" to listOf(mapOf("old key" to listOf("old value")))) - val parent = UiSavedStateRegistry(restoredValues = restoredValues, canBeSaved = { true }) + val parent = SaveableStateRegistry(restoredValues = restoredValues, canBeSaved = { true }) val holder = SavedStateHolder("pk") var registry = holder.updateAndReturnRegistry(parent, isVisible = true) @@ -78,7 +79,7 @@ class SavedStateHolderTest { @Test fun `parent saves contain values from the currently visible screen`() { - val parent = UiSavedStateRegistry(restoredValues = null, canBeSaved = { true }) + val parent = SaveableStateRegistry(restoredValues = null, canBeSaved = { true }) val holder = SavedStateHolder("pk") val registry = holder.updateAndReturnRegistry(parent, isVisible = true) @@ -90,14 +91,14 @@ class SavedStateHolderTest { @Test fun `parent saves contain values from non-visible screens`() { - val parent = UiSavedStateRegistry(restoredValues = null, canBeSaved = { true }) + val parent = SaveableStateRegistry(restoredValues = null, canBeSaved = { true }) val holder = SavedStateHolder("pk") val valueProvider = { "value" } var registry = holder.updateAndReturnRegistry(parent, isVisible = true) - registry.registerProvider("ck", valueProvider) - registry = holder.updateAndReturnRegistry(parent, isVisible = false) - registry.unregisterProvider("ck", valueProvider) + val entry = registry.registerProvider("ck", valueProvider) + /*registry =*/ holder.updateAndReturnRegistry(parent, isVisible = false) + entry.unregister() val values = parent.performSave() assertThat(values["pk"]).isEqualTo(listOf(mapOf("ck" to listOf("value")))) @@ -108,7 +109,7 @@ class SavedStateHolderTest { val topStateHolder = SavedStateHolder("pk1") val middleStateHolder = SavedStateHolder("pk2") - val topRegistry = UiSavedStateRegistry(restoredValues = null, canBeSaved = { true }) + val topRegistry = SaveableStateRegistry(restoredValues = null, canBeSaved = { true }) val middleRegistry = topStateHolder.updateAndReturnRegistry(topRegistry, isVisible = true) val bottomRegistry = middleStateHolder.updateAndReturnRegistry(middleRegistry, isVisible = true) diff --git a/gradle.properties b/gradle.properties index 4d15d01..f6e147b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,6 +16,6 @@ org.gradle.jvmargs=-Xmx2048m # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true # Automatically convert third-party libraries to use AndroidX -android.enableJetifier=true +android.enableJetifier=false # Kotlin code style for this project: "official" or "obsolete": -kotlin.code.style=official \ No newline at end of file +kotlin.code.style=official diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 14e30f7..25d3265 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index 61a6140..66db178 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -17,6 +17,7 @@ android { dependencies { implementation(project(":compose-backstack-viewer")) implementation(Dependencies.AndroidX.appcompat) + implementation(Dependencies.Compose.activity) implementation(Dependencies.Compose.foundation) implementation(Dependencies.Compose.util) diff --git a/sample/src/androidTest/java/com/zachklipp/compose/backstack/sample/SampleAppTest.kt b/sample/src/androidTest/java/com/zachklipp/compose/backstack/sample/SampleAppTest.kt index 872f663..e3cda8d 100644 --- a/sample/src/androidTest/java/com/zachklipp/compose/backstack/sample/SampleAppTest.kt +++ b/sample/src/androidTest/java/com/zachklipp/compose/backstack/sample/SampleAppTest.kt @@ -1,9 +1,9 @@ package com.zachklipp.compose.backstack.sample +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.ui.test.assertIsDisplayed -import androidx.ui.test.createAndroidComposeRule -import androidx.ui.test.onNodeWithSubstring import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -16,11 +16,11 @@ class SampleAppTest { @Test fun launches() { - compose.onNodeWithSubstring("Slow animations") + compose.onNodeWithText("Slow animations", substring = true) } @Test fun showsCounter() { - compose.onNodeWithSubstring("Counter:").assertIsDisplayed() + compose.onNodeWithText("Counter:", substring = true).assertIsDisplayed() } } diff --git a/sample/src/main/java/com/zachklipp/compose/backstack/sample/ComposeBackstackActivity.kt b/sample/src/main/java/com/zachklipp/compose/backstack/sample/ComposeBackstackActivity.kt index d856d46..b681a06 100644 --- a/sample/src/main/java/com/zachklipp/compose/backstack/sample/ComposeBackstackActivity.kt +++ b/sample/src/main/java/com/zachklipp/compose/backstack/sample/ComposeBackstackActivity.kt @@ -1,8 +1,8 @@ package com.zachklipp.compose.backstack.sample import android.os.Bundle +import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity -import androidx.compose.ui.platform.setContent import com.zachklipp.compose.backstack.viewer.BackstackViewerApp class ComposeBackstackActivity : AppCompatActivity() { diff --git a/sample/src/main/java/com/zachklipp/compose/backstack/sample/FancyTransition.kt b/sample/src/main/java/com/zachklipp/compose/backstack/sample/FancyTransition.kt index 7ae9d6e..26ec018 100644 --- a/sample/src/main/java/com/zachklipp/compose/backstack/sample/FancyTransition.kt +++ b/sample/src/main/java/com/zachklipp/compose/backstack/sample/FancyTransition.kt @@ -1,7 +1,7 @@ package com.zachklipp.compose.backstack.sample import androidx.compose.ui.Modifier -import androidx.compose.ui.drawLayer +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.util.lerp import com.zachklipp.compose.backstack.BackstackTransition import com.zachklipp.compose.backstack.BackstackTransition.Crossfade @@ -13,6 +13,7 @@ import kotlin.math.pow * with some additional math and other transformations. */ object FancyTransition : BackstackTransition { + @Suppress("ModifierFactoryExtensionFunction") override fun modifierForScreen( visibility: Float, isTop: Boolean @@ -25,7 +26,7 @@ object FancyTransition : BackstackTransition { } else { // Move the non-top screen back, but only a little. val scaleVisibility = lerp(.9f, 1f, visibility) - Modifier.drawLayer(scaleX = scaleVisibility, scaleY = scaleVisibility) + Modifier.graphicsLayer(scaleX = scaleVisibility, scaleY = scaleVisibility) .then(Crossfade.modifierForScreen(visibility.pow(.5f), isTop)) } }