Merge "Investigating KSP Type Annotation possibility for Room support." into androidx-main
diff --git a/benchmark/benchmark-macro-junit4/build.gradle b/benchmark/benchmark-macro-junit4/build.gradle
index 5fa8211..75bd5ec 100644
--- a/benchmark/benchmark-macro-junit4/build.gradle
+++ b/benchmark/benchmark-macro-junit4/build.gradle
@@ -36,10 +36,10 @@
api(libs.kotlinStdlib)
api("androidx.annotation:annotation:1.1.0")
api(project(":benchmark:benchmark-macro"))
+ api("androidx.test.uiautomator:uiautomator:2.2.0")
implementation(project(":benchmark:benchmark-common"))
implementation("androidx.test:rules:1.5.0")
implementation("androidx.test:runner:1.5.0")
- implementation("androidx.test.uiautomator:uiautomator:2.2.0")
androidTestImplementation(project(":internal-testutils-ktx"))
androidTestImplementation(libs.testExtJunit)
diff --git a/buildSrc-tests/src/test/java/androidx/build/testConfiguration/AndroidTestConfigBuilderTest.kt b/buildSrc-tests/src/test/java/androidx/build/testConfiguration/AndroidTestConfigBuilderTest.kt
index 3d8531a..f53eb4f 100644
--- a/buildSrc-tests/src/test/java/androidx/build/testConfiguration/AndroidTestConfigBuilderTest.kt
+++ b/buildSrc-tests/src/test/java/androidx/build/testConfiguration/AndroidTestConfigBuilderTest.kt
@@ -349,9 +349,6 @@
<option name="config-descriptor:metadata" key="applicationId" value="com.androidx.placeholder.Placeholder" />
<option name="wifi:disable" value="true" />
<option name="instrumentation-arg" key="notAnnotation" value="androidx.test.filters.FlakyTest" />
- <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
- <option name="run-command" value="cmd package compile -f -m speed com.androidx.placeholder.Placeholder" />
- </target_preparer>
<option name="instrumentation-arg" key="androidx.benchmark.output.enable" value="true" />
<option name="instrumentation-arg" key="listener" value="androidx.benchmark.junit4.InstrumentationResultsRunListener" />
<include name="google/unbundled/common/setup" />
@@ -360,6 +357,9 @@
<option name="install-arg" value="-t" />
<option name="test-file-name" value="placeholder.apk" />
</target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="run-command" value="cmd package compile -f -m speed com.androidx.placeholder.Placeholder" />
+ </target_preparer>
<test class="com.android.tradefed.testtype.AndroidJUnitTest">
<option name="runner" value="com.example.Runner"/>
<option name="package" value="com.androidx.placeholder.Placeholder" />
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt
index e33eaf4..a3c6691 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt
@@ -92,7 +92,6 @@
.append(WIFI_DISABLE_OPTION)
.append(FLAKY_TEST_OPTION)
if (isBenchmark) {
- sb.append(benchmarkPostInstallCommandOption(applicationId))
if (isPostsubmit) {
sb.append(BENCHMARK_POSTSUBMIT_OPTIONS)
} else {
@@ -105,7 +104,11 @@
if (!appApkName.isNullOrEmpty())
sb.append(APK_INSTALL_OPTION.replace("APK_NAME", appApkName!!))
sb.append(TARGET_PREPARER_CLOSE)
- .append(TEST_BLOCK_OPEN)
+ // Post install commands after SuiteApkInstaller is declared
+ if (isBenchmark) {
+ sb.append(benchmarkPostInstallCommandOption(applicationId))
+ }
+ sb.append(TEST_BLOCK_OPEN)
.append(RUNNER_OPTION.replace("TEST_RUNNER", testRunner))
.append(PACKAGE_OPTION.replace("APPLICATION_ID", applicationId))
.append(TEST_BLOCK_CLOSE)
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/OwnersService.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/OwnersService.kt
index 56384dd..10a9cc1 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/OwnersService.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/OwnersService.kt
@@ -38,7 +38,13 @@
@Input
fun getSerialized(): String {
val gson = GsonBuilder().setPrettyPrinting().create()
- return gson.toJson(testModules.associateBy { it.name })
+ // media service/client tests are created from multiple projects, so we get multiple
+ // entries with the same TestModule.name. This code merges all the TestModule.path entries
+ // across the test modules with the same name.
+ val data = testModules.groupBy { it.name }.map {
+ TestModule(name = it.key, path = it.value.flatMap { module -> module.path })
+ }.associateBy { it.name }
+ return gson.toJson(data)
}
@TaskAction
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
index abc2099..ce67a56 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
@@ -60,8 +60,7 @@
) {
val xmlName = "${path.asFilenamePrefix()}$variantName.xml"
val jsonName = "_${path.asFilenamePrefix()}$variantName.json"
- rootProject.tasks.named("createModuleInfo").configure {
- it as ModuleInfoGenerator
+ rootProject.tasks.named<ModuleInfoGenerator>("createModuleInfo").configure {
it.testModules.add(
TestModule(
name = xmlName,
@@ -284,10 +283,57 @@
) {
val mediaPrefix = getMediaConfigTaskPrefix(isMedia2)
val mediaTask = getOrCreateMediaTestConfigTask(this, isMedia2)
+
+ fun getJsonName(clientToT: Boolean, serviceToT: Boolean, clientTests: Boolean): String {
+ return "_${mediaPrefix}Client${
+ if (clientToT) "ToT" else "Previous"
+ }Service${
+ if (serviceToT) "ToT" else "Previous"
+ }${
+ if (clientTests) "Client" else "Service"
+ }Tests$variantName.json"
+ }
+
+ fun ModuleInfoGenerator.addTestModule(clientToT: Boolean, serviceToT: Boolean) {
+ // We don't test the combination of previous versions of service and client as that is not
+ // useful data. We always want at least one tip of tree project.
+ if (!clientToT && !serviceToT) return
+ testModules.add(
+ TestModule(
+ name = getJsonName(
+ clientToT = clientToT,
+ serviceToT = serviceToT,
+ clientTests = true
+ ),
+ path = listOf(projectDir.toRelativeString(getSupportRootFolder()))
+ )
+ )
+ testModules.add(
+ TestModule(
+ name = getJsonName(
+ clientToT = clientToT,
+ serviceToT = serviceToT,
+ clientTests = false
+ ),
+ path = listOf(projectDir.toRelativeString(getSupportRootFolder()))
+ )
+ )
+ }
+ val isClient = this.name.contains("client")
+ val isPrevious = this.name.contains("previous")
+
+ rootProject.tasks.named<ModuleInfoGenerator>("createModuleInfo").configure {
+ if (isClient) {
+ it.addTestModule(clientToT = !isPrevious, serviceToT = false)
+ it.addTestModule(clientToT = !isPrevious, serviceToT = true)
+ } else {
+ it.addTestModule(clientToT = true, serviceToT = !isPrevious)
+ it.addTestModule(clientToT = false, serviceToT = !isPrevious)
+ }
+ }
mediaTask.configure {
- it as GenerateMediaTestConfigurationTask
- if (this.name.contains("client")) {
- if (this.name.contains("previous")) {
+ if (isClient) {
+ if (isPrevious) {
it.clientPreviousFolder.set(artifacts.get(SingleArtifact.APK))
it.clientPreviousLoader.set(artifacts.getBuiltArtifactsLoader())
} else {
@@ -295,7 +341,7 @@
it.clientToTLoader.set(artifacts.getBuiltArtifactsLoader())
}
} else {
- if (this.name.contains("previous")) {
+ if (isPrevious) {
it.servicePreviousFolder.set(artifacts.get(SingleArtifact.APK))
it.servicePreviousLoader.set(artifacts.getBuiltArtifactsLoader())
} else {
@@ -305,32 +351,32 @@
}
it.jsonClientPreviousServiceToTClientTests.set(
getFileInTestConfigDirectory(
- "_${mediaPrefix}ClientPreviousServiceToTClientTests$variantName.json"
+ getJsonName(clientToT = false, serviceToT = true, clientTests = true)
)
)
it.jsonClientPreviousServiceToTServiceTests.set(
getFileInTestConfigDirectory(
- "_${mediaPrefix}ClientPreviousServiceToTServiceTests$variantName.json"
+ getJsonName(clientToT = false, serviceToT = true, clientTests = false)
)
)
it.jsonClientToTServicePreviousClientTests.set(
getFileInTestConfigDirectory(
- "_${mediaPrefix}ClientToTServicePreviousClientTests$variantName.json"
+ getJsonName(clientToT = true, serviceToT = false, clientTests = true)
)
)
it.jsonClientToTServicePreviousServiceTests.set(
getFileInTestConfigDirectory(
- "_${mediaPrefix}ClientToTServicePreviousServiceTests$variantName.json"
+ getJsonName(clientToT = true, serviceToT = false, clientTests = false)
)
)
it.jsonClientToTServiceToTClientTests.set(
getFileInTestConfigDirectory(
- "_${mediaPrefix}ClientToTServiceToTClientTests$variantName.json"
+ getJsonName(clientToT = true, serviceToT = true, clientTests = true)
)
)
it.jsonClientToTServiceToTServiceTests.set(
getFileInTestConfigDirectory(
- "_${mediaPrefix}ClientToTServiceToTServiceTests$variantName.json"
+ getJsonName(clientToT = true, serviceToT = true, clientTests = false)
)
)
it.totClientApk.set(getFileInTestConfigDirectory("${mediaPrefix}ClientToT$variantName.apk"))
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/IntrinsicTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/IntrinsicTest.kt
index dcc13ab..09bf890 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/IntrinsicTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/IntrinsicTest.kt
@@ -49,6 +49,76 @@
@RunWith(AndroidJUnit4::class)
class IntrinsicTest : LayoutTest() {
@Test
+ fun testMaxIntrinsic_HandleNegative() = with(density) {
+ val positionedLatch = CountDownLatch(2)
+ val size = Ref<IntSize>()
+ val position = Ref<Offset>()
+ val sizeTwo = Ref<IntSize>()
+ val positionTwo = Ref<Offset>()
+ val measurePolicy = object : MeasurePolicy {
+ override fun MeasureScope.measure(
+ measurables: List<Measurable>,
+ constraints: Constraints
+ ): MeasureResult {
+ return layout(0, 0) {}
+ }
+
+ override fun IntrinsicMeasureScope.minIntrinsicHeight(
+ measurables: List<IntrinsicMeasurable>,
+ width: Int
+ ): Int {
+ return -1
+ }
+
+ override fun IntrinsicMeasureScope.minIntrinsicWidth(
+ measurables: List<IntrinsicMeasurable>,
+ height: Int
+ ): Int {
+ return -1
+ }
+
+ override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+ measurables: List<IntrinsicMeasurable>,
+ width: Int
+ ): Int {
+ return -1
+ }
+
+ override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+ measurables: List<IntrinsicMeasurable>,
+ height: Int
+ ): Int {
+ return -1
+ }
+ }
+ show {
+ Column {
+ Layout(modifier = Modifier
+ .width(IntrinsicSize.Min)
+ .height(IntrinsicSize.Min)
+ .saveLayoutInfo(
+ size = size,
+ position = position,
+ positionedLatch = positionedLatch
+ ), measurePolicy = measurePolicy)
+ Layout(modifier = Modifier
+ .width(IntrinsicSize.Max)
+ .height(IntrinsicSize.Max)
+ .saveLayoutInfo(
+ size = sizeTwo,
+ position = positionTwo,
+ positionedLatch = positionedLatch
+ ), measurePolicy = measurePolicy)
+ }
+ }
+ assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
+ assertEquals(IntSize(0.dp.roundToPx(), 0.dp.roundToPx()), size.value)
+ assertEquals(IntSize(0.dp.roundToPx(), 0.dp.roundToPx()), sizeTwo.value)
+ assertEquals(Offset(0f, 0f), position.value)
+ assertEquals(Offset(0f, 0f), positionTwo.value)
+ }
+
+ @Test
fun testMinIntrinsicWidth() = with(density) {
val positionedLatch = CountDownLatch(2)
val minIntrinsicWidthSize = Ref<IntSize>()
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt
index fd13029..91c442b 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt
@@ -3838,6 +3838,53 @@
}
@Test
+ fun testRow_withNoItems_hasCorrectIntrinsicMeasurements() = with(density) {
+ testIntrinsics(
+ @Composable {
+ Row(
+ Modifier.width(IntrinsicSize.Max).height(IntrinsicSize.Max),
+ horizontalArrangement = Arrangement.spacedBy(
+ 48.dp
+ ),
+ ) { }
+ },
+ @Composable {
+ Row(
+ Modifier.width(IntrinsicSize.Min).height(IntrinsicSize.Min),
+ horizontalArrangement = Arrangement.spacedBy(
+ 48.dp
+ ),
+ ) { }
+ },
+ @Composable {
+ Column(
+ Modifier.width(IntrinsicSize.Max).height(IntrinsicSize.Max),
+ verticalArrangement = Arrangement.spacedBy(
+ 48.dp
+ ),
+ ) { }
+ },
+ @Composable {
+ Column(
+ Modifier.width(IntrinsicSize.Min).height(IntrinsicSize.Min),
+ verticalArrangement = Arrangement.spacedBy(
+ 48.dp
+ ),
+ ) { }
+ },
+ ) { minIntrinsicWidth, minIntrinsicHeight, maxIntrinsicWidth, maxIntrinsicHeight ->
+ // Min width.
+ assertEquals(0.toDp().roundToPx(), minIntrinsicWidth(0.toDp().roundToPx()))
+ // Min height.
+ assertEquals(0.toDp().roundToPx(), minIntrinsicHeight(0.toDp().roundToPx()))
+ // Max width.
+ assertEquals(0.toDp().roundToPx(), maxIntrinsicWidth(0.toDp().roundToPx()))
+ // Max height.
+ assertEquals(0.toDp().roundToPx(), maxIntrinsicHeight(0.toDp().roundToPx()))
+ }
+ }
+
+ @Test
fun testRow_withWeightChildren_hasCorrectIntrinsicMeasurements() = with(density) {
testIntrinsics(
@Composable {
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Intrinsic.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Intrinsic.kt
index b957ba4..37fe411 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Intrinsic.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Intrinsic.kt
@@ -162,11 +162,12 @@
measurable: Measurable,
constraints: Constraints
): Constraints {
- val measuredWidth = if (width == IntrinsicSize.Min) {
+ var measuredWidth = if (width == IntrinsicSize.Min) {
measurable.minIntrinsicWidth(constraints.maxHeight)
} else {
measurable.maxIntrinsicWidth(constraints.maxHeight)
}
+ if (measuredWidth < 0) { measuredWidth = 0 }
return Constraints.fixedWidth(measuredWidth)
}
@@ -217,11 +218,12 @@
measurable: Measurable,
constraints: Constraints
): Constraints {
- val measuredHeight = if (height == IntrinsicSize.Min) {
+ var measuredHeight = if (height == IntrinsicSize.Min) {
measurable.minIntrinsicHeight(constraints.maxWidth)
} else {
measurable.maxIntrinsicHeight(constraints.maxWidth)
}
+ if (measuredHeight < 0) { measuredHeight = 0 }
return Constraints.fixedHeight(measuredHeight)
}
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
index ce9028a..39edd26 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
@@ -557,6 +557,7 @@
crossAxisAvailable: Int,
mainAxisSpacing: Int
): Int {
+ if (children.isEmpty()) return 0
var weightUnitSpace = 0
var fixedSpace = 0
var totalWeight = 0f
@@ -581,6 +582,7 @@
mainAxisAvailable: Int,
mainAxisSpacing: Int
): Int {
+ if (children.isEmpty()) return 0
var fixedSpace = min((children.size - 1) * mainAxisSpacing, mainAxisAvailable)
var crossAxisMax = 0
var totalWeight = 0f
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/OverscrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/OverscrollTest.kt
index f0f7f96..826dfbbc 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/OverscrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/OverscrollTest.kt
@@ -24,6 +24,7 @@
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -61,6 +62,7 @@
import com.google.common.truth.Truth.assertWithMessage
import kotlin.math.abs
import kotlinx.coroutines.runBlocking
+import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -786,6 +788,25 @@
}
}
+ @MediumTest
+ @Test
+ fun testOverscrollModifierDrawsOnce() {
+ var drawCount = 0
+ rule.setContent {
+ Spacer(
+ modifier = Modifier.testTag(boxTag)
+ .size(100.dp)
+ .overscroll(ScrollableDefaults.overscrollEffect())
+ .drawBehind {
+ drawCount++
+ }
+ )
+ }
+ rule.runOnIdle {
+ assertEquals(1, drawCount)
+ }
+ }
+
@OptIn(ExperimentalTestApi::class)
@ExperimentalFoundationApi
@MediumTest
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/selection/TextFieldLongClickTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/selection/TextFieldLongClickTest.kt
index 64cb81d..767804ea 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/selection/TextFieldLongClickTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/selection/TextFieldLongClickTest.kt
@@ -42,7 +42,6 @@
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -123,9 +122,8 @@
assertThat(state.text.selectionInChars).isEqualTo(TextRange(4, 7))
}
- @SdkSuppress(minSdkVersion = 23)
@Test
- fun longClickOnWhitespace_selectsNextWord() {
+ fun longClickOnWhitespace_doesNotSelectWhitespace() {
val state = TextFieldState("abc def ghi")
rule.setContent {
BasicTextField2(
@@ -141,7 +139,8 @@
rule.onNode(isSelectionHandle(Handle.SelectionStart)).assertIsDisplayed()
rule.onNode(isSelectionHandle(Handle.SelectionEnd)).assertIsDisplayed()
- assertThat(state.text.selectionInChars).isEqualTo(TextRange(8, 11))
+ assertThat(state.text.selectionInChars).isNotEqualTo(TextRange(7, 8))
+ assertThat(state.text.selectionInChars.collapsed).isFalse()
}
@Test
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/AndroidOverscroll.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/AndroidOverscroll.kt
index 48fccdc..2da447e 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/AndroidOverscroll.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/AndroidOverscroll.kt
@@ -27,9 +27,10 @@
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.neverEqualPolicy
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.DrawModifier
import androidx.compose.ui.geometry.Offset
@@ -123,7 +124,9 @@
allEffects.fastForEach { it.color = overscrollConfig.glowColor.toArgb() }
}
- private val redrawSignal = mutableStateOf(Unit, neverEqualPolicy())
+ // TODO replace with mutableStateOf(Unit, neverEqualPolicy()) after b/291647821 is addressed
+ private var consumeCount = -1
+ private var invalidateCount by mutableIntStateOf(0)
@VisibleForTesting
internal var invalidationEnabled = true
@@ -349,7 +352,7 @@
return
}
this.drawIntoCanvas {
- redrawSignal.value // <-- value read to redraw if needed
+ consumeCount = invalidateCount // <-- value read to redraw if needed
val canvas = it.nativeCanvas
var needsInvalidate = false
// each side workflow:
@@ -435,7 +438,9 @@
private fun invalidateOverscroll() {
if (invalidationEnabled) {
- redrawSignal.value = Unit
+ if (consumeCount == invalidateCount) {
+ invalidateCount++
+ }
}
}
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt
index 013adfe..b4bbc9c 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt
@@ -437,11 +437,9 @@
content: @Composable (PaddingValues) -> Unit
) {
// b/278692145 Remove this once deprecated methods without density are removed
- if (scaffoldState.bottomSheetState.density == null) {
- val density = LocalDensity.current
- SideEffect {
- scaffoldState.bottomSheetState.density = density
- }
+ val density = LocalDensity.current
+ SideEffect {
+ scaffoldState.bottomSheetState.density = density
}
val peekHeightPx = with(LocalDensity.current) { sheetPeekHeight.toPx() }
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
index 6fbc761..bd51ba9 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
@@ -648,11 +648,9 @@
content: @Composable () -> Unit
) {
// b/278692145 Remove this once deprecated methods without density are removed
- if (drawerState.density == null) {
- val density = LocalDensity.current
- SideEffect {
- drawerState.density = density
- }
+ val density = LocalDensity.current
+ SideEffect {
+ drawerState.density = density
}
val scope = rememberCoroutineScope()
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
index dfe23a1..a07039a 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
@@ -564,11 +564,9 @@
content: @Composable () -> Unit
) {
// b/278692145 Remove this once deprecated methods without density are removed
- if (sheetState.density == null) {
- val density = LocalDensity.current
- SideEffect {
- sheetState.density = density
- }
+ val density = LocalDensity.current
+ SideEffect {
+ sheetState.density = density
}
val scope = rememberCoroutineScope()
val orientation = Orientation.Vertical
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index 227827f..615e6a4 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -530,7 +530,7 @@
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class DismissState {
- ctor public DismissState(androidx.compose.material3.DismissValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissValue,java.lang.Boolean> confirmValueChange, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Float,java.lang.Float> positionalThreshold);
+ ctor @Deprecated public DismissState(androidx.compose.material3.DismissValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissValue,java.lang.Boolean> confirmValueChange, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold);
method public suspend Object? dismiss(androidx.compose.material3.DismissDirection direction, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public androidx.compose.material3.DismissValue getCurrentValue();
method public androidx.compose.material3.DismissDirection? getDismissDirection();
@@ -548,7 +548,8 @@
}
public static final class DismissState.Companion {
- method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.DismissState,androidx.compose.material3.DismissValue> Saver(kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissValue,java.lang.Boolean> confirmValueChange, kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Float,java.lang.Float> positionalThreshold);
+ method @Deprecated public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.DismissState,androidx.compose.material3.DismissValue> Saver(kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissValue,java.lang.Boolean> confirmValueChange, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold);
+ method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.DismissState,androidx.compose.material3.DismissValue> Saver(kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissValue,java.lang.Boolean> confirmValueChange, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold, androidx.compose.ui.unit.Density density);
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public enum DismissValue {
@@ -1094,7 +1095,7 @@
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class RangeSliderState {
- ctor public RangeSliderState(optional float initialActiveRangeStart, optional float initialActiveRangeEnd, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.FloatRange,kotlin.Unit>? initialOnValueChange, optional int steps, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished);
+ ctor public RangeSliderState(optional float initialActiveRangeStart, optional float initialActiveRangeEnd, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.FloatRange,kotlin.Unit>? initialOnValueChange, optional @IntRange(from=0L) int steps, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished);
method public float getActiveRangeEnd();
method public float getActiveRangeStart();
method public kotlin.jvm.functions.Function0<kotlin.Unit>? getOnValueChangeFinished();
@@ -1286,8 +1287,12 @@
property public final androidx.compose.foundation.shape.CornerBasedShape small;
}
+ public final class SheetDefaultsKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.material3.SheetState SheetState(boolean skipPartiallyExpanded, androidx.compose.ui.unit.Density density, optional androidx.compose.material3.SheetValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SheetValue,java.lang.Boolean> confirmValueChange, optional boolean skipHiddenState);
+ }
+
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class SheetState {
- ctor public SheetState(boolean skipPartiallyExpanded, optional androidx.compose.material3.SheetValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SheetValue,java.lang.Boolean> confirmValueChange, optional boolean skipHiddenState);
+ ctor @Deprecated public SheetState(boolean skipPartiallyExpanded, optional androidx.compose.material3.SheetValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SheetValue,java.lang.Boolean> confirmValueChange, optional boolean skipHiddenState);
method public suspend Object? expand(kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public androidx.compose.material3.SheetValue getCurrentValue();
method public boolean getHasExpandedState();
@@ -1307,7 +1312,8 @@
}
public static final class SheetState.Companion {
- method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.SheetState,androidx.compose.material3.SheetValue> Saver(boolean skipPartiallyExpanded, kotlin.jvm.functions.Function1<? super androidx.compose.material3.SheetValue,java.lang.Boolean> confirmValueChange);
+ method @Deprecated public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.SheetState,androidx.compose.material3.SheetValue> Saver(boolean skipPartiallyExpanded, kotlin.jvm.functions.Function1<? super androidx.compose.material3.SheetValue,java.lang.Boolean> confirmValueChange);
+ method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.SheetState,androidx.compose.material3.SheetValue> Saver(boolean skipPartiallyExpanded, kotlin.jvm.functions.Function1<? super androidx.compose.material3.SheetValue,java.lang.Boolean> confirmValueChange, androidx.compose.ui.unit.Density density);
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public enum SheetValue {
@@ -1356,11 +1362,11 @@
public final class SliderKt {
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RangeSlider(androidx.compose.material3.RangeSliderState state, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource startInteractionSource, optional androidx.compose.foundation.interaction.MutableInteractionSource endInteractionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.RangeSliderState,kotlin.Unit> startThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.RangeSliderState,kotlin.Unit> endThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.RangeSliderState,kotlin.Unit> track);
- method @androidx.compose.runtime.Composable public static void RangeSlider(kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> value, kotlin.jvm.functions.Function1<? super kotlin.ranges.ClosedFloatingPointRange<java.lang.Float>,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RangeSlider(long value, kotlin.jvm.functions.Function1<? super androidx.compose.material3.FloatRange,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource startInteractionSource, optional androidx.compose.foundation.interaction.MutableInteractionSource endInteractionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.RangeSliderState,kotlin.Unit> startThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.RangeSliderState,kotlin.Unit> endThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.RangeSliderState,kotlin.Unit> track, optional int steps);
+ method @androidx.compose.runtime.Composable public static void RangeSlider(kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> value, kotlin.jvm.functions.Function1<? super kotlin.ranges.ClosedFloatingPointRange<java.lang.Float>,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional @IntRange(from=0L) int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RangeSlider(long value, kotlin.jvm.functions.Function1<? super androidx.compose.material3.FloatRange,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource startInteractionSource, optional androidx.compose.foundation.interaction.MutableInteractionSource endInteractionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.RangeSliderState,kotlin.Unit> startThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.RangeSliderState,kotlin.Unit> endThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.RangeSliderState,kotlin.Unit> track, optional @IntRange(from=0L) int steps);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Slider(androidx.compose.material3.SliderState state, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderState,kotlin.Unit> thumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderState,kotlin.Unit> track);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional int steps, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderState,kotlin.Unit> thumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderState,kotlin.Unit> track, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange);
- method @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional @IntRange(from=0L) int steps, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderState,kotlin.Unit> thumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderState,kotlin.Unit> track, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange);
+ method @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional @IntRange(from=0L) int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
}
@androidx.compose.runtime.Stable public final class SliderPositions {
@@ -1372,7 +1378,7 @@
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class SliderState {
- ctor public SliderState(optional float initialValue, optional kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit>? initialOnValueChange, optional int steps, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished);
+ ctor public SliderState(optional float initialValue, optional kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit>? initialOnValueChange, optional @IntRange(from=0L) int steps, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished);
method public kotlin.jvm.functions.Function0<kotlin.Unit>? getOnValueChangeFinished();
method public int getSteps();
method public float getValue();
@@ -1476,14 +1482,15 @@
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class SwipeToDismissDefaults {
- method public kotlin.jvm.functions.Function2<androidx.compose.ui.unit.Density,java.lang.Float,java.lang.Float> getFixedPositionalThreshold();
- property public final kotlin.jvm.functions.Function2<androidx.compose.ui.unit.Density,java.lang.Float,java.lang.Float> FixedPositionalThreshold;
+ method @androidx.compose.runtime.Composable public kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> getFixedPositionalThreshold();
+ property @androidx.compose.runtime.Composable public final kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> fixedPositionalThreshold;
field public static final androidx.compose.material3.SwipeToDismissDefaults INSTANCE;
}
public final class SwipeToDismissKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.material3.DismissState DismissState(androidx.compose.material3.DismissValue initialValue, androidx.compose.ui.unit.Density density, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissValue,java.lang.Boolean> confirmValueChange, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SwipeToDismiss(androidx.compose.material3.DismissState state, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> background, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> dismissContent, optional androidx.compose.ui.Modifier modifier, optional java.util.Set<? extends androidx.compose.material3.DismissDirection> directions);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.DismissState rememberDismissState(optional androidx.compose.material3.DismissValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissValue,java.lang.Boolean> confirmValueChange, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Float,java.lang.Float> positionalThreshold);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.DismissState rememberDismissState(optional androidx.compose.material3.DismissValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissValue,java.lang.Boolean> confirmValueChange, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold);
}
@androidx.compose.runtime.Immutable public final class SwitchColors {
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index 227827f..615e6a4 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -530,7 +530,7 @@
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class DismissState {
- ctor public DismissState(androidx.compose.material3.DismissValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissValue,java.lang.Boolean> confirmValueChange, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Float,java.lang.Float> positionalThreshold);
+ ctor @Deprecated public DismissState(androidx.compose.material3.DismissValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissValue,java.lang.Boolean> confirmValueChange, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold);
method public suspend Object? dismiss(androidx.compose.material3.DismissDirection direction, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public androidx.compose.material3.DismissValue getCurrentValue();
method public androidx.compose.material3.DismissDirection? getDismissDirection();
@@ -548,7 +548,8 @@
}
public static final class DismissState.Companion {
- method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.DismissState,androidx.compose.material3.DismissValue> Saver(kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissValue,java.lang.Boolean> confirmValueChange, kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Float,java.lang.Float> positionalThreshold);
+ method @Deprecated public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.DismissState,androidx.compose.material3.DismissValue> Saver(kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissValue,java.lang.Boolean> confirmValueChange, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold);
+ method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.DismissState,androidx.compose.material3.DismissValue> Saver(kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissValue,java.lang.Boolean> confirmValueChange, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold, androidx.compose.ui.unit.Density density);
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public enum DismissValue {
@@ -1094,7 +1095,7 @@
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class RangeSliderState {
- ctor public RangeSliderState(optional float initialActiveRangeStart, optional float initialActiveRangeEnd, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.FloatRange,kotlin.Unit>? initialOnValueChange, optional int steps, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished);
+ ctor public RangeSliderState(optional float initialActiveRangeStart, optional float initialActiveRangeEnd, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.FloatRange,kotlin.Unit>? initialOnValueChange, optional @IntRange(from=0L) int steps, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished);
method public float getActiveRangeEnd();
method public float getActiveRangeStart();
method public kotlin.jvm.functions.Function0<kotlin.Unit>? getOnValueChangeFinished();
@@ -1286,8 +1287,12 @@
property public final androidx.compose.foundation.shape.CornerBasedShape small;
}
+ public final class SheetDefaultsKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.material3.SheetState SheetState(boolean skipPartiallyExpanded, androidx.compose.ui.unit.Density density, optional androidx.compose.material3.SheetValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SheetValue,java.lang.Boolean> confirmValueChange, optional boolean skipHiddenState);
+ }
+
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class SheetState {
- ctor public SheetState(boolean skipPartiallyExpanded, optional androidx.compose.material3.SheetValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SheetValue,java.lang.Boolean> confirmValueChange, optional boolean skipHiddenState);
+ ctor @Deprecated public SheetState(boolean skipPartiallyExpanded, optional androidx.compose.material3.SheetValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SheetValue,java.lang.Boolean> confirmValueChange, optional boolean skipHiddenState);
method public suspend Object? expand(kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public androidx.compose.material3.SheetValue getCurrentValue();
method public boolean getHasExpandedState();
@@ -1307,7 +1312,8 @@
}
public static final class SheetState.Companion {
- method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.SheetState,androidx.compose.material3.SheetValue> Saver(boolean skipPartiallyExpanded, kotlin.jvm.functions.Function1<? super androidx.compose.material3.SheetValue,java.lang.Boolean> confirmValueChange);
+ method @Deprecated public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.SheetState,androidx.compose.material3.SheetValue> Saver(boolean skipPartiallyExpanded, kotlin.jvm.functions.Function1<? super androidx.compose.material3.SheetValue,java.lang.Boolean> confirmValueChange);
+ method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.SheetState,androidx.compose.material3.SheetValue> Saver(boolean skipPartiallyExpanded, kotlin.jvm.functions.Function1<? super androidx.compose.material3.SheetValue,java.lang.Boolean> confirmValueChange, androidx.compose.ui.unit.Density density);
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public enum SheetValue {
@@ -1356,11 +1362,11 @@
public final class SliderKt {
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RangeSlider(androidx.compose.material3.RangeSliderState state, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource startInteractionSource, optional androidx.compose.foundation.interaction.MutableInteractionSource endInteractionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.RangeSliderState,kotlin.Unit> startThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.RangeSliderState,kotlin.Unit> endThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.RangeSliderState,kotlin.Unit> track);
- method @androidx.compose.runtime.Composable public static void RangeSlider(kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> value, kotlin.jvm.functions.Function1<? super kotlin.ranges.ClosedFloatingPointRange<java.lang.Float>,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RangeSlider(long value, kotlin.jvm.functions.Function1<? super androidx.compose.material3.FloatRange,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource startInteractionSource, optional androidx.compose.foundation.interaction.MutableInteractionSource endInteractionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.RangeSliderState,kotlin.Unit> startThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.RangeSliderState,kotlin.Unit> endThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.RangeSliderState,kotlin.Unit> track, optional int steps);
+ method @androidx.compose.runtime.Composable public static void RangeSlider(kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> value, kotlin.jvm.functions.Function1<? super kotlin.ranges.ClosedFloatingPointRange<java.lang.Float>,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional @IntRange(from=0L) int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RangeSlider(long value, kotlin.jvm.functions.Function1<? super androidx.compose.material3.FloatRange,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource startInteractionSource, optional androidx.compose.foundation.interaction.MutableInteractionSource endInteractionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.RangeSliderState,kotlin.Unit> startThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.RangeSliderState,kotlin.Unit> endThumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.RangeSliderState,kotlin.Unit> track, optional @IntRange(from=0L) int steps);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Slider(androidx.compose.material3.SliderState state, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderState,kotlin.Unit> thumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderState,kotlin.Unit> track);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional int steps, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderState,kotlin.Unit> thumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderState,kotlin.Unit> track, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange);
- method @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional @IntRange(from=0L) int steps, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderState,kotlin.Unit> thumb, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SliderState,kotlin.Unit> track, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange);
+ method @androidx.compose.runtime.Composable public static void Slider(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional @IntRange(from=0L) int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished, optional androidx.compose.material3.SliderColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
}
@androidx.compose.runtime.Stable public final class SliderPositions {
@@ -1372,7 +1378,7 @@
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class SliderState {
- ctor public SliderState(optional float initialValue, optional kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit>? initialOnValueChange, optional int steps, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished);
+ ctor public SliderState(optional float initialValue, optional kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit>? initialOnValueChange, optional @IntRange(from=0L) int steps, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onValueChangeFinished);
method public kotlin.jvm.functions.Function0<kotlin.Unit>? getOnValueChangeFinished();
method public int getSteps();
method public float getValue();
@@ -1476,14 +1482,15 @@
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class SwipeToDismissDefaults {
- method public kotlin.jvm.functions.Function2<androidx.compose.ui.unit.Density,java.lang.Float,java.lang.Float> getFixedPositionalThreshold();
- property public final kotlin.jvm.functions.Function2<androidx.compose.ui.unit.Density,java.lang.Float,java.lang.Float> FixedPositionalThreshold;
+ method @androidx.compose.runtime.Composable public kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> getFixedPositionalThreshold();
+ property @androidx.compose.runtime.Composable public final kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> fixedPositionalThreshold;
field public static final androidx.compose.material3.SwipeToDismissDefaults INSTANCE;
}
public final class SwipeToDismissKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.material3.DismissState DismissState(androidx.compose.material3.DismissValue initialValue, androidx.compose.ui.unit.Density density, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissValue,java.lang.Boolean> confirmValueChange, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SwipeToDismiss(androidx.compose.material3.DismissState state, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> background, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> dismissContent, optional androidx.compose.ui.Modifier modifier, optional java.util.Set<? extends androidx.compose.material3.DismissDirection> directions);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.DismissState rememberDismissState(optional androidx.compose.material3.DismissValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissValue,java.lang.Boolean> confirmValueChange, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Float,java.lang.Float> positionalThreshold);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.DismissState rememberDismissState(optional androidx.compose.material3.DismissValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.DismissValue,java.lang.Boolean> confirmValueChange, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold);
}
@androidx.compose.runtime.Immutable public final class SwitchColors {
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
index 71aa2de..6735b7d 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
@@ -253,6 +253,7 @@
skipPartiallyExpanded = false,
skipHiddenState = true,
initialValue = SheetValue.PartiallyExpanded,
+ density = rule.density
)
rule.setContent {
scope = rememberCoroutineScope()
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
index 8a34a90..a3fcc88 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
@@ -107,7 +107,7 @@
@Test
fun modalBottomSheet_isDismissedOnTapOutside() {
var showBottomSheet by mutableStateOf(true)
- val sheetState = SheetState(skipPartiallyExpanded = false)
+ val sheetState = SheetState(skipPartiallyExpanded = false, density = rule.density)
rule.setContent {
val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
@@ -294,7 +294,7 @@
@Test
fun modalBottomSheet_shortSheet_isDismissedOnBackPress() {
var showBottomSheet by mutableStateOf(true)
- val sheetState = SheetState(skipPartiallyExpanded = true)
+ val sheetState = SheetState(skipPartiallyExpanded = true, density = rule.density)
rule.setContent {
val dispatcher = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher
@@ -333,7 +333,7 @@
@Test
fun modalBottomSheet_tallSheet_isDismissedOnBackPress() {
var showBottomSheet by mutableStateOf(true)
- val sheetState = SheetState(skipPartiallyExpanded = false)
+ val sheetState = SheetState(skipPartiallyExpanded = false, density = rule.density)
rule.setContent {
val dispatcher = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher
@@ -565,7 +565,7 @@
fun modalBottomSheet_missingAnchors_findsClosest() {
val topTag = "ModalBottomSheetLayout"
var showShortContent by mutableStateOf(false)
- val sheetState = SheetState(skipPartiallyExpanded = false)
+ val sheetState = SheetState(skipPartiallyExpanded = false, density = rule.density)
lateinit var scope: CoroutineScope
rule.setContent {
@@ -809,6 +809,7 @@
lateinit var scope: CoroutineScope
val bottomSheetState = SheetState(
skipPartiallyExpanded = true,
+ density = rule.density
)
rule.setContent {
scope = rememberCoroutineScope()
@@ -1000,7 +1001,7 @@
@Test
fun modalBottomSheet_shortSheet_anchorChangeHandler_previousTargetNotInAnchors_reconciles() {
- val sheetState = SheetState(skipPartiallyExpanded = false)
+ val sheetState = SheetState(skipPartiallyExpanded = false, density = rule.density)
var hasSheetContent by mutableStateOf(false) // Start out with empty sheet content
lateinit var scope: CoroutineScope
rule.setContent {
@@ -1039,7 +1040,7 @@
@Test
fun modalBottomSheet_tallSheet_anchorChangeHandler_previousTargetNotInAnchors_reconciles() {
- val sheetState = SheetState(skipPartiallyExpanded = false)
+ val sheetState = SheetState(skipPartiallyExpanded = false, density = rule.density)
var hasSheetContent by mutableStateOf(false) // Start out with empty sheet content
lateinit var scope: CoroutineScope
rule.setContent {
@@ -1080,7 +1081,7 @@
fun modalBottomSheet_callsOnDismissRequest_onNestedScrollFling() {
var callCount by mutableStateOf(0)
val expectedCallCount = 1
- val sheetState = SheetState(skipPartiallyExpanded = true)
+ val sheetState = SheetState(skipPartiallyExpanded = true, density = rule.density)
val nestedScrollDispatcher = NestedScrollDispatcher()
val nestedScrollConnection = object : NestedScrollConnection {
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/DynamicTonalPalette.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/DynamicTonalPalette.kt
index 231f4d5..bafe835 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/DynamicTonalPalette.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/DynamicTonalPalette.kt
@@ -20,6 +20,7 @@
import android.os.Build
import androidx.annotation.ColorRes
import androidx.annotation.DoNotInline
+import androidx.annotation.FloatRange
import androidx.annotation.RequiresApi
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.colorspace.ColorSpaces
@@ -240,7 +241,7 @@
* @param newLuminance 0 <= newLuminance <= 100; invalid values are corrected.
*/
internal fun Color.setLuminance(
- /*@FloatRange(from = 0.0, to = 100.0)*/
+ @FloatRange(from = 0.0, to = 100.0)
newLuminance: Float
): Color {
if ((newLuminance < 0.0001) or (newLuminance > 99.9999)) {
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt
index 1dfdc5a..abd7fee 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt
@@ -48,6 +48,7 @@
import androidx.compose.runtime.CompositionContext
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -64,6 +65,7 @@
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.AbstractComposeView
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.ViewRootForInspector
@@ -135,6 +137,11 @@
windowInsets: WindowInsets = BottomSheetDefaults.windowInsets,
content: @Composable ColumnScope.() -> Unit,
) {
+ // b/291735717 Remove this once deprecated methods without density are removed
+ val density = LocalDensity.current
+ SideEffect {
+ sheetState.density = density
+ }
val scope = rememberCoroutineScope()
val animateToDismiss: () -> Unit = {
if (sheetState.swipeableState.confirmValueChange(Hidden)) {
@@ -247,8 +254,7 @@
}
} else if (hasPartiallyExpandedState) {
collapse(collapseActionLabel) {
- if (
- swipeableState.confirmValueChange(
+ if (swipeableState.confirmValueChange(
PartiallyExpanded
)
) {
@@ -329,12 +335,12 @@
screenHeight: Float,
onDragStopped: CoroutineScope.(velocity: Float) -> Unit,
) = draggable(
- state = sheetState.swipeableState.swipeDraggableState,
- orientation = Orientation.Vertical,
- enabled = sheetState.isVisible,
- startDragImmediately = sheetState.swipeableState.isAnimationRunning,
- onDragStopped = onDragStopped
- )
+ state = sheetState.swipeableState.swipeDraggableState,
+ orientation = Orientation.Vertical,
+ enabled = sheetState.isVisible,
+ startDragImmediately = sheetState.swipeableState.isAnimationRunning,
+ onDragStopped = onDragStopped
+)
.swipeAnchors(
state = sheetState.swipeableState,
anchorChangeHandler = anchorChangeHandler,
@@ -347,6 +353,7 @@
sheetState.skipPartiallyExpanded -> null
else -> screenHeight / 2f
}
+
Expanded -> if (sheetSize.height != 0) {
max(0f, screenHeight - sheetSize.height)
} else null
@@ -479,8 +486,8 @@
// Flags specific to modal bottom sheet.
flags = flags and (
WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES or
- WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
- ).inv()
+ WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
+ ).inv()
flags = flags or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/BottomSheetScaffold.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/BottomSheetScaffold.kt
index 43e8c1a..0e30d0c 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/BottomSheetScaffold.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/BottomSheetScaffold.kt
@@ -28,6 +28,7 @@
import androidx.compose.material3.SheetValue.Hidden
import androidx.compose.material3.SheetValue.PartiallyExpanded
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -326,6 +327,11 @@
containerColor: Color,
contentColor: Color,
) {
+ // b/291735717 Remove this once deprecated methods without density are removed
+ val density = LocalDensity.current
+ SideEffect {
+ sheetState.density = density
+ }
SubcomposeLayout { constraints ->
val layoutWidth = constraints.maxWidth
val layoutHeight = constraints.maxHeight
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SheetDefaults.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SheetDefaults.kt
index 5a6a3366..e59e502 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SheetDefaults.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SheetDefaults.kt
@@ -38,8 +38,10 @@
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
@@ -48,7 +50,37 @@
/**
* State of a sheet composable, such as [ModalBottomSheet]
*
- * Contains states relating to it's swipe position as well as animations between state values.
+ * Contains states relating to its swipe position as well as animations between state values.
+ *
+ * @param skipPartiallyExpanded Whether the partially expanded state, if the sheet is large
+ * enough, should be skipped. If true, the sheet will always expand to the [Expanded] state and move
+ * to the [Hidden] state if available when hiding the sheet, either programmatically or by user
+ * interaction.
+ * @param initialValue The initial value of the state.
+ * @param density The density that this state can use to convert values to and from dp.
+ * @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
+ * @param skipHiddenState Whether the hidden state should be skipped. If true, the sheet will always
+ * expand to the [Expanded] state and move to the [PartiallyExpanded] if available, either
+ * programmatically or by user interaction.
+ */
+@ExperimentalMaterial3Api
+@Suppress("Deprecation")
+fun SheetState(
+ skipPartiallyExpanded: Boolean,
+ density: Density,
+ initialValue: SheetValue = Hidden,
+ confirmValueChange: (SheetValue) -> Boolean = { true },
+ skipHiddenState: Boolean = false,
+) = SheetState(
+ skipPartiallyExpanded, initialValue, confirmValueChange, skipHiddenState
+).also {
+ it.density = density
+}
+
+/**
+ * State of a sheet composable, such as [ModalBottomSheet]
+ *
+ * Contains states relating to its swipe position as well as animations between state values.
*
* @param skipPartiallyExpanded Whether the partially expanded state, if the sheet is large
* enough, should be skipped. If true, the sheet will always expand to the [Expanded] state and move
@@ -62,7 +94,15 @@
*/
@Stable
@ExperimentalMaterial3Api
-class SheetState(
+class SheetState @Deprecated(
+ message = "This constructor is deprecated. " +
+ "Please use the constructor that provides a [Density]",
+ replaceWith = ReplaceWith(
+ "SheetState(" +
+ "skipPartiallyExpanded, LocalDensity.current, initialValue, " +
+ "confirmValueChange, skipHiddenState)"
+ )
+) constructor(
internal val skipPartiallyExpanded: Boolean,
initialValue: SheetValue = Hidden,
confirmValueChange: (SheetValue) -> Boolean = { true },
@@ -237,16 +277,46 @@
initialValue = initialValue,
animationSpec = SwipeableV2Defaults.AnimationSpec,
confirmValueChange = confirmValueChange,
+ positionalThreshold = { with(requireDensity()) { 56.dp.toPx() } },
+ velocityThreshold = { with(requireDensity()) { 125.dp.toPx() } }
)
internal val offset: Float? get() = swipeableState.offset
+ internal var density: Density? = null
+ private fun requireDensity() = requireNotNull(density) {
+ "SheetState did not have a density attached. Are you using SheetState with " +
+ "BottomSheetScaffold or ModalBottomSheet component?"
+ }
+
companion object {
/**
* The default [Saver] implementation for [SheetState].
*/
fun Saver(
skipPartiallyExpanded: Boolean,
+ confirmValueChange: (SheetValue) -> Boolean,
+ density: Density
+ ) = Saver<SheetState, SheetValue>(
+ save = { it.currentValue },
+ restore = { savedValue ->
+ SheetState(skipPartiallyExpanded, density, savedValue, confirmValueChange)
+ }
+ )
+
+ /**
+ * The default [Saver] implementation for [SheetState].
+ */
+ @Deprecated(
+ message = "This function is deprecated. Please use the overload where Density is" +
+ " provided.",
+ replaceWith = ReplaceWith(
+ "Saver(skipPartiallyExpanded, confirmValueChange, LocalDensity.current)"
+ )
+ )
+ @Suppress("Deprecation")
+ fun Saver(
+ skipPartiallyExpanded: Boolean,
confirmValueChange: (SheetValue) -> Boolean
) = Saver<SheetState, SheetValue>(
save = { it.currentValue },
@@ -287,17 +357,17 @@
/** The default shape for bottom sheets in a [Hidden] state. */
val HiddenShape: Shape
@Composable get() =
- SheetBottomTokens.DockedMinimizedContainerShape.value
+ SheetBottomTokens.DockedMinimizedContainerShape.value
/** The default shape for a bottom sheets in [PartiallyExpanded] and [Expanded] states. */
val ExpandedShape: Shape
@Composable get() =
- SheetBottomTokens.DockedContainerShape.value
+ SheetBottomTokens.DockedContainerShape.value
/** The default container color for a bottom sheet. */
val ContainerColor: Color
@Composable get() =
- SheetBottomTokens.DockedContainerColor.value
+ SheetBottomTokens.DockedContainerColor.value
/** The default elevation for a bottom sheet. */
val Elevation = SheetBottomTokens.DockedModalContainerElevation
@@ -305,7 +375,7 @@
/** The default color of the scrim overlay for background content. */
val ScrimColor: Color
@Composable get() =
- ScrimTokens.ContainerColor.value.copy(ScrimTokens.ContainerOpacity)
+ ScrimTokens.ContainerColor.value.copy(ScrimTokens.ContainerOpacity)
/**
* The default peek height used by [BottomSheetScaffold].
@@ -414,14 +484,23 @@
initialValue: SheetValue = Hidden,
skipHiddenState: Boolean = false,
): SheetState {
+
+ val density = LocalDensity.current
return rememberSaveable(
skipPartiallyExpanded, confirmValueChange,
saver = SheetState.Saver(
skipPartiallyExpanded = skipPartiallyExpanded,
- confirmValueChange = confirmValueChange
+ confirmValueChange = confirmValueChange,
+ density = density
)
) {
- SheetState(skipPartiallyExpanded, initialValue, confirmValueChange, skipHiddenState)
+ SheetState(
+ skipPartiallyExpanded,
+ density,
+ initialValue,
+ confirmValueChange,
+ skipHiddenState
+ )
}
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
index a00e616..32ea286 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
@@ -16,6 +16,7 @@
package androidx.compose.material3
+import androidx.annotation.IntRange
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.MutatePriority
import androidx.compose.foundation.MutatorMutex
@@ -153,7 +154,7 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
- /*@IntRange(from = 0)*/
+ @IntRange(from = 0)
steps: Int = 0,
onValueChangeFinished: (() -> Unit)? = null,
colors: SliderColors = SliderDefaults.colors(),
@@ -249,7 +250,7 @@
onValueChangeFinished: (() -> Unit)? = null,
colors: SliderColors = SliderDefaults.colors(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- /*@IntRange(from = 0)*/
+ @IntRange(from = 0)
steps: Int = 0,
thumb: @Composable (SliderState) -> Unit = {
SliderDefaults.Thumb(
@@ -412,7 +413,7 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
- /*@IntRange(from = 0)*/
+ @IntRange(from = 0)
steps: Int = 0,
onValueChangeFinished: (() -> Unit)? = null,
colors: SliderColors = SliderDefaults.colors()
@@ -541,7 +542,7 @@
rangeSliderState = rangeSliderState
)
},
- /*@IntRange(from = 0)*/
+ @IntRange(from = 0)
steps: Int = 0
) {
val state = remember(
@@ -1789,7 +1790,7 @@
class SliderState(
initialValue: Float = 0f,
initialOnValueChange: ((Float) -> Unit)? = null,
- /*@IntRange(from = 0)*/
+ @IntRange(from = 0)
val steps: Int = 0,
val valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
var onValueChangeFinished: (() -> Unit)? = null
@@ -1909,7 +1910,7 @@
initialActiveRangeStart: Float = 0f,
initialActiveRangeEnd: Float = 1f,
initialOnValueChange: ((FloatRange) -> Unit)? = null,
- /*@IntRange(from = 0)*/
+ @IntRange(from = 0)
val steps: Int = 0,
val valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
var onValueChangeFinished: (() -> Unit)? = null,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeToDismiss.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeToDismiss.kt
index bce5e38..4506e25 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeToDismiss.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeToDismiss.kt
@@ -23,14 +23,17 @@
import androidx.compose.foundation.layout.offset
import androidx.compose.material3.DismissDirection.EndToStart
import androidx.compose.material3.DismissDirection.StartToEnd
+import androidx.compose.material3.DismissState.Companion.Saver
import androidx.compose.material3.DismissValue.Default
import androidx.compose.material3.DismissValue.DismissedToEnd
import androidx.compose.material3.DismissValue.DismissedToStart
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
@@ -80,6 +83,7 @@
* State of the [SwipeToDismiss] composable.
*
* @param initialValue The initial value of the state.
+ * @param density The density that this state can use to convert values to and from dp.
* @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
* @param positionalThreshold The positional threshold to be used when calculating the target state
* while a swipe is in progress and when settling after the swipe ends. This is the distance from
@@ -87,17 +91,49 @@
* subtracted from/to the origin offset. It should always be a positive value.
*/
@ExperimentalMaterial3Api
-class DismissState(
+@Suppress("Deprecation", "PrimitiveInLambda")
+fun DismissState(
+ initialValue: DismissValue,
+ density: Density,
+ confirmValueChange: (DismissValue) -> Boolean = { true },
+ positionalThreshold: (totalDistance: Float) -> Float
+) = DismissState(
+ initialValue = initialValue,
+ confirmValueChange = confirmValueChange,
+ positionalThreshold = positionalThreshold
+).also {
+ it.density = density
+}
+
+/**
+ * State of the [SwipeToDismiss] composable.
+ *
+ * @param initialValue The initial value of the state.
+ * @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
+ * @param positionalThreshold The positional threshold to be used when calculating the target state
+ * while a swipe is in progress and when settling after the swipe ends. This is the distance from
+ * the start of a transition. It will be, depending on the direction of the interaction, added or
+ * subtracted from/to the origin offset. It should always be a positive value.
+ */
+@Suppress("PrimitiveInLambda")
+@ExperimentalMaterial3Api
+class DismissState @Deprecated(
+ message = "This constructor is deprecated. " +
+ "Please use the constructor that provides a [Density]",
+ replaceWith = ReplaceWith(
+ "DismissState(" +
+ "initialValue, LocalDensity.current, confirmValueChange, positionalThreshold)"
+ )
+) constructor(
initialValue: DismissValue,
confirmValueChange: (DismissValue) -> Boolean = { true },
- positionalThreshold: Density.(totalDistance: Float) -> Float =
- SwipeToDismissDefaults.FixedPositionalThreshold,
+ positionalThreshold: (totalDistance: Float) -> Float
) {
internal val swipeableState = SwipeableV2State(
initialValue = initialValue,
confirmValueChange = confirmValueChange,
positionalThreshold = positionalThreshold,
- velocityThreshold = DismissThreshold
+ velocityThreshold = { with(requireDensity()) { DismissThreshold.toPx() } }
)
internal val offset: Float? get() = swipeableState.offset
@@ -132,8 +168,10 @@
* If the composable is settled at the default state, then this will be null. Use this to
* change the background of the [SwipeToDismiss] if you want different actions on each side.
*/
- val dismissDirection: DismissDirection? get() =
- if (offset == 0f || offset == null) null else if (offset!! > 0f) StartToEnd else EndToStart
+ val dismissDirection: DismissDirection?
+ get() = if (offset == 0f || offset == null)
+ null
+ else if (offset!! > 0f) StartToEnd else EndToStart
/**
* Whether the component has been dismissed in the given [direction].
@@ -173,19 +211,52 @@
swipeableState.animateTo(targetValue = targetValue)
}
+ internal var density: Density? = null
+ private fun requireDensity() = requireNotNull(density) {
+ "DismissState did not have a density attached. Are you using DismissState with " +
+ "the SwipeToDismiss component?"
+ }
+
companion object {
+
/**
* The default [Saver] implementation for [DismissState].
*/
fun Saver(
confirmValueChange: (DismissValue) -> Boolean,
- positionalThreshold: Density.(totalDistance: Float) -> Float,
+ positionalThreshold: (totalDistance: Float) -> Float,
+ density: Density
) =
Saver<DismissState, DismissValue>(
save = { it.currentValue },
restore = {
DismissState(
- it, confirmValueChange, positionalThreshold)
+ it, density, confirmValueChange, positionalThreshold
+ )
+ }
+ )
+
+ /**
+ * The default [Saver] implementation for [DismissState].
+ */
+ @Deprecated(
+ message = "This function is deprecated. Please use the overload where Density is" +
+ " provided.",
+ replaceWith = ReplaceWith(
+ "Saver(confirmValueChange, positionalThreshold, LocalDensity.current)"
+ )
+ )
+ @Suppress("Deprecation")
+ fun Saver(
+ confirmValueChange: (DismissValue) -> Boolean,
+ positionalThreshold: (totalDistance: Float) -> Float,
+ ) =
+ Saver<DismissState, DismissValue>(
+ save = { it.currentValue },
+ restore = {
+ DismissState(
+ it, confirmValueChange, positionalThreshold
+ )
}
)
}
@@ -201,17 +272,24 @@
* the start of a transition. It will be, depending on the direction of the interaction, added or
* subtracted from/to the origin offset. It should always be a positive value.
*/
+@Suppress("PrimitiveInLambda")
@Composable
@ExperimentalMaterial3Api
fun rememberDismissState(
initialValue: DismissValue = Default,
confirmValueChange: (DismissValue) -> Boolean = { true },
- positionalThreshold: Density.(totalDistance: Float) -> Float =
- SwipeToDismissDefaults.FixedPositionalThreshold,
+ positionalThreshold: (totalDistance: Float) -> Float =
+ SwipeToDismissDefaults.fixedPositionalThreshold,
): DismissState {
+ val density = LocalDensity.current
return rememberSaveable(
- saver = DismissState.Saver(confirmValueChange, positionalThreshold)) {
- DismissState(initialValue, confirmValueChange, positionalThreshold)
+ saver = DismissState.Saver(
+ confirmValueChange = confirmValueChange,
+ density = density,
+ positionalThreshold = positionalThreshold
+ )
+ ) {
+ DismissState(initialValue, density, confirmValueChange, positionalThreshold)
}
}
@@ -236,6 +314,13 @@
modifier: Modifier = Modifier,
directions: Set<DismissDirection> = setOf(EndToStart, StartToEnd),
) {
+
+ // b/278692145 Remove this once deprecated methods without density are removed
+ val density = LocalDensity.current
+ SideEffect {
+ state.density = density
+ }
+
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
Box(
@@ -257,23 +342,27 @@
Default -> 0f
}
}
- ) {
- Row(
- content = background,
- modifier = Modifier.matchParentSize()
- )
- Row(
- content = dismissContent,
- modifier = Modifier.offset { IntOffset(state.requireOffset().roundToInt(), 0) }
- )
- }
+ ) {
+ Row(
+ content = background,
+ modifier = Modifier.matchParentSize()
+ )
+ Row(
+ content = dismissContent,
+ modifier = Modifier.offset { IntOffset(state.requireOffset().roundToInt(), 0) }
+ )
+ }
}
/** Contains default values for [SwipeToDismiss] and [DismissState]. */
+@Suppress("PrimitiveInLambda")
@ExperimentalMaterial3Api
object SwipeToDismissDefaults {
/** Default positional threshold of 56.dp for [DismissState]. */
- val FixedPositionalThreshold: Density.(totalDistance: Float) -> Float = { _ -> 56.dp.toPx() }
+ val fixedPositionalThreshold: (totalDistance: Float) -> Float
+ @Composable get() = with(LocalDensity.current) {
+ { 56.dp.toPx() }
+ }
}
private val DismissThreshold = 125.dp
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Swipeable.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Swipeable.kt
index e135720..f06b0e3 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Swipeable.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Swipeable.kt
@@ -16,6 +16,7 @@
package androidx.compose.material3
+import androidx.annotation.FloatRange
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.SpringSpec
@@ -431,7 +432,7 @@
internal class SwipeProgress<T>(
val from: T,
val to: T,
- /*@FloatRange(from = 0.0, to = 1.0)*/
+ @FloatRange(from = 0.0, to = 1.0)
val fraction: Float
) {
override fun equals(other: Any?): Boolean {
@@ -536,7 +537,7 @@
* the new anchor. The target anchor is calculated based on the provided positional [thresholds].
*
* Swiping is constrained between the minimum and maximum anchors. If the user attempts to swipe
- * past these bounds, a resistance effect will be applied by default. The amount of resistance at
+ * past these bounds, a resistance effect will bfe applied by default. The amount of resistance at
* each edge is specified by the [resistance] config. To disable all resistance, set it to `null`.
*
* @param T The type of the state.
@@ -650,7 +651,7 @@
@Immutable
@ExperimentalMaterial3Api
internal data class FractionalThreshold(
- /*@FloatRange(from = 0.0, to = 1.0)*/
+ @FloatRange(from = 0.0, to = 1.0)
private val fraction: Float
) : ThresholdConfig {
override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float {
@@ -682,11 +683,11 @@
*/
@Immutable
internal class ResistanceConfig(
- /*@FloatRange(from = 0.0, fromInclusive = false)*/
+ @FloatRange(from = 0.0, fromInclusive = false)
val basis: Float,
- /*@FloatRange(from = 0.0)*/
+ @FloatRange(from = 0.0)
val factorAtMin: Float = StandardResistanceFactor,
- /*@FloatRange(from = 0.0)*/
+ @FloatRange(from = 0.0)
val factorAtMax: Float = StandardResistanceFactor
) {
fun computeResistance(overflow: Float): Float {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt
index 8db36b9..dfead31 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt
@@ -18,6 +18,7 @@
package androidx.compose.material3
+import androidx.annotation.FloatRange
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.SpringSpec
import androidx.compose.animation.core.animate
@@ -38,17 +39,8 @@
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-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.layout.OnRemeasuredModifier
-import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.platform.InspectorValueInfo
-import androidx.compose.ui.platform.debugInspectorInfo
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import kotlin.math.abs
@@ -111,37 +103,27 @@
possibleValues: Set<T>,
anchorChangeHandler: AnchorChangeHandler<T>? = null,
calculateAnchor: (value: T, layoutSize: IntSize) -> Float?,
-) = this.then(SwipeAnchorsModifier(
- onDensityChanged = { state.density = it },
- onSizeChanged = { layoutSize ->
- val previousAnchors = state.anchors
- val newAnchors = mutableMapOf<T, Float>()
- possibleValues.forEach {
- val anchorValue = calculateAnchor(it, layoutSize)
- if (anchorValue != null) {
- newAnchors[it] = anchorValue
- }
+) = onSizeChanged { layoutSize ->
+ val previousAnchors = state.anchors
+ val newAnchors = mutableMapOf<T, Float>()
+ possibleValues.forEach {
+ val anchorValue = calculateAnchor(it, layoutSize)
+ if (anchorValue != null) {
+ newAnchors[it] = anchorValue
}
- if (previousAnchors != newAnchors) {
- val previousTarget = state.targetValue
- val stateRequiresCleanup = state.updateAnchors(newAnchors)
- if (stateRequiresCleanup) {
- anchorChangeHandler?.onAnchorsChanged(
- previousTarget,
- previousAnchors,
- newAnchors
- )
- }
- }
- },
- inspectorInfo = debugInspectorInfo {
- name = "swipeAnchors"
- properties["state"] = state
- properties["possibleValues"] = possibleValues
- properties["anchorChangeHandler"] = anchorChangeHandler
- properties["calculateAnchor"] = calculateAnchor
}
-))
+ if (previousAnchors != newAnchors) {
+ val previousTarget = state.targetValue
+ val stateRequiresCleanup = state.updateAnchors(newAnchors)
+ if (stateRequiresCleanup) {
+ anchorChangeHandler?.onAnchorsChanged(
+ previousTarget,
+ previousAnchors,
+ newAnchors
+ )
+ }
+ }
+}
/**
* State of the [swipeableV2] modifier.
@@ -153,24 +135,23 @@
* @param initialValue The initial value of the state.
* @param animationSpec The default animation that will be used to animate to a new state.
* @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
- * @param positionalThreshold The positional threshold to be used when calculating the target state
- * while a swipe is in progress and when settling after the swipe ends. This is the distance from
- * the start of a transition. It will be, depending on the direction of the interaction, added or
- * subtracted from/to the origin offset. It should always be a positive value. See the
- * [fractionalPositionalThreshold] and [fixedPositionalThreshold] methods.
- * @param velocityThreshold The velocity threshold (in dp per second) that the end velocity has to
+ * @param positionalThreshold The positional threshold, in px, to be used when calculating the
+ * target state while a swipe is in progress and when settling after the swipe ends. This is the
+ * distance from the start of a transition. It will be, depending on the direction of the
+ * interaction, added or subtracted from/to the origin offset. It should always be a positive value.
+ * @param velocityThreshold The velocity threshold (in px per second) that the end velocity has to
* exceed in order to animate to the next state, even if the [positionalThreshold] has not been
* reached.
*/
+@Suppress("PrimitiveInLambda")
@Stable
@ExperimentalMaterial3Api
internal class SwipeableV2State<T>(
initialValue: T,
+ internal val positionalThreshold: (totalDistance: Float) -> Float,
+ internal val velocityThreshold: () -> Float,
internal val animationSpec: AnimationSpec<Float> = SwipeableV2Defaults.AnimationSpec,
internal val confirmValueChange: (newValue: T) -> Boolean = { true },
- internal val positionalThreshold: Density.(totalDistance: Float) -> Float =
- SwipeableV2Defaults.PositionalThreshold,
- internal val velocityThreshold: Dp = SwipeableV2Defaults.VelocityThreshold,
) {
private val swipeMutex = InternalMutatorMutex()
@@ -250,7 +231,7 @@
* The fraction of the progress going from [currentValue] to [targetValue], within [0f..1f]
* bounds.
*/
- /*@FloatRange(from = 0f, to = 1f)*/
+ @get:FloatRange(from = 0.0, to = 1.0)
val progress: Float by derivedStateOf {
val a = anchors[currentValue] ?: 0f
val b = anchors[targetValue] ?: 0f
@@ -287,8 +268,6 @@
internal var anchors by mutableStateOf(emptyMap<T, Float>())
- internal var density: Density? = null
-
/**
* Update the anchors.
* If the previous set of anchors was empty, attempt to update the offset to match the initial
@@ -417,8 +396,7 @@
): T {
val currentAnchors = anchors
val currentAnchor = currentAnchors[currentValue]
- val currentDensity = requireDensity()
- val velocityThresholdPx = with(currentDensity) { velocityThreshold.toPx() }
+ val velocityThresholdPx = velocityThreshold()
return if (currentAnchor == offset || currentAnchor == null) {
currentValue
} else if (currentAnchor < offset) {
@@ -428,7 +406,7 @@
} else {
val upper = currentAnchors.closestAnchor(offset, true)
val distance = abs(currentAnchors.getValue(upper) - currentAnchor)
- val relativeThreshold = abs(positionalThreshold(currentDensity, distance))
+ val relativeThreshold = abs(positionalThreshold(distance))
val absoluteThreshold = abs(currentAnchor + relativeThreshold)
if (offset < absoluteThreshold) currentValue else upper
}
@@ -439,7 +417,7 @@
} else {
val lower = currentAnchors.closestAnchor(offset, false)
val distance = abs(currentAnchor - currentAnchors.getValue(lower))
- val relativeThreshold = abs(positionalThreshold(currentDensity, distance))
+ val relativeThreshold = abs(positionalThreshold(distance))
val absoluteThreshold = abs(currentAnchor - relativeThreshold)
if (offset < 0) {
// For negative offsets, larger absolute thresholds are closer to lower anchors
@@ -452,11 +430,6 @@
}
}
- private fun requireDensity() = requireNotNull(density) {
- "SwipeableState did not have a density attached. Are you using Modifier.swipeable with " +
- "this=$this SwipeableState?"
- }
-
private suspend fun swipe(
swipePriority: MutatePriority = MutatePriority.Default,
action: suspend () -> Unit
@@ -490,8 +463,8 @@
fun <T : Any> Saver(
animationSpec: AnimationSpec<Float>,
confirmValueChange: (T) -> Boolean,
- positionalThreshold: Density.(distance: Float) -> Float,
- velocityThreshold: Dp
+ positionalThreshold: (distance: Float) -> Float,
+ velocityThreshold: () -> Float
) = Saver<SwipeableV2State<T>, T>(
save = { it.currentValue },
restore = {
@@ -514,6 +487,7 @@
* @param animationSpec The default animation that will be used to animate to a new value.
* @param confirmValueChange Optional callback invoked to confirm or veto a pending value change.
*/
+@Suppress("PrimitiveInLambda")
@Composable
@ExperimentalMaterial3Api
internal fun <T : Any> rememberSwipeableV2State(
@@ -521,51 +495,32 @@
animationSpec: AnimationSpec<Float> = SwipeableV2Defaults.AnimationSpec,
confirmValueChange: (newValue: T) -> Boolean = { true }
): SwipeableV2State<T> {
+ val positionalThreshold = SwipeableV2Defaults.positionalThreshold
+ val velocityThreshold = SwipeableV2Defaults.velocityThreshold
+
return rememberSaveable(
- initialValue, animationSpec, confirmValueChange,
+ initialValue, animationSpec, confirmValueChange, positionalThreshold, velocityThreshold,
saver = SwipeableV2State.Saver(
animationSpec = animationSpec,
confirmValueChange = confirmValueChange,
- positionalThreshold = SwipeableV2Defaults.PositionalThreshold,
- velocityThreshold = SwipeableV2Defaults.VelocityThreshold
+ positionalThreshold = positionalThreshold,
+ velocityThreshold = velocityThreshold
),
) {
SwipeableV2State(
initialValue = initialValue,
animationSpec = animationSpec,
confirmValueChange = confirmValueChange,
- positionalThreshold = SwipeableV2Defaults.PositionalThreshold,
- velocityThreshold = SwipeableV2Defaults.VelocityThreshold
+ positionalThreshold = positionalThreshold,
+ velocityThreshold = velocityThreshold
)
}
}
/**
- * Expresses a fixed positional threshold of [threshold] dp. This will be the distance from an
- * anchor that needs to be reached for [SwipeableV2State] to settle to the next closest anchor.
- *
- * @see [fractionalPositionalThreshold] for a fractional positional threshold
- */
-@ExperimentalMaterial3Api
-internal fun fixedPositionalThreshold(threshold: Dp): Density.(distance: Float) -> Float = {
- threshold.toPx()
-}
-
-/**
- * Expresses a relative positional threshold of the [fraction] of the distance to the closest anchor
- * in the current direction. This will be the distance from an anchor that needs to be reached for
- * [SwipeableV2State] to settle to the next closest anchor.
- *
- * @see [fixedPositionalThreshold] for a fixed positional threshold
- */
-@ExperimentalMaterial3Api
-internal fun fractionalPositionalThreshold(
- fraction: Float
-): Density.(distance: Float) -> Float = { distance -> distance * fraction }
-
-/**
* Contains useful defaults for [swipeableV2] and [SwipeableV2State].
*/
+@Suppress("PrimitiveInLambda")
@Stable
@ExperimentalMaterial3Api
internal object SwipeableV2Defaults {
@@ -579,14 +534,17 @@
* The default velocity threshold (1.8 dp per millisecond) used by [rememberSwipeableV2State].
*/
@ExperimentalMaterial3Api
- val VelocityThreshold: Dp = 125.dp
+ val velocityThreshold: () -> Float
+ @Composable get() = with(LocalDensity.current) { { 125.dp.toPx() } }
/**
* The default positional threshold (56 dp) used by [rememberSwipeableV2State]
*/
@ExperimentalMaterial3Api
- val PositionalThreshold: Density.(totalDistance: Float) -> Float =
- fixedPositionalThreshold(56.dp)
+ val positionalThreshold: (totalDistance: Float) -> Float
+ @Composable get() = with(LocalDensity.current) {
+ { 56.dp.toPx() }
+ }
/**
* A [AnchorChangeHandler] implementation that attempts to reconcile an in-progress animation
@@ -646,37 +604,6 @@
)
}
-@Stable
-private class SwipeAnchorsModifier(
- private val onDensityChanged: (density: Density) -> Unit,
- private val onSizeChanged: (layoutSize: IntSize) -> Unit,
- inspectorInfo: InspectorInfo.() -> Unit,
-) : LayoutModifier, OnRemeasuredModifier, InspectorValueInfo(inspectorInfo) {
-
- private var lastDensity: Float = -1f
- private var lastFontScale: Float = -1f
-
- override fun MeasureScope.measure(
- measurable: Measurable,
- constraints: Constraints
- ): MeasureResult {
- if (density != lastDensity || fontScale != lastFontScale) {
- onDensityChanged(Density(density, fontScale))
- lastDensity = density
- lastFontScale = fontScale
- }
- val placeable = measurable.measure(constraints)
- return layout(placeable.width, placeable.height) { placeable.place(0, 0) }
- }
-
- override fun onRemeasured(size: IntSize) {
- onSizeChanged(size)
- }
-
- override fun toString() = "SwipeAnchorsModifierImpl(updateDensity=$onDensityChanged, " +
- "onSizeChanged=$onSizeChanged)"
-}
-
private fun <T> Map<T, Float>.closestAnchor(
offset: Float = 0f,
searchUpwards: Boolean = false
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
index 8d021e3..f99c430 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
@@ -543,6 +543,25 @@
}
}
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun testVectorStrokeWidth() {
+ val strokeWidth = mutableStateOf(100)
+ rule.setContent {
+ VectorStroke(strokeWidth = strokeWidth.value)
+ }
+ takeScreenShot(200).apply {
+ assertEquals(Color.Yellow.toArgb(), getPixel(100, 25))
+ assertEquals(Color.Blue.toArgb(), getPixel(100, 75))
+ }
+ rule.runOnUiThread { strokeWidth.value = 200 }
+ rule.waitForIdle()
+ takeScreenShot(200).apply {
+ assertEquals(Color.Yellow.toArgb(), getPixel(100, 25))
+ assertEquals(Color.Yellow.toArgb(), getPixel(100, 75))
+ }
+ }
+
@Composable
private fun VectorTint(
size: Int = 200,
@@ -681,6 +700,47 @@
}
@Composable
+ private fun VectorStroke(
+ size: Int = 200,
+ strokeWidth: Int = 100,
+ minimumSize: Int = size,
+ alignment: Alignment = Alignment.Center
+ ) {
+ val sizePx = size.toFloat()
+ val sizeDp = (size / LocalDensity.current.density).dp
+ val strokeWidthPx = strokeWidth.toFloat()
+ val background = Modifier.paint(
+ rememberVectorPainter(
+ defaultWidth = sizeDp,
+ defaultHeight = sizeDp,
+ autoMirror = false
+ ) { _, _ ->
+ Path(
+ pathData = PathData {
+ lineTo(sizePx, 0.0f)
+ lineTo(sizePx, sizePx)
+ lineTo(0.0f, sizePx)
+ close()
+ },
+ fill = SolidColor(Color.Blue)
+ )
+ // A thick stroke
+ Path(
+ pathData = PathData {
+ moveTo(0.0f, 0.0f)
+ lineTo(sizePx, 0.0f)
+ },
+ stroke = SolidColor(Color.Yellow),
+ strokeLineWidth = strokeWidthPx,
+ )
+ },
+ alignment = alignment
+ )
+ AtLeastSize(size = minimumSize, modifier = background) {
+ }
+ }
+
+ @Composable
private fun VectorMirror(size: Int): VectorPainter {
val sizePx = size.toFloat()
val sizeDp = (size / LocalDensity.current.density).dp
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/Vector.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/Vector.kt
index 9c256c7..2eed50f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/Vector.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/Vector.kt
@@ -214,6 +214,7 @@
var strokeLineWidth = DefaultStrokeLineWidth
set(value) {
field = value
+ isStrokeDirty = true
invalidate()
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt
index a6940e0..648babc7 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt
@@ -219,6 +219,7 @@
return next
}
+ // TODO replace with mutableStateOf(Unit, neverEqualPolicy()) after b/291647821 is addressed
private var invalidateCount by mutableIntStateOf(0)
@Composable
diff --git a/constraintlayout/constraintlayout-compose/api/current.txt b/constraintlayout/constraintlayout-compose/api/current.txt
index 6e529b9..3eafaf2 100644
--- a/constraintlayout/constraintlayout-compose/api/current.txt
+++ b/constraintlayout/constraintlayout-compose/api/current.txt
@@ -480,29 +480,6 @@
property public final androidx.constraintlayout.compose.VerticalAnchorable start;
}
- public final class InvalidationStrategy {
- ctor public InvalidationStrategy(optional androidx.constraintlayout.compose.InvalidationStrategy.OnIncomingConstraints? onIncomingConstraints, kotlin.jvm.functions.Function0<kotlin.Unit>? onObservedStateChange);
- method public androidx.constraintlayout.compose.InvalidationStrategy.OnIncomingConstraints? getOnIncomingConstraints();
- method public kotlin.jvm.functions.Function0<kotlin.Unit>? getOnObservedStateChange();
- property public final androidx.constraintlayout.compose.InvalidationStrategy.OnIncomingConstraints? onIncomingConstraints;
- property public final kotlin.jvm.functions.Function0<kotlin.Unit>? onObservedStateChange;
- field public static final androidx.constraintlayout.compose.InvalidationStrategy.Companion Companion;
- }
-
- public static final class InvalidationStrategy.Companion {
- method public androidx.constraintlayout.compose.InvalidationStrategy getDefaultInvalidationStrategy();
- property public final androidx.constraintlayout.compose.InvalidationStrategy DefaultInvalidationStrategy;
- }
-
- public static fun interface InvalidationStrategy.OnIncomingConstraints {
- method public operator boolean invoke(androidx.constraintlayout.compose.InvalidationStrategyScope, long old, long new);
- }
-
- public final class InvalidationStrategyScope {
- method public boolean fixedHeightRate(long oldConstraints, long newConstraints, int skipCount, int threshold);
- method public boolean fixedWidthRate(long oldConstraints, long newConstraints, int skipCount, int threshold);
- }
-
public final class KeyAttributeScope extends androidx.constraintlayout.compose.BaseKeyFrameScope {
method public float getAlpha();
method public float getRotationX();
@@ -662,9 +639,9 @@
}
public final class MotionLayoutKt {
- method @androidx.compose.runtime.Composable public static inline void MotionLayout(androidx.constraintlayout.compose.ConstraintSet start, androidx.constraintlayout.compose.ConstraintSet end, float progress, optional androidx.compose.ui.Modifier modifier, optional androidx.constraintlayout.compose.Transition? transition, optional int debugFlags, optional int optimizationLevel, optional androidx.constraintlayout.compose.InvalidationStrategy invalidationStrategy, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static inline void MotionLayout(androidx.constraintlayout.compose.MotionScene motionScene, float progress, optional androidx.compose.ui.Modifier modifier, optional String transitionName, optional int debugFlags, optional int optimizationLevel, optional androidx.constraintlayout.compose.InvalidationStrategy invalidationStrategy, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static inline void MotionLayout(androidx.constraintlayout.compose.MotionScene motionScene, String? constraintSetName, androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? finishedAnimationListener, optional int debugFlags, optional int optimizationLevel, optional androidx.constraintlayout.compose.InvalidationStrategy invalidationStrategy, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static inline void MotionLayout(androidx.constraintlayout.compose.ConstraintSet start, androidx.constraintlayout.compose.ConstraintSet end, float progress, optional androidx.compose.ui.Modifier modifier, optional androidx.constraintlayout.compose.Transition? transition, optional int debugFlags, optional int optimizationLevel, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static inline void MotionLayout(androidx.constraintlayout.compose.MotionScene motionScene, float progress, optional androidx.compose.ui.Modifier modifier, optional String transitionName, optional int debugFlags, optional int optimizationLevel, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static inline void MotionLayout(androidx.constraintlayout.compose.MotionScene motionScene, String? constraintSetName, androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? finishedAnimationListener, optional int debugFlags, optional int optimizationLevel, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
}
@androidx.compose.foundation.layout.LayoutScopeMarker public final class MotionLayoutScope {
diff --git a/constraintlayout/constraintlayout-compose/api/restricted_current.txt b/constraintlayout/constraintlayout-compose/api/restricted_current.txt
index af5cd2f..60b18d4 100644
--- a/constraintlayout/constraintlayout-compose/api/restricted_current.txt
+++ b/constraintlayout/constraintlayout-compose/api/restricted_current.txt
@@ -525,29 +525,6 @@
property public final androidx.constraintlayout.compose.VerticalAnchorable start;
}
- public final class InvalidationStrategy {
- ctor public InvalidationStrategy(optional androidx.constraintlayout.compose.InvalidationStrategy.OnIncomingConstraints? onIncomingConstraints, kotlin.jvm.functions.Function0<kotlin.Unit>? onObservedStateChange);
- method public androidx.constraintlayout.compose.InvalidationStrategy.OnIncomingConstraints? getOnIncomingConstraints();
- method public kotlin.jvm.functions.Function0<kotlin.Unit>? getOnObservedStateChange();
- property public final androidx.constraintlayout.compose.InvalidationStrategy.OnIncomingConstraints? onIncomingConstraints;
- property public final kotlin.jvm.functions.Function0<kotlin.Unit>? onObservedStateChange;
- field public static final androidx.constraintlayout.compose.InvalidationStrategy.Companion Companion;
- }
-
- public static final class InvalidationStrategy.Companion {
- method public androidx.constraintlayout.compose.InvalidationStrategy getDefaultInvalidationStrategy();
- property public final androidx.constraintlayout.compose.InvalidationStrategy DefaultInvalidationStrategy;
- }
-
- public static fun interface InvalidationStrategy.OnIncomingConstraints {
- method public operator boolean invoke(androidx.constraintlayout.compose.InvalidationStrategyScope, long old, long new);
- }
-
- public final class InvalidationStrategyScope {
- method public boolean fixedHeightRate(long oldConstraints, long newConstraints, int skipCount, int threshold);
- method public boolean fixedWidthRate(long oldConstraints, long newConstraints, int skipCount, int threshold);
- }
-
public final class KeyAttributeScope extends androidx.constraintlayout.compose.BaseKeyFrameScope {
method public float getAlpha();
method public float getRotationX();
@@ -745,12 +722,12 @@
}
public final class MotionLayoutKt {
- method @androidx.compose.runtime.Composable public static inline void MotionLayout(androidx.constraintlayout.compose.ConstraintSet start, androidx.constraintlayout.compose.ConstraintSet end, float progress, optional androidx.compose.ui.Modifier modifier, optional androidx.constraintlayout.compose.Transition? transition, optional int debugFlags, optional int optimizationLevel, optional androidx.constraintlayout.compose.InvalidationStrategy invalidationStrategy, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static inline void MotionLayout(androidx.constraintlayout.compose.MotionScene motionScene, float progress, optional androidx.compose.ui.Modifier modifier, optional String transitionName, optional int debugFlags, optional int optimizationLevel, optional androidx.constraintlayout.compose.InvalidationStrategy invalidationStrategy, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static inline void MotionLayout(androidx.constraintlayout.compose.MotionScene motionScene, String? constraintSetName, androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? finishedAnimationListener, optional int debugFlags, optional int optimizationLevel, optional androidx.constraintlayout.compose.InvalidationStrategy invalidationStrategy, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static void MotionLayoutCore(androidx.constraintlayout.compose.ConstraintSet start, androidx.constraintlayout.compose.ConstraintSet end, androidx.constraintlayout.compose.Transition? transition, float progress, androidx.constraintlayout.compose.LayoutInformationReceiver? informationReceiver, int optimizationLevel, boolean showBounds, boolean showPaths, boolean showKeyPositions, androidx.compose.ui.Modifier modifier, androidx.compose.runtime.MutableState<kotlin.Unit> contentTracker, androidx.compose.ui.node.Ref<androidx.constraintlayout.compose.CompositionSource> compositionSource, androidx.constraintlayout.compose.InvalidationStrategy invalidationStrategy, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static void MotionLayoutCore(androidx.constraintlayout.compose.MotionScene motionScene, float progress, String transitionName, int optimizationLevel, int debugFlags, androidx.compose.ui.Modifier modifier, androidx.compose.runtime.MutableState<kotlin.Unit> contentTracker, androidx.compose.ui.node.Ref<androidx.constraintlayout.compose.CompositionSource> compositionSource, androidx.constraintlayout.compose.InvalidationStrategy invalidationStrategy, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static void MotionLayoutCore(androidx.constraintlayout.compose.MotionScene motionScene, String? constraintSetName, androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? finishedAnimationListener, optional int debugFlags, optional int optimizationLevel, androidx.compose.runtime.MutableState<kotlin.Unit> contentTracker, androidx.compose.ui.node.Ref<androidx.constraintlayout.compose.CompositionSource> compositionSource, androidx.constraintlayout.compose.InvalidationStrategy invalidationStrategy, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static inline void MotionLayout(androidx.constraintlayout.compose.ConstraintSet start, androidx.constraintlayout.compose.ConstraintSet end, float progress, optional androidx.compose.ui.Modifier modifier, optional androidx.constraintlayout.compose.Transition? transition, optional int debugFlags, optional int optimizationLevel, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static inline void MotionLayout(androidx.constraintlayout.compose.MotionScene motionScene, float progress, optional androidx.compose.ui.Modifier modifier, optional String transitionName, optional int debugFlags, optional int optimizationLevel, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static inline void MotionLayout(androidx.constraintlayout.compose.MotionScene motionScene, String? constraintSetName, androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? finishedAnimationListener, optional int debugFlags, optional int optimizationLevel, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static void MotionLayoutCore(androidx.constraintlayout.compose.ConstraintSet start, androidx.constraintlayout.compose.ConstraintSet end, androidx.constraintlayout.compose.Transition? transition, float progress, androidx.constraintlayout.compose.LayoutInformationReceiver? informationReceiver, int optimizationLevel, boolean showBounds, boolean showPaths, boolean showKeyPositions, androidx.compose.ui.Modifier modifier, androidx.compose.runtime.State<kotlin.Unit> contentTracker, androidx.compose.ui.node.Ref<androidx.constraintlayout.compose.CompositionSource> compositionSource, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static void MotionLayoutCore(androidx.constraintlayout.compose.MotionScene motionScene, float progress, String transitionName, int optimizationLevel, int debugFlags, androidx.compose.ui.Modifier modifier, androidx.compose.runtime.State<kotlin.Unit> contentTracker, androidx.compose.ui.node.Ref<androidx.constraintlayout.compose.CompositionSource> compositionSource, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static void MotionLayoutCore(androidx.constraintlayout.compose.MotionScene motionScene, String? constraintSetName, androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? finishedAnimationListener, optional int debugFlags, optional int optimizationLevel, androidx.compose.runtime.State<kotlin.Unit> contentTracker, androidx.compose.ui.node.Ref<androidx.constraintlayout.compose.CompositionSource> compositionSource, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
}
@androidx.compose.foundation.layout.LayoutScopeMarker public final class MotionLayoutScope {
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark-target/src/main/java/androidx/constraintlayout/compose/integration/macrobenchmark/target/MotionLayoutActivity.kt b/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark-target/src/main/java/androidx/constraintlayout/compose/integration/macrobenchmark/target/MotionLayoutActivity.kt
index d3d54ab..91a28e8 100644
--- a/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark-target/src/main/java/androidx/constraintlayout/compose/integration/macrobenchmark/target/MotionLayoutActivity.kt
+++ b/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark-target/src/main/java/androidx/constraintlayout/compose/integration/macrobenchmark/target/MotionLayoutActivity.kt
@@ -32,7 +32,6 @@
import androidx.constraintlayout.compose.integration.macrobenchmark.target.graphs.DynamicGraphsPreview
import androidx.constraintlayout.compose.integration.macrobenchmark.target.newmessage.NewMotionMessagePreview
import androidx.constraintlayout.compose.integration.macrobenchmark.target.newmessage.NewMotionMessagePreviewWithDsl
-import androidx.constraintlayout.compose.integration.macrobenchmark.target.newmessage.NewMotionMessagePreviewWithDslOptimized
import androidx.constraintlayout.compose.integration.macrobenchmark.target.toolbar.MotionCollapseToolbarPreview
class MotionLayoutActivity : ComponentActivity() {
@@ -56,23 +55,15 @@
"NewMessageJson" -> {
NewMotionMessagePreview()
}
-
"NewMessageDsl" -> {
NewMotionMessagePreviewWithDsl()
}
-
- "OptimizedNewMessageDsl" -> {
- NewMotionMessagePreviewWithDslOptimized()
- }
-
"CollapsibleToolbar" -> {
MotionCollapseToolbarPreview()
}
-
"DynamicGraphs" -> {
DynamicGraphsPreview()
}
-
else -> {
throw IllegalArgumentException("No Composable with name: $name")
}
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark-target/src/main/java/androidx/constraintlayout/compose/integration/macrobenchmark/target/newmessage/NewMessage.kt b/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark-target/src/main/java/androidx/constraintlayout/compose/integration/macrobenchmark/target/newmessage/NewMessage.kt
index 7b952db..0c12a36 100644
--- a/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark-target/src/main/java/androidx/constraintlayout/compose/integration/macrobenchmark/target/newmessage/NewMessage.kt
+++ b/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark-target/src/main/java/androidx/constraintlayout/compose/integration/macrobenchmark/target/newmessage/NewMessage.kt
@@ -64,7 +64,6 @@
import androidx.constraintlayout.compose.ConstraintSet
import androidx.constraintlayout.compose.Dimension
import androidx.constraintlayout.compose.ExperimentalMotionApi
-import androidx.constraintlayout.compose.InvalidationStrategy
import androidx.constraintlayout.compose.MotionLayout
import androidx.constraintlayout.compose.MotionLayoutScope
import androidx.constraintlayout.compose.MotionScene
@@ -76,26 +75,19 @@
@Preview
@Composable
fun NewMotionMessagePreview() {
- NewMotionMessageWithControls(useDsl = false, optimize = false)
+ NewMotionMessageWithControls(useDsl = false)
}
@Preview
@Composable
fun NewMotionMessagePreviewWithDsl() {
- NewMotionMessageWithControls(useDsl = true, optimize = false)
-}
-
-@Preview
-@Composable
-fun NewMotionMessagePreviewWithDslOptimized() {
- NewMotionMessageWithControls(useDsl = true, optimize = true)
+ NewMotionMessageWithControls(useDsl = true)
}
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMotionApi::class)
@Composable
fun NewMotionMessageWithControls(
- useDsl: Boolean,
- optimize: Boolean
+ useDsl: Boolean
) {
val initialLayout = NewMessageLayout.Full
val newMessageState = rememberNewMessageState(initialLayoutState = initialLayout)
@@ -119,20 +111,10 @@
text = "Mini"
)
}
- val invalidationStrategy = remember(newMessageState, optimize) {
- if (optimize) {
- InvalidationStrategy {
- newMessageState.currentState
- }
- } else {
- InvalidationStrategy.DefaultInvalidationStrategy
- }
- }
NewMessageButton(
modifier = Modifier.fillMaxSize(),
motionScene = motionScene,
state = newMessageState,
- invalidationStrategy = invalidationStrategy,
)
}
}
@@ -551,18 +533,16 @@
@Composable
private fun NewMessageButton(
- motionScene: MotionScene,
- state: NewMessageState,
- invalidationStrategy: InvalidationStrategy,
modifier: Modifier = Modifier,
+ motionScene: MotionScene,
+ state: NewMessageState
) {
val currentStateName = state.currentState.name
MotionLayout(
motionScene = motionScene,
animationSpec = tween(700),
constraintSetName = currentStateName,
- modifier = modifier,
- invalidationStrategy = invalidationStrategy
+ modifier = modifier
) {
MotionMessageContent(state = state)
}
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark/src/main/java/androidx/constraintlayout/compose/integration/macrobenchmark/MotionLayoutBenchmark.kt b/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark/src/main/java/androidx/constraintlayout/compose/integration/macrobenchmark/MotionLayoutBenchmark.kt
index 530ec36..73f439c 100644
--- a/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark/src/main/java/androidx/constraintlayout/compose/integration/macrobenchmark/MotionLayoutBenchmark.kt
+++ b/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark/src/main/java/androidx/constraintlayout/compose/integration/macrobenchmark/MotionLayoutBenchmark.kt
@@ -50,9 +50,6 @@
@Test
fun messageDsl() = benchmarkRule.testNewMessage(NewMessageMode.Dsl)
- @Test
- fun messageOptimizedDsl() = benchmarkRule.testNewMessage(NewMessageMode.OptimizedDsl)
-
/**
* Transitions the Layout through its three different ConstraintSets using the MotionScene JSON.
*/
@@ -161,8 +158,7 @@
internal enum class NewMessageMode(val composableName: String) {
Json("NewMessageJson"),
- Dsl("NewMessageDsl"),
- OptimizedDsl("OptimizedNewMessageDsl")
+ Dsl("NewMessageDsl")
}
private fun UiDevice.waitForComposeIdle(timeoutMs: Long = 3000) {
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MotionLayoutTest.kt b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MotionLayoutTest.kt
index 7355336..88438d3 100644
--- a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MotionLayoutTest.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MotionLayoutTest.kt
@@ -759,91 +759,6 @@
assertEquals(IntOffset(rootSizePx - boxSizePx, 0), boxPosition)
}
- @Test
- fun testInvalidationStrategy_onObservedStateChange() = with(rule.density) {
- val rootSizePx = 200
- val progress = mutableStateOf(0f)
- val textContent = mutableStateOf("Foo")
- val optimizeCorrectly = mutableStateOf(false)
- val textId = "text"
-
- rule.setContent {
- WithConsistentTextStyle {
- MotionLayout(
- motionScene = remember {
- MotionScene {
- val textRef = createRefFor(textId)
-
- defaultTransition(
- from = constraintSet {
- constrain(textRef) {
- centerTo(parent)
- }
- },
- to = constraintSet {
- constrain(textRef) {
- centerTo(parent)
- }
- }
- )
- }
- },
- progress = progress.value,
- modifier = Modifier.size(rootSizePx.toDp()),
- invalidationStrategy = remember(optimizeCorrectly.value) {
- if (optimizeCorrectly.value) {
- InvalidationStrategy {
- textContent.value
- }
- } else {
- InvalidationStrategy {
- // Do not invalidate on recomposition
- }
- }
- }
- ) {
- Text(
- text = textContent.value,
- fontSize = 10.sp,
- modifier = Modifier.layoutTestId(textId)
- )
- }
- }
- }
-
- rule.waitForIdle()
- var actualTextSize = rule.onNodeWithTag(textId).getUnclippedBoundsInRoot()
- assertEquals(18, actualTextSize.width.value.roundToInt())
- assertEquals(14, actualTextSize.height.value.roundToInt())
-
- textContent.value = "Foo\nBar"
-
- // Because we are optimizing "incorrectly" the text layout remains unchanged
- rule.waitForIdle()
- actualTextSize = rule.onNodeWithTag(textId).getUnclippedBoundsInRoot()
- assertEquals(18, actualTextSize.width.value.roundToInt())
- assertEquals(14, actualTextSize.height.value.roundToInt())
-
- textContent.value = "Foo"
- optimizeCorrectly.value = true
-
- // We change the text back and update the optimization strategy to be correct, text should
- // be the same as in its initial state
- rule.waitForIdle()
- actualTextSize = rule.onNodeWithTag(textId).getUnclippedBoundsInRoot()
- assertEquals(18, actualTextSize.width.value.roundToInt())
- assertEquals(14, actualTextSize.height.value.roundToInt())
-
- textContent.value = "Foo\nBar"
-
- // With the appropriate optimization strategy, the layout is invalidated when the text
- // changes
- rule.waitForIdle()
- actualTextSize = rule.onNodeWithTag(textId).getUnclippedBoundsInRoot()
- assertEquals(18, actualTextSize.width.value.roundToInt())
- assertEquals(25, actualTextSize.height.value.roundToInt())
- }
-
private fun Color.toHexString(): String = toArgb().toUInt().toString(16)
}
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionLayout.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionLayout.kt
index 18e7f36..eaa32df 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionLayout.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionLayout.kt
@@ -26,7 +26,6 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableFloatState
-import androidx.compose.runtime.MutableState
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
@@ -36,7 +35,6 @@
import androidx.compose.runtime.neverEqualPolicy
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawBehind
@@ -53,13 +51,11 @@
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.constraintlayout.core.widgets.Optimizer
-import kotlin.math.absoluteValue
import kotlinx.coroutines.channels.Channel
/**
@@ -118,12 +114,9 @@
* @param modifier Modifier to apply to this layout node.
* @param transition Defines the interpolation parameters between the [ConstraintSet]s to achieve
* fine-tuned animations.
- * @param debugFlags Flags to enable visual debugging. [DebugFlags.None] by default.
* @param optimizationLevel Optimization parameter for the underlying ConstraintLayout,
* [Optimizer.OPTIMIZATION_STANDARD] by default.
- * @param invalidationStrategy Provides strategies to optimize invalidations in [MotionLayout].
- * Excessive invalidations will be the typical cause of bad performance in [MotionLayout]. See
- * [InvalidationStrategy] to learn how to apply common strategies.
+ * @param debugFlags Flags to enable visual debugging. [DebugFlags.None] by default.
* @param content The content to be laid out by MotionLayout, note that each layout Composable
* should be bound to an ID defined in the [ConstraintSet]s using
* [Modifier.layoutId][androidx.compose.ui.layout.layoutId].
@@ -137,7 +130,6 @@
transition: Transition? = null,
debugFlags: DebugFlags = DebugFlags.None,
optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
- invalidationStrategy: InvalidationStrategy = InvalidationStrategy.DefaultInvalidationStrategy,
crossinline content: @Composable MotionLayoutScope.() -> Unit
) {
/**
@@ -158,8 +150,7 @@
// the same pass as the content. The only expected reader is our MeasurePolicy.
contentTracker.value = Unit
- if (invalidationStrategy.onObservedStateChange == null &&
- compositionSource.value == CompositionSource.Unknown) {
+ if (compositionSource.value == CompositionSource.Unknown) {
// Set the content as the original composition source if the MotionLayout was not
// recomposed by the caller or by itself
compositionSource.value = CompositionSource.Content
@@ -179,7 +170,6 @@
modifier = modifier,
contentTracker = contentTracker,
compositionSource = compositionSource,
- invalidationStrategy = invalidationStrategy,
content = contentDelegate
)
}
@@ -229,12 +219,9 @@
* @param modifier Modifier to apply to this layout node.
* @param transitionName The name of the transition to apply on the layout. By default, it will
* target the transition defined with [MotionSceneScope.defaultTransition].
- * @param debugFlags Flags to enable visual debugging. [DebugFlags.None] by default.
* @param optimizationLevel Optimization parameter for the underlying ConstraintLayout,
* [Optimizer.OPTIMIZATION_STANDARD] by default.
- * @param invalidationStrategy Provides strategies to optimize invalidations in [MotionLayout].
- * Excessive invalidations will be the typical cause of bad performance in [MotionLayout]. See
- * [InvalidationStrategy] to learn how to apply common strategies.
+ * @param debugFlags Flags to enable visual debugging. [DebugFlags.None] by default.
* @param content The content to be laid out by MotionLayout, note that each layout Composable
* should be bound to an ID defined in the [ConstraintSet]s using
* [Modifier.layoutId][androidx.compose.ui.layout.layoutId].
@@ -247,7 +234,6 @@
transitionName: String = "default",
debugFlags: DebugFlags = DebugFlags.None,
optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
- invalidationStrategy: InvalidationStrategy = InvalidationStrategy.DefaultInvalidationStrategy,
crossinline content: @Composable (MotionLayoutScope.() -> Unit),
) {
/**
@@ -268,8 +254,7 @@
// the same pass as the content. The only expected reader is our MeasurePolicy.
contentTracker.value = Unit
- if (invalidationStrategy.onObservedStateChange == null &&
- compositionSource.value == CompositionSource.Unknown) {
+ if (compositionSource.value == CompositionSource.Unknown) {
// Set the content as the original composition source if the MotionLayout was not
// recomposed by the caller or by itself
compositionSource.value = CompositionSource.Content
@@ -286,7 +271,6 @@
modifier = modifier,
contentTracker = contentTracker,
compositionSource = compositionSource,
- invalidationStrategy = invalidationStrategy,
content = contentDelegate
)
}
@@ -354,12 +338,9 @@
* @param modifier Modifier to apply to this layout node.
* @param finishedAnimationListener Called when an animation triggered by a change in
* [constraintSetName] has ended.
- * @param debugFlags Flags to enable visual debugging. [DebugFlags.None] by default.
* @param optimizationLevel Optimization parameter for the underlying ConstraintLayout,
* [Optimizer.OPTIMIZATION_STANDARD] by default.
- * @param invalidationStrategy Provides strategies to optimize invalidations in [MotionLayout].
- * Excessive invalidations will be the typical cause of bad performance in [MotionLayout]. See
- * [InvalidationStrategy] to learn how to apply common strategies.
+ * @param debugFlags Flags to enable visual debugging. [DebugFlags.None] by default.
* @param content The content to be laid out by MotionLayout, note that each layout Composable
* should be bound to an ID defined in the [ConstraintSet]s using
* [Modifier.layoutId][androidx.compose.ui.layout.layoutId].
@@ -373,7 +354,6 @@
noinline finishedAnimationListener: (() -> Unit)? = null,
debugFlags: DebugFlags = DebugFlags.None,
optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
- invalidationStrategy: InvalidationStrategy = InvalidationStrategy.DefaultInvalidationStrategy,
@Suppress("HiddenTypeParameter")
crossinline content: @Composable (MotionLayoutScope.() -> Unit)
) {
@@ -395,8 +375,7 @@
// the same pass as the content. The only expected reader is our MeasurePolicy.
contentTracker.value = Unit
- if (invalidationStrategy.onObservedStateChange == null &&
- compositionSource.value == CompositionSource.Unknown) {
+ if (compositionSource.value == CompositionSource.Unknown) {
// Set the content as the original composition source if the MotionLayout was not
// recomposed by the caller or by itself
compositionSource.value = CompositionSource.Content
@@ -414,7 +393,6 @@
optimizationLevel = optimizationLevel,
contentTracker = contentTracker,
compositionSource = compositionSource,
- invalidationStrategy = invalidationStrategy,
content = contentDelegate
)
}
@@ -429,9 +407,8 @@
finishedAnimationListener: (() -> Unit)? = null,
debugFlags: DebugFlags = DebugFlags.None,
optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
- contentTracker: MutableState<Unit>,
+ contentTracker: State<Unit>,
compositionSource: Ref<CompositionSource>,
- invalidationStrategy: InvalidationStrategy,
@Suppress("HiddenTypeParameter")
content: @Composable (MotionLayoutScope.() -> Unit)
) {
@@ -509,7 +486,6 @@
modifier = modifier,
contentTracker = contentTracker,
compositionSource = compositionSource,
- invalidationStrategy = invalidationStrategy,
content = content
)
}
@@ -524,9 +500,8 @@
optimizationLevel: Int,
debugFlags: DebugFlags,
modifier: Modifier,
- contentTracker: MutableState<Unit>,
+ contentTracker: State<Unit>,
compositionSource: Ref<CompositionSource>,
- invalidationStrategy: InvalidationStrategy,
@Suppress("HiddenTypeParameter")
content: @Composable MotionLayoutScope.() -> Unit,
) {
@@ -559,7 +534,6 @@
modifier = modifier,
contentTracker = contentTracker,
compositionSource = compositionSource,
- invalidationStrategy = invalidationStrategy,
content = content
)
}
@@ -577,9 +551,8 @@
showPaths: Boolean,
showKeyPositions: Boolean,
modifier: Modifier,
- contentTracker: MutableState<Unit>,
+ contentTracker: State<Unit>,
compositionSource: Ref<CompositionSource>,
- invalidationStrategy: InvalidationStrategy,
@Suppress("HiddenTypeParameter")
content: @Composable MotionLayoutScope.() -> Unit
) {
@@ -611,23 +584,6 @@
true // Remember is required to return a non-Unit value
}
- if (invalidationStrategy.onObservedStateChange != null) {
- Snapshot.observe(
- readObserver = {
- // Perform a reassignment to the State tracker, this will force readers to recompose at
- // the same pass as the content. The only expected reader is our MeasurePolicy.
- contentTracker.value = Unit
-
- if (compositionSource.value == CompositionSource.Unknown) {
- // Set the content as the original composition source if the MotionLayout was not
- // recomposed by the caller or by itself
- compositionSource.value = CompositionSource.Content
- }
- },
- block = invalidationStrategy.onObservedStateChange
- )
- }
-
val measurePolicy = motionLayoutMeasurePolicy(
contentTracker = contentTracker,
compositionSource = compositionSource,
@@ -636,8 +592,7 @@
transition = transitionImpl,
motionProgress = motionProgress,
measurer = measurer,
- optimizationLevel = optimizationLevel,
- invalidationStrategy = invalidationStrategy
+ optimizationLevel = optimizationLevel
)
measurer.addLayoutInformationReceiver(informationReceiver)
@@ -988,7 +943,6 @@
motionProgress: MutableFloatState,
measurer: MotionMeasurer,
optimizationLevel: Int,
- invalidationStrategy: InvalidationStrategy
): MeasurePolicy =
MeasurePolicy { measurables, constraints ->
// Do a state read, to guarantee that we control measure when the content recomposes without
@@ -996,16 +950,15 @@
contentTracker.value
val layoutSize = measurer.performInterpolationMeasure(
- constraints = constraints,
- layoutDirection = this.layoutDirection,
- constraintSetStart = constraintSetStart,
- constraintSetEnd = constraintSetEnd,
- transition = transition,
- measurables = measurables,
- optimizationLevel = optimizationLevel,
- progress = motionProgress.floatValue,
- compositionSource = compositionSource.value ?: CompositionSource.Unknown,
- invalidateOnConstraintsCallback = invalidationStrategy.shouldInvalidate
+ constraints,
+ this.layoutDirection,
+ constraintSetStart,
+ constraintSetEnd,
+ transition,
+ measurables,
+ optimizationLevel,
+ motionProgress.floatValue,
+ compositionSource.value ?: CompositionSource.Unknown
)
compositionSource.value = CompositionSource.Unknown // Reset after measuring
@@ -1196,488 +1149,3 @@
return view.isShowingLayoutBounds
}
}
-
-/**
- * Helper scope that provides some strategies to improve performance based on incoming constraints.
- *
- * As a starting approach, we recommend trying the following:
- *
- * ```
- * MotionLayout(
- * ...,
- * invalidationStrategy = remember {
- * InvalidationStrategy(
- * onIncomingConstraints = { old, new ->
- * // We invalidate every third frame, or when the change is higher than 5 pixels
- * fixedWidthRate(old, new, skipCount = 3, threshold = 5) ||
- * fixedHeightRate(old, new, skipCount = 3, threshold = 5)
- * },
- * onObservedStateChange = null // Default behavior
- * )
- * }
- * ) {
- * // content
- * }
- * ```
- *
- * See either [fixedWidthRate] or [fixedHeightRate] to learn more about the intent behind
- * rate-limiting invalidation.
- */
-class InvalidationStrategyScope internal constructor() {
- private var widthRateCount = 0
-
- /**
- * Limits the rate at which MotionLayout is invalidated while [Constraints.hasFixedWidth] is
- * true.
- *
- *
- *
- * The rate limit is defined by two variables. Use [skipCount] to indicate how many consecutive
- * measure passes should skip invalidation, you may then provide a [threshold] (in pixels) to
- * indicate when to invalidate regardless of how many passes are left to skip. This is
- * important since you only want to skip invalidation passes when there's **not** a significant
- * change in dimensions.
- *
- *
- *
- * Overall, you don't want [skipCount] to be too high otherwise it'll result in a "jumpy" layout
- * behavior, but you also don't want the [threshold] to be too low, otherwise you'll lose the
- * benefit of rate limiting.
- *
- * A good starting point is setting [skipCount] to 3 and [threshold] to 5. You can then
- * adjust based on your expectations of performance and perceived smoothness.
- */
- fun fixedWidthRate(
- oldConstraints: Constraints,
- newConstraints: Constraints,
- skipCount: Int,
- threshold: Int
- ): Boolean {
- if (oldConstraints.hasFixedWidth && newConstraints.hasFixedWidth) {
- val diff = (newConstraints.maxWidth - oldConstraints.maxWidth).absoluteValue
- if (diff >= threshold) {
- widthRateCount = 0
- return true
- }
- if (diff != 0) {
- widthRateCount++
- if (widthRateCount > skipCount) {
- widthRateCount = 0
- return true
- }
- }
- } else {
- widthRateCount = 0
- }
- return false
- }
-
- private var heightRateCount = 0
-
- /**
- * Limits the rate at which MotionLayout is invalidated while [Constraints.hasFixedHeight] is
- * true.
- *
- *
- *
- * The rate limit is defined by two variables. Use [skipCount] to indicate how many consecutive
- * measure passes should skip invalidation, you may then provide a [threshold] (in pixels) to
- * indicate when to invalidate regardless of how many passes are left to skip. This is
- * important since you only want to skip invalidation passes when there's **not** a significant
- * change in dimensions.
- *
- *
- *
- * Overall, you don't want [skipCount] to be too high otherwise it'll result in a "jumpy" layout
- * behavior, but you also don't want the [threshold] to be too low, otherwise you'll lose the
- * benefit of rate limiting.
- *
- * A good starting point is setting [skipCount] to 3 and [threshold] to 5. You can then
- * adjust based on your expectations of performance and perceived smoothness.
- */
- fun fixedHeightRate(
- oldConstraints: Constraints,
- newConstraints: Constraints,
- skipCount: Int,
- threshold: Int
- ): Boolean {
- if (oldConstraints.hasFixedHeight && newConstraints.hasFixedHeight) {
- val diff = (newConstraints.maxHeight - oldConstraints.maxHeight).absoluteValue
- if (diff >= threshold) {
- heightRateCount = 0
- return true
- }
- if (diff != 0) {
- heightRateCount++
- if (heightRateCount > skipCount) {
- heightRateCount = 0
- return true
- }
- }
- } else {
- heightRateCount = 0
- }
- return false
- }
-}
-
-/**
- * Provide different invalidation strategies for [MotionLayout].
- *
- *
- *
- * Whenever [MotionLayout] needs invalidating, it has to recalculate all animations based on the
- * current state at the measure pass, this is the slowest process in the [MotionLayout] cycle.
- *
- * An invalidation can be triggered by two reasons:
- * - Incoming fixed size constraints have changed. This is necessary since layouts are highly
- * dependent on their available space, it'll typically happen if you are externally animating the
- * dimensions of [MotionLayout].
- * - The content of MotionLayout recomposes. This is necessary since Layouts in Compose don't know
- * the reason for a new measure pass, so we need to recalculate animations even if recomposition
- * didn't affect the actual Layout. For example, this **definitely** happens if you are using
- * [MotionLayoutScope.customProperties], even when you are just animating a background color, the
- * custom property will trigger a recomposition in the content and [MotionLayout] will be forced to
- * invalidate since it cannot know that the Layout was not affected.
- *
- * So, you may use [InvalidationStrategy] to help [MotionLayout] decide when to invalidate:
- *
- * - [onObservedStateChange]: Mitigates invalidation from content recomposition by explicitly
- * reading the State variables you want to cause invalidation. You'll likely want to
- * apply this strategy to most of your [MotionLayout] Composables. As, in the most simple cases you
- * can just provide an empty lambda. Here's a full example:
- *
- * ```
- * val progress = remember { Animatable(0f) }
- *
- * MotionLayout(
- * motionScene = remember {
- * // A simple MotionScene that animates a background color from Red to Blue
- * MotionScene {
- * val (textRef) = createRefsFor("text")
- *
- * val start = constraintSet {
- * constrain(textRef) {
- * centerTo(parent)
- * customColor("background", Color.Red)
- * }
- * }
- * val end = constraintSet(extendConstraintSet = start) {
- * constrain(textRef) {
- * customColor("background", Color.Blue)
- * }
- * }
- * defaultTransition(from = start, to = end)
- * }
- * },
- * progress = progress.value,
- * modifier = Modifier.fillMaxSize(),
- * invalidationStrategy = remember {
- * InvalidationStrategy(
- * onObservedStateChange = { /* Empty, no need to invalidate on content recomposition */ }
- * )
- * }
- * ) {
- * // The content doesn't depend on any State variable that may affect the Layout's measure result
- * Text(
- * text = "Hello, World",
- * modifier = Modifier
- * .layoutId("text")
- * // However, the custom color is causing recomposition on each animated frame
- * .background(customColor("text", "background"))
- * )
- * }
- * LaunchedEffect(Unit) {
- * delay(1000)
- * progress.animateTo(targetValue = 1f, tween(durationMillis = 1200))
- * }
- * ```
- *
- * *When should I provide States to read then?*
- *
- *
- *
- * Whenever a State backed variable that affects the Layout's measure result changes. The most
- * common cases are Strings on the Text Composable.
- *
- * Here's an example where the text changes half-way through the animation:
- * ```
- * val progress = remember { Animatable(0f) }
- *
- * var textString by remember { mutableStateOf("Hello, World") }
- * MotionLayout(
- * motionScene = remember {
- * // A MotionScene that animates a Text from one corner to the other with an animated
- * // background color
- * MotionScene {
- * val (textRef) = createRefsFor("text")
- *
- * defaultTransition(
- * from = constraintSet {
- * constrain(textRef) {
- * top.linkTo(parent.top)
- * start.linkTo(parent.start)
- *
- * customColor("background", Color.LightGray)
- * }
- * },
- * to = constraintSet {
- * constrain(textRef) {
- * bottom.linkTo(parent.bottom)
- * end.linkTo(parent.end)
- *
- * customColor("background", Color.Gray)
- * }
- * }
- * )
- * }
- * },
- * progress = progress.value,
- * modifier = Modifier.fillMaxSize(),
- * invalidationStrategy = remember {
- * InvalidationStrategy(
- * onObservedStateChange = @Suppress("UNUSED_EXPRESSION"){
- * // We read our State String variable in this block, to guarantee that
- * // MotionLayout will invalidate to accommodate the new Text Layout.
- * // Note that we do not read the custom color here since it doesn't affect the Layout
- * textString
- * }
- * )
- * }
- * ) {
- * // The text Layout will change based on the provided State String
- * Text(
- * text = textString,
- * modifier = Modifier
- * .layoutId("text")
- * // Without an invalidation strategy, the custom color would normally invalidate
- * // MotionLayout due to recomposition
- * .background(customColor("text", "background"))
- * )
- * }
- * LaunchedEffect(Unit) {
- * delay(1000)
- * progress.animateTo(targetValue = 1f, tween(durationMillis = 3000)) {
- * if (value >= 0.5f) {
- * textString = "This is a\n" + "significantly different text."
- * }
- * }
- * }
- * ```
- *
- * *What if my Text changes continuously?*
- *
- *
- *
- * There's a few strategies you can take depending on how you expect the Text to behave.
- *
- * For example, if you don't expect the text to need more than one line, you can set the Text with
- * `softWrap = false` and `overflow = TextOverflow.Visible`:
- *
- * ```
- * MotionLayout(
- * motionScene = motionScene,
- * progress = progress,
- * modifier = Modifier.size(200.dp),
- * invalidationStrategy = remember { InvalidationStrategy { /* Do not invalidate on content recomposition */ } }
- * ) {
- * Text(
- * text = <your-State-String>,
- * modifier = Modifier.layoutId("text"),
- * softWrap = false,
- * overflow = TextOverflow.Visible
- * )
- * }
- * ```
- *
- * The Text layout won't change significantly and performance will be much improved.
- *
- * - [onIncomingConstraints]: With this lambda you can mitigate invalidation from incoming
- * constraints. You'll only have to worry about providing this lambda if you or the Layout you're
- * using is animating measuring constraints on [MotionLayout]. If the size is only changing in specific,
- * discrete values, then you should allow [MotionLayout] to invalidate normally.
- *
- * Here's an example where we manually animate [MotionLayout]'s size through a Modifier (along with
- * the MotionLayout animation), and shows how to mitigate invalidation by rate-limiting:
- *
- * ```
- * val textId = "text"
- * val progress = remember { Animatable(0f) }
- *
- * val initial = remember { DpSize(100.dp, 100.dp) }
- * val target = remember { DpSize(120.dp, 200.dp) }
- * var size by remember { mutableStateOf(initial) }
- *
- * MotionLayout(
- * motionScene = remember {
- * MotionScene {
- * val (textRef) = createRefsFor( "text")
- *
- * // Animate text from the bottom of the layout to the top
- * defaultTransition(
- * from = constraintSet {
- * constrain(textRef) {
- * centerHorizontallyTo(parent)
- * bottom.linkTo(parent.bottom)
- * }
- * },
- * to = constraintSet {
- * constrain(textRef) {
- * centerHorizontallyTo(parent)
- * top.linkTo(parent.top)
- * }
- * }
- * )
- * }
- * },
- * progress = progress.value,
- * modifier = Modifier.background(Color.Cyan).size(size),
- * invalidationStrategy = remember {
- * InvalidationStrategy(
- * onIncomingConstraints = { old, new ->
- * // We invalidate every third frame, or when the change is higher than 5 pixels
- * fixedWidthRate(old, new, skipCount = 3, threshold = 5) ||
- * fixedHeightRate(old, new, skipCount = 3, threshold = 5)
- * },
- * // No need to worry about content state changes for this example
- * onObservedStateChange = {}
- * )
- * }
- * ) {
- * Text("Hello, World!", Modifier.layoutId(textId))
- * }
- *
- * // Animate the size along with the MotionLayout. Without an invalidation strategy, this will cause
- * // MotionLayout to invalidate at every measure pass since it's getting fixed size Constraints at
- * // different values
- * LaunchedEffect(Unit) {
- * val sizeDifference = target - initial
- * delay(1000)
- * progress.animateTo(1f, tween(1200)) {
- * size = initial + (sizeDifference * value)
- * }
- * }
- * ```
- *
- * Note that [fixedWidthRate][InvalidationStrategyScope.fixedWidthRate] and [fixedHeightRate][InvalidationStrategyScope.fixedHeightRate]
- * are helper methods available in [InvalidationStrategyScope].
- *
- *
- *
- * An alternative to rate-limiting is to "simply" avoid invalidation from changed fixed size constraints.
- * This can be done by leaving [MotionLayout] as wrap content and then have it choose its own start
- * and ending size. Naturally, this is not always feasible, specially if it's a parent Composable the one
- * that's animating the size constraints.
- *
- * But, here's the MotionScene showing how to achieve this behavior based on the example above:
- *
- * ```
- * MotionScene {
- * // We'll use fakeParentRef to choose our starting and ending size then constrain everything
- * // else to it. MotionLayout will animate without invalidating.
- * // There's no need to bind "fakeParent" to any actual Composable.
- * val (fakeParentRef, textRef) = createRefsFor("fakeParent", "text")
- *
- * defaultTransition(
- * from = constraintSet {
- * constrain(fakeParentRef) {
- * width = 100.dp.asDimension()
- * height = 100.dp.asDimension()
- * }
- *
- * constrain(textRef) {
- * bottom.linkTo(fakeParentRef.bottom)
- * }
- * },
- * to = constraintSet {
- * constrain(fakeParentRef) {
- * width = 120.dp.asDimension()
- * height = 200.dp.asDimension()
- * }
- *
- * constrain(textRef) {
- * top.linkTo(fakeParentRef.top)
- * }
- * }
- * )
- * }
- * ```
- *
- * You can then remove the size modifier and the invalidation strategy for `onIncomingConstraints`,
- * as [MotionLayout] will animate through both sizes without invalidating.
- *
- * @see InvalidationStrategy.DefaultInvalidationStrategy
- * @see InvalidationStrategy.OnIncomingConstraints
- * @see InvalidationStrategyScope
- * @see InvalidationStrategyScope.fixedWidthRate
- * @see InvalidationStrategyScope.fixedHeightRate
- *
- * @property onObservedStateChange
- */
-class InvalidationStrategy(
- val onIncomingConstraints: OnIncomingConstraints? = null,
- /**
- * Lambda to implement invalidation on observed State changes.
- *
- * [State][androidx.compose.runtime.State] based variables should be read in the block of
- * this lambda to have [MotionLayout] invalidate whenever any of those variables
- * have changed.
- *
- * You may use an assigned value or delegated variable for this purpose:
- * ```
- * val stateVar0 = remember { mutableStateOf("Foo") }
- * var stateVar1 by remember { mutableStateOf("Bar") }
- * val invalidationStrategy = remember {
- * InvalidationStrategy(
- * onObservedStateChange = @Suppress("UNUSED_EXPRESSION") {
- * stateVar0.value
- * stateVar1
- * }
- * )
- * }
- * ```
- *
- * See [InvalidationStrategy] to learn more about common strategies regarding invalidation on
- * onObservedStateChange.
- */
- val onObservedStateChange: (() -> Unit)?
-) {
- private val scope = InvalidationStrategyScope()
-
- /**
- * Hacky thing to transform: `(InvalidationStrategyScope.(old: Constraints, new: Constraints) -> Boolean)?`
- * into `((old: Constraints, new: Constraints) -> Boolean)?`
- */
- internal val shouldInvalidate: ShouldInvalidateCallback? = kotlin.run {
- if (onIncomingConstraints == null) {
- null
- } else {
- ShouldInvalidateCallback { old, new ->
- with(onIncomingConstraints) {
- scope(old, new)
- }
- }
- }
- }
-
- companion object {
- /**
- * Default invalidation strategy for [MotionLayout].
- *
- * This will cause it to invalidate whenever its content recomposes or when it receives different
- * fixed size [Constraints] at the measure pass.
- */
- val DefaultInvalidationStrategy = InvalidationStrategy(null, null)
- }
-
- /**
- * Functional interface to implement invalidation on incoming constraints.
- *
- * See [InvalidationStrategy] or either of [fixedWidthRate][InvalidationStrategyScope.fixedWidthRate]/[fixedHeightRate][InvalidationStrategyScope.fixedHeightRate].
- *
- * To learn some strategies on how to improve invalidation due to incoming constraints.
- */
- fun interface OnIncomingConstraints {
- operator fun InvalidationStrategyScope.invoke(old: Constraints, new: Constraints): Boolean
- }
-}
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionMeasurer.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionMeasurer.kt
index e3b4da5..a8a1f1c 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionMeasurer.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionMeasurer.kt
@@ -85,14 +85,10 @@
measurables: List<Measurable>,
optimizationLevel: Int,
progress: Float,
- compositionSource: CompositionSource,
- invalidateOnConstraintsCallback: ShouldInvalidateCallback?
+ compositionSource: CompositionSource
): IntSize {
- val needsRemeasure = needsRemeasure(
- constraints = constraints,
- source = compositionSource,
- invalidateOnConstraintsCallback = invalidateOnConstraintsCallback
- )
+ val needsRemeasure = needsRemeasure(constraints, compositionSource)
+
if (lastProgressInInterpolation != progress ||
(layoutInformationReceiver?.getForcedWidth() != Int.MIN_VALUE &&
layoutInformationReceiver?.getForcedHeight() != Int.MIN_VALUE) ||
@@ -110,19 +106,10 @@
remeasure = needsRemeasure
)
}
- oldConstraints = constraints
return IntSize(root.width, root.height)
}
/**
- * Nullable reference of [Constraints] used for the `invalidateOnConstraintsCallback`.
- *
- * Helps us to indicate when we can start calling the callback, as we need at least one measure
- * pass to populate this reference.
- */
- private var oldConstraints: Constraints? = null
-
- /**
* Indicates if the layout requires measuring before computing the interpolation.
*
* This might happen if the size of MotionLayout or any of its children changed.
@@ -130,28 +117,17 @@
* MotionLayout size might change from its parent Layout, and in some cases the children size
* might change (eg: A Text layout has a longer string appended).
*/
- private fun needsRemeasure(
- constraints: Constraints,
- source: CompositionSource,
- invalidateOnConstraintsCallback: ShouldInvalidateCallback?
- ): Boolean {
+ private fun needsRemeasure(constraints: Constraints, source: CompositionSource): Boolean {
if (this.transition.isEmpty || frameCache.isEmpty()) {
// Nothing measured (by MotionMeasurer)
return true
}
- if (oldConstraints != null && invalidateOnConstraintsCallback != null) {
- if (invalidateOnConstraintsCallback(oldConstraints!!, constraints)) {
- // User is deciding when to invalidate
- return true
- }
- } else {
- if ((constraints.hasFixedHeight && !state.sameFixedHeight(constraints.maxHeight)) ||
- (constraints.hasFixedWidth && !state.sameFixedWidth(constraints.maxWidth))
- ) {
- // Layout size changed
- return true
- }
+ if ((constraints.hasFixedHeight && !state.sameFixedHeight(constraints.maxHeight)) ||
+ (constraints.hasFixedWidth && !state.sameFixedWidth(constraints.maxWidth))
+ ) {
+ // Layout size changed
+ return true
}
// Content recomposed
@@ -566,11 +542,3 @@
transition.applyAllTo(this.transition)
}
}
-
-/**
- * Functional interface to represent the callback of type
- * `(old: Constraints, new: Constraints) -> Boolean`
- */
-internal fun interface ShouldInvalidateCallback {
- operator fun invoke(old: Constraints, new: Constraints): Boolean
-}
diff --git a/core/core-performance-testing/src/test/java/androidx/core/performance/testing/FakeDevicePerformanceRetrieverTest.kt b/core/core-performance-testing/src/test/java/androidx/core/performance/testing/FakeDevicePerformanceRetrieverTest.kt
new file mode 100644
index 0000000..b45d2d7
--- /dev/null
+++ b/core/core-performance-testing/src/test/java/androidx/core/performance/testing/FakeDevicePerformanceRetrieverTest.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2023 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.core.performance.testing
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+/** Unit test for [FakeDevicePerformanceRetriever]. */
+class FakeDevicePerformanceRetrieverTest {
+
+ @Test
+ fun mediaPerformanceClass_30() {
+ val retriever = FakeDevicePerformanceRetriever(30)
+ val mpc = retriever.getPerformanceClass()
+ assertThat(mpc).isEqualTo(30)
+ }
+}
diff --git a/core/core-performance/src/test/java/androidx/core/performance/MediaPerformanceTest.kt b/core/core-performance/src/test/java/androidx/core/performance/MediaPerformanceTest.kt
new file mode 100644
index 0000000..2f30c43
--- /dev/null
+++ b/core/core-performance/src/test/java/androidx/core/performance/MediaPerformanceTest.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023 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.core.performance
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+/** Unit tests for [MediaPerformance]. */
+class MediaPerformanceTest {
+
+ @Test
+ fun mediaPerformanceClassWithUnspecifiedRetriever() {
+ val mpc = MediaPerformance.getPerformanceClass()
+ assertThat(mpc).isEqualTo(0)
+ }
+
+ @Test
+ fun mediaPerformanceClassWithSpecifiedDefaultRetriever() {
+ val mpc = MediaPerformance.getPerformanceClass(
+ DefaultDevicePerformanceRetriever()
+ )
+ assertThat(mpc).isEqualTo(0)
+ }
+}
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 408df66..efce220 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -1324,4 +1324,7 @@
# > Task :compose:ui:ui:compileReleaseKotlinAndroid
e: Daemon compilation failed: Could not connect to Kotlin compile daemon
java\.lang\.RuntimeException: Could not connect to Kotlin compile daemon
-Errors were stored into \$SUPPORT/\.gradle/kotlin/errors/errors\-[0-9]+\.log
\ No newline at end of file
+Errors were stored into \$SUPPORT/\.gradle/kotlin/errors/errors\-[0-9]+\.log
+# :benchmark:benchmark-baseline-profile-gradle-plugin:jar https://github.com/gradle/gradle/issues/22545
+# TODO(aurimas): remove when upgrading to Gradle 8.3
+.*No valid plugin descriptors were found in META\-INF/gradle-plugins.*
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 842f536..853eb1f 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,5 +1,5 @@
# TODO(https://github.com/spdx/spdx-gradle-plugin/issues/16) remove `-DSPDXParser.OnlyUseLocalLicenses=true`
-org.gradle.jvmargs=-Xmx8g -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC -Dkotlin.daemon.jvm.options=-XX:MaxMetaspaceSize=1g -Dlint.nullness.ignore-deprecated=true -Dorg.gradle.configuration-cache.internal.load-after-store=false -DSPDXParser.OnlyUseLocalLicenses=true
+org.gradle.jvmargs=-Xmx8g -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC -Dkotlin.daemon.jvm.options=-XX:MaxMetaspaceSize=1g -Dlint.nullness.ignore-deprecated=true -DSPDXParser.OnlyUseLocalLicenses=true
org.gradle.daemon=true
org.gradle.configureondemand=true
org.gradle.parallel=true
diff --git a/hilt/hilt-compiler/build.gradle b/hilt/hilt-compiler/build.gradle
index c25c109..2418bf7 100644
--- a/hilt/hilt-compiler/build.gradle
+++ b/hilt/hilt-compiler/build.gradle
@@ -16,6 +16,7 @@
import androidx.build.LibraryType
import androidx.build.SdkHelperKt
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -31,18 +32,29 @@
kapt(libs.autoService)
compileOnly(libs.gradleIncapHelper)
kapt(libs.gradleIncapHelperProcessor)
- implementation(libs.autoCommon)
+ implementation(project(":room:room-compiler-processing"))
implementation(libs.javapoet)
+ implementation(libs.kspApi)
testImplementation(project(":hilt:hilt-common"))
+ testImplementation(project(":annotation:annotation"))
testImplementation(libs.junit)
- testImplementation(libs.truth)
- testImplementation(libs.googleCompileTesting)
+ testImplementation(project(":kruth:kruth"))
+ testImplementation(project(":room:room-compiler-processing-testing"))
testImplementation(libs.hiltCore)
testImplementationAarAsJar(project(":hilt:hilt-work"))
testImplementation(SdkHelperKt.getSdkDependency(project))
}
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions {
+ freeCompilerArgs += [
+ "-opt-in=androidx.room.compiler.processing.ExperimentalProcessingApi",
+ "-opt-in=com.squareup.kotlinpoet.javapoet.KotlinPoetJavaPoetPreview"
+ ]
+ }
+}
+
tasks.withType(Test).configureEach {
// https://github.com/google/compile-testing/issues/222
it.jvmArgs "--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED"
diff --git a/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/AndroidXHiltKspProcessor.kt b/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/AndroidXHiltKspProcessor.kt
new file mode 100644
index 0000000..7c6fef4
--- /dev/null
+++ b/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/AndroidXHiltKspProcessor.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2020 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.
+ */
+
+import androidx.hilt.work.WorkerStep
+import androidx.room.compiler.processing.ksp.KspBasicAnnotationProcessor
+import com.google.auto.service.AutoService
+import com.google.devtools.ksp.processing.SymbolProcessor
+import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
+import com.google.devtools.ksp.processing.SymbolProcessorProvider
+
+class AndroidXHiltKspProcessor(
+ environment: SymbolProcessorEnvironment
+) : KspBasicAnnotationProcessor(environment) {
+ override fun processingSteps() = listOf(WorkerStep())
+
+ @AutoService(SymbolProcessorProvider::class)
+ class Provider : SymbolProcessorProvider {
+ override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
+ return AndroidXHiltKspProcessor(environment)
+ }
+ }
+}
diff --git a/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/AndroidXHiltProcessor.kt b/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/AndroidXHiltProcessor.kt
index 277140b..cf646af 100644
--- a/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/AndroidXHiltProcessor.kt
+++ b/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/AndroidXHiltProcessor.kt
@@ -17,13 +17,10 @@
package androidx.hilt
import androidx.hilt.work.WorkerStep
+import androidx.room.compiler.processing.javac.JavacBasicAnnotationProcessor
import com.google.auto.service.AutoService
-import javax.annotation.processing.AbstractProcessor
import javax.annotation.processing.Processor
-import javax.annotation.processing.RoundEnvironment
import javax.lang.model.SourceVersion
-import javax.lang.model.element.Element
-import javax.lang.model.element.TypeElement
import net.ltgt.gradle.incap.IncrementalAnnotationProcessor
import net.ltgt.gradle.incap.IncrementalAnnotationProcessorType.ISOLATING
@@ -32,32 +29,9 @@
*/
@AutoService(Processor::class)
@IncrementalAnnotationProcessor(ISOLATING)
-class AndroidXHiltProcessor : AbstractProcessor() {
+class AndroidXHiltProcessor : JavacBasicAnnotationProcessor() {
- override fun getSupportedAnnotationTypes() = setOf(
- ClassNames.HILT_WORKER.canonicalName()
- )
+ override fun processingSteps() = listOf(WorkerStep())
override fun getSupportedSourceVersion() = SourceVersion.latest()
-
- override fun process(
- annotations: MutableSet<out TypeElement>,
- roundEnv: RoundEnvironment
- ): Boolean {
- getSteps().forEach { step ->
- annotations.firstOrNull { it.qualifiedName.contentEquals(step.annotation()) }?.let {
- step.process(roundEnv.getElementsAnnotatedWith(it))
- }
- }
- return false
- }
-
- private fun getSteps() = listOf(
- WorkerStep(processingEnv)
- )
-
- interface Step {
- fun annotation(): String
- fun process(annotatedElements: Set<Element>)
- }
}
diff --git a/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/assisted/AssistedFactoryGenerator.kt b/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/assisted/AssistedFactoryGenerator.kt
deleted file mode 100644
index b452df0..0000000
--- a/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/assisted/AssistedFactoryGenerator.kt
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Copyright 2020 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.hilt.assisted
-
-import androidx.hilt.ClassNames
-import androidx.hilt.ext.L
-import androidx.hilt.ext.T
-import androidx.hilt.ext.W
-import androidx.hilt.ext.addGeneratedAnnotation
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.CodeBlock
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.JavaFile
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeSpec
-import javax.annotation.processing.ProcessingEnvironment
-import javax.lang.model.element.Modifier
-import javax.lang.model.element.TypeElement
-import javax.lang.model.util.ElementFilter
-
-/**
- * Source generator for assisted factories.
- */
-internal class AssistedFactoryGenerator(
- private val processingEnv: ProcessingEnvironment,
- private val productClassName: ClassName,
- private val factoryClassName: ClassName,
- private val factorySuperTypeName: ParameterizedTypeName,
- private val originatingElement: TypeElement,
- private val dependencyRequests: List<DependencyRequest>
-) {
-
- fun generate() {
- val factoryTypeSpec = TypeSpec.classBuilder(factoryClassName)
- .addOriginatingElement(originatingElement)
- .addSuperinterface(factorySuperTypeName)
- .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
- .addGeneratedAnnotation(processingEnv.elementUtils, processingEnv.sourceVersion)
- .addFields(getFieldSpecs())
- .addMethod(getConstructorMethodSpec())
- .addMethod(getCreateMethodSpec())
- .build()
- JavaFile.builder(factoryClassName.packageName(), factoryTypeSpec)
- .build()
- .writeTo(processingEnv.filer)
- }
-
- private fun getFieldSpecs() = dependencyRequests
- .filterNot { it.isAssisted }
- .map { dependencyRequest ->
- val fieldTypeName = dependencyRequest.providerTypeName.withoutAnnotations()
- FieldSpec.builder(fieldTypeName, dependencyRequest.name)
- .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
- .build()
- }
-
- private fun getConstructorMethodSpec() =
- MethodSpec.constructorBuilder()
- .addAnnotation(ClassNames.INJECT)
- .apply {
- dependencyRequests
- .filterNot { it.isAssisted }
- .forEach { dependencyRequest ->
- addParameter(dependencyRequest.providerTypeName, dependencyRequest.name)
- addStatement("this.$1N = $1N", dependencyRequest.name)
- }
- }
- .build()
-
- private fun getCreateMethodSpec(): MethodSpec {
- val factoryTypeElement =
- processingEnv.elementUtils.getTypeElement(factorySuperTypeName.rawType.canonicalName())
- val factoryMethod = ElementFilter.methodsIn(factoryTypeElement.enclosedElements).first()
- val parameterSpecs = factoryMethod.parameters.map { ParameterSpec.get(it) }
- val constructorArgs = dependencyRequests.map {
- val paramLiteral = when {
- it.isAssisted -> {
- factoryMethod.parameters.first { param ->
- TypeName.get(param.asType()) == it.type
- }.simpleName.toString()
- }
- it.isProvider -> it.name
- else -> "${it.name}.get()"
- }
- CodeBlock.of(L, paramLiteral)
- }
- return MethodSpec.methodBuilder(factoryMethod.simpleName.toString())
- .addAnnotation(Override::class.java)
- .addAnnotation(ClassNames.NON_NULL)
- .addModifiers(Modifier.PUBLIC)
- .returns(productClassName)
- .addParameters(parameterSpecs)
- .addStatement(
- "return new $T($L)",
- productClassName, CodeBlock.join(constructorArgs, ",$W")
- )
- .build()
- }
-}
diff --git a/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/assisted/DependencyRequest.kt b/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/assisted/DependencyRequest.kt
index 822c778..6130cc7c 100644
--- a/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/assisted/DependencyRequest.kt
+++ b/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/assisted/DependencyRequest.kt
@@ -17,11 +17,12 @@
package androidx.hilt.assisted
import androidx.hilt.ClassNames
-import androidx.hilt.ext.hasAnnotation
+import androidx.room.compiler.codegen.toJavaPoet
+import androidx.room.compiler.processing.XVariableElement
+import androidx.room.compiler.processing.toAnnotationSpec
import com.squareup.javapoet.AnnotationSpec
import com.squareup.javapoet.ParameterizedTypeName
import com.squareup.javapoet.TypeName
-import javax.lang.model.element.VariableElement
/**
* Data class that represents a binding request for an assisted injected type.
@@ -51,17 +52,16 @@
}
}
-internal fun VariableElement.toDependencyRequest(): DependencyRequest {
- val qualifier = annotationMirrors.find {
- it.annotationType.asElement().hasAnnotation("javax.inject.Qualifier")
- }?.let { AnnotationSpec.get(it) }
- val type = TypeName.get(asType())
+internal fun XVariableElement.toDependencyRequest(): DependencyRequest {
+ val qualifier = getAllAnnotations().find {
+ it.qualifiedName == "javax.inject.Qualifier"
+ }?.toAnnotationSpec(includeDefaultValues = false)
return DependencyRequest(
- name = simpleName.toString(),
- type = type,
+ name = this.name,
+ type = this.type.asTypeName().toJavaPoet(),
isAssisted = (
- hasAnnotation(ClassNames.ANDROIDX_ASSISTED.canonicalName()) ||
- hasAnnotation(ClassNames.ASSISTED.canonicalName())
+ this.hasAnnotation(ClassNames.ANDROIDX_ASSISTED) ||
+ this.hasAnnotation(ClassNames.ASSISTED)
) && qualifier == null,
qualifier = qualifier
)
diff --git a/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/ext/annotationProcessing.kt b/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/ext/annotationProcessing.kt
deleted file mode 100644
index 21827d2..0000000
--- a/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/ext/annotationProcessing.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright 2020 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.hilt.ext
-
-import com.google.auto.common.MoreElements
-import javax.lang.model.element.Element
-import kotlin.reflect.KClass
-
-fun Element.hasAnnotation(clazz: KClass<out Annotation>) =
- MoreElements.isAnnotationPresent(this, clazz.java)
-
-fun Element.hasAnnotation(qName: String) = annotationMirrors.any {
- MoreElements.asType(it.annotationType.asElement()).qualifiedName.contentEquals(qName)
-}
diff --git a/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/ext/javaPoet.kt b/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/ext/javaPoet.kt
index d3fc1c2..9aae5b2 100644
--- a/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/ext/javaPoet.kt
+++ b/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/ext/javaPoet.kt
@@ -17,10 +17,10 @@
package androidx.hilt.ext
import androidx.hilt.AndroidXHiltProcessor
-import com.google.auto.common.GeneratedAnnotationSpecs
+import androidx.room.compiler.processing.XProcessingEnv
+import com.squareup.javapoet.AnnotationSpec
+import com.squareup.javapoet.ClassName
import com.squareup.javapoet.TypeSpec
-import javax.lang.model.SourceVersion
-import javax.lang.model.util.Elements
const val L = "\$L"
const val T = "\$T"
@@ -28,15 +28,13 @@
const val S = "\$S"
const val W = "\$W"
-internal fun TypeSpec.Builder.addGeneratedAnnotation(
- elements: Elements,
- sourceVersion: SourceVersion
-) = apply {
- GeneratedAnnotationSpecs.generatedAnnotationSpec(
- elements,
- sourceVersion,
- AndroidXHiltProcessor::class.java
- ).ifPresent { generatedAnnotation ->
- addAnnotation(generatedAnnotation)
+internal fun TypeSpec.Builder.addGeneratedAnnotation(env: XProcessingEnv) = apply {
+ env.findGeneratedAnnotation()?.let {
+ addAnnotation(
+ AnnotationSpec.builder(ClassName.bestGuess(it.asClassName().canonicalName))
+ .addMember("value", S, AndroidXHiltProcessor::class.java.canonicalName)
+ .build()
+
+ )
}
}
diff --git a/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/work/WorkerElements.kt b/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/work/WorkerElement.kt
similarity index 75%
rename from hilt/hilt-compiler/src/main/kotlin/androidx/hilt/work/WorkerElements.kt
rename to hilt/hilt-compiler/src/main/kotlin/androidx/hilt/work/WorkerElement.kt
index f62c1ee..52d4ff4 100644
--- a/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/work/WorkerElements.kt
+++ b/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/work/WorkerElement.kt
@@ -18,23 +18,23 @@
import androidx.hilt.ClassNames
import androidx.hilt.assisted.toDependencyRequest
-import com.google.auto.common.MoreElements
+import androidx.room.compiler.codegen.toJavaPoet
+import androidx.room.compiler.processing.XConstructorElement
+import androidx.room.compiler.processing.XTypeElement
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.ParameterizedTypeName
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.element.TypeElement
/**
* Data class that represents a Hilt injected Worker
*/
-internal data class WorkerElements(
- val typeElement: TypeElement,
- val constructorElement: ExecutableElement
+internal data class WorkerElement(
+ val typeElement: XTypeElement,
+ val constructorElement: XConstructorElement
) {
- val className = ClassName.get(typeElement)
+ val className = typeElement.asClassName().toJavaPoet()
val factoryClassName = ClassName.get(
- MoreElements.getPackage(typeElement).qualifiedName.toString(),
+ typeElement.packageName,
"${className.simpleNames().joinToString("_")}_AssistedFactory"
)
@@ -44,7 +44,7 @@
)
val moduleClassName = ClassName.get(
- MoreElements.getPackage(typeElement).qualifiedName.toString(),
+ typeElement.packageName,
"${className.simpleNames().joinToString("_")}_HiltModule"
)
diff --git a/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/work/WorkerGenerator.kt b/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/work/WorkerGenerator.kt
index 4052322..155f2e4 100644
--- a/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/work/WorkerGenerator.kt
+++ b/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/work/WorkerGenerator.kt
@@ -20,13 +20,15 @@
import androidx.hilt.ext.S
import androidx.hilt.ext.T
import androidx.hilt.ext.addGeneratedAnnotation
+import androidx.room.compiler.processing.XProcessingEnv
+import androidx.room.compiler.processing.addOriginatingElement
+import androidx.room.compiler.processing.writeTo
import com.squareup.javapoet.AnnotationSpec
import com.squareup.javapoet.JavaFile
import com.squareup.javapoet.MethodSpec
import com.squareup.javapoet.ParameterizedTypeName
import com.squareup.javapoet.TypeSpec
import com.squareup.javapoet.WildcardTypeName
-import javax.annotation.processing.ProcessingEnvironment
import javax.lang.model.element.Modifier
/**
@@ -52,13 +54,13 @@
* ```
*/
internal class WorkerGenerator(
- private val processingEnv: ProcessingEnvironment,
- private val injectedWorker: WorkerElements
+ private val processingEnv: XProcessingEnv,
+ private val injectedWorker: WorkerElement
) {
fun generate() {
val assistedFactoryTypeSpec = TypeSpec.interfaceBuilder(injectedWorker.factoryClassName)
.addOriginatingElement(injectedWorker.typeElement)
- .addGeneratedAnnotation(processingEnv.elementUtils, processingEnv.sourceVersion)
+ .addGeneratedAnnotation(processingEnv)
.addAnnotation(ClassNames.ASSISTED_FACTORY)
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(injectedWorker.factorySuperTypeName)
@@ -69,7 +71,7 @@
val hiltModuleTypeSpec = TypeSpec.interfaceBuilder(injectedWorker.moduleClassName)
.addOriginatingElement(injectedWorker.typeElement)
- .addGeneratedAnnotation(processingEnv.elementUtils, processingEnv.sourceVersion)
+ .addGeneratedAnnotation(processingEnv)
.addAnnotation(ClassNames.MODULE)
.addAnnotation(
AnnotationSpec.builder(ClassNames.INSTALL_IN)
diff --git a/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/work/WorkerStep.kt b/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/work/WorkerStep.kt
index d06abf9..a02521a 100644
--- a/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/work/WorkerStep.kt
+++ b/hilt/hilt-compiler/src/main/kotlin/androidx/hilt/work/WorkerStep.kt
@@ -16,99 +16,83 @@
package androidx.hilt.work
-import androidx.hilt.AndroidXHiltProcessor
import androidx.hilt.ClassNames
-import androidx.hilt.ext.hasAnnotation
-import com.google.auto.common.MoreElements
-import com.squareup.javapoet.TypeName
-import javax.annotation.processing.ProcessingEnvironment
-import javax.lang.model.element.Element
-import javax.lang.model.element.Modifier
-import javax.lang.model.element.NestingKind
-import javax.lang.model.element.TypeElement
-import javax.lang.model.util.ElementFilter
+import androidx.room.compiler.codegen.toJavaPoet
+import androidx.room.compiler.processing.XElement
+import androidx.room.compiler.processing.XProcessingEnv
+import androidx.room.compiler.processing.XProcessingStep
+import androidx.room.compiler.processing.XTypeElement
import javax.tools.Diagnostic
/**
* Processing step that generates code enabling assisted injection of Workers using Hilt.
*/
-class WorkerStep(
- private val processingEnv: ProcessingEnvironment
-) : AndroidXHiltProcessor.Step {
+class WorkerStep : XProcessingStep {
- private val elements = processingEnv.elementUtils
- private val types = processingEnv.typeUtils
- private val messager = processingEnv.messager
+ override fun annotations() = setOf(ClassNames.HILT_WORKER.canonicalName())
- override fun annotation() = ClassNames.HILT_WORKER.canonicalName()
-
- override fun process(annotatedElements: Set<Element>) {
- val parsedElements = mutableSetOf<TypeElement>()
- annotatedElements.forEach { element ->
- val typeElement = MoreElements.asType(element)
- if (parsedElements.add(typeElement)) {
- parse(typeElement)?.let { worker ->
- WorkerGenerator(
- processingEnv,
- worker
- ).generate()
- }
- }
- }
+ override fun process(
+ env: XProcessingEnv,
+ elementsByAnnotation: Map<String, Set<XElement>>,
+ isLastRound: Boolean
+ ): Set<XElement> {
+ elementsByAnnotation[ClassNames.HILT_WORKER.canonicalName()]
+ ?.filterIsInstance<XTypeElement>()
+ ?.mapNotNull { element -> parse(env, element) }
+ ?.forEach { worker -> WorkerGenerator(env, worker).generate() }
+ return emptySet()
}
- private fun parse(typeElement: TypeElement): WorkerElements? {
+ private fun parse(env: XProcessingEnv, workerTypeElement: XTypeElement): WorkerElement? {
var valid = true
- if (elements.getTypeElement(ClassNames.WORKER_ASSISTED_FACTORY.toString()) == null) {
- error(
+ if (env.findTypeElement(ClassNames.WORKER_ASSISTED_FACTORY) == null) {
+ env.error(
"To use @HiltWorker you must add the 'work' artifact. " +
"androidx.hilt:hilt-work:<version>"
)
valid = false
}
- if (!types.isSubtype(
- typeElement.asType(),
- elements.getTypeElement(ClassNames.LISTENABLE_WORKER.toString()).asType()
- )
- ) {
- error(
+ val workerType = workerTypeElement.type
+ val listenableWorkerType = env.requireType(ClassNames.LISTENABLE_WORKER)
+ if (!listenableWorkerType.isAssignableFrom(workerType)) {
+ env.error(
"@HiltWorker is only supported on types that subclass " +
- "${ClassNames.LISTENABLE_WORKER}."
+ "${ClassNames.LISTENABLE_WORKER}.",
+ workerTypeElement
)
valid = false
}
- val constructors = ElementFilter.constructorsIn(typeElement.enclosedElements).filter {
- if (it.hasAnnotation(ClassNames.INJECT.canonicalName())) {
- error(
+ val constructors = workerTypeElement.getConstructors().filter {
+ if (it.hasAnnotation(ClassNames.INJECT)) {
+ env.error(
"Worker constructor should be annotated with @AssistedInject instead of " +
- "@Inject."
+ "@Inject.",
+ it
)
valid = false
}
- it.hasAnnotation(ClassNames.ASSISTED_INJECT.canonicalName())
+ it.hasAnnotation(ClassNames.ASSISTED_INJECT)
}
if (constructors.size != 1) {
- error(
+ env.error(
"@HiltWorker annotated class should contain exactly one @AssistedInject " +
"annotated constructor.",
- typeElement
+ workerTypeElement
)
valid = false
}
- constructors.filter { it.modifiers.contains(Modifier.PRIVATE) }.forEach {
- error("@AssistedInject annotated constructors must not be private.", it)
+ constructors.filter { it.isPrivate() }.forEach {
+ env.error("@AssistedInject annotated constructors must not be private.", it)
valid = false
}
- if (typeElement.nestingKind == NestingKind.MEMBER &&
- !typeElement.modifiers.contains(Modifier.STATIC)
- ) {
- error(
+ if (workerTypeElement.isNested() && !workerTypeElement.isStatic()) {
+ env.error(
"@HiltWorker may only be used on inner classes if they are static.",
- typeElement
+ workerTypeElement
)
valid = false
}
@@ -119,35 +103,40 @@
var contextIndex = -1
var workerParametersIndex = -1
injectConstructor.parameters.forEachIndexed { index, param ->
- if (TypeName.get(param.asType()) == ClassNames.CONTEXT) {
- if (!param.hasAnnotation(ClassNames.ASSISTED.canonicalName())) {
- error("Missing @Assisted annotation in param '${param.simpleName}'.", param)
+ if (param.type.asTypeName().toJavaPoet() == ClassNames.CONTEXT) {
+ if (!param.hasAnnotation(ClassNames.ASSISTED)) {
+ env.error("Missing @Assisted annotation in param '${param.name}'.", param)
valid = false
}
contextIndex = index
}
- if (TypeName.get(param.asType()) == ClassNames.WORKER_PARAMETERS) {
- if (!param.hasAnnotation(ClassNames.ASSISTED.canonicalName())) {
- error("Missing @Assisted annotation in param '${param.simpleName}'.", param)
+ if (param.type.asTypeName().toJavaPoet() == ClassNames.WORKER_PARAMETERS) {
+ if (!param.hasAnnotation(ClassNames.ASSISTED)) {
+ env.error("Missing @Assisted annotation in param '${param.name}'.", param)
valid = false
}
workerParametersIndex = index
}
}
if (contextIndex > workerParametersIndex) {
- error(
+ env.error(
"The 'Context' parameter must be declared before the 'WorkerParameters' in the " +
"@AssistedInject constructor of a @HiltWorker annotated class.",
injectConstructor
)
+ valid = false
}
if (!valid) return null
- return WorkerElements(typeElement, injectConstructor)
+ return WorkerElement(workerTypeElement, injectConstructor)
}
- private fun error(message: String, element: Element? = null) {
- messager.printMessage(Diagnostic.Kind.ERROR, message, element)
+ private fun XProcessingEnv.error(message: String, element: XElement? = null) {
+ if (element != null) {
+ messager.printMessage(Diagnostic.Kind.ERROR, message, element)
+ } else {
+ messager.printMessage(Diagnostic.Kind.ERROR, message)
+ }
}
}
diff --git a/hilt/hilt-compiler/src/test/kotlin/androidx/hilt/testUtils.kt b/hilt/hilt-compiler/src/test/kotlin/androidx/hilt/ext/testUtils.kt
similarity index 68%
rename from hilt/hilt-compiler/src/test/kotlin/androidx/hilt/testUtils.kt
rename to hilt/hilt-compiler/src/test/kotlin/androidx/hilt/ext/testUtils.kt
index b24ad5e..722c714 100644
--- a/hilt/hilt-compiler/src/test/kotlin/androidx/hilt/testUtils.kt
+++ b/hilt/hilt-compiler/src/test/kotlin/androidx/hilt/ext/testUtils.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2023 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.
@@ -14,13 +14,11 @@
* limitations under the License.
*/
-package androidx.hilt
+package androidx.hilt.ext
-import com.google.testing.compile.Compiler
-import com.google.testing.compile.Compiler.javac
-import com.google.testing.compile.JavaFileObjects
+import androidx.hilt.ClassNames
+import androidx.room.compiler.processing.util.Source
import java.io.File
-import javax.tools.JavaFileObject
val GENERATED_TYPE = try {
Class.forName("javax.annotation.processing.Generated")
@@ -55,11 +53,7 @@
}
}
-fun loadJavaSource(fileName: String, qName: String): JavaFileObject {
- val contents = File("src/test/data/sources/$fileName").readText(Charsets.UTF_8)
- return JavaFileObjects.forSourceString(qName, contents)
-}
-
-fun compiler(): Compiler = javac().withProcessors(AndroidXHiltProcessor())
-
-fun String.toJFO(qName: String) = JavaFileObjects.forSourceString(qName, this.trimIndent())
+fun loadJavaSource(fileName: String, qName: String) = Source.loadJavaSource(
+ file = File("src/test/data/sources/$fileName"),
+ qName = qName
+)
diff --git a/hilt/hilt-compiler/src/test/kotlin/androidx/hilt/work/WorkerGeneratorTest.kt b/hilt/hilt-compiler/src/test/kotlin/androidx/hilt/work/WorkerGeneratorTest.kt
index fc892a0..b9d87f4 100644
--- a/hilt/hilt-compiler/src/test/kotlin/androidx/hilt/work/WorkerGeneratorTest.kt
+++ b/hilt/hilt-compiler/src/test/kotlin/androidx/hilt/work/WorkerGeneratorTest.kt
@@ -16,12 +16,11 @@
package androidx.hilt.work
-import androidx.hilt.GENERATED_ANNOTATION
-import androidx.hilt.GENERATED_TYPE
-import androidx.hilt.Sources
-import androidx.hilt.compiler
-import androidx.hilt.toJFO
-import com.google.testing.compile.CompilationSubject.assertThat
+import androidx.hilt.ext.GENERATED_ANNOTATION
+import androidx.hilt.ext.GENERATED_TYPE
+import androidx.hilt.ext.Sources
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.runProcessorTest
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -31,111 +30,128 @@
@Test
fun verifyAssistedFactory_mixedArgs() {
- val foo = """
- package androidx.hilt.work.test;
+ val foo = Source.java(
+ "androidx.hilt.work.test.Foo",
+ """
+ package androidx.hilt.work.test;
- public class Foo { }
- """.toJFO("androidx.hilt.work.test.Foo")
+ public class Foo { }
+ """
+ )
- val myWorker = """
- package androidx.hilt.work.test;
+ val myWorker = Source.java(
+ "androidx.hilt.work.test.MyWorker",
+ """
+ package androidx.hilt.work.test;
- import android.content.Context;
- import androidx.hilt.work.HiltWorker;
- import androidx.work.Worker;
- import androidx.work.WorkerParameters;
- import dagger.assisted.Assisted;
- import dagger.assisted.AssistedInject;
- import java.lang.String;
+ import android.content.Context;
+ import androidx.hilt.work.HiltWorker;
+ import androidx.work.Worker;
+ import androidx.work.WorkerParameters;
+ import dagger.assisted.Assisted;
+ import dagger.assisted.AssistedInject;
+ import java.lang.String;
- @HiltWorker
- class MyWorker extends Worker {
- @AssistedInject
- MyWorker(@Assisted Context context, @Assisted WorkerParameters params, String s,
- Foo f, long l) {
- super(context, params);
+ @HiltWorker
+ class MyWorker extends Worker {
+ @AssistedInject
+ MyWorker(@Assisted Context context, @Assisted WorkerParameters params, String s,
+ Foo f, long l) {
+ super(context, params);
+ }
}
- }
- """.toJFO("androidx.hilt.work.test.MyWorker")
+ """
+ )
- val expected = """
- package androidx.hilt.work.test;
+ val expected = Source.java(
+ "androidx.hilt.work.test.MyWorker_AssistedFactory",
+ """
+ package androidx.hilt.work.test;
- import androidx.hilt.work.WorkerAssistedFactory;
- import dagger.assisted.AssistedFactory;
- import $GENERATED_TYPE;
+ import androidx.hilt.work.WorkerAssistedFactory;
+ import dagger.assisted.AssistedFactory;
+ import $GENERATED_TYPE;
- $GENERATED_ANNOTATION
- @AssistedFactory
- public interface MyWorker_AssistedFactory extends WorkerAssistedFactory<MyWorker> {
- }
- """.toJFO("androidx.hilt.work.test.MyWorker_AssistedFactory")
+ $GENERATED_ANNOTATION
+ @AssistedFactory
+ public interface MyWorker_AssistedFactory extends WorkerAssistedFactory<MyWorker> {
+ }
+ """
+ )
- val compilation = compiler()
- .compile(
+ runProcessorTest(
+ sources = listOf(
foo, myWorker, Sources.LISTENABLE_WORKER, Sources.WORKER,
Sources.WORKER_PARAMETERS
- )
- assertThat(compilation).apply {
- succeeded()
- generatedSourceFile("androidx.hilt.work.test.MyWorker_AssistedFactory")
- .hasSourceEquivalentTo(expected)
+ ),
+ createProcessingSteps = { listOf(WorkerStep()) }
+ ) {
+ it.generatedSource(expected)
}
}
@Test
fun verifyMultibindModule() {
- val myWorker = """
- package androidx.hilt.work.test;
+ val myWorker = Source.java(
+ "androidx.hilt.work.test.MyWorker",
+ """
+ package androidx.hilt.work.test;
- import android.content.Context;
- import androidx.hilt.work.HiltWorker;
- import androidx.work.Worker;
- import androidx.work.WorkerParameters;
- import dagger.assisted.Assisted;
- import dagger.assisted.AssistedInject;
+ import android.content.Context;
+ import androidx.hilt.work.HiltWorker;
+ import androidx.work.Worker;
+ import androidx.work.WorkerParameters;
+ import dagger.assisted.Assisted;
+ import dagger.assisted.AssistedInject;
- @HiltWorker
- class MyWorker extends Worker {
- @AssistedInject
- MyWorker(@Assisted Context context, @Assisted WorkerParameters params) {
- super(context, params);
+ @HiltWorker
+ class MyWorker extends Worker {
+ @AssistedInject
+ MyWorker(@Assisted Context context, @Assisted WorkerParameters params) {
+ super(context, params);
+ }
}
- }
- """.toJFO("androidx.hilt.work.test.MyWorker")
+ """
+ )
- val expected = """
- package androidx.hilt.work.test;
+ val expected = Source.java(
+ "androidx.hilt.work.test.MyWorker_HiltModule",
+ """
+ package androidx.hilt.work.test;
- import androidx.hilt.work.WorkerAssistedFactory;
- import androidx.work.ListenableWorker;
- import dagger.Binds;
- import dagger.Module;
- import dagger.hilt.InstallIn;
- import dagger.hilt.codegen.OriginatingElement;
- import dagger.hilt.components.SingletonComponent;
- import dagger.multibindings.IntoMap;
- import dagger.multibindings.StringKey;
- import $GENERATED_TYPE;
+ import androidx.hilt.work.WorkerAssistedFactory;
+ import androidx.work.ListenableWorker;
+ import dagger.Binds;
+ import dagger.Module;
+ import dagger.hilt.InstallIn;
+ import dagger.hilt.codegen.OriginatingElement;
+ import dagger.hilt.components.SingletonComponent;
+ import dagger.multibindings.IntoMap;
+ import dagger.multibindings.StringKey;
+ import $GENERATED_TYPE;
- $GENERATED_ANNOTATION
- @Module
- @InstallIn(SingletonComponent.class)
- @OriginatingElement(topLevelClass = MyWorker.class)
- public interface MyWorker_HiltModule {
- @Binds
- @IntoMap
- @StringKey("androidx.hilt.work.test.MyWorker")
- WorkerAssistedFactory<? extends ListenableWorker> bind(MyWorker_AssistedFactory factory)
- }
- """.toJFO("androidx.hilt.work.test.MyWorker_HiltModule")
+ $GENERATED_ANNOTATION
+ @Module
+ @InstallIn(SingletonComponent.class)
+ @OriginatingElement(
+ topLevelClass = MyWorker.class
+ )
+ public interface MyWorker_HiltModule {
+ @Binds
+ @IntoMap
+ @StringKey("androidx.hilt.work.test.MyWorker")
+ WorkerAssistedFactory<? extends ListenableWorker> bind(MyWorker_AssistedFactory factory);
+ }
+ """
+ )
- val compilation = compiler()
- .compile(myWorker, Sources.LISTENABLE_WORKER, Sources.WORKER, Sources.WORKER_PARAMETERS)
- assertThat(compilation).apply {
- succeeded()
- generatedSourceFile("androidx.hilt.work.test.MyWorker_HiltModule")
- .hasSourceEquivalentTo(expected)
+ runProcessorTest(
+ sources = listOf(
+ myWorker, Sources.LISTENABLE_WORKER, Sources.WORKER, Sources.WORKER_PARAMETERS
+ ),
+ createProcessingSteps = { listOf(WorkerStep()) }
+ ) {
+ it.generatedSource(expected)
}
}
}
diff --git a/hilt/hilt-compiler/src/test/kotlin/androidx/hilt/work/WorkerStepTest.kt b/hilt/hilt-compiler/src/test/kotlin/androidx/hilt/work/WorkerStepTest.kt
index 5d7c63a..2c6022e 100644
--- a/hilt/hilt-compiler/src/test/kotlin/androidx/hilt/work/WorkerStepTest.kt
+++ b/hilt/hilt-compiler/src/test/kotlin/androidx/hilt/work/WorkerStepTest.kt
@@ -14,12 +14,14 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalProcessingApi::class)
+
package androidx.hilt.work
-import androidx.hilt.Sources
-import androidx.hilt.compiler
-import androidx.hilt.toJFO
-import com.google.testing.compile.CompilationSubject.assertThat
+import androidx.hilt.ext.Sources
+import androidx.room.compiler.processing.ExperimentalProcessingApi
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.runProcessorTest
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -29,28 +31,33 @@
@Test
fun verifyEnclosingElementExtendsWorker() {
- val myWorker = """
- package androidx.hilt.work.test;
+ val myWorker = Source.java(
+ "androidx.hilt.work.test.MyWorker",
+ """
+ package androidx.hilt.work.test;
- import android.content.Context;
- import androidx.hilt.work.HiltWorker;
- import androidx.work.WorkerParameters;
- import dagger.assisted.Assisted;
- import dagger.assisted.AssistedInject;
+ import android.content.Context;
+ import androidx.hilt.work.HiltWorker;
+ import androidx.work.WorkerParameters;
+ import dagger.assisted.Assisted;
+ import dagger.assisted.AssistedInject;
- @HiltWorker
- class MyWorker {
- @AssistedInject
- MyWorker(@Assisted Context context, @Assisted WorkerParameters params) { }
- }
- """.toJFO("androidx.hilt.work.work.MyWorker")
+ @HiltWorker
+ class MyWorker {
+ @AssistedInject
+ MyWorker(@Assisted Context context, @Assisted WorkerParameters params) { }
+ }
+ """
+ )
- val compilation = compiler()
- .compile(myWorker, Sources.LISTENABLE_WORKER, Sources.WORKER, Sources.WORKER_PARAMETERS)
- assertThat(compilation).apply {
- failed()
- hadErrorCount(1)
- hadErrorContainingMatch(
+ runProcessorTest(
+ sources = listOf(
+ myWorker, Sources.LISTENABLE_WORKER, Sources.WORKER, Sources.WORKER_PARAMETERS
+ ),
+ createProcessingSteps = { listOf(WorkerStep()) }
+ ) {
+ it.hasErrorCount(1)
+ it.hasErrorContaining(
"@HiltWorker is only supported on types that subclass " +
"androidx.work.ListenableWorker."
)
@@ -59,37 +66,42 @@
@Test
fun verifySingleAnnotatedConstructor() {
- val myWorker = """
- package androidx.hilt.work.test;
+ val myWorker = Source.java(
+ "androidx.hilt.work.test.MyWorker",
+ """
+ package androidx.hilt.work.test;
- import android.content.Context;
- import androidx.hilt.work.HiltWorker;
- import androidx.work.Worker;
- import androidx.work.WorkerParameters;
- import dagger.assisted.Assisted;
- import dagger.assisted.AssistedInject;
- import java.lang.String;
+ import android.content.Context;
+ import androidx.hilt.work.HiltWorker;
+ import androidx.work.Worker;
+ import androidx.work.WorkerParameters;
+ import dagger.assisted.Assisted;
+ import dagger.assisted.AssistedInject;
+ import java.lang.String;
- @HiltWorker
- class MyWorker extends Worker {
- @AssistedInject
- MyWorker(@Assisted Context context, @Assisted WorkerParameters params) {
- super(context, params);
+ @HiltWorker
+ class MyWorker extends Worker {
+ @AssistedInject
+ MyWorker(@Assisted Context context, @Assisted WorkerParameters params) {
+ super(context, params);
+ }
+
+ @AssistedInject
+ MyWorker(Context context, WorkerParameters params, String s) {
+ super(context, params);
+ }
}
+ """
+ )
- @AssistedInject
- MyWorker(Context context, WorkerParameters params, String s) {
- super(context, params);
- }
- }
- """.toJFO("androidx.hilt.work.test.MyWorker")
-
- val compilation = compiler()
- .compile(myWorker, Sources.LISTENABLE_WORKER, Sources.WORKER, Sources.WORKER_PARAMETERS)
- assertThat(compilation).apply {
- failed()
- hadErrorCount(1)
- hadErrorContainingMatch(
+ runProcessorTest(
+ sources = listOf(
+ myWorker, Sources.LISTENABLE_WORKER, Sources.WORKER, Sources.WORKER_PARAMETERS
+ ),
+ createProcessingSteps = { listOf(WorkerStep()) }
+ ) {
+ it.hasErrorCount(1)
+ it.hasErrorContaining(
"@HiltWorker annotated class should contain exactly one @AssistedInject " +
"annotated constructor."
)
@@ -98,31 +110,36 @@
@Test
fun verifyNonPrivateConstructor() {
- val myWorker = """
- package androidx.hilt.work.test;
+ val myWorker = Source.java(
+ "androidx.hilt.work.test.MyWorker",
+ """
+ package androidx.hilt.work.test;
- import android.content.Context;
- import androidx.hilt.work.HiltWorker;
- import androidx.work.Worker;
- import androidx.work.WorkerParameters;
- import dagger.assisted.Assisted;
- import dagger.assisted.AssistedInject;
+ import android.content.Context;
+ import androidx.hilt.work.HiltWorker;
+ import androidx.work.Worker;
+ import androidx.work.WorkerParameters;
+ import dagger.assisted.Assisted;
+ import dagger.assisted.AssistedInject;
- @HiltWorker
- class MyWorker extends Worker {
- @AssistedInject
- private MyWorker(@Assisted Context context, @Assisted WorkerParameters params) {
- super(context, params);
+ @HiltWorker
+ class MyWorker extends Worker {
+ @AssistedInject
+ private MyWorker(@Assisted Context context, @Assisted WorkerParameters params) {
+ super(context, params);
+ }
}
- }
- """.toJFO("androidx.hilt.work.test.MyWorker")
+ """
+ )
- val compilation = compiler()
- .compile(myWorker, Sources.LISTENABLE_WORKER, Sources.WORKER, Sources.WORKER_PARAMETERS)
- assertThat(compilation).apply {
- failed()
- hadErrorCount(1)
- hadErrorContainingMatch(
+ runProcessorTest(
+ sources = listOf(
+ myWorker, Sources.LISTENABLE_WORKER, Sources.WORKER, Sources.WORKER_PARAMETERS
+ ),
+ createProcessingSteps = { listOf(WorkerStep()) }
+ ) {
+ it.hasErrorCount(1)
+ it.hasErrorContaining(
"@AssistedInject annotated constructors must not be private."
)
}
@@ -130,33 +147,38 @@
@Test
fun verifyInnerClassIsStatic() {
- val myWorker = """
- package androidx.hilt.work.test;
+ val myWorker = Source.java(
+ "androidx.hilt.work.test.Outer",
+ """
+ package androidx.hilt.work.test;
- import android.content.Context;
- import androidx.hilt.work.HiltWorker;
- import androidx.work.Worker;
- import androidx.work.WorkerParameters;
- import dagger.assisted.Assisted;
- import dagger.assisted.AssistedInject;
+ import android.content.Context;
+ import androidx.hilt.work.HiltWorker;
+ import androidx.work.Worker;
+ import androidx.work.WorkerParameters;
+ import dagger.assisted.Assisted;
+ import dagger.assisted.AssistedInject;
- class Outer {
- @HiltWorker
- class MyWorker extends Worker {
- @AssistedInject
- MyWorker(@Assisted Context context, @Assisted WorkerParameters params) {
- super(context, params);
+ class Outer {
+ @HiltWorker
+ class MyWorker extends Worker {
+ @AssistedInject
+ MyWorker(@Assisted Context context, @Assisted WorkerParameters params) {
+ super(context, params);
+ }
}
}
- }
- """.toJFO("androidx.hilt.work.test.Outer")
+ """
+ )
- val compilation = compiler()
- .compile(myWorker, Sources.LISTENABLE_WORKER, Sources.WORKER, Sources.WORKER_PARAMETERS)
- assertThat(compilation).apply {
- failed()
- hadErrorCount(1)
- hadErrorContainingMatch(
+ runProcessorTest(
+ sources = listOf(
+ myWorker, Sources.LISTENABLE_WORKER, Sources.WORKER, Sources.WORKER_PARAMETERS
+ ),
+ createProcessingSteps = { listOf(WorkerStep()) }
+ ) {
+ it.hasErrorCount(1)
+ it.hasErrorContaining(
"@HiltWorker may only be used on inner classes " +
"if they are static."
)
@@ -165,32 +187,37 @@
@Test
fun verifyConstructorAnnotation() {
- val myWorker = """
- package androidx.hilt.work.test;
+ val myWorker = Source.java(
+ "androidx.hilt.work.test.MyWorker",
+ """
+ package androidx.hilt.work.test;
- import android.content.Context;
- import androidx.hilt.work.HiltWorker;
- import androidx.work.Worker;
- import androidx.work.WorkerParameters;
- import dagger.assisted.Assisted;
- import dagger.assisted.AssistedInject;
- import java.lang.String;
- import javax.inject.Inject;
+ import android.content.Context;
+ import androidx.hilt.work.HiltWorker;
+ import androidx.work.Worker;
+ import androidx.work.WorkerParameters;
+ import dagger.assisted.Assisted;
+ import dagger.assisted.AssistedInject;
+ import java.lang.String;
+ import javax.inject.Inject;
- @HiltWorker
- class MyWorker extends Worker {
- @Inject
- MyWorker(@Assisted Context context, @Assisted WorkerParameters params) {
- super(context, params);
+ @HiltWorker
+ class MyWorker extends Worker {
+ @Inject
+ MyWorker(@Assisted Context context, @Assisted WorkerParameters params) {
+ super(context, params);
+ }
}
- }
- """.toJFO("androidx.hilt.work.test.MyWorker")
+ """
+ )
- val compilation = compiler()
- .compile(myWorker, Sources.LISTENABLE_WORKER, Sources.WORKER, Sources.WORKER_PARAMETERS)
- assertThat(compilation).apply {
- failed()
- hadErrorContainingMatch(
+ runProcessorTest(
+ sources = listOf(
+ myWorker, Sources.LISTENABLE_WORKER, Sources.WORKER, Sources.WORKER_PARAMETERS
+ ),
+ createProcessingSteps = { listOf(WorkerStep()) }
+ ) {
+ it.hasErrorContaining(
"Worker constructor should be annotated with @AssistedInject instead of @Inject."
)
}
@@ -198,31 +225,36 @@
@Test
fun verifyAssistedParamOrder() {
- val myWorker = """
- package androidx.hilt.work.test;
+ val myWorker = Source.java(
+ "androidx.hilt.work.test.MyWorker",
+ """
+ package androidx.hilt.work.test;
- import android.content.Context;
- import androidx.hilt.work.HiltWorker;
- import androidx.work.Worker;
- import androidx.work.WorkerParameters;
- import dagger.assisted.Assisted;
- import dagger.assisted.AssistedInject;
- import java.lang.String;
+ import android.content.Context;
+ import androidx.hilt.work.HiltWorker;
+ import androidx.work.Worker;
+ import androidx.work.WorkerParameters;
+ import dagger.assisted.Assisted;
+ import dagger.assisted.AssistedInject;
+ import java.lang.String;
- @HiltWorker
- class MyWorker extends Worker {
- @AssistedInject
- MyWorker(@Assisted WorkerParameters params, @Assisted Context context) {
- super(context, params);
+ @HiltWorker
+ class MyWorker extends Worker {
+ @AssistedInject
+ MyWorker(@Assisted WorkerParameters params, @Assisted Context context) {
+ super(context, params);
+ }
}
- }
- """.toJFO("androidx.hilt.work.test.MyWorker")
+ """
+ )
- val compilation = compiler()
- .compile(myWorker, Sources.LISTENABLE_WORKER, Sources.WORKER, Sources.WORKER_PARAMETERS)
- assertThat(compilation).apply {
- failed()
- hadErrorContainingMatch(
+ runProcessorTest(
+ sources = listOf(
+ myWorker, Sources.LISTENABLE_WORKER, Sources.WORKER, Sources.WORKER_PARAMETERS
+ ),
+ createProcessingSteps = { listOf(WorkerStep()) }
+ ) {
+ it.hasErrorContaining(
"The 'Context' parameter must be declared before the 'WorkerParameters' in the " +
"@AssistedInject constructor of a @HiltWorker annotated class.",
)
diff --git a/navigation/navigation-safe-args-generator/build.gradle b/navigation/navigation-safe-args-generator/build.gradle
index c63d57d..4a019c6 100644
--- a/navigation/navigation-safe-args-generator/build.gradle
+++ b/navigation/navigation-safe-args-generator/build.gradle
@@ -24,6 +24,14 @@
androidx.configureAarAsJarForConfiguration("testImplementation")
+// android.jar and xmlpull has the same classes, but android.jar has stubs instead of real
+// implementation, so we remove org.xmlpull.* from android.jar used for tests
+tasks.register("strippedAndroidJar", Jar).configure {
+ it.from(zipTree(SdkHelperKt.getSdkDependency(project).getFiles().first()))
+ it.exclude("org/xmlpull/**")
+ it.archiveFileName.set("stripped-android.jar")
+}
+
dependencies {
implementation(libs.xpp3)
implementation(libs.xmlpull)
@@ -37,24 +45,16 @@
testImplementation(projectOrArtifact(":room:room-compiler-processing-testing"), {
exclude group: "androidx.room", module: "room-compiler-processing"
})
- testImplementation(SdkHelperKt.getSdkDependency(project))
testImplementationAarAsJar(project(":navigation:navigation-common"))
testImplementationAarAsJar(project(":lifecycle:lifecycle-viewmodel-savedstate"))
+ testImplementation(tasks.named("strippedAndroidJar").map { it.outputs.files })
}
-tasks.findByName("test").doFirst {
- // android.jar and xmlpull has the same classes, but android.jar has stubs instead of real
- // implementation, so we move android.jar to end of classpath
- def classpath = it.classpath.getFiles()
- def androidJar = classpath.find { it.name == "android.jar" }
- it.classpath = files(classpath.minus(androidJar).plus(androidJar))
-}
tasks.withType(Test).configureEach {
// https://github.com/google/compile-testing/issues/222
it.jvmArgs "--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED"
}
-
androidx {
name = "Navigation TypeSafe Arguments Generator"
type = LibraryType.OTHER_CODE_PROCESSOR
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerRemoveShownItemsTest.kt b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerRemoveShownItemsTest.kt
index 3d907a4..68cd698 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerRemoveShownItemsTest.kt
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerRemoveShownItemsTest.kt
@@ -90,10 +90,18 @@
mLayoutManager.waitForLayout(2)
// .. then the views after the removed view are moved into the gap
- assertEquals(expectedResultingAdapterPosition, llm.findFirstVisibleItemPosition())
- assertEquals(expectedResultingAdapterPosition, llm.findFirstCompletelyVisibleItemPosition())
- assertEquals(expectedResultingAdapterPosition, llm.findLastCompletelyVisibleItemPosition())
- assertEquals(expectedResultingAdapterPosition, llm.findLastVisibleItemPosition())
+ mActivityRule.runOnUiThread {
+ assertEquals(expectedResultingAdapterPosition, llm.findFirstVisibleItemPosition())
+ assertEquals(
+ expectedResultingAdapterPosition,
+ llm.findFirstCompletelyVisibleItemPosition()
+ )
+ assertEquals(
+ expectedResultingAdapterPosition,
+ llm.findLastCompletelyVisibleItemPosition()
+ )
+ assertEquals(expectedResultingAdapterPosition, llm.findLastVisibleItemPosition())
+ }
}
/**
@@ -136,13 +144,18 @@
mLayoutManager.waitForLayout(2)
// .. then the views after the removed view are moved into the gap
- assertEquals(expectedResultingAdapterPosition, llm.findFirstVisibleItemPosition())
- assertEquals(expectedResultingAdapterPosition, llm.findFirstCompletelyVisibleItemPosition())
- assertEquals(
- expectedResultingAdapterPosition + 1,
- llm.findLastCompletelyVisibleItemPosition()
- )
- assertEquals(expectedResultingAdapterPosition + 1, llm.findLastVisibleItemPosition())
+ mActivityRule.runOnUiThread {
+ assertEquals(expectedResultingAdapterPosition, llm.findFirstVisibleItemPosition())
+ assertEquals(
+ expectedResultingAdapterPosition,
+ llm.findFirstCompletelyVisibleItemPosition()
+ )
+ assertEquals(
+ expectedResultingAdapterPosition + 1,
+ llm.findLastCompletelyVisibleItemPosition()
+ )
+ assertEquals(expectedResultingAdapterPosition + 1, llm.findLastVisibleItemPosition())
+ }
}
/**
@@ -183,13 +196,21 @@
mLayoutManager.waitForLayout(2)
// .. then the views after the removed view are moved into the gap
- assertEquals(expectedResultingAdapterPosition, llm.findFirstVisibleItemPosition())
- assertEquals(expectedResultingAdapterPosition, llm.findFirstCompletelyVisibleItemPosition())
- assertEquals(expectedResultingAdapterPosition, llm.findLastCompletelyVisibleItemPosition())
- assertEquals(expectedResultingAdapterPosition, llm.findLastVisibleItemPosition())
+ mActivityRule.runOnUiThread {
+ assertEquals(expectedResultingAdapterPosition, llm.findFirstVisibleItemPosition())
+ assertEquals(
+ expectedResultingAdapterPosition,
+ llm.findFirstCompletelyVisibleItemPosition()
+ )
+ assertEquals(
+ expectedResultingAdapterPosition,
+ llm.findLastCompletelyVisibleItemPosition()
+ )
+ assertEquals(expectedResultingAdapterPosition, llm.findLastVisibleItemPosition())
+ }
}
- private inner class MyLayoutManager internal constructor(context: Context, val itemSize: Int) :
+ private inner class MyLayoutManager(context: Context, val itemSize: Int) :
WrappedLinearLayoutManager(context, config.mOrientation, config.mReverseLayout) {
override fun calculateExtraLayoutSpace(
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt
index fa7b4d7..08e61c1 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt
@@ -161,9 +161,13 @@
* * thrown types are copied if the backing element is from java
*/
@JvmStatic
+ @JvmOverloads
fun overriding(
elm: XMethodElement,
- owner: XType
+ owner: XType =
+ checkNotNull(elm.enclosingElement.type) {
+ "Cannot override method without enclosing class"
+ }
): MethodSpec.Builder {
val asMember = elm.asMemberOf(owner)
return overriding(
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/JvmDescriptorUtils.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/JvmDescriptorUtils.kt
index f7dae3e..402ab9e 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/JvmDescriptorUtils.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/kotlin/JvmDescriptorUtils.kt
@@ -169,12 +169,26 @@
NestingKind.MEMBER, NestingKind.LOCAL ->
enclosingElement.internalName + "$" + simpleName
NestingKind.ANONYMOUS ->
- error("Unsupported nesting $nestingKind")
+ elementError("Unsupported nesting $nestingKind", this)
else ->
- error("Unsupported, nestingKind == null")
+ elementError("Unsupported, nestingKind == null", this)
}
is ExecutableElement -> enclosingElement.internalName
is QualifiedNameable -> qualifiedName.toString().replace('.', '/')
else -> simpleName.toString()
}
+
+ /**
+ * Throws an exception with the error [msg] and the [element] and its enclosing elements appended.
+ */
+ private fun elementError(msg: String, element: Element): Nothing {
+ fun buildName(element: Element): String {
+ val enclosingPart =
+ element.enclosingElement?.let { buildName(it) + "." } ?: ""
+ val simpleName = element.simpleName.ifEmpty { "<unnamed>" }
+ return enclosingPart + simpleName
+ }
+ val name = buildName(element)
+ error("$msg - On element $name")
+ }
}
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt
index c427a9b..eee3dd6 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt
@@ -43,7 +43,8 @@
// if true, pre-compile sources then run the test to account for changes between .class files
// and source files
val preCompiledCode: Boolean,
- val shouldMarkParamsFinal: Boolean
+ val shouldMarkParamsFinal: Boolean,
+ val ignoreOwner: Boolean
) {
@Test
fun javaOverrides() {
@@ -460,17 +461,15 @@
) { invocation ->
val (target, methods) = invocation.getOverrideTestTargets(ignoreInheritedMethods)
methods.forEachIndexed { index, method ->
- val func = if (shouldMarkParamsFinal)
- MethodSpecHelper::overridingWithFinalParams
- else
- MethodSpecHelper::overriding
+ val subject =
+ if (ignoreOwner)
+ MethodSpecHelper.overriding(method)
+ else if (shouldMarkParamsFinal)
+ MethodSpecHelper.overridingWithFinalParams(method, target.type)
+ else
+ MethodSpecHelper.overriding(method, target.type)
- val subject = func(
- method,
- target.type
- ).toSignature()
-
- assertThat(subject).isEqualTo(golden[index])
+ assertThat(subject.toSignature()).isEqualTo(golden[index])
}
}
}
@@ -573,6 +572,9 @@
owner: DeclaredType,
typeUtils: Types
): MethodSpec.Builder {
+ if (ignoreOwner) {
+ return MethodSpec.overriding(elm)
+ }
val baseSpec = MethodSpec.overriding(elm, owner, typeUtils)
.build()
@@ -600,7 +602,10 @@
companion object {
@JvmStatic
- @Parameterized.Parameters(name = "preCompiledCode={0}, shouldMarkParamsFinal={1}")
- fun params() = generateAllEnumerations(listOf(false, true), listOf(false, true))
+ @Parameterized.Parameters(
+ name = "preCompiledCode={0}, shouldMarkParamsFinal={1}, ignoreOwner={2}")
+ fun params() =
+ generateAllEnumerations(
+ listOf(false, true), listOf(false, true), listOf(false, true))
}
}
diff --git a/transition/transition/api/current.txt b/transition/transition/api/current.txt
index 672d6db..8b6b763 100644
--- a/transition/transition/api/current.txt
+++ b/transition/transition/api/current.txt
@@ -212,6 +212,7 @@
method public static void endTransitions(android.view.ViewGroup?);
method public static void go(androidx.transition.Scene);
method public static void go(androidx.transition.Scene, androidx.transition.Transition?);
+ method public static androidx.transition.TransitionSeekController? seekTo(androidx.transition.Scene, androidx.transition.Transition);
method public void setTransition(androidx.transition.Scene, androidx.transition.Scene, androidx.transition.Transition?);
method public void setTransition(androidx.transition.Scene, androidx.transition.Transition?);
method public void transitionTo(androidx.transition.Scene);
@@ -225,14 +226,18 @@
}
public interface TransitionSeekController {
+ method public void addOnProgressChangedListener(androidx.core.util.Consumer<androidx.transition.TransitionSeekController!>);
method public void addOnReadyListener(androidx.core.util.Consumer<androidx.transition.TransitionSeekController!>);
method public void animateToEnd();
method public void animateToStart();
- method public long getCurrentPlayTimeMillis();
- method public long getDurationMillis();
+ method @FloatRange(from=0.0, to=1.0) public float getCurrentFraction();
+ method @IntRange(from=0) public long getCurrentPlayTimeMillis();
+ method @IntRange(from=0) public long getDurationMillis();
method public boolean isReady();
+ method public void removeOnProgressChangedListener(androidx.core.util.Consumer<androidx.transition.TransitionSeekController!>);
method public void removeOnReadyListener(androidx.core.util.Consumer<androidx.transition.TransitionSeekController!>);
- method public void setCurrentPlayTimeMillis(long);
+ method public void setCurrentFraction(@FloatRange(from=0.0, to=1.0) float);
+ method public void setCurrentPlayTimeMillis(@IntRange(from=0) long);
}
public class TransitionSet extends androidx.transition.Transition {
diff --git a/transition/transition/api/restricted_current.txt b/transition/transition/api/restricted_current.txt
index 187290f..bd19de1 100644
--- a/transition/transition/api/restricted_current.txt
+++ b/transition/transition/api/restricted_current.txt
@@ -245,6 +245,7 @@
method public static void endTransitions(android.view.ViewGroup?);
method public static void go(androidx.transition.Scene);
method public static void go(androidx.transition.Scene, androidx.transition.Transition?);
+ method public static androidx.transition.TransitionSeekController? seekTo(androidx.transition.Scene, androidx.transition.Transition);
method public void setTransition(androidx.transition.Scene, androidx.transition.Scene, androidx.transition.Transition?);
method public void setTransition(androidx.transition.Scene, androidx.transition.Transition?);
method public void transitionTo(androidx.transition.Scene);
@@ -258,14 +259,18 @@
}
public interface TransitionSeekController {
+ method public void addOnProgressChangedListener(androidx.core.util.Consumer<androidx.transition.TransitionSeekController!>);
method public void addOnReadyListener(androidx.core.util.Consumer<androidx.transition.TransitionSeekController!>);
method public void animateToEnd();
method public void animateToStart();
- method public long getCurrentPlayTimeMillis();
- method public long getDurationMillis();
+ method @FloatRange(from=0.0, to=1.0) public float getCurrentFraction();
+ method @IntRange(from=0) public long getCurrentPlayTimeMillis();
+ method @IntRange(from=0) public long getDurationMillis();
method public boolean isReady();
+ method public void removeOnProgressChangedListener(androidx.core.util.Consumer<androidx.transition.TransitionSeekController!>);
method public void removeOnReadyListener(androidx.core.util.Consumer<androidx.transition.TransitionSeekController!>);
- method public void setCurrentPlayTimeMillis(long);
+ method public void setCurrentFraction(@FloatRange(from=0.0, to=1.0) float);
+ method public void setCurrentPlayTimeMillis(@IntRange(from=0) long);
}
public class TransitionSet extends androidx.transition.Transition {
diff --git a/transition/transition/build.gradle b/transition/transition/build.gradle
index b899cd4..8ed8a8e 100644
--- a/transition/transition/build.gradle
+++ b/transition/transition/build.gradle
@@ -12,6 +12,7 @@
implementation("androidx.collection:collection:1.1.0")
compileOnly("androidx.fragment:fragment:1.2.5")
compileOnly("androidx.appcompat:appcompat:1.0.1")
+ implementation("androidx.dynamicanimation:dynamicanimation:1.0.0")
androidTestImplementation(libs.kotlinStdlib)
androidTestImplementation(libs.testExtJunit)
diff --git a/transition/transition/src/androidTest/java/androidx/transition/SeekTransitionTest.kt b/transition/transition/src/androidTest/java/androidx/transition/SeekTransitionTest.kt
index 1700300..852db4e 100644
--- a/transition/transition/src/androidTest/java/androidx/transition/SeekTransitionTest.kt
+++ b/transition/transition/src/androidTest/java/androidx/transition/SeekTransitionTest.kt
@@ -27,6 +27,7 @@
import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import androidx.testutils.AnimationDurationScaleRule.Companion.createForAllTests
+import androidx.testutils.PollingCheck
import androidx.transition.Transition.TransitionListener
import androidx.transition.test.R
import com.google.common.truth.Truth.assertThat
@@ -43,7 +44,7 @@
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@MediumTest
class SeekTransitionTest : BaseTest() {
@get:Rule
@@ -69,7 +70,6 @@
@Test(expected = IllegalArgumentException::class)
@UiThreadTest
fun onlySeekingTransitions() {
- if (Build.VERSION.SDK_INT < 34) throw IllegalArgumentException()
transition = object : Visibility() {}
TransitionManager.controlDelayedTransition(root, transition)
fail("Expected IllegalArgumentException")
@@ -77,7 +77,6 @@
@Test
fun waitForReady() {
- if (Build.VERSION.SDK_INT < 34) return
lateinit var seekController: TransitionSeekController
@Suppress("UNCHECKED_CAST")
@@ -99,7 +98,6 @@
@Test
fun waitForReadyNoChange() {
- if (Build.VERSION.SDK_INT < 34) return
lateinit var seekController: TransitionSeekController
@Suppress("UNCHECKED_CAST")
@@ -120,7 +118,6 @@
@Test
fun addListenerAfterReady() {
- if (Build.VERSION.SDK_INT < 34) return
lateinit var seekController: TransitionSeekController
@Suppress("UNCHECKED_CAST")
@@ -148,7 +145,6 @@
@Test
fun seekTransition() {
- if (Build.VERSION.SDK_INT < 34) return
lateinit var seekController: TransitionSeekController
val listener = spy(TransitionListenerAdapter())
@@ -170,15 +166,60 @@
assertThat(seekController.durationMillis).isEqualTo(300)
assertThat(seekController.currentPlayTimeMillis).isEqualTo(0)
+ assertThat(seekController.currentFraction).isEqualTo(0f)
assertThat(view.transitionAlpha).isEqualTo(1f)
seekController.currentPlayTimeMillis = 150
+ assertThat(seekController.currentFraction).isEqualTo(0.5f)
assertThat(view.transitionAlpha).isEqualTo(0.5f)
seekController.currentPlayTimeMillis = 299
+ assertThat(seekController.currentFraction).isWithin(0.001f).of(299f / 300f)
assertThat(view.transitionAlpha).isWithin(0.001f).of(1f / 300f)
seekController.currentPlayTimeMillis = 300
+ assertThat(seekController.currentFraction).isEqualTo(1f)
+ verify(listener, times(1)).onTransitionEnd(any())
+ assertThat(view.transitionAlpha).isEqualTo(1f)
+ assertThat(view.visibility).isEqualTo(View.GONE)
+ }
+ }
+
+ @Test
+ fun seekTransitionWithFraction() {
+ lateinit var seekController: TransitionSeekController
+
+ val listener = spy(TransitionListenerAdapter())
+ transition.addListener(listener)
+
+ rule.runOnUiThread {
+ val controller = TransitionManager.controlDelayedTransition(root, transition)
+ assertThat(controller).isNotNull()
+ seekController = controller!!
+ assertThat(seekController.isReady).isFalse()
+ view.visibility = View.GONE
+ }
+
+ verify(listener, timeout(1000)).onTransitionStart(any())
+ verify(listener, times(0)).onTransitionEnd(any())
+
+ rule.runOnUiThread {
+ assertThat(view.visibility).isEqualTo(View.VISIBLE)
+
+ assertThat(seekController.durationMillis).isEqualTo(300)
+ assertThat(seekController.currentPlayTimeMillis).isEqualTo(0)
+ assertThat(seekController.currentFraction).isEqualTo(0f)
+
+ assertThat(view.transitionAlpha).isEqualTo(1f)
+
+ seekController.currentFraction = 0.5f
+ assertThat(seekController.currentPlayTimeMillis).isEqualTo(150)
+ assertThat(view.transitionAlpha).isEqualTo(0.5f)
+ seekController.currentFraction = 299f / 300f
+ assertThat(seekController.currentPlayTimeMillis).isEqualTo(299)
+ assertThat(view.transitionAlpha).isWithin(0.001f).of(1f / 300f)
+ seekController.currentFraction = 1f
+ assertThat(seekController.currentPlayTimeMillis).isEqualTo(300)
verify(listener, times(1)).onTransitionEnd(any())
assertThat(view.transitionAlpha).isEqualTo(1f)
@@ -188,7 +229,6 @@
@Test
fun animationDoesNotTakeOverSeek() {
- if (Build.VERSION.SDK_INT < 34) return
lateinit var seekController: TransitionSeekController
val stateListener1 = spy(TransitionListenerAdapter())
@@ -231,7 +271,6 @@
@Test
fun seekCannotTakeOverAnimation() {
- if (Build.VERSION.SDK_INT < 34) return
lateinit var seekController: TransitionSeekController
val stateListener1 = spy(TransitionListenerAdapter())
@@ -271,7 +310,6 @@
@Test
fun seekCannotTakeOverSeek() {
- if (Build.VERSION.SDK_INT < 34) return
lateinit var seekController1: TransitionSeekController
val stateListener1 = spy(TransitionListenerAdapter())
@@ -317,7 +355,6 @@
@Test
fun seekReplacesSeek() {
- if (Build.VERSION.SDK_INT < 34) return
lateinit var seekController1: TransitionSeekController
val stateListener1 = spy(TransitionListenerAdapter())
@@ -360,7 +397,6 @@
@Test
fun animateToEnd() {
- if (Build.VERSION.SDK_INT < 34) return
lateinit var seekController: TransitionSeekController
val listener = spy(TransitionListenerAdapter())
@@ -387,7 +423,6 @@
@Test
fun animateToStart() {
- if (Build.VERSION.SDK_INT < 34) return
lateinit var seekController: TransitionSeekController
val listener = spy(TransitionListenerAdapter())
@@ -428,7 +463,6 @@
@Test
fun animateToStartAfterAnimateToEnd() {
- if (Build.VERSION.SDK_INT < 34) return
lateinit var seekController: TransitionSeekController
val listener = spy(TransitionListenerAdapter())
@@ -448,7 +482,7 @@
seekController.animateToStart()
}
- verify(listener, timeout(3000)).onTransitionEnd(any())
+ verify(listener, timeout(3000)).onTransitionEnd(any(), eq(true))
rule.runOnUiThread {
assertThat(view.visibility).isEqualTo(View.VISIBLE)
@@ -458,7 +492,6 @@
@Test
fun animateToEndAfterAnimateToStart() {
- if (Build.VERSION.SDK_INT < 34) return
lateinit var seekController: TransitionSeekController
val listener = spy(TransitionListenerAdapter())
@@ -488,7 +521,6 @@
@Test(expected = IllegalStateException::class)
fun seekAfterAnimate() {
- if (Build.VERSION.SDK_INT < 34) throw IllegalStateException("Not supported before U")
lateinit var seekController: TransitionSeekController
transition.duration = 5000
@@ -507,9 +539,28 @@
}
}
+ @Test(expected = IllegalStateException::class)
+ fun seekFractionAfterAnimate() {
+ lateinit var seekController: TransitionSeekController
+ transition.duration = 5000
+
+ rule.runOnUiThread {
+ seekController = TransitionManager.controlDelayedTransition(root, transition)!!
+ view.visibility = View.GONE
+ }
+
+ rule.runOnUiThread {
+ seekController.currentFraction = 0.5f
+ seekController.animateToEnd()
+ }
+
+ rule.runOnUiThread {
+ seekController.currentFraction = 0.2f
+ }
+ }
+
@Test
fun seekTransitionSet() {
- if (Build.VERSION.SDK_INT < 34) return
transition = TransitionSet().also {
it.addTransition(Fade(Fade.MODE_OUT))
.addTransition(Fade(Fade.MODE_IN))
@@ -579,7 +630,6 @@
@Test
fun animateToEndTransitionSet() {
- if (Build.VERSION.SDK_INT < 34) return
transition = TransitionSet().also {
it.addTransition(Fade(Fade.MODE_OUT))
.addTransition(Fade(Fade.MODE_IN))
@@ -629,7 +679,6 @@
@Test
fun animateToStartTransitionSet() {
- if (Build.VERSION.SDK_INT < 34) return
transition = TransitionSet().also {
it.addTransition(Fade(Fade.MODE_OUT))
.addTransition(Fade(Fade.MODE_IN))
@@ -695,7 +744,6 @@
@Test
fun cancelPartOfTransitionSet() {
- if (Build.VERSION.SDK_INT < 34) return
transition = TransitionSet().also {
it.addTransition(Fade(Fade.MODE_OUT))
.addTransition(Fade(Fade.MODE_IN))
@@ -763,7 +811,6 @@
@Test
fun onTransitionCallsForwardAndReversed() {
- if (Build.VERSION.SDK_INT < 34) return
val listener = spy(TransitionListenerAdapter())
transition = Fade()
transition.addListener(listener)
@@ -792,7 +839,6 @@
@Test
fun onTransitionCallsForwardAndReversedTransitionSet() {
- if (Build.VERSION.SDK_INT < 34) return
val fadeOut = Fade(Fade.MODE_OUT)
val outListener = spy(TransitionListenerAdapter())
fadeOut.addListener(outListener)
@@ -881,7 +927,6 @@
@Test
fun pauseResumeOnSeek() {
- if (Build.VERSION.SDK_INT < 34) return
var pauseCount = 0
var resumeCount = 0
var setPauseCount = 0
@@ -942,4 +987,131 @@
assertThat(setResumeCount).isEqualTo(1)
}
}
+
+ @Test
+ fun animationListener() {
+ lateinit var seekController: TransitionSeekController
+ var animatedFraction = -1f
+ var animatedMillis = -1L
+ rule.runOnUiThread {
+ seekController = TransitionManager.controlDelayedTransition(root, Fade())!!
+ view.visibility = View.GONE
+
+ seekController.addOnProgressChangedListener {
+ animatedFraction = it.currentFraction
+ animatedMillis = it.currentPlayTimeMillis
+ }
+ }
+
+ rule.runOnUiThread {
+ assertThat(animatedFraction).isEqualTo(0f)
+ assertThat(animatedMillis).isEqualTo(0)
+ seekController.currentFraction = 0.25f
+ assertThat(animatedFraction).isEqualTo(0.25f)
+ assertThat(animatedMillis).isEqualTo(75)
+ seekController.animateToEnd()
+ }
+
+ PollingCheck.waitFor {
+ animatedFraction == 1f
+ }
+ }
+
+ @Test
+ fun animationListenerRemoval() {
+ lateinit var seekController: TransitionSeekController
+ rule.runOnUiThread {
+ seekController = TransitionManager.controlDelayedTransition(root, Fade())!!
+ view.visibility = View.GONE
+ }
+
+ var animatedFraction = -1f
+ var animatedMillis = -1L
+ val removeListener = object : Consumer<TransitionSeekController> {
+ override fun accept(t: TransitionSeekController?) {
+ seekController.removeOnProgressChangedListener(this)
+ }
+ }
+ seekController.addOnProgressChangedListener(removeListener)
+ val changeListener = Consumer<TransitionSeekController> {
+ animatedFraction = it.currentFraction
+ animatedMillis = it.currentPlayTimeMillis
+ }
+ seekController.addOnProgressChangedListener(changeListener)
+
+ rule.runOnUiThread {
+ assertThat(animatedFraction).isEqualTo(0f)
+ assertThat(animatedMillis).isEqualTo(0)
+ seekController.removeOnProgressChangedListener(changeListener)
+ seekController.currentFraction = 0.25f
+ assertThat(animatedFraction).isEqualTo(0)
+ assertThat(animatedMillis).isEqualTo(0)
+ }
+ }
+
+ @Test
+ fun seekToScene() {
+ lateinit var seekController: TransitionSeekController
+ val scene1 = Scene(root, view)
+ val view2 = View(view.context)
+ val scene2 = Scene(root, view2)
+ rule.runOnUiThread {
+ TransitionManager.go(scene1)
+ }
+
+ rule.runOnUiThread {
+ val controller = TransitionManager.seekTo(scene2, Fade())
+ assertThat(controller).isNotNull()
+ seekController = controller!!
+ }
+
+ rule.runOnUiThread {
+ assertThat(seekController.currentFraction).isEqualTo(0f)
+ assertThat(view.visibility).isEqualTo(View.VISIBLE)
+ assertThat(view.transitionAlpha).isEqualTo(1f)
+ assertThat(view.isAttachedToWindow).isTrue()
+ assertThat(view2.visibility).isEqualTo(View.VISIBLE)
+ assertThat(view2.transitionAlpha).isEqualTo(0f)
+ assertThat(view2.isAttachedToWindow).isTrue()
+ seekController.currentFraction = 1f
+ assertThat(view.visibility).isEqualTo(View.VISIBLE)
+ assertThat(view.transitionAlpha).isEqualTo(1f)
+ assertThat(view.isAttachedToWindow).isFalse()
+ assertThat(view2.visibility).isEqualTo(View.VISIBLE)
+ assertThat(view2.transitionAlpha).isEqualTo(1f)
+ assertThat(view2.isAttachedToWindow).isTrue()
+ }
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun seekToScene_notSupportedTransition() {
+ class NoSeekingTransition : Fade() {
+ override fun isSeekingSupported(): Boolean = false
+ }
+ val scene1 = Scene(root, view)
+ val view2 = View(view.context)
+ val scene2 = Scene(root, view2)
+ rule.runOnUiThread {
+ TransitionManager.go(scene1)
+ }
+
+ rule.runOnUiThread {
+ TransitionManager.seekTo(scene2, NoSeekingTransition())
+ }
+ }
+
+ @Test
+ fun seekToScene_alreadyRunningTransition() {
+ val scene1 = Scene(root, view)
+ val view2 = View(view.context)
+ val scene2 = Scene(root, view2)
+ rule.runOnUiThread {
+ TransitionManager.go(scene1)
+ }
+
+ rule.runOnUiThread {
+ TransitionManager.go(scene2, Fade())
+ assertThat(TransitionManager.seekTo(scene1, Fade())).isNull()
+ }
+ }
}
diff --git a/transition/transition/src/androidTest/java/androidx/transition/VelocityTracker1DTest.kt b/transition/transition/src/androidTest/java/androidx/transition/VelocityTracker1DTest.kt
new file mode 100644
index 0000000..d74c93e
--- /dev/null
+++ b/transition/transition/src/androidTest/java/androidx/transition/VelocityTracker1DTest.kt
@@ -0,0 +1,430 @@
+/*
+ * Copyright 2023 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.transition
+
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
+import kotlin.math.abs
+import org.junit.Test
+
+// Velocities between (1-Tolerance)*RV and (1+Tolerance)*RV are accepted
+// where RV is the "Real Velocity"
+private const val Tolerance: Float = 0.2f
+
+@SmallTest
+class VelocityTracker1DTest : BaseTest() {
+ @Test
+ fun twoPoints_nonDifferentialValues() {
+ checkTestCase(
+ VelocityTrackingTestCase(
+ dataPoints = listOf(1 to 5f, 2 to 15f),
+ expectedVelocity = 10000f
+ )
+ )
+ }
+
+ @Test
+ fun threePoints_pointerStoppedMoving_nonDifferentialValues() {
+ checkTestCase(
+ VelocityTrackingTestCase(
+ dataPoints = listOf(
+ 25 to 25f,
+ 50 to 50f,
+ 100 to 100f,
+ ),
+ // Expect 0 velocities, as the pointer will be considered to have stopped moving,
+ // due to the (100-50)=40ms gap from the last data point (i.e. it's effectively
+ // a data set with only 1 data point).
+ expectedVelocity = 0f,
+ )
+ )
+ }
+
+ /** Impulse strategy specific test cases. */
+ @Test
+ fun threePoints_zeroVelocity_nonDifferentialValues() {
+ checkTestCase(
+ VelocityTrackingTestCase(
+ dataPoints = listOf(
+ 0 to 273f,
+ 1 to 273f,
+ 2 to 273f,
+ ),
+ expectedVelocity = 0f
+ ),
+ )
+ }
+
+ @Test
+ fun resetTracking_defaultConstructor() {
+ // Fixed velocity at 5 points per 10 milliseconds
+ val tracker = VelocityTracker1D()
+ tracker.addDataPoint(0, 0f)
+ tracker.addDataPoint(10, 5f)
+ tracker.addDataPoint(20, 10f)
+ tracker.addDataPoint(30, 15f)
+ tracker.addDataPoint(40, 30f)
+
+ tracker.resetTracking()
+
+ assertThat(tracker.calculateVelocity()).isZero()
+ }
+
+ @Test
+ fun resetTracking_nonDifferentialValues_impulse() {
+ // Fixed velocity at 5 points per 10 milliseconds
+ val tracker = VelocityTracker1D()
+ tracker.addDataPoint(0, 0f)
+ tracker.addDataPoint(10, 5f)
+ tracker.addDataPoint(20, 10f)
+ tracker.addDataPoint(30, 15f)
+ tracker.addDataPoint(40, 30f)
+
+ tracker.resetTracking()
+
+ assertThat(tracker.calculateVelocity()).isZero()
+ }
+
+ @Test
+ fun linearMotion_positiveVelocity_positiveDataPoints_nonDifferentialValues() {
+ // Fixed velocity at 5 points per 10 milliseconds
+ checkTestCase(
+ VelocityTrackingTestCase(
+ dataPoints = listOf(
+ 0 to 0f,
+ 10 to 5f,
+ 20 to 10f,
+ 30 to 15f,
+ ),
+ expectedVelocity = 500f,
+ )
+ )
+ }
+
+ @Test
+ fun linearMotion_positiveVelocity_negativeDataPoints_nonDifferentialValues() {
+ // Fixed velocity at 5 points per 10 milliseconds
+ checkTestCase(
+ VelocityTrackingTestCase(
+ dataPoints = listOf(
+ 0 to -20f,
+ 10 to -15f,
+ 20 to -10f,
+ ),
+ expectedVelocity = 500f,
+ )
+ )
+ }
+
+ @Test
+ fun linearMotion_positiveVelocity_mixedSignDataPoints_nonDifferentialValues() {
+ // Fixed velocity at 5 points per 10 milliseconds
+ checkTestCase(
+ VelocityTrackingTestCase(
+ dataPoints = listOf(
+ 0 to -5f,
+ 10 to 0f,
+ 20 to 5f,
+ ),
+ expectedVelocity = 500f,
+ )
+ )
+ }
+
+ @Test
+ fun linearMotion_negativeVelocity_negativeDataPoints_nonDifferentialValues() {
+ // Fixed velocity at 5 points per 10 milliseconds
+ checkTestCase(
+ VelocityTrackingTestCase(
+ dataPoints = listOf(
+ 0 to 0f,
+ 10 to -5f,
+ 20 to -10f,
+ ),
+ expectedVelocity = -500f,
+ )
+ )
+ }
+
+ @Test
+ fun linearMotion_negativeVelocity_postiveDataPoints_nonDifferentialValues() {
+ // Fixed velocity at 5 points per 10 milliseconds
+ checkTestCase(
+ VelocityTrackingTestCase(
+ dataPoints = listOf(
+ 0 to 20f,
+ 10 to 15f,
+ 20 to 10f,
+ ),
+ expectedVelocity = -500f,
+ )
+ )
+ }
+
+ @Test
+ fun linearMotion_negativeVelocity_mixedSignDataPoints_nonDifferentialValues() {
+ // Fixed velocity at 5 points per 10 milliseconds
+ checkTestCase(
+ VelocityTrackingTestCase(
+ dataPoints = listOf(
+ 0 to 5f,
+ 10 to 0f,
+ 20 to -5f,
+ ),
+ expectedVelocity = -500f,
+ )
+ )
+ }
+
+ @Test
+ fun linearHalfMotion() {
+ // Stay still for 50 ms, and then move 100 points in the final 50 ms.
+ // The final line is sloped at 2 units/ms.
+ // This can be visualized as 2 lines: flat line (50ms), and line with slope of 2 units/ms.
+ checkTestCase(
+ VelocityTrackingTestCase(
+ dataPoints = listOf(
+ 0 to 0f,
+ 10 to 0f,
+ 20 to 0f,
+ 30 to 0f,
+ 40 to 0f,
+ 50 to 0f,
+ 60 to 20f,
+ 70 to 40f,
+ 80 to 60f,
+ 90 to 80f,
+ 100 to 100f,
+ ),
+ expectedVelocity = 2000f
+ ),
+ )
+ }
+
+ @Test
+ fun linearHalfMotionSampled() {
+ // Linear half motion, but sampled much less frequently. The resulting velocity is higher
+ // than the previous test, because the path looks significantly different now if you
+ // were to just plot these points.
+ checkTestCase(
+ VelocityTrackingTestCase(
+ dataPoints = listOf(
+ 0 to 0f,
+ 30 to 0f,
+ 40 to 0f,
+ 70 to 40f,
+ 100 to 100f,
+ ),
+ expectedVelocity = 2018.2f
+ )
+ )
+ }
+
+ @Test
+ fun linearMotionFollowedByFlatLine() {
+ // Fixed velocity at first, but flat line afterwards.
+ checkTestCase(
+ VelocityTrackingTestCase(
+ dataPoints = listOf(
+ 0 to 0f,
+ 10 to 10f,
+ 20 to 20f,
+ 30 to 30f,
+ 40 to 40f,
+ 50 to 50f,
+ 60 to 50f,
+ 70 to 50f,
+ 80 to 50f,
+ 90 to 50f,
+ 100 to 50f,
+ ),
+ expectedVelocity = 1000f
+ )
+ )
+ }
+
+ @Test
+ fun linearMotionFollowedByFlatLineWithoutIntermediatePoints() {
+ // Fixed velocity at first, but flat line afterwards
+ checkTestCase(
+ VelocityTrackingTestCase(
+ dataPoints = listOf(
+ 0 to 0f,
+ 50 to 50f,
+ 100 to 50f,
+ ),
+ expectedVelocity = 0f
+ ),
+ )
+ }
+
+ @Test
+ fun swordfishFlingDown_xValues() {
+ checkTestCase(
+ VelocityTrackingTestCase(
+ dataPoints = listOf(
+ 0 to 271f,
+ 16 to 269.786346f,
+ 35 to 267.983063f,
+ 52 to 262.638397f,
+ 68 to 266.138824f,
+ 85 to 274.79245f,
+ 96 to 274.79245f,
+ ),
+ expectedVelocity = 623.57f
+ )
+ )
+ }
+
+ @Test
+ fun swordfishFlingDown_yValues() {
+ checkTestCase(
+ VelocityTrackingTestCase(
+ dataPoints = listOf(
+ 0 to 96f,
+ 16 to 106.922775f,
+ 35 to 156.660034f,
+ 52 to 220.339081f,
+ 68 to 331.581116f,
+ 85 to 428.113159f,
+ 96 to 428.113159f,
+ ),
+ expectedVelocity = 5970.73f
+ )
+ )
+ }
+
+ @Test
+ fun sailfishFlingUpSlow_xValues() {
+ checkTestCase(
+ VelocityTrackingTestCase(
+ dataPoints = listOf(
+ 235089067 to 528.0f,
+ 235089084 to 527.0f,
+ 235089093 to 527.0f,
+ 235089095 to 527.0f,
+ 235089101 to 527.0f,
+ 235089110 to 528.0f,
+ 235089112 to 528.25f,
+ 235089118 to 531.0f,
+ 235089126 to 535.0f,
+ 235089129 to 536.33f,
+ 235089135 to 540.0f,
+ 235089144 to 546.0f,
+ 235089146 to 547.21f,
+ 235089152 to 553.0f,
+ 235089160 to 559.0f,
+ 235089162 to 560.66f,
+ ),
+ expectedVelocity = 764.34f,
+ )
+ )
+ }
+
+ @Test
+ fun sailfishFlingUpSlow_yValues() {
+ checkTestCase(
+ VelocityTrackingTestCase(
+ dataPoints = listOf(
+ 235089067 to 983.0f,
+ 235089084 to 981.0f,
+ 235089093 to 977.0f,
+ 235089095 to 975.93f,
+ 235089101 to 970.0f,
+ 235089110 to 960.0f,
+ 235089112 to 957.51f,
+ 235089118 to 946.0f,
+ 235089126 to 931.0f,
+ 235089129 to 926.02f,
+ 235089135 to 914.0f,
+ 235089144 to 896.0f,
+ 235089146 to 892.36f,
+ 235089152 to 877.0f,
+ 235089160 to 851.0f,
+ 235089162 to 843.82f,
+ ),
+ expectedVelocity = -3604.82f,
+ )
+ )
+ }
+
+ @Test
+ fun sailfishFlingUpFast_xValues() {
+ // Some "repeated" data points are removed, since the conversion from ns to ms made some
+ // data ponits "repeated"
+ checkTestCase(
+ VelocityTrackingTestCase(
+ dataPoints = listOf(
+ 920922 to 561.0f,
+ 920930 to 559.0f,
+ 920938 to 559.0f,
+ 920947 to 562.91f,
+ 920955 to 577.0f,
+ 920963 to 596.87f,
+ 920972 to 631.0f,
+ 920980 to 671.31f,
+ 920989 to 715.0f,
+ ),
+ expectedVelocity = 5670.32f,
+ )
+ )
+ }
+
+ @Test
+ fun sailfishFlingUpFast_yValues() {
+ // Some "repeated" data points are removed, since the conversion from ns to ms made some
+ // data ponits "repeated"
+ checkTestCase(
+ VelocityTrackingTestCase(
+ dataPoints = listOf(
+ 920922 to 1412.0f,
+ 920930 to 1377.0f,
+ 920938 to 1371.0f,
+ 920947 to 1342.68f,
+ 920955 to 1272.0f,
+ 920963 to 1190.54f,
+ 920972 to 1093.0f,
+ 920980 to 994.68f,
+ 920989 to 903.0f,
+ ),
+ expectedVelocity = -13021.10f,
+ )
+ )
+ }
+
+ private fun checkTestCase(testCase: VelocityTrackingTestCase) {
+ val expectedVelocity = testCase.expectedVelocity
+ val tracker = VelocityTracker1D()
+ testCase.dataPoints.forEach {
+ tracker.addDataPoint(it.first.toLong(), it.second)
+ }
+
+ Truth.assertWithMessage(
+ "Wrong velocity for data points: ${testCase.dataPoints}" +
+ "\nExpected velocity: {$expectedVelocity}"
+ )
+ .that(tracker.calculateVelocity())
+ .isWithin(abs(expectedVelocity) * Tolerance)
+ .of(expectedVelocity)
+ }
+}
+
+/** Holds configs for a velocity tracking test case, for convenience. */
+private data class VelocityTrackingTestCase(
+ val dataPoints: List<Pair<Int, Float>>,
+ val expectedVelocity: Float
+)
diff --git a/transition/transition/src/main/java/androidx/transition/Transition.java b/transition/transition/src/main/java/androidx/transition/Transition.java
index c2c0986..9cdb387 100644
--- a/transition/transition/src/main/java/androidx/transition/Transition.java
+++ b/transition/transition/src/main/java/androidx/transition/Transition.java
@@ -22,7 +22,6 @@
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.TimeInterpolator;
-import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
@@ -55,6 +54,10 @@
import androidx.core.content.res.TypedArrayUtils;
import androidx.core.util.Consumer;
import androidx.core.view.ViewCompat;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.FloatValueHolder;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -1920,7 +1923,7 @@
runAnimators();
} else if (Build.VERSION.SDK_INT >= 34) {
prepareAnimatorsForSeeking();
- mSeekController.setCurrentPlayTimeMillis(0);
+ mSeekController.initPlayTime();
mSeekController.ready();
}
}
@@ -2690,14 +2693,17 @@
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
class SeekController extends TransitionListenerAdapter implements TransitionSeekController,
- ValueAnimator.AnimatorUpdateListener {
+ DynamicAnimation.OnAnimationUpdateListener {
+ // Animation calculations appear to work better with numbers that range greater than 1
private long mCurrentPlayTime = -1;
private ArrayList<Consumer<TransitionSeekController>> mOnReadyListeners = null;
+ private ArrayList<Consumer<TransitionSeekController>> mOnProgressListeners = null;
private boolean mIsReady;
private boolean mIsCanceled;
- private ValueAnimator mAnimator;
- private boolean mIsAnimatingReversed;
+ private SpringAnimation mSpringAnimation;
+ private Consumer<TransitionSeekController>[] mListenerCache = null;
+ private final VelocityTracker1D mVelocityTracker = new VelocityTracker1D();
@Override
public long getDurationMillis() {
@@ -2710,6 +2716,11 @@
}
@Override
+ public float getCurrentFraction() {
+ return ((float) getCurrentPlayTimeMillis()) / ((float) getDurationMillis());
+ }
+
+ @Override
public boolean isReady() {
return mIsReady;
}
@@ -2723,34 +2734,54 @@
onReadyListeners.get(i).accept(this);
}
}
+ callProgressListeners();
}
@Override
public void setCurrentPlayTimeMillis(long playTimeMillis) {
- if (mAnimator != null) {
+ if (mSpringAnimation != null) {
throw new IllegalStateException("setCurrentPlayTimeMillis() called after animation "
+ "has been started");
}
- if (playTimeMillis == mCurrentPlayTime) {
+ if (playTimeMillis == mCurrentPlayTime || !isReady()) {
return; // no change
}
+ long targetPlayTime = playTimeMillis;
if (!mIsCanceled) {
- if (playTimeMillis == 0 && mCurrentPlayTime > 0) {
+ if (targetPlayTime == 0 && mCurrentPlayTime > 0) {
// Force the transition to end
- playTimeMillis = -1;
+ targetPlayTime = -1;
} else {
long duration = getDurationMillis();
// Force the transition to the end
- if (playTimeMillis == duration && mCurrentPlayTime < duration) {
- playTimeMillis = duration + 1;
+ if (targetPlayTime == duration && mCurrentPlayTime < duration) {
+ targetPlayTime = duration + 1;
}
}
- if (playTimeMillis != mCurrentPlayTime) {
- Transition.this.setCurrentPlayTimeMillis(playTimeMillis, mCurrentPlayTime);
- mCurrentPlayTime = playTimeMillis;
+ if (targetPlayTime != mCurrentPlayTime) {
+ Transition.this.setCurrentPlayTimeMillis(targetPlayTime, mCurrentPlayTime);
+ mCurrentPlayTime = targetPlayTime;
}
}
+ callProgressListeners();
+ mVelocityTracker.addDataPoint(AnimationUtils.currentAnimationTimeMillis(),
+ (float) targetPlayTime);
+ }
+
+ void initPlayTime() {
+ long playTime = (getDurationMillis() == 0) ? 1 : 0;
+ Transition.this.setCurrentPlayTimeMillis(playTime, mCurrentPlayTime);
+ mCurrentPlayTime = playTime;
+ }
+
+ @Override
+ public void setCurrentFraction(float fraction) {
+ if (mSpringAnimation != null) {
+ throw new IllegalStateException("setCurrentFraction() called after animation "
+ + "has been started");
+ }
+ setCurrentPlayTimeMillis((long) (fraction * getDurationMillis()));
}
@Override
@@ -2785,55 +2816,85 @@
}
@Override
- public void onAnimationUpdate(@NonNull ValueAnimator valueAnimator) {
- long time = Math.max(-1,
- Math.min(getDurationMillis() + 1, mAnimator.getCurrentPlayTime())
- );
- if (mIsAnimatingReversed) {
- time = getDurationMillis() - time;
- }
+ public void onAnimationUpdate(DynamicAnimation animation, float value, float velocity) {
+ long time = Math.max(-1, Math.min(getDurationMillis() + 1, Math.round((double) value)));
Transition.this.setCurrentPlayTimeMillis(time, mCurrentPlayTime);
mCurrentPlayTime = time;
+ callProgressListeners();
}
- private void createAnimator() {
- long duration = getDurationMillis() + 1;
- mAnimator = ValueAnimator.ofInt((int) duration);
- mAnimator.setInterpolator(null);
- mAnimator.setDuration(duration);
- mAnimator.addUpdateListener(this);
+ private void ensureAnimation() {
+ if (mSpringAnimation != null) {
+ return;
+ }
+ mVelocityTracker.addDataPoint(AnimationUtils.currentAnimationTimeMillis(),
+ (float) mCurrentPlayTime);
+ mSpringAnimation = new SpringAnimation(new FloatValueHolder());
+ SpringForce springForce = new SpringForce();
+ springForce.setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY);
+ springForce.setStiffness(SpringForce.STIFFNESS_LOW);
+ mSpringAnimation.setSpring(springForce);
+ mSpringAnimation.setStartValue((float) mCurrentPlayTime);
+ mSpringAnimation.addUpdateListener(this);
+ mSpringAnimation.setStartVelocity(mVelocityTracker.calculateVelocity());
+ mSpringAnimation.setMaxValue((float) (getDurationMillis() + 1));
+ mSpringAnimation.setMinValue(-1f);
+ mSpringAnimation.setMinimumVisibleChange(4f); // 4 milliseconds ~ 1/2 frame @ 120Hz
+ mSpringAnimation.addEndListener((anim, canceled, value, velocity) -> {
+ if (!canceled) {
+ boolean isReversed = value < 1f;
+ notifyListeners(TransitionNotification.ON_END, isReversed);
+ }
+ mSpringAnimation = null;
+ });
}
@Override
public void animateToEnd() {
- if (mAnimator != null) {
- mAnimator.cancel();
- }
- final long duration = getDurationMillis();
- if (mCurrentPlayTime > duration) {
- return; // we're already at the end
- }
- createAnimator();
- mIsAnimatingReversed = false;
- mAnimator.setCurrentPlayTime(mCurrentPlayTime);
- mAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- notifyListeners(TransitionNotification.ON_END, false);
- }
- });
- mAnimator.start();
+ ensureAnimation();
+ mSpringAnimation.animateToFinalPosition((float) (getDurationMillis() + 1));
}
@Override
public void animateToStart() {
- if (mAnimator != null) {
- mAnimator.cancel();
+ ensureAnimation();
+ mSpringAnimation.animateToFinalPosition(-1);
+ }
+
+ @Override
+ public void addOnProgressChangedListener(
+ @NonNull Consumer<TransitionSeekController> consumer) {
+ if (mOnProgressListeners == null) {
+ mOnProgressListeners = new ArrayList<>();
}
- createAnimator();
- mAnimator.setCurrentPlayTime(getDurationMillis() - mCurrentPlayTime);
- mIsAnimatingReversed = true;
- mAnimator.start();
+ mOnProgressListeners.add(consumer);
+ }
+
+ @Override
+ public void removeOnProgressChangedListener(
+ @NonNull Consumer<TransitionSeekController> consumer) {
+ if (mOnProgressListeners != null) {
+ mOnProgressListeners.remove(consumer);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private void callProgressListeners() {
+ if (mOnProgressListeners == null || mOnProgressListeners.isEmpty()) {
+ return;
+ }
+ int size = mOnProgressListeners.size();
+ if (mListenerCache == null) {
+ mListenerCache = new Consumer[size];
+ }
+ Consumer<TransitionSeekController>[] cache =
+ mOnProgressListeners.toArray(mListenerCache);
+ mListenerCache = null;
+ for (int i = 0; i < size; i++) {
+ cache[i].accept(this);
+ cache[i] = null;
+ }
+ mListenerCache = cache;
}
}
}
diff --git a/transition/transition/src/main/java/androidx/transition/TransitionManager.java b/transition/transition/src/main/java/androidx/transition/TransitionManager.java
index 62ee14c..2fdb6d5 100644
--- a/transition/transition/src/main/java/androidx/transition/TransitionManager.java
+++ b/transition/transition/src/main/java/androidx/transition/TransitionManager.java
@@ -350,6 +350,59 @@
}
/**
+ * Convenience method to seek to the given scene using the given transition. If seeking
+ * is not supported because the device is {@link Build.VERSION_CODES.TIRAMISU} or earlier,
+ * the scene transition is immediate and {@code null} is returned.
+ *
+ * @param scene The Scene to change to
+ * @param transition The transition to use for this scene change.
+ * @return a {@link TransitionSeekController} that can be used control the animation to the
+ * destination scene. {@code null} is returned when seeking is not supported on the scene,
+ * either because it is running on {@link android.os.Build.VERSION_CODES.TIRAMISU} or earlier,
+ * another Transition is being captured for {@code sceneRoot}, or {@code sceneRoot} hasn't
+ * had a layout yet.
+ * @throws IllegalArgumentException if {@code transition} returns {@code false} from
+ * {@link Transition#isSeekingSupported()}.
+ */
+ @Nullable
+ public static TransitionSeekController seekTo(
+ @NonNull Scene scene,
+ @NonNull Transition transition
+ ) {
+ final ViewGroup sceneRoot = scene.getSceneRoot();
+
+ if (!transition.isSeekingSupported()) {
+ throw new IllegalArgumentException("The Transition must support seeking.");
+ }
+ if (sPendingTransitions.contains(sceneRoot)) {
+ return null; // Already in the process of transitioning
+ }
+ Scene oldScene = Scene.getCurrentScene(sceneRoot);
+ if (!ViewCompat.isLaidOut(sceneRoot)
+ || Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE
+ ) {
+ // Can't control it, so just change the scene immediately
+ if (oldScene != null) {
+ oldScene.exit();
+ }
+ scene.enter();
+ return null;
+ }
+ sPendingTransitions.add(sceneRoot);
+ final Transition transitionClone = transition.clone();
+ final TransitionSet set = new TransitionSet();
+ set.addTransition(transitionClone);
+ if (oldScene != null && oldScene.isCreatedFromLayoutResource()) {
+ set.setCanRemoveViews(true);
+ }
+ sceneChangeSetup(sceneRoot, set);
+ scene.enter();
+
+ sceneChangeRunTransition(sceneRoot, set);
+ return set.createSeekController();
+ }
+
+ /**
* Convenience method to simply change to the given scene using
* the given transition.
*
diff --git a/transition/transition/src/main/java/androidx/transition/TransitionSeekController.java b/transition/transition/src/main/java/androidx/transition/TransitionSeekController.java
index bcede11..f9b00c5 100644
--- a/transition/transition/src/main/java/androidx/transition/TransitionSeekController.java
+++ b/transition/transition/src/main/java/androidx/transition/TransitionSeekController.java
@@ -18,6 +18,8 @@
import android.view.ViewGroup;
+import androidx.annotation.FloatRange;
+import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.core.util.Consumer;
@@ -31,15 +33,24 @@
/**
* @return The total duration, in milliseconds, of the Transition's animations.
*/
+ @IntRange(from = 0)
long getDurationMillis();
/**
* @return The time, in milliseconds, of the animation. This will be between 0
* and {@link #getDurationMillis()}.
*/
+ @IntRange(from = 0)
long getCurrentPlayTimeMillis();
/**
+ * @return The fraction, between 0 and 1, of the progress of the transition.
+ * @see #getCurrentPlayTimeMillis()
+ */
+ @FloatRange(from = 0.0, to = 1.0)
+ float getCurrentFraction();
+
+ /**
* Returns {@code true} when the Transition is ready to seek or {@code false}
* when the Transition's animations have yet to be built.
*/
@@ -73,14 +84,26 @@
void animateToEnd();
/**
+ * Sets the position of the Transition's animation. {@code fraction} should be
+ * between 0 and 1, inclusive, where 0 indicates that the transition hasn't progressed and 1
+ * indicates that the transition is completed. Calling this before {@link #isReady()} is
+ * {@code true} will do nothing.
+ *
+ * @param fraction The fraction, between 0 and 1, inclusive, of the progress of the transition.
+ * @see #setCurrentPlayTimeMillis(long)
+ */
+ void setCurrentFraction(@FloatRange(from = 0.0, to = 1.0) float fraction);
+
+ /**
* Sets the position of the Transition's animation. {@code playTimeMillis} should be
- * between 0 and {@link #getDurationMillis()}. This should not be called when
- * {@link #isReady()} is {@code false}.
+ * between 0 and {@link #getDurationMillis()}. Calling this before {@link #isReady()} is
+ * {@code true} will do nothing.
*
* @param playTimeMillis The time, between 0 and {@link #getDurationMillis()} that the
* animation should play.
+ * @see #setCurrentFraction(float)
*/
- void setCurrentPlayTimeMillis(long playTimeMillis);
+ void setCurrentPlayTimeMillis(@IntRange(from = 0) long playTimeMillis);
/**
* Adds a listener to know when {@link #isReady()} is {@code true}. The listener will
@@ -98,5 +121,20 @@
* @param onReadyListener The listener to be removed so that it won't be notified when ready.
*/
void removeOnReadyListener(@NonNull Consumer<TransitionSeekController> onReadyListener);
+
+ /**
+ * Add a listener for whenever the progress of the transition is changed. This will be called
+ * when {@link #setCurrentPlayTimeMillis(long)} or {@link #setCurrentFraction(float)} are
+ * called as well as when the animation from {@link #animateToEnd()} or
+ * {@link #animateToStart()} changes the progress.
+ * @param consumer A method that accepts this TransitionSeekController.
+ */
+ void addOnProgressChangedListener(@NonNull Consumer<TransitionSeekController> consumer);
+
+ /**
+ * Remove a listener previously added in {@link #addOnProgressChangedListener(Consumer)}\
+ * @param consumer The listener to be removed.
+ */
+ void removeOnProgressChangedListener(@NonNull Consumer<TransitionSeekController> consumer);
}
diff --git a/transition/transition/src/main/java/androidx/transition/VelocityTracker1D.java b/transition/transition/src/main/java/androidx/transition/VelocityTracker1D.java
new file mode 100644
index 0000000..e4e7ccb
--- /dev/null
+++ b/transition/transition/src/main/java/androidx/transition/VelocityTracker1D.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2023 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.transition;
+
+import java.util.Arrays;
+
+/**
+ * Velocity Tracker, simplified from compose's VelocityTracker1D.
+ */
+class VelocityTracker1D {
+ private static final int HISTORY_SIZE = 20;
+ private static final int ASSUME_POINTER_MOVE_STOPPED_MILLIS = 40;
+ private static final int HORIZON_MILLIS = 100;
+
+ // Circular buffer; current sample at index.
+ private long[] mTimeSamples = new long[HISTORY_SIZE];
+ private float[] mDataSamples = new float[HISTORY_SIZE];
+ private int mIndex = 0;
+
+ VelocityTracker1D() {
+ Arrays.fill(mTimeSamples, Long.MIN_VALUE);
+ }
+
+ /**
+ * Adds a data point for velocity calculation at a given time, {@code timeMillis}. The data
+ * point represents an absolute position.
+ * <p>
+ * Use the same units for the data points provided. For example, having some data points in `cm`
+ * and some in `m` will result in incorrect velocity calculations, as this method (and the
+ * tracker) has no knowledge of the units used.
+ */
+ public void addDataPoint(long timeMillis, float data) {
+ mIndex = (mIndex + 1) % HISTORY_SIZE;
+ mTimeSamples[mIndex] = timeMillis;
+ mDataSamples[mIndex] = data;
+ }
+
+ public void resetTracking() {
+ mIndex = 0;
+ Arrays.fill(mTimeSamples, Long.MIN_VALUE);
+ Arrays.fill(mDataSamples, 0f);
+ }
+
+ /**
+ * Computes the estimated velocity at the time of the last provided data point. The units of
+ * velocity will be `units/second`, where `units` is the units of the data points provided via
+ * [addDataPoint].
+ *
+ * This can be expensive. Only call this when you need the velocity.
+ */
+ float calculateVelocity() {
+ int sampleCount = 0;
+ int index = mIndex;
+
+ if (index == 0 && mTimeSamples[index] == Long.MIN_VALUE) {
+ return 0f; // We haven't received any data
+ }
+
+ // The sample at index is our newest sample. If it is null, we have no samples so return.
+ long newestTime = mTimeSamples[index];
+
+ long previousTime = newestTime;
+
+ // Starting with the most recent sample, iterate backwards while
+ // the samples represent continuous motion.
+ do {
+ long sampleTime = mTimeSamples[index];
+ if (sampleTime == Long.MIN_VALUE) {
+ break; // no point here
+ }
+ float age = newestTime - sampleTime;
+ float delta = Math.abs(sampleTime - previousTime);
+ previousTime = sampleTime;
+
+ if (age > HORIZON_MILLIS || delta > ASSUME_POINTER_MOVE_STOPPED_MILLIS) {
+ break;
+ }
+
+ index = (index == 0 ? HISTORY_SIZE : index) - 1;
+ sampleCount++;
+ } while (sampleCount < HISTORY_SIZE);
+
+ if (sampleCount < 2) {
+ return 0f; // Not enough data to have a velocity
+ }
+
+ if (sampleCount == 2) {
+ // Simple diff in time
+ int prevIndex = mIndex == 0 ? HISTORY_SIZE - 1 : mIndex - 1;
+ float timeDiff = mTimeSamples[mIndex] - mTimeSamples[prevIndex];
+ if (timeDiff == 0f) {
+ return 0f;
+ }
+ float dataDiff = mDataSamples[mIndex] - mDataSamples[prevIndex];
+ return dataDiff / timeDiff * 1000;
+ }
+
+ float work = 0f;
+ int startIndex = (mIndex - sampleCount + HISTORY_SIZE + 1) % HISTORY_SIZE;
+ int endIndex = (mIndex + 1 + HISTORY_SIZE) % HISTORY_SIZE;
+ previousTime = mTimeSamples[startIndex];
+ float previousData = mDataSamples[startIndex];
+ for (int i = (startIndex + 1) % HISTORY_SIZE; i != endIndex; i = (i + 1) % HISTORY_SIZE) {
+ long time = mTimeSamples[i];
+ long timeDelta = time - previousTime;
+ if (timeDelta == 0f) {
+ continue;
+ }
+ float data = mDataSamples[i];
+ float vPrev = kineticEnergyToVelocity(work);
+ float dataPointsDelta = data - previousData;
+
+ float vCurr = dataPointsDelta / timeDelta;
+ work += (vCurr - vPrev) * Math.abs(vCurr);
+ if (i == startIndex + 1) {
+ work = (work * 0.5f);
+ }
+ previousTime = time;
+ previousData = data;
+ }
+ return kineticEnergyToVelocity(work) * 1000;
+ }
+
+ private float kineticEnergyToVelocity(float kineticEnergy) {
+ return (float) (Math.signum(kineticEnergy) * Math.sqrt(2 * Math.abs(kineticEnergy)));
+ }
+}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/CardDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/CardDemo.kt
new file mode 100644
index 0000000..45c3ad7
--- /dev/null
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/CardDemo.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2023 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.wear.compose.material3.demos
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
+import androidx.wear.compose.material3.AppCard
+import androidx.wear.compose.material3.CardDefaults
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.ListHeader
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.samples.AppCardSample
+import androidx.wear.compose.material3.samples.AppCardWithIconSample
+import androidx.wear.compose.material3.samples.CardSample
+import androidx.wear.compose.material3.samples.OutlinedAppCardSample
+import androidx.wear.compose.material3.samples.OutlinedCardSample
+import androidx.wear.compose.material3.samples.OutlinedTitleCardSample
+import androidx.wear.compose.material3.samples.R
+import androidx.wear.compose.material3.samples.TitleCardSample
+import androidx.wear.compose.material3.samples.TitleCardWithImageSample
+
+@Composable
+fun CardDemo() {
+ ScalingLazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ item { ListHeader { Text("Card") } }
+ item { CardSample() }
+ item { OutlinedCardSample() }
+
+ item { ListHeader { Text("App card") } }
+ item { AppCardSample() }
+ item { AppCardWithIconSample() }
+ item { OutlinedAppCardSample() }
+
+ item { ListHeader { Text("Title card") } }
+ item { TitleCardSample() }
+ item { OutlinedTitleCardSample() }
+
+ item { ListHeader { Text("Image card") } }
+ item {
+ AppCard(
+ onClick = { /* Do something */ },
+ appName = { Text("App name") },
+ appImage = {
+ Icon(
+ Icons.Filled.Favorite,
+ contentDescription = "favourites",
+ modifier = Modifier.size(CardDefaults.AppImageSize)
+ )
+ },
+ title = { Text("Card title") },
+ time = { Text("now") },
+ colors = CardDefaults.imageCardColors(
+ containerPainter = CardDefaults.imageWithScrimBackgroundPainter(
+ backgroundImagePainter = painterResource(id = R.drawable.backgroundimage)
+ ),
+ contentColor = MaterialTheme.colorScheme.onSurface,
+ titleColor = MaterialTheme.colorScheme.onSurface
+ ),
+ modifier = Modifier.semantics { contentDescription = "Background image" }
+ ) {
+ Text("Card content")
+ }
+ }
+ item { TitleCardWithImageSample() }
+ }
+}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
index 13e7784..c239cfe 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
@@ -16,27 +16,16 @@
package androidx.wear.compose.material3.demos
-import androidx.compose.ui.Alignment
-import androidx.wear.compose.foundation.lazy.AutoCenteringParams
-import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.integration.demos.common.Centralize
import androidx.wear.compose.integration.demos.common.ComposableDemo
import androidx.wear.compose.integration.demos.common.DemoCategory
-import androidx.wear.compose.material3.samples.AppCardSample
-import androidx.wear.compose.material3.samples.AppCardWithIconSample
-import androidx.wear.compose.material3.samples.CardSample
import androidx.wear.compose.material3.samples.EdgeSwipeForSwipeToDismiss
import androidx.wear.compose.material3.samples.FixedFontSize
-import androidx.wear.compose.material3.samples.OutlinedAppCardSample
-import androidx.wear.compose.material3.samples.OutlinedCardSample
-import androidx.wear.compose.material3.samples.OutlinedTitleCardSample
import androidx.wear.compose.material3.samples.SimpleSwipeToDismissBox
import androidx.wear.compose.material3.samples.StatefulSwipeToDismissBox
import androidx.wear.compose.material3.samples.StepperSample
import androidx.wear.compose.material3.samples.StepperWithIntegerSample
import androidx.wear.compose.material3.samples.StepperWithRangeSemanticsSample
-import androidx.wear.compose.material3.samples.TitleCardSample
-import androidx.wear.compose.material3.samples.TitleCardWithImageSample
val WearMaterial3Demos = DemoCategory(
"Material 3",
@@ -64,26 +53,9 @@
},
)
),
- DemoCategory(
- "Card",
- listOf(
- ComposableDemo("Samples") {
- ScalingLazyColumn(
- horizontalAlignment = Alignment.CenterHorizontally,
- autoCentering = AutoCenteringParams(itemIndex = 0)
- ) {
- item { CardSample() }
- item { AppCardSample() }
- item { AppCardWithIconSample() }
- item { TitleCardSample() }
- item { TitleCardWithImageSample() }
- item { OutlinedCardSample() }
- item { OutlinedAppCardSample() }
- item { OutlinedTitleCardSample() }
- }
- }
- )
- ),
+ ComposableDemo("Card") {
+ CardDemo()
+ },
ComposableDemo("Text Button") {
TextButtonDemo()
},
@@ -113,7 +85,7 @@
"Slider",
SliderDemos
),
- ComposableDemo("List Headers") {
+ ComposableDemo("List Header") {
Centralize {
ListHeaderDemo()
}
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/CardSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/CardSample.kt
index 4e5af0f..17424be 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/CardSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/CardSample.kt
@@ -19,10 +19,14 @@
import androidx.annotation.Sampled
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
import androidx.wear.compose.material3.AppCard
import androidx.wear.compose.material3.Card
import androidx.wear.compose.material3.CardDefaults
@@ -47,8 +51,8 @@
fun AppCardSample() {
AppCard(
onClick = { /* Do something */ },
- appName = { Text("AppName") },
- title = { Text("AppCard") },
+ appName = { Text("App name") },
+ title = { Text("Card title") },
time = { Text("now") },
) {
Text("Card content")
@@ -60,7 +64,7 @@
fun AppCardWithIconSample() {
AppCard(
onClick = { /* Do something */ },
- appName = { Text("AppName") },
+ appName = { Text("App name") },
appImage = {
Icon(
painter = painterResource(id = android.R.drawable.star_big_off),
@@ -70,7 +74,7 @@
.wrapContentSize(align = Alignment.Center),
)
},
- title = { Text("AppCard with icon") },
+ title = { Text("Card title") },
time = { Text("now") },
) {
Text("Card content")
@@ -82,7 +86,7 @@
fun TitleCardSample() {
TitleCard(
onClick = { /* Do something */ },
- title = { Text("TitleCard") },
+ title = { Text("Title card") },
time = { Text("now") },
) {
Text("Card content")
@@ -94,14 +98,16 @@
fun TitleCardWithImageSample() {
TitleCard(
onClick = { /* Do something */ },
- title = { Text("TitleCard With an ImageBackground") },
+ title = { Text("Card title") },
+ time = { Text("now") },
colors = CardDefaults.imageCardColors(
containerPainter = CardDefaults.imageWithScrimBackgroundPainter(
backgroundImagePainter = painterResource(id = R.drawable.backgroundimage)
),
contentColor = MaterialTheme.colorScheme.onSurface,
titleColor = MaterialTheme.colorScheme.onSurface
- )
+ ),
+ modifier = Modifier.semantics { contentDescription = "Background image" }
) {
Text("Card content")
}
@@ -113,7 +119,7 @@
OutlinedCard(
onClick = { /* Do something */ },
) {
- Text("OutlinedCard")
+ Text("Outlined card")
}
}
@@ -122,8 +128,16 @@
fun OutlinedAppCardSample() {
AppCard(
onClick = { /* Do something */ },
- appName = { Text("AppName") },
- title = { Text("Outlined AppCard") },
+ appName = { Text("App name") },
+ appImage = {
+ Icon(
+ Icons.Filled.Favorite,
+ contentDescription = "favourites",
+ modifier = Modifier.size(CardDefaults.AppImageSize)
+ )
+ },
+ title = { Text("App card") },
+ time = { Text("now") },
colors = CardDefaults.outlinedCardColors(),
border = CardDefaults.outlinedCardBorder(),
) {
@@ -136,7 +150,8 @@
fun OutlinedTitleCardSample() {
TitleCard(
onClick = { /* Do something */ },
- title = { Text("Outlined TitleCard") },
+ title = { Text("Title card") },
+ time = { Text("now") },
colors = CardDefaults.outlinedCardColors(),
border = CardDefaults.outlinedCardBorder(),
) {
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ListHeaderSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ListHeaderSample.kt
index ef263e8..3b32885 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ListHeaderSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ListHeaderSample.kt
@@ -29,7 +29,7 @@
@Composable
fun ListHeaderSample() {
ListHeader {
- Text("List Header")
+ Text("Header")
}
}
@@ -37,7 +37,7 @@
@Composable
fun ListSubheaderSample() {
ListSubheader {
- Text("List Subheader")
+ Text("Subheader")
}
}
@@ -45,7 +45,7 @@
@Composable
fun ListSubheaderWithIconSample() {
ListSubheader(
- label = { Text(text = "List Subheader") },
+ label = { Text(text = "Subheader") },
icon = { Icon(imageVector = Icons.Outlined.Home, "home") }
)
}
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt
index 8dd28b2..2109c6a 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt
@@ -158,7 +158,7 @@
)
) {
val modifier = Modifier.height(LocalConfiguration.current.screenHeightDp.dp / 2)
- repeat(3) { i ->
+ repeat(10) { i ->
ExampleCard(modifier, i)
}
}
@@ -192,7 +192,7 @@
focusRequester = focusRequester
)
) {
- items(5) { i ->
+ items(10) { i ->
val modifier = Modifier.fillParentMaxHeight(0.5f)
ExampleCard(modifier = modifier, i = i)
}
@@ -235,7 +235,7 @@
ListHeader { Text("Cards") }
}
- items(5) { i ->
+ items(10) { i ->
ExampleCard(Modifier.fillParentMaxHeight(0.5f), i)
}
}
@@ -276,7 +276,7 @@
ListHeader { Text("Chips") }
}
- items(5) { i ->
+ items(10) { i ->
ExampleChip(Modifier.fillMaxWidth(), i)
}
}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/ProtoLayoutDiffer.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/ProtoLayoutDiffer.java
index 906634e..65f4fa2 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/ProtoLayoutDiffer.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/ProtoLayoutDiffer.java
@@ -272,13 +272,24 @@
return new LayoutDiff(changedNodes);
}
- /** Check whether 2 nodes represented by the given fingerprints are equivalent. */
+ /** Check whether two nodes represented by the given fingerprints are equivalent. */
@RestrictTo(Scope.LIBRARY_GROUP)
public static boolean areNodesEquivalent(
@NonNull NodeFingerprint nodeA, @NonNull NodeFingerprint nodeB) {
return getChangeType(nodeA, nodeB) == NodeChangeType.NO_CHANGE;
}
+ /** Check whether two {@link TreeFingerprint} objects are equivalent. */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static boolean areSameFingerprints(
+ @NonNull TreeFingerprint first, @NonNull TreeFingerprint second) {
+ NodeFingerprint prev = first.getRoot();
+ NodeFingerprint current = second.getRoot();
+ return current.getSelfTypeValue() == prev.getSelfTypeValue()
+ && current.getSelfPropsValue() == prev.getSelfPropsValue()
+ && current.getChildNodesValue() == prev.getChildNodesValue();
+ }
+
private static void addChangedNodes(
@NonNull NodeFingerprint prevNodeFingerprint,
@NonNull TreeNode node,
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
index 5f53a02..1762d0b 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 The Android Open Source Project
+ * Copyright 2023 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.
@@ -39,12 +39,16 @@
import androidx.wear.protolayout.expression.pipeline.FixedQuotaManagerImpl;
import androidx.wear.protolayout.expression.pipeline.PlatformDataProvider;
import androidx.wear.protolayout.expression.pipeline.StateStore;
+import androidx.wear.protolayout.proto.LayoutElementProto.ArcLayoutElement;
+import androidx.wear.protolayout.proto.LayoutElementProto.ArcLayoutElement.InnerCase;
import androidx.wear.protolayout.proto.LayoutElementProto.Layout;
+import androidx.wear.protolayout.proto.LayoutElementProto.LayoutElement;
import androidx.wear.protolayout.proto.ResourceProto;
import androidx.wear.protolayout.proto.StateProto.State;
import androidx.wear.protolayout.renderer.ProtoLayoutExtensionViewProvider;
import androidx.wear.protolayout.renderer.ProtoLayoutTheme;
import androidx.wear.protolayout.renderer.ProtoLayoutVisibilityState;
+import androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer;
import androidx.wear.protolayout.renderer.dynamicdata.ProtoLayoutDynamicDataPipeline;
import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater;
import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.InflateResult;
@@ -55,12 +59,14 @@
import androidx.wear.protolayout.renderer.inflater.ResourceResolvers;
import androidx.wear.protolayout.renderer.inflater.StandardResourceResolvers;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.SettableFuture;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CancellationException;
@@ -89,7 +95,7 @@
}
private static final int DEFAULT_MAX_CONCURRENT_RUNNING_ANIMATIONS = 4;
-
+ static final int MAX_LAYOUT_ELEMENT_DEPTH = 30;
@NonNull private static final String TAG = "ProtoLayoutViewInstance";
@NonNull private final Context mUiContext;
@@ -134,6 +140,12 @@
@Nullable private Layout mPrevLayout = null;
/**
+ * This field is used to avoid unnecessarily checking layout depth if the layout was previously
+ * failing the check.
+ */
+ private boolean mPrevLayoutAlreadyFailingDepthCheck = false;
+
+ /**
* This is used as the Future for the currently running inflation session. The first time
* "attach" is called, it should start the renderer. Subsequent attach calls should only ever
* re-attach "inflateParent".
@@ -703,6 +715,21 @@
return new FailedRenderResult();
}
+ boolean sameFingerprint =
+ prevRenderedMetadata != null
+ && ProtoLayoutDiffer.areSameFingerprints(
+ prevRenderedMetadata.getTreeFingerprint(), layout.getFingerprint());
+
+ if (sameFingerprint) {
+ if (mPrevLayoutAlreadyFailingDepthCheck) {
+ throwExceptionForLayoutDepthCheckFailure();
+ }
+ } else {
+ checkLayoutDepth(layout.getRoot(), MAX_LAYOUT_ELEMENT_DEPTH);
+ }
+
+ mPrevLayoutAlreadyFailingDepthCheck = false;
+
ProtoLayoutInflater.Config.Builder inflaterConfigBuilder =
new ProtoLayoutInflater.Config.Builder(mUiContext, layout, resolvers)
.setLoadActionExecutor(mUiExecutorService)
@@ -1059,6 +1086,52 @@
}
}
+ /** Returns true if the layout element depth doesn't exceed the given {@code allowedDepth}. */
+ private void checkLayoutDepth(LayoutElement layoutElement, int allowedDepth) {
+ if (allowedDepth <= 0) {
+ throwExceptionForLayoutDepthCheckFailure();
+ }
+ List<LayoutElement> children = ImmutableList.of();
+ switch (layoutElement.getInnerCase()) {
+ case COLUMN:
+ children = layoutElement.getColumn().getContentsList();
+ break;
+ case ROW:
+ children = layoutElement.getRow().getContentsList();
+ break;
+ case BOX:
+ children = layoutElement.getBox().getContentsList();
+ break;
+ case ARC:
+ List<ArcLayoutElement> arcElements = layoutElement.getArc().getContentsList();
+ if (!arcElements.isEmpty() && allowedDepth == 1) {
+ throwExceptionForLayoutDepthCheckFailure();
+ }
+ for (ArcLayoutElement element : arcElements) {
+ if (element.getInnerCase() == InnerCase.ADAPTER) {
+ checkLayoutDepth(element.getAdapter().getContent(), allowedDepth - 1);
+ }
+ }
+ break;
+ case SPANNABLE:
+ if (layoutElement.getSpannable().getSpansCount() > 0 && allowedDepth == 1) {
+ throwExceptionForLayoutDepthCheckFailure();
+ }
+ break;
+ default:
+ // Other LayoutElements have depth of one.
+ }
+ for (LayoutElement child : children) {
+ checkLayoutDepth(child, allowedDepth - 1);
+ }
+ }
+
+ private void throwExceptionForLayoutDepthCheckFailure() {
+ mPrevLayoutAlreadyFailingDepthCheck = true;
+ throw new IllegalStateException(
+ "Layout depth exceeds maximum allowed depth: " + MAX_LAYOUT_ELEMENT_DEPTH);
+ }
+
@Override
public void close() throws Exception {
detachInternal();
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/RenderedMetadata.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/RenderedMetadata.java
index 11cbf345..5b6bdc5 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/RenderedMetadata.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/RenderedMetadata.java
@@ -43,7 +43,7 @@
}
@NonNull
- TreeFingerprint getTreeFingerprint() {
+ public TreeFingerprint getTreeFingerprint() {
return mTreeFingerprint;
}
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/common/ProtoLayoutDifferTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/common/ProtoLayoutDifferTest.java
index b86f294..deb6dbc 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/common/ProtoLayoutDifferTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/common/ProtoLayoutDifferTest.java
@@ -303,6 +303,30 @@
}
@Test
+ public void areSameFingerprints() {
+ assertThat(
+ ProtoLayoutDiffer.areSameFingerprints(
+ referenceLayout().getFingerprint(),
+ referenceLayout().getFingerprint()))
+ .isTrue();
+ assertThat(
+ ProtoLayoutDiffer.areSameFingerprints(
+ referenceLayout().getFingerprint(),
+ layoutWithOneUpdatedNode().getFingerprint()))
+ .isFalse();
+ assertThat(
+ ProtoLayoutDiffer.areSameFingerprints(
+ referenceLayout().getFingerprint(),
+ layoutWithDifferentNumberOfChildren().getFingerprint()))
+ .isFalse();
+ assertThat(
+ ProtoLayoutDiffer.areSameFingerprints(
+ referenceLayout().getFingerprint(),
+ layoutWithUpdateToNodeSelfFingerprint().getFingerprint()))
+ .isFalse();
+ }
+
+ @Test
public void isChildOf_forAnActualChild_returnsTrue() {
String childPosId = "pT1.2.3";
String parentPosId = "pT1.2";
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/helper/TestDsl.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/helper/TestDsl.java
index b158c38..9f7018ae 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/helper/TestDsl.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/helper/TestDsl.java
@@ -37,6 +37,7 @@
import androidx.wear.protolayout.proto.FingerprintProto.TreeFingerprint;
import androidx.wear.protolayout.proto.LayoutElementProto;
import androidx.wear.protolayout.proto.LayoutElementProto.Arc;
+import androidx.wear.protolayout.proto.LayoutElementProto.ArcAdapter;
import androidx.wear.protolayout.proto.LayoutElementProto.ArcLayoutElement;
import androidx.wear.protolayout.proto.LayoutElementProto.ArcText;
import androidx.wear.protolayout.proto.LayoutElementProto.Box;
@@ -433,6 +434,10 @@
return arcInternal(/* propsConsumer= */ null, nodes);
}
+ public static LayoutNode arcAdapter(LayoutNode layoutNode) {
+ return arcAdapterInternal(layoutNode);
+ }
+
private static LayoutNode arcInternal(
@Nullable Consumer<ArcProps> propsConsumer, LayoutNode... nodes) {
LayoutNode element = new LayoutNode();
@@ -449,6 +454,15 @@
return element;
}
+ private static LayoutNode arcAdapterInternal(LayoutNode node) {
+ LayoutNode element = new LayoutNode();
+ ArcAdapter.Builder builder = ArcAdapter.newBuilder().setContent(node.mLayoutElement);
+ int selfPropsFingerprint = 0;
+ element.mArcLayoutElement = ArcLayoutElement.newBuilder().setAdapter(builder.build());
+ element.mFingerprint = fingerprint("arcAdapter", selfPropsFingerprint, node);
+ return element;
+ }
+
public static LayoutNode arcText(String text) {
LayoutNode element = new LayoutNode();
element.mArcLayoutElement =
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java
index ca3f94e..3ad5656 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java
@@ -17,10 +17,16 @@
package androidx.wear.protolayout.renderer.impl;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+import static androidx.wear.protolayout.renderer.helper.TestDsl.arc;
+import static androidx.wear.protolayout.renderer.helper.TestDsl.arcAdapter;
+import static androidx.wear.protolayout.renderer.helper.TestDsl.box;
import static androidx.wear.protolayout.renderer.helper.TestDsl.column;
import static androidx.wear.protolayout.renderer.helper.TestDsl.dynamicFixedText;
import static androidx.wear.protolayout.renderer.helper.TestDsl.layout;
+import static androidx.wear.protolayout.renderer.helper.TestDsl.spanText;
+import static androidx.wear.protolayout.renderer.helper.TestDsl.spannable;
import static androidx.wear.protolayout.renderer.helper.TestDsl.text;
+import static androidx.wear.protolayout.renderer.impl.ProtoLayoutViewInstance.MAX_LAYOUT_ELEMENT_DEPTH;
import static com.google.common.truth.Truth.assertThat;
@@ -39,6 +45,7 @@
import androidx.wear.protolayout.expression.pipeline.StateStore;
import androidx.wear.protolayout.proto.LayoutElementProto.Layout;
import androidx.wear.protolayout.proto.ResourceProto.Resources;
+import androidx.wear.protolayout.renderer.helper.TestDsl.LayoutNode;
import androidx.wear.protolayout.renderer.impl.ProtoLayoutViewInstance.Config;
import com.google.common.collect.ImmutableList;
@@ -55,6 +62,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.AbstractExecutorService;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@RunWith(AndroidJUnit4.class)
@@ -390,6 +398,107 @@
assertThat(mRootContainer.getChildCount()).isEqualTo(0);
}
+ @Test
+ public void layoutDepthExceedsMaximumDepth_renderingFail() throws Exception {
+ setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
+ assertThrows(
+ ExecutionException.class,
+ () -> renderAndAttachLayout(layout(recursiveBox(MAX_LAYOUT_ELEMENT_DEPTH + 1))));
+ }
+
+ @Test
+ public void layoutDepthIsEqualToMaximumDepth_renderingPass() throws Exception {
+ setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
+
+ LayoutNode[] children = new LayoutNode[MAX_LAYOUT_ELEMENT_DEPTH];
+ for (int i = 0; i < children.length; i++) {
+ children[i] = recursiveBox(MAX_LAYOUT_ELEMENT_DEPTH - 1);
+ }
+ ListenableFuture<Void> result =
+ mInstanceUnderTest.renderAndAttach(
+ // MAX_LAYOUT_ELEMENT_DEPTH branches of depth MAX_LAYOUT_ELEMENT_DEPTH - 1.
+ // Total depth is MAX_LAYOUT_ELEMENT_DEPTH (if we count the head).
+ layout(box(children)), RESOURCES, mRootContainer);
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertNoException(result);
+ assertThat(mRootContainer.getChildCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void layoutDepthForLayoutWithSpanner() throws Exception {
+ setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
+
+ assertThrows(
+ ExecutionException.class,
+ () ->
+ renderAndAttachLayout(
+ // Total number of views is = MAX_LAYOUT_ELEMENT_DEPTH + 1 (span
+ // text)
+ layout(
+ recursiveBox(
+ MAX_LAYOUT_ELEMENT_DEPTH,
+ spannable(spanText("Hello"))))));
+
+ ListenableFuture<Void> result =
+ mInstanceUnderTest.renderAndAttach(
+ // Total number of views is = (MAX_LAYOUT_ELEMENT_DEPTH -1) + 1 (span text)
+ layout(
+ recursiveBox(
+ MAX_LAYOUT_ELEMENT_DEPTH - 1,
+ spannable(spanText("Hello")))),
+ RESOURCES,
+ mRootContainer);
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertNoException(result);
+ assertThat(mRootContainer.getChildCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void layoutDepthForLayoutWithArcAdapter() throws Exception {
+ setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
+ assertThrows(
+ ExecutionException.class,
+ () ->
+ renderAndAttachLayout(
+ // Total number of views is = 1 (Arc) + (MAX_LAYOUT_ELEMENT_DEPTH)
+ layout(arc(arcAdapter(recursiveBox(MAX_LAYOUT_ELEMENT_DEPTH))))));
+
+ ListenableFuture<Void> result =
+ mInstanceUnderTest.renderAndAttach(
+ // Total number of views is = 1 (Arc) + (MAX_LAYOUT_ELEMENT_DEPTH - 1)
+ // = MAX_LAYOUT_ELEMENT_DEPTH
+ layout(arc(arcAdapter(recursiveBox(MAX_LAYOUT_ELEMENT_DEPTH - 1)))),
+ RESOURCES,
+ mRootContainer);
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertNoException(result);
+ assertThat(mRootContainer.getChildCount()).isEqualTo(1);
+ }
+
+ private void renderAndAttachLayout(Layout layout) throws Exception {
+ ListenableFuture<Void> result =
+ mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ shadowOf(Looper.getMainLooper()).idle();
+ assertNoException(result);
+ }
+
+ private static LayoutNode recursiveBox(int depth) {
+ if (depth == 1) {
+ return box();
+ }
+ return box(recursiveBox(depth - 1));
+ }
+
+ private static LayoutNode recursiveBox(int depth, LayoutNode leaf) {
+ if (depth == 1) {
+ return leaf;
+ }
+ return box(recursiveBox(depth - 1, leaf));
+ }
+
private void setupInstance(boolean adaptiveUpdateRatesEnabled) {
FakeExecutorService uiThreadExecutor =
new FakeExecutorService(new Handler(Looper.getMainLooper()));
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java
index 3219024..6a30db4 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java
@@ -2834,10 +2834,16 @@
* Sets the style of font to use (size, bold etc). If not specified, defaults to the
* platform's default body font.
*
+ * DynamicColor is not supported for SpanText.
+ *
* @since 1.0
*/
@NonNull
public Builder setFontStyle(@NonNull FontStyle fontStyle) {
+ ColorProp colorProp = fontStyle.getColor();
+ if (colorProp != null && colorProp.getDynamicValue() != null) {
+ throw new IllegalArgumentException("SpanText does not support DynamicColor.");
+ }
mImpl.setFontStyle(fontStyle.toProto());
mFingerprint.recordPropertyUpdate(
2, checkNotNull(fontStyle.getFingerprint()).aggregateValueAsInt());
diff --git a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt
index dd4398b..1790ab2 100644
--- a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt
+++ b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt
@@ -183,11 +183,11 @@
*
* Manifest requirements:
* - The manifest declaration of this service must include an intent filter for
- * android.support.wearable.complications.ACTION_COMPLICATION_UPDATE_REQUEST.
+ * `android.support.wearable.complications.ACTION_COMPLICATION_UPDATE_REQUEST`.
* - A ComplicationDataSourceService must include a `meta-data` tag with
- * android.support.wearable.complications.SUPPORTED_TYPES in its manifest entry.
+ * `android.support.wearable.complications.SUPPORTED_TYPES` in its manifest entry.
*
- * The value of android.support.wearable.complications.SUPPORTED_TYPES should be a comma separated
+ * The value of `android.support.wearable.complications.SUPPORTED_TYPES` should be a comma separated
* list of types supported by the data source, from this table:
*
* | Androidx class | Tag name |
@@ -205,47 +205,49 @@
* multiple types in a single complication slot, the watch face will determine which types it
* prefers.
*
- * For example, a complication data source that supports the RANGED_VALUE, SHORT_TEXT, and ICON
- * types would include the following in its manifest entry:
+ * For example, a complication data source that supports the `RANGED_VALUE`, `SHORT_TEXT`, and
+ * `ICON` types would include the following in its manifest entry:
* ```
- * <meta-data android:name="android.support.wearable.complications.SUPPORTED_TYPES"
- * android:value="RANGED_VALUE,SHORT_TEXT,ICON"/>
+ * <meta-data
+ * android:name="android.support.wearable.complications.SUPPORTED_TYPES"
+ * android:value="RANGED_VALUE,SHORT_TEXT,ICON" />
* ```
*
- * From android T onwards, it is recommended for Complication DataSourceServices to be direct boot
+ * From android T onwards, it is recommended for [ComplicationDataSourceService]s to be direct boot
* aware because the system is able to fetch complications before the lock screen has been removed.
- * To do this add android:directBootAware="true" to your service tag.
+ * To do this add `android:directBootAware="true"` to your service tag.
* - A provider can choose to trust one or more watch faces by including the following in its
* manifest entry:
* ```
- * <meta-data android:name="android.support.wearable.complications.SAFE_WATCH_FACES
- * android:value="com.pkg1/com.trusted.wf1,com.pkg2/com.trusted.wf2"/>
+ * <meta-data
+ * android:name="android.support.wearable.complications.SAFE_WATCH_FACES"
+ * android:value="com.pkg1/com.trusted.wf1,com.pkg2/com.trusted.wf2" />
* ```
*
* The listed watch faces will not need
- * 'com.google.android.wearable.permission.RECEIVE_COMPLICATION_DATA' in order to receive
+ * `com.google.android.wearable.permission.RECEIVE_COMPLICATION_DATA` in order to receive
* complications from this provider. Also the provider may choose to serve different types to safe
* watch faces by including the following in its manifest:
* ```
- * <meta-data android:name=
- * "androidx.wear.watchface.complications.datasource.SAFE_WATCH_FACE_SUPPORTED_TYPES"
- * android:value="ICON"/>
+ * <meta-data
+ * android:name="androidx.wear.watchface.complications.datasource.SAFE_WATCH_FACE_SUPPORTED_TYPES"
+ * android:value="ICON" />
* ```
*
* In addition the provider can learn if a request is for a safe watchface by examining
- * [ComplicationRequest.isForSafeWatchFace]. Note SAFE_WATCH_FACE_SUPPORTED_TYPES and
- * isForSafeWatchFace are gated behind the privileged permission
+ * [ComplicationRequest.isForSafeWatchFace]. Note `SAFE_WATCH_FACE_SUPPORTED_TYPES` and
+ * `isForSafeWatchFace` are gated behind the privileged permission
* `com.google.wear.permission.GET_IS_FOR_SAFE_WATCH_FACE`.
* - A ComplicationDataSourceService should include a `meta-data` tag with
- * `android.support.wearable.complications.UPDATE_PERIOD_SECONDS` its manifest entry. The value of
- * this tag is the number of seconds the complication data source would like to elapse between
+ * `android.support.wearable.complications.UPDATE_PERIOD_SECONDS` in its manifest entry. The value
+ * of this tag is the number of seconds the complication data source would like to elapse between
* update requests.
*
* **Note that update requests are not guaranteed to be sent with this frequency.** For
- * complications with frequent updates they can also register a separate
- * [METADATA_KEY_IMMEDIATE_UPDATE_PERIOD_MILLISECONDS] meta data tag which supports sampling at up
- * to 1Hz when the watch face is visible and non-ambient, however this also requires the
- * DataSourceService to have the privileged permission
+ * complications with frequent updates they can also register a separate `meta-data` tag with
+ * `androidx.wear.watchface.complications.data.source.IMMEDIATE_UPDATE_PERIOD_MILLISECONDS` in their
+ * manifest which supports sampling at up to 1Hz when the watch face is visible and non-ambient,
+ * however this also requires the application to have the privileged permission
* `com.google.android.wearable.permission.USE_IMMEDIATE_COMPLICATION_UPDATE`.
*
* If a complication data source never needs to receive update requests beyond the one sent when a
@@ -254,37 +256,39 @@
* For example, a complication data source that would like to update at most every hour should
* include the following in its manifest entry:
* ```
- * <meta-data android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS"
- * android:value="3600"/>
+ * <meta-data
+ * android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS"
+ * android:value="3600" />
* ```
- * - A ComplicationDataSourceService can include a `meta-data` tag with
+ * - A [ComplicationDataSourceService] can include a `meta-data` tag with
* android.support.wearable.complications.PROVIDER_CONFIG_ACTION its manifest entry to cause a
* configuration activity to be shown when the complication data source is selected.
*
* The configuration activity must reside in the same package as the complication data source, and
* must register an intent filter for the action specified here, including
- * android.support.wearable.complications.category.PROVIDER_CONFIG as well as
+ * `android.support.wearable.complications.category.PROVIDER_CONFIG` as well as
* [Intent.CATEGORY_DEFAULT] as categories.
*
* The complication id being configured will be included in the intent that starts the config
- * activity using the extra key android.support.wearable.complications.EXTRA_CONFIG_COMPLICATION_ID.
+ * activity using the extra key
+ * `android.support.wearable.complications.EXTRA_CONFIG_COMPLICATION_ID`.
*
* The complication type that will be requested from the complication data source will also be
- * included, using the extra key android.support.wearable.complications
- * .EXTRA_CONFIG_COMPLICATION_TYPE.
+ * included, using the extra key
+ * `android.support.wearable.complications.EXTRA_CONFIG_COMPLICATION_TYPE`.
*
* The complication data source's [ComponentName] will also be included in the intent that starts
* the config activity, using the extra key
- * android.support.wearable.complications.EXTRA_CONFIG_PROVIDER_COMPONENT.
+ * `android.support.wearable.complications.EXTRA_CONFIG_PROVIDER_COMPONENT`.
*
* The config activity must call [Activity.setResult] with either [Activity.RESULT_OK] or
* [Activity.RESULT_CANCELED] before it is finished, to tell the system whether or not the
* complication data source should be set on the given complication.
*
- * It is possible to provide additional 'meta-data' tag
- * androidx.watchface.complications.datasource.DEFAULT_CONFIG_SUPPORTED in the service set to "true"
- * to let the system know that the data source is able to provide complication data before it is
- * configured.
+ * It is possible to provide additional `meta-data` tag
+ * `androidx.watchface.complications.datasource.DEFAULT_CONFIG_SUPPORTED` in the service set to
+ * `"true"` to let the system know that the data source is able to provide complication data before
+ * it is configured.
* - The manifest entry for the service should also include an android:icon attribute. The icon
* provided there should be a single-color white icon that represents the complication data
* source. This icon will be shown in the complication data source chooser interface, and may also
@@ -298,8 +302,8 @@
* limit of 100 data sources per APK. Above that the companion watchface editor won't support this
* complication data source app.
*
- * There's no need to call setDataSource for any the ComplicationData Builders because the system
- * will append this value on your behalf.
+ * There's no need to call `setDataSource` for any the [ComplicationData] Builders because the
+ * system will append this value on your behalf.
*/
public abstract class ComplicationDataSourceService : Service() {
private var wrapper: IComplicationProviderWrapper? = null
@@ -768,15 +772,18 @@
* the [ComplicationData], but omitting the "TYPE_" prefix, e.g. `SHORT_TEXT`, `LONG_TEXT`,
* `RANGED_VALUE`.
*
- * The order in which types are listed has no significance. In the case where a watch face
- * supports multiple types in a single complication slot, the watch face will determine
- * which types it prefers.
+ * The order of types in `METADATA_KEY_SUPPORTED_TYPES` has no significance. During
+ * complication data source selection, each item in the complication slot's supported types
+ * is checked against entries in the data source's `METADATA_KEY_SUPPORTED_TYPES` and the
+ * first matching entry from the slot's support types (if any) is chosen. If there are no
+ * matches then this data source is not eligible to be selected in that slot.
*
* For example, a complication data source that supports the RANGED_VALUE, SHORT_TEXT, and
* ICON type would include the following in its manifest entry:
* ```
- * <meta-data android:name="android.support.wearable.complications.SUPPORTED_TYPES"
- * android:value="RANGED_VALUE,SHORT_TEXT,ICON"/>
+ * <meta-data
+ * android:name="android.support.wearable.complications.SUPPORTED_TYPES"
+ * android:value="RANGED_VALUE,SHORT_TEXT,ICON" />
* ```
*/
// TODO(b/192233205): Migrate value to androidx.
@@ -812,8 +819,9 @@
* For example, a complication data source that would like to update every ten minutes
* should include the following in its manifest entry:
* ```
- * <meta-data android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS"
- * android:value="600"/>
+ * <meta-data
+ * android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS"
+ * android:value="600" />
* ```
*/
// TODO(b/192233205): Migrate value to androidx.
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
index 5f14a6f..c36b80e 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
@@ -345,8 +345,11 @@
* @param accessibilityTraversalIndex Used to sort Complications when generating accessibility
* content description labels.
* @param bounds The complication slot's [ComplicationSlotBounds].
- * @param supportedTypes The list of [ComplicationType]s accepted by this complication slot. Used
- * during complication data source selection, this list should be non-empty.
+ * @param supportedTypes The list of [ComplicationType]s accepted by this complication slot, must be
+ * non-empty. During complication data source selection, each item in this list is compared in
+ * turn with entries from a data source's data source's supported types. The first matching entry
+ * from `supportedTypes` is chosen. If there are no matches then that data source is not eligible
+ * to be selected in this slot.
* @param defaultPolicy The [DefaultComplicationDataSourcePolicy] which controls the initial
* complication data source when the watch face is first installed.
* @param defaultDataSourceType The default [ComplicationType] for the default complication data
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index aff5089..c32fa26 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -1320,7 +1320,12 @@
internal var immutableSystemStateDone = false
internal var immutableChinHeightDone = false
internal var systemHasSentWatchUiState = false
- internal var resourceOnlyWatchFacePackageName: String? = headlessComponentName?.packageName
+ internal var resourceOnlyWatchFacePackageName: String? =
+ if (this@WatchFaceService is WatchFaceRuntimeService) {
+ headlessComponentName?.packageName
+ } else {
+ null
+ }
private var asyncWatchFaceConstructionPending = false
@@ -2103,7 +2108,10 @@
setWatchUiState(params.watchUiState, fromSysUi = false)
initialUserStyle = params.userStyle
- resourceOnlyWatchFacePackageName = params.auxiliaryComponentPackageName
+ // For a resource only watch face, the auxiliaryComponentPackageName will be null.
+ if (this@WatchFaceService is WatchFaceRuntimeService) {
+ resourceOnlyWatchFacePackageName = params.auxiliaryComponentPackageName
+ }
mutableWatchState.watchFaceInstanceId.value = sanitizeWatchFaceId(params.instanceId)
val watchState = mutableWatchState.asWatchState()
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index d55140d..3a6c4ec 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -88,6 +88,7 @@
import androidx.wear.watchface.data.WatchUiState
import androidx.wear.watchface.style.CurrentUserStyleRepository
import androidx.wear.watchface.style.UserStyle
+import androidx.wear.watchface.style.UserStyleFlavors
import androidx.wear.watchface.style.UserStyleSchema
import androidx.wear.watchface.style.UserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting
@@ -6549,11 +6550,12 @@
@Test
public fun createHeadlessSessionDelegate_onDestroy() {
val context = ApplicationProvider.getApplicationContext<Context>()
- val componentName = ComponentName(context, TestNopCanvasWatchFaceService::class.java)
+ val componentName =
+ ComponentName(context, TestNopCanvasWatchFaceServiceWithHandler::class.java)
lateinit var delegate: WatchFace.EditorDelegate
// Allows us to programmatically control tasks.
- TestNopCanvasWatchFaceService.handler = this.handler
+ TestNopCanvasWatchFaceServiceWithHandler.handler = this.handler
CoroutineScope(handler.asCoroutineDispatcher().immediate).launch {
delegate =
@@ -6609,7 +6611,7 @@
)
// Allows us to programmatically control tasks.
- TestNopCanvasWatchFaceService.handler = this.handler
+ TestNopCanvasWatchFaceServiceWithHandler.handler = this.handler
CoroutineScope(handler.asCoroutineDispatcher().immediate).launch {
delegate =
@@ -6709,6 +6711,115 @@
.isEqualTo(INTERACTIVE_INSTANCE_ID)
}
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.R])
+ @RequiresApi(Build.VERSION_CODES.R)
+ public fun interactive_wf_has_null_resourceOnlyWatchFacePackageName() {
+ initWallpaperInteractiveWatchFaceInstance(
+ WatchFaceType.ANALOG,
+ emptyList(),
+ UserStyleSchema(emptyList()),
+ WallpaperInteractiveWatchFaceInstanceParams(
+ INTERACTIVE_INSTANCE_ID,
+ DeviceConfig(false, false, 0, 0),
+ WatchUiState(false, 0),
+ UserStyle(emptyMap()).toWireFormat(),
+ null,
+ null,
+ null
+ )
+ )
+
+ assertThat(engineWrapper.resourceOnlyWatchFacePackageName).isNull()
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.R])
+ @RequiresApi(Build.VERSION_CODES.O_MR1)
+ public fun headless_wf_has_null_resourceOnlyWatchFacePackageName() {
+ val service = TestNopCanvasWatchFaceService(context)
+ val componentName = ComponentName("test.watchface.app", "test.watchface.class")
+
+ engineWrapper =
+ service.createHeadlessEngine(componentName) as WatchFaceService.EngineWrapper
+ engineWrapper.createHeadlessInstance(
+ HeadlessWatchFaceInstanceParams(
+ componentName,
+ DeviceConfig(false, false, 100, 200),
+ 100,
+ 100,
+ null
+ )
+ )
+
+ assertThat(engineWrapper.resourceOnlyWatchFacePackageName).isNull()
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.R])
+ @RequiresApi(Build.VERSION_CODES.R)
+ public fun interactive_wf_runtime_has_non_null_resourceOnlyWatchFacePackageName() {
+ val service = TestNopWatchFaceRuntimeService(context)
+ InteractiveInstanceManager
+ .getExistingInstanceOrSetPendingWallpaperInteractiveWatchFaceInstance(
+ InteractiveInstanceManager.PendingWallpaperInteractiveWatchFaceInstance(
+ WallpaperInteractiveWatchFaceInstanceParams(
+ SYSTEM_SUPPORTS_CONSISTENT_IDS_PREFIX + "Interactive",
+ DeviceConfig(false, false, 0, 0),
+ WatchUiState(false, 0),
+ UserStyle(emptyMap()).toWireFormat(),
+ emptyList(),
+ "com.resource.only.package",
+ null
+ ),
+ object : IPendingInteractiveWatchFace.Stub() {
+ override fun getApiVersion() = IPendingInteractiveWatchFace.API_VERSION
+
+ override fun onInteractiveWatchFaceCreated(
+ iInteractiveWatchFace: IInteractiveWatchFace
+ ) {
+ interactiveWatchFaceInstance = iInteractiveWatchFace
+ }
+
+ override fun onInteractiveWatchFaceCrashed(exception: CrashInfoParcel?) {
+ fail("WatchFace crashed: $exception")
+ }
+ }
+ )
+ )
+
+ engineWrapper = service.onCreateEngine() as WatchFaceService.EngineWrapper
+ engineWrapper.onCreate(surfaceHolder)
+ engineWrapper.onSurfaceChanged(surfaceHolder, 0, 100, 100)
+
+ assertThat(engineWrapper.resourceOnlyWatchFacePackageName)
+ .isEqualTo("com.resource.only.package")
+ engineWrapper.onDestroy()
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.R])
+ @RequiresApi(Build.VERSION_CODES.O_MR1)
+ public fun headless_wf_runtime_has_non_null_resourceOnlyWatchFacePackageName() {
+ val service = TestNopWatchFaceRuntimeService(context)
+ val componentName = ComponentName("com.resource.only.package", "null")
+
+ engineWrapper =
+ service.createHeadlessEngine(componentName) as WatchFaceService.EngineWrapper
+ engineWrapper.createHeadlessInstance(
+ HeadlessWatchFaceInstanceParams(
+ componentName,
+ DeviceConfig(false, false, 100, 200),
+ 100,
+ 100,
+ null
+ )
+ )
+
+ assertThat(engineWrapper.resourceOnlyWatchFacePackageName)
+ .isEqualTo("com.resource.only.package")
+ }
+
private fun getLeftShortTextComplicationDataText(): CharSequence {
val complication =
complicationSlotsManager[LEFT_COMPLICATION_ID]!!.complicationData.value
@@ -6743,7 +6854,115 @@
private suspend fun <T> Deferred<T>.awaitWithTimeout(): T = withTimeout(1000) { await() }
}
-class TestNopCanvasWatchFaceService : WatchFaceService() {
+class TestNopWatchFaceRuntimeService(testContext: Context) : WatchFaceRuntimeService() {
+ var lastResourceOnlyWatchFacePackageName: String? = null
+
+ init {
+ attachBaseContext(testContext)
+ }
+
+ override fun createUserStyleSchema(resourceOnlyWatchFacePackageName: String) =
+ UserStyleSchema(emptyList())
+
+ override fun createComplicationSlotsManager(
+ currentUserStyleRepository: CurrentUserStyleRepository,
+ resourceOnlyWatchFacePackageName: String
+ ) = ComplicationSlotsManager(emptyList(), currentUserStyleRepository)
+
+ override fun createUserStyleFlavors(
+ currentUserStyleRepository: CurrentUserStyleRepository,
+ complicationSlotsManager: ComplicationSlotsManager,
+ resourceOnlyWatchFacePackageName: String
+ ) = UserStyleFlavors()
+
+ override suspend fun createWatchFace(
+ surfaceHolder: SurfaceHolder,
+ watchState: WatchState,
+ complicationSlotsManager: ComplicationSlotsManager,
+ currentUserStyleRepository: CurrentUserStyleRepository,
+ resourceOnlyWatchFacePackageName: String
+ ): WatchFace {
+ lastResourceOnlyWatchFacePackageName = resourceOnlyWatchFacePackageName
+ System.out.println("<<< createWatchFace " + resourceOnlyWatchFacePackageName)
+ return WatchFace(
+ WatchFaceType.DIGITAL,
+ @Suppress("deprecation")
+ object :
+ Renderer.CanvasRenderer(
+ surfaceHolder,
+ currentUserStyleRepository,
+ watchState,
+ CanvasType.HARDWARE,
+ 16
+ ) {
+ override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {
+ // Intentionally empty.
+ }
+
+ override fun renderHighlightLayer(
+ canvas: Canvas,
+ bounds: Rect,
+ zonedDateTime: ZonedDateTime
+ ) {
+ // Intentionally empty.
+ }
+ }
+ )
+ }
+
+ override fun getSystemTimeProvider() =
+ object : SystemTimeProvider {
+ override fun getSystemTimeMillis() = 123456789L
+
+ override fun getSystemTimeZoneId() = ZoneId.of("UTC")
+ }
+}
+
+class TestNopCanvasWatchFaceService(testContext: Context) : WatchFaceService() {
+ init {
+ attachBaseContext(testContext)
+ }
+
+ override suspend fun createWatchFace(
+ surfaceHolder: SurfaceHolder,
+ watchState: WatchState,
+ complicationSlotsManager: ComplicationSlotsManager,
+ currentUserStyleRepository: CurrentUserStyleRepository
+ ) =
+ WatchFace(
+ WatchFaceType.DIGITAL,
+ @Suppress("deprecation")
+ object :
+ Renderer.CanvasRenderer(
+ surfaceHolder,
+ currentUserStyleRepository,
+ watchState,
+ CanvasType.HARDWARE,
+ 16
+ ) {
+ override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {
+ // Intentionally empty.
+ }
+
+ override fun renderHighlightLayer(
+ canvas: Canvas,
+ bounds: Rect,
+ zonedDateTime: ZonedDateTime
+ ) {
+ // Intentionally empty.
+ }
+ }
+ )
+
+ override fun getSystemTimeProvider() =
+ object : SystemTimeProvider {
+ override fun getSystemTimeMillis() = 123456789L
+
+ override fun getSystemTimeZoneId() = ZoneId.of("UTC")
+ }
+}
+
+class TestNopCanvasWatchFaceServiceWithHandler : WatchFaceService() {
companion object {
lateinit var handler: Handler
}
@@ -6796,6 +7015,6 @@
@RequiresApi(27)
class TestWatchFaceControlService : WatchFaceControlService() {
override fun createWatchFaceService(watchFaceName: ComponentName): WatchFaceService? {
- return TestNopCanvasWatchFaceService()
+ return TestNopCanvasWatchFaceServiceWithHandler()
}
}