Merge "Bump to Kotlin 1.6.21" into androidx-main
diff --git a/buildSrc/remoteBuildCache.gradle b/buildSrc/remoteBuildCache.gradle
index d3965ce..6a5e7ea 100644
--- a/buildSrc/remoteBuildCache.gradle
+++ b/buildSrc/remoteBuildCache.gradle
@@ -1,31 +1,63 @@
-def useRemoteBuildCache = System.getenv("USE_ANDROIDX_REMOTE_BUILD_CACHE") == "true"
+import androidx.build.gradle.gcpbuildcache.GcpBuildCache
+import androidx.build.gradle.gcpbuildcache.GcpBuildCacheServiceFactory
-if (useRemoteBuildCache) {
- gradle.settingsEvaluated { settings ->
- settings.buildCache {
- remote(HttpBuildCache) {
- def osName = System.getProperty("os.name").toLowerCase()
- switch (osName) {
- case { it.contains("mac os x") }:
- case { it.contains("darwin") }:
- case { it.contains("osx") }:
- url = "http://gradle-remote-cache.uplink2.goog:999/cache/"
- break
- default:
- url = "http://gradle-remote-cache.uplink.goog:999/cache/"
- break
+buildscript {
+ ext.supportRootFolder = buildscript.sourceFile.getParentFile().getParentFile()
+ apply(from: "repos.gradle")
+ repos.addMavenRepositories(repositories)
+
+ dependencies {
+ classpath("androidx.build.gradle.gcpbuildcache:gcpbuildcache:1.0.0-alpha02")
+ }
+}
+
+def cacheSetting = System.getenv("USE_ANDROIDX_REMOTE_BUILD_CACHE")
+def BUILD_NUMBER = System.getenv("BUILD_NUMBER")
+
+switch (cacheSetting) {
+ case "true":
+ case "uplink": // legacy build cache
+ gradle.settingsEvaluated { settings ->
+ settings.buildCache {
+ remote(HttpBuildCache) {
+ def osName = System.getProperty("os.name").toLowerCase()
+ switch (osName) {
+ case { it.contains("mac os x") }:
+ case { it.contains("darwin") }:
+ case { it.contains("osx") }:
+ url = "http://gradle-remote-cache.uplink2.goog:999/cache/"
+ break
+ default:
+ url = "http://gradle-remote-cache.uplink.goog:999/cache/"
+ break
+ }
+ allowInsecureProtocol = true
+ push = true
}
- allowInsecureProtocol = true
- push = true
}
}
- }
-} else {
- def uplinkLinux = new File("/usr/bin/uplink-helper")
- def uplinkMac = new File("/usr/local/bin/uplink-helper")
- if (uplinkLinux.exists() || uplinkMac.exists()) {
- logger.warn("\u001B[31m\nIt looks like you are a Googler running without remote build "
- + "cache. Enable it for faster builds, see " +
- "http://go/androidx-dev#remote-build-cache\u001B[0m\n")
- }
+ break
+ case "gcp":
+ gradle.settingsEvaluated { settings ->
+ buildCache {
+ registerBuildCacheService(GcpBuildCache, GcpBuildCacheServiceFactory)
+ }
+
+ settings.buildCache {
+ remote(GcpBuildCache) {
+ projectId = "androidx-ge"
+ bucketName = "androidx-gradle-build-cache"
+ push = (BUILD_NUMBER != null && !BUILD_NUMBER.startsWith("P"))
+ }
+ }
+ }
+ break
+ default:
+ def uplinkLinux = new File("/usr/bin/uplink-helper")
+ def uplinkMac = new File("/usr/local/bin/uplink-helper")
+ if (uplinkLinux.exists() || uplinkMac.exists()) {
+ logger.warn("\u001B[31m\nIt looks like you are a Googler running without remote build "
+ + "cache. Enable it for faster builds, see " +
+ "http://go/androidx-dev#remote-build-cache\u001B[0m\n")
+ }
}
diff --git a/compose/foundation/foundation-layout/api/current.txt b/compose/foundation/foundation-layout/api/current.txt
index ec3582e..1f5a59d 100644
--- a/compose/foundation/foundation-layout/api/current.txt
+++ b/compose/foundation/foundation-layout/api/current.txt
@@ -217,6 +217,9 @@
public static final class WindowInsets.Companion {
}
+ public final class WindowInsetsConnection_androidKt {
+ }
+
public final class WindowInsetsKt {
method public static androidx.compose.foundation.layout.WindowInsets WindowInsets(optional int left, optional int top, optional int right, optional int bottom);
method public static androidx.compose.foundation.layout.WindowInsets WindowInsets(optional float left, optional float top, optional float right, optional float bottom);
diff --git a/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt b/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
index 2e12685..9e0d3ee 100644
--- a/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
@@ -220,6 +220,10 @@
public static final class WindowInsets.Companion {
}
+ public final class WindowInsetsConnection_androidKt {
+ method @androidx.compose.foundation.layout.ExperimentalLayoutApi public static androidx.compose.ui.Modifier imeNestedScroll(androidx.compose.ui.Modifier);
+ }
+
public final class WindowInsetsKt {
method public static androidx.compose.foundation.layout.WindowInsets WindowInsets(optional int left, optional int top, optional int right, optional int bottom);
method public static androidx.compose.foundation.layout.WindowInsets WindowInsets(optional float left, optional float top, optional float right, optional float bottom);
diff --git a/compose/foundation/foundation-layout/api/restricted_current.txt b/compose/foundation/foundation-layout/api/restricted_current.txt
index 585f2e0..e1603e2 100644
--- a/compose/foundation/foundation-layout/api/restricted_current.txt
+++ b/compose/foundation/foundation-layout/api/restricted_current.txt
@@ -222,6 +222,9 @@
public static final class WindowInsets.Companion {
}
+ public final class WindowInsetsConnection_androidKt {
+ }
+
public final class WindowInsetsKt {
method public static androidx.compose.foundation.layout.WindowInsets WindowInsets(optional int left, optional int top, optional int right, optional int bottom);
method public static androidx.compose.foundation.layout.WindowInsets WindowInsets(optional float left, optional float top, optional float right, optional float bottom);
diff --git a/compose/foundation/foundation-layout/build.gradle b/compose/foundation/foundation-layout/build.gradle
index 8498807..6a468dd 100644
--- a/compose/foundation/foundation-layout/build.gradle
+++ b/compose/foundation/foundation-layout/build.gradle
@@ -41,6 +41,7 @@
implementation(project(":compose:runtime:runtime"))
implementation("androidx.compose.ui:ui-util:1.0.0")
implementation("androidx.core:core:1.7.0")
+ implementation("androidx.compose.animation:animation-core:1.1.1")
implementation(libs.kotlinStdlibCommon)
testImplementation(libs.testRules)
@@ -90,6 +91,7 @@
androidMain.dependencies {
api("androidx.annotation:annotation:1.1.0")
implementation("androidx.core:core:1.7.0")
+ implementation("androidx.compose.animation:animation-core:1.1.1")
}
desktopMain.dependencies {
diff --git a/compose/foundation/foundation-layout/samples/src/main/java/androidx/compose/foundation/layout/samples/WindowInsetsConnectionSample.kt b/compose/foundation/foundation-layout/samples/src/main/java/androidx/compose/foundation/layout/samples/WindowInsetsConnectionSample.kt
new file mode 100644
index 0000000..8bd86e3
--- /dev/null
+++ b/compose/foundation/foundation-layout/samples/src/main/java/androidx/compose/foundation/layout/samples/WindowInsetsConnectionSample.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.layout.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.imeNestedScroll
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+@OptIn(ExperimentalLayoutApi::class)
+@Sampled
+@Composable
+fun windowInsetsNestedScrollDemo() {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize() // fill the window
+ .imePadding() // pad out the bottom for the IME
+ .imeNestedScroll(), // scroll IME at the bottom
+ reverseLayout = true // First item is at the bottom
+ ) {
+ // content
+ items(50) {
+ Text("Hello World")
+ }
+ }
+}
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/AndroidManifest.xml b/compose/foundation/foundation-layout/src/androidAndroidTest/AndroidManifest.xml
index 2f531ed..9f5c83b 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/AndroidManifest.xml
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/AndroidManifest.xml
@@ -19,5 +19,9 @@
<activity
android:name="androidx.compose.foundation.layout.TestActivity"
android:theme="@android:style/Theme.Material.NoActionBar.Fullscreen" />
+ <activity
+ android:name="androidx.compose.foundation.layout.WindowInsetsActivity"
+ android:windowSoftInputMode="adjustResize"
+ android:theme="@android:style/Theme.Material.NoActionBar.Fullscreen" />
</application>
</manifest>
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsActivity.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsActivity.kt
new file mode 100644
index 0000000..41e064f
--- /dev/null
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsActivity.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.compose.foundation.layout
+
+import android.os.Build
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.annotation.RequiresApi
+import java.util.concurrent.CountDownLatch
+
+class WindowInsetsActivity : ComponentActivity() {
+ val createdLatch = CountDownLatch(1)
+
+ @RequiresApi(Build.VERSION_CODES.R)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ window.setDecorFitsSystemWindows(false)
+ super.onCreate(savedInstanceState)
+ createdLatch.countDown()
+ }
+}
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsControllerTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsControllerTest.kt
new file mode 100644
index 0000000..410e493
--- /dev/null
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsControllerTest.kt
@@ -0,0 +1,689 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.layout
+
+import android.graphics.Insets
+import android.os.Build
+import android.os.SystemClock
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MonotonicFrameClock
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.TouchInjectionScope
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeDown
+import androidx.compose.ui.test.swipeUp
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Velocity
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import kotlin.math.abs
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalLayoutApi::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+class WindowInsetsControllerTest {
+ @get:Rule
+ val rule = createAndroidComposeRule<WindowInsetsActivity>()
+
+ val testTag = "TestTag"
+
+ /**
+ * The size of the inset when shown.
+ */
+ private var shownSize = 0
+
+ /**
+ * This is the fling velocity that will move enough so that a spring will show at least
+ * 1 pixel of movement. This should be considered a small fling.
+ */
+ private val FlingToSpring1Pixel = 300f
+
+ // ========================================================
+ // The specific insets are extracted here so that different
+ // insets can be tested locally. IME works for R+, but
+ // status bars only work on S+. The following allows
+ // extracting out the insets particulars so that tests
+ // work with different insets types.
+ // ========================================================
+
+ /**
+ * The android WindowInsets type.
+ */
+ private val insetType = android.view.WindowInsets.Type.statusBars()
+ private val insetSide = WindowInsetsSides.Top
+
+ private val windowInsets: AndroidWindowInsets
+ @Composable
+ get() = WindowInsetsHolder.current().ime
+
+ private val WindowInsets.value: Int
+ get() = getBottom(Density(1f))
+
+ private val Insets.value: Int
+ get() = bottom
+
+ private val reverseLazyColumn = true
+
+ private fun TouchInjectionScope.swipeAwayFromInset() {
+ swipeUp()
+ }
+
+ private fun TouchInjectionScope.swipeTowardInset() {
+ swipeDown()
+ }
+
+ /**
+ * A motion in this direction moves away from the insets
+ */
+ val directionMultiplier: Float = -1f
+
+ private var shownAtStart = false
+
+ @Before
+ fun setup() {
+ rule.activity.createdLatch.await(1, TimeUnit.SECONDS)
+ rule.runOnUiThread {
+ val view = rule.activity.window.decorView
+ shownAtStart = view.rootWindowInsets.isVisible(insetType)
+ }
+ }
+ @After
+ fun teardown() {
+ rule.runOnUiThread {
+ val view = rule.activity.window.decorView
+ if (shownAtStart) {
+ view.windowInsetsController?.show(insetType)
+ } else {
+ view.windowInsetsController?.hide(insetType)
+ }
+ }
+ }
+
+ /**
+ * Scrolling away from the inset with the inset hidden should show it.
+ */
+ @Test
+ fun canScrollToShow() {
+ if (!initializeDeviceWithInsetsHidden()) {
+ return // The insets don't exist on this device
+ }
+ lateinit var connection: NestedScrollConnection
+ lateinit var coordinates: LayoutCoordinates
+
+ rule.setContent {
+ connection =
+ rememberWindowInsetsConnection(windowInsets, insetSide)
+ Box(Modifier
+ .fillMaxSize()
+ .windowInsetsPadding(windowInsets)
+ .nestedScroll(connection)
+ .onPlaced { coordinates = it }
+ )
+ }
+
+ val sizeBefore = coordinates.size
+
+ rule.runOnUiThread {
+ // The first scroll triggers the animation controller to be requested
+ val consumed = connection.onPostScroll(
+ consumed = Offset.Zero,
+ available = Offset(3f, directionMultiplier),
+ source = NestedScrollSource.Drag
+ )
+ assertThat(consumed).isEqualTo(Offset(0f, directionMultiplier))
+ }
+ // We don't know when the animation controller request will be fulfilled, so loop
+ // until we're sure
+ val startTime = SystemClock.uptimeMillis()
+ do {
+ assertThat(SystemClock.uptimeMillis()).isLessThan(startTime + 1000)
+ val size = rule.runOnUiThread {
+ connection.onPostScroll(
+ consumed = Offset.Zero,
+ available = Offset(3f, directionMultiplier * 5f),
+ source = NestedScrollSource.Drag
+ )
+ coordinates.size
+ }
+ } while (size == sizeBefore)
+
+ rule.runOnIdle {
+ val sizeAfter = coordinates.size
+ assertThat(sizeBefore.height).isGreaterThan(sizeAfter.height)
+ }
+ }
+
+ /**
+ * Scrolling toward the inset with the inset shown should hide it.
+ */
+ @Test
+ fun canScrollToHide() {
+ if (!initializeDeviceWithInsetsShown()) {
+ return // The insets don't exist on this device
+ }
+ lateinit var connection: NestedScrollConnection
+ lateinit var coordinates: LayoutCoordinates
+
+ rule.setContent {
+ connection =
+ rememberWindowInsetsConnection(windowInsets, insetSide)
+ Box(Modifier
+ .fillMaxSize()
+ .windowInsetsPadding(windowInsets)
+ .nestedScroll(connection)
+ .onPlaced { coordinates = it }
+ )
+ }
+
+ val sizeBefore = coordinates.size
+
+ rule.runOnUiThread {
+ // The first scroll triggers the animation controller to be requested
+ val consumed = connection.onPreScroll(
+ available = Offset(3f, -directionMultiplier),
+ source = NestedScrollSource.Drag
+ )
+ assertThat(consumed).isEqualTo(Offset(0f, -directionMultiplier))
+ }
+ // We don't know when the animation controller request will be fulfilled, so loop
+ // until we're sure
+ val startTime = SystemClock.uptimeMillis()
+ do {
+ assertThat(SystemClock.uptimeMillis()).isLessThan(startTime + 1000)
+ val size = rule.runOnUiThread {
+ connection.onPreScroll(
+ available = Offset(3f, directionMultiplier * -5f),
+ source = NestedScrollSource.Drag
+ )
+ coordinates.size
+ }
+ } while (size == sizeBefore)
+
+ rule.runOnIdle {
+ val sizeAfter = coordinates.size
+ assertThat(sizeBefore.height).isLessThan(sizeAfter.height)
+ }
+ }
+
+ /**
+ * Flinging away from an inset should show it.
+ */
+ @Test
+ fun canFlingToShow() {
+ if (!initializeDeviceWithInsetsHidden()) {
+ return // The insets don't exist on this device
+ }
+ lateinit var connection: NestedScrollConnection
+ lateinit var coordinates: LayoutCoordinates
+
+ rule.setContent {
+ connection =
+ rememberWindowInsetsConnection(windowInsets, insetSide)
+ Box(Modifier
+ .fillMaxSize()
+ .windowInsetsPadding(windowInsets)
+ .nestedScroll(connection)
+ .onPlaced { coordinates = it }
+ )
+ }
+
+ val sizeBefore = coordinates.size
+
+ runBlockingOnUiThread {
+ val consumed = connection.onPostFling(
+ consumed = Velocity.Zero,
+ available = Velocity(3f, directionMultiplier * 5000f)
+ )
+ assertThat(consumed.x).isEqualTo(0f)
+ assertThat(abs(consumed.y)).isLessThan(5000f)
+ }
+
+ rule.runOnIdle {
+ val sizeAfter = coordinates.size
+ assertThat(sizeBefore.height).isGreaterThan(sizeAfter.height)
+ }
+ }
+
+ /**
+ * Flinging toward an inset should hide it.
+ */
+ @Test
+ fun canFlingToHide() {
+ if (!initializeDeviceWithInsetsShown()) {
+ return // The insets don't exist on this device
+ }
+ lateinit var connection: NestedScrollConnection
+ lateinit var coordinates: LayoutCoordinates
+
+ rule.setContent {
+ connection =
+ rememberWindowInsetsConnection(windowInsets, insetSide)
+ Box(Modifier
+ .fillMaxSize()
+ .windowInsetsPadding(windowInsets)
+ .nestedScroll(connection)
+ .onPlaced { coordinates = it }
+ )
+ }
+
+ val sizeBefore = coordinates.size
+
+ runBlockingOnUiThread {
+ val consumed = connection.onPreFling(
+ available = Velocity(3f, -directionMultiplier * 5000f)
+ )
+ assertThat(consumed.x).isEqualTo(0f)
+ assertThat(abs(consumed.y)).isLessThan(5000f)
+ }
+
+ rule.runOnIdle {
+ val sizeAfter = coordinates.size
+ assertThat(sizeBefore.height).isLessThan(sizeAfter.height)
+ }
+ }
+
+ /**
+ * A small fling should use an animation to bounce back to hiding the inset
+ */
+ @Test
+ fun smallFlingSpringsBackToHide() {
+ if (!initializeDeviceWithInsetsHidden()) {
+ return // The insets don't exist on this device
+ }
+ lateinit var connection: NestedScrollConnection
+
+ var maxVisible = 0
+ var isVisible = false
+
+ rule.setContent {
+ connection =
+ rememberWindowInsetsConnection(windowInsets, insetSide)
+ maxVisible = maxOf(maxVisible, windowInsets.value)
+ isVisible = windowInsets.isVisible
+ Box(Modifier
+ .fillMaxSize()
+ .windowInsetsPadding(windowInsets)
+ .nestedScroll(connection)
+ )
+ }
+
+ runBlockingOnUiThread {
+ connection.onPostFling(
+ consumed = Velocity.Zero,
+ available = Velocity(0f, directionMultiplier * FlingToSpring1Pixel)
+ )
+ assertThat(maxVisible).isGreaterThan(0)
+ }
+
+ rule.runOnIdle {
+ assertThat(isVisible).isFalse()
+ }
+ }
+
+ /**
+ * A small fling should use an animation to bounce back to showing the inset
+ */
+ @Test
+ fun smallFlingSpringsBackToShow() {
+ if (!initializeDeviceWithInsetsShown()) {
+ return // The insets don't exist on this device
+ }
+ lateinit var connection: NestedScrollConnection
+
+ var minVisible = 0
+ var isVisible = false
+
+ rule.setContent {
+ connection =
+ rememberWindowInsetsConnection(windowInsets, insetSide)
+ minVisible = minOf(minVisible, windowInsets.value)
+ isVisible = windowInsets.isVisible
+ Box(Modifier
+ .fillMaxSize()
+ .windowInsetsPadding(windowInsets)
+ .nestedScroll(connection)
+ )
+ }
+
+ runBlockingOnUiThread {
+ connection.onPostFling(
+ consumed = Velocity.Zero,
+ available = Velocity(0f, directionMultiplier * FlingToSpring1Pixel)
+ )
+ assertThat(minVisible).isLessThan(shownSize)
+ }
+
+ rule.runOnIdle {
+ assertThat(isVisible).isTrue()
+ }
+ }
+
+ /**
+ * A fling past the middle should animate to fully showing the inset
+ */
+ @Test
+ fun flingPastMiddleSpringsToShow() {
+ if (!initializeDeviceWithInsetsHidden()) {
+ return // The insets don't exist on this device
+ }
+ lateinit var connection: NestedScrollConnection
+
+ var isVisible = false
+ var insetsSize = 0
+
+ rule.setContent {
+ connection =
+ rememberWindowInsetsConnection(windowInsets, insetSide)
+ isVisible = windowInsets.isVisible
+ insetsSize = windowInsets.value
+ Box(Modifier
+ .fillMaxSize()
+ .windowInsetsPadding(windowInsets)
+ .nestedScroll(connection)
+ )
+ }
+
+ // We don't know when the animation controller request will be fulfilled, so loop
+ // we scroll
+ val startTime = SystemClock.uptimeMillis()
+ do {
+ assertThat(SystemClock.uptimeMillis()).isLessThan(startTime + 1000)
+ rule.runOnIdle {
+ connection.onPostScroll(
+ consumed = Offset.Zero,
+ available = Offset(0f, directionMultiplier),
+ source = NestedScrollSource.Drag
+ )
+ }
+ } while (!isVisible)
+
+ // now scroll to just short of half way
+ rule.runOnIdle {
+ val sizeDifference = shownSize / 2f - 1f - insetsSize
+ connection.onPostScroll(
+ consumed = Offset.Zero,
+ available = Offset(0f, directionMultiplier * sizeDifference),
+ source = NestedScrollSource.Drag
+ )
+ }
+
+ rule.waitForIdle()
+
+ runBlockingOnUiThread {
+ connection.onPostFling(
+ consumed = Velocity.Zero,
+ available = Velocity(0f, directionMultiplier * FlingToSpring1Pixel)
+ )
+ }
+
+ rule.runOnIdle {
+ assertThat(isVisible).isTrue()
+ }
+ }
+
+ /**
+ * A fling that moves more than half way toward hiding should animate to fully hiding the inset
+ */
+ @Test
+ fun flingPastMiddleSpringsToHide() {
+ if (!initializeDeviceWithInsetsShown()) {
+ return // The insets don't exist on this device
+ }
+ lateinit var connection: NestedScrollConnection
+
+ var isVisible = false
+ var insetsSize = 0
+
+ rule.setContent {
+ connection =
+ rememberWindowInsetsConnection(windowInsets, insetSide)
+ isVisible = windowInsets.isVisible
+ insetsSize = windowInsets.value
+ Box(Modifier
+ .fillMaxSize()
+ .windowInsetsPadding(windowInsets)
+ .nestedScroll(connection)
+ )
+ }
+
+ // We don't know when the animation controller request will be fulfilled, so loop
+ // we scroll
+ val startTime = SystemClock.uptimeMillis()
+ do {
+ assertThat(SystemClock.uptimeMillis()).isLessThan(startTime + 1000)
+ rule.runOnIdle {
+ connection.onPreScroll(
+ available = Offset(0f, directionMultiplier * -1f),
+ source = NestedScrollSource.Drag
+ )
+ }
+ } while (insetsSize != shownSize)
+
+ // now scroll to just short of half way
+ rule.runOnIdle {
+ val sizeDifference = shownSize / 2f + 1f - insetsSize
+ connection.onPreScroll(
+ available = Offset(0f, directionMultiplier * sizeDifference),
+ source = NestedScrollSource.Drag
+ )
+ }
+
+ runBlockingOnUiThread {
+ // should fling at least one pixel past the middle
+ connection.onPreFling(
+ available = Velocity(0f, directionMultiplier * -FlingToSpring1Pixel)
+ )
+ }
+
+ rule.runOnIdle {
+ assertThat(isVisible).isFalse()
+ }
+ }
+
+ /**
+ * The insets shouldn't get in the way of normal scrolling on the normal content.
+ */
+ @Test
+ fun allowsContentScroll() {
+ if (!initializeDeviceWithInsetsHidden()) {
+ return // The insets don't exist on this device
+ }
+ lateinit var connection: NestedScrollConnection
+ lateinit var coordinates: LayoutCoordinates
+ val lazyListState = LazyListState()
+
+ rule.setContent {
+ connection =
+ rememberWindowInsetsConnection(windowInsets, insetSide)
+ val boxSize = with(LocalDensity.current) { 100.toDp() }
+ LazyColumn(
+ reverseLayout = reverseLazyColumn,
+ state = lazyListState,
+ modifier = Modifier
+ .fillMaxSize()
+ .windowInsetsPadding(windowInsets)
+ .nestedScroll(connection)
+ .testTag(testTag)
+ .onPlaced { coordinates = it }
+ ) {
+ items(1000) {
+ Box(Modifier.size(boxSize))
+ }
+ }
+ }
+
+ val sizeBefore = coordinates.size
+
+ rule.onNodeWithTag(testTag)
+ .performTouchInput {
+ swipeTowardInset()
+ }
+
+ rule.runOnIdle {
+ assertThat(coordinates.size.height).isEqualTo(sizeBefore.height)
+ // The drag should result in 1 item scrolled, but the fling should give more than 1
+ assertThat(lazyListState.firstVisibleItemIndex).isGreaterThan(2)
+ }
+
+ val firstVisibleIndex = lazyListState.firstVisibleItemIndex
+
+ rule.onNodeWithTag(testTag)
+ .performTouchInput {
+ swipeAwayFromInset()
+ }
+
+ rule.runOnIdle {
+ assertThat(coordinates.size.height).isEqualTo(sizeBefore.height)
+ // The drag should result in 1 item scrolled, but the fling should give more than 1
+ assertThat(lazyListState.firstVisibleItemIndex).isLessThan(firstVisibleIndex - 2)
+ }
+ }
+
+ /**
+ * When flinging more than the inset, it should animate the insets closed and then fling
+ * the content.
+ */
+ @Test
+ fun flingRemainderMovesContent() {
+ if (!initializeDeviceWithInsetsShown()) {
+ return // The insets don't exist on this device
+ }
+ lateinit var connection: NestedScrollConnection
+ lateinit var coordinates: LayoutCoordinates
+ val lazyListState = LazyListState()
+
+ rule.setContent {
+ connection =
+ rememberWindowInsetsConnection(windowInsets, insetSide)
+ val boxSize = with(LocalDensity.current) { 100.toDp() }
+ LazyColumn(
+ reverseLayout = reverseLazyColumn,
+ state = lazyListState,
+ modifier = Modifier
+ .fillMaxSize()
+ .windowInsetsPadding(windowInsets)
+ .nestedScroll(connection)
+ .testTag(testTag)
+ .onPlaced { coordinates = it }
+ ) {
+ items(1000) {
+ Box(Modifier.size(boxSize))
+ }
+ }
+ }
+
+ val sizeBefore = coordinates.size
+
+ rule.onNodeWithTag(testTag)
+ .performTouchInput {
+ swipeTowardInset()
+ }
+
+ rule.runOnIdle {
+ assertThat(coordinates.size.height).isGreaterThan(sizeBefore.height)
+ // The fling should get at least one item moved
+ assertThat(lazyListState.firstVisibleItemIndex).isGreaterThan(0)
+ }
+ }
+
+ private fun initializeDeviceWithInsetsShown(): Boolean {
+ val view = rule.activity.window.decorView
+
+ rule.runOnUiThread {
+ view.windowInsetsController?.show(insetType)
+ }
+
+ return rule.runOnIdle {
+ val windowInsets = view.rootWindowInsets
+ val insets = windowInsets.getInsets(insetType)
+ shownSize = insets.value
+ windowInsets.isVisible(insetType) && insets.value != 0
+ }
+ }
+
+ private fun initializeDeviceWithInsetsHidden(): Boolean {
+ if (!initializeDeviceWithInsetsShown()) {
+ return false
+ }
+ val view = rule.activity.window.decorView
+ rule.runOnUiThread {
+ view.windowInsetsController?.hide(insetType)
+ }
+ return rule.runOnUiThread {
+ val windowInsets = view.rootWindowInsets
+ !windowInsets.isVisible(insetType)
+ }
+ }
+
+ @OptIn(DelicateCoroutinesApi::class)
+ private fun runBlockingOnUiThread(block: suspend CoroutineScope.() -> Unit) {
+ val latch = CountDownLatch(1)
+ val clock = MyTestFrameClock()
+ GlobalScope.launch(Dispatchers.Main) {
+ val context = coroutineContext + clock
+ withContext(context, block)
+ latch.countDown()
+ }
+ var frameTimeNanos = 0L
+ while (latch.count > 0) {
+ frameTimeNanos += 4_000_000L // 4ms
+ clock.trySendFrame(frameTimeNanos)
+ rule.waitForIdle()
+ }
+ }
+
+ private class MyTestFrameClock : MonotonicFrameClock {
+ private val frameCh = Channel<Long>(1)
+
+ fun trySendFrame(frameTimeNanos: Long) {
+ frameCh.trySend(frameTimeNanos)
+ }
+
+ override suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R {
+ return onFrame(frameCh.receive())
+ }
+ }
+}
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsIgnoringVisibilityTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsIgnoringVisibilityTest.kt
index 671d982..7481422 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsIgnoringVisibilityTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsIgnoringVisibilityTest.kt
@@ -404,4 +404,4 @@
return insets.toWindowInsets()!!
}
}
-}
\ No newline at end of file
+}
diff --git a/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsets.android.kt b/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsets.android.kt
index 9d59fb7..d0b595673 100644
--- a/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsets.android.kt
+++ b/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsets.android.kt
@@ -18,13 +18,14 @@
import androidx.core.graphics.Insets as AndroidXInsets
import android.os.Build
+import android.os.SystemClock
import android.view.View
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.MutableState
import androidx.compose.runtime.NonRestartableComposable
+import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -37,6 +38,8 @@
import androidx.core.view.WindowInsetsCompat
import java.util.WeakHashMap
import androidx.compose.ui.R
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
import org.jetbrains.annotations.TestOnly
internal fun AndroidXInsets.toInsetsValues(): InsetsValues =
@@ -46,6 +49,62 @@
ValueInsets(insets.toInsetsValues(), name)
/**
+ * [WindowInsets] provided by the Android framework. These can be used in
+ * [rememberWindowInsetsConnection] to control the insets.
+ */
+@Stable
+internal class AndroidWindowInsets(
+ internal val type: Int,
+ private val name: String
+) : WindowInsets {
+ internal var insets by mutableStateOf(AndroidXInsets.NONE)
+
+ /**
+ * Returns whether the insets are visible, irrespective of whether or not they
+ * intersect with the Window.
+ */
+ var isVisible by mutableStateOf(true)
+ private set
+
+ override fun getLeft(density: Density, layoutDirection: LayoutDirection): Int {
+ return insets.left
+ }
+
+ override fun getTop(density: Density): Int {
+ return insets.top
+ }
+
+ override fun getRight(density: Density, layoutDirection: LayoutDirection): Int {
+ return insets.right
+ }
+
+ override fun getBottom(density: Density): Int {
+ return insets.bottom
+ }
+
+ @OptIn(ExperimentalLayoutApi::class)
+ internal fun update(windowInsetsCompat: WindowInsetsCompat) {
+ insets = windowInsetsCompat.getInsets(type)
+ isVisible = windowInsetsCompat.isVisible(type)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is AndroidWindowInsets) return false
+
+ return type == other.type
+ }
+
+ override fun hashCode(): Int {
+ return type
+ }
+
+ override fun toString(): String {
+ return "$name(${insets.left}, ${insets.top}, ${insets.right}, ${insets.bottom})"
+ }
+}
+
+/**
* Indicates whether access to [WindowInsets] within the [content][ComposeView.setContent]
* should consume the Android [android.view.WindowInsets]. The default value is `true`, meaning
* that access to [WindowInsets.Companion] will consume the Android WindowInsets.
@@ -251,7 +310,7 @@
@ExperimentalLayoutApi
@Composable
@NonRestartableComposable
- get() = WindowInsetsHolder.current().isCaptionBarVisible
+ get() = WindowInsetsHolder.current().captionBar.isVisible
/**
* `true` when the [soft keyboard][ime] is being displayed, irrespective of
@@ -263,7 +322,7 @@
@ExperimentalLayoutApi
@Composable
@NonRestartableComposable
- get() = WindowInsetsHolder.current().isImeVisible
+ get() = WindowInsetsHolder.current().ime.isVisible
/**
* `true` when the [statusBars] are being displayed, irrespective of
@@ -275,7 +334,7 @@
@ExperimentalLayoutApi
@Composable
@NonRestartableComposable
- get() = WindowInsetsHolder.current().areStatusBarsVisible
+ get() = WindowInsetsHolder.current().statusBars.isVisible
/**
* `true` when the [navigationBars] are being displayed, irrespective of
@@ -287,7 +346,7 @@
@ExperimentalLayoutApi
@Composable
@NonRestartableComposable
- get() = WindowInsetsHolder.current().areNavigationBarsVisible
+ get() = WindowInsetsHolder.current().navigationBars.isVisible
/**
* `true` when the [systemBars] are being displayed, irrespective of
@@ -299,7 +358,7 @@
@ExperimentalLayoutApi
@Composable
@NonRestartableComposable
- get() = WindowInsetsHolder.current().areSystemBarsVisible
+ get() = WindowInsetsHolder.current().systemBars.isVisible
/**
* `true` when the [tappableElement] is being displayed, irrespective of
* whether they intersects with the Window.
@@ -310,32 +369,33 @@
@ExperimentalLayoutApi
@Composable
@NonRestartableComposable
- get() = WindowInsetsHolder.current().isTappableElementVisible
+ get() = WindowInsetsHolder.current().tappableElement.isVisible
/**
* The insets for various values in the current window.
*/
+@OptIn(ExperimentalLayoutApi::class)
internal class WindowInsetsHolder private constructor(insets: WindowInsetsCompat?) {
val captionBar =
- valueInsets(insets, WindowInsetsCompat.Type.captionBar(), "captionBar")
+ systemInsets(insets, WindowInsetsCompat.Type.captionBar(), "captionBar")
val displayCutout =
- valueInsets(insets, WindowInsetsCompat.Type.displayCutout(), "displayCutout")
- val ime = valueInsets(insets, WindowInsetsCompat.Type.ime(), "ime")
- val mandatorySystemGestures = valueInsets(
+ systemInsets(insets, WindowInsetsCompat.Type.displayCutout(), "displayCutout")
+ val ime = systemInsets(insets, WindowInsetsCompat.Type.ime(), "ime")
+ val mandatorySystemGestures = systemInsets(
insets,
WindowInsetsCompat.Type.mandatorySystemGestures(),
"mandatorySystemGestures"
)
val navigationBars =
- valueInsets(insets, WindowInsetsCompat.Type.navigationBars(), "navigationBars")
+ systemInsets(insets, WindowInsetsCompat.Type.navigationBars(), "navigationBars")
val statusBars =
- valueInsets(insets, WindowInsetsCompat.Type.statusBars(), "statusBars")
+ systemInsets(insets, WindowInsetsCompat.Type.statusBars(), "statusBars")
val systemBars =
- valueInsets(insets, WindowInsetsCompat.Type.systemBars(), "systemBars")
+ systemInsets(insets, WindowInsetsCompat.Type.systemBars(), "systemBars")
val systemGestures =
- valueInsets(insets, WindowInsetsCompat.Type.systemGestures(), "systemGestures")
+ systemInsets(insets, WindowInsetsCompat.Type.systemGestures(), "systemGestures")
val tappableElement =
- valueInsets(insets, WindowInsetsCompat.Type.tappableElement(), "tappableElement")
+ systemInsets(insets, WindowInsetsCompat.Type.tappableElement(), "tappableElement")
val waterfall =
ValueInsets(insets?.displayCutout?.waterfallInsets ?: AndroidXInsets.NONE, "waterfall")
val safeDrawing =
@@ -368,19 +428,6 @@
"tappableElementIgnoringVisibility"
)
- var isCaptionBarVisible by mutableStateIsVisible(insets, WindowInsetsCompat.Type.captionBar())
- var isImeVisible by mutableStateIsVisible(insets, WindowInsetsCompat.Type.ime())
- var areNavigationBarsVisible by mutableStateIsVisible(
- insets,
- WindowInsetsCompat.Type.navigationBars()
- )
- var areStatusBarsVisible by mutableStateIsVisible(insets, WindowInsetsCompat.Type.statusBars())
- var areSystemBarsVisible by mutableStateIsVisible(insets, WindowInsetsCompat.Type.systemBars())
- var isTappableElementVisible by mutableStateIsVisible(
- insets,
- WindowInsetsCompat.Type.tappableElement()
- )
-
/**
* `true` unless the `ComposeView` [ComposeView.consumeWindowInsets] is set to `false`.
*/
@@ -431,63 +478,48 @@
* Updates the WindowInsets values and notifies changes.
*/
fun update(windowInsets: WindowInsetsCompat) {
- Snapshot.withMutableSnapshot {
- val insets = if (testInsets) {
- // WindowInsetsCompat erases insets that aren't part of the device.
- // For example, if there is no navigation bar because of hardware keys,
- // the bottom navigation bar will be removed. By using the constructor
- // that doesn't accept a View, it doesn't remove the insets that aren't
- // possible. This is important for testing on arbitrary hardware.
- WindowInsetsCompat.toWindowInsetsCompat(windowInsets.toWindowInsets()!!)
- } else {
- windowInsets
- }
- captionBar.value =
- insets.getInsets(WindowInsetsCompat.Type.captionBar()).toInsetsValues()
- captionBarIgnoringVisibility.value = insets.getInsetsIgnoringVisibility(
- WindowInsetsCompat.Type.captionBar()
- ).toInsetsValues()
- isCaptionBarVisible = insets.isVisible(WindowInsetsCompat.Type.captionBar())
- ime.value =
- insets.getInsets(WindowInsetsCompat.Type.ime()).toInsetsValues()
- isImeVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
- displayCutout.value =
- insets.getInsets(WindowInsetsCompat.Type.displayCutout()).toInsetsValues()
- navigationBars.value =
- insets.getInsets(WindowInsetsCompat.Type.navigationBars()).toInsetsValues()
- navigationBarsIgnoringVisibility.value = insets.getInsetsIgnoringVisibility(
- WindowInsetsCompat.Type.navigationBars()
- ).toInsetsValues()
- areNavigationBarsVisible = insets.isVisible(WindowInsetsCompat.Type.navigationBars())
- statusBars.value =
- insets.getInsets(WindowInsetsCompat.Type.statusBars()).toInsetsValues()
- statusBarsIgnoringVisibility.value = insets.getInsetsIgnoringVisibility(
- WindowInsetsCompat.Type.statusBars()
- ).toInsetsValues()
- areStatusBarsVisible = insets.isVisible(WindowInsetsCompat.Type.statusBars())
- systemBars.value =
- insets.getInsets(WindowInsetsCompat.Type.systemBars()).toInsetsValues()
- systemBarsIgnoringVisibility.value = insets.getInsetsIgnoringVisibility(
- WindowInsetsCompat.Type.systemBars()
- ).toInsetsValues()
- areSystemBarsVisible = insets.isVisible(WindowInsetsCompat.Type.systemBars())
- systemGestures.value =
- insets.getInsets(WindowInsetsCompat.Type.systemGestures()).toInsetsValues()
- tappableElement.value =
- insets.getInsets(WindowInsetsCompat.Type.tappableElement()).toInsetsValues()
- tappableElementIgnoringVisibility.value = insets.getInsetsIgnoringVisibility(
- WindowInsetsCompat.Type.tappableElement()
- ).toInsetsValues()
- isTappableElementVisible = insets.isVisible(WindowInsetsCompat.Type.tappableElement())
- mandatorySystemGestures.value =
- insets.getInsets(WindowInsetsCompat.Type.mandatorySystemGestures()).toInsetsValues()
-
- val cutout = insets.displayCutout
- if (cutout != null) {
- val waterfallInsets = cutout.waterfallInsets
- waterfall.value = waterfallInsets.toInsetsValues()
- }
+ val insets = if (testInsets) {
+ // WindowInsetsCompat erases insets that aren't part of the device.
+ // For example, if there is no navigation bar because of hardware keys,
+ // the bottom navigation bar will be removed. By using the constructor
+ // that doesn't accept a View, it doesn't remove the insets that aren't
+ // possible. This is important for testing on arbitrary hardware.
+ WindowInsetsCompat.toWindowInsetsCompat(windowInsets.toWindowInsets()!!)
+ } else {
+ windowInsets
}
+ captionBar.update(insets)
+ ime.update(insets)
+ displayCutout.update(insets)
+ navigationBars.update(insets)
+ statusBars.update(insets)
+ systemBars.update(insets)
+ systemGestures.update(insets)
+ tappableElement.update(insets)
+ mandatorySystemGestures.update(insets)
+
+ captionBarIgnoringVisibility.value = insets.getInsetsIgnoringVisibility(
+ WindowInsetsCompat.Type.captionBar()
+ ).toInsetsValues()
+ navigationBarsIgnoringVisibility.value = insets.getInsetsIgnoringVisibility(
+ WindowInsetsCompat.Type.navigationBars()
+ ).toInsetsValues()
+ statusBarsIgnoringVisibility.value = insets.getInsetsIgnoringVisibility(
+ WindowInsetsCompat.Type.statusBars()
+ ).toInsetsValues()
+ systemBarsIgnoringVisibility.value = insets.getInsetsIgnoringVisibility(
+ WindowInsetsCompat.Type.systemBars()
+ ).toInsetsValues()
+ tappableElementIgnoringVisibility.value = insets.getInsetsIgnoringVisibility(
+ WindowInsetsCompat.Type.tappableElement()
+ ).toInsetsValues()
+
+ val cutout = insets.displayCutout
+ if (cutout != null) {
+ val waterfallInsets = cutout.waterfallInsets
+ waterfall.value = waterfallInsets.toInsetsValues()
+ }
+ Snapshot.sendApplyNotifications()
}
companion object {
@@ -546,14 +578,11 @@
/**
* Creates a [ValueInsets] using the value from [windowInsets] if it isn't `null`
*/
- private fun valueInsets(
+ private fun systemInsets(
windowInsets: WindowInsetsCompat?,
type: Int,
name: String
- ): ValueInsets {
- val initial = windowInsets?.getInsets(type) ?: AndroidXInsets.NONE
- return ValueInsets(initial, name)
- }
+ ) = AndroidWindowInsets(type, name).apply { windowInsets?.let { update(it) } }
/**
* Creates a [ValueInsets] using the "ignoring visibility" value from [windowInsets]
@@ -567,18 +596,6 @@
val initial = windowInsets?.getInsetsIgnoringVisibility(type) ?: AndroidXInsets.NONE
return ValueInsets(initial, name)
}
-
- /**
- * Creates a [ValueInsets] using the "ignoring visibility" value from [windowInsets]
- * if it isn't `null`
- */
- private fun mutableStateIsVisible(
- windowInsets: WindowInsetsCompat?,
- type: Int
- ): MutableState<Boolean> {
- val initial = windowInsets?.isVisible(type) ?: true
- return mutableStateOf(initial)
- }
}
}
@@ -598,17 +615,78 @@
private class InsetsListener(
val composeInsets: WindowInsetsHolder,
) : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP), OnApplyWindowInsetsListener {
+ /**
+ * When [android.view.WindowInsetsController.controlWindowInsetsAnimation] is called,
+ * the [onApplyWindowInsets] is called after [onPrepare] with the target size. We
+ * don't want to report the target size, we want to always report the current size,
+ * so we must ignore those calls. However, the animation may be canceled before it
+ * progresses. On R, it won't make any callbacks, so we have to figure out whether
+ * the [onApplyWindowInsets] is from a canceled animation or if it is from the
+ * controlled animation. We just have to guess that if we don't receive an [onStart]
+ * before a certain time that the animation has been canceled, and to treat the
+ * [onApplyWindowInsets] as a real call. [prepareGiveUpTime] has the time that we
+ * give up waiting for the [onStart] or [onEnd].
+ */
+ var prepareGiveUpTime = 0L
+
+ /**
+ * `true` if the [onStart] has been called, so we know that we're part of an animation
+ * and [onApplyWindowInsets] calls should be ignored.
+ */
+ var started = false
+
+ override fun onPrepare(animation: WindowInsetsAnimationCompat) {
+ prepareGiveUpTime = SystemClock.uptimeMillis() + AnimationCanceledMillis
+ super.onPrepare(animation)
+ }
+
+ override fun onStart(
+ animation: WindowInsetsAnimationCompat,
+ bounds: WindowInsetsAnimationCompat.BoundsCompat
+ ): WindowInsetsAnimationCompat.BoundsCompat {
+ started = true
+ return super.onStart(animation, bounds)
+ }
override fun onProgress(
insets: WindowInsetsCompat,
runningAnimations: MutableList<WindowInsetsAnimationCompat>
): WindowInsetsCompat {
+ prepareGiveUpTime = 0L
composeInsets.update(insets)
return if (composeInsets.consumes) WindowInsetsCompat.CONSUMED else insets
}
+ override fun onEnd(animation: WindowInsetsAnimationCompat) {
+ started = false
+ prepareGiveUpTime = 0L
+ super.onEnd(animation)
+ }
+
override fun onApplyWindowInsets(view: View, insets: WindowInsetsCompat): WindowInsetsCompat {
+ val prepareGiveUpTime = prepareGiveUpTime
+ this.prepareGiveUpTime = 0L
+
+ // There may be no callback on R if the animation is canceled after onPrepare(),
+ // so we won't know if the onPrepare() was canceled or if the
+ // So we must allow onApplyWindowInsets() to run if it isn't directly after the
+ // onPrepare().
+ val preparing = prepareGiveUpTime != 0L &&
+ (Build.VERSION.SDK_INT > Build.VERSION_CODES.R ||
+ prepareGiveUpTime > SystemClock.uptimeMillis())
+ if (started || preparing) {
+ // Just ignore this one. It came from the onPrepare.
+ return insets
+ }
composeInsets.update(insets)
return if (composeInsets.consumes) WindowInsetsCompat.CONSUMED else insets
}
+
+ companion object {
+ // If an [onApplyWindowInsets] is received this number of milliseconds after
+ // [onPrepare] and the animation hasn't started, then it is assumed that the
+ // animation was canceled before starting. On R and earlier, we don't get any
+ // signal about cancellation.
+ const val AnimationCanceledMillis = 100L
+ }
}
diff --git a/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsetsConnection.android.kt b/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsetsConnection.android.kt
new file mode 100644
index 0000000..e406da8
--- /dev/null
+++ b/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsetsConnection.android.kt
@@ -0,0 +1,708 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.layout
+
+import android.graphics.Insets
+import android.os.Build
+import android.os.CancellationSignal
+import android.view.View
+import android.view.ViewConfiguration
+import android.view.WindowInsetsAnimationControlListener
+import android.view.WindowInsetsAnimationController
+import androidx.annotation.RequiresApi
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.FloatDecayAnimationSpec
+import androidx.compose.animation.core.animateDecay
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.Velocity
+import kotlin.math.roundToInt
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.util.packFloats
+import androidx.compose.ui.util.unpackFloat1
+import androidx.compose.ui.util.unpackFloat2
+import kotlin.math.abs
+import kotlin.math.exp
+import kotlin.math.ln
+import kotlin.math.sign
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * Controls the soft keyboard as a nested scrolling on Android [R][Build.VERSION_CODES.R]
+ * and later. This allows the user to drag the soft keyboard up and down.
+ *
+ * After scrolling, the IME will animate either to the fully shown or fully hidden position,
+ * depending on the position and fling.
+ *
+ * @sample androidx.compose.foundation.layout.samples.windowInsetsNestedScrollDemo
+ */
+@ExperimentalLayoutApi
+fun Modifier.imeNestedScroll(): Modifier {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ return this
+ }
+ return composed(
+ debugInspectorInfo {
+ name = "imeNestedScroll"
+ }
+ ) {
+ val nestedScrollConnection = rememberWindowInsetsConnection(
+ WindowInsetsHolder.current().ime,
+ WindowInsetsSides.Bottom
+ )
+ nestedScroll(nestedScrollConnection)
+ }
+}
+
+/**
+ * Returns a [NestedScrollConnection] that can be used with [WindowInsets] on Android
+ * [R][Build.VERSION_CODES.R] and later.
+ *
+ * The [NestedScrollConnection] can be used when a developer wants to control a [WindowInsets],
+ * either directly animating it or allowing the user to manually manipulate it. User interactions
+ * will result in the [WindowInsets] animating either hidden or shown, depending on its
+ * current position and the fling velocity received in [NestedScrollConnection.onPreFling] and
+ * [NestedScrollConnection.onPostFling].
+ *
+ * @param windowInsets The insets to be changed by the scroll effect
+ * @param side The side of the [windowInsets] that is to be affected. Can only be one of
+ * [WindowInsetsSides.Left], [WindowInsetsSides.Top], [WindowInsetsSides.Right],
+ * [WindowInsetsSides.Bottom], [WindowInsetsSides.Start], [WindowInsetsSides.End].
+ */
+@ExperimentalLayoutApi
+@Composable
+internal fun rememberWindowInsetsConnection(
+ windowInsets: AndroidWindowInsets,
+ side: WindowInsetsSides
+): NestedScrollConnection {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ return DoNothingNestedScrollConnection
+ }
+ val layoutDirection = LocalLayoutDirection.current
+ val sideCalculator = SideCalculator.chooseCalculator(side, layoutDirection)
+ val view = LocalView.current
+ val density = LocalDensity.current
+ val connection = remember(windowInsets, view, sideCalculator, density) {
+ WindowInsetsNestedScrollConnection(windowInsets, view, sideCalculator, density)
+ }
+ DisposableEffect(connection) {
+ onDispose {
+ connection.dispose()
+ }
+ }
+ return connection
+}
+
+/**
+ * A [NestedScrollConnection] that does nothing, for versions before R.
+ */
+private object DoNothingNestedScrollConnection : NestedScrollConnection
+
+@OptIn(ExperimentalCoroutinesApi::class, ExperimentalLayoutApi::class)
+@RequiresApi(Build.VERSION_CODES.R)
+private class WindowInsetsNestedScrollConnection(
+ val windowInsets: AndroidWindowInsets,
+ val view: View,
+ val sideCalculator: SideCalculator,
+ val density: Density
+) : NestedScrollConnection,
+ WindowInsetsAnimationControlListener {
+
+ /**
+ * The [WindowInsetsAnimationController] is only available once the insets are starting
+ * to be manipulated. This is used to set the current insets position.
+ */
+ private var animationController: WindowInsetsAnimationController? = null
+
+ /**
+ * `true` when we've requested a [WindowInsetsAnimationController] so that we don't
+ * ask for one when we've already asked for one. This should be `false` until we've
+ * made a request or when we've cleared [animationController] after it is finished.
+ */
+ private var isControllerRequested = false
+
+ /**
+ * We never need to cancel the animation because we always control it directly instead
+ * of using the [WindowInsetsAnimationController] to animate its value.
+ */
+ private val cancellationSignal = CancellationSignal()
+
+ /**
+ * Because touch motion has finer granularity than integers, we capture the fractions of
+ * integers here so that we can keep the finger more in line with the touch. Without this,
+ * we'd accumulate error.
+ */
+ private var partialConsumption = 0f
+
+ /**
+ * The [Job] that is launched to animate the insets during a fling. This can be canceled
+ * when the user touches the screen.
+ */
+ private var animationJob: Job? = null
+
+ /**
+ * Request an animation controller because it is `null`. If one has already been requested,
+ * this method does nothing.
+ */
+ private fun requestAnimationController() {
+ if (!isControllerRequested) {
+ isControllerRequested = true
+ view.windowInsetsController?.controlWindowInsetsAnimation(
+ windowInsets.type, // type
+ -1, // durationMillis
+ null, // interpolator
+ cancellationSignal,
+ this
+ )
+ }
+ }
+
+ private var continuation: CancellableContinuation<WindowInsetsAnimationController?>? = null
+
+ /**
+ * Allows us to suspend, waiting for the animation controller to be returned.
+ */
+ private suspend fun getAnimationController(): WindowInsetsAnimationController? =
+ animationController ?: suspendCancellableCoroutine { continuation ->
+ this.continuation = continuation
+ requestAnimationController()
+ }
+
+ /**
+ * Handle the dragging that hides the WindowInsets.
+ */
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset =
+ scroll(available, sideCalculator.hideMotion(available.x, available.y))
+
+ /**
+ * Handle the dragging that exposes the WindowInsets.
+ */
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset = scroll(available, sideCalculator.showMotion(available.x, available.y))
+
+ /**
+ * Scrolls [scrollAmount] and returns the consumed amount of [available].
+ */
+ private fun scroll(available: Offset, scrollAmount: Float): Offset {
+ animationJob?.let {
+ it.cancel()
+ animationJob = null
+ }
+
+ val animationController = animationController
+
+ if (scrollAmount == 0f ||
+ (windowInsets.isVisible == (scrollAmount > 0f) && animationController == null)
+ ) {
+ // No motion in the right direction or this is already fully shown/hidden.
+ return Offset.Zero
+ }
+
+ if (animationController == null) {
+ partialConsumption = 0f
+ // The animation controller isn't ready yet. Just consume the scroll.
+ requestAnimationController()
+ return sideCalculator.consumedOffsets(available)
+ }
+
+ val hidden = sideCalculator.valueOf(animationController.hiddenStateInsets)
+ val shown = sideCalculator.valueOf(animationController.shownStateInsets)
+ val currentInsets = animationController.currentInsets
+ val current = sideCalculator.valueOf(currentInsets)
+
+ val target = if (scrollAmount > 0f) shown else hidden
+
+ if (current == target) {
+ // This is already correct, so nothing to consume
+ partialConsumption = 0f
+ return Offset.Zero
+ }
+
+ val total = current + scrollAmount + partialConsumption
+ val next = total.roundToInt().coerceIn(hidden, shown)
+ partialConsumption = total - total.roundToInt()
+
+ if (next != current) {
+ animationController.setInsetsAndAlpha(
+ sideCalculator.adjustInsets(currentInsets, next),
+ 1f, // alpha
+ 0f, // progress
+ )
+ }
+ return sideCalculator.consumedOffsets(available)
+ }
+
+ /**
+ * Handle flinging toward hiding the insets.
+ */
+ override suspend fun onPreFling(available: Velocity): Velocity =
+ fling(available, sideCalculator.hideMotion(available.x, available.y), false)
+
+ /**
+ * Handle flinging toward showing the insets.
+ */
+ override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity =
+ fling(available, sideCalculator.showMotion(available.x, available.y), true)
+
+ /**
+ * Handle flinging by [flingAmount] and return the consumed velocity of [available].
+ * [towardShown] should be `true` when the intended motion is to show the insets or `false`
+ * if to hide them. We always handle flinging toward the insets if the [flingAmount] is
+ * `0` so that the insets animate to a fully-shown or fully-hidden state.
+ */
+ private suspend fun fling(
+ available: Velocity,
+ flingAmount: Float,
+ towardShown: Boolean
+ ): Velocity {
+ animationJob?.cancel()
+ animationJob = null
+ partialConsumption = 0f
+
+ if ((flingAmount == 0f && !towardShown) ||
+ (animationController == null && windowInsets.isVisible == towardShown)
+ ) {
+ // Either there's no motion to hide or we're certain that
+ // the inset is already correct.
+ return Velocity.Zero
+ }
+
+ val animationController = getAnimationController() ?: return Velocity.Zero
+
+ val hidden = sideCalculator.valueOf(animationController.hiddenStateInsets)
+ val shown = sideCalculator.valueOf(animationController.shownStateInsets)
+ val currentInsets = animationController.currentInsets
+ val current = sideCalculator.valueOf(currentInsets)
+
+ if ((flingAmount <= 0 && current == hidden) || (flingAmount >= 0 && current == shown)) {
+ // We've already reached the destination
+ animationController.finish(current == shown)
+ this@WindowInsetsNestedScrollConnection.animationController = null
+ return Velocity.Zero
+ }
+
+ // Let's see if the velocity is enough to get open
+ val spec = SplineBasedFloatDecayAnimationSpec(density)
+ val distance = current + spec.flingDistance(flingAmount)
+
+ val endPercent = (distance - hidden) / (shown - hidden)
+ val targetShown = endPercent > 0.5f
+ val target = if (targetShown) shown else hidden
+
+ if (distance > shown || distance < hidden) {
+ var endVelocity = 0f
+ // This is enough to reach hidden or shown state, so we can use the Android
+ // spline animation.
+ coroutineScope {
+ animationJob = launch {
+ animateDecay(
+ initialValue = current.toFloat(),
+ initialVelocity = flingAmount,
+ animationSpec = spec
+ ) { value, velocity ->
+ if (value in hidden.toFloat()..shown.toFloat()) {
+ adjustInsets(value)
+ } else {
+ // We've reached the end
+ endVelocity = velocity
+ animationController.finish(targetShown)
+ this@WindowInsetsNestedScrollConnection.animationController = null
+ animationJob?.cancel()
+ }
+ }
+ }
+ animationJob?.join()
+ animationJob = null
+ }
+ return sideCalculator.consumedVelocity(available, endVelocity)
+ } else {
+ // This fling won't make it to the end, so animate to shown or hidden state using
+ // a spring animation
+ coroutineScope {
+ animationJob = launch {
+ val animatedValue = Animatable(current.toFloat())
+ animatedValue.animateTo(target.toFloat(), initialVelocity = flingAmount) {
+ adjustInsets(value)
+ }
+ animationController.finish(targetShown)
+ this@WindowInsetsNestedScrollConnection.animationController = null
+ }
+ }
+ return sideCalculator.consumedVelocity(available, 0f)
+ }
+ }
+
+ /**
+ * Change the inset's side to [inset].
+ */
+ private fun adjustInsets(inset: Float) {
+ animationController?.let {
+ val currentInsets = it.currentInsets
+ val nextInsets = sideCalculator.adjustInsets(currentInsets, inset.roundToInt())
+ it.setInsetsAndAlpha(
+ nextInsets,
+ 1f, // alpha
+ 0f, // progress
+ )
+ }
+ }
+
+ /**
+ * Called after [requestAnimationController] and the [animationController] is ready.
+ */
+ override fun onReady(controller: WindowInsetsAnimationController, types: Int) {
+ animationController = controller
+ isControllerRequested = false
+ continuation?.resume(controller) { }
+ continuation = null
+ }
+
+ fun dispose() {
+ continuation?.resume(null) { }
+ animationJob?.cancel()
+ val animationController = animationController
+ if (animationController != null) {
+ // We don't want to leave the insets in a partially open or closed state, so finish
+ // the animation
+ val visible = animationController.currentInsets != animationController.hiddenStateInsets
+ animationController.finish(visible)
+ }
+ }
+
+ override fun onFinished(controller: WindowInsetsAnimationController) {
+ animationEnded()
+ }
+
+ override fun onCancelled(controller: WindowInsetsAnimationController?) {
+ animationEnded()
+ }
+
+ /**
+ * The controlled animation has been terminated.
+ */
+ private fun animationEnded() {
+ if (animationController?.isReady == true) {
+ animationController?.finish(windowInsets.isVisible)
+ }
+ animationController = null
+
+ // The animation controller may not have been given to us, so we have to cancel animations
+ // waiting for it.
+ continuation?.resume(null) { }
+ continuation = null
+
+ // Cancel any animation that's running.
+ animationJob?.cancel()
+ animationJob = null
+
+ partialConsumption = 0f
+ isControllerRequested = false
+ }
+}
+
+/**
+ * This interface allows logic for the specific side (left, top, right, bottom) to be
+ * extracted from the logic controlling showing and hiding insets. For example, an inset
+ * at the top will show when dragging down, while an inset at the bottom will hide
+ * when dragging down.
+ */
+@RequiresApi(Build.VERSION_CODES.R)
+private interface SideCalculator {
+ /**
+ * Returns the insets value for the side that this [SideCalculator] is associated with.
+ */
+ fun valueOf(insets: Insets): Int
+
+ /**
+ * Returns the motion, adjusted for side direction, that the [x], and [y] grant. A positive
+ * result indicates that it is in the direction of opening the insets on that side and
+ * a negative result indicates a closing of the insets on that side.
+ */
+ fun motionOf(x: Float, y: Float): Float
+
+ /**
+ * The motion of [x], [y] that indicates showing more of the insets on the side or `0` if
+ * no motion is given to showing more insets.
+ */
+ fun showMotion(x: Float, y: Float): Float = motionOf(x, y).coerceAtLeast(0f)
+
+ /**
+ * The motion of [x], [y] that indicates showing less of the insets on the side or `0` if
+ * no motion is given to showing less insets.
+ */
+ fun hideMotion(x: Float, y: Float): Float = motionOf(x, y).coerceAtMost(0f)
+
+ /**
+ * Takes all values of [oldInsets], except for this side and replaces this side with [newValue].
+ */
+ fun adjustInsets(oldInsets: Insets, newValue: Int): Insets
+
+ /**
+ * Returns the [Offset] that consumes [available] in the direction of this side.
+ */
+ fun consumedOffsets(available: Offset): Offset
+
+ /**
+ * Returns the [Velocity] that consumes [available] in the direction of this side.
+ */
+ fun consumedVelocity(available: Velocity, remaining: Float): Velocity
+
+ companion object {
+ /**
+ * Returns a [SideCalculator] for [side] and the given [layoutDirection]. This only
+ * works for one side and no combination of sides.
+ */
+ fun chooseCalculator(side: WindowInsetsSides, layoutDirection: LayoutDirection) =
+ when (side) {
+ WindowInsetsSides.Left -> LeftSideCalculator
+ WindowInsetsSides.Top -> TopSideCalculator
+ WindowInsetsSides.Right -> RightSideCalculator
+ WindowInsetsSides.Bottom -> BottomSideCalculator
+ WindowInsetsSides.Start -> if (layoutDirection == LayoutDirection.Ltr) {
+ LeftSideCalculator
+ } else {
+ RightSideCalculator
+ }
+ WindowInsetsSides.End -> if (layoutDirection == LayoutDirection.Ltr) {
+ RightSideCalculator
+ } else {
+ LeftSideCalculator
+ }
+ else -> error("Only Left, Top, Right, Bottom, Start and End are allowed")
+ }
+
+ private val LeftSideCalculator = object : SideCalculator {
+ override fun valueOf(insets: Insets): Int = insets.left
+ override fun motionOf(x: Float, y: Float): Float = x
+ override fun adjustInsets(oldInsets: Insets, newValue: Int): Insets =
+ Insets.of(newValue, oldInsets.top, oldInsets.right, oldInsets.bottom)
+ override fun consumedOffsets(available: Offset): Offset = Offset(available.x, 0f)
+ override fun consumedVelocity(available: Velocity, remaining: Float): Velocity =
+ Velocity(available.x - remaining, 0f)
+ }
+
+ private val TopSideCalculator = object : SideCalculator {
+ override fun valueOf(insets: Insets): Int = insets.top
+ override fun motionOf(x: Float, y: Float): Float = y
+ override fun adjustInsets(oldInsets: Insets, newValue: Int): Insets =
+ Insets.of(oldInsets.left, newValue, oldInsets.right, oldInsets.bottom)
+ override fun consumedOffsets(available: Offset): Offset = Offset(0f, available.y)
+ override fun consumedVelocity(available: Velocity, remaining: Float): Velocity =
+ Velocity(0f, available.y - remaining)
+ }
+
+ private val RightSideCalculator = object : SideCalculator {
+ override fun valueOf(insets: Insets): Int = insets.right
+ override fun motionOf(x: Float, y: Float): Float = -x
+ override fun adjustInsets(oldInsets: Insets, newValue: Int): Insets =
+ Insets.of(oldInsets.left, oldInsets.top, newValue, oldInsets.bottom)
+ override fun consumedOffsets(available: Offset): Offset = Offset(available.x, 0f)
+ override fun consumedVelocity(available: Velocity, remaining: Float): Velocity =
+ Velocity(available.x + remaining, 0f)
+ }
+
+ private val BottomSideCalculator = object : SideCalculator {
+ override fun valueOf(insets: Insets): Int = insets.bottom
+ override fun motionOf(x: Float, y: Float): Float = -y
+ override fun adjustInsets(oldInsets: Insets, newValue: Int): Insets =
+ Insets.of(oldInsets.left, oldInsets.top, oldInsets.right, newValue)
+ override fun consumedOffsets(available: Offset): Offset = Offset(0f, available.y)
+ override fun consumedVelocity(available: Velocity, remaining: Float): Velocity =
+ Velocity(0f, available.y + remaining)
+ }
+ }
+}
+
+// SplineBasedFloatDecayAnimationSpec is in animation:animation library, which depends on
+// foundation-layout, so I've copied it below, but a bit trimmed to only have what is needed.
+
+// These constants are copied from the Android spline decay rate
+private const val Inflection = 0.35f // Tension lines cross at (Inflection, 1)
+private val PlatformFlingScrollFriction = ViewConfiguration.getScrollFriction()
+private const val GravityEarth = 9.80665f
+private const val InchesPerMeter = 39.37f
+private val DecelerationRate = ln(0.78) / ln(0.9)
+private val DecelMinusOne = DecelerationRate - 1.0
+private const val StartTension = 0.5f
+private const val EndTension = 1.0f
+private const val P1 = StartTension * Inflection
+private const val P2 = 1.0f - EndTension * (1.0f - Inflection)
+
+private class SplineBasedFloatDecayAnimationSpec(density: Density) :
+ FloatDecayAnimationSpec {
+
+ override val absVelocityThreshold: Float get() = 0f
+
+ /**
+ * A density-specific coefficient adjusted to physical values.
+ */
+ private val magicPhysicalCoefficient: Float =
+ GravityEarth * InchesPerMeter * density.density * 160f * 0.84f
+
+ private fun getSplineDeceleration(velocity: Float): Double =
+ AndroidFlingSpline.deceleration(
+ velocity,
+ PlatformFlingScrollFriction * magicPhysicalCoefficient
+ )
+
+ /**
+ * Compute the distance of a fling in units given an initial [velocity] of units/second
+ */
+ fun flingDistance(velocity: Float): Float {
+ val l = getSplineDeceleration(velocity)
+ return (
+ PlatformFlingScrollFriction * magicPhysicalCoefficient
+ * exp(DecelerationRate / DecelMinusOne * l)
+ ).toFloat() * sign(velocity)
+ }
+
+ override fun getTargetValue(initialValue: Float, initialVelocity: Float): Float =
+ initialValue + flingDistance(initialVelocity)
+
+ @Suppress("MethodNameUnits")
+ override fun getValueFromNanos(
+ playTimeNanos: Long,
+ initialValue: Float,
+ initialVelocity: Float
+ ): Float {
+ val duration = getDurationNanos(0f, initialVelocity)
+ val splinePos = if (duration > 0) playTimeNanos / duration.toFloat() else 1f
+ val distance = flingDistance(initialVelocity)
+ return initialValue + distance *
+ AndroidFlingSpline.flingPosition(splinePos).distanceCoefficient
+ }
+
+ @Suppress("MethodNameUnits")
+ override fun getDurationNanos(initialValue: Float, initialVelocity: Float): Long {
+ val l = getSplineDeceleration(initialVelocity)
+ return (1_000_000_000.0 * exp(l / DecelMinusOne)).toLong()
+ }
+
+ @Suppress("MethodNameUnits")
+ override fun getVelocityFromNanos(
+ playTimeNanos: Long,
+ initialValue: Float,
+ initialVelocity: Float
+ ): Float {
+ val duration = getDurationNanos(0f, initialVelocity)
+ val splinePos = if (duration > 0L) playTimeNanos / duration.toFloat() else 1f
+ val distance = flingDistance(initialVelocity)
+ return AndroidFlingSpline.flingPosition(splinePos).velocityCoefficient *
+ distance / duration * 1_000_000_000.0f
+ }
+}
+
+private object AndroidFlingSpline {
+ private const val NbSamples = 100
+ private val SplinePositions = FloatArray(NbSamples + 1)
+ private val SplineTimes = FloatArray(NbSamples + 1)
+
+ init {
+ var xMin = 0.0f
+ var yMin = 0.0f
+ for (i in 0 until NbSamples) {
+ val alpha = i.toFloat() / NbSamples
+ var xMax = 1.0f
+ var x: Float
+ var tx: Float
+ var coef: Float
+ while (true) {
+ x = xMin + (xMax - xMin) / 2.0f
+ coef = 3.0f * x * (1.0f - x)
+ tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x
+ if (abs(tx - alpha) < 1E-5) break
+ if (tx > alpha) xMax = x else xMin = x
+ }
+ SplinePositions[i] = coef * ((1.0f - x) * StartTension + x) + x * x * x
+ var yMax = 1.0f
+ var y: Float
+ var dy: Float
+ while (true) {
+ y = yMin + (yMax - yMin) / 2.0f
+ coef = 3.0f * y * (1.0f - y)
+ dy = coef * ((1.0f - y) * StartTension + y) + y * y * y
+ if (abs(dy - alpha) < 1E-5) break
+ if (dy > alpha) yMax = y else yMin = y
+ }
+ SplineTimes[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y
+ }
+ SplineTimes[NbSamples] = 1.0f
+ SplinePositions[NbSamples] = SplineTimes[NbSamples]
+ }
+
+ /**
+ * Compute an instantaneous fling position along the scroller spline.
+ *
+ * @param time progress through the fling animation from 0-1
+ */
+ fun flingPosition(time: Float): FlingResult {
+ val index = (NbSamples * time).toInt()
+ var distanceCoef = 1f
+ var velocityCoef = 0f
+ if (index < NbSamples) {
+ val tInf = index.toFloat() / NbSamples
+ val tSup = (index + 1).toFloat() / NbSamples
+ val dInf = SplinePositions[index]
+ val dSup = SplinePositions[index + 1]
+ velocityCoef = (dSup - dInf) / (tSup - tInf)
+ distanceCoef = dInf + (time - tInf) * velocityCoef
+ }
+ return FlingResult(packFloats(distanceCoef, velocityCoef))
+ }
+
+ /**
+ * The rate of deceleration along the spline motion given [velocity] and [friction].
+ */
+ fun deceleration(velocity: Float, friction: Float): Double =
+ ln(Inflection * abs(velocity) / friction.toDouble())
+
+ /**
+ * Result coefficients of a scroll computation
+ */
+ @JvmInline
+ value class FlingResult(private val packedValue: Long) {
+ /**
+ * Linear distance traveled from 0-1, from source (0) to destination (1)
+ */
+ val distanceCoefficient: Float get() = unpackFloat1(packedValue)
+ /**
+ * Instantaneous velocity coefficient at this point in the fling expressed in
+ * total distance per unit time
+ */
+ val velocityCoefficient: Float get() = unpackFloat2(packedValue)
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.kt
index 5fbb82fc..ffc7e85 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.kt
@@ -129,7 +129,8 @@
val horizontal = left + right
val vertical = top + bottom
- val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))
+ val childConstraints = constraints.offset(-horizontal, -vertical)
+ val placeable = measurable.measure(childConstraints)
val width = constraints.constrainWidth(placeable.width + horizontal)
val height = constraints.constrainHeight(placeable.height + vertical)
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index 43e0f74..4b0918f 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -154,13 +154,13 @@
}
public final class ChipKt {
- method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void AssistChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.ChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ChipBorder? border, optional androidx.compose.material3.ChipColors colors);
- method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ElevatedAssistChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.ChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ChipBorder? border, optional androidx.compose.material3.ChipColors colors);
- method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ElevatedFilterChip(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? selectedIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.SelectableChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SelectableChipBorder? border, optional androidx.compose.material3.SelectableChipColors colors);
- method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ElevatedSuggestionChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.ChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ChipBorder? border, optional androidx.compose.material3.ChipColors colors);
- method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void FilterChip(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? selectedIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.SelectableChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SelectableChipBorder? border, optional androidx.compose.material3.SelectableChipColors colors);
- method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void InputChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? avatar, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.ChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ChipBorder? border, optional androidx.compose.material3.ChipColors colors);
- method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SuggestionChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.ChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ChipBorder? border, optional androidx.compose.material3.ChipColors colors);
+ method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void AssistChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.ChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ChipBorder? border, optional androidx.compose.material3.ChipColors colors);
+ method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ElevatedAssistChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.ChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ChipBorder? border, optional androidx.compose.material3.ChipColors colors);
+ method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ElevatedFilterChip(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? selectedIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.SelectableChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SelectableChipBorder? border, optional androidx.compose.material3.SelectableChipColors colors);
+ method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ElevatedSuggestionChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.ChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ChipBorder? border, optional androidx.compose.material3.ChipColors colors);
+ method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void FilterChip(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? selectedIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.SelectableChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SelectableChipBorder? border, optional androidx.compose.material3.SelectableChipColors colors);
+ method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void InputChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? avatar, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.ChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ChipBorder? border, optional androidx.compose.material3.ChipColors colors);
+ method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SuggestionChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.ChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ChipBorder? border, optional androidx.compose.material3.ChipColors colors);
}
@androidx.compose.runtime.Stable public final class ColorScheme {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
index 2bbf677..d7784eb 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
@@ -30,7 +30,6 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
@@ -50,11 +49,10 @@
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
@@ -109,7 +107,7 @@
@Composable
fun AssistChip(
onClick: () -> Unit,
- label: @Composable RowScope.() -> Unit,
+ label: @Composable () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
leadingIcon: @Composable (() -> Unit)? = null,
@@ -152,7 +150,7 @@
* calendar event from the home screen. Assist chips function as though the user asked an assistant
* to complete the action. They should appear dynamically and contextually in a UI.
*
- * ![Assist chip image](https://developer.android.com/images/reference/androidx/compose/material3/assist-chip.png)
+ * ![Assist chip image](https://developer.android.com/images/reference/androidx/compose/material3/elevated-assist-chip.png)
*
* This assist chip is applied with an elevated style. If you want a flat style, use the
* [AssistChip].
@@ -188,7 +186,7 @@
@Composable
fun ElevatedAssistChip(
onClick: () -> Unit,
- label: @Composable RowScope.() -> Unit,
+ label: @Composable () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
leadingIcon: @Composable (() -> Unit)? = null,
@@ -276,7 +274,7 @@
fun FilterChip(
selected: Boolean,
onClick: () -> Unit,
- label: @Composable RowScope.() -> Unit,
+ label: @Composable () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
leadingIcon: @Composable (() -> Unit)? = null,
@@ -315,7 +313,7 @@
* Filter chips use tags or descriptive words to filter content. They can be a good alternative to
* toggle buttons or checkboxes.
*
- * ![Filter chip image](https://developer.android.com/images/reference/androidx/compose/material3/filter-chip.png)
+ * ![Filter chip image](https://developer.android.com/images/reference/androidx/compose/material3/elevated-filter-chip.png)
*
* This filter chip is applied with an elevated style. If you want a flat style, use the
* [FilterChip].
@@ -358,7 +356,7 @@
fun ElevatedFilterChip(
selected: Boolean,
onClick: () -> Unit,
- label: @Composable RowScope.() -> Unit,
+ label: @Composable () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
leadingIcon: @Composable (() -> Unit)? = null,
@@ -442,7 +440,7 @@
@Composable
fun InputChip(
onClick: () -> Unit,
- label: @Composable RowScope.() -> Unit,
+ label: @Composable () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
leadingIcon: @Composable (() -> Unit)? = null,
@@ -459,11 +457,14 @@
var shapedAvatar: @Composable (() -> Unit)? = null
if (avatar != null) {
val avatarOpacity = if (enabled) 1f else InputChipTokens.DisabledAvatarOpacity
+ val avatarShape = InputChipTokens.AvatarShape.toShape()
shapedAvatar = @Composable {
Box(
- modifier = Modifier
- .alpha(avatarOpacity)
- .clip(InputChipTokens.AvatarShape.toShape()),
+ modifier = Modifier.graphicsLayer {
+ this.alpha = avatarOpacity
+ this.shape = avatarShape
+ this.clip = true
+ },
contentAlignment = Alignment.Center
) {
avatar()
@@ -542,7 +543,7 @@
@Composable
fun SuggestionChip(
onClick: () -> Unit,
- label: @Composable RowScope.() -> Unit,
+ label: @Composable () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
icon: @Composable (() -> Unit)? = null,
@@ -583,7 +584,7 @@
* Suggestion chips help narrow a user's intent by presenting dynamically generated suggestions,
* such as possible responses or search filters.
*
- * ![Suggestion chip image](https://developer.android.com/images/reference/androidx/compose/material3/suggestion-chip.png)
+ * ![Suggestion chip image](https://developer.android.com/images/reference/androidx/compose/material3/elevated-suggestion-chip.png)
*
* This suggestion chip is applied with an elevated style. If you want a flat style, use the
* [SuggestionChip].
@@ -620,7 +621,7 @@
@Composable
fun ElevatedSuggestionChip(
onClick: () -> Unit,
- label: @Composable RowScope.() -> Unit,
+ label: @Composable () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
icon: @Composable (() -> Unit)? = null,
@@ -662,8 +663,8 @@
* [interactionSource]. This should typically be the same value as the [shadowElevation].
*
* Tonal elevation is used to apply a color shift to the surface to give the it higher emphasis.
- * When surface's color is [ColorScheme.surface], a higher the elevation will result
- * in a darker color in light theme and lighter color in dark theme.
+ * When surface's color is [ColorScheme.surface], a higher elevation will result in a darker
+ * color in light theme and lighter color in dark theme.
*
* See [shadowElevation] which controls the elevation of the shadow drawn around the Chip.
*
@@ -699,8 +700,8 @@
* [interactionSource]. This should typically be the same value as the [shadowElevation].
*
* Tonal elevation is used to apply a color shift to the surface to give the it higher emphasis.
- * When surface's color is [ColorScheme.surface], a higher the elevation will result
- * in a darker color in light theme and lighter color in dark theme.
+ * When surface's color is [ColorScheme.surface], a higher elevation will result in a darker
+ * color in light theme and lighter color in dark theme.
*
* See [shadowElevation] which controls the elevation of the shadow drawn around the Chip.
*
@@ -854,7 +855,7 @@
* @param selected whether the chip is selected
*/
@Composable
- fun borderStroke(enabled: Boolean, selected: Boolean): State<BorderStroke>
+ fun borderStroke(enabled: Boolean, selected: Boolean): State<BorderStroke?>
}
/**
@@ -1615,7 +1616,7 @@
modifier: Modifier,
onClick: () -> Unit,
enabled: Boolean,
- label: @Composable RowScope.() -> Unit,
+ label: @Composable () -> Unit,
labelTextStyle: TextStyle,
labelColor: Color,
leadingIcon: @Composable (() -> Unit)?,
@@ -1665,7 +1666,7 @@
modifier: Modifier,
onClick: () -> Unit,
enabled: Boolean,
- label: @Composable RowScope.() -> Unit,
+ label: @Composable () -> Unit,
labelTextStyle: TextStyle,
leadingIcon: @Composable (() -> Unit)?,
trailingIcon: @Composable (() -> Unit)?,
@@ -1709,7 +1710,7 @@
@Composable
private fun ChipContent(
- label: @Composable (RowScope.() -> Unit),
+ label: @Composable () -> Unit,
labelTextStyle: TextStyle,
labelColor: Color,
leadingIcon: @Composable (() -> Unit)?,
@@ -2158,7 +2159,7 @@
private val selectedBorderWidth: Dp
) : SelectableChipBorder {
@Composable
- override fun borderStroke(enabled: Boolean, selected: Boolean): State<BorderStroke> {
+ override fun borderStroke(enabled: Boolean, selected: Boolean): State<BorderStroke?> {
val color = if (enabled) {
if (selected) selectedBorderColor else borderColor
} else {
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index a7061ca..53c4c95 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -39,6 +39,7 @@
leakcanary = "2.7"
metalava = "1.0.0-alpha06"
mockito = "2.25.0"
+protobuf = "3.19.4"
skiko = "0.7.7"
sqldelight = "1.3.0"
wire = "3.6.0"
@@ -148,10 +149,10 @@
playServicesBase = { module = "com.google.android.gms:play-services-base", version = "17.0.0" }
playServicesBasement = { module = "com.google.android.gms:play-services-basement", version = "17.0.0" }
playServicesWearable = { module = "com.google.android.gms:play-services-wearable", version = "17.1.0" }
-protobuf = { module = "com.google.protobuf:protobuf-java", version = "3.4.0" }
-protobufCompiler = { module = "com.google.protobuf:protoc", version = "3.19.4" }
+protobuf = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" }
+protobufCompiler = { module = "com.google.protobuf:protoc", version.ref = "protobuf" }
protobufGradlePluginz = { module = "com.google.protobuf:protobuf-gradle-plugin", version = "0.8.18" }
-protobufLite = { module = "com.google.protobuf:protobuf-javalite", version = "3.19.4" }
+protobufLite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobuf" }
reactiveStreams = { module = "org.reactivestreams:reactive-streams", version = "1.0.0" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version = "2.7.2" }
robolectric = { module = "org.robolectric:robolectric", version = "4.7.3" }
diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys
index b069851b..3100613 100644
--- a/gradle/verification-keyring.keys
+++ b/gradle/verification-keyring.keys
@@ -2478,6 +2478,65 @@
-----END PGP PUBLIC KEY BLOCK-----
+pub 3AD93C3C677A106E
+uid Carl Mastrangelo <carl@carlmastrangelo.com>
+
+sub 9B2A1B698A113AAD
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+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
+U/UZPX6m9g==
+=XXe6
+-----END PGP PUBLIC KEY BLOCK-----
+
+
pub 3C0A8F4744F37328
sub D17266C6E05F9993
-----BEGIN PGP PUBLIC KEY BLOCK-----
@@ -5769,6 +5828,53 @@
-----END PGP PUBLIC KEY BLOCK-----
+pub 8D7F1BEC1E2ECAE7
+uid Tatu Saloranta (cowtowncoder) <tatu.saloranta@iki.fi>
+uid Tatu Saloranta <tatu.saloranta@iki.fi>
+
+sub E98008460EB9BB34
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+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
+Jqg9YirnHcrlYF0r
+=gukc
+-----END PGP PUBLIC KEY BLOCK-----
+
+
pub 8E3F0DE7AE354651
sub D3047B0BA4452AE1
-----BEGIN PGP PUBLIC KEY BLOCK-----
@@ -7782,6 +7888,65 @@
-----END PGP PUBLIC KEY BLOCK-----
+pub AC7A514BC9F9BB70
+uid Punyashloka Biswal <punya@google.com>
+
+sub 7B92B768F9D37337
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+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
+sg==
+=w5lW
+-----END PGP PUBLIC KEY BLOCK-----
+
+
pub B0F3710FA64900E7
uid ?amonn McManus <eamonn@mcmanus.net>
@@ -8791,6 +8956,54 @@
-----END PGP PUBLIC KEY BLOCK-----
+pub D041CAD2E452550F
+uid Deanna <deannagarcia@google.com>
+
+sub 5199F3DAE89C332D
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+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
+f8npB1l1fT5cozzQJkPVYzhBWH1KXP5X
+=vnEw
+-----END PGP PUBLIC KEY BLOCK-----
+
+
pub D4DA5EAB3CD7E958
uid Jiaxiang Chen <jiaxiang@google.com>
@@ -9557,6 +9770,70 @@
-----END PGP PUBLIC KEY BLOCK-----
+pub E16AB52D79FD224F
+sub 5A34A5E06B936F93
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+mQENBFF/4bYBCADTeOLZiVGNbjlPrwG7UcMl+yXmEqpf9dB1A9cuicH3PWXj0WOb
+LSzHjzoRvRekEqSUmgoveey1lPuA2qjOUkXY6Kiyx+oLiG0/ObJHUQW2O+tjSQ0R
+ZXKd4ftaw65SLbwYO2JHzj5fLC9j2mZQiRjGs1bWM58c/dOKp1XaOc1/ffcl3L3q
+Up64jWH9r3yhPemh5SHo47UxNvItdaJJYnt20azpZj9oq1ebUuQFMaQDc/RTALhf
+Xb4BWO+z2PCmChz60i/Ko2ZKPJV2TqPqWO+aklgxTTwZZ0IvgFm/5n3Dtn5p5iGf
+qwKkHPJIDWc8cWYtxC608LFdqiAlYmp/oPi5ABEBAAG5AQ0EUX/htgEIALToF36j
+45OitNd4k17BSZJKnuS3uIL3tTw0fRqLv0/3EBaj4zD5Qc5YTKFgM66Bb5ybI63c
+wYhfSBHP2ZRS7oNdDbPd/30jDKNvmcDjIhGLT7bZJwC9SJVifHuvtzr6wBR8xoIt
+yYva5D3ax8ZvnzqIbMPeHou+0ZnRYSPjy2c2TxAJTjDOG461h9mVXDdK74wL8kQs
+IxqqYRIeEdmrXMrd/B8IPwuIv8w7LwzadNgRnXaJ5Q5bnMvvhVLnWKRt5aiQVBxc
+67FTujjqFF4Y/1UJb311K+1LSqNrTT7As8nhf2Gu/Gb47kw1bb7wBdKv2Swx5mYq
+iW5+ARQU7jCiUVkAEQEAAYkBHwQYAQIACQUCUX/htgIbDAAKCRDharUtef0iT2Sy
+CADAznSkG/8EdIU5UQhp/lY9h3WLzYI7aARw0IA6O4ijGLwcytO7TaWjEzUCMZdw
+01vAjVH1xNn9QvTgQV+2GyqyBNsjmgGt5/tK/+JtMgXUwr8+KsBf3908rOqAAZ3Y
+GyM9N8sRsyfPB/PHfv289sL2IKPxiFTGI0NGS3qOAKQ5TZvV7OPsP5+yHfeJG/Xh
+CW8p+nkMGpH4rE8Z6NKgLe/WC6J36aQ4kBfYneueH90Dc400rfGyL+0Gn1Rzuj2K
+FuUFK6q/GBlFaNo0azCqtdpcO6C3GpJYtISxpQ1Rp9kSEzSCL3tOli8Xs6gsruc+
+vCSIy8lzRw19ZO9G7qhjcHLc
+=vO0K
+-----END PGP PUBLIC KEY BLOCK-----
+
+
+pub E16AB52D79FD224F
+uid Google Api Client Libraries (Releases) <google-api-client-libraries@google.com>
+
+sub 5A34A5E06B936F93
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+mQENBFF/4bYBCADTeOLZiVGNbjlPrwG7UcMl+yXmEqpf9dB1A9cuicH3PWXj0WOb
+LSzHjzoRvRekEqSUmgoveey1lPuA2qjOUkXY6Kiyx+oLiG0/ObJHUQW2O+tjSQ0R
+ZXKd4ftaw65SLbwYO2JHzj5fLC9j2mZQiRjGs1bWM58c/dOKp1XaOc1/ffcl3L3q
+Up64jWH9r3yhPemh5SHo47UxNvItdaJJYnt20azpZj9oq1ebUuQFMaQDc/RTALhf
+Xb4BWO+z2PCmChz60i/Ko2ZKPJV2TqPqWO+aklgxTTwZZ0IvgFm/5n3Dtn5p5iGf
+qwKkHPJIDWc8cWYtxC608LFdqiAlYmp/oPi5ABEBAAG0T0dvb2dsZSBBcGkgQ2xp
+ZW50IExpYnJhcmllcyAoUmVsZWFzZXMpIDxnb29nbGUtYXBpLWNsaWVudC1saWJy
+YXJpZXNAZ29vZ2xlLmNvbT6JATgEEwECACIFAlF/4bYCGwMGCwkIBwMCBhUIAgkK
+CwQWAgMBAh4BAheAAAoJEOFqtS15/SJP6NYIAMbwdE5S9M/5tIhLPg2arbvnxfh2
+i1e2aKE6PcRlBGeNq8YzWQStIewRhoDbY4MY3dG0nX9wgXU6XFJNxjyjE5Gqpyrc
+xEhs6r+HfxlGwB/OCaDMGR89lcWn1xF7uju3Qsdkb6o2JuCSAqOh4M1zIqADSMMq
+OjuSJ13GddcUFQ/MEvouE7Leesls9AM724BNZfnyJjIfJqfk38rXCxKpMbxCJ2v2
+louDMu0LClA1efdvDBPOrc8+A74dsVh7cQqMoe5lCqrcoC5apMqlJ71UV7SIBNBr
+7+AD6p0ZdGY8C/pTCl+NGe/Skjdqk7uEbmyePpm4BaJTmbwIgiZdGxthUgO5AQ0E
+UX/htgEIALToF36j45OitNd4k17BSZJKnuS3uIL3tTw0fRqLv0/3EBaj4zD5Qc5Y
+TKFgM66Bb5ybI63cwYhfSBHP2ZRS7oNdDbPd/30jDKNvmcDjIhGLT7bZJwC9SJVi
+fHuvtzr6wBR8xoItyYva5D3ax8ZvnzqIbMPeHou+0ZnRYSPjy2c2TxAJTjDOG461
+h9mVXDdK74wL8kQsIxqqYRIeEdmrXMrd/B8IPwuIv8w7LwzadNgRnXaJ5Q5bnMvv
+hVLnWKRt5aiQVBxc67FTujjqFF4Y/1UJb311K+1LSqNrTT7As8nhf2Gu/Gb47kw1
+bb7wBdKv2Swx5mYqiW5+ARQU7jCiUVkAEQEAAYkBHwQYAQIACQUCUX/htgIbDAAK
+CRDharUtef0iT2SyCADAznSkG/8EdIU5UQhp/lY9h3WLzYI7aARw0IA6O4ijGLwc
+ytO7TaWjEzUCMZdw01vAjVH1xNn9QvTgQV+2GyqyBNsjmgGt5/tK/+JtMgXUwr8+
+KsBf3908rOqAAZ3YGyM9N8sRsyfPB/PHfv289sL2IKPxiFTGI0NGS3qOAKQ5TZvV
+7OPsP5+yHfeJG/XhCW8p+nkMGpH4rE8Z6NKgLe/WC6J36aQ4kBfYneueH90Dc400
+rfGyL+0Gn1Rzuj2KFuUFK6q/GBlFaNo0azCqtdpcO6C3GpJYtISxpQ1Rp9kSEzSC
+L3tOli8Xs6gsruc+vCSIy8lzRw19ZO9G7qhjcHLc
+=JOMJ
+-----END PGP PUBLIC KEY BLOCK-----
+
+
pub E3822B59020A349D
sub 9351716690874F25
sub 60EB70DDAAC2EC21
@@ -10146,132 +10423,3 @@
Pt2uco8an9pO9/oqU6vlZUr38w==
=alQS
-----END PGP PUBLIC KEY BLOCK-----
-
-pub D041CAD2E452550F
------BEGIN PGP PUBLIC KEY BLOCK-----
-Comment: Hostname:
-Version: Hockeypuck ~unreleased
-
-xsDNBGCtdhoBDADdopjDt4eUNEqLJSw1ZICSR0oq09SOVtJSaSYdF8UiXjBfL1Ds
-fhTDqSv5pT2a2gLj0OU3tFhWHvINLaKKCjQnHVcFXi2LTxt+XBOjRYkFjHVisbaZ
-PZ6HnTMStPrvs+hQ168vU3VfYOsOLN22j53I/Ba+FA7E0G0bqkratuT5L7BTR1mC
-fqDaeisWSCllfe6EEysaFF+/1RcRy+Yt+8ZWV0FZEF7UwQvqKHcYmlkqPIn3v/8y
-J/yvmzIEtCQ1F+bvJbzaROmeJf254G2Uh7IfMYEm9WlqnGwNdbIhil7bdxq8Y/0H
-XbQPaESxkki7yL5JTfH/+UzdklMe+Dga273L/cgzfjV3zJJ9vR94W5ABAbGYh4ZW
-aKvNnT1m4vTbEMfo4r3NF2zc+K9Ly/JNaHqkR5M4SVElvN2lsC5KNUiRvExhg+h0
-mKyx61mu3gUIrC1UOmqhtx7RzQQf7ESMdzmNHY0P93lR0Ic10fyli0wfl7A6q7+q
-zV2a1V2k9Yg6B9sAEQEAAc0gRGVhbm5hIDxkZWFubmFnYXJjaWFAZ29vZ2xlLmNv
-bT7CwRQEEwEKAD4WIQRpa2GZoqnYwpznjMDQQcrS5FJVDwUCYK12GgIbAwUJA8Jn
-AAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRDQQcrS5FJVD51GC/4oTue5F8jx
-oeMyDzoZdeiEnEFvrTU80/9y1CrlSIX0HdD5m9nENQOfDjSv9RrHC16WfJyQtKYs
-ADujaZGf2Da4zl2XZ29zeowQK7nzlh2y2bWz7eb9i+nzRApvM07AvSe1ILgFXy1S
-MEMZTnE7ceEosF1hbP/yRDT83ylhEA6ncFGR42WwHHLZMBjBdehl+LxBIkKJyHKb
-M/T2dBGEOBEjSxqDD9jCaRG4n9yq1TWY3iQD2EuxpfnEYeGLZ68E3ipctlCv/nLd
-e3V1se0bvRdx/FlpF7GTZlBcCpmOxTAUon8O8rpgktdoEkz4hknl2cVod4Preax8
-b3jMyf9GFMdl+b0xU3ho3IyfiMMtez02lNTjDJA43R5hLGLgZPV6crHeukbMmQcm
-l4ckUkABFBMuwxn+TT5CEaDG4UoNmOS3n8vDM31G+FcJYJHeNAYNXj5ZW4T38rX3
-47Hz+R9ZAiHrypAPZXGnAizzEQ+bdAqge3Ye6KA/krv2A9rP272UiBTOwM0EYK12
-GgEMAMgP3//QeBsTS3IrfSp3m44el96X6BWona2yo4DvVyuwqfULZE+Nhj7I+kEZ
-LrA29AOySOD/6quJ4MIJZfq/Do920Di8/10WQ00OdCM1wH7bMz2UvcSqsr0iOgQt
-ycuUf7JOHSTME9vqk+C3Lhn0r59AVaRdXEe6zBgNZyzZJeCr5F8wRhglPlwvhOGs
-2aLEqlCxFnY4pLayQFoQyw1lDjHIXHg5JtfOHvqiNXVDcGpyKLG8SzImp62iL4sf
-uA0weVIQeS9kZiQabSYKvSf3TvNXYTgmFz/vjPbYhv9LTkBroTlVg3l+UmAxLrHV
-uXMx0zX3jfNNHAqUjVhPYZhnifMkmGJgLeMIVqr5Q/tx8pzyYiiOcqQ1zDg8ubJD
-GRue1JjlUGdw19OvhFDs+lydukt8Mmhb0gPkBLi2syZHgYHtEooXPLwEsJ+SynZC
-FhZiWj8BsWNFJpaDd8ynNeWhMAcwi3B5ZeQiZaAlV0sItxsrzvbu4ZYZtkjAkQds
-aaTWSwARAQABwsD8BBgBCgAmFiEEaWthmaKp2MKc54zA0EHK0uRSVQ8FAmCtdhoC
-GwwFCQPCZwAACgkQ0EHK0uRSVQ+G7wwAvaVPDgnM+i2pGQPwq6MkSzhKEG4H1pvB
-WyYR8H9D3p/dE33IjVu3EEy1h37Nzdyp46KtASGNe3KBodSsh6gvPlV5pNGxMNbX
-6fo8ZGtS83C+6uTF1cYmuO1nmi8P4+7qtcNZg4xv/ujAZIC20kemYKDth3FvPxEX
-soxY+Ns7sxgd3SqoyLhjcyoczI8uyhim5nfvvbnEd6WrdiBPBtb/F1h/nfqdFj2T
-cZkAlnzGnlVlgU8J60u6zE+9VvBm0lJR73Ar55mQEwarGFPL1a3/A7ZEeNa0Dc3O
-a5sKMYtxMlGKZ0WGUoGcDWiaDEsv5YyRnaSOaXKM1NkJCR013QArRcHrRBPo+0/R
-IZVE+b8oEcmGzdL8HNwnm7e06ruZryF9LQA5YBmCKE0urigmgEvCzZsj/fMJ+OIZ
-cAhE7UVae48GpW2kLATxmK01oSzvizIlmN3rVz2EnjOun2iuuEpF/lmDbjK5n1r3
-f8npB1l1fT5cozzQJkPVYzhBWH1KXP5X
-=qJgQ
------END PGP PUBLIC KEY BLOCK-----
-
-pub E16AB52D79FD224F
-sub 5A34A5E06B936F93
------BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.68
-
-mQENBFF/4bYBCADTeOLZiVGNbjlPrwG7UcMl+yXmEqpf9dB1A9cuicH3PWXj0WOb
-LSzHjzoRvRekEqSUmgoveey1lPuA2qjOUkXY6Kiyx+oLiG0/ObJHUQW2O+tjSQ0R
-ZXKd4ftaw65SLbwYO2JHzj5fLC9j2mZQiRjGs1bWM58c/dOKp1XaOc1/ffcl3L3q
-Up64jWH9r3yhPemh5SHo47UxNvItdaJJYnt20azpZj9oq1ebUuQFMaQDc/RTALhf
-Xb4BWO+z2PCmChz60i/Ko2ZKPJV2TqPqWO+aklgxTTwZZ0IvgFm/5n3Dtn5p5iGf
-qwKkHPJIDWc8cWYtxC608LFdqiAlYmp/oPi5ABEBAAG5AQ0EUX/htgEIALToF36j
-45OitNd4k17BSZJKnuS3uIL3tTw0fRqLv0/3EBaj4zD5Qc5YTKFgM66Bb5ybI63c
-wYhfSBHP2ZRS7oNdDbPd/30jDKNvmcDjIhGLT7bZJwC9SJVifHuvtzr6wBR8xoIt
-yYva5D3ax8ZvnzqIbMPeHou+0ZnRYSPjy2c2TxAJTjDOG461h9mVXDdK74wL8kQs
-IxqqYRIeEdmrXMrd/B8IPwuIv8w7LwzadNgRnXaJ5Q5bnMvvhVLnWKRt5aiQVBxc
-67FTujjqFF4Y/1UJb311K+1LSqNrTT7As8nhf2Gu/Gb47kw1bb7wBdKv2Swx5mYq
-iW5+ARQU7jCiUVkAEQEAAYkBHwQYAQIACQUCUX/htgIbDAAKCRDharUtef0iT2Sy
-CADAznSkG/8EdIU5UQhp/lY9h3WLzYI7aARw0IA6O4ijGLwcytO7TaWjEzUCMZdw
-01vAjVH1xNn9QvTgQV+2GyqyBNsjmgGt5/tK/+JtMgXUwr8+KsBf3908rOqAAZ3Y
-GyM9N8sRsyfPB/PHfv289sL2IKPxiFTGI0NGS3qOAKQ5TZvV7OPsP5+yHfeJG/Xh
-CW8p+nkMGpH4rE8Z6NKgLe/WC6J36aQ4kBfYneueH90Dc400rfGyL+0Gn1Rzuj2K
-FuUFK6q/GBlFaNo0azCqtdpcO6C3GpJYtISxpQ1Rp9kSEzSCL3tOli8Xs6gsruc+
-vCSIy8lzRw19ZO9G7qhjcHLc
-=vO0K
------END PGP PUBLIC KEY BLOCK-----
-
-pub 3AD93C3C677A106E
-uid Carl Mastrangelo <carl@carlmastrangelo.com>
-
-sub 9B2A1B698A113AAD
------BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.68
-
-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
-U/UZPX6m9g==
-=XXe6
------END PGP PUBLIC KEY BLOCK-----
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 869f77d..be71621 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -57,9 +57,7 @@
<trusted-key id="160a7a9cf46221a56b06ad64461a804f2609fd89" group="com.github.shyiko.klob" name="klob"/>
<trusted-key id="1861c322c56014b2" group="commons-lang"/>
<trusted-key id="190d5a957ff22273e601f7a7c92c5fec70161c62" group="org.codehaus.mojo"/>
- <trusted-key id="19beab2d799c020f17c69126b16698a4adf4d638">
- <trusting group="org.checkerframework"/>
- </trusted-key>
+ <trusted-key id="19beab2d799c020f17c69126b16698a4adf4d638" group="org.checkerframework"/>
<trusted-key id="1b2718089ce964b8" group="com.thoughtworks.qdox"/>
<trusted-key id="1bc86444bbd2a24c3a40904a438e9634a2319637" group="co.nstant.in" name="cbor"/>
<trusted-key id="1d0a8b5e77c678a7c724445abf984b4145ea13f7" group="com.squareup"/>
@@ -135,7 +133,16 @@
<trusted-key id="438e9634a2319637" group="co.nstant.in"/>
<trusted-key id="44ce7bf2825ea2cd" group="com.ibm.icu"/>
<trusted-key id="461a804f2609fd89" group="com.github.shyiko.klob"/>
- <trusted-key id="47504b76cf89c15c0512d9afe16ab52d79fd224f" group="com.google.api.grpc"/>
+ <trusted-key id="47504b76cf89c15c0512d9afe16ab52d79fd224f">
+ <trusting group="com.google.api"/>
+ <trusting group="com.google.api-client"/>
+ <trusting group="com.google.api.grpc"/>
+ <trusting group="com.google.apis"/>
+ <trusting group="com.google.auth"/>
+ <trusting group="com.google.cloud"/>
+ <trusting group="com.google.http-client"/>
+ <trusting group="com.google.oauth-client"/>
+ </trusted-key>
<trusted-key id="47586a1b75ef0de5" group="com.squareup.wire"/>
<trusted-key id="475f3b8e59e6e63aa78067482c7b12f2a511e325" group="org.slf4j"/>
<trusted-key id="476634a4694e716a" group="com.googlecode.java-diff-utils"/>
@@ -176,6 +183,7 @@
<trusted-key id="5ed22f661bbf0acc" group="com.almworks.sqlite4java"/>
<trusted-key id="5f7786df73e61f56" group="com.google.devtools.ksp"/>
<trusted-key id="5fa41c402006eac55d72aafd99ce9d9f22dc5c99" group="org.json"/>
+ <trusted-key id="600ea202b1ec682f4a788e5aac7a514bc9f9bb70" group="io.opencensus"/>
<trusted-key id="6214760097dc5cfad0175ac2c9fbaa83a8753994">
<trusting group="com.fasterxml"/>
<trusting group="com.fasterxml.jackson"/>
@@ -198,6 +206,7 @@
<trusting group="com.google.jimfs"/>
<trusting group="^com[.]google($|([.].*))" regex="true"/>
</trusted-key>
+ <trusted-key id="696b6199a2a9d8c29ce78cc0d041cad2e452550f" group="com.google.protobuf"/>
<trusted-key id="6a65176a0fb1cd0b" group="org.codehaus.groovy"/>
<trusted-key id="6ccc36cc6c69fc17" group="com.google"/>
<trusted-key id="6dd3b8c64ef75253beb2c53ad908a43fb7ec07ac">
@@ -252,6 +261,11 @@
</trusted-key>
<trusted-key id="8858d45be9b276802318155b96fb9db219f3338d" group="kr.motd.maven"/>
<trusted-key id="88bb19a33a18445f" group="net.ltgt.gradle.incap"/>
+ <trusted-key id="8a10792983023d5d14c93b488d7f1bec1e2ecae7">
+ <trusting group="com.fasterxml"/>
+ <trusting group="com.fasterxml.jackson"/>
+ <trusting group="com.fasterxml.jackson.core"/>
+ </trusted-key>
<trusted-key id="8f9a3c6d105b9f57844a721d79e193516be7998f" group="org.dom4j" name="dom4j"/>
<trusted-key id="908366594e746bf3c449f5622be5d98f751f4136" group="org.pcollections" name="pcollections"/>
<trusted-key id="90ee19787a7bcf6fd37a1e9180c08b1c29100955">
@@ -310,7 +324,10 @@
<trusted-key id="afcc4c7594d09e2182c60e0f7a01b0f236e5430f" group="com.google.code.gson"/>
<trusted-key id="b02335aa54ccf21e52bbf9abd9c565aa72ba2fdd" group="io.grpc"/>
<trusted-key id="b252e5789636134a311e4463971b04f56669b805" group="com.google.jsilver" name="jsilver"/>
- <trusted-key id="b41089a2da79b0fa5810252872385ff0af338d52" group="org.threeten" name="threeten-extra"/>
+ <trusted-key id="b41089a2da79b0fa5810252872385ff0af338d52">
+ <trusting group="org.threeten" name="threeten-extra"/>
+ <trusting group="org.threeten" name="threetenbp"/>
+ </trusted-key>
<trusted-key id="b47034c19c9b1f3dc3702f8d476634a4694e716a" group="com.googlecode.java-diff-utils" name="diffutils"/>
<trusted-key id="b4c70893b62babe8" group="org.apache.logging.log4j"/>
<trusted-key id="b57bd58ef6d0a713" group="com.google.protobuf"/>
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/datatype/RecordsTypeNameMap.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/datatype/RecordsTypeNameMap.kt
index 0882ee3..789386b 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/datatype/RecordsTypeNameMap.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/datatype/RecordsTypeNameMap.kt
@@ -16,6 +16,7 @@
package androidx.health.connect.client.impl.converters.datatype
import androidx.health.connect.client.records.ActiveCaloriesBurned
+import androidx.health.connect.client.records.ActiveEnergyBurned
import androidx.health.connect.client.records.ActivityEvent
import androidx.health.connect.client.records.ActivityLap
import androidx.health.connect.client.records.ActivitySession
@@ -74,6 +75,7 @@
private val ALL_RECORDS_TYPES =
setOf(
ActiveCaloriesBurned::class,
+ ActiveEnergyBurned::class,
ActivityEvent::class,
ActivityLap::class,
ActivitySession::class,
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
index e1af71d..4eb318e 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
@@ -16,6 +16,7 @@
package androidx.health.connect.client.impl.converters.records
import androidx.health.connect.client.records.ActiveCaloriesBurned
+import androidx.health.connect.client.records.ActiveEnergyBurned
import androidx.health.connect.client.records.ActivityEvent
import androidx.health.connect.client.records.ActivityLap
import androidx.health.connect.client.records.ActivitySession
@@ -358,6 +359,15 @@
endZoneOffset = endZoneOffset,
metadata = metadata
)
+ "ActiveEnergyBurned" ->
+ ActiveEnergyBurned(
+ energyKcal = getDouble("energy"),
+ startTime = startTime,
+ startZoneOffset = startZoneOffset,
+ endTime = endTime,
+ endZoneOffset = endZoneOffset,
+ metadata = metadata
+ )
"ActivityEvent" ->
ActivityEvent(
eventType = getEnum("eventType") ?: "",
@@ -478,6 +488,7 @@
"Repetitions" ->
Repetitions(
count = getLong("count"),
+ type = getEnum("type") ?: "",
startTime = startTime,
startZoneOffset = startZoneOffset,
endTime = endTime,
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt
index eb883bb..02b3c01 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt
@@ -16,6 +16,7 @@
package androidx.health.connect.client.impl.converters.records
import androidx.health.connect.client.records.ActiveCaloriesBurned
+import androidx.health.connect.client.records.ActiveEnergyBurned
import androidx.health.connect.client.records.ActivityEvent
import androidx.health.connect.client.records.ActivityLap
import androidx.health.connect.client.records.ActivitySession
@@ -288,6 +289,11 @@
.setDataType(protoDataType("ActiveCaloriesBurned"))
.apply { putValues("energy", doubleVal(energyKcal)) }
.build()
+ is ActiveEnergyBurned ->
+ intervalProto()
+ .setDataType(protoDataType("ActiveEnergyBurned"))
+ .apply { putValues("energy", doubleVal(energyKcal)) }
+ .build()
is ActivityEvent ->
intervalProto()
.setDataType(protoDataType("ActivityEvent"))
@@ -468,7 +474,10 @@
is Repetitions ->
intervalProto()
.setDataType(protoDataType("Repetitions"))
- .apply { putValues("count", longVal(count)) }
+ .apply {
+ putValues("count", longVal(count))
+ putValues("type", enumVal(type))
+ }
.build()
is SleepSession ->
intervalProto()
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/ActiveEnergyBurned.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/ActiveEnergyBurned.kt
new file mode 100644
index 0000000..20bd09a
--- /dev/null
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/ActiveEnergyBurned.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.health.connect.client.records
+
+import androidx.annotation.RestrictTo
+import androidx.health.connect.client.metadata.Metadata
+import java.time.Instant
+import java.time.ZoneOffset
+
+/**
+ * Captures the estimated active energy burned by the user (in kilocalories), excluding basal
+ * metabolic rate (BMR). Each record represents the total kilocalories burned over a time interval,
+ * so both the start and end times should be set.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class ActiveEnergyBurned(
+ /** Energy in kilocalories. Required field. Valid range: 0-1000000. */
+ public val energyKcal: Double,
+ override val startTime: Instant,
+ override val startZoneOffset: ZoneOffset?,
+ override val endTime: Instant,
+ override val endZoneOffset: ZoneOffset?,
+ override val metadata: Metadata = Metadata.EMPTY,
+) : IntervalRecord {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is ActiveEnergyBurned) return false
+
+ if (energyKcal != other.energyKcal) return false
+ if (startTime != other.startTime) return false
+ if (startZoneOffset != other.startZoneOffset) return false
+ if (endTime != other.endTime) return false
+ if (endZoneOffset != other.endZoneOffset) return false
+ if (metadata != other.metadata) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = 0
+ result = 31 * result + energyKcal.hashCode()
+ result = 31 * result + (startZoneOffset?.hashCode() ?: 0)
+ result = 31 * result + endTime.hashCode()
+ result = 31 * result + (endZoneOffset?.hashCode() ?: 0)
+ result = 31 * result + metadata.hashCode()
+ return result
+ }
+}
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/RepetitionActivityTypes.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/RepetitionActivityTypes.kt
new file mode 100644
index 0000000..0e75226
--- /dev/null
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/RepetitionActivityTypes.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.health.connect.client.records
+
+import androidx.annotation.RestrictTo
+import androidx.annotation.StringDef
+
+/** Activity types supported by repetitions. */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public object RepetitionActivityTypes {
+ const val ARM_CURL = "arm_curl"
+ const val BACK_EXTENSION = "back_extension"
+ const val BALL_SLAM = "ball_slam"
+ const val BENCH_PRESS = "bench_press"
+ const val BURPEE = "burpee"
+ const val CRUNCH = "crunch"
+ const val DEADLIFT = "deadlift"
+ const val DOUBLE_ARM_TRICEPS_EXTENSION = "double_arm_triceps_extension"
+ const val DUMBBELL_ROW = "dumbbell_row"
+ const val FRONT_RAISE = "front_raise"
+ const val HIP_THRUST = "hip_thrust"
+ const val HULA_HOOP = "hula_hoop"
+ const val JUMPING_JACK = "jumping_jack"
+ const val JUMP_ROPE = "jump_rope"
+ const val KETTLEBELL_SWING = "kettlebell_swing"
+ const val LATERAL_RAISE = "lateral_raise"
+ const val LAT_PULL_DOWN = "lat_pull_down"
+ const val LEG_CURL = "leg_curl"
+ const val LEG_EXTENSION = "leg_extension"
+ const val LEG_PRESS = "leg_press"
+ const val LEG_RAISE = "leg_raise"
+ const val LUNGE = "lunge"
+ const val MOUNTAIN_CLIMBER = "mountain_climber"
+ const val PLANK = "plank"
+ const val PULL_UP = "pull_up"
+ const val PUNCH = "punch"
+ const val SHOULDER_PRESS = "shoulder_press"
+ const val SINGLE_ARM_TRICEPS_EXTENSION = "single_arm_triceps_extension"
+ const val SIT_UP = "sit_up"
+ const val SQUAT = "squat"
+}
+
+/**
+ * Activity types supported by repetitions.
+ * @suppress
+ */
+@Retention(AnnotationRetention.SOURCE)
+@StringDef(
+ value =
+ [
+ RepetitionActivityTypes.ARM_CURL,
+ RepetitionActivityTypes.BACK_EXTENSION,
+ RepetitionActivityTypes.BALL_SLAM,
+ RepetitionActivityTypes.BENCH_PRESS,
+ RepetitionActivityTypes.BURPEE,
+ RepetitionActivityTypes.CRUNCH,
+ RepetitionActivityTypes.DEADLIFT,
+ RepetitionActivityTypes.DOUBLE_ARM_TRICEPS_EXTENSION,
+ RepetitionActivityTypes.DUMBBELL_ROW,
+ RepetitionActivityTypes.FRONT_RAISE,
+ RepetitionActivityTypes.HIP_THRUST,
+ RepetitionActivityTypes.HULA_HOOP,
+ RepetitionActivityTypes.JUMPING_JACK,
+ RepetitionActivityTypes.JUMP_ROPE,
+ RepetitionActivityTypes.KETTLEBELL_SWING,
+ RepetitionActivityTypes.LATERAL_RAISE,
+ RepetitionActivityTypes.LAT_PULL_DOWN,
+ RepetitionActivityTypes.LEG_CURL,
+ RepetitionActivityTypes.LEG_EXTENSION,
+ RepetitionActivityTypes.LEG_PRESS,
+ RepetitionActivityTypes.LEG_RAISE,
+ RepetitionActivityTypes.LUNGE,
+ RepetitionActivityTypes.MOUNTAIN_CLIMBER,
+ RepetitionActivityTypes.PLANK,
+ RepetitionActivityTypes.PULL_UP,
+ RepetitionActivityTypes.PUNCH,
+ RepetitionActivityTypes.SHOULDER_PRESS,
+ RepetitionActivityTypes.SINGLE_ARM_TRICEPS_EXTENSION,
+ RepetitionActivityTypes.SIT_UP,
+ RepetitionActivityTypes.SQUAT]
+)
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+annotation class RepetitionActivityType
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/Repetitions.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/Repetitions.kt
index da712fd..c7309f5 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/Repetitions.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/Repetitions.kt
@@ -25,6 +25,10 @@
public class Repetitions(
/** Count. Required field. Valid range: 1-1000000. */
public val count: Long,
+ /**
+ * Type of activity being repeated. Optional field. Allowed values: [RepetitionActivityType].
+ */
+ @property:RepetitionActivityType public val type: String,
override val startTime: Instant,
override val startZoneOffset: ZoneOffset?,
override val endTime: Instant,
@@ -36,6 +40,7 @@
if (other !is Repetitions) return false
if (count != other.count) return false
+ if (type != other.type) return false
if (startTime != other.startTime) return false
if (startZoneOffset != other.startZoneOffset) return false
if (endTime != other.endTime) return false
@@ -48,6 +53,7 @@
override fun hashCode(): Int {
var result = 0
result = 31 * result + count.hashCode()
+ result = 31 * result + type.hashCode()
result = 31 * result + (startZoneOffset?.hashCode() ?: 0)
result = 31 * result + endTime.hashCode()
result = 31 * result + (endZoneOffset?.hashCode() ?: 0)
diff --git a/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt b/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt
index c9adb81..fdd5da4 100644
--- a/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt
+++ b/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt
@@ -31,6 +31,7 @@
import androidx.health.connect.client.metadata.Device
import androidx.health.connect.client.metadata.Metadata
import androidx.health.connect.client.permission.Permission
+import androidx.health.connect.client.permission.Permission.Companion.createReadPermission
import androidx.health.connect.client.records.ActiveCaloriesBurned
import androidx.health.connect.client.records.Nutrition
import androidx.health.connect.client.records.Steps
@@ -182,9 +183,7 @@
@Test
fun getGrantedPermissions_none() = runTest {
val deferred = async {
- healthConnectClient.getGrantedPermissions(
- setOf(Permission.createReadPermission(Steps::class))
- )
+ healthConnectClient.getGrantedPermissions(setOf(createReadPermission(Steps::class)))
}
advanceUntilIdle()
@@ -205,16 +204,14 @@
)
)
val deferred = async {
- healthConnectClient.getGrantedPermissions(
- setOf(Permission.createReadPermission(Steps::class))
- )
+ healthConnectClient.getGrantedPermissions(setOf(createReadPermission(Steps::class)))
}
advanceUntilIdle()
waitForMainLooperIdle()
val response = deferred.await()
- assertThat(response).containsExactly(Permission.createReadPermission(Steps::class))
+ assertThat(response).containsExactly(createReadPermission(Steps::class))
}
@Test
diff --git a/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt b/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
index 24f4b37..a487acf 100644
--- a/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
+++ b/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
@@ -19,6 +19,7 @@
import androidx.health.connect.client.metadata.Device
import androidx.health.connect.client.metadata.Metadata
import androidx.health.connect.client.records.ActiveCaloriesBurned
+import androidx.health.connect.client.records.ActiveEnergyBurned
import androidx.health.connect.client.records.ActivityEvent
import androidx.health.connect.client.records.ActivityEventTypes
import androidx.health.connect.client.records.ActivityLap
@@ -63,6 +64,7 @@
import androidx.health.connect.client.records.OvulationTestResults
import androidx.health.connect.client.records.OxygenSaturation
import androidx.health.connect.client.records.Power
+import androidx.health.connect.client.records.RepetitionActivityTypes
import androidx.health.connect.client.records.Repetitions
import androidx.health.connect.client.records.RespiratoryRate
import androidx.health.connect.client.records.RestingHeartRate
@@ -631,6 +633,21 @@
}
@Test
+ fun testActiveEnergyBurned() {
+ val data =
+ ActiveEnergyBurned(
+ energyKcal = 1.0,
+ startTime = START_TIME,
+ startZoneOffset = START_ZONE_OFFSET,
+ endTime = END_TIME,
+ endZoneOffset = END_ZONE_OFFSET,
+ metadata = TEST_METADATA
+ )
+
+ assertThat(toRecord(data.toProto())).isEqualTo(data)
+ }
+
+ @Test
fun testActivityEvent() {
val data =
ActivityEvent(
@@ -800,6 +817,7 @@
val data =
Repetitions(
count = 1,
+ type = RepetitionActivityTypes.JUMPING_JACK,
startTime = START_TIME,
startZoneOffset = START_ZONE_OFFSET,
endTime = END_TIME,
diff --git a/health/health-services-client/build.gradle b/health/health-services-client/build.gradle
index cb61e51..467e4aa 100644
--- a/health/health-services-client/build.gradle
+++ b/health/health-services-client/build.gradle
@@ -30,7 +30,7 @@
implementation(libs.guavaListenableFuture)
implementation(libs.guavaAndroid)
implementation("androidx.core:core-ktx:1.5.0-alpha04")
- implementation "com.google.protobuf:protobuf-javalite:3.10.0"
+ implementation(libs.protobufLite)
}
android {
@@ -45,7 +45,7 @@
protobuf {
protoc {
- artifact = "com.google.protobuf:protoc:3.10.0"
+ artifact = libs.protobufCompiler.get()
}
// Generates the java proto-lite code for the protos in this project. See
diff --git a/libraryversions.toml b/libraryversions.toml
index cfe4eac..bb41f7d 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -1,5 +1,5 @@
[versions]
-ACTIVITY = "1.5.0-beta01"
+ACTIVITY = "1.5.0-rc01"
ADS_IDENTIFIER = "1.0.0-alpha05"
ANNOTATION = "1.4.0-alpha03"
ANNOTATION_EXPERIMENTAL = "1.3.0-alpha01"
@@ -22,7 +22,7 @@
COMPOSE_MATERIAL3 = "1.0.0-alpha10"
CONTENTPAGER = "1.1.0-alpha01"
COORDINATORLAYOUT = "1.3.0-alpha01"
-CORE = "1.8.0-beta01"
+CORE = "1.8.0-rc01"
CORE_ANIMATION = "1.0.0-beta01"
CORE_ANIMATION_TESTING = "1.0.0-beta01"
CORE_APPDIGEST = "1.0.0-alpha01"
@@ -67,7 +67,7 @@
LEANBACK_TAB = "1.1.0-beta01"
LEGACY = "1.1.0-alpha01"
LIBYUV = "0.1.0-dev01"
-LIFECYCLE = "2.5.0-beta01"
+LIFECYCLE = "2.5.0-rc01"
LIFECYCLE_EXTENSIONS = "2.2.0"
LOADER = "1.2.0-alpha01"
MEDIA = "1.7.0-alpha01"
@@ -88,7 +88,7 @@
REMOTECALLBACK = "1.0.0-alpha02"
RESOURCEINSPECTION = "1.1.0-alpha01"
ROOM = "2.5.0-alpha02"
-SAVEDSTATE = "1.2.0-beta01"
+SAVEDSTATE = "1.2.0-rc01"
SECURITY = "1.1.0-alpha04"
SECURITY_APP_AUTHENTICATOR = "1.0.0-alpha03"
SECURITY_APP_AUTHENTICATOR_TESTING = "1.0.0-alpha02"
diff --git a/room/room-compiler-processing/build.gradle b/room/room-compiler-processing/build.gradle
index 2024abe..075cb85 100644
--- a/room/room-compiler-processing/build.gradle
+++ b/room/room-compiler-processing/build.gradle
@@ -47,6 +47,7 @@
tasks.withType(KotlinCompile).configureEach {
kotlinOptions {
freeCompilerArgs += [
+ "-Xjvm-default=all",
"-opt-in=kotlin.RequiresOptIn",
"-opt-in=kotlin.contracts.ExperimentalContracts",
"-opt-in=androidx.room.compiler.processing.ExperimentalProcessingApi"
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/JavaImplProcessingStep.java b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/JavaImplProcessingStep.java
new file mode 100644
index 0000000..f613a8f
--- /dev/null
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/JavaImplProcessingStep.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.compiler.processing;
+
+import androidx.annotation.NonNull;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Not a JUnit test but a test nonetheless, it verifies that a Java source implementation of
+ * XProcessingStep does not need to override interface methods with a body since Kotlin should
+ * generate a default method, i.e. it verifies Kotlin's jvm-default is turned ON.
+ */
+@SuppressWarnings("deprecation") // On purpose overriding deprecated method.
+public class JavaImplProcessingStep implements XProcessingStep {
+ @NonNull
+ @Override
+ public Set<XElement> process(@NonNull XProcessingEnv env,
+ @NonNull Map<String, ? extends Set<? extends XElement>> elementsByAnnotation) {
+ return XProcessingStep.super.process(env, elementsByAnnotation);
+ }
+
+ @Override
+ public void processOver(@NonNull XProcessingEnv env,
+ @NonNull Map<String, ? extends Set<? extends XElement>> elementsByAnnotation) {
+ XProcessingStep.super.processOver(env, elementsByAnnotation);
+ }
+
+ @NonNull
+ @Override
+ public Set<String> annotations() {
+ return Collections.emptySet();
+ }
+}
diff --git a/room/room-compiler/build.gradle b/room/room-compiler/build.gradle
index e80685d..9a4310c 100644
--- a/room/room-compiler/build.gradle
+++ b/room/room-compiler/build.gradle
@@ -286,6 +286,7 @@
tasks.withType(KotlinCompile).configureEach {
kotlinOptions {
freeCompilerArgs += [
+ "-Xjvm-default=all",
"-opt-in=kotlin.RequiresOptIn",
"-opt-in=kotlin.contracts.ExperimentalContracts",
"-opt-in=androidx.room.compiler.processing.ExperimentalProcessingApi"
diff --git a/settings.gradle b/settings.gradle
index f582cf6..8bd3b85 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -23,6 +23,7 @@
dependencies {
classpath("com.gradle:gradle-enterprise-gradle-plugin:3.9")
classpath("com.gradle:common-custom-user-data-gradle-plugin:1.6.5")
+ classpath("androidx.build.gradle.gcpbuildcache:gcpbuildcache:1.0.0-alpha02")
}
}
@@ -59,6 +60,7 @@
apply(plugin: "com.gradle.enterprise")
apply(plugin: "com.gradle.common-custom-user-data-gradle-plugin")
+apply(plugin: "androidx.build.gradle.gcpbuildcache")
gradleEnterprise {
server = "https://ge.androidx.dev"
diff --git a/sqlite/integration-tests/inspection-room-testapp/build.gradle b/sqlite/integration-tests/inspection-room-testapp/build.gradle
index af17c84..a4048b05 100644
--- a/sqlite/integration-tests/inspection-room-testapp/build.gradle
+++ b/sqlite/integration-tests/inspection-room-testapp/build.gradle
@@ -31,7 +31,7 @@
androidTestImplementation(project(":room:room-runtime"))
androidTestImplementation(project(":sqlite:sqlite-inspection"))
androidTestImplementation(project(":inspection:inspection-testing"))
- androidTestImplementation("com.google.protobuf:protobuf-javalite:3.10.0")
+ androidTestImplementation(libs.protobufLite)
kaptAndroidTest(project(":room:room-compiler"))
}
diff --git a/sqlite/integration-tests/inspection-sqldelight-testapp/build.gradle b/sqlite/integration-tests/inspection-sqldelight-testapp/build.gradle
index 1245b22..02a31e9 100644
--- a/sqlite/integration-tests/inspection-sqldelight-testapp/build.gradle
+++ b/sqlite/integration-tests/inspection-sqldelight-testapp/build.gradle
@@ -31,7 +31,7 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(project(":sqlite:sqlite-inspection"))
androidTestImplementation(project(":inspection:inspection-testing"))
- androidTestImplementation("com.google.protobuf:protobuf-javalite:3.10.0")
+ androidTestImplementation(libs.protobufLite)
}
android {
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Button.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Button.kt
index 241330a..573900c 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Button.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Button.kt
@@ -81,7 +81,7 @@
* appearance / behavior of this Button in different [Interaction]s.
*/
@Composable
-fun Button(
+public fun Button(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
@@ -156,7 +156,7 @@
* appearance / behavior of this Button in different [Interaction]s.
*/
@Composable
-fun CompactButton(
+public fun CompactButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
@@ -202,14 +202,14 @@
* in a secondary styled [Button].
*/
@Stable
-interface ButtonColors {
+public interface ButtonColors {
/**
* Represents the background color for this button, depending on [enabled].
*
* @param enabled whether the button is enabled
*/
@Composable
- fun backgroundColor(enabled: Boolean): State<Color>
+ public fun backgroundColor(enabled: Boolean): State<Color>
/**
* Represents the content color for this button, depending on [enabled].
@@ -217,13 +217,22 @@
* @param enabled whether the button is enabled
*/
@Composable
- fun contentColor(enabled: Boolean): State<Color>
+ public fun contentColor(enabled: Boolean): State<Color>
}
/**
* Contains the default values used by [Button].
*/
public object ButtonDefaults {
+ /**
+ * Creates a [ButtonColors] that represents the default background and content colors for a
+ * primary [Button]. Primary buttons have a colored background with a contrasting content color.
+ * If a button is disabled then the color will have an alpha ([ContentAlpha.disabled]) value
+ * applied.
+ *
+ * @param backgroundColor The background color of this [Button] when enabled
+ * @param contentColor The content color of this [Button] when enabled
+ */
@Composable
public fun primaryButtonColors(
backgroundColor: Color = MaterialTheme.colors.primary,
@@ -235,6 +244,15 @@
)
}
+ /**
+ * Creates a [ButtonColors] that represents the default background and content colors for a
+ * secondary [Button]. Secondary buttons have a muted background with a contrasting content
+ * color. If a button is disabled then the color will have an alpha ([ContentAlpha.disabled])
+ * value applied.
+ *
+ * @param backgroundColor The background color of this [Button] when enabled
+ * @param contentColor The content color of this [Button] when enabled
+ */
@Composable
public fun secondaryButtonColors(
backgroundColor: Color = MaterialTheme.colors.surface,
@@ -246,6 +264,13 @@
)
}
+ /**
+ * Creates a [ButtonColors] that represents the content colors for
+ * an icon-only [Button]. If a button is disabled then the color will have an alpha
+ * ([ContentAlpha.disabled]) value applied.
+ *
+ * @param contentColor The content color of this [Button] when enabled
+ */
@Composable
public fun iconButtonColors(
contentColor: Color = MaterialTheme.colors.onSurface,
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ToggleButton.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ToggleButton.kt
index c57c6d7..26279b2 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ToggleButton.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ToggleButton.kt
@@ -77,7 +77,7 @@
* @param content The icon, image or text to be drawn inside the toggle button.
*/
@Composable
-fun ToggleButton(
+public fun ToggleButton(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
@@ -124,7 +124,7 @@
* primary-styled for a checked toggle button and surface-styled for unchecked.
*/
@Stable
-interface ToggleButtonColors {
+public interface ToggleButtonColors {
/**
* Represents the background color for this toggle button, depending on [enabled] and [checked].
*
@@ -132,7 +132,7 @@
* @param checked whether the toggle button is checked
*/
@Composable
- fun backgroundColor(enabled: Boolean, checked: Boolean): State<Color>
+ public fun backgroundColor(enabled: Boolean, checked: Boolean): State<Color>
/**
* Represents the content color for this toggle button, depending on [enabled] and [checked].
@@ -141,7 +141,7 @@
* @param checked whether the toggle button is checked
*/
@Composable
- fun contentColor(enabled: Boolean, checked: Boolean): State<Color>
+ public fun contentColor(enabled: Boolean, checked: Boolean): State<Color>
}
/**
diff --git a/wear/tiles/tiles-renderer/build.gradle b/wear/tiles/tiles-renderer/build.gradle
index c0d8dab..f7c230b 100644
--- a/wear/tiles/tiles-renderer/build.gradle
+++ b/wear/tiles/tiles-renderer/build.gradle
@@ -44,7 +44,7 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
- androidTestImplementation("com.google.protobuf:protobuf-java:3.10.0")
+ androidTestImplementation(libs.protobuf)
// I'm not 100% sure why, but androidTestImplementation doesn't appear to use the standard
// results of a project build. This leads to it not using the shadow configuration from
@@ -93,7 +93,7 @@
protobuf {
protoc {
- artifact = "com.google.protobuf:protoc:3.10.0"
+ artifact = libs.protobufCompiler.get()
}
// Generates the java proto-lite code for the protos in this project. See