Merge "Account for core/layout offset in CursorAnchorInfo" into androidx-main
diff --git a/benchmark/benchmark-macro/api/current.txt b/benchmark/benchmark-macro/api/current.txt
index 7116325..333b233 100644
--- a/benchmark/benchmark-macro/api/current.txt
+++ b/benchmark/benchmark-macro/api/current.txt
@@ -153,6 +153,8 @@
method public static androidx.benchmark.macro.PowerMetric.Type.Battery Battery();
method public static androidx.benchmark.macro.PowerMetric.Type.Energy Energy(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
method public static androidx.benchmark.macro.PowerMetric.Type.Power Power(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
+ method public static boolean deviceBatteryHasMinimumCharge();
+ method public static boolean deviceSupportsPowerEnergy();
field public static final androidx.benchmark.macro.PowerMetric.Companion Companion;
}
@@ -160,6 +162,8 @@
method public androidx.benchmark.macro.PowerMetric.Type.Battery Battery();
method public androidx.benchmark.macro.PowerMetric.Type.Energy Energy(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
method public androidx.benchmark.macro.PowerMetric.Type.Power Power(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
+ method public boolean deviceBatteryHasMinimumCharge();
+ method public boolean deviceSupportsPowerEnergy();
}
public abstract static sealed class PowerMetric.Type {
diff --git a/benchmark/benchmark-macro/api/restricted_current.txt b/benchmark/benchmark-macro/api/restricted_current.txt
index 820522c..c3cc53c 100644
--- a/benchmark/benchmark-macro/api/restricted_current.txt
+++ b/benchmark/benchmark-macro/api/restricted_current.txt
@@ -166,6 +166,8 @@
method public static androidx.benchmark.macro.PowerMetric.Type.Battery Battery();
method public static androidx.benchmark.macro.PowerMetric.Type.Energy Energy(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
method public static androidx.benchmark.macro.PowerMetric.Type.Power Power(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
+ method public static boolean deviceBatteryHasMinimumCharge();
+ method public static boolean deviceSupportsPowerEnergy();
field public static final androidx.benchmark.macro.PowerMetric.Companion Companion;
}
@@ -173,6 +175,8 @@
method public androidx.benchmark.macro.PowerMetric.Type.Battery Battery();
method public androidx.benchmark.macro.PowerMetric.Type.Energy Energy(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
method public androidx.benchmark.macro.PowerMetric.Type.Power Power(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
+ method public boolean deviceBatteryHasMinimumCharge();
+ method public boolean deviceSupportsPowerEnergy();
}
public abstract static sealed class PowerMetric.Type {
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PowerMetricTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PowerMetricTest.kt
index b47435e..f5b8ee3 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PowerMetricTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PowerMetricTest.kt
@@ -16,15 +16,15 @@
package androidx.benchmark.macro
-import android.os.Build
-import androidx.annotation.RequiresApi
import androidx.benchmark.macro.Metric.Measurement
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
import androidx.benchmark.perfetto.PerfettoTraceProcessor
+import androidx.test.filters.SdkSuppress
import kotlin.test.assertEquals
import org.junit.Assume.assumeTrue
import org.junit.Test
+@SdkSuppress(minSdkVersion = 29)
@OptIn(ExperimentalMetricApi::class)
class PowerMetricTest {
private val captureInfo = Metric.CaptureInfo(
@@ -34,7 +34,6 @@
StartupMode.COLD
)
- @RequiresApi(Build.VERSION_CODES.Q)
@Test
fun successfulFixedTraceEnergyBreakdown() {
assumeTrue(isAbiSupported())
@@ -72,7 +71,6 @@
)
}
- @RequiresApi(Build.VERSION_CODES.Q)
@Test
fun successfulFixedTracePowerTotal() {
assumeTrue(isAbiSupported())
@@ -101,7 +99,6 @@
)
}
- @RequiresApi(Build.VERSION_CODES.Q)
@Test
fun successfulFixedTracePowerMix() {
assumeTrue(isAbiSupported())
@@ -134,7 +131,6 @@
)
}
- @RequiresApi(Build.VERSION_CODES.Q)
@Test
fun emptyFixedTrace() {
assumeTrue(isAbiSupported())
@@ -150,7 +146,7 @@
assertEquals(emptyList(), actualMetrics)
}
- @RequiresApi(Build.VERSION_CODES.Q)
+ @SdkSuppress(minSdkVersion = 29)
@Test
fun successfulFixedTraceBatteryDischarge() {
assumeTrue(isAbiSupported())
@@ -171,4 +167,20 @@
threshold = 0.1
)
}
+
+ @Test
+ fun deviceSupportsPowerEnergy() {
+ assertEquals(
+ PowerRail.hasMetrics(throwOnMissingMetrics = false),
+ PowerMetric.deviceSupportsPowerEnergy()
+ )
+ }
+
+ @Test
+ fun deviceBatteryHasMinimumCharge() {
+ assertEquals(
+ BatteryCharge.hasMinimumCharge(throwOnMissingMetrics = false),
+ PowerMetric.deviceBatteryHasMinimumCharge()
+ )
+ }
}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PowerRailTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PowerRailTest.kt
index 0d729e3..6f714b2 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PowerRailTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PowerRailTest.kt
@@ -18,6 +18,7 @@
import android.os.Build
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
@@ -30,12 +31,14 @@
@SmallTest
class PowerRailTest {
+ @SdkSuppress(minSdkVersion = 32)
@Test
fun hasMetrics_Pixel6() {
- assumeTrue(Build.VERSION.SDK_INT > 31 && Build.MODEL.lowercase() == "oriole")
+ assumeTrue(Build.MODEL.lowercase() == "oriole")
assertTrue(PowerRail.hasMetrics(throwOnMissingMetrics = true))
assertTrue(PowerRail.hasMetrics(throwOnMissingMetrics = false))
+ assertTrue(PowerMetric.deviceSupportsPowerEnergy())
}
@Test
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
index 6437ad16..1f96839 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
@@ -712,6 +712,47 @@
): Type.Power {
return Type.Power(categories)
}
+
+ /**
+ * Returns true if the current device can be used for power/energy metrics.
+ *
+ * This can be used to change behavior or fall back to lower precision tracking:
+ *
+ * ```
+ * metrics = listOf(
+ * if (PowerMetric.deviceSupportsPowerEnergy()) {
+ * PowerMetric(Type.Energy()) // high precision tracking
+ * } else {
+ * PowerMetric(Type.Battery()) // fall back to less precise tracking
+ * }
+ * )
+ * ```
+ *
+ * Or to skip a test when detailed tracking isn't available:
+ * ```
+ * @Test fun myDetailedPowerBenchmark {
+ * assumeTrue(PowerMetric.deviceSupportsPowerEnergy())
+ * macrobenchmarkRule.measureRepeated (
+ * metrics = listOf(PowerMetric(Type.Energy(...)))
+ * ) {
+ * ...
+ * }
+ * }
+ * ```
+ */
+ @JvmStatic
+ fun deviceSupportsPowerEnergy(): Boolean = hasMetrics(throwOnMissingMetrics = false)
+
+ /**
+ * Returns true if [Type.Battery] measurements can be performed, based on current device
+ * charge.
+ *
+ * This can be used to change behavior or throw a clear error before metric configuration,
+ * or to skip the test, e.g. with `assumeTrue(PowerMetric.deviceBatteryHasMinimumCharge())`
+ */
+ @JvmStatic
+ fun deviceBatteryHasMinimumCharge(): Boolean =
+ hasMinimumCharge(throwOnMissingMetrics = false)
}
/**
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/PowerRail.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/PowerRail.kt
index 1013b84..ecaf2ad 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/PowerRail.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/PowerRail.kt
@@ -65,7 +65,7 @@
adb shell $DUMPSYS_POWERSTATS
- To check at runtime for this, use PowerRail.hasMetrics()
+ To check at runtime for this, use PowerMetric.deviceSupportsPowerEnergy()
""".trimIndent()
)
diff --git a/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/PathEasingTest.kt b/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/PathEasingTest.kt
index 0f8a79b..5d7d570 100644
--- a/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/PathEasingTest.kt
+++ b/compose/animation/animation-core/src/androidInstrumentedTest/kotlin/androidx/compose/animation/core/PathEasingTest.kt
@@ -167,4 +167,20 @@
previousFraction = newFraction
}
}
+
+ @Test
+ fun pathEasing_Overshoots() {
+ val path = Path().apply {
+ cubicTo(0.34f, 1.56f, 0.64f, 1.0f, 1.0f, 1.0f)
+ }
+ assertThat(PathEasing(path).transform(0.6f)).isGreaterThan(1.0f)
+ }
+
+ @Test
+ fun pathEasing_Undershoots() {
+ val path = Path().apply {
+ cubicTo(0.68f, -0.6f, 0.32f, 1.6f, 1.0f, 1.0f)
+ }
+ assertThat(PathEasing(path).transform(0.1f)).isLessThan(0.0f)
+ }
}
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/EasingTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/EasingTest.kt
index eb5c46e..beea6dbd 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/EasingTest.kt
+++ b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/EasingTest.kt
@@ -28,8 +28,8 @@
@RunWith(JUnit4::class)
class EasingTest {
- val ZeroEpsilon = -(1.0f.ulp)
- val OneEpsilon = 1.0f + 1.0f.ulp
+ private val ZeroEpsilon = -(1.0f.ulp)
+ private val OneEpsilon = 1.0f + 1.0f.ulp
@Test
fun cubicBezierStartsAt0() {
@@ -40,7 +40,19 @@
@Test
fun cubicBezierEndsAt1() {
val easing = FastOutLinearInEasing
- assertThat(easing.transform(1f) == 1f)
+ assertThat(easing.transform(1f)).isEqualTo(1.0f)
+ }
+
+ @Test
+ fun cubicBezierDoesntExceed1() {
+ val easing = CubicBezierEasing(0f, 0f, 0.15f, 1f)
+ assertThat(easing.transform(0.999999f) <= 1.0f).isTrue()
+ }
+
+ @Test
+ fun cubicBezierDoesExceed1() {
+ val easing = CubicBezierEasing(0.34f, 1.56f, 0.64f, 1.0f)
+ assertThat(easing.transform(0.6f)).isGreaterThan(1.0f)
}
@Test
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Easing.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Easing.kt
index 51dbbbf..f69cb99 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Easing.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Easing.kt
@@ -18,8 +18,10 @@
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
+import androidx.compose.ui.graphics.computeCubicVerticalBounds
import androidx.compose.ui.graphics.evaluateCubic
import androidx.compose.ui.graphics.findFirstCubicRoot
+import androidx.compose.ui.util.fastCoerceIn
/**
* Easing is a way to adjust an animation’s fraction. Easing allows transitioning
@@ -106,10 +108,17 @@
private val c: Float,
private val d: Float
) : Easing {
+ private val min: Float
+ private val max: Float
+
init {
requirePrecondition(!a.isNaN() && !b.isNaN() && !c.isNaN() && !d.isNaN()) {
"Parameters to CubicBezierEasing cannot be NaN. Actual parameters are: $a, $b, $c, $d."
}
+ val roots = FloatArray(5)
+ val extrema = computeCubicVerticalBounds(0.0f, b, d, 1.0f, roots, 0)
+ min = extrema.first
+ max = extrema.second
}
/**
@@ -131,20 +140,24 @@
// No root, the cubic curve has no solution
if (t.isNaN()) {
- throw IllegalArgumentException(
- "The cubic curve with parameters ($a, $b, $c, $d) has no solution at $fraction"
- )
+ throwNoSolution(fraction)
}
// Don't clamp the values since the curve might be used to over- or under-shoot
// The test above that checks if fraction is in ]0..1[ will ensure we start and
// end at 0 and 1 respectively
- evaluateCubic(b, d, t)
+ evaluateCubic(b, d, t).fastCoerceIn(min, max)
} else {
fraction
}
}
+ private fun throwNoSolution(fraction: Float) {
+ throw IllegalArgumentException(
+ "The cubic curve with parameters ($a, $b, $c, $d) has no solution at $fraction"
+ )
+ }
+
override fun equals(other: Any?): Boolean {
return other is CubicBezierEasing && a == other.a && b == other.b && c == other.c &&
d == other.d
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/PathEasing.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/PathEasing.kt
index 43446776..9aac1bb 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/PathEasing.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/PathEasing.kt
@@ -24,7 +24,6 @@
import androidx.compose.ui.graphics.computeHorizontalBounds
import androidx.compose.ui.graphics.evaluateY
import androidx.compose.ui.graphics.findFirstRoot
-import androidx.compose.ui.util.fastCoerceIn
/**
* An easing function for an arbitrary [Path].
@@ -66,39 +65,7 @@
}
if (!::intervals.isInitialized) {
- val roots = FloatArray(5)
-
- // Using an interval tree is a bit heavy handed but since we are dealing with
- // easing curves, we don't expect many segments, and therefore few allocations.
- // The interval tree allows us to quickly query for the correct segment inside
- // the transform() function.
- val segmentIntervals = IntervalTree<PathSegment>().apply {
- // A path easing curve is defined in the domain 0..1, use an error
- // appropriate for this domain (the default is 0.25). Conic segments
- // should be unlikely in path easing curves, but just in case...
- val iterator = path.iterator(
- PathIterator.ConicEvaluation.AsQuadratics,
- 2e-4f
- )
- while (iterator.hasNext()) {
- val segment = iterator.next()
- requirePrecondition(segment.type != PathSegment.Type.Close) {
- "The path cannot contain a close() command."
- }
- if (segment.type != PathSegment.Type.Move &&
- segment.type != PathSegment.Type.Done
- ) {
- val bounds = computeHorizontalBounds(segment, roots)
- addInterval(bounds.first, bounds.second, segment)
- }
- }
- }
-
- requirePrecondition(0.0f in segmentIntervals && 1.0f in segmentIntervals) {
- "The easing path must start at 0.0f and end at 1.0f."
- }
-
- intervals = segmentIntervals
+ initializeEasing()
}
val result = intervals.findFirstOverlap(fraction)
@@ -111,6 +78,42 @@
"The easing path is invalid. Make sure it does not contain NaN/Infinity values."
}
- return evaluateY(segment, t).fastCoerceIn(0.0f, 1.0f)
+ return evaluateY(segment, t)
+ }
+
+ private fun initializeEasing() {
+ val roots = FloatArray(5)
+
+ // Using an interval tree is a bit heavy handed but since we are dealing with
+ // easing curves, we don't expect many segments, and therefore few allocations.
+ // The interval tree allows us to quickly query for the correct segment inside
+ // the transform() function.
+ val segmentIntervals = IntervalTree<PathSegment>().apply {
+ // A path easing curve is defined in the domain 0..1, use an error
+ // appropriate for this domain (the default is 0.25). Conic segments
+ // should be unlikely in path easing curves, but just in case...
+ val iterator = path.iterator(
+ PathIterator.ConicEvaluation.AsQuadratics,
+ 2e-4f
+ )
+ while (iterator.hasNext()) {
+ val segment = iterator.next()
+ requirePrecondition(segment.type != PathSegment.Type.Close) {
+ "The path cannot contain a close() command."
+ }
+ if (segment.type != PathSegment.Type.Move &&
+ segment.type != PathSegment.Type.Done
+ ) {
+ val bounds = computeHorizontalBounds(segment, roots)
+ addInterval(bounds.first, bounds.second, segment)
+ }
+ }
+ }
+
+ requirePrecondition(0.0f in segmentIntervals && 1.0f in segmentIntervals) {
+ "The easing path must start at 0.0f and end at 1.0f."
+ }
+
+ intervals = segmentIntervals
}
}
diff --git a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/SharedTransitionTest.kt b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/SharedTransitionTest.kt
index 012d184..a268210 100644
--- a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/SharedTransitionTest.kt
+++ b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/SharedTransitionTest.kt
@@ -29,16 +29,20 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Text
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@@ -67,6 +71,7 @@
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
@@ -79,6 +84,7 @@
import kotlin.math.roundToInt
import kotlin.math.sqrt
import kotlin.random.Random
+import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -2569,6 +2575,139 @@
isSquare = !isSquare
rule.waitForIdle()
}
+
+ sealed class Screen {
+ abstract val key: Int
+
+ object List : Screen() {
+ override val key = 1
+ }
+
+ data class Details(val item: Int) : Screen() {
+ override val key = 2
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = 26)
+ @Test
+ fun testRandomScrollingWithInterruption() {
+ @Suppress("PrimitiveInCollection")
+ val colors = listOf(
+ Color(0xffff6f69),
+ Color(0xffffcc5c),
+ Color(0xff2a9d84),
+ Color(0xff264653)
+ )
+ var state by mutableStateOf<Screen>(Screen.List)
+ val lazyListState = LazyListState()
+ rule.setContent {
+ SharedTransitionLayout(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(800.dp)
+ ) {
+ AnimatedContent(state, label = "", contentKey = { it.key },
+ transitionSpec = {
+ if (initialState == Screen.List) {
+ slideInHorizontally { -it } + fadeIn() togetherWith
+ slideOutHorizontally { it } + fadeOut()
+ } else {
+ slideInHorizontally { it } + fadeIn() togetherWith
+ slideOutHorizontally { -it } + fadeOut()
+ }
+ }) {
+ when (it) {
+ Screen.List -> {
+ LazyColumn(state = lazyListState) {
+ items(50) { item ->
+ Row(modifier = Modifier.fillMaxWidth()) {
+ Box(
+ modifier = Modifier
+ .size(100.dp)
+ .then(
+ Modifier.sharedElement(
+ rememberSharedContentState(
+ key = "item-image$item"
+ ),
+ this@AnimatedContent,
+ )
+ )
+ .background(colors[item % 4]),
+ )
+ Spacer(Modifier.size(15.dp))
+ Text("Item $item")
+ }
+ }
+ }
+ }
+
+ is Screen.Details -> {
+ val item = it.item
+ Column(modifier = Modifier.fillMaxSize()) {
+ Box(
+ modifier = Modifier
+ .sharedElement(
+ rememberSharedContentState(key = "item-image$item"),
+ this@AnimatedContent,
+ )
+ .background(colors[item % 4])
+ .fillMaxWidth(),
+ )
+ Text(
+ "Item $item",
+ fontSize = 23.sp
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ rule.waitForIdle()
+ rule.mainClock.autoAdvance = false
+ state = Screen.Details(5)
+ repeat(10) {
+ rule.waitForIdle()
+ rule.mainClock.advanceTimeByFrame()
+ }
+ state = Screen.List
+ repeat(3) {
+ rule.waitForIdle()
+ rule.mainClock.advanceTimeByFrame()
+ }
+
+ repeat(3) {
+ repeat(5) {
+ rule.runOnIdle {
+ runBlocking {
+ lazyListState.scrollToItem(it * 10)
+ }
+ }
+ }
+ rule.mainClock.advanceTimeByFrame()
+ repeat(5) {
+ rule.runOnIdle {
+ runBlocking {
+ lazyListState.scrollToItem(40 - it * 10)
+ }
+ }
+ }
+ rule.mainClock.advanceTimeByFrame()
+ repeat(10) {
+ rule.runOnIdle {
+ runBlocking {
+ lazyListState.scrollToItem(it * 5)
+ }
+ }
+ }
+ rule.mainClock.advanceTimeByFrame()
+ rule.runOnIdle {
+ runBlocking {
+ lazyListState.scrollToItem(0)
+ }
+ }
+ }
+ }
}
private fun assertEquals(a: IntSize, b: IntSize, delta: IntSize) {
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SharedContentNode.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SharedContentNode.kt
index e8913d0..62b5f02 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SharedContentNode.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SharedContentNode.kt
@@ -42,6 +42,7 @@
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.requireDensity
import androidx.compose.ui.node.requireGraphicsContext
+import androidx.compose.ui.node.requireLayoutCoordinates
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
@@ -87,23 +88,18 @@
provide(ModifierLocalSharedElementInternalState, value)
state.parentState = ModifierLocalSharedElementInternalState.current
state.layer = layer
- state.lookaheadCoords = lookaheadCoords
- state.lookaheadSize = lookaheadSize
+ state.lookaheadCoords = { requireLookaheadLayoutCoordinates() }
}
}
}
- private var lookaheadCoords: LayoutCoordinates? = state.lookaheadCoords
- set(value) {
- state.lookaheadCoords = value
- field = value
+ @OptIn(ExperimentalComposeUiApi::class)
+ private fun requireLookaheadLayoutCoordinates(): LayoutCoordinates =
+ with(state.sharedElement.scope) {
+ requireLayoutCoordinates().toLookaheadCoordinates()
}
+
private val boundsAnimation: BoundsAnimation get() = state.boundsAnimation
- private var lookaheadSize: Size? = state.lookaheadSize
- set(value) {
- state.lookaheadSize = value
- field = value
- }
private var layer: GraphicsLayer? = state.layer
set(value) {
@@ -126,13 +122,21 @@
provide(ModifierLocalSharedElementInternalState, state)
state.parentState = ModifierLocalSharedElementInternalState.current
layer = requireGraphicsContext().createGraphicsLayer()
+ state.lookaheadCoords = { requireLookaheadLayoutCoordinates() }
}
override fun onDetach() {
super.onDetach()
layer = null
state.parentState = null
- lookaheadCoords = null
+ state.lookaheadCoords = { null }
+ }
+
+ override fun onReset() {
+ super.onReset()
+ // Reset layer
+ layer?.let { requireGraphicsContext().releaseGraphicsLayer(it) }
+ layer = requireGraphicsContext().createGraphicsLayer()
}
override fun MeasureScope.measure(
@@ -141,17 +145,14 @@
): MeasureResult {
// Lookahead pass: Record lookahead size and lookahead coordinates
val placeable = measurable.measure(constraints)
- lookaheadSize = Size(placeable.width.toFloat(), placeable.height.toFloat())
+ val lookaheadSize = Size(placeable.width.toFloat(), placeable.height.toFloat())
return layout(placeable.width, placeable.height) {
val topLeft = coordinates?.let {
- lookaheadCoords = it
rootLookaheadCoords.localPositionOf(it, Offset.Zero).also { topLeft ->
if (sharedElement.currentBounds == null) {
sharedElement.currentBounds = Rect(
topLeft,
- requireNotNull(lookaheadSize) {
- "Error: Lookahead measure has not happened."
- }
+ lookaheadSize
)
}
}
@@ -160,14 +161,14 @@
// Update the lookahead result after child placement, so that child has an
// opportunity to use its placement to influence the bounds animation.
topLeft?.let {
- sharedElement.onLookaheadResult(state, lookaheadSize!!, it)
+ sharedElement.onLookaheadResult(state, lookaheadSize, it)
}
}
}
private fun MeasureScope.place(placeable: Placeable): MeasureResult {
val (w, h) = state.placeHolderSize.calculateSize(
- lookaheadSize!!.roundToIntSize(),
+ requireLookaheadLayoutCoordinates().size,
IntSize(placeable.width, placeable.height)
)
return layout(w, h) {
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SharedElement.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SharedElement.kt
index 13841b8..10d63ec 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SharedElement.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SharedElement.kt
@@ -35,6 +35,7 @@
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.unit.toSize
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachReversed
@@ -55,9 +56,7 @@
val targetBounds: Rect?
get() {
_targetBounds = targetBoundsProvider?.run {
- Rect(calculateLookaheadOffset(), requireNotNull(lookaheadSize) {
- "Error: target has not been lookahead measured."
- })
+ Rect(calculateLookaheadOffset(), nonNullLookaheadSize)
}
return _targetBounds
}
@@ -196,17 +195,15 @@
var overlayClip: SharedTransitionScope.OverlayClip by mutableStateOf(overlayClip)
var userState: SharedTransitionScope.SharedContentState by mutableStateOf(userState)
- init {
- sharedElement.scope.onStateAdded(this)
- sharedElement.updateTargetBoundsProvider()
- }
-
internal var clipPathInOverlay: Path? = null
override fun drawInOverlay(drawScope: DrawScope) {
val layer = layer ?: return
if (shouldRenderInOverlay) {
with(drawScope) {
+ requireNotNull(sharedElement.currentBounds) {
+ "Error: current bounds not set yet."
+ }
val (x, y) = sharedElement.currentBounds?.topLeft!!
clipPathInOverlay?.let {
clipPath(it) {
@@ -219,21 +216,26 @@
}
}
- var lookaheadSize: Size? = null
- var lookaheadCoords: LayoutCoordinates? = null
+ val nonNullLookaheadSize: Size
+ get() = requireNotNull(lookaheadCoords()) {
+ "Error: lookahead coordinates is null."
+ }.size.toSize()
+ var lookaheadCoords: () -> LayoutCoordinates? = { null }
override var parentState: SharedElementInternalState? = null
// This can only be accessed during placement
fun calculateLookaheadOffset(): Offset {
- val c = requireNotNull(lookaheadCoords) {
- "Error: target has not been placed in lookahead pass yet."
+ val c = requireNotNull(lookaheadCoords()) {
+ "Error: lookahead coordinates is null."
}
return sharedElement.scope.lookaheadRoot.localPositionOf(c, Offset.Zero)
}
val target: Boolean get() = boundsAnimation.target
- var layer: GraphicsLayer? = null
+ // Delegate the property to a mutable state, so that when layer is updated, the rendering
+ // gets invalidated.
+ var layer: GraphicsLayer? by mutableStateOf(null)
private val shouldRenderBasedOnTarget: Boolean
get() = sharedElement.targetBoundsProvider == this || !renderOnlyWhenVisible
@@ -246,6 +248,8 @@
get() = !sharedElement.foundMatch || (!shouldRenderInOverlay && shouldRenderBasedOnTarget)
override fun onRemembered() {
+ sharedElement.scope.onStateAdded(this)
+ sharedElement.updateTargetBoundsProvider()
}
override fun onForgotten() {
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt
index 6d1df04..b5227f8 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt
@@ -105,13 +105,12 @@
}
}
- if (pluginContext.platform.isNative() &&
- descriptorSerializerContext?.hideFromObjCDeclarationsSet != null) {
+ if (pluginContext.platform.isNative()) {
AddHiddenFromObjCLowering(
pluginContext,
symbolRemapper,
metrics,
- descriptorSerializerContext.hideFromObjCDeclarationsSet,
+ descriptorSerializerContext?.hideFromObjCDeclarationsSet,
stabilityInferencer
).lower(moduleFragment)
}
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/DecoyTransformBase.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/DecoyTransformBase.kt
index c73f49b..037b408 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/DecoyTransformBase.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/DecoyTransformBase.kt
@@ -40,6 +40,7 @@
import org.jetbrains.kotlin.ir.types.IrType
import org.jetbrains.kotlin.ir.types.IrTypeArgument
import org.jetbrains.kotlin.ir.types.impl.IrSimpleTypeImpl
+import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
import org.jetbrains.kotlin.ir.util.DeepCopyTypeRemapper
import org.jetbrains.kotlin.ir.util.IdSignature
import org.jetbrains.kotlin.ir.util.SymbolRenamer
@@ -206,7 +207,17 @@
source: IrFunction,
target: IrFunction
): T {
- return deepCopyWithSymbols(target) { symbolRemapper, typeRemapper ->
+ val typeParamsAwareSymbolRemapper = object : DeepCopySymbolRemapper() {
+ init {
+ for ((orig, new) in source.typeParameters.zip(target.typeParameters)) {
+ typeParameters[orig.symbol] = new.symbol
+ }
+ }
+ }
+ return deepCopyWithSymbols(
+ target,
+ typeParamsAwareSymbolRemapper
+ ) { symbolRemapper, typeRemapper ->
val typeParamRemapper = object : TypeRemapper by typeRemapper {
override fun remapType(type: IrType): IrType {
return typeRemapper.remapType(type.remapTypeParameters(source, target))
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/hiddenfromobjc/AddHiddenFromObjCLowering.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/hiddenfromobjc/AddHiddenFromObjCLowering.kt
index d749568..648bf66 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/hiddenfromobjc/AddHiddenFromObjCLowering.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/hiddenfromobjc/AddHiddenFromObjCLowering.kt
@@ -23,8 +23,11 @@
import androidx.compose.compiler.plugins.kotlin.lower.containsComposableAnnotation
import androidx.compose.compiler.plugins.kotlin.lower.needsComposableRemapping
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
+import org.jetbrains.kotlin.backend.common.pop
+import org.jetbrains.kotlin.backend.common.push
import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
import org.jetbrains.kotlin.ir.IrStatement
+import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrDeclaration
import org.jetbrains.kotlin.ir.declarations.IrFunction
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
@@ -41,13 +44,13 @@
/**
* AddHiddenFromObjCLowering looks for functions and properties with @Composable types and
* adds the `kotlin.native.HiddenFromObjC` annotation to them.
- * @see https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native/-hidden-from-obj-c/
+ * [docs](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native/-hidden-from-obj-c/)
*/
class AddHiddenFromObjCLowering(
private val pluginContext: IrPluginContext,
symbolRemapper: ComposableSymbolRemapper,
metrics: ModuleMetrics,
- private val hideFromObjCDeclarationsSet: HideFromObjCDeclarationsSet,
+ private val hideFromObjCDeclarationsSet: HideFromObjCDeclarationsSet?,
stabilityInferencer: StabilityInferencer,
) : AbstractComposeLowering(pluginContext, symbolRemapper, metrics, stabilityInferencer) {
@@ -55,6 +58,9 @@
getTopLevelClass(ClassId.fromString("kotlin/native/HiddenFromObjC"))
}
+ private val shouldAnnotateClass = ArrayDeque<Boolean>()
+ private var currentShouldAnnotateClass = false
+
override fun lower(module: IrModuleFragment) {
require(context.platform.isNative()) {
"AddHiddenFromObjCLowering is expected to run only for k/native. " +
@@ -63,6 +69,27 @@
module.transformChildrenVoid(this)
}
+ /** `visitClass` is only needed until [issue](https://youtrack.jetbrains.com/issue/KT-65288/) fix
+ * after the issue is resolved, `visitClass` could be removed entirely
+ */
+ override fun visitClass(declaration: IrClass): IrStatement {
+ shouldAnnotateClass.push(currentShouldAnnotateClass)
+ currentShouldAnnotateClass = false
+
+ val cls = super.visitClass(declaration) as IrClass
+
+ // We see an issue only with data classes containing something Composable.
+ // Adding an annotation to all classes makes the FirNativeHiddenFromObjCInheritanceChecker (kotlin) complain.
+ // data classes can't be open, so it should work.
+ if (currentShouldAnnotateClass && cls.isData) {
+ cls.addHiddenFromObjCAnnotation()
+ hideFromObjCDeclarationsSet?.add(cls)
+ }
+
+ currentShouldAnnotateClass = shouldAnnotateClass.pop()
+ return cls
+ }
+
override fun visitFunction(declaration: IrFunction): IrStatement {
val f = super.visitFunction(declaration) as IrFunction
if (f.isLocal ||
@@ -72,7 +99,8 @@
if (f.hasComposableAnnotation() || f.needsComposableRemapping()) {
f.addHiddenFromObjCAnnotation()
- hideFromObjCDeclarationsSet.add(f)
+ hideFromObjCDeclarationsSet?.add(f)
+ currentShouldAnnotateClass = true
}
return f
@@ -88,7 +116,8 @@
if (shouldAdd) {
p.addHiddenFromObjCAnnotation()
- hideFromObjCDeclarationsSet.add(p)
+ hideFromObjCDeclarationsSet?.add(p)
+ currentShouldAnnotateClass = true
}
return p
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/hiddenfromobjc/AddHiddenFromObjCSerializationPlugin.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/hiddenfromobjc/AddHiddenFromObjCSerializationPlugin.kt
index 62e06a8..86b897f 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/hiddenfromobjc/AddHiddenFromObjCSerializationPlugin.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/hiddenfromobjc/AddHiddenFromObjCSerializationPlugin.kt
@@ -16,6 +16,7 @@
package androidx.compose.compiler.plugins.kotlin.lower.hiddenfromobjc
+import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.ConstructorDescriptor
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
@@ -47,6 +48,20 @@
id = extension.stringTable.getQualifiedClassNameIndex(annotationToAdd)
}.build()
+ override fun afterClass(
+ descriptor: ClassDescriptor,
+ proto: ProtoBuf.Class.Builder,
+ versionRequirementTable: MutableVersionRequirementTable,
+ childSerializer: DescriptorSerializer,
+ extension: SerializerExtension
+ ) {
+ if (descriptor in hideFromObjCDeclarationsSet) {
+ val annotationProto = createAnnotationProto(extension)
+ proto.addExtension(KlibMetadataSerializerProtocol.classAnnotation, annotationProto)
+ proto.flags = proto.flags or hasAnnotationFlag
+ }
+ }
+
override fun afterConstructor(
descriptor: ConstructorDescriptor,
proto: ProtoBuf.Constructor.Builder,
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/hiddenfromobjc/HideFromObjCDeclarationsSet.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/hiddenfromobjc/HideFromObjCDeclarationsSet.kt
index ec2b064..5495671 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/hiddenfromobjc/HideFromObjCDeclarationsSet.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/hiddenfromobjc/HideFromObjCDeclarationsSet.kt
@@ -18,6 +18,7 @@
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI
+import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrFunction
import org.jetbrains.kotlin.ir.declarations.IrProperty
import org.jetbrains.kotlin.name.FqName
@@ -51,6 +52,10 @@
set.add(property.descriptor.fqNameSafe)
}
+ fun add(cls: IrClass) {
+ set.add(cls.descriptor.fqNameSafe)
+ }
+
operator fun contains(item: DeclarationDescriptor): Boolean {
return set.contains(item.fqNameSafe)
}
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemAppearanceAnimationTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemAppearanceAnimationTest.kt
index 61c2ef9..03845b8 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemAppearanceAnimationTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemAppearanceAnimationTest.kt
@@ -54,6 +54,7 @@
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
import kotlin.math.abs
+import kotlin.math.roundToInt
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Rule
@@ -316,6 +317,46 @@
}
}
+ @Test
+ fun itemOutsideOfViewPortBeingAnimatedIn_shouldBePlacedAtTheEndOfList() {
+ var list by mutableStateOf(
+ listOf(
+ Color.Black,
+ Color.Green,
+ Color.Blue,
+ Color.Yellow,
+ Color.DarkGray
+ )
+ )
+ rule.setContent {
+ LazyList(containerSize = itemSizeDp * 2.5f) {
+ items(list, key = { it.toArgb() }) {
+ Item(it)
+ }
+ }
+ }
+
+ rule.runOnUiThread {
+ // item 0 will leave, item 3 will pop up
+ list = listOf(Color.Green, Color.Blue, Color.Yellow, Color.DarkGray)
+ }
+
+ onAnimationFrame { fraction ->
+ if (fraction.isCloseTo(0.5f)) {
+ assertPixels((itemSize * 2.5f).roundToInt()) { offset ->
+ when (offset) {
+ // green item is first
+ in 0 until itemSize -> Color.Green
+ // blue item is second
+ in itemSize until 2 * itemSize -> Color.Blue
+ // yellow item pops up at the bottom
+ else -> Color.Yellow
+ }
+ }
+ }
+ }
+ }
+
private fun assertPixels(
mainAxisSize: Int,
crossAxisSize: Int = this.crossAxisSize,
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemPlacementAnimationTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemPlacementAnimationTest.kt
index 7ea574b..b90192b 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemPlacementAnimationTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListItemPlacementAnimationTest.kt
@@ -1001,6 +1001,50 @@
}
@Test
+ fun removingItemsCauseOutOfBoundsItemToPopUp_withContentPadding() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+ val rawStartPadding = 8f
+ val rawEndPadding = 12f
+ val (startPaddingDp, endPaddingDp) = with(rule.density) {
+ rawStartPadding.toDp() to rawEndPadding.toDp()
+ }
+ rule.setContent {
+ // only 4 items will be visible 0, 1, 2, 3
+ LazyList(
+ maxSize = itemSizeDp * 4,
+ startPadding = startPaddingDp,
+ endPadding = endPaddingDp
+ ) {
+ items(list, key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ val startPadding = if (reverseLayout) rawEndPadding else rawStartPadding
+ assertPositions(
+ 0 to startPadding,
+ 1 to startPadding + itemSize,
+ 2 to startPadding + itemSize * 2,
+ 3 to startPadding + itemSize * 3
+ )
+
+ rule.runOnUiThread {
+ list = listOf(1, 2, 3, 4)
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 1 to startPadding + itemSize - itemSize * fraction,
+ 2 to startPadding + itemSize * 2 - itemSize * fraction,
+ 3 to startPadding + itemSize * 3 - itemSize * fraction,
+ 4 to startPadding + itemSize * 4 - itemSize * fraction,
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
fun reorderFirstAndLastItems_noNewLayoutInfoProduced() {
var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/CombinedClickableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/CombinedClickableTest.kt
index b67d4b3..ef97634 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/CombinedClickableTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/CombinedClickableTest.kt
@@ -163,7 +163,7 @@
@Test
fun longClickSemantics() {
var counter = 0
- val onClick: () -> Unit = { ++counter }
+ val onLongClick: () -> Unit = { ++counter }
rule.setContent {
Box {
@@ -171,7 +171,7 @@
"ClickableText",
modifier = Modifier
.testTag("myClickable")
- .combinedClickable(onLongClick = onClick) {}
+ .combinedClickable(onLongClick = onLongClick) {}
)
}
}
@@ -193,6 +193,59 @@
}
@Test
+ fun changingLongClickSemantics() {
+ var counter = 0
+ var onLongClick: (() -> Unit)? by mutableStateOf(null)
+
+ rule.setContent {
+ Box {
+ BasicText(
+ "ClickableText",
+ modifier = Modifier
+ .testTag("myClickable")
+ .combinedClickable(onLongClick = onLongClick) {}
+ )
+ }
+ }
+
+ rule.onNodeWithTag("myClickable")
+ .assertIsEnabled()
+ .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.OnLongClick))
+
+ rule.runOnIdle {
+ // Add a no-op long click
+ onLongClick = { /* no-op */ }
+ }
+
+ rule.onNodeWithTag("myClickable")
+ .assertIsEnabled()
+ .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.OnLongClick))
+ .performSemanticsAction(SemanticsActions.OnLongClick)
+
+ rule.runOnIdle {
+ // no-op long click handler
+ assertThat(counter).isEqualTo(0)
+ // Change to mutate counter
+ onLongClick = { ++counter }
+ }
+
+ rule.onNodeWithTag("myClickable")
+ .performSemanticsAction(SemanticsActions.OnLongClick)
+
+ rule.runOnIdle {
+ // Changes should now be applied
+ assertThat(counter).isEqualTo(1)
+ // Make onLongClick null
+ onLongClick = null
+ }
+
+ rule.onNodeWithTag("myClickable")
+ .assertIsEnabled()
+ // Long click action should be removed
+ .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.OnLongClick))
+ }
+
+ @Test
fun click() {
var counter = 0
val onClick: () -> Unit = {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextLayoutTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextLayoutTest.kt
new file mode 100644
index 0000000..b45e79b
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextLayoutTest.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.sp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+// largest size of a unfocused dimension from Constraints.kt
+private val UnfocusedDimensionConstraintMax = 2 shl 13
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class BasicTextLayoutTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun simple_layoutText_doesNotThrow_when2shl14char() {
+ var textLayoutResult: TextLayoutResult? = null
+ rule.setContent {
+ BasicText(
+ text = "a".repeat(2 shl 14),
+ style = TextStyle(fontSize = 48.sp),
+ onTextLayout = { textLayoutResult = it }
+ )
+ }
+ rule.waitForIdle()
+
+ assertThat(textLayoutResult?.multiParagraph?.height).isGreaterThan(
+ UnfocusedDimensionConstraintMax
+ )
+ }
+
+ @Test
+ fun annotatedString_layoutText_doesNotThrow_when2shl14char() {
+ var textLayoutResult: TextLayoutResult? = null
+ rule.setContent {
+ BasicText(
+ text = AnnotatedString("a".repeat(2 shl 14)),
+ style = TextStyle(fontSize = 48.sp),
+ onTextLayout = { textLayoutResult = it }
+ )
+ }
+ rule.waitForIdle()
+ assertThat(textLayoutResult?.multiParagraph?.height).isGreaterThan(
+ UnfocusedDimensionConstraintMax
+ )
+ }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextDelegateIntegrationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextDelegateIntegrationTest.kt
index ecc70a7..51d2ef0 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextDelegateIntegrationTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/TextDelegateIntegrationTest.kt
@@ -36,7 +36,6 @@
import org.junit.Test
import org.junit.runner.RunWith
-@OptIn(InternalFoundationTextApi::class)
@RunWith(AndroidJUnit4::class)
@SmallTest
class TextDelegateIntegrationTest {
@@ -200,6 +199,23 @@
assertThat(layoutResultLtr.size.width).isEqualTo(layoutResultRtl.size.width)
}
+
+ @Test
+ fun layoutText_doesntThrow_when2shl14char() {
+ val textDelegate = TextDelegate(
+ text = AnnotatedString(text = "a".repeat(2 shl 14)),
+ style = TextStyle.Default,
+ density = density,
+ fontFamilyResolver = fontFamilyResolver
+ )
+ val subject = textDelegate.layout(
+ Constraints() /* unbounded */,
+ layoutDirection = LayoutDirection.Ltr,
+ null
+ )
+ assertThat(subject.size.width).isGreaterThan(0)
+ assertThat(subject.size.height).isGreaterThan(0)
+ }
}
private fun TextLayoutResult.toBitmap() = Bitmap.createBitmap(
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldDrawPhaseToggleTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldDrawPhaseToggleTest.kt
index 4a8dfe9..21b9699 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldDrawPhaseToggleTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldDrawPhaseToggleTest.kt
@@ -182,7 +182,7 @@
* Instead of looking for an exact match of pixel values, this assertion provides the ability to
* judge each pixel individually to whether it fits a predefined filter.
*/
-private inline fun ImageBitmap.assertPixelConsistency(
+internal inline fun ImageBitmap.assertPixelConsistency(
filter: (color: Color) -> Boolean
) {
val pixel = toPixelMap()
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldLayoutPhaseToggleTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldLayoutPhaseToggleTest.kt
new file mode 100644
index 0000000..b7f00c2
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldLayoutPhaseToggleTest.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2024 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.text.input
+
+import android.os.Build
+import androidx.compose.foundation.background
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.TEST_FONT_FAMILY
+import androidx.compose.foundation.text.matchers.assertThat
+import androidx.compose.foundation.text.selection.fetchTextLayoutResult
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asAndroidBitmap
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.hasSetTextAction
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.sp
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+
+class BasicTextFieldLayoutPhaseToggleTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ private lateinit var state: TextFieldState
+
+ private val fontSize = 20.sp
+ private val textStyle = TextStyle(
+ fontSize = fontSize,
+ fontFamily = TEST_FONT_FAMILY
+ )
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ fun fontWeightChange_reflectsOnView() {
+ state = TextFieldState("abc")
+ var fontWeight by mutableStateOf(FontWeight.Normal)
+ rule.setContent {
+ BasicTextField(
+ state = state,
+ textStyle = TextStyle(fontWeight = fontWeight),
+ modifier = Modifier.background(Color.White)
+ )
+ }
+
+ val firstBitmap = rule.onNode(hasSetTextAction()).captureToImage().asAndroidBitmap()
+ val firstTextLayoutResult = rule.onNode(hasSetTextAction()).fetchTextLayoutResult()
+
+ assertThat(firstTextLayoutResult.layoutInput.style.fontWeight).isEqualTo(FontWeight.Normal)
+
+ fontWeight = FontWeight.Bold
+
+ val secondBitmap = rule.onNode(hasSetTextAction()).captureToImage().asAndroidBitmap()
+ val secondTextLayoutResult = rule.onNode(hasSetTextAction()).fetchTextLayoutResult()
+
+ assertThat(secondTextLayoutResult.layoutInput.style.fontWeight).isEqualTo(FontWeight.Bold)
+ assertThat(firstBitmap).isNotEqualToBitmap(secondBitmap)
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ fun textAlignChange_reflectsOnView() {
+ state = TextFieldState("abc")
+ var textAlign by mutableStateOf(TextAlign.End)
+ rule.setContent {
+ BasicTextField(
+ state = state,
+ textStyle = textStyle.copy(textAlign = textAlign),
+ modifier = Modifier.background(Color.White)
+ )
+ }
+
+ val firstBitmap = rule.onNode(hasSetTextAction()).captureToImage().asAndroidBitmap()
+ val firstTextLayoutResult = rule.onNode(hasSetTextAction()).fetchTextLayoutResult()
+
+ assertThat(firstTextLayoutResult.layoutInput.style.textAlign).isEqualTo(TextAlign.End)
+
+ textAlign = TextAlign.Start
+
+ val secondBitmap = rule.onNode(hasSetTextAction()).captureToImage().asAndroidBitmap()
+ val secondTextLayoutResult = rule.onNode(hasSetTextAction()).fetchTextLayoutResult()
+
+ assertThat(secondTextLayoutResult.layoutInput.style.textAlign).isEqualTo(TextAlign.Start)
+ assertThat(firstBitmap).isNotEqualToBitmap(secondBitmap)
+ }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/TextFieldLayoutStateCacheTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/TextFieldLayoutStateCacheTest.kt
index 9acdbc4..4efd4b4 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/TextFieldLayoutStateCacheTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/TextFieldLayoutStateCacheTest.kt
@@ -492,6 +492,7 @@
) { old, new ->
Truth.assertThat(old.layoutInput.style.fontSize).isEqualTo(12.sp)
Truth.assertThat(new.layoutInput.style.fontSize).isEqualTo(23.sp)
+ Truth.assertThat(old.multiParagraph).isNotSameInstanceAs(new.multiParagraph)
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtilsKtTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtilsKtTest.kt
index 8863ec7..d4efca6 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtilsKtTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtilsKtTest.kt
@@ -18,17 +18,44 @@
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Constraints.Companion.fitPrioritizingWidth
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
+/**
+ * Constraints are packed see [Constraints] implementation.
+ *
+ * These constants are the largest values that each slot can hold. The following pairings are the
+ * only ones allowed:
+ *
+ * (Big, Tiny), (Tiny, Big)
+ * (Medium, Small), (Small, Medium)
+ *
+ * For more information see [Constraints] implementation
+ */
+internal const val BigConstraintValue = (1 shl 18) - 1
+
@RunWith(AndroidJUnit4::class)
@SmallTest
class LayoutUtilsKtTest {
@Test
+ fun finalConstraints_doesntThrowWhenLarge() {
+ // this used to throw, ensure it doesn't
+ val subject = finalConstraints(
+ /* minWidth != maxWidth */
+ Constraints(0, 500, 0, BigConstraintValue - 1),
+ true /* width matters */,
+ TextOverflow.Ellipsis,
+ (BigConstraintValue - 1).toFloat()
+ )
+ assertThat(subject).isNotNull()
+ }
+
+ @Test
fun finalConstraints_returnsTightWidth() {
val subject = finalConstraints(
Constraints(500, 500, 0, 50),
@@ -99,7 +126,12 @@
while (1 shl b > 0) {
val height = 1 shl b
/* shouldn't crash */
- val constraints = Constraints.fixedCoerceHeightAndWidthForBits(width, height)
+ val constraints = fitPrioritizingWidth(
+ minWidth = width,
+ maxWidth = width,
+ minHeight = height,
+ maxHeight = height
+ )
println("$width $height => $constraints")
b++
}
@@ -107,48 +139,4 @@
a++
}
}
-
- @Test
- fun fixedCoerce_BigToTiny() {
- val subject = Constraints.fixedCoerceHeightAndWidthForBits(
- BigConstraintValue,
- BigConstraintValue
- )
- assertThat(subject).isEqualTo(
- Constraints.fixed(BigConstraintValue - 1, TinyConstraintValue - 1)
- )
- }
-
- @Test
- fun fixdCoerce_MediumToSmall() {
- val subject = Constraints.fixedCoerceHeightAndWidthForBits(
- MediumConstraintValue - 1,
- BigConstraintValue
- )
- assertThat(subject).isEqualTo(
- Constraints.fixed(MediumConstraintValue - 1, SmallConstraintValue - 1)
- )
- }
-
- @Test
- fun fixdCoerce_SmallToMedium() {
- val subject = Constraints.fixedCoerceHeightAndWidthForBits(
- SmallConstraintValue - 1,
- BigConstraintValue
- )
- assertThat(subject).isEqualTo(
- Constraints.fixed(SmallConstraintValue - 1, MediumConstraintValue - 1)
- )
- }
-
- @Test
- fun fixdCoerce_TinyToBig() {
- val subject = Constraints.fixedCoerceHeightAndWidthForBits(
- TinyConstraintValue - 1,
- BigConstraintValue
- )
- assertThat(subject).isEqualTo(
- Constraints.fixed(TinyConstraintValue - 1, BigConstraintValue - 1)
- )
- }
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
index 2cea5ef..28375ae 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
@@ -823,13 +823,12 @@
if ((this.onLongClick == null) != (onLongClick == null)) {
// Adding or removing longClick should cancel any existing press interactions
disposeInteractions()
+ // Adding or removing longClick should add / remove the corresponding property
+ invalidateSemantics()
resetPointerInputHandling = true
}
- if (this.onLongClick !== onLongClick) {
- this.onLongClick = onLongClick
- invalidateSemantics()
- }
+ this.onLongClick = onLongClick
if ((this.onDoubleClick == null) != (onDoubleClick == null)) {
resetPointerInputHandling = true
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
index f95084a..5db29b1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
@@ -455,71 +455,7 @@
}
}
- private val pointerInputNode = delegate(SuspendingPointerInputModifierNode {
- // TODO: conditionally undelegate when aosp/2462416 lands?
- if ([email protected]) return@SuspendingPointerInputModifierNode
- // re-create tracker when pointer input block restarts. This lazily creates the tracker
- // only when it is need.
- val velocityTracker = VelocityTracker()
- coroutineScope {
- try {
- awaitPointerEventScope {
- while (isActive) {
- awaitDownAndSlop(
- _canDrag,
- ::startDragImmediately,
- velocityTracker,
- pointerDirectionConfig
- )?.let {
- /**
- * The gesture crossed the touch slop, events are now relevant
- * and should be propagated
- */
- if (!isListeningForEvents) {
- if (channel == null) {
- channel = Channel(capacity = Channel.UNLIMITED)
- }
- startListeningForEvents()
- }
- var isDragSuccessful = false
- try {
- isDragSuccessful = awaitDrag(
- it.first,
- it.second,
- velocityTracker,
- channel
- ) { event ->
- pointerDirectionConfig.calculateDeltaChange(
- event.positionChangeIgnoreConsumed()
- ) != 0f
- }
- } catch (cancellation: CancellationException) {
- isDragSuccessful = false
- if (!isActive) throw cancellation
- } finally {
- val maximumVelocity = currentValueOf(LocalViewConfiguration)
- .maximumFlingVelocity
- val event = if (isDragSuccessful) {
- val velocity = velocityTracker.calculateVelocity(
- Velocity(maximumVelocity, maximumVelocity)
- )
- velocityTracker.resetTracking()
- DragStopped(velocity)
- } else {
- DragCancelled
- }
- channel?.trySend(event)
- }
- }
- }
- }
- } catch (exception: CancellationException) {
- if (!isActive) {
- throw exception
- }
- }
- }
- })
+ private var pointerInputNode: SuspendingPointerInputModifierNode? = null
override fun onDetach() {
isListeningForEvents = false
@@ -531,11 +467,80 @@
pass: PointerEventPass,
bounds: IntSize
) {
- pointerInputNode.onPointerEvent(pointerEvent, pass, bounds)
+ if (enabled && pointerInputNode == null) {
+ pointerInputNode = delegate(initializePointerInputNode())
+ }
+ pointerInputNode?.onPointerEvent(pointerEvent, pass, bounds)
+ }
+
+ private fun initializePointerInputNode(): SuspendingPointerInputModifierNode {
+ return SuspendingPointerInputModifierNode {
+ // re-create tracker when pointer input block restarts. This lazily creates the tracker
+ // only when it is need.
+ val velocityTracker = VelocityTracker()
+ coroutineScope {
+ try {
+ awaitPointerEventScope {
+ while (isActive) {
+ awaitDownAndSlop(
+ _canDrag,
+ ::startDragImmediately,
+ velocityTracker,
+ pointerDirectionConfig
+ )?.let {
+ /**
+ * The gesture crossed the touch slop, events are now relevant
+ * and should be propagated
+ */
+ if (!isListeningForEvents) {
+ if (channel == null) {
+ channel = Channel(capacity = Channel.UNLIMITED)
+ }
+ startListeningForEvents()
+ }
+ var isDragSuccessful = false
+ try {
+ isDragSuccessful = awaitDrag(
+ it.first,
+ it.second,
+ velocityTracker,
+ channel
+ ) { event ->
+ pointerDirectionConfig.calculateDeltaChange(
+ event.positionChangeIgnoreConsumed()
+ ) != 0f
+ }
+ } catch (cancellation: CancellationException) {
+ isDragSuccessful = false
+ if (!isActive) throw cancellation
+ } finally {
+ val maximumVelocity = currentValueOf(LocalViewConfiguration)
+ .maximumFlingVelocity
+ val event = if (isDragSuccessful) {
+ val velocity = velocityTracker.calculateVelocity(
+ Velocity(maximumVelocity, maximumVelocity)
+ )
+ velocityTracker.resetTracking()
+ DragStopped(velocity)
+ } else {
+ DragCancelled
+ }
+ channel?.trySend(event)
+ }
+ }
+ }
+ }
+ } catch (exception: CancellationException) {
+ if (!isActive) {
+ throw exception
+ }
+ }
+ }
+ }
}
override fun onCancelPointerInput() {
- pointerInputNode.onCancelPointerInput()
+ pointerInputNode?.onCancelPointerInput()
}
private suspend fun CoroutineScope.processDragStart(event: DragStarted) {
@@ -578,11 +583,14 @@
isResetPointerInputHandling: Boolean = false
) {
var resetPointerInputHandling = isResetPointerInputHandling
+
this.canDrag = canDrag
if (this.enabled != enabled) {
this.enabled = enabled
if (!enabled) {
disposeInteractionSource()
+ pointerInputNode?.let { undelegate(it) }
+ pointerInputNode = null
}
resetPointerInputHandling = true
}
@@ -592,7 +600,7 @@
}
if (resetPointerInputHandling) {
- pointerInputNode.resetPointerInputHandler()
+ pointerInputNode?.resetPointerInputHandler()
}
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
index 30babe0..5dcee03 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
@@ -81,6 +81,8 @@
consumedScroll = 0,
layoutWidth = layoutWidth,
layoutHeight = layoutHeight,
+ beforeContentPadding = beforeContentPadding,
+ afterContentPadding = afterContentPadding,
positionedItems = mutableListOf(),
keyIndexMap = measuredItemProvider.keyIndexMap,
itemProvider = measuredItemProvider,
@@ -352,6 +354,8 @@
consumedScroll = consumedScroll.toInt(),
layoutWidth = layoutWidth,
layoutHeight = layoutHeight,
+ beforeContentPadding = beforeContentPadding,
+ afterContentPadding = afterContentPadding,
positionedItems = positionedItems,
keyIndexMap = measuredItemProvider.keyIndexMap,
itemProvider = measuredItemProvider,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
index 8953e39..34f7ca9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
@@ -81,6 +81,8 @@
consumedScroll = 0,
layoutWidth = layoutWidth,
layoutHeight = layoutHeight,
+ beforeContentPadding = beforeContentPadding,
+ afterContentPadding = afterContentPadding,
positionedItems = mutableListOf(),
keyIndexMap = measuredItemProvider.keyIndexMap,
itemProvider = measuredItemProvider,
@@ -319,6 +321,8 @@
consumedScroll = consumedScroll.toInt(),
layoutWidth = layoutWidth,
layoutHeight = layoutHeight,
+ beforeContentPadding = beforeContentPadding,
+ afterContentPadding = afterContentPadding,
positionedItems = positionedItems,
keyIndexMap = measuredItemProvider.keyIndexMap,
itemProvider = measuredItemProvider,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemAnimator.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemAnimator.kt
index f1adbb1..612ba62 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemAnimator.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemAnimator.kt
@@ -71,6 +71,8 @@
consumedScroll: Int,
layoutWidth: Int,
layoutHeight: Int,
+ beforeContentPadding: Int,
+ afterContentPadding: Int,
positionedItems: MutableList<T>,
keyIndexMap: LazyLayoutKeyIndexMap,
itemProvider: LazyLayoutMeasuredItemProvider<T>,
@@ -186,8 +188,10 @@
movingInFromEndBound.sortBy { previousKeyToIndexMap.getIndex(it.key) }
movingInFromEndBound.fastForEach { item ->
val accumulatedOffset = accumulatedOffsetPerLane.updateAndReturnOffsetFor(item)
- val mainAxisOffset =
- mainAxisLayoutSize + accumulatedOffset - item.mainAxisSizeWithSpacings
+ // Compensate content padding
+ val contentPadding = beforeContentPadding + afterContentPadding
+ val mainAxisOffset = mainAxisLayoutSize + accumulatedOffset -
+ item.mainAxisSizeWithSpacings + contentPadding
initializeAnimation(item, mainAxisOffset)
startPlacementAnimationsIfNeeded(item)
}
@@ -285,7 +289,8 @@
positionedItems.last()
.let { it.mainAxisOffset }
else {
- mainAxisLayoutSize - item.mainAxisSizeWithSpacings
+ val contentPadding = beforeContentPadding + afterContentPadding
+ mainAxisLayoutSize - item.mainAxisSizeWithSpacings + contentPadding
} + accumulatedOffset
val itemInfo = keyToItemInfoMap[item.key]!!
@@ -390,19 +395,20 @@
return maxOffset
}
- val minSizeToFitDisappearingItems: IntSize get() {
- var size = IntSize.Zero
- disappearingItems.fastForEach {
- val layer = it.layer
- if (layer != null) {
- size = IntSize(
- width = maxOf(size.width, it.rawOffset.x + layer.size.width),
- height = maxOf(size.height, it.rawOffset.y + layer.size.height)
- )
+ val minSizeToFitDisappearingItems: IntSize
+ get() {
+ var size = IntSize.Zero
+ disappearingItems.fastForEach {
+ val layer = it.layer
+ if (layer != null) {
+ size = IntSize(
+ width = maxOf(size.width, it.rawOffset.x + layer.size.width),
+ height = maxOf(size.height, it.rawOffset.y + layer.size.height)
+ )
+ }
}
+ return size
}
- return size
- }
val modifier: Modifier = DisplayingDisappearingItemsElement(this)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
index eeb9286..5928a6e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
@@ -263,6 +263,8 @@
consumedScroll = 0,
layoutWidth = layoutWidth,
layoutHeight = layoutHeight,
+ beforeContentPadding = beforeContentPadding,
+ afterContentPadding = afterContentPadding,
positionedItems = mutableListOf(),
keyIndexMap = measuredItemProvider.keyIndexMap,
itemProvider = measuredItemProvider,
@@ -870,6 +872,8 @@
consumedScroll = consumedScroll.toInt(),
layoutWidth = layoutWidth,
layoutHeight = layoutHeight,
+ beforeContentPadding = beforeContentPadding,
+ afterContentPadding = afterContentPadding,
positionedItems = positionedItems,
keyIndexMap = measuredItemProvider.keyIndexMap,
itemProvider = measuredItemProvider,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
index 4fecf94..1b48576 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
@@ -21,7 +21,6 @@
import androidx.compose.foundation.text.modifiers.TextAnnotatedStringElement
import androidx.compose.foundation.text.modifiers.TextAnnotatedStringNode
import androidx.compose.foundation.text.modifiers.TextStringSimpleElement
-import androidx.compose.foundation.text.modifiers.fixedCoerceHeightAndWidthForBits
import androidx.compose.foundation.text.modifiers.hasLinks
import androidx.compose.foundation.text.selection.LocalSelectionRegistrar
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
@@ -53,6 +52,7 @@
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Constraints.Companion.fitPrioritizingWidth
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.util.fastFilter
import androidx.compose.ui.util.fastForEach
@@ -453,9 +453,11 @@
textRangeLayoutMeasureScope.measure()
}
val placeable = measurable.measure(
- Constraints.fixedCoerceHeightAndWidthForBits(
- rangeMeasureResult.width,
- rangeMeasureResult.height
+ fitPrioritizingWidth(
+ minWidth = rangeMeasureResult.width,
+ maxWidth = rangeMeasureResult.width,
+ minHeight = rangeMeasureResult.height,
+ maxHeight = rangeMeasureResult.height
)
)
Pair(placeable, rangeMeasureResult.place)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextDelegate.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextDelegate.kt
index 7320768..dc8e0d5 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextDelegate.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextDelegate.kt
@@ -199,7 +199,12 @@
return MultiParagraph(
intrinsics = nonNullIntrinsics,
- constraints = Constraints(maxWidth = width, maxHeight = constraints.maxHeight),
+ constraints = Constraints.fitPrioritizingWidth(
+ minWidth = 0,
+ maxWidth = width,
+ minHeight = 0,
+ maxHeight = constraints.maxHeight
+ ),
// This is a fallback behavior for ellipsis. Native
maxLines = finalMaxLines,
ellipsis = overflow == TextOverflow.Ellipsis
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextLinkScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextLinkScope.kt
index 2703b2d..14af807 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextLinkScope.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextLinkScope.kt
@@ -201,7 +201,7 @@
uriHandler: UriHandler
) {
when (link) {
- is LinkAnnotation.Url -> link.linkInteractionListener?.onClicked(link) ?: try {
+ is LinkAnnotation.Url -> link.linkInteractionListener?.onClick(link) ?: try {
uriHandler.openUri(link.url)
} catch (_: IllegalArgumentException) {
// we choose to silently fail when the uri can't be opened to avoid crashes
@@ -209,7 +209,7 @@
// handlers themselves and therefore I suspect are less likely to test them
// manually.
}
- is LinkAnnotation.Clickable -> link.linkInteractionListener?.onClicked(link)
+ is LinkAnnotation.Clickable -> link.linkInteractionListener?.onClick(link)
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldLayoutStateCache.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldLayoutStateCache.kt
index 547eeac..8426da0 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldLayoutStateCache.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldLayoutStateCache.kt
@@ -165,12 +165,17 @@
cachedRecord.constraints == measureInputs.constraints &&
cachedRecord.fontFamilyResolver == measureInputs.fontFamilyResolver
) {
+ val isLayoutAffectingSame = cachedRecord.textStyle
+ ?.hasSameLayoutAffectingAttributes(nonMeasureInputs.textStyle) ?: false
+
+ val isDrawAffectingSame = cachedRecord.textStyle
+ ?.hasSameDrawAffectingAttributes(nonMeasureInputs.textStyle) ?: false
+
// Fast path: None of the inputs changed.
- if (cachedRecord.textStyle == nonMeasureInputs.textStyle) return cachedResult
+ if (isLayoutAffectingSame && isDrawAffectingSame) return cachedResult
+
// Slightly slower than fast path: Layout did not change but TextLayoutInput did
- if (cachedRecord.textStyle
- ?.hasSameDrawAffectingAttributes(nonMeasureInputs.textStyle) == true
- ) {
+ if (isLayoutAffectingSame) {
return cachedResult.copy(
layoutInput = TextLayoutInput(
cachedResult.layoutInput.text,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt
index ed89579..201baaf 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt
@@ -29,7 +29,9 @@
overflow: TextOverflow,
maxIntrinsicWidth: Float
): Constraints = Constraints(
+ minWidth = 0,
maxWidth = finalMaxWidth(constraints, softWrap, overflow, maxIntrinsicWidth),
+ minHeight = 0,
maxHeight = constraints.maxHeight
)
@@ -86,48 +88,3 @@
val overwriteMaxLines = !softWrap && overflow == TextOverflow.Ellipsis
return if (overwriteMaxLines) 1 else maxLinesIn.coerceAtLeast(1)
}
-
-/**
- * Constraints are packed see [Constraints] implementation.
- *
- * These constants are the largest values that each slot can hold. The following pairings are the
- * only ones allowed:
- *
- * (Big, Tiny), (Tiny, Big)
- * (Medium, Small), (Small, Medium)
- *
- * For more information see [Constraints] implementation
- */
-internal const val BigConstraintValue = (1 shl 18) - 1
-internal const val MediumConstraintValue = (1 shl 16) - 1
-internal const val SmallConstraintValue = (1 shl 15) - 1
-internal const val TinyConstraintValue = (1 shl 13) - 1
-
-/**
- * Make constraints that never throw from being too large. Prefer to keep accurate width information
- * first, then constrain height based on the size of width.
- *
- * This will return a Constraint with the same or smaller dimensions than the passed (width, height)
- *
- * see b/312294386 for more details
- *
- * This particular logic is text specific, so not generalizing.
- *
- * @param width desired width (has priority)
- * @param height desired height (uses the remaining bits after width)
- *
- * @return a safe Constraint that never throws for running out of bits
- */
-internal fun Constraints.Companion.fixedCoerceHeightAndWidthForBits(
- width: Int,
- height: Int
-): Constraints {
- val safeWidth = minOf(width, BigConstraintValue - 1)
- val safeHeight = when {
- safeWidth < TinyConstraintValue -> minOf(height, BigConstraintValue - 1)
- safeWidth < SmallConstraintValue -> minOf(height, MediumConstraintValue - 1)
- safeWidth < MediumConstraintValue -> minOf(height, SmallConstraintValue - 1)
- else -> minOf(height, TinyConstraintValue - 1)
- }
- return fixed(safeWidth, safeHeight)
-}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
index 6496137..79212ae 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
@@ -60,6 +60,7 @@
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Constraints.Companion.fitPrioritizingWidth
import androidx.compose.ui.unit.Density
import androidx.compose.ui.util.fastRoundToInt
@@ -442,9 +443,11 @@
// then allow children to measure _inside_ our final box, with the above placeholders
val placeable = measurable.measure(
- Constraints.fixedCoerceHeightAndWidthForBits(
- width = textLayoutResult.size.width,
- height = textLayoutResult.size.height
+ fitPrioritizingWidth(
+ minWidth = textLayoutResult.size.width,
+ maxWidth = textLayoutResult.size.width,
+ minHeight = textLayoutResult.size.height,
+ maxHeight = textLayoutResult.size.height
)
)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt
index 8ce81b6..e3ce8a7 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt
@@ -55,6 +55,7 @@
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Constraints.Companion.fitPrioritizingWidth
import androidx.compose.ui.unit.Density
import androidx.compose.ui.util.fastRoundToInt
@@ -359,9 +360,11 @@
// then allow children to measure _inside_ our final box, with the above placeholders
val placeable = measurable.measure(
- Constraints.fixedCoerceHeightAndWidthForBits(
- layoutSize.width,
- layoutSize.height
+ fitPrioritizingWidth(
+ minWidth = layoutSize.width,
+ maxWidth = layoutSize.width,
+ minHeight = layoutSize.height,
+ maxHeight = layoutSize.height
)
)
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SliderSample.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SliderSample.kt
index e2e6f8b..0e014ff 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SliderSample.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SliderSample.kt
@@ -17,6 +17,8 @@
package androidx.compose.material.samples
import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.MaterialTheme
import androidx.compose.material.RangeSlider
@@ -28,34 +30,43 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import kotlin.math.roundToInt
@Sampled
@Composable
fun SliderSample() {
var sliderPosition by remember { mutableStateOf(0f) }
- Text(text = sliderPosition.toString())
- Slider(value = sliderPosition, onValueChange = { sliderPosition = it })
+ Column(modifier = Modifier.padding(horizontal = 16.dp)) {
+ Text(text = "%.2f".format(sliderPosition))
+ Slider(value = sliderPosition, onValueChange = { sliderPosition = it })
+ }
}
@Sampled
@Composable
fun StepsSliderSample() {
var sliderPosition by remember { mutableStateOf(0f) }
- Text(text = sliderPosition.toString())
- Slider(
- value = sliderPosition,
- onValueChange = { sliderPosition = it },
- valueRange = 0f..100f,
- onValueChangeFinished = {
- // launch some business logic update with the state you hold
- // viewModel.updateSelectedSliderValue(sliderPosition)
- },
- steps = 5,
- colors = SliderDefaults.colors(
- thumbColor = MaterialTheme.colors.secondary,
- activeTrackColor = MaterialTheme.colors.secondary
+ Column(modifier = Modifier.padding(horizontal = 16.dp)) {
+ Text(text = sliderPosition.roundToInt().toString())
+ Slider(
+ value = sliderPosition,
+ onValueChange = { sliderPosition = it },
+ valueRange = 0f..100f,
+ onValueChangeFinished = {
+ // launch some business logic update with the state you hold
+ // viewModel.updateSelectedSliderValue(sliderPosition)
+ },
+ // Only allow multiples of 10. Excluding the endpoints of `valueRange`,
+ // there are 9 steps (10, 20, ..., 90).
+ steps = 9,
+ colors = SliderDefaults.colors(
+ thumbColor = MaterialTheme.colors.secondary,
+ activeTrackColor = MaterialTheme.colors.secondary
+ )
)
- )
+ }
}
@Sampled
@@ -63,16 +74,20 @@
@OptIn(ExperimentalMaterialApi::class)
fun RangeSliderSample() {
var sliderPosition by remember { mutableStateOf(0f..100f) }
- Text(text = sliderPosition.toString())
- RangeSlider(
- value = sliderPosition,
- onValueChange = { sliderPosition = it },
- valueRange = 0f..100f,
- onValueChangeFinished = {
- // launch some business logic update with the state you hold
- // viewModel.updateSelectedSliderValue(sliderPosition)
- },
- )
+ Column(modifier = Modifier.padding(horizontal = 16.dp)) {
+ val rangeStart = "%.2f".format(sliderPosition.start)
+ val rangeEnd = "%.2f".format(sliderPosition.endInclusive)
+ Text(text = "$rangeStart .. $rangeEnd")
+ RangeSlider(
+ value = sliderPosition,
+ onValueChange = { sliderPosition = it },
+ valueRange = 0f..100f,
+ onValueChangeFinished = {
+ // launch some business logic update with the state you hold
+ // viewModel.updateSelectedSliderValue(sliderPosition)
+ },
+ )
+ }
}
@Sampled
@@ -80,19 +95,25 @@
@OptIn(ExperimentalMaterialApi::class)
fun StepRangeSliderSample() {
var sliderPosition by remember { mutableStateOf(0f..100f) }
- Text(text = sliderPosition.toString())
- RangeSlider(
- steps = 5,
- value = sliderPosition,
- onValueChange = { sliderPosition = it },
- valueRange = 0f..100f,
- onValueChangeFinished = {
- // launch some business logic update with the state you hold
- // viewModel.updateSelectedSliderValue(sliderPosition)
- },
- colors = SliderDefaults.colors(
- thumbColor = MaterialTheme.colors.secondary,
- activeTrackColor = MaterialTheme.colors.secondary
+ Column(modifier = Modifier.padding(horizontal = 16.dp)) {
+ val rangeStart = sliderPosition.start.roundToInt()
+ val rangeEnd = sliderPosition.endInclusive.roundToInt()
+ Text(text = "$rangeStart .. $rangeEnd")
+ RangeSlider(
+ value = sliderPosition,
+ onValueChange = { sliderPosition = it },
+ valueRange = 0f..100f,
+ onValueChangeFinished = {
+ // launch some business logic update with the state you hold
+ // viewModel.updateSelectedSliderValue(sliderPosition)
+ },
+ // Only allow multiples of 10. Excluding the endpoints of `valueRange`,
+ // there are 9 steps (10, 20, ..., 90).
+ steps = 9,
+ colors = SliderDefaults.colors(
+ thumbColor = MaterialTheme.colors.secondary,
+ activeTrackColor = MaterialTheme.colors.secondary
+ )
)
- )
+ }
}
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
index 3004819..85fc6f7 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
@@ -132,9 +132,9 @@
* @param enabled whether or not component is enabled and can be interacted with or not
* @param valueRange range of values that Slider value can take. Passed [value] will be coerced to
* this range
- * @param steps if greater than 0, specifies the amounts of discrete values, evenly distributed
- * between across the whole value range. If 0, slider will behave as a continuous slider and allow
- * to choose any value from the range specified. Must not be negative.
+ * @param steps if positive, specifies the amount of discrete allowable values (in addition to the
+ * endpoints of the value range). Step values are evenly distributed across the range. If 0, the
+ * slider will behave continuously and allow any value from the range. Must not be negative.
* @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
* shouldn't be used to update the slider value (use [onValueChange] for that), but rather to
* know when the user has completed selecting a new value by ending a drag or a click.
@@ -282,9 +282,9 @@
* @param enabled whether or not component is enabled and can we interacted with or not
* @param valueRange range of values that Range Slider values can take. Passed [value] will be
* coerced to this range
- * @param steps if greater than 0, specifies the amounts of discrete values, evenly distributed
- * between across the whole value range. If 0, range slider will behave as a continuous slider and
- * allow to choose any values from the range specified. Must not be negative.
+ * @param steps if positive, specifies the amount of discrete allowable values (in addition to the
+ * endpoints of the value range). Step values are evenly distributed across the range. If 0, the
+ * range slider will behave continuously and allow any value from the range. Must not be negative.
* @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
* shouldn't be used to update the range slider values (use [onValueChange] for that), but rather to
* know when the user has completed selecting a new value by ending a drag or a click.
diff --git a/compose/material3/adaptive/adaptive-layout/api/current.txt b/compose/material3/adaptive/adaptive-layout/api/current.txt
index 9d1a055..cf79bdb 100644
--- a/compose/material3/adaptive/adaptive-layout/api/current.txt
+++ b/compose/material3/adaptive/adaptive-layout/api/current.txt
@@ -38,7 +38,7 @@
method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void ListDetailPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> listPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> detailPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit>? extraPane);
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class ListDetailPaneScaffoldRole {
+ public final class ListDetailPaneScaffoldRole {
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getDetail();
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getExtra();
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getList();
@@ -63,7 +63,7 @@
method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void AnimatedPane(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.AnimatedPaneScope,kotlin.Unit> content);
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Immutable public final class PaneScaffoldDirective {
+ @androidx.compose.runtime.Immutable public final class PaneScaffoldDirective {
ctor public PaneScaffoldDirective(int maxHorizontalPartitions, float horizontalPartitionSpacerSize, int maxVerticalPartitions, float verticalPartitionSpacerSize, float defaultPanePreferredWidth, java.util.List<androidx.compose.ui.geometry.Rect> excludedBounds);
method public androidx.compose.material3.adaptive.layout.PaneScaffoldDirective copy(optional int maxHorizontalPartitions, optional float horizontalPartitionSpacerSize, optional int maxVerticalPartitions, optional float verticalPartitionSpacerSize, optional float defaultPanePreferredWidth, optional java.util.List<androidx.compose.ui.geometry.Rect> excludedBounds);
method public float getDefaultPanePreferredWidth();
@@ -91,7 +91,7 @@
method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldScope {
+ public sealed interface PaneScaffoldScope {
method public androidx.compose.ui.Modifier preferredWidth(androidx.compose.ui.Modifier, float width);
}
@@ -127,7 +127,7 @@
property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole pane;
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public enum ThreePaneScaffoldRole {
+ public enum ThreePaneScaffoldRole {
enum_constant public static final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Primary;
enum_constant public static final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Secondary;
enum_constant public static final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Tertiary;
diff --git a/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt b/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
index 9d1a055..cf79bdb 100644
--- a/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
+++ b/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
@@ -38,7 +38,7 @@
method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void ListDetailPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> listPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> detailPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit>? extraPane);
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class ListDetailPaneScaffoldRole {
+ public final class ListDetailPaneScaffoldRole {
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getDetail();
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getExtra();
method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole getList();
@@ -63,7 +63,7 @@
method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void AnimatedPane(androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.AnimatedPaneScope,kotlin.Unit> content);
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Immutable public final class PaneScaffoldDirective {
+ @androidx.compose.runtime.Immutable public final class PaneScaffoldDirective {
ctor public PaneScaffoldDirective(int maxHorizontalPartitions, float horizontalPartitionSpacerSize, int maxVerticalPartitions, float verticalPartitionSpacerSize, float defaultPanePreferredWidth, java.util.List<androidx.compose.ui.geometry.Rect> excludedBounds);
method public androidx.compose.material3.adaptive.layout.PaneScaffoldDirective copy(optional int maxHorizontalPartitions, optional float horizontalPartitionSpacerSize, optional int maxVerticalPartitions, optional float verticalPartitionSpacerSize, optional float defaultPanePreferredWidth, optional java.util.List<androidx.compose.ui.geometry.Rect> excludedBounds);
method public float getDefaultPanePreferredWidth();
@@ -91,7 +91,7 @@
method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public static androidx.compose.material3.adaptive.layout.PaneScaffoldDirective calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(androidx.compose.material3.adaptive.WindowAdaptiveInfo windowAdaptiveInfo, optional int verticalHingePolicy);
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public sealed interface PaneScaffoldScope {
+ public sealed interface PaneScaffoldScope {
method public androidx.compose.ui.Modifier preferredWidth(androidx.compose.ui.Modifier, float width);
}
@@ -127,7 +127,7 @@
property public final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole pane;
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public enum ThreePaneScaffoldRole {
+ public enum ThreePaneScaffoldRole {
enum_constant public static final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Primary;
enum_constant public static final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Secondary;
enum_constant public static final androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole Tertiary;
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/LargeScreenTestUtils.kt b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/LargeScreenTestUtils.kt
index 5dfa0175..37edf98c 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/LargeScreenTestUtils.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/LargeScreenTestUtils.kt
@@ -19,7 +19,6 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
-import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -30,7 +29,6 @@
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.toSize
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
internal fun ComposeContentTestRule.setContentWithSimulatedSize(
simulatedWidth: Dp,
simulatedHeight: Dp,
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt
index 829f4a2..f738efb 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt
@@ -166,7 +166,6 @@
}
}
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private val MockScaffoldDirective = PaneScaffoldDirective.Default
internal const val ThreePaneScaffoldTestTag = "SampleThreePaneScaffold"
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirectiveTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirectiveTest.kt
index 52b3b90..f7b506b 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirectiveTest.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirectiveTest.kt
@@ -268,18 +268,21 @@
private val hingeList = listOf(
HingeInfo(
bounds = Rect(0F, 0F, 1F, 1F),
+ isFlat = true,
isVertical = true,
isSeparating = false,
isOccluding = true
),
HingeInfo(
bounds = Rect(1F, 1F, 2F, 2F),
+ isFlat = false,
isVertical = true,
isSeparating = false,
isOccluding = true
),
HingeInfo(
bounds = Rect(2F, 2F, 3F, 3F),
+ isFlat = true,
isVertical = true,
isSeparating = true,
isOccluding = false
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ListDetailPaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ListDetailPaneScaffold.kt
index 74f39ac..6ab8f53 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ListDetailPaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ListDetailPaneScaffold.kt
@@ -95,7 +95,6 @@
* of three pane scaffolds. We suggest you to use the values defined here instead of the raw
* [ThreePaneScaffoldRole] under the context of [ListDetailPaneScaffold] for better code clarity.
*/
-@ExperimentalMaterial3AdaptiveApi
object ListDetailPaneScaffoldRole {
/**
* The list pane of [ListDetailPaneScaffold], which is supposed to hold a list of item summaries
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
index e164e2f..b840410 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
@@ -16,7 +16,6 @@
package androidx.compose.material3.adaptive.layout
-import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.ParentDataModifierNode
@@ -29,7 +28,6 @@
/**
* Scope for the panes of pane scaffolds.
*/
-@ExperimentalMaterial3AdaptiveApi
sealed interface PaneScaffoldScope {
/**
* This modifier specifies the preferred width for a pane, and the pane scaffold implementation
@@ -43,7 +41,6 @@
fun Modifier.preferredWidth(width: Dp): Modifier
}
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
internal abstract class PaneScaffoldScopeImpl : PaneScaffoldScope {
override fun Modifier.preferredWidth(width: Dp): Modifier {
require(width == Dp.Unspecified || width > 0.dp) { "invalid width" }
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirective.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirective.kt
index 8ba2159..8072c34 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirective.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirective.kt
@@ -126,7 +126,6 @@
}
}
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun getExcludedVerticalBounds(posture: Posture, hingePolicy: HingePolicy): List<Rect> {
return when (hingePolicy) {
HingePolicy.AvoidSeparating -> posture.separatingVerticalHingeBounds
@@ -155,9 +154,7 @@
* @property excludedBounds the bounds of all areas in the window that the layout needs to avoid
* displaying anything upon it. Usually these bounds represent where physical hinges are.
*/
-@ExperimentalMaterial3AdaptiveApi
@Immutable
-// TODO(conradchen): Hide the constructor. Please use the copy() method instead.
class PaneScaffoldDirective(
val maxHorizontalPartitions: Int,
val horizontalPartitionSpacerSize: Dp,
@@ -190,8 +187,7 @@
maxVerticalPartitions: Int = this.maxVerticalPartitions,
verticalPartitionSpacerSize: Dp = this.verticalPartitionSpacerSize,
defaultPanePreferredWidth: Dp = this.defaultPanePreferredWidth,
- @Suppress("ListIterator") // No guarantee to be an array list
- excludedBounds: List<Rect> = this.excludedBounds.toList()
+ excludedBounds: List<Rect> = this.excludedBounds
): PaneScaffoldDirective = PaneScaffoldDirective(
maxHorizontalPartitions,
horizontalPartitionSpacerSize,
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
index 86e729e..6e92343 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
@@ -580,7 +580,6 @@
}
}
- @OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun MutableList<PaneMeasurable>.createPaneMeasurableIfNeeded(
measurables: List<Measurable>,
priority: Int,
@@ -632,7 +631,6 @@
)
}
- @OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun Placeable.PlacementScope.measureAndPlacePanesWithLocalBounds(
partitionBounds: IntRect,
spacerSize: Int,
@@ -747,7 +745,6 @@
}
}
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private class PaneMeasurable(
val measurable: Measurable,
val priority: Int,
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldHorizontalOrder.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldHorizontalOrder.kt
index 589c663..a672945 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldHorizontalOrder.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldHorizontalOrder.kt
@@ -111,7 +111,6 @@
/**
* The set of the available pane roles of [ThreePaneScaffold].
*/
-@ExperimentalMaterial3AdaptiveApi
enum class ThreePaneScaffoldRole {
/**
* The primary pane of [ThreePaneScaffold]. It is supposed to have the highest priority during
diff --git a/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/ListDetailPaneScaffoldNavigatorTest.kt b/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/ListDetailPaneScaffoldNavigatorTest.kt
index 5218d92..6307eda 100644
--- a/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/ListDetailPaneScaffoldNavigatorTest.kt
+++ b/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/ListDetailPaneScaffoldNavigatorTest.kt
@@ -566,10 +566,8 @@
}
}
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private val MockSinglePaneScaffoldDirective = PaneScaffoldDirective.Default
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private val MockDualPaneScaffoldDirective = PaneScaffoldDirective.Default.copy(
maxHorizontalPartitions = 2,
horizontalPartitionSpacerSize = 16.dp,
diff --git a/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/SupportingPaneScaffoldNavigatorTest.kt b/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/SupportingPaneScaffoldNavigatorTest.kt
index d270567..baa76ed 100644
--- a/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/SupportingPaneScaffoldNavigatorTest.kt
+++ b/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/SupportingPaneScaffoldNavigatorTest.kt
@@ -580,10 +580,8 @@
}
}
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private val MockSinglePaneScaffoldDirective = PaneScaffoldDirective.Default
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private val MockDualPaneScaffoldDirective = PaneScaffoldDirective.Default.copy(
maxHorizontalPartitions = 2,
horizontalPartitionSpacerSize = 16.dp,
diff --git a/compose/material3/adaptive/adaptive/api/current.txt b/compose/material3/adaptive/adaptive/api/current.txt
index 6fe8f2a..4532ffe 100644
--- a/compose/material3/adaptive/adaptive/api/current.txt
+++ b/compose/material3/adaptive/adaptive/api/current.txt
@@ -15,12 +15,14 @@
}
@androidx.compose.runtime.Immutable public final class HingeInfo {
- ctor public HingeInfo(androidx.compose.ui.geometry.Rect bounds, boolean isVertical, boolean isSeparating, boolean isOccluding);
+ ctor public HingeInfo(androidx.compose.ui.geometry.Rect bounds, boolean isFlat, boolean isVertical, boolean isSeparating, boolean isOccluding);
method public androidx.compose.ui.geometry.Rect getBounds();
+ method public boolean isFlat();
method public boolean isOccluding();
method public boolean isSeparating();
method public boolean isVertical();
property public final androidx.compose.ui.geometry.Rect bounds;
+ property public final boolean isFlat;
property public final boolean isOccluding;
property public final boolean isSeparating;
property public final boolean isVertical;
diff --git a/compose/material3/adaptive/adaptive/api/restricted_current.txt b/compose/material3/adaptive/adaptive/api/restricted_current.txt
index 6fe8f2a..4532ffe 100644
--- a/compose/material3/adaptive/adaptive/api/restricted_current.txt
+++ b/compose/material3/adaptive/adaptive/api/restricted_current.txt
@@ -15,12 +15,14 @@
}
@androidx.compose.runtime.Immutable public final class HingeInfo {
- ctor public HingeInfo(androidx.compose.ui.geometry.Rect bounds, boolean isVertical, boolean isSeparating, boolean isOccluding);
+ ctor public HingeInfo(androidx.compose.ui.geometry.Rect bounds, boolean isFlat, boolean isVertical, boolean isSeparating, boolean isOccluding);
method public androidx.compose.ui.geometry.Rect getBounds();
+ method public boolean isFlat();
method public boolean isOccluding();
method public boolean isSeparating();
method public boolean isVertical();
property public final androidx.compose.ui.geometry.Rect bounds;
+ property public final boolean isFlat;
property public final boolean isOccluding;
property public final boolean isSeparating;
property public final boolean isVertical;
diff --git a/compose/material3/adaptive/adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CollectFoldingFeaturesAsStateTest.kt b/compose/material3/adaptive/adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CollectFoldingFeaturesAsStateTest.kt
index 1eb4823..26a5113 100644
--- a/compose/material3/adaptive/adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CollectFoldingFeaturesAsStateTest.kt
+++ b/compose/material3/adaptive/adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CollectFoldingFeaturesAsStateTest.kt
@@ -33,7 +33,6 @@
import org.junit.rules.TestRule
import org.junit.runner.RunWith
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class CollectFoldingFeaturesAsStateTest {
diff --git a/compose/material3/adaptive/adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CollectWindowSizeAsStateTest.kt b/compose/material3/adaptive/adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CollectWindowSizeAsStateTest.kt
index 8ff84ab..4a62e9d 100644
--- a/compose/material3/adaptive/adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CollectWindowSizeAsStateTest.kt
+++ b/compose/material3/adaptive/adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CollectWindowSizeAsStateTest.kt
@@ -37,7 +37,6 @@
import org.junit.Test
import org.junit.runner.RunWith
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class CollectWindowSizeAsStateTest {
diff --git a/compose/material3/adaptive/adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/AndroidPosture.android.kt b/compose/material3/adaptive/adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/AndroidPosture.android.kt
index 61ad91e..7907e41 100644
--- a/compose/material3/adaptive/adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/AndroidPosture.android.kt
+++ b/compose/material3/adaptive/adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/AndroidPosture.android.kt
@@ -35,6 +35,7 @@
}
hingeList.add(HingeInfo(
bounds = it.bounds.toComposeRect(),
+ isFlat = it.state == FoldingFeature.State.FLAT,
isVertical = it.orientation == FoldingFeature.Orientation.VERTICAL,
isSeparating = it.isSeparating,
isOccluding = it.occlusionType == FoldingFeature.OcclusionType.FULL
diff --git a/compose/material3/adaptive/adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/Posture.kt b/compose/material3/adaptive/adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/Posture.kt
index 1510287..349405b 100644
--- a/compose/material3/adaptive/adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/Posture.kt
+++ b/compose/material3/adaptive/adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/Posture.kt
@@ -100,6 +100,8 @@
* A class that contains the info of a hinge relevant to a [Posture].
*
* @param bounds the bounds of the hinge in the relevant viewport.
+ * @param isFlat `true` if the hinge is fully open and the relevant window space presented to the
+ * user is flat.
* @param isVertical `true` if the hinge is a vertical one, i.e., it separates the viewport into
* left and right; `false` if the hinge is horizontal, i.e., it separates the viewport
* into top and bottom.
@@ -109,6 +111,7 @@
@Immutable
class HingeInfo(
val bounds: Rect,
+ val isFlat: Boolean,
val isVertical: Boolean,
val isSeparating: Boolean,
val isOccluding: Boolean
@@ -117,6 +120,7 @@
if (this === other) return true
if (other !is HingeInfo) return false
if (bounds != other.bounds) return false
+ if (isFlat != other.isFlat) return false
if (isVertical != other.isVertical) return false
if (isSeparating != other.isSeparating) return false
if (isOccluding != other.isOccluding) return false
@@ -125,6 +129,7 @@
override fun hashCode(): Int {
var result = bounds.hashCode()
+ result = 31 * result + isFlat.hashCode()
result = 31 * result + isVertical.hashCode()
result = 31 * result + isSeparating.hashCode()
result = 31 * result + isOccluding.hashCode()
@@ -133,6 +138,7 @@
override fun toString(): String {
return "HingeInfo(bounds=$bounds, " +
+ "isFlat=$isFlat, " +
"isVertical=$isVertical, " +
"isSeparating=$isSeparating, " +
"isOccluding=$isOccluding)"
diff --git a/compose/material3/adaptive/benchmark/src/androidTest/java/androidx/compose/material3/adaptive/benchmark/TestUtils.kt b/compose/material3/adaptive/benchmark/src/androidTest/java/androidx/compose/material3/adaptive/benchmark/TestUtils.kt
index fd44e30..217aa37 100644
--- a/compose/material3/adaptive/benchmark/src/androidTest/java/androidx/compose/material3/adaptive/benchmark/TestUtils.kt
+++ b/compose/material3/adaptive/benchmark/src/androidTest/java/androidx/compose/material3/adaptive/benchmark/TestUtils.kt
@@ -34,10 +34,8 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
val singlePaneDirective = PaneScaffoldDirective.Default
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
val dualPaneDirective = PaneScaffoldDirective.Default.copy(
maxHorizontalPartitions = 2,
horizontalPartitionSpacerSize = 24.dp,
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/NavigationSuiteScaffoldBenchmarkTest.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/NavigationSuiteScaffoldBenchmarkTest.kt
index 6b2b165..16a22cc 100644
--- a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/NavigationSuiteScaffoldBenchmarkTest.kt
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/NavigationSuiteScaffoldBenchmarkTest.kt
@@ -19,7 +19,6 @@
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableIntState
@@ -59,7 +58,6 @@
}
}
-@OptIn(ExperimentalMaterial3AdaptiveNavigationSuiteApi::class)
internal class NavigationSuiteScaffoldTestCase : LayeredComposeTestCase(), ToggleableTestCase {
private lateinit var selectedIndexState: MutableIntState
diff --git a/compose/material3/material3-adaptive-navigation-suite/api/current.txt b/compose/material3/material3-adaptive-navigation-suite/api/current.txt
index 7a7bc4a..83d23c0 100644
--- a/compose/material3/material3-adaptive-navigation-suite/api/current.txt
+++ b/compose/material3/material3-adaptive-navigation-suite/api/current.txt
@@ -4,7 +4,7 @@
@SuppressCompatibility @kotlin.RequiresOptIn(message="This material3-adaptive-navigation-suite API is experimental and is likely to" + "change or to be removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalMaterial3AdaptiveNavigationSuiteApi {
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteColors {
+ public final class NavigationSuiteColors {
method public long getNavigationBarContainerColor();
method public long getNavigationBarContentColor();
method public long getNavigationDrawerContainerColor();
@@ -19,13 +19,13 @@
property public final long navigationRailContentColor;
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteDefaults {
+ public final class NavigationSuiteDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteColors colors(optional long navigationBarContainerColor, optional long navigationBarContentColor, optional long navigationRailContainerColor, optional long navigationRailContentColor, optional long navigationDrawerContainerColor, optional long navigationDrawerContentColor);
method @androidx.compose.runtime.Composable public androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItemColors itemColors(optional androidx.compose.material3.NavigationBarItemColors navigationBarItemColors, optional androidx.compose.material3.NavigationRailItemColors navigationRailItemColors, optional androidx.compose.material3.NavigationDrawerItemColors navigationDrawerItemColors);
field public static final androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaults INSTANCE;
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteItemColors {
+ public final class NavigationSuiteItemColors {
ctor public NavigationSuiteItemColors(androidx.compose.material3.NavigationBarItemColors navigationBarItemColors, androidx.compose.material3.NavigationRailItemColors navigationRailItemColors, androidx.compose.material3.NavigationDrawerItemColors navigationDrawerItemColors);
method public androidx.compose.material3.NavigationBarItemColors getNavigationBarItemColors();
method public androidx.compose.material3.NavigationDrawerItemColors getNavigationDrawerItemColors();
@@ -35,7 +35,7 @@
property public final androidx.compose.material3.NavigationRailItemColors navigationRailItemColors;
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteScaffoldDefaults {
+ public final class NavigationSuiteScaffoldDefaults {
method public String calculateFromAdaptiveInfo(androidx.compose.material3.adaptive.WindowAdaptiveInfo adaptiveInfo);
method @androidx.compose.runtime.Composable public long getContainerColor();
method @androidx.compose.runtime.Composable public long getContentColor();
@@ -45,16 +45,16 @@
}
public final class NavigationSuiteScaffoldKt {
- method @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuite(optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteColors colors, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScope,kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScope,kotlin.Unit> navigationSuiteItems, optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteColors navigationSuiteColors, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffoldLayout(kotlin.jvm.functions.Function0<kotlin.Unit> navigationSuite, optional String layoutType, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void NavigationSuite(optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteColors colors, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void NavigationSuiteScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScope,kotlin.Unit> navigationSuiteItems, optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteColors navigationSuiteColors, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void NavigationSuiteScaffoldLayout(kotlin.jvm.functions.Function0<kotlin.Unit> navigationSuite, optional String layoutType, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public sealed interface NavigationSuiteScope {
+ public sealed interface NavigationSuiteScope {
method public void item(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional boolean alwaysShowLabel, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItemColors? colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @kotlin.jvm.JvmInline public final value class NavigationSuiteType {
+ @kotlin.jvm.JvmInline public final value class NavigationSuiteType {
field public static final androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType.Companion Companion;
}
diff --git a/compose/material3/material3-adaptive-navigation-suite/api/restricted_current.txt b/compose/material3/material3-adaptive-navigation-suite/api/restricted_current.txt
index 7a7bc4a..83d23c0 100644
--- a/compose/material3/material3-adaptive-navigation-suite/api/restricted_current.txt
+++ b/compose/material3/material3-adaptive-navigation-suite/api/restricted_current.txt
@@ -4,7 +4,7 @@
@SuppressCompatibility @kotlin.RequiresOptIn(message="This material3-adaptive-navigation-suite API is experimental and is likely to" + "change or to be removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalMaterial3AdaptiveNavigationSuiteApi {
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteColors {
+ public final class NavigationSuiteColors {
method public long getNavigationBarContainerColor();
method public long getNavigationBarContentColor();
method public long getNavigationDrawerContainerColor();
@@ -19,13 +19,13 @@
property public final long navigationRailContentColor;
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteDefaults {
+ public final class NavigationSuiteDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteColors colors(optional long navigationBarContainerColor, optional long navigationBarContentColor, optional long navigationRailContainerColor, optional long navigationRailContentColor, optional long navigationDrawerContainerColor, optional long navigationDrawerContentColor);
method @androidx.compose.runtime.Composable public androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItemColors itemColors(optional androidx.compose.material3.NavigationBarItemColors navigationBarItemColors, optional androidx.compose.material3.NavigationRailItemColors navigationRailItemColors, optional androidx.compose.material3.NavigationDrawerItemColors navigationDrawerItemColors);
field public static final androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaults INSTANCE;
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteItemColors {
+ public final class NavigationSuiteItemColors {
ctor public NavigationSuiteItemColors(androidx.compose.material3.NavigationBarItemColors navigationBarItemColors, androidx.compose.material3.NavigationRailItemColors navigationRailItemColors, androidx.compose.material3.NavigationDrawerItemColors navigationDrawerItemColors);
method public androidx.compose.material3.NavigationBarItemColors getNavigationBarItemColors();
method public androidx.compose.material3.NavigationDrawerItemColors getNavigationDrawerItemColors();
@@ -35,7 +35,7 @@
property public final androidx.compose.material3.NavigationRailItemColors navigationRailItemColors;
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteScaffoldDefaults {
+ public final class NavigationSuiteScaffoldDefaults {
method public String calculateFromAdaptiveInfo(androidx.compose.material3.adaptive.WindowAdaptiveInfo adaptiveInfo);
method @androidx.compose.runtime.Composable public long getContainerColor();
method @androidx.compose.runtime.Composable public long getContentColor();
@@ -45,16 +45,16 @@
}
public final class NavigationSuiteScaffoldKt {
- method @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuite(optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteColors colors, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScope,kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScope,kotlin.Unit> navigationSuiteItems, optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteColors navigationSuiteColors, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffoldLayout(kotlin.jvm.functions.Function0<kotlin.Unit> navigationSuite, optional String layoutType, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void NavigationSuite(optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteColors colors, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void NavigationSuiteScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScope,kotlin.Unit> navigationSuiteItems, optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteColors navigationSuiteColors, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void NavigationSuiteScaffoldLayout(kotlin.jvm.functions.Function0<kotlin.Unit> navigationSuite, optional String layoutType, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public sealed interface NavigationSuiteScope {
+ public sealed interface NavigationSuiteScope {
method public void item(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional boolean alwaysShowLabel, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItemColors? colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
}
- @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @kotlin.jvm.JvmInline public final value class NavigationSuiteType {
+ @kotlin.jvm.JvmInline public final value class NavigationSuiteType {
field public static final androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType.Companion Companion;
}
diff --git a/compose/material3/material3-adaptive-navigation-suite/samples/src/main/java/androidx/compose/material3/adaptive/navigationsuite/samples/NavigationSuiteScaffoldSamples.kt b/compose/material3/material3-adaptive-navigation-suite/samples/src/main/java/androidx/compose/material3/adaptive/navigationsuite/samples/NavigationSuiteScaffoldSamples.kt
index 45d63a4..8326319 100644
--- a/compose/material3/material3-adaptive-navigation-suite/samples/src/main/java/androidx/compose/material3/adaptive/navigationsuite/samples/NavigationSuiteScaffoldSamples.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/samples/src/main/java/androidx/compose/material3/adaptive/navigationsuite/samples/NavigationSuiteScaffoldSamples.kt
@@ -27,7 +27,6 @@
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
-import androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuite
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults
@@ -43,10 +42,7 @@
import androidx.compose.ui.unit.dp
import androidx.window.core.layout.WindowWidthSizeClass
-@OptIn(
- ExperimentalMaterial3AdaptiveApi::class,
- ExperimentalMaterial3AdaptiveNavigationSuiteApi::class
-)
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Preview
@Sampled
@Composable
@@ -76,10 +72,7 @@
}
}
-@OptIn(
- ExperimentalMaterial3AdaptiveApi::class,
- ExperimentalMaterial3AdaptiveNavigationSuiteApi::class
-)
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Preview
@Sampled
@Composable
@@ -117,10 +110,7 @@
}
}
-@OptIn(
- ExperimentalMaterial3AdaptiveApi::class,
- ExperimentalMaterial3AdaptiveNavigationSuiteApi::class
-)
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Preview
@Sampled
@Composable
diff --git a/compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffoldTest.kt b/compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffoldTest.kt
index 737aa7f..11dfa5f 100644
--- a/compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffoldTest.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffoldTest.kt
@@ -34,7 +34,6 @@
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
-@OptIn(ExperimentalMaterial3AdaptiveNavigationSuiteApi::class)
@RunWith(JUnit4::class)
class NavigationSuiteScaffoldTest {
@get:Rule
@@ -92,7 +91,6 @@
}
}
-@OptIn(ExperimentalMaterial3AdaptiveNavigationSuiteApi::class)
@Composable
private fun SampleNavigationSuiteScaffoldLayout(
layoutType: NavigationSuiteType
diff --git a/compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteTest.kt b/compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteTest.kt
index df34599..ea3718c 100644
--- a/compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteTest.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteTest.kt
@@ -28,7 +28,6 @@
import org.junit.Test
import org.junit.runner.RunWith
-@OptIn(ExperimentalMaterial3AdaptiveNavigationSuiteApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class NavigationSuiteTest {
@@ -47,7 +46,6 @@
}
}
-@OptIn(ExperimentalMaterial3AdaptiveNavigationSuiteApi::class)
@Composable
private fun SampleNavigationSuite(
layoutType: NavigationSuiteType
diff --git a/compose/material3/material3-adaptive-navigation-suite/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffoldTest.kt b/compose/material3/material3-adaptive-navigation-suite/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffoldTest.kt
index 0183840..12ec173 100644
--- a/compose/material3/material3-adaptive-navigation-suite/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffoldTest.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffoldTest.kt
@@ -25,8 +25,7 @@
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
-@OptIn(ExperimentalMaterial3AdaptiveNavigationSuiteApi::class,
- ExperimentalMaterial3AdaptiveApi::class)
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@RunWith(JUnit4::class)
class NavigationSuiteScaffoldTest {
diff --git a/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt b/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt
index e2055fb..b951ec3 100644
--- a/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt
@@ -42,7 +42,6 @@
import androidx.compose.material3.PermanentDrawerSheet
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
-import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.contentColorFor
@@ -84,8 +83,6 @@
* passed in [content] lambda inside the navigation suite scaffold.
* @param content the content of your screen
*/
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
-@ExperimentalMaterial3AdaptiveNavigationSuiteApi
@Composable
fun NavigationSuiteScaffold(
navigationSuiteItems: NavigationSuiteScope.() -> Unit,
@@ -142,8 +139,6 @@
* [NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo]
* @param content the content of your screen
*/
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
-@ExperimentalMaterial3AdaptiveNavigationSuiteApi
@Composable
fun NavigationSuiteScaffoldLayout(
navigationSuite: @Composable () -> Unit,
@@ -221,8 +216,6 @@
* @param content the content inside the current navigation component, typically
* [NavigationSuiteScope.item]s
*/
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
-@ExperimentalMaterial3AdaptiveNavigationSuiteApi
@Composable
fun NavigationSuite(
modifier: Modifier = Modifier,
@@ -310,7 +303,6 @@
}
/** The scope associated with the [NavigationSuiteScope]. */
-@ExperimentalMaterial3AdaptiveNavigationSuiteApi
sealed interface NavigationSuiteScope {
/**
@@ -360,7 +352,6 @@
* The [NavigationSuiteType] informs the [NavigationSuite] of what navigation component to expect.
*/
@JvmInline
-@ExperimentalMaterial3AdaptiveNavigationSuiteApi
value class NavigationSuiteType private constructor(private val description: String) {
override fun toString(): String {
return description
@@ -400,7 +391,6 @@
}
/** Contains the default values used by the [NavigationSuiteScaffold]. */
-@ExperimentalMaterial3AdaptiveNavigationSuiteApi
object NavigationSuiteScaffoldDefaults {
/**
* Returns the expected [NavigationSuiteType] according to the provided [WindowAdaptiveInfo].
@@ -409,7 +399,6 @@
* @param adaptiveInfo the provided [WindowAdaptiveInfo]
* @see NavigationSuiteScaffold
*/
- @OptIn(ExperimentalMaterial3AdaptiveApi::class)
fun calculateFromAdaptiveInfo(adaptiveInfo: WindowAdaptiveInfo): NavigationSuiteType {
return with(adaptiveInfo) {
if (windowPosture.isTabletop ||
@@ -434,7 +423,6 @@
}
/** Contains the default values used by the [NavigationSuite]. */
-@ExperimentalMaterial3AdaptiveNavigationSuiteApi
object NavigationSuiteDefaults {
/**
* Creates a [NavigationSuiteColors] with the provided colors for the container color, according
@@ -519,7 +507,6 @@
* @param navigationDrawerContentColor the content color for the [PermanentDrawerSheet] of the
* [NavigationSuite]
*/
-@ExperimentalMaterial3AdaptiveNavigationSuiteApi
class NavigationSuiteColors
internal constructor(
val navigationBarContainerColor: Color,
@@ -543,14 +530,12 @@
* @param navigationDrawerItemColors the [NavigationDrawerItemColors] associated with the
* [NavigationDrawerItem] of the [NavigationSuiteScope.item]
*/
-@ExperimentalMaterial3AdaptiveNavigationSuiteApi
-class NavigationSuiteItemColors constructor(
+class NavigationSuiteItemColors(
val navigationBarItemColors: NavigationBarItemColors,
val navigationRailItemColors: NavigationRailItemColors,
val navigationDrawerItemColors: NavigationDrawerItemColors,
)
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
internal val WindowAdaptiveInfoDefault
@Composable
get() = currentWindowAdaptiveInfo()
@@ -560,7 +545,6 @@
val itemList: MutableVector<NavigationSuiteItem>
}
-@OptIn(ExperimentalMaterial3AdaptiveNavigationSuiteApi::class)
private class NavigationSuiteItem(
val selected: Boolean,
val onClick: () -> Unit,
@@ -575,7 +559,6 @@
val interactionSource: MutableInteractionSource
)
-@OptIn(ExperimentalMaterial3AdaptiveNavigationSuiteApi::class)
private class NavigationSuiteScopeImpl : NavigationSuiteScope,
NavigationSuiteItemProvider {
@@ -614,7 +597,6 @@
get() = itemList.size
}
-@OptIn(ExperimentalMaterial3AdaptiveNavigationSuiteApi::class)
@Composable
private fun rememberStateOfItems(
content: NavigationSuiteScope.() -> Unit
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index 26756e5..cfed154 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -996,22 +996,24 @@
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable public final class ModalBottomSheetDefaults {
- method public androidx.compose.material3.ModalBottomSheetProperties properties(optional androidx.compose.ui.window.SecureFlagPolicy securePolicy, optional boolean isFocusable, optional boolean shouldDismissOnBackPress);
+ method public androidx.compose.material3.ModalBottomSheetProperties getProperties();
+ method @Deprecated public androidx.compose.material3.ModalBottomSheetProperties properties(optional androidx.compose.ui.window.SecureFlagPolicy securePolicy, optional boolean isFocusable, optional boolean shouldDismissOnBackPress);
+ property public final androidx.compose.material3.ModalBottomSheetProperties properties;
field public static final androidx.compose.material3.ModalBottomSheetDefaults INSTANCE;
}
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class ModalBottomSheetProperties {
- ctor public ModalBottomSheetProperties(androidx.compose.ui.window.SecureFlagPolicy securePolicy, boolean isFocusable, boolean shouldDismissOnBackPress);
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable public final class ModalBottomSheetProperties {
+ ctor public ModalBottomSheetProperties(optional androidx.compose.ui.window.SecureFlagPolicy securePolicy, optional boolean shouldDismissOnBackPress);
+ ctor @Deprecated public ModalBottomSheetProperties(androidx.compose.ui.window.SecureFlagPolicy securePolicy, boolean isFocusable, boolean shouldDismissOnBackPress);
method public androidx.compose.ui.window.SecureFlagPolicy getSecurePolicy();
method public boolean getShouldDismissOnBackPress();
- method public boolean isFocusable();
- property public final boolean isFocusable;
property public final androidx.compose.ui.window.SecureFlagPolicy securePolicy;
property public final boolean shouldDismissOnBackPress;
}
public final class ModalBottomSheet_androidKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ModalBottomSheet(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SheetState sheetState, optional float sheetMaxWidth, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, optional float tonalElevation, optional long scrimColor, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dragHandle, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.ModalBottomSheetProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ModalBottomSheet(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SheetState sheetState, optional float sheetMaxWidth, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, optional float tonalElevation, optional long scrimColor, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dragHandle, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.ModalBottomSheetProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ModalBottomSheet(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SheetState sheetState, optional float sheetMaxWidth, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, optional float tonalElevation, optional long scrimColor, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dragHandle, optional kotlin.jvm.functions.Function0<? extends androidx.compose.foundation.layout.WindowInsets> contentWindowInsets, optional androidx.compose.material3.ModalBottomSheetProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.SheetState rememberModalBottomSheetState(optional boolean skipPartiallyExpanded, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SheetValue,java.lang.Boolean> confirmValueChange);
}
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index 26756e5..cfed154 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -996,22 +996,24 @@
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable public final class ModalBottomSheetDefaults {
- method public androidx.compose.material3.ModalBottomSheetProperties properties(optional androidx.compose.ui.window.SecureFlagPolicy securePolicy, optional boolean isFocusable, optional boolean shouldDismissOnBackPress);
+ method public androidx.compose.material3.ModalBottomSheetProperties getProperties();
+ method @Deprecated public androidx.compose.material3.ModalBottomSheetProperties properties(optional androidx.compose.ui.window.SecureFlagPolicy securePolicy, optional boolean isFocusable, optional boolean shouldDismissOnBackPress);
+ property public final androidx.compose.material3.ModalBottomSheetProperties properties;
field public static final androidx.compose.material3.ModalBottomSheetDefaults INSTANCE;
}
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class ModalBottomSheetProperties {
- ctor public ModalBottomSheetProperties(androidx.compose.ui.window.SecureFlagPolicy securePolicy, boolean isFocusable, boolean shouldDismissOnBackPress);
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable public final class ModalBottomSheetProperties {
+ ctor public ModalBottomSheetProperties(optional androidx.compose.ui.window.SecureFlagPolicy securePolicy, optional boolean shouldDismissOnBackPress);
+ ctor @Deprecated public ModalBottomSheetProperties(androidx.compose.ui.window.SecureFlagPolicy securePolicy, boolean isFocusable, boolean shouldDismissOnBackPress);
method public androidx.compose.ui.window.SecureFlagPolicy getSecurePolicy();
method public boolean getShouldDismissOnBackPress();
- method public boolean isFocusable();
- property public final boolean isFocusable;
property public final androidx.compose.ui.window.SecureFlagPolicy securePolicy;
property public final boolean shouldDismissOnBackPress;
}
public final class ModalBottomSheet_androidKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ModalBottomSheet(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SheetState sheetState, optional float sheetMaxWidth, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, optional float tonalElevation, optional long scrimColor, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dragHandle, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.ModalBottomSheetProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ModalBottomSheet(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SheetState sheetState, optional float sheetMaxWidth, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, optional float tonalElevation, optional long scrimColor, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dragHandle, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.ModalBottomSheetProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ModalBottomSheet(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.SheetState sheetState, optional float sheetMaxWidth, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, optional float tonalElevation, optional long scrimColor, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dragHandle, optional kotlin.jvm.functions.Function0<? extends androidx.compose.foundation.layout.WindowInsets> contentWindowInsets, optional androidx.compose.material3.ModalBottomSheetProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.SheetState rememberModalBottomSheetState(optional boolean skipPartiallyExpanded, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.SheetValue,java.lang.Boolean> confirmValueChange);
}
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/common/CatalogScaffold.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/common/CatalogScaffold.kt
index 6156e44..7a50700 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/common/CatalogScaffold.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/common/CatalogScaffold.kt
@@ -17,7 +17,6 @@
package androidx.compose.material3.catalog.library.ui.common
import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
@@ -93,7 +92,6 @@
ModalBottomSheet(
onDismissRequest = { openThemePicker = false },
sheetState = sheetState,
- windowInsets = WindowInsets(0),
content = {
ThemePicker(
theme = theme,
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/BottomSheetSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/BottomSheetSamples.kt
index ae1e46b..b720202 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/BottomSheetSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/BottomSheetSamples.kt
@@ -23,7 +23,6 @@
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -34,7 +33,6 @@
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Menu
-import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
@@ -73,8 +71,7 @@
@Composable
fun ModalBottomSheetSample() {
var openBottomSheet by rememberSaveable { mutableStateOf(false) }
- var skipPartiallyExpanded by remember { mutableStateOf(false) }
- var edgeToEdgeEnabled by remember { mutableStateOf(false) }
+ var skipPartiallyExpanded by rememberSaveable { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val bottomSheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = skipPartiallyExpanded
@@ -96,17 +93,6 @@
Spacer(Modifier.width(16.dp))
Text("Skip partially expanded State")
}
- Row(
- Modifier.toggleable(
- value = edgeToEdgeEnabled,
- role = Role.Checkbox,
- onValueChange = { checked -> edgeToEdgeEnabled = checked }
- )
- ) {
- Checkbox(checked = edgeToEdgeEnabled, onCheckedChange = null)
- Spacer(Modifier.width(16.dp))
- Text("Toggle edge to edge enabled")
- }
Button(
onClick = { openBottomSheet = !openBottomSheet },
modifier = Modifier.align(Alignment.CenterHorizontally)
@@ -117,15 +103,11 @@
// Sheet content
if (openBottomSheet) {
- val windowInsets = if (edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
ModalBottomSheet(
onDismissRequest = { openBottomSheet = false },
sheetState = bottomSheetState,
- windowInsets = windowInsets
) {
-
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
Button(
// Note: If you provide logic outside of onDismissRequest to remove the sheet,
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSamples.kt
index 34aa79f..d763b89 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSamples.kt
@@ -137,7 +137,9 @@
// launch some business logic update with the state you hold
// viewModel.updateSelectedSliderValue(sliderPosition)
},
- steps = 4
+ // Only allow multiples of 10. Excluding the endpoints of `valueRange`,
+ // there are 9 steps (10, 20, ..., 90).
+ steps = 9
)
}
}
@@ -344,7 +346,9 @@
// launch some business logic update with the state you hold
// viewModel.updateSelectedSliderValue(sliderPosition)
},
- steps = 4
+ // Only allow multiples of 10. Excluding the endpoints of `valueRange`,
+ // there are 9 steps (10, 20, ..., 90).
+ steps = 9
)
}
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
index a3464b2..6bdf964 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
@@ -57,11 +57,12 @@
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertHeightIsEqualTo
import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
import androidx.compose.ui.test.assertWidthIsEqualTo
import androidx.compose.ui.test.getUnclippedBoundsInRoot
-import androidx.compose.ui.test.isPopup
+import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.onNodeWithTag
@@ -79,6 +80,7 @@
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.height
import androidx.compose.ui.unit.width
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
@@ -96,12 +98,11 @@
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
@MediumTest
-@RunWith(Parameterized::class)
+@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalMaterial3Api::class)
-class ModalBottomSheetTest(private val edgeToEdgeWrapper: EdgeToEdgeWrapper) {
+class ModalBottomSheetTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@@ -119,14 +120,10 @@
val sheetState = SheetState(skipPartiallyExpanded = false, density = rule.density)
rule.setContent {
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
-
if (showBottomSheet) {
ModalBottomSheet(
sheetState = sheetState,
onDismissRequest = { showBottomSheet = false },
- windowInsets = windowInsets
) {
Box(
Modifier
@@ -141,7 +138,7 @@
// Tap Scrim
val outsideY = with(rule.density) {
- rule.onAllNodes(isPopup()).onFirst().getUnclippedBoundsInRoot().height.roundToPx() / 4
+ rule.onAllNodes(isDialog()).onFirst().getUnclippedBoundsInRoot().height.roundToPx() / 4
}
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).click(0, outsideY)
rule.waitForIdle()
@@ -156,14 +153,10 @@
val sheetState = SheetState(skipPartiallyExpanded = false, density = rule.density)
rule.setContent {
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
-
if (showBottomSheet) {
ModalBottomSheet(
sheetState = sheetState,
onDismissRequest = { showBottomSheet = false },
- windowInsets = windowInsets
) {
Box(
Modifier
@@ -194,12 +187,9 @@
rule.setContent {
val context = LocalContext.current
screenWidth = context.resources.displayMetrics.widthPixels
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
ModalBottomSheet(
onDismissRequest = {},
- windowInsets = windowInsets
) {
Box(
Modifier
@@ -235,11 +225,8 @@
try {
latch.await(1500, TimeUnit.MILLISECONDS)
rule.setContent {
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
ModalBottomSheet(
onDismissRequest = {},
- windowInsets = windowInsets
) {
Box(
Modifier
@@ -249,7 +236,7 @@
}
}
rule.waitForIdle()
- val simulatedRootWidth = rule.onNode(isPopup()).getUnclippedBoundsInRoot().width
+ val simulatedRootWidth = rule.onNode(isDialog()).getUnclippedBoundsInRoot().width
val maxSheetWidth = 640.dp
val expectedSheetWidth = maxSheetWidth.coerceAtMost(simulatedRootWidth)
// Our sheet should be max 640 dp but fill the width if the container is less wide
@@ -299,12 +286,9 @@
rule.setContent {
val context = LocalContext.current
screenWidthPx = context.resources.displayMetrics.widthPixels
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
ModalBottomSheet(
onDismissRequest = {},
sheetMaxWidth = Dp.Unspecified,
- windowInsets = windowInsets
) {
Box(
Modifier
@@ -330,17 +314,13 @@
var height by mutableStateOf(0.dp)
rule.setContent {
- val config = LocalContext.current.resources.configuration
- height = config.screenHeightDp.dp
sheetState = rememberModalBottomSheetState()
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
ModalBottomSheet(
onDismissRequest = {},
sheetState = sheetState,
dragHandle = null,
- windowInsets = windowInsets
+ contentWindowInsets = { WindowInsets(0) }
) {
Box(
Modifier
@@ -351,7 +331,7 @@
}
}
- height = rule.onNode(isPopup()).getUnclippedBoundsInRoot().height
+ height = rule.onNode(isDialog()).getUnclippedBoundsInRoot().height
assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
rule.onNodeWithTag(sheetTag).assertTopPositionInRootIsEqualTo(height - sheetHeight)
}
@@ -363,12 +343,9 @@
rule.setContent {
sheetState = rememberModalBottomSheetState()
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
ModalBottomSheet(
onDismissRequest = {},
sheetState = sheetState,
- windowInsets = windowInsets
) {
Box(
Modifier
@@ -380,7 +357,7 @@
}
rule.waitForIdle()
screenHeightPx = with(rule.density) {
- rule.onNode(isPopup()).getUnclippedBoundsInRoot().height.toPx()
+ rule.onNode(isDialog()).getUnclippedBoundsInRoot().height.toPx()
}
assertThat(sheetState.targetValue).isEqualTo(SheetValue.PartiallyExpanded)
assertThat(sheetState.requireOffset())
@@ -395,13 +372,10 @@
rule.setContent {
val dispatcher = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
if (showBottomSheet) {
ModalBottomSheet(
sheetState = sheetState,
onDismissRequest = { showBottomSheet = false },
- windowInsets = windowInsets
) {
Box(
Modifier
@@ -435,13 +409,10 @@
rule.setContent {
val dispatcher = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
if (showBottomSheet) {
ModalBottomSheet(
sheetState = sheetState,
onDismissRequest = { showBottomSheet = false },
- windowInsets = windowInsets
) {
Box(
Modifier
@@ -481,13 +452,11 @@
val context = LocalContext.current
screenHeight = context.resources.configuration.screenHeightDp.dp
state = rememberModalBottomSheetState()
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
ModalBottomSheet(
onDismissRequest = {},
sheetState = state,
dragHandle = null,
- windowInsets = windowInsets
+ contentWindowInsets = { WindowInsets(0) }
) {
Box(
Modifier
@@ -496,7 +465,7 @@
)
}
}
- screenHeight = rule.onNode(isPopup()).getUnclippedBoundsInRoot().height
+ screenHeight = rule.onNode(isDialog()).getUnclippedBoundsInRoot().height
assertThat(state.requireOffset()).isWithin(1f).of(expectedExpandedAnchor)
size = 100.dp
@@ -513,8 +482,6 @@
lateinit var sheetMaxWidth: MutableState<Dp>
var screenWidth by mutableStateOf(0.dp)
rule.setContent {
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
sheetMaxWidth = remember { mutableStateOf(0.dp) }
val context = LocalContext.current
val density = LocalDensity.current
@@ -522,7 +489,6 @@
ModalBottomSheet(
onDismissRequest = {},
sheetMaxWidth = sheetMaxWidth.value,
- windowInsets = windowInsets
) {
Box(
Modifier
@@ -547,14 +513,12 @@
rule.setContent {
state = rememberModalBottomSheetState()
scope = rememberCoroutineScope()
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
ModalBottomSheet(
onDismissRequest = {},
sheetState = state,
dragHandle = null,
- windowInsets = windowInsets
+ contentWindowInsets = { WindowInsets(0) }
) {}
}
assertThat(state.anchoredDraggableState.currentValue).isEqualTo(SheetValue.Hidden)
@@ -575,14 +539,11 @@
lateinit var scope: CoroutineScope
rule.setContent {
state = rememberModalBottomSheetState()
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
-
ModalBottomSheet(
onDismissRequest = {},
sheetState = state,
dragHandle = null,
- windowInsets = windowInsets
+ contentWindowInsets = { WindowInsets(0) }
) {
scope = rememberCoroutineScope()
LazyColumn {
@@ -630,7 +591,7 @@
sheetState = rememberModalBottomSheetState()
ModalBottomSheet(
onDismissRequest = {},
- sheetState = sheetState
+ sheetState = sheetState,
) {
scrollState = rememberScrollState()
Column(
@@ -700,14 +661,10 @@
rule.setContent {
scope = rememberCoroutineScope()
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
-
ModalBottomSheet(
onDismissRequest = {},
modifier = Modifier.testTag(topTag),
sheetState = sheetState,
- windowInsets = windowInsets
) {
if (showShortContent) {
Box(
@@ -748,12 +705,9 @@
lateinit var sheetState: SheetState
rule.setContent {
sheetState = rememberModalBottomSheetState()
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
ModalBottomSheet(
onDismissRequest = {},
sheetState = sheetState,
- windowInsets = windowInsets
) {
Box(
Modifier
@@ -784,8 +738,6 @@
newState != SheetValue.Hidden
}
)
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
ModalBottomSheet(
onDismissRequest = {},
@@ -797,7 +749,6 @@
.size(dragHandleSize)
)
},
- windowInsets = windowInsets
) {
Box(
Modifier
@@ -827,7 +778,7 @@
// Tap Scrim
val outsideY = with(rule.density) {
- rule.onAllNodes(isPopup()).onFirst().getUnclippedBoundsInRoot().height.roundToPx() / 4
+ rule.onAllNodes(isDialog()).onFirst().getUnclippedBoundsInRoot().height.roundToPx() / 4
}
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).click(0, outsideY)
rule.waitForIdle()
@@ -843,12 +794,9 @@
rule.setContent {
sheetState = rememberModalBottomSheetState()
scope = rememberCoroutineScope()
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
ModalBottomSheet(
onDismissRequest = {},
sheetState = sheetState,
- windowInsets = windowInsets
) {
Box(
Modifier
@@ -880,13 +828,10 @@
lateinit var sheetState: SheetState
rule.setContent {
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
ModalBottomSheet(
onDismissRequest = {},
sheetState = sheetState,
- windowInsets = windowInsets
) {
Box(
Modifier
@@ -916,12 +861,9 @@
lateinit var sheetState: SheetState
rule.setContent {
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
ModalBottomSheet(
onDismissRequest = {},
sheetState = sheetState,
- windowInsets = windowInsets
) {
Box(
Modifier
@@ -946,13 +888,9 @@
)
rule.setContent {
scope = rememberCoroutineScope()
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
-
ModalBottomSheet(
onDismissRequest = {},
sheetState = bottomSheetState,
- windowInsets = windowInsets
) {
Box(
Modifier
@@ -976,8 +914,6 @@
@Test
fun modalBottomSheet_testDismissAction_tallBottomSheet_whenPartiallyExpanded() {
rule.setContent {
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
ModalBottomSheet(
onDismissRequest = {},
dragHandle = {
@@ -987,7 +923,6 @@
.size(dragHandleSize)
)
},
- windowInsets = windowInsets
) {
Box(
Modifier
@@ -1009,8 +944,6 @@
lateinit var sheetState: SheetState
rule.setContent {
sheetState = rememberModalBottomSheetState()
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
ModalBottomSheet(
onDismissRequest = {},
sheetState = sheetState,
@@ -1021,7 +954,6 @@
.size(dragHandleSize)
)
},
- windowInsets = windowInsets
) {
Box(
Modifier
@@ -1052,9 +984,6 @@
rule.setContent {
sheetState = rememberModalBottomSheetState()
scope = rememberCoroutineScope()
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
-
ModalBottomSheet(
onDismissRequest = {},
sheetState = sheetState,
@@ -1065,7 +994,6 @@
.size(dragHandleSize)
)
},
- windowInsets = windowInsets
) {
Box(
Modifier
@@ -1075,7 +1003,7 @@
}
}
screenHeightPx = with(rule.density) {
- rule.onNode(isPopup()).getUnclippedBoundsInRoot().height.toPx()
+ rule.onNode(isDialog()).getUnclippedBoundsInRoot().height.toPx()
}
scope.launch {
sheetState.expand()
@@ -1103,9 +1031,6 @@
rule.setContent {
sheetState = rememberModalBottomSheetState()
scope = rememberCoroutineScope()
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
-
ModalBottomSheet(
onDismissRequest = {},
sheetState = sheetState,
@@ -1116,7 +1041,6 @@
.size(dragHandleSize)
)
},
- windowInsets = windowInsets
) {
Box(
Modifier
@@ -1126,7 +1050,7 @@
}
}
screenHeightPx = with(rule.density) {
- rule.onNode(isPopup()).getUnclippedBoundsInRoot().height.toPx()
+ rule.onNode(isDialog()).getUnclippedBoundsInRoot().height.toPx()
}
scope.launch {
sheetState.expand()
@@ -1151,14 +1075,12 @@
lateinit var scope: CoroutineScope
rule.setContent {
scope = rememberCoroutineScope()
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
ModalBottomSheet(
onDismissRequest = {},
sheetState = sheetState,
dragHandle = null,
- windowInsets = windowInsets
+ contentWindowInsets = { WindowInsets(0) }
) {
if (hasSheetContent) {
Box(Modifier.fillMaxHeight(0.4f))
@@ -1190,14 +1112,11 @@
lateinit var scope: CoroutineScope
rule.setContent {
scope = rememberCoroutineScope()
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
-
ModalBottomSheet(
onDismissRequest = {},
sheetState = sheetState,
dragHandle = null,
- windowInsets = windowInsets
+ contentWindowInsets = { WindowInsets(0) }
) {
if (hasSheetContent) {
Box(Modifier.fillMaxHeight(0.6f))
@@ -1236,13 +1155,9 @@
rule.setContent {
scope = rememberCoroutineScope()
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
-
ModalBottomSheet(
onDismissRequest = { callCount += 1 },
sheetState = sheetState,
- windowInsets = windowInsets
) {
Column(
Modifier
@@ -1288,14 +1203,10 @@
lateinit var sheetState: SheetState
rule.setContent {
- val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
- WindowInsets(0) else BottomSheetDefaults.windowInsets
sheetState = rememberModalBottomSheetState()
ModalBottomSheet(
sheetState = sheetState,
onDismissRequest = {},
- windowInsets = windowInsets,
- properties = ModalBottomSheetDefaults.properties(isFocusable = true)
) {
Box(Modifier.testTag(sheetTag)) {
TextField(
@@ -1311,7 +1222,7 @@
rule.mainClock.autoAdvance = false
val textFieldNode = rule.onNodeWithTag(textFieldTag)
- var sheetNode = rule.onNodeWithTag(sheetTag)
+ val sheetNode = rule.onNodeWithTag(sheetTag)
val initialTop = sheetNode.getUnclippedBoundsInRoot().top
// Focus on the text field to force ime visibility.
@@ -1331,7 +1242,9 @@
var value = LayoutDirection.Ltr
rule.setContent {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
- ModalBottomSheet(onDismissRequest = { /*TODO*/ }) {
+ ModalBottomSheet(
+ onDismissRequest = { /*TODO*/ },
+ ) {
value = LocalLayoutDirection.current
}
}
@@ -1341,18 +1254,18 @@
}
}
- companion object {
- @Parameterized.Parameters(name = "{0}")
- @JvmStatic
- fun parameters() = arrayOf(
- EdgeToEdgeWrapper("EdgeToEdge", true),
- EdgeToEdgeWrapper("NonEdgeToEdge", false)
- )
- }
-
- class EdgeToEdgeWrapper(val name: String, val edgeToEdgeEnabled: Boolean) {
- override fun toString(): String {
- return name
+ @Test
+ fun modalBottomSheetContent_respectsProvidedInsets() {
+ rule.setContent {
+ ModalBottomSheet(
+ onDismissRequest = { /*TODO*/ },
+ dragHandle = {},
+ contentWindowInsets = { WindowInsets(bottom = sheetHeight) },
+ ) {
+ Box(Modifier.testTag(sheetTag))
+ }
}
+ // Size entirely filled by padding provided by WindowInsetPadding
+ rule.onNodeWithTag(sheetTag).onParent().assertHeightIsEqualTo(sheetHeight)
}
}
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt
index 9bfec4d..e441735 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt
@@ -17,17 +17,20 @@
package androidx.compose.material3
import android.content.Context
-import android.graphics.PixelFormat
+import android.graphics.Outline
import android.os.Build
-import android.view.Gravity
-import android.view.KeyEvent
+import android.view.ContextThemeWrapper
+import android.view.MotionEvent
import android.view.View
-import android.view.ViewTreeObserver
+import android.view.ViewOutlineProvider
+import android.view.Window
import android.view.WindowManager
import android.window.BackEvent
import android.window.OnBackAnimationCallback
import android.window.OnBackInvokedCallback
import android.window.OnBackInvokedDispatcher
+import androidx.activity.ComponentDialog
+import androidx.activity.addCallback
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
import androidx.compose.animation.core.Animatable
@@ -62,6 +65,7 @@
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -72,6 +76,7 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.R
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.GraphicsLayerScope
import androidx.compose.ui.graphics.Shape
@@ -81,21 +86,25 @@
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.AbstractComposeView
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.ViewRootForInspector
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.collapse
+import androidx.compose.ui.semantics.dialog
import androidx.compose.ui.semantics.dismiss
import androidx.compose.ui.semantics.expand
import androidx.compose.ui.semantics.paneTitle
-import androidx.compose.ui.semantics.popup
import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
+import androidx.compose.ui.window.DialogWindowProvider
import androidx.compose.ui.window.SecureFlagPolicy
+import androidx.core.view.WindowCompat
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.findViewTreeViewModelStoreOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
@@ -139,10 +148,10 @@
* darker color in light theme and lighter color in dark theme. See also: [Surface].
* @param scrimColor Color of the scrim that obscures content when the bottom sheet is open.
* @param dragHandle Optional visual marker to swipe the bottom sheet.
- * @param windowInsets window insets to be passed to the bottom sheet window via [PaddingValues]
+ * @param contentWindowInsets window insets to be passed to the bottom sheet content via [PaddingValues]
* params.
* @param properties [ModalBottomSheetProperties] for further customization of this
- * modal bottom sheet's behavior.
+ * modal bottom sheet's window behavior.
* @param content The content to be displayed inside the bottom sheet.
*/
@Composable
@@ -158,8 +167,8 @@
tonalElevation: Dp = 0.dp,
scrimColor: Color = BottomSheetDefaults.ScrimColor,
dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
- windowInsets: WindowInsets = BottomSheetDefaults.windowInsets,
- properties: ModalBottomSheetProperties = ModalBottomSheetDefaults.properties(),
+ contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets },
+ properties: ModalBottomSheetProperties = ModalBottomSheetDefaults.properties,
content: @Composable ColumnScope.() -> Unit,
) {
val scope = rememberCoroutineScope()
@@ -180,7 +189,7 @@
val predictiveBackProgress = remember { Animatable(initialValue = 0f) }
- ModalBottomSheetPopup(
+ ModalBottomSheetDialog(
properties = properties,
onDismissRequest = {
if (sheetState.currentValue == Expanded && sheetState.hasPartiallyExpandedState) {
@@ -194,9 +203,13 @@
}
},
predictiveBackProgress = predictiveBackProgress,
- windowInsets = windowInsets,
) {
- Box(Modifier.fillMaxSize(), propagateMinConstraints = false) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .imePadding(),
+ propagateMinConstraints = false
+ ) {
Scrim(
color = scrimColor,
onDismissRequest = animateToDismiss,
@@ -215,6 +228,7 @@
contentColor,
tonalElevation,
dragHandle,
+ contentWindowInsets,
content
)
}
@@ -241,6 +255,7 @@
contentColor: Color = contentColorFor(containerColor),
tonalElevation: Dp = BottomSheetDefaults.Elevation,
dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
+ contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets },
content: @Composable ColumnScope.() -> Unit
) {
val bottomSheetPaneTitle = getString(string = Strings.BottomSheetPaneTitle)
@@ -314,6 +329,7 @@
Column(
Modifier
.fillMaxWidth()
+ .windowInsetsPadding(contentWindowInsets())
.graphicsLayer {
val progress = predictiveBackProgress.value
val predictiveBackScaleX = calculatePredictiveBackScaleX(progress)
@@ -392,39 +408,46 @@
}
}
+// Logic forked from androidx.compose.ui.window.DialogProperties. Removed dismissOnClickOutside
+// and usePlatformDefaultWidth as they are not relevant for fullscreen experience.
/**
* Properties used to customize the behavior of a [ModalBottomSheet].
*
* @param securePolicy Policy for setting [WindowManager.LayoutParams.FLAG_SECURE] on the bottom
* sheet's window.
- * @param isFocusable Whether the modal bottom sheet is focusable. When true,
- * the modal bottom sheet will receive IME events and key presses, such as when
- * the back button is pressed.
* @param shouldDismissOnBackPress Whether the modal bottom sheet can be dismissed by pressing
* the back button. If true, pressing the back button will call onDismissRequest.
- * Note that [isFocusable] must be set to true in order to receive key events such as
- * the back button - if the modal bottom sheet is not focusable then this property does nothing.
*/
+@Immutable
@ExperimentalMaterial3Api
class ModalBottomSheetProperties(
- val securePolicy: SecureFlagPolicy,
- val isFocusable: Boolean,
- val shouldDismissOnBackPress: Boolean
+ val securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit,
+ val shouldDismissOnBackPress: Boolean = true,
) {
+ @Deprecated(
+ message = "'isFocusable' param is no longer used. Use constructor without this parameter.",
+ level = DeprecationLevel.WARNING,
+ replaceWith = ReplaceWith(
+ "ModalBottomSheetProperties(securePolicy, shouldDismissOnBackPress)"
+ )
+ )
+ @Suppress("UNUSED_PARAMETER")
+ constructor(
+ securePolicy: SecureFlagPolicy,
+ isFocusable: Boolean,
+ shouldDismissOnBackPress: Boolean,
+ ) : this(securePolicy, shouldDismissOnBackPress)
+
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ModalBottomSheetProperties) return false
-
if (securePolicy != other.securePolicy) return false
- if (isFocusable != other.isFocusable) return false
- if (shouldDismissOnBackPress != other.shouldDismissOnBackPress) return false
return true
}
override fun hashCode(): Int {
var result = securePolicy.hashCode()
- result = 31 * result + isFocusable.hashCode()
result = 31 * result + shouldDismissOnBackPress.hashCode()
return result
}
@@ -436,6 +459,11 @@
@Immutable
@ExperimentalMaterial3Api
object ModalBottomSheetDefaults {
+
+ /**
+ * Properties used to customize the behavior of a [ModalBottomSheet]. */
+ val properties = ModalBottomSheetProperties()
+
/**
* Properties used to customize the behavior of a [ModalBottomSheet].
*
@@ -449,11 +477,17 @@
* Note that [isFocusable] must be set to true in order to receive key events such as
* the back button - if the modal bottom sheet is not focusable then this property does nothing.
*/
+ @Deprecated(
+ level = DeprecationLevel.WARNING,
+ message = "'isFocusable' param is no longer used. Use value without this parameter.",
+ replaceWith = ReplaceWith("properties")
+ )
+ @Suppress("UNUSED_PARAMETER")
fun properties(
securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit,
isFocusable: Boolean = true,
shouldDismissOnBackPress: Boolean = true
- ) = ModalBottomSheetProperties(securePolicy, isFocusable, shouldDismissOnBackPress)
+ ) = ModalBottomSheetProperties(securePolicy, shouldDismissOnBackPress)
}
/**
@@ -475,6 +509,95 @@
initialValue = Hidden,
)
+/**
+ * <a href="https://m3.material.io/components/bottom-sheets/overview" class="external" target="_blank">Material Design modal bottom sheet</a>.
+ *
+ * Modal bottom sheets are used as an alternative to inline menus or simple dialogs on mobile,
+ * especially when offering a long list of action items, or when items require longer descriptions
+ * and icons. Like dialogs, modal bottom sheets appear in front of app content, disabling all other
+ * app functionality when they appear, and remaining on screen until confirmed, dismissed, or a
+ * required action has been taken.
+ *
+ * 
+ *
+ * A simple example of a modal bottom sheet looks like this:
+ *
+ * @sample androidx.compose.material3.samples.ModalBottomSheetSample
+ *
+ * @param onDismissRequest Executes when the user clicks outside of the bottom sheet, after sheet
+ * animates to [Hidden].
+ * @param modifier Optional [Modifier] for the bottom sheet.
+ * @param sheetState The state of the bottom sheet.
+ * @param sheetMaxWidth [Dp] that defines what the maximum width the sheet will take.
+ * Pass in [Dp.Unspecified] for a sheet that spans the entire screen width.
+ * @param shape The shape of the bottom sheet.
+ * @param containerColor The color used for the background of this bottom sheet
+ * @param contentColor The preferred color for content inside this bottom sheet. Defaults to either
+ * the matching content color for [containerColor], or to the current [LocalContentColor] if
+ * [containerColor] is not a color from the theme.
+ * @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color
+ * overlay is applied on top of the container. A higher tonal elevation value will result in a
+ * darker color in light theme and lighter color in dark theme. See also: [Surface].
+ * @param scrimColor Color of the scrim that obscures content when the bottom sheet is open.
+ * @param dragHandle Optional visual marker to swipe the bottom sheet.
+ * @param windowInsets window insets to be passed to the bottom sheet content via [PaddingValues]
+ * params.
+ * @param properties [ModalBottomSheetProperties] for further customization of this
+ * modal bottom sheet's window behavior.
+ * @param content The content to be displayed inside the bottom sheet.
+ */
+@Composable
+@ExperimentalMaterial3Api
+@Deprecated(
+ level = DeprecationLevel.HIDDEN,
+ message = "Use constructor with contentWindowInsets parameter.",
+ replaceWith = ReplaceWith(
+ "ModalBottomSheet(" +
+ "onDismissRequest," +
+ "modifier," +
+ "sheetState," +
+ "sheetMaxWidth," +
+ "shape," +
+ "containerColor," +
+ "contentColor," +
+ "tonalElevation," +
+ "scrimColor," +
+ "dragHandle," +
+ "{ windowInsets }," +
+ "properties," +
+ "content," +
+ ")")
+)
+fun ModalBottomSheet(
+ onDismissRequest: () -> Unit,
+ modifier: Modifier = Modifier,
+ sheetState: SheetState = rememberModalBottomSheetState(),
+ sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth,
+ shape: Shape = BottomSheetDefaults.ExpandedShape,
+ containerColor: Color = BottomSheetDefaults.ContainerColor,
+ contentColor: Color = contentColorFor(containerColor),
+ tonalElevation: Dp = 0.dp,
+ scrimColor: Color = BottomSheetDefaults.ScrimColor,
+ dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
+ windowInsets: WindowInsets = BottomSheetDefaults.windowInsets,
+ properties: ModalBottomSheetProperties = ModalBottomSheetDefaults.properties,
+ content: @Composable ColumnScope.() -> Unit,
+) = ModalBottomSheet(
+ onDismissRequest = onDismissRequest,
+ modifier = modifier,
+ sheetState = sheetState,
+ sheetMaxWidth = sheetMaxWidth,
+ shape = shape,
+ containerColor = containerColor,
+ contentColor = contentColor,
+ tonalElevation = tonalElevation,
+ scrimColor = scrimColor,
+ dragHandle = dragHandle,
+ contentWindowInsets = { windowInsets },
+ properties = properties,
+ content = content,
+)
+
@Composable
private fun Scrim(
color: Color,
@@ -507,195 +630,96 @@
}
}
-/**
- * Popup specific for modal bottom sheet.
- */
+// Fork of androidx.compose.ui.window.AndroidDialog_androidKt.Dialog
+// Added predictiveBackProgress param to pass into BottomSheetDialogWrapper.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-internal fun ModalBottomSheetPopup(
- properties: ModalBottomSheetProperties,
+private fun ModalBottomSheetDialog(
onDismissRequest: () -> Unit,
+ properties: ModalBottomSheetProperties,
predictiveBackProgress: Animatable<Float, AnimationVector1D>,
- windowInsets: WindowInsets,
- content: @Composable () -> Unit,
+ content: @Composable () -> Unit
) {
val view = LocalView.current
- val id = rememberSaveable { UUID.randomUUID() }
- val parentComposition = rememberCompositionContext()
- val currentContent by rememberUpdatedState(content)
- val scope = rememberCoroutineScope()
+ val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
- val modalBottomSheetWindow = remember {
- ModalBottomSheetWindow(
- properties = properties,
- onDismissRequest = onDismissRequest,
- composeView = view,
- saveId = id,
- predictiveBackProgress = predictiveBackProgress,
- scope = scope
+ val composition = rememberCompositionContext()
+ val currentContent by rememberUpdatedState(content)
+ val dialogId = rememberSaveable { UUID.randomUUID() }
+ val scope = rememberCoroutineScope()
+ val dialog = remember(view, density) {
+ ModalBottomSheetDialogWrapper(
+ onDismissRequest,
+ properties,
+ view,
+ layoutDirection,
+ density,
+ dialogId,
+ predictiveBackProgress,
+ scope,
).apply {
- setCustomContent(
- parent = parentComposition,
- content = {
- Box(
- Modifier
- .semantics { this.popup() }
- .windowInsetsPadding(windowInsets)
- .then(
- // TODO(b/290893168): Figure out a solution for APIs < 30.
- if (Build.VERSION.SDK_INT >= 33)
- Modifier.imePadding()
- else Modifier
- )
- ) {
- currentContent()
- }
+ setContent(composition) {
+ Box(
+ Modifier.semantics { dialog() },
+ ) {
+ currentContent()
}
- )
+ }
}
}
- DisposableEffect(modalBottomSheetWindow) {
- modalBottomSheetWindow.show()
- modalBottomSheetWindow.superSetLayoutDirection(layoutDirection)
+ DisposableEffect(dialog) {
+ dialog.show()
+
onDispose {
- modalBottomSheetWindow.disposeComposition()
- modalBottomSheetWindow.dismiss()
+ dialog.dismiss()
+ dialog.disposeComposition()
}
}
+
+ SideEffect {
+ dialog.updateParameters(
+ onDismissRequest = onDismissRequest,
+ properties = properties,
+ layoutDirection = layoutDirection
+ )
+ }
}
-/** Custom compose view for [ModalBottomSheet] */
-@OptIn(ExperimentalMaterial3Api::class)
-private class ModalBottomSheetWindow(
- private val properties: ModalBottomSheetProperties,
- private var onDismissRequest: () -> Unit,
- private val composeView: View,
+// Fork of androidx.compose.ui.window.DialogLayout
+// Additional parameters required for current predictive back implementation.
+@Suppress("ViewConstructor")
+private class ModalBottomSheetDialogLayout(
+ context: Context,
+ override val window: Window,
+ val shouldDismissOnBackPress: Boolean,
+ private val onDismissRequest: () -> Unit,
private val predictiveBackProgress: Animatable<Float, AnimationVector1D>,
private val scope: CoroutineScope,
- saveId: UUID
-) :
- AbstractComposeView(composeView.context),
- ViewTreeObserver.OnGlobalLayoutListener,
- ViewRootForInspector {
-
- private var backCallback: Any? = null
-
- init {
- id = android.R.id.content
- // Set up view owners
- setViewTreeLifecycleOwner(composeView.findViewTreeLifecycleOwner())
- setViewTreeViewModelStoreOwner(composeView.findViewTreeViewModelStoreOwner())
- setViewTreeSavedStateRegistryOwner(composeView.findViewTreeSavedStateRegistryOwner())
- setTag(androidx.compose.ui.R.id.compose_view_saveable_id_tag, "Popup:$saveId")
- // Enable children to draw their shadow by not clipping them
- clipChildren = false
- }
-
- private val windowManager =
- composeView.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
-
- private val displayWidth: Int
- get() = context.resources.displayMetrics.widthPixels
-
- private val params: WindowManager.LayoutParams =
- WindowManager.LayoutParams().apply {
- // Position bottom sheet from the bottom of the screen
- gravity = Gravity.BOTTOM or Gravity.START
- // Application panel window
- type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
- // Fill up the entire app view
- width = displayWidth
- height = WindowManager.LayoutParams.MATCH_PARENT
-
- // Format of screen pixels
- format = PixelFormat.TRANSLUCENT
- // Title used as fallback for a11y services
- // TODO: Provide bottom sheet window resource
- title = composeView.context.resources.getString(
- androidx.compose.ui.R.string.default_popup_window_title
- )
- // Get the Window token from the parent view
- token = composeView.applicationWindowToken
-
- // Flags specific to modal bottom sheet.
- flags = flags and (
- WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES or
- WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
- ).inv()
-
- flags = flags or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
-
- // Security flag
- val secureFlagEnabled =
- properties.securePolicy.shouldApplySecureFlag(composeView.isFlagSecureEnabled())
- if (secureFlagEnabled) {
- flags = flags or WindowManager.LayoutParams.FLAG_SECURE
- } else {
- flags = flags and (WindowManager.LayoutParams.FLAG_SECURE.inv())
- }
-
- // Focusable
- if (!properties.isFocusable) {
- flags = flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
- } else {
- flags = flags and (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE.inv())
- }
- }
+) : AbstractComposeView(context), DialogWindowProvider {
private var content: @Composable () -> Unit by mutableStateOf({})
+ private var backCallback: Any? = null
+
override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
private set
+ fun setContent(parent: CompositionContext, content: @Composable () -> Unit) {
+ setParentCompositionContext(parent)
+ this.content = content
+ shouldCreateCompositionOnAttachedToWindow = true
+ createComposition()
+ }
+
+ // Display width and height logic removed, size will always span fillMaxSize().
+
@Composable
override fun Content() {
content()
}
- fun setCustomContent(
- parent: CompositionContext? = null,
- content: @Composable () -> Unit
- ) {
- parent?.let { setParentCompositionContext(it) }
- this.content = content
- shouldCreateCompositionOnAttachedToWindow = true
- }
-
- fun show() {
- windowManager.addView(this, params)
- }
-
- fun dismiss() {
- setViewTreeLifecycleOwner(null)
- setViewTreeSavedStateRegistryOwner(null)
- composeView.viewTreeObserver.removeOnGlobalLayoutListener(this)
- windowManager.removeViewImmediate(this)
- }
-
- /**
- * Taken from PopupWindow. Calls [onDismissRequest] when back button is pressed.
- */
- override fun dispatchKeyEvent(event: KeyEvent): Boolean {
- if (event.keyCode == KeyEvent.KEYCODE_BACK && properties.shouldDismissOnBackPress) {
- if (keyDispatcherState == null) {
- return super.dispatchKeyEvent(event)
- }
- if (event.action == KeyEvent.ACTION_DOWN && event.repeatCount == 0) {
- val state = keyDispatcherState
- state?.startTracking(event, this)
- return true
- } else if (event.action == KeyEvent.ACTION_UP) {
- val state = keyDispatcherState
- if (state != null && state.isTracking(event) && !event.isCanceled) {
- onDismissRequest()
- return true
- }
- }
- }
- return super.dispatchKeyEvent(event)
- }
-
+ // Existing predictive back behavior below.
override fun onAttachedToWindow() {
super.onAttachedToWindow()
@@ -709,7 +733,7 @@
}
private fun maybeRegisterBackCallback() {
- if (!properties.shouldDismissOnBackPress || Build.VERSION.SDK_INT < 33) {
+ if (!shouldDismissOnBackPress || Build.VERSION.SDK_INT < 33) {
return
}
if (backCallback == null) {
@@ -729,24 +753,6 @@
backCallback = null
}
- override fun onGlobalLayout() {
- // No-op
- }
-
- override fun setLayoutDirection(layoutDirection: Int) {
- // Do nothing. ViewRootImpl will call this method attempting to set the layout direction
- // from the context's locale, but we have one already from the parent composition.
- }
-
- // Sets the "real" layout direction for our content that we obtain from the parent composition.
- fun superSetLayoutDirection(layoutDirection: LayoutDirection) {
- val direction = when (layoutDirection) {
- LayoutDirection.Ltr -> android.util.LayoutDirection.LTR
- LayoutDirection.Rtl -> android.util.LayoutDirection.RTL
- }
- super.setLayoutDirection(direction)
- }
-
@RequiresApi(34)
private object Api34Impl {
@JvmStatic
@@ -807,8 +813,165 @@
}
}
-// Taken from AndroidPopup.android.kt
-private fun View.isFlagSecureEnabled(): Boolean {
+// Fork of androidx.compose.ui.window.DialogWrapper.
+// predictiveBackProgress and scope params added for predictive back implementation.
+// EdgeToEdgeFloatingDialogWindowTheme provided to allow theme to extend into status bar.
+@ExperimentalMaterial3Api
+private class ModalBottomSheetDialogWrapper(
+ private var onDismissRequest: () -> Unit,
+ private var properties: ModalBottomSheetProperties,
+ private val composeView: View,
+ layoutDirection: LayoutDirection,
+ density: Density,
+ dialogId: UUID,
+ predictiveBackProgress: Animatable<Float, AnimationVector1D>,
+ scope: CoroutineScope,
+) : ComponentDialog(
+ ContextThemeWrapper(
+ composeView.context,
+ androidx.compose.material3.R.style.EdgeToEdgeFloatingDialogWindowTheme
+ )
+), ViewRootForInspector {
+
+ private val dialogLayout: ModalBottomSheetDialogLayout
+
+ // On systems older than Android S, there is a bug in the surface insets matrix math used by
+ // elevation, so high values of maxSupportedElevation break accessibility services: b/232788477.
+ private val maxSupportedElevation = 8.dp
+
+ override val subCompositionView: AbstractComposeView get() = dialogLayout
+
+ init {
+ val window = window ?: error("Dialog has no window")
+ window.requestFeature(Window.FEATURE_NO_TITLE)
+ window.setBackgroundDrawableResource(android.R.color.transparent)
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ dialogLayout = ModalBottomSheetDialogLayout(
+ context,
+ window,
+ properties.shouldDismissOnBackPress,
+ onDismissRequest,
+ predictiveBackProgress,
+ scope,
+ ).apply {
+ // Set unique id for AbstractComposeView. This allows state restoration for the state
+ // defined inside the Dialog via rememberSaveable()
+ setTag(R.id.compose_view_saveable_id_tag, "Dialog:$dialogId")
+ // Enable children to draw their shadow by not clipping them
+ clipChildren = false
+ // Allocate space for elevation
+ with(density) { elevation = maxSupportedElevation.toPx() }
+ // Simple outline to force window manager to allocate space for shadow.
+ // Note that the outline affects clickable area for the dismiss listener. In case of
+ // shapes like circle the area for dismiss might be to small (rectangular outline
+ // consuming clicks outside of the circle).
+ outlineProvider = object : ViewOutlineProvider() {
+ override fun getOutline(view: View, result: Outline) {
+ result.setRect(0, 0, view.width, view.height)
+ // We set alpha to 0 to hide the view's shadow and let the composable to draw
+ // its own shadow. This still enables us to get the extra space needed in the
+ // surface.
+ result.alpha = 0f
+ }
+ }
+ }
+ // Clipping logic removed because we are spanning edge to edge.
+
+ setContentView(dialogLayout)
+ dialogLayout.setViewTreeLifecycleOwner(composeView.findViewTreeLifecycleOwner())
+ dialogLayout.setViewTreeViewModelStoreOwner(composeView.findViewTreeViewModelStoreOwner())
+ dialogLayout.setViewTreeSavedStateRegistryOwner(
+ composeView.findViewTreeSavedStateRegistryOwner()
+ )
+
+ // Initial setup
+ updateParameters(onDismissRequest, properties, layoutDirection)
+
+ WindowCompat.getInsetsController(window, window.decorView).apply {
+ isAppearanceLightStatusBars = false
+ isAppearanceLightNavigationBars = false
+ }
+ // Due to how the onDismissRequest callback works
+ // (it enforces a just-in-time decision on whether to update the state to hide the dialog)
+ // we need to unconditionally add a callback here that is always enabled,
+ // meaning we'll never get a system UI controlled predictive back animation
+ // for these dialogs
+ onBackPressedDispatcher.addCallback(this) {
+ if (properties.shouldDismissOnBackPress) {
+ onDismissRequest()
+ }
+ }
+ }
+
+ private fun setLayoutDirection(layoutDirection: LayoutDirection) {
+ dialogLayout.layoutDirection = when (layoutDirection) {
+ LayoutDirection.Ltr -> android.util.LayoutDirection.LTR
+ LayoutDirection.Rtl -> android.util.LayoutDirection.RTL
+ }
+ }
+
+ fun setContent(parentComposition: CompositionContext, children: @Composable () -> Unit) {
+ dialogLayout.setContent(parentComposition, children)
+ }
+
+ private fun setSecurePolicy(securePolicy: SecureFlagPolicy) {
+ val secureFlagEnabled =
+ securePolicy.shouldApplySecureFlag(composeView.isFlagSecureEnabled())
+ window!!.setFlags(
+ if (secureFlagEnabled) {
+ WindowManager.LayoutParams.FLAG_SECURE
+ } else {
+ WindowManager.LayoutParams.FLAG_SECURE.inv()
+ },
+ WindowManager.LayoutParams.FLAG_SECURE
+ )
+ }
+
+ fun updateParameters(
+ onDismissRequest: () -> Unit,
+ properties: ModalBottomSheetProperties,
+ layoutDirection: LayoutDirection
+ ) {
+ this.onDismissRequest = onDismissRequest
+ this.properties = properties
+ setSecurePolicy(properties.securePolicy)
+ setLayoutDirection(layoutDirection)
+
+ // Window flags to span parent window.
+ window?.setLayout(
+ WindowManager.LayoutParams.MATCH_PARENT,
+ WindowManager.LayoutParams.MATCH_PARENT,
+ )
+ window?.setSoftInputMode(
+ if (Build.VERSION.SDK_INT >= 30) {
+ WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING
+ } else {
+ @Suppress("DEPRECATION")
+ WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
+ },
+ )
+ }
+
+ fun disposeComposition() {
+ dialogLayout.disposeComposition()
+ }
+
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ val result = super.onTouchEvent(event)
+ if (result) {
+ onDismissRequest()
+ }
+
+ return result
+ }
+
+ override fun cancel() {
+ // Prevents the dialog from dismissing itself
+ return
+ }
+}
+
+internal fun View.isFlagSecureEnabled(): Boolean {
val windowParams = rootView.layoutParams as? WindowManager.LayoutParams
if (windowParams != null) {
return (windowParams.flags and WindowManager.LayoutParams.FLAG_SECURE) != 0
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SheetDefaults.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SheetDefaults.kt
index a8977c5..a04b579 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SheetDefaults.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SheetDefaults.kt
@@ -25,6 +25,7 @@
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.material3.SheetValue.Expanded
import androidx.compose.material3.SheetValue.Hidden
@@ -34,7 +35,6 @@
import androidx.compose.material3.internal.animateTo
import androidx.compose.material3.internal.getString
import androidx.compose.material3.internal.snapTo
-import androidx.compose.material3.internal.systemBarsForVisualComponents
import androidx.compose.material3.tokens.ScrimTokens
import androidx.compose.material3.tokens.SheetBottomTokens
import androidx.compose.runtime.Composable
@@ -332,11 +332,11 @@
val SheetMaxWidth = 640.dp
/**
- * Default insets to be used and consumed by the [ModalBottomSheet] window.
+ * Default insets to be used and consumed by the [ModalBottomSheet]'s content.
*/
val windowInsets: WindowInsets
@Composable
- get() = WindowInsets.systemBarsForVisualComponents.only(WindowInsetsSides.Vertical)
+ get() = WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom)
/**
* The optional visual marker placed on top of a bottom sheet to indicate it may be dragged.
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
index 0683f94..c06a03e 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
@@ -140,9 +140,9 @@
* services.
* @param valueRange range of values that this slider can take. The passed [value] will be coerced
* to this range.
- * @param steps if greater than 0, specifies the amount of discrete allowable values, evenly
- * distributed across the whole value range. If 0, the slider will behave continuously and allow any
- * value from the range specified. Must not be negative.
+ * @param steps if positive, specifies the amount of discrete allowable values (in addition to the
+ * endpoints of the value range). Step values are evenly distributed across the range. If 0, the
+ * slider will behave continuously and allow any value from the range. Must not be negative.
* @param onValueChangeFinished called when value change has ended. This should not be used to
* update the slider value (use [onValueChange] instead), but rather to know when the user has
* completed selecting a new value by ending a drag or a click.
@@ -236,9 +236,9 @@
* @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
* for this slider. You can create and pass in your own `remember`ed instance to observe
* [Interaction]s and customize the appearance / behavior of this slider in different states.
- * @param steps if greater than 0, specifies the amount of discrete allowable values, evenly
- * distributed across the whole value range. If 0, the slider will behave continuously and allow any
- * value from the range specified. Must not be negative.
+ * @param steps if positive, specifies the amount of discrete allowable values (in addition to the
+ * endpoints of the value range). Step values are evenly distributed across the range. If 0, the
+ * slider will behave continuously and allow any value from the range. Must not be negative.
* @param thumb the thumb to be displayed on the slider, it is placed on top of the track. The
* lambda receives a [SliderState] which is used to obtain the current active track.
* @param track the track to be displayed on the slider, it is placed underneath the thumb. The
@@ -402,9 +402,9 @@
* @param enabled whether or not component is enabled and can we interacted with or not
* @param valueRange range of values that Range Slider values can take. Passed [value] will be
* coerced to this range
- * @param steps if greater than 0, specifies the amounts of discrete values, evenly distributed
- * between across the whole value range. If 0, range slider will behave as a continuous slider and
- * allow to choose any value from the range specified. Must not be negative.
+ * @param steps if positive, specifies the amount of discrete allowable values (in addition to the
+ * endpoints of the value range). Step values are evenly distributed across the range. If 0, the
+ * range slider will behave continuously and allow any value from the range. Must not be negative.
* @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
* shouldn't be used to update the range slider values (use [onValueChange] for that), but rather to
* know when the user has completed selecting a new value by ending a drag or a click.
@@ -503,9 +503,9 @@
* @param endInteractionSource the [MutableInteractionSource] representing the stream of
* [Interaction]s for the end thumb. You can create and pass in your own
* `remember`ed instance to observe.
- * @param steps if greater than 0, specifies the amounts of discrete values, evenly distributed
- * between across the whole value range. If 0, range slider will behave as a continuous slider and
- * allow to choose any value from the range specified. Must not be negative.
+ * @param steps if positive, specifies the amount of discrete allowable values (in addition to the
+ * endpoints of the value range). Step values are evenly distributed across the range. If 0, the
+ * range slider will behave continuously and allow any value from the range. Must not be negative.
* @param startThumb the start thumb to be displayed on the Range Slider. The lambda receives a
* [RangeSliderState] which is used to obtain the current active track.
* @param endThumb the end thumb to be displayed on the Range Slider. The lambda receives a
@@ -2031,9 +2031,9 @@
* @param value [Float] that indicates the initial
* position of the thumb. If outside of [valueRange]
* provided, value will be coerced to this range.
- * @param steps if greater than 0, specifies the amounts of discrete values, evenly distributed
- * between across the whole value range. If 0, range slider will behave as a continuous slider and
- * allow to choose any value from the range specified. Must not be negative.
+ * @param steps if positive, specifies the amount of discrete allowable values (in addition to the
+ * endpoints of the value range). Step values are evenly distributed across the range. If 0, the
+ * slider will behave continuously and allow any value from the range. Must not be negative.
* @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
* shouldn't be used to update the range slider values (use [onValueChange] for that),
* but rather to know when the user has completed selecting a new value by ending a drag or a click.
@@ -2158,9 +2158,9 @@
* @param activeRangeEnd [Float] that indicates the initial
* end of the active range of the slider. If outside of [valueRange]
* provided, value will be coerced to this range.
- * @param steps if greater than 0, specifies the amounts of discrete values, evenly distributed
- * between across the whole value range. If 0, range slider will behave as a continuous slider and
- * allow to choose any value from the range specified. Must not be negative.
+ * @param steps if positive, specifies the amount of discrete allowable values (in addition to the
+ * endpoints of the value range). Step values are evenly distributed across the range. If 0, the
+ * range slider will behave continuously and allow any value from the range. Must not be negative.
* @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
* shouldn't be used to update the range slider values (use [onValueChange] for that), but rather
* to know when the user has completed selecting a new value by ending a drag or a click.
diff --git a/compose/material3/material3/src/main/res/values-v30/styles.xml b/compose/material3/material3/src/main/res/values-v30/styles.xml
new file mode 100644
index 0000000..3d9425e
--- /dev/null
+++ b/compose/material3/material3/src/main/res/values-v30/styles.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+-->
+<resources>
+ <!-- "always" Value for windowLayoutInDisplayCutoutMode. Needed for APIs 30+. -->
+ <integer name="m3c_window_layout_in_display_cutout_mode">3</integer>
+</resources>
\ No newline at end of file
diff --git a/compose/material3/material3/src/main/res/values/styles.xml b/compose/material3/material3/src/main/res/values/styles.xml
new file mode 100644
index 0000000..8249ac4
--- /dev/null
+++ b/compose/material3/material3/src/main/res/values/styles.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+-->
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <style name="EdgeToEdgeFloatingDialogWindowTheme">
+ <item name="android:dialogTheme">@style/EdgeToEdgeFloatingDialogTheme</item>
+ </style>
+ <style name="EdgeToEdgeFloatingDialogTheme" parent="android:Theme.DeviceDefault.Dialog">
+ <item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="27">@integer/m3c_window_layout_in_display_cutout_mode</item>
+ <item name="android:windowClipToOutline">false</item>
+ <item name="android:windowIsFloating">false</item>
+ <item name="android:statusBarColor">@android:color/transparent</item>
+ <item name="android:navigationBarColor">@android:color/transparent</item>
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:windowElevation">0dp</item>
+ </style>
+ <!-- "shortEdges" Value for windowLayoutInDisplayCutoutMode. Needed for APIs 27-29. -->
+ <integer name="m3c_window_layout_in_display_cutout_mode">1</integer>
+</resources>
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/api/restricted_current.txt b/compose/ui/ui-graphics/api/restricted_current.txt
index cd5b143..68a8faa 100644
--- a/compose/ui/ui-graphics/api/restricted_current.txt
+++ b/compose/ui/ui-graphics/api/restricted_current.txt
@@ -193,6 +193,7 @@
}
public final class BezierKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static long computeCubicVerticalBounds(float p0y, float p1y, float p2y, float p3y, float[] roots, optional int index);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static long computeHorizontalBounds(androidx.compose.ui.graphics.PathSegment segment, float[] roots, optional int index);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static float evaluateCubic(float p1, float p2, float t);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static float evaluateY(androidx.compose.ui.graphics.PathSegment segment, float t);
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt
index 5713996..541181b 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt
@@ -20,17 +20,13 @@
import android.view.View
import android.view.ViewGroup
import androidx.annotation.RequiresApi
-import androidx.compose.ui.graphics.drawscope.DefaultDensity
import androidx.compose.ui.graphics.layer.GraphicsLayer
-import androidx.compose.ui.graphics.layer.GraphicsLayerImpl
import androidx.compose.ui.graphics.layer.GraphicsLayerV23
import androidx.compose.ui.graphics.layer.GraphicsLayerV29
import androidx.compose.ui.graphics.layer.GraphicsViewLayer
import androidx.compose.ui.graphics.layer.LayerManager
import androidx.compose.ui.graphics.layer.view.DrawChildContainer
import androidx.compose.ui.graphics.layer.view.ViewLayerContainer
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection
/**
* Create a new [GraphicsContext] with the provided [ViewGroup] to contain [View] based layers.
@@ -73,27 +69,7 @@
)
}
return GraphicsLayer(layerImpl, layerManager).also { layer ->
- // Do a placeholder recording of drawing instructions to avoid errors when doing a
- // persistence render.
- // This will be overridden by the consumer of the created GraphicsLayer
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P &&
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- // Only API levels between M (inclusive) and P (exclusive) require a placeholder
- // displaylist for persistence rendering. On some API levels like (ex. API 28)
- // actually doing a placeholder render before the activity is setup
- // (ex in unit tests) causes the emulator to crash with an NPE in native code
- // on the HWUI canvas implementation
- layer.record(
- DefaultDensity,
- LayoutDirection.Ltr,
- IntSize(1, 1),
- GraphicsLayerImpl.DefaultDrawBlock
- )
- }
layerManager.persist(layer)
- // Reset the size to zero so that immediately after GraphicsLayer creation
- // we do not advertise a size of 1 x 1
- layer.size = IntSize.Zero
}
}
}
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayerV23.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayerV23.android.kt
index f240a88..c8500ae 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayerV23.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayerV23.android.kt
@@ -57,6 +57,7 @@
private var layerPaint: android.graphics.Paint? = null
private var matrix: android.graphics.Matrix? = null
private var outlineIsProvided = false
+ private var recordWasCalled = false
private fun obtainLayerPaint(): android.graphics.Paint =
layerPaint ?: android.graphics.Paint().also { layerPaint = it }
@@ -282,6 +283,7 @@
layer: GraphicsLayer,
block: DrawScope.() -> Unit
) {
+ recordWasCalled = true
val recordingCanvas = renderNode.start(size.width, size.height)
canvasHolder.drawInto(recordingCanvas) {
canvasDrawScope.draw(
@@ -298,6 +300,14 @@
}
override fun draw(canvas: androidx.compose.ui.graphics.Canvas) {
+ if (!recordWasCalled) {
+ recordWasCalled = true
+ // Do a placeholder recording of drawing instructions to avoid errors when doing a
+ // persistence render.
+ // This will be overridden by the consumer of the created GraphicsLayer
+ val recordingCanvas = renderNode.start(1, 1)
+ renderNode.end(recordingCanvas)
+ }
(canvas.nativeCanvas as DisplayListCanvas).drawRenderNode(renderNode)
}
@@ -327,6 +337,7 @@
}
private fun discardDisplayListInternal() {
+ recordWasCalled = false
// See b/216660268. RenderNode#discardDisplayList was originally called
// destroyDisplayListData on Android M and below. Make sure we gate on the corresponding
// API level and call the original method name on these API levels, otherwise invoke
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Bezier.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Bezier.kt
index acf82f8..9beda3b 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Bezier.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Bezier.kt
@@ -421,9 +421,11 @@
*/
private fun findDerivativeRoots(
segment: PathSegment,
+ horizontal: Boolean,
roots: FloatArray,
- index: Int = 0,
+ index: Int
): Int {
+ val offset = if (horizontal) 0 else 1
val points = segment.points
return when (segment.type) {
PathSegment.Type.Move -> 0
@@ -434,8 +436,8 @@
// Line derivative of a quadratic function
// We do the computation inline to avoid using arrays of other data
// structures to return the result
- val d0 = 2 * (points[2] - points[0])
- val d1 = 2 * (points[4] - points[2])
+ val d0 = 2 * (points[offset + 2] - points[offset + 0])
+ val d1 = 2 * (points[offset + 4] - points[offset + 2])
findLineRoot(d0, d1, roots, index)
}
@@ -446,9 +448,9 @@
// Quadratic derivative of a cubic function
// We do the computation inline to avoid using arrays of other data
// structures to return the result
- val d0 = 3.0f * (points[2] - points[0])
- val d1 = 3.0f * (points[4] - points[2])
- val d2 = 3.0f * (points[6] - points[4])
+ val d0 = 3.0f * (points[offset + 2] - points[offset + 0])
+ val d1 = 3.0f * (points[offset + 4] - points[offset + 2])
+ val d2 = 3.0f * (points[offset + 6] - points[offset + 4])
val count = findQuadraticRoots(d0, d1, d2, roots, index)
// Compute the second derivative as a line
@@ -477,7 +479,7 @@
roots: FloatArray,
index: Int = 0
): FloatFloatPair {
- val count = findDerivativeRoots(segment, roots, index)
+ val count = findDerivativeRoots(segment, true, roots, index)
var minX = min(segment.startX, segment.endX)
var maxX = max(segment.startX, segment.endX)
@@ -504,18 +506,54 @@
roots: FloatArray,
index: Int = 0
): FloatFloatPair {
- val count = findDerivativeRoots(segment, roots, index)
- var minX = min(segment.startY, segment.endY)
- var maxX = max(segment.startY, segment.endY)
+ val count = findDerivativeRoots(segment, false, roots, index)
+ var minY = min(segment.startY, segment.endY)
+ var maxY = max(segment.startY, segment.endY)
for (i in 0 until count) {
val t = roots[i]
val x = evaluateY(segment, t)
- minX = min(minX, x)
- maxX = max(maxX, x)
+ minY = min(minY, x)
+ maxY = max(maxY, x)
}
- return FloatFloatPair(minX, maxX)
+ return FloatFloatPair(minY, maxY)
+}
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+fun computeCubicVerticalBounds(
+ p0y: Float,
+ p1y: Float,
+ p2y: Float,
+ p3y: Float,
+ roots: FloatArray,
+ index: Int = 0
+): FloatFloatPair {
+ // Quadratic derivative of a cubic function
+ // We do the computation inline to avoid using arrays of other data
+ // structures to return the result
+ val d0 = 3.0f * (p1y - p0y)
+ val d1 = 3.0f * (p2y - p1y)
+ val d2 = 3.0f * (p3y - p2y)
+ var count = findQuadraticRoots(d0, d1, d2, roots, index)
+
+ // Compute the second derivative as a line
+ val dd0 = 2.0f * (d1 - d0)
+ val dd1 = 2.0f * (d2 - d1)
+ count += findLineRoot(dd0, dd1, roots, index + count)
+
+ var minY = min(p0y, p3y)
+ var maxY = max(p0y, p3y)
+
+ for (i in 0 until count) {
+ val t = roots[i]
+ println(t)
+ val y = evaluateCubic(p0y, p1y, p2y, p3y, t)
+ minY = min(minY, y)
+ maxY = max(maxY, y)
+ }
+
+ return FloatFloatPair(minY, maxY)
}
@Suppress("NOTHING_TO_INLINE")
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index 1f1425a..76206b2 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -170,7 +170,7 @@
}
public fun interface LinkInteractionListener {
- method public void onClicked(androidx.compose.ui.text.LinkAnnotation link);
+ method public void onClick(androidx.compose.ui.text.LinkAnnotation link);
}
public final class MultiParagraph {
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index 01a1f6f..2156b8f 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -170,7 +170,7 @@
}
public fun interface LinkInteractionListener {
- method public void onClicked(androidx.compose.ui.text.LinkAnnotation link);
+ method public void onClick(androidx.compose.ui.text.LinkAnnotation link);
}
public final class MultiParagraph {
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/URLSpanCache.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/URLSpanCache.android.kt
index 137833f..790beec 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/URLSpanCache.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/URLSpanCache.android.kt
@@ -76,6 +76,6 @@
private class ComposeClickableSpan(private val link: LinkAnnotation) : ClickableSpan() {
override fun onClick(widget: View) {
- link.linkInteractionListener?.onClicked(link)
+ link.linkInteractionListener?.onClick(link)
}
}
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/LinkAnnotation.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/LinkAnnotation.kt
index 86d1c54..3a1c75a 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/LinkAnnotation.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/LinkAnnotation.kt
@@ -52,7 +52,7 @@
/**
* An annotation that contains a [url] string. When clicking on the text to which this annotation
* is attached, the app will try to open the url using [androidx.compose.ui.platform.UriHandler].
- * However, if [linkInteractionListener] is provided, its [LinkInteractionListener.onClicked]
+ * However, if [linkInteractionListener] is provided, its [LinkInteractionListener.onClick]
* method will be called instead and so you need to then handle opening url manually (for
* example by calling [androidx.compose.ui.platform.UriHandler]).
*/
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/LinkInteractionListener.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/LinkInteractionListener.kt
index dde0f0e..dc1aa74 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/LinkInteractionListener.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/LinkInteractionListener.kt
@@ -22,5 +22,5 @@
fun interface LinkInteractionListener {
/** Triggered when a user clicks on the [link] */
- fun onClicked(link: LinkAnnotation)
+ fun onClick(link: LinkAnnotation)
}
diff --git a/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/MathHelpers.kt b/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/MathHelpers.kt
index 21d84a2..c0e5ba0 100644
--- a/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/MathHelpers.kt
+++ b/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/MathHelpers.kt
@@ -41,7 +41,7 @@
/**
* Returns the smaller of the given values. If any value is NaN, returns NaN.
* Preferred over `kotlin.comparisons.minfOf()` for 4 arguments as it avoids
- * allocaing an array because of the varargs.
+ * allocating an array because of the varargs.
*/
@Suppress("NOTHING_TO_INLINE")
inline fun fastMinOf(a: Float, b: Float, c: Float, d: Float): Float {
@@ -52,7 +52,7 @@
/**
* Returns the largest of the given values. If any value is NaN, returns NaN.
* Preferred over `kotlin.comparisons.maxOf()` for 4 arguments as it avoids
- * allocaing an array because of the varargs.
+ * allocating an array because of the varargs.
*/
@Suppress("NOTHING_TO_INLINE")
inline fun fastMaxOf(a: Float, b: Float, c: Float, d: Float): Float {
diff --git a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
index 48c1ab5..1e4745c 100644
--- a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
+++ b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
@@ -643,6 +643,290 @@
}
@Test
+ @SmallTest
+ public void testSetFNumber_decimalString() throws Exception {
+ File imageFile =
+ copyFromResourceToFile(
+ R.raw.jpeg_with_exif_byte_order_ii, "jpeg_with_exif_byte_order_ii.jpg");
+ ExifInterface exifInterface = new ExifInterface(imageFile);
+
+ String value = "1.4";
+ exifInterface.setAttribute(ExifInterface.TAG_F_NUMBER, value);
+
+ assertThat(exifInterface.getAttribute(ExifInterface.TAG_F_NUMBER)).isEqualTo(value);
+ double result =
+ exifInterface.getAttributeDouble(
+ ExifInterface.TAG_F_NUMBER, /* defaultValue= */ -1);
+ assertThat(result).isEqualTo(1.4);
+
+ exifInterface.saveAttributes();
+ exifInterface = new ExifInterface(imageFile);
+
+ assertThat(exifInterface.getAttribute(ExifInterface.TAG_F_NUMBER)).isEqualTo(value);
+ result =
+ exifInterface.getAttributeDouble(
+ ExifInterface.TAG_F_NUMBER, /* defaultValue= */ -1);
+ assertThat(result).isEqualTo(1.4);
+ }
+
+ @Test
+ @SmallTest
+ public void testSetFNumber_rationalString() throws Exception {
+ File imageFile =
+ copyFromResourceToFile(
+ R.raw.jpeg_with_exif_byte_order_ii, "jpeg_with_exif_byte_order_ii.jpg");
+ ExifInterface exifInterface = new ExifInterface(imageFile);
+
+ exifInterface.setAttribute(ExifInterface.TAG_F_NUMBER, "7/5");
+
+ assertThat(exifInterface.getAttribute(ExifInterface.TAG_F_NUMBER)).isEqualTo("1.4");
+ double result =
+ exifInterface.getAttributeDouble(
+ ExifInterface.TAG_F_NUMBER, /* defaultValue= */ -1);
+ assertThat(result).isEqualTo(1.4);
+
+ exifInterface.saveAttributes();
+ exifInterface = new ExifInterface(imageFile);
+
+ assertThat(exifInterface.getAttribute(ExifInterface.TAG_F_NUMBER)).isEqualTo("1.4");
+ result =
+ exifInterface.getAttributeDouble(
+ ExifInterface.TAG_F_NUMBER, /* defaultValue= */ -1);
+ assertThat(result).isEqualTo(1.4);
+ }
+
+ @Test
+ @SmallTest
+ public void testSetDigitalZoomRatio_decimalString() throws Exception {
+ File imageFile =
+ copyFromResourceToFile(
+ R.raw.jpeg_with_exif_byte_order_ii, "jpeg_with_exif_byte_order_ii.jpg");
+ ExifInterface exifInterface = new ExifInterface(imageFile);
+
+ String value = "0.8";
+ exifInterface.setAttribute(ExifInterface.TAG_DIGITAL_ZOOM_RATIO, value);
+
+ assertThat(exifInterface.getAttribute(ExifInterface.TAG_DIGITAL_ZOOM_RATIO))
+ .isEqualTo("0.8");
+ double result =
+ exifInterface.getAttributeDouble(
+ ExifInterface.TAG_DIGITAL_ZOOM_RATIO, /* defaultValue= */ -1);
+ assertThat(result).isEqualTo(0.8);
+
+ exifInterface.saveAttributes();
+ exifInterface = new ExifInterface(imageFile);
+
+ assertThat(exifInterface.getAttribute(ExifInterface.TAG_DIGITAL_ZOOM_RATIO))
+ .isEqualTo("0.8");
+ result =
+ exifInterface.getAttributeDouble(
+ ExifInterface.TAG_DIGITAL_ZOOM_RATIO, /* defaultValue= */ -1);
+ assertThat(result).isEqualTo(0.8);
+ }
+
+ @Test
+ @SmallTest
+ public void testSetDigitalZoomRatio_rationalString() throws Exception {
+ File imageFile =
+ copyFromResourceToFile(
+ R.raw.jpeg_with_exif_byte_order_ii, "jpeg_with_exif_byte_order_ii.jpg");
+ ExifInterface exifInterface = new ExifInterface(imageFile);
+
+ exifInterface.setAttribute(ExifInterface.TAG_DIGITAL_ZOOM_RATIO, "12/5");
+
+ assertThat(exifInterface.getAttribute(ExifInterface.TAG_DIGITAL_ZOOM_RATIO))
+ .isEqualTo("2.4");
+ double result =
+ exifInterface.getAttributeDouble(
+ ExifInterface.TAG_DIGITAL_ZOOM_RATIO, /* defaultValue= */ -1);
+ assertThat(result).isEqualTo(2.4);
+
+ exifInterface.saveAttributes();
+ exifInterface = new ExifInterface(imageFile);
+
+ assertThat(exifInterface.getAttribute(ExifInterface.TAG_DIGITAL_ZOOM_RATIO))
+ .isEqualTo("2.4");
+ result =
+ exifInterface.getAttributeDouble(
+ ExifInterface.TAG_DIGITAL_ZOOM_RATIO, /* defaultValue= */ -1);
+ assertThat(result).isEqualTo(2.4);
+ }
+
+ // https://issuetracker.google.com/312680558
+ @Test
+ @SmallTest
+ public void testSetExposureTime_decimalString() throws Exception {
+ File imageFile =
+ copyFromResourceToFile(
+ R.raw.jpeg_with_exif_byte_order_ii, "jpeg_with_exif_byte_order_ii.jpg");
+ ExifInterface exifInterface = new ExifInterface(imageFile);
+
+ exifInterface.setAttribute(ExifInterface.TAG_EXPOSURE_TIME, "0.000625");
+
+ assertThat(exifInterface.getAttribute(ExifInterface.TAG_EXPOSURE_TIME))
+ .isEqualTo("6.25E-4");
+ double result =
+ exifInterface.getAttributeDouble(
+ ExifInterface.TAG_EXPOSURE_TIME, /* defaultValue= */ -1);
+ assertThat(result).isEqualTo(0.000625);
+
+ exifInterface.saveAttributes();
+ exifInterface = new ExifInterface(imageFile);
+
+ assertThat(exifInterface.getAttribute(ExifInterface.TAG_EXPOSURE_TIME))
+ .isEqualTo("6.25E-4");
+ result =
+ exifInterface.getAttributeDouble(
+ ExifInterface.TAG_EXPOSURE_TIME, /* defaultValue= */ -1);
+ assertThat(result).isEqualTo(0.000625);
+ }
+
+ @Test
+ @SmallTest
+ public void testSetExposureTime_rationalString() throws Exception {
+ File imageFile =
+ copyFromResourceToFile(
+ R.raw.jpeg_with_exif_byte_order_ii, "jpeg_with_exif_byte_order_ii.jpg");
+ ExifInterface exifInterface = new ExifInterface(imageFile);
+
+ exifInterface.setAttribute(ExifInterface.TAG_EXPOSURE_TIME, "1/1600");
+
+ assertThat(exifInterface.getAttribute(ExifInterface.TAG_EXPOSURE_TIME))
+ .isEqualTo("6.25E-4");
+ double result =
+ exifInterface.getAttributeDouble(
+ ExifInterface.TAG_EXPOSURE_TIME, /* defaultValue= */ -1);
+ assertThat(result).isEqualTo(0.000625);
+
+ exifInterface.saveAttributes();
+ exifInterface = new ExifInterface(imageFile);
+
+ assertThat(exifInterface.getAttribute(ExifInterface.TAG_EXPOSURE_TIME))
+ .isEqualTo("6.25E-4");
+ result =
+ exifInterface.getAttributeDouble(
+ ExifInterface.TAG_EXPOSURE_TIME, /* defaultValue= */ -1);
+ assertThat(result).isEqualTo(0.000625);
+ }
+
+ @Test
+ @SmallTest
+ public void testSetSubjectDistance_decimalString() throws Exception {
+ File imageFile =
+ copyFromResourceToFile(
+ R.raw.jpeg_with_exif_byte_order_ii, "jpeg_with_exif_byte_order_ii.jpg");
+ ExifInterface exifInterface = new ExifInterface(imageFile);
+
+ String value = "3.5";
+ exifInterface.setAttribute(ExifInterface.TAG_SUBJECT_DISTANCE, value);
+
+ assertThat(exifInterface.getAttribute(ExifInterface.TAG_SUBJECT_DISTANCE)).isEqualTo(value);
+ double result =
+ exifInterface.getAttributeDouble(
+ ExifInterface.TAG_SUBJECT_DISTANCE, /* defaultValue= */ -1);
+ assertThat(result).isEqualTo(3.5);
+
+ exifInterface.saveAttributes();
+ exifInterface = new ExifInterface(imageFile);
+
+ assertThat(exifInterface.getAttribute(ExifInterface.TAG_SUBJECT_DISTANCE)).isEqualTo(value);
+ result =
+ exifInterface.getAttributeDouble(
+ ExifInterface.TAG_SUBJECT_DISTANCE, /* defaultValue= */ -1);
+ assertThat(result).isEqualTo(3.5);
+ }
+
+ @Test
+ @SmallTest
+ public void testSetSubjectDistance_rationalString() throws Exception {
+ File imageFile =
+ copyFromResourceToFile(
+ R.raw.jpeg_with_exif_byte_order_ii, "jpeg_with_exif_byte_order_ii.jpg");
+ ExifInterface exifInterface = new ExifInterface(imageFile);
+
+ exifInterface.setAttribute(ExifInterface.TAG_SUBJECT_DISTANCE, "7/2");
+
+ assertThat(exifInterface.getAttribute(ExifInterface.TAG_SUBJECT_DISTANCE)).isEqualTo("3.5");
+ double result =
+ exifInterface.getAttributeDouble(
+ ExifInterface.TAG_SUBJECT_DISTANCE, /* defaultValue= */ -1);
+ assertThat(result).isEqualTo(3.5);
+
+ exifInterface.saveAttributes();
+ exifInterface = new ExifInterface(imageFile);
+
+ assertThat(exifInterface.getAttribute(ExifInterface.TAG_SUBJECT_DISTANCE)).isEqualTo("3.5");
+ result =
+ exifInterface.getAttributeDouble(
+ ExifInterface.TAG_SUBJECT_DISTANCE, /* defaultValue= */ -1);
+ assertThat(result).isEqualTo(3.5);
+ }
+
+ @Test
+ @SmallTest
+ public void testSetGpsTimestamp_integers() throws Exception {
+ // Deliberately use an image with an existing GPS timestamp value to overwrite.
+ File imageFile =
+ copyFromResourceToFile(
+ R.raw.jpeg_with_exif_with_xmp, "jpeg_with_exif_with_xmp.jpg");
+ ExifInterface exifInterface = new ExifInterface(imageFile);
+
+ String timestamp = "11:06:52";
+ exifInterface.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, timestamp);
+
+ assertThat(exifInterface.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP))
+ .isEqualTo(timestamp);
+
+ exifInterface.saveAttributes();
+ exifInterface = new ExifInterface(imageFile);
+
+ assertThat(exifInterface.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP))
+ .isEqualTo(timestamp);
+ }
+
+ @Test
+ @SmallTest
+ public void testSetGpsTimestamp_rationals_failsSilently() throws Exception {
+ // Deliberately use an image with an existing GPS timestamp value to overwrite.
+ File imageFile =
+ copyFromResourceToFile(
+ R.raw.jpeg_with_exif_with_xmp, "jpeg_with_exif_with_xmp.jpg");
+ ExifInterface exifInterface = new ExifInterface(imageFile);
+
+ exifInterface.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "11/2:06/5:52/8");
+
+ assertThat(exifInterface.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP))
+ .isEqualTo(ExpectedAttributes.JPEG_WITH_EXIF_WITH_XMP.gpsTimestamp);
+
+ exifInterface.saveAttributes();
+ exifInterface = new ExifInterface(imageFile);
+
+ assertThat(exifInterface.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP))
+ .isEqualTo(ExpectedAttributes.JPEG_WITH_EXIF_WITH_XMP.gpsTimestamp);
+ }
+
+ @Test
+ @SmallTest
+ public void testSetGpsTimestamp_decimals_failsSilently() throws Exception {
+ // Deliberately use an image with an existing GPS timestamp value to overwrite.
+ File imageFile =
+ copyFromResourceToFile(
+ R.raw.jpeg_with_exif_with_xmp, "jpeg_with_exif_with_xmp.jpg");
+ ExifInterface exifInterface = new ExifInterface(imageFile);
+
+ exifInterface.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "11.5:06.3:52.8");
+
+ assertThat(exifInterface.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP))
+ .isEqualTo(ExpectedAttributes.JPEG_WITH_EXIF_WITH_XMP.gpsTimestamp);
+
+ exifInterface.saveAttributes();
+ exifInterface = new ExifInterface(imageFile);
+
+ assertThat(exifInterface.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP))
+ .isEqualTo(ExpectedAttributes.JPEG_WITH_EXIF_WITH_XMP.gpsTimestamp);
+ }
+
+ @Test
@LargeTest
public void testAddDefaultValuesForCompatibility() throws Exception {
File imageFile =
@@ -1143,6 +1427,136 @@
assertThat(exif.getAttribute(newTag)).isEqualTo(isoValue);
}
+ @Test
+ @SmallTest
+ public void testRationalFromDouble() {
+ double value = 0.12345678;
+
+ ExifInterface.Rational result = ExifInterface.Rational.createFromDouble(value);
+
+ expect.that(result.numerator).isEqualTo(150549);
+ expect.that(result.denominator).isEqualTo(1219447);
+ expect.that((double) result.numerator / result.denominator)
+ .isWithin(0.00000000001)
+ .of(value);
+ }
+
+ @Test
+ @SmallTest
+ public void testRationalFromDouble_niceFraction() {
+ double value = 1.0 / 1600;
+ ExifInterface.Rational result = ExifInterface.Rational.createFromDouble(value);
+
+ expect.that(result.numerator).isEqualTo(1);
+ expect.that(result.denominator).isEqualTo(1600);
+ expect.that((double) result.numerator / result.denominator).isEqualTo(value);
+ }
+
+ @Test
+ @SmallTest
+ public void testRationalFromDouble_recurringDecimal() {
+ double value = 1.0 / 3;
+ ExifInterface.Rational result = ExifInterface.Rational.createFromDouble(value);
+
+ expect.that(result.numerator).isEqualTo(1);
+ expect.that(result.denominator).isEqualTo(3);
+ expect.that((double) result.numerator / result.denominator).isEqualTo(value);
+ }
+
+ @Test
+ @SmallTest
+ public void testRationalFromDouble_negative() {
+ double value = -0.12345678;
+
+ ExifInterface.Rational result = ExifInterface.Rational.createFromDouble(value);
+
+ expect.that(result.numerator).isEqualTo(-150549);
+ expect.that(result.denominator).isEqualTo(1219447);
+ expect.that((double) result.numerator / result.denominator)
+ .isWithin(0.00000000001)
+ .of(value);
+ }
+
+ @Test
+ @SmallTest
+ public void testRationalFromDouble_maxLong() {
+ double value = Long.MAX_VALUE;
+
+ ExifInterface.Rational result = ExifInterface.Rational.createFromDouble(value);
+
+ expect.that(result.numerator).isEqualTo(Long.MAX_VALUE);
+ expect.that(result.denominator).isEqualTo(1);
+ }
+
+ @Test
+ @SmallTest
+ public void testRationalFromDouble_justLargerThanMaxLong() {
+ double value = Math.nextUp(Long.MAX_VALUE);
+
+ ExifInterface.Rational result = ExifInterface.Rational.createFromDouble(value);
+
+ expect.that(result.numerator).isEqualTo(Long.MAX_VALUE);
+ expect.that(result.denominator).isEqualTo(1);
+ }
+
+ @Test
+ @SmallTest
+ public void testRationalFromDouble_muchLargerThanMaxLong() {
+ double value = Long.MAX_VALUE + 10000.0;
+
+ ExifInterface.Rational result = ExifInterface.Rational.createFromDouble(value);
+
+ expect.that(result.numerator).isEqualTo(Long.MAX_VALUE);
+ expect.that(result.denominator).isEqualTo(1);
+ }
+
+ @Test
+ @SmallTest
+ public void testRationalFromDouble_minLong() {
+ double value = Math.nextDown(Long.MIN_VALUE);
+
+ ExifInterface.Rational result = ExifInterface.Rational.createFromDouble(value);
+
+ expect.that(result.numerator).isEqualTo(Long.MIN_VALUE);
+ expect.that(result.denominator).isEqualTo(1);
+ }
+
+ // Ensure that a very large negative number, which is just higher (closer to positive infinity)
+ // than Long.MIN_VALUE doesn't cause overflow.
+ @Test
+ @SmallTest
+ public void testRationalFromDouble_justHigherThanMinLong() {
+ double value = Math.nextUp(Long.MIN_VALUE);
+
+ ExifInterface.Rational result = ExifInterface.Rational.createFromDouble(value);
+
+ long expectedNumerator = Math.round(value);
+ expect.that(result.numerator).isEqualTo(expectedNumerator);
+ expect.that(result.denominator).isEqualTo(1);
+ }
+
+ @Test
+ @SmallTest
+ public void testRationalFromDouble_justLowerThanMinLong() {
+ double value = Math.nextDown(Long.MIN_VALUE);
+
+ ExifInterface.Rational result = ExifInterface.Rational.createFromDouble(value);
+
+ expect.that(result.numerator).isEqualTo(Long.MIN_VALUE);
+ expect.that(result.denominator).isEqualTo(1);
+ }
+
+ @Test
+ @SmallTest
+ public void testRationalFromDouble_muchLowerThanMinLong() {
+ double value = Long.MIN_VALUE - 1000.0;
+
+ ExifInterface.Rational result = ExifInterface.Rational.createFromDouble(value);
+
+ expect.that(result.numerator).isEqualTo(Long.MIN_VALUE);
+ expect.that(result.denominator).isEqualTo(1);
+ }
+
private void printExifTagsAndValues(String fileName, ExifInterface exifInterface) {
// Prints thumbnail information.
if (exifInterface.hasThumbnail()) {
@@ -1215,7 +1629,9 @@
if (expectedAttributes.hasLatLong) {
expect.that(latLong)
.usingExactEquality()
- .containsExactly(expectedAttributes.latitude, expectedAttributes.longitude)
+ .containsExactly(
+ expectedAttributes.computedLatitude,
+ expectedAttributes.computedLongitude)
.inOrder();
expect.that(exifInterface.hasAttribute(ExifInterface.TAG_GPS_LATITUDE)).isTrue();
expect.that(exifInterface.hasAttribute(ExifInterface.TAG_GPS_LONGITUDE)).isTrue();
@@ -1224,7 +1640,7 @@
expect.that(exifInterface.hasAttribute(ExifInterface.TAG_GPS_LATITUDE)).isFalse();
expect.that(exifInterface.hasAttribute(ExifInterface.TAG_GPS_LONGITUDE)).isFalse();
}
- expect.that(exifInterface.getAltitude(.0)).isEqualTo(expectedAttributes.altitude);
+ expect.that(exifInterface.getAltitude(.0)).isEqualTo(expectedAttributes.computedAltitude);
// Checks values.
expectStringTag(exifInterface, ExifInterface.TAG_MAKE, expectedAttributes.make);
@@ -1424,7 +1840,8 @@
expect.that(exifInterface.getAttributeRange(ExifInterface.TAG_GPS_LATITUDE))
.asList()
.containsExactly(
- expectedAttributes.latitudeOffset, expectedAttributes.latitudeLength)
+ expectedAttributes.gpsLatitudeOffset,
+ expectedAttributes.gpsLatitudeLength)
.inOrder();
// TODO: Add code for retrieving raw latitude data using offset and length
} else {
diff --git a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExpectedAttributes.java b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExpectedAttributes.java
index c5611c4..6a4b30e 100644
--- a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExpectedAttributes.java
+++ b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExpectedAttributes.java
@@ -39,7 +39,7 @@
.setThumbnailSize(512, 288)
.setIsThumbnailCompressed(true)
.setMake("SAMSUNG")
- .setMakeOffsetAndLength(160, 8)
+ .setMakeOffset(160)
.setModel("SM-N900S")
.setAperture(2.2)
.setDateTimeOriginal("2016:01:29 18:32:27")
@@ -64,11 +64,11 @@
/** Expected attributes for {@link R.raw#jpeg_with_exif_byte_order_mm}. */
public static final ExpectedAttributes JPEG_WITH_EXIF_BYTE_ORDER_MM =
new Builder()
- .setLatitudeOffsetAndLength(584, 24)
- .setLatLong(0, 0)
- .setAltitude(0)
+ .setGpsLatitudeOffsetAndLength(584, 24)
+ .setComputedLatLong(0, 0)
+ .setComputedAltitude(0)
.setMake("LGE")
- .setMakeOffsetAndLength(414, 4)
+ .setMakeOffset(414)
.setModel("Nexus 5")
.setAperture(2.4)
.setDateTimeOriginal("2016:01:29 15:44:58")
@@ -94,7 +94,7 @@
public static final ExpectedAttributes JPEG_WITH_EXIF_BYTE_ORDER_MM_STANDALONE =
JPEG_WITH_EXIF_BYTE_ORDER_MM
.buildUpon()
- .setLatitudeOffset(JPEG_WITH_EXIF_BYTE_ORDER_MM.latitudeOffset - 6)
+ .setGpsLatitudeOffset(JPEG_WITH_EXIF_BYTE_ORDER_MM.gpsLatitudeOffset - 6)
.setMakeOffset(JPEG_WITH_EXIF_BYTE_ORDER_MM.makeOffset - 6)
.setImageSize(0, 0)
.build();
@@ -116,11 +116,11 @@
.setThumbnailOffsetAndLength(12570, 15179)
.setThumbnailSize(256, 144)
.setIsThumbnailCompressed(true)
- .setLatitudeOffsetAndLength(12486, 24)
- .setLatLong(53.83450833333334, 10.69585)
- .setAltitude(0)
+ .setGpsLatitudeOffsetAndLength(12486, 24)
+ .setComputedLatLong(53.83450833333334, 10.69585)
+ .setComputedAltitude(0)
.setMake("LGE")
- .setMakeOffsetAndLength(102, 4)
+ .setMakeOffset(102)
.setModel("LG-H815")
.setAperture(1.8)
.setDateTimeOriginal("2015:11:12 16:46:18")
@@ -143,7 +143,7 @@
DNG_WITH_EXIF_WITH_XMP
.buildUpon()
.clearThumbnail()
- .setLatitudeOffset(1692)
+ .setGpsLatitudeOffset(1692)
.setMakeOffset(84)
.setOrientation(ExifInterface.ORIENTATION_NORMAL)
.setXmpResourceId(R.raw.jpeg_xmp)
@@ -175,7 +175,7 @@
public static final ExpectedAttributes HEIF_WITH_EXIF_BELOW_API_31 =
new Builder()
.setMake("LGE")
- .setMakeOffsetAndLength(3519, 4)
+ .setMakeOffset(3519)
.setModel("Nexus 5")
.setImageSize(1920, 1080)
.setOrientation(ExifInterface.ORIENTATION_NORMAL)
@@ -203,14 +203,22 @@
// GPS information.
private boolean mHasLatLong;
- private long mLatitudeOffset;
- private long mLatitudeLength;
- private double mLatitude;
- private double mLongitude;
- private double mAltitude;
+ private double mComputedLatitude;
+ private double mComputedLongitude;
+ private double mComputedAltitude;
+ @Nullable private String mGpsAltitude;
+ @Nullable private String mGpsAltitudeRef;
+ @Nullable private String mGpsDatestamp;
+ @Nullable private String mGpsLatitude;
+ private long mGpsLatitudeOffset;
+ private long mGpsLatitudeLength;
+ @Nullable private String mGpsLatitudeRef;
+ @Nullable private String mGpsLongitude;
+ @Nullable private String mGpsLongitudeRef;
+ @Nullable private String mGpsProcessingMethod;
+ @Nullable private String mGpsTimestamp;
// Make information
- private boolean mHasMake;
private long mMakeOffset;
private long mMakeLength;
@Nullable private String mMake;
@@ -222,15 +230,6 @@
private double mExposureTime;
private double mFlash;
@Nullable private String mFocalLength;
- @Nullable private String mGpsAltitude;
- @Nullable private String mGpsAltitudeRef;
- @Nullable private String mGpsDatestamp;
- @Nullable private String mGpsLatitude;
- @Nullable private String mGpsLatitudeRef;
- @Nullable private String mGpsLongitude;
- @Nullable private String mGpsLongitudeRef;
- @Nullable private String mGpsProcessingMethod;
- @Nullable private String mGpsTimestamp;
private int mImageLength;
private int mImageWidth;
@Nullable private String mIso;
@@ -254,12 +253,20 @@
mThumbnailHeight = attributes.thumbnailHeight;
mIsThumbnailCompressed = attributes.isThumbnailCompressed;
mHasLatLong = attributes.hasLatLong;
- mLatitude = attributes.latitude;
- mLatitudeOffset = attributes.latitudeOffset;
- mLatitudeLength = attributes.latitudeLength;
- mLongitude = attributes.longitude;
- mAltitude = attributes.altitude;
- mHasMake = attributes.hasMake;
+ mComputedLatitude = attributes.computedLatitude;
+ mComputedLongitude = attributes.computedLongitude;
+ mComputedAltitude = attributes.computedAltitude;
+ mGpsAltitude = attributes.gpsAltitude;
+ mGpsAltitudeRef = attributes.gpsAltitudeRef;
+ mGpsDatestamp = attributes.gpsDatestamp;
+ mGpsLatitude = attributes.gpsLatitude;
+ mGpsLatitudeOffset = attributes.gpsLatitudeOffset;
+ mGpsLatitudeLength = attributes.gpsLatitudeLength;
+ mGpsLatitudeRef = attributes.gpsLatitudeRef;
+ mGpsLongitude = attributes.gpsLongitude;
+ mGpsLongitudeRef = attributes.gpsLongitudeRef;
+ mGpsProcessingMethod = attributes.gpsProcessingMethod;
+ mGpsTimestamp = attributes.gpsTimestamp;
mMakeOffset = attributes.makeOffset;
mMakeLength = attributes.makeLength;
mMake = attributes.make;
@@ -268,15 +275,6 @@
mDateTimeOriginal = attributes.dateTimeOriginal;
mExposureTime = attributes.exposureTime;
mFocalLength = attributes.focalLength;
- mGpsAltitude = attributes.gpsAltitude;
- mGpsAltitudeRef = attributes.gpsAltitudeRef;
- mGpsDatestamp = attributes.gpsDatestamp;
- mGpsLatitude = attributes.gpsLatitude;
- mGpsLatitudeRef = attributes.gpsLatitudeRef;
- mGpsLongitude = attributes.gpsLongitude;
- mGpsLongitudeRef = attributes.gpsLongitudeRef;
- mGpsProcessingMethod = attributes.gpsProcessingMethod;
- mGpsTimestamp = attributes.gpsTimestamp;
mImageLength = attributes.imageLength;
mImageWidth = attributes.imageWidth;
mIso = attributes.iso;
@@ -328,68 +326,101 @@
return this;
}
- public Builder setLatLong(double latitude, double longitude) {
+ public Builder setComputedLatLong(double computedLatitude, double computedLongitude) {
mHasLatLong = true;
- mLatitude = latitude;
- mLongitude = longitude;
+ mComputedLatitude = computedLatitude;
+ mComputedLongitude = computedLongitude;
return this;
}
- public Builder setLatitudeOffsetAndLength(long offset, long length) {
- mHasLatLong = true;
- mLatitudeOffset = offset;
- mLatitudeLength = length;
+ public Builder clearComputedLatLong() {
+ mHasLatLong = false;
+ mComputedLatitude = 0;
+ mComputedLongitude = 0;
return this;
}
- public Builder setLatitudeOffset(long offset) {
+ public Builder setGpsAltitude(@Nullable String gpsAltitude) {
+ mGpsAltitude = gpsAltitude;
+ return this;
+ }
+
+ public Builder setGpsAltitudeRef(@Nullable String gpsAltitudeRef) {
+ mGpsAltitudeRef = gpsAltitudeRef;
+ return this;
+ }
+
+ public Builder setGpsDatestamp(@Nullable String gpsDatestamp) {
+ mGpsDatestamp = gpsDatestamp;
+ return this;
+ }
+
+ public Builder setGpsLatitude(@Nullable String gpsLatitude) {
+ mGpsLatitude = gpsLatitude;
+ return this;
+ }
+
+ public Builder setGpsLatitudeOffsetAndLength(long offset, long length) {
+ mHasLatLong = true;
+ mGpsLatitudeOffset = offset;
+ mGpsLatitudeLength = length;
+ return this;
+ }
+
+ public Builder setGpsLatitudeOffset(long offset) {
if (!mHasLatLong) {
throw new IllegalStateException(
"Latitude position in the file must first be "
+ "set with setLatitudeOffsetAndLength(...)");
}
- mLatitudeOffset = offset;
+ mGpsLatitudeOffset = offset;
return this;
}
- public Builder clearLatLong() {
- mHasLatLong = false;
- mLatitude = 0;
- mLongitude = 0;
+ public Builder setGpsLatitudeRef(@Nullable String gpsLatitudeRef) {
+ mGpsLatitudeRef = gpsLatitudeRef;
return this;
}
- public Builder setAltitude(double altitude) {
- mAltitude = altitude;
+ public Builder setGpsLongitude(@Nullable String gpsLongitude) {
+ mGpsLongitude = gpsLongitude;
+ return this;
+ }
+
+ public Builder setGpsLongitudeRef(@Nullable String gpsLongitudeRef) {
+ mGpsLongitudeRef = gpsLongitudeRef;
+ return this;
+ }
+
+ public Builder setGpsProcessingMethod(@Nullable String gpsProcessingMethod) {
+ mGpsProcessingMethod = gpsProcessingMethod;
+ return this;
+ }
+
+ public Builder setGpsTimestamp(@Nullable String gpsTimestamp) {
+ mGpsTimestamp = gpsTimestamp;
+ return this;
+ }
+
+ public Builder setComputedAltitude(double computedAltitude) {
+ mComputedAltitude = computedAltitude;
return this;
}
public Builder setMake(@Nullable String make) {
if (make == null) {
- mHasMake = false;
mMakeOffset = 0;
mMakeLength = 0;
} else {
- mHasMake = true;
mMake = make;
+ mMakeLength = make.length() + 1;
}
return this;
}
- // TODO: b/270554381 - consider deriving length automatically from `make.length() + 1`
- // (since the string is null-terminated in the format).
- public Builder setMakeOffsetAndLength(long offset, long length) {
- mHasMake = true;
- mMakeOffset = offset;
- mMakeLength = length;
- return this;
- }
-
public Builder setMakeOffset(long offset) {
- if (!mHasMake) {
- throw new IllegalStateException(
- "Make position in the file must first be set with"
- + " setMakeOffsetAndLength(...)");
+ if (mMake == null) {
+ throw new IllegalStateException("Make must first be set with setMake(...)");
}
mMakeOffset = offset;
return this;
@@ -425,51 +456,6 @@
return this;
}
- public Builder setGpsAltitude(@Nullable String gpsAltitude) {
- mGpsAltitude = gpsAltitude;
- return this;
- }
-
- public Builder setGpsAltitudeRef(@Nullable String gpsAltitudeRef) {
- mGpsAltitudeRef = gpsAltitudeRef;
- return this;
- }
-
- public Builder setGpsDatestamp(@Nullable String gpsDatestamp) {
- mGpsDatestamp = gpsDatestamp;
- return this;
- }
-
- public Builder setGpsLatitude(@Nullable String gpsLatitude) {
- mGpsLatitude = gpsLatitude;
- return this;
- }
-
- public Builder setGpsLatitudeRef(@Nullable String gpsLatitudeRef) {
- mGpsLatitudeRef = gpsLatitudeRef;
- return this;
- }
-
- public Builder setGpsLongitude(@Nullable String gpsLongitude) {
- mGpsLongitude = gpsLongitude;
- return this;
- }
-
- public Builder setGpsLongitudeRef(@Nullable String gpsLongitudeRef) {
- mGpsLongitudeRef = gpsLongitudeRef;
- return this;
- }
-
- public Builder setGpsProcessingMethod(@Nullable String gpsProcessingMethod) {
- mGpsProcessingMethod = gpsProcessingMethod;
- return this;
- }
-
- public Builder setGpsTimestamp(@Nullable String gpsTimestamp) {
- mGpsTimestamp = gpsTimestamp;
- return this;
- }
-
public Builder setImageSize(int imageWidth, int imageLength) {
mImageWidth = imageWidth;
mImageLength = imageLength;
@@ -546,81 +532,77 @@
}
}
- // TODO: b/270554381 - Add nullability annotations below.
-
// Thumbnail information.
public final boolean hasThumbnail;
public final int thumbnailWidth;
public final int thumbnailHeight;
public final boolean isThumbnailCompressed;
- // TODO: b/270554381 - Merge these offset and length (and others) into long[] arrays, and
- // move them down to their own section. This may also allow removing some of the hasXXX
- // fields.
public final long thumbnailOffset;
public final long thumbnailLength;
// GPS information.
public final boolean hasLatLong;
- // TODO: b/270554381 - Merge this and longitude into a double[]
- public final double latitude;
- public final long latitudeOffset;
- public final long latitudeLength;
- public final double longitude;
- public final double altitude;
+ public final double computedLatitude;
+ public final double computedLongitude;
+ public final double computedAltitude;
+ @Nullable public final String gpsAltitude;
+ @Nullable public final String gpsAltitudeRef;
+ @Nullable public final String gpsDatestamp;
+ @Nullable public final String gpsLatitude;
+ public final long gpsLatitudeOffset;
+ public final long gpsLatitudeLength;
+ @Nullable public final String gpsLatitudeRef;
+ @Nullable public final String gpsLongitude;
+ @Nullable public final String gpsLongitudeRef;
+ @Nullable public final String gpsProcessingMethod;
+ @Nullable public final String gpsTimestamp;
// Make information
- public final boolean hasMake;
public final long makeOffset;
public final long makeLength;
- public final String make;
+ @Nullable public final String make;
// Values.
- public final String model;
+ @Nullable public final String model;
public final double aperture;
- public final String dateTimeOriginal;
+ @Nullable public final String dateTimeOriginal;
public final double exposureTime;
- public final String focalLength;
- // TODO: b/270554381 - Rename these to make them clear they're strings, or original values,
- // and move them closer to the (computed) latitude/longitude/altitude values. Consider
- // also having a verification check that they are consistent with latitude/longitude (but
- // not sure how to reconcile that with "don't duplicate business logic in tests").
- public final String gpsAltitude;
- public final String gpsAltitudeRef;
- public final String gpsDatestamp;
- public final String gpsLatitude;
- public final String gpsLatitudeRef;
- public final String gpsLongitude;
- public final String gpsLongitudeRef;
- public final String gpsProcessingMethod;
- public final String gpsTimestamp;
+ @Nullable public final String focalLength;
public final int imageLength;
public final int imageWidth;
- public final String iso;
+ @Nullable public final String iso;
public final int orientation;
// XMP information.
+ public final boolean hasXmp;
@Nullable private final String mXmp;
@Nullable private final Integer mXmpResourceId;
@Nullable private String mMemoizedXmp;
- public final boolean hasXmp;
public final long xmpOffset;
public final long xmpLength;
private ExpectedAttributes(Builder builder) {
- // TODO: b/270554381 - Re-order these assignments to match the fields above.
hasThumbnail = builder.mHasThumbnail;
- thumbnailOffset = builder.mThumbnailOffset;
- thumbnailLength = builder.mThumbnailLength;
thumbnailWidth = builder.mThumbnailWidth;
thumbnailHeight = builder.mThumbnailHeight;
isThumbnailCompressed = builder.mIsThumbnailCompressed;
+ thumbnailOffset = builder.mThumbnailOffset;
+ thumbnailLength = builder.mThumbnailLength;
hasLatLong = builder.mHasLatLong;
- latitudeOffset = builder.mLatitudeOffset;
- latitudeLength = builder.mLatitudeLength;
- latitude = builder.mLatitude;
- longitude = builder.mLongitude;
- altitude = builder.mAltitude;
- hasMake = builder.mHasMake;
+ computedLatitude = builder.mComputedLatitude;
+ computedLongitude = builder.mComputedLongitude;
+ computedAltitude = builder.mComputedAltitude;
+ gpsAltitude = builder.mGpsAltitude;
+ gpsAltitudeRef = builder.mGpsAltitudeRef;
+ gpsDatestamp = builder.mGpsDatestamp;
+ gpsLatitude = builder.mGpsLatitude;
+ gpsLatitudeOffset = builder.mGpsLatitudeOffset;
+ gpsLatitudeLength = builder.mGpsLatitudeLength;
+ gpsLatitudeRef = builder.mGpsLatitudeRef;
+ gpsLongitude = builder.mGpsLongitude;
+ gpsLongitudeRef = builder.mGpsLongitudeRef;
+ gpsProcessingMethod = builder.mGpsProcessingMethod;
+ gpsTimestamp = builder.mGpsTimestamp;
makeOffset = builder.mMakeOffset;
makeLength = builder.mMakeLength;
make = builder.mMake;
@@ -629,15 +611,6 @@
dateTimeOriginal = builder.mDateTimeOriginal;
exposureTime = builder.mExposureTime;
focalLength = builder.mFocalLength;
- gpsAltitude = builder.mGpsAltitude;
- gpsAltitudeRef = builder.mGpsAltitudeRef;
- gpsDatestamp = builder.mGpsDatestamp;
- gpsLatitude = builder.mGpsLatitude;
- gpsLatitudeRef = builder.mGpsLatitudeRef;
- gpsLongitude = builder.mGpsLongitude;
- gpsLongitudeRef = builder.mGpsLongitudeRef;
- gpsProcessingMethod = builder.mGpsProcessingMethod;
- gpsTimestamp = builder.mGpsTimestamp;
imageLength = builder.mImageLength;
imageWidth = builder.mImageWidth;
iso = builder.mIso;
diff --git a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
index 5eb80b4..863584a0 100644
--- a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
+++ b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
@@ -42,6 +42,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
import androidx.exifinterface.media.ExifInterfaceUtils.Api21Impl;
import androidx.exifinterface.media.ExifInterfaceUtils.Api23Impl;
@@ -69,6 +70,7 @@
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Arrays;
+import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
@@ -875,27 +877,41 @@
// G. Tags related to picture-taking condition
/**
- * <p>Exposure time, given in seconds.</p>
+ * Exposure time, given in seconds.
*
- * <ul>
- * <li>Tag = 33434</li>
- * <li>Type = Unsigned rational</li>
- * <li>Count = 1</li>
- * <li>Default = None</li>
- * </ul>
+ * <p>Note: For backwards compatibility this attribute is returned from {@link
+ * #getAttribute(String)} in decimal form (i.e. the format produced by {@link
+ * Double#toString(double)}). It is accepted into {@link #setAttribute(String, String)} in both
+ * rational (e.g. {@code "1/3"}) and decimal forms. The decimal format is anything accepted by
+ * {@link Double#parseDouble(String)}, e.g. {@code "0.125"}.
+ *
+ * <ul>
+ * <li>Tag = 33434
+ * <li>Type = Unsigned rational
+ * <li>Count = 1
+ * <li>Default = None
+ * </ul>
*/
public static final String TAG_EXPOSURE_TIME = "ExposureTime";
+
/**
- * <p>The F number.</p>
+ * The F number.
*
- * <ul>
- * <li>Tag = 33437</li>
- * <li>Type = Unsigned rational</li>
- * <li>Count = 1</li>
- * <li>Default = None</li>
- * </ul>
+ * <p>Note: For backwards compatibility this attribute is returned from {@link
+ * #getAttribute(String)} in decimal form (i.e. the format produced by {@link
+ * Double#toString(double)}). It is accepted into {@link #setAttribute(String, String)} in both
+ * rational (e.g. {@code "1/3"}) and decimal forms. The decimal format is anything accepted by
+ * {@link Double#parseDouble(String)}, e.g. {@code "0.125"}.
+ *
+ * <ul>
+ * <li>Tag = 33437
+ * <li>Type = Unsigned rational
+ * <li>Count = 1
+ * <li>Default = None
+ * </ul>
*/
public static final String TAG_F_NUMBER = "FNumber";
+
/**
* <p>The class of the program used by the camera to set exposure when the picture is taken.
* The tag values are as follows.</p>
@@ -1109,19 +1125,28 @@
* </ul>
*/
public static final String TAG_MAX_APERTURE_VALUE = "MaxApertureValue";
+
/**
- * <p>The distance to the subject, given in meters. Note that if the numerator of the recorded
- * value is 0xFFFFFFFF, Infinity shall be indicated; and if the numerator is 0, Distance
- * unknown shall be indicated.</p>
+ * The distance to the subject, given in meters.
*
- * <ul>
- * <li>Tag = 37382</li>
- * <li>Type = Unsigned rational</li>
- * <li>Count = 1</li>
- * <li>Default = None</li>
- * </ul>
+ * <p>Note that if the numerator of the recorded value is 0xFFFFFFFF, Infinity shall be
+ * indicated; and if the numerator is 0, Distance unknown shall be indicated.
+ *
+ * <p>Note: For backwards compatibility this attribute is returned from {@link
+ * #getAttribute(String)} in decimal form (i.e. the format produced by {@link
+ * Double#toString(double)}). It is accepted into {@link #setAttribute(String, String)} in both
+ * rational (e.g. {@code "1/3"}) and decimal forms. The decimal format is anything accepted by
+ * {@link Double#parseDouble(String)}, e.g. {@code "0.125"}.
+ *
+ * <ul>
+ * <li>Tag = 37382
+ * <li>Type = Unsigned rational
+ * <li>Count = 1
+ * <li>Default = None
+ * </ul>
*/
public static final String TAG_SUBJECT_DISTANCE = "SubjectDistance";
+
/**
* <p>The metering mode.</p>
*
@@ -1443,18 +1468,26 @@
* @see #WHITEBALANCE_MANUAL
*/
public static final String TAG_WHITE_BALANCE = "WhiteBalance";
+
/**
- * <p>This tag indicates the digital zoom ratio when the image was shot. If the numerator of
- * the recorded value is 0, this indicates that digital zoom was not used.</p>
+ * This tag indicates the digital zoom ratio when the image was shot. If the numerator of the
+ * recorded value is 0, this indicates that digital zoom was not used.
*
- * <ul>
- * <li>Tag = 41988</li>
- * <li>Type = Unsigned rational</li>
- * <li>Count = 1</li>
- * <li>Default = None</li>
- * </ul>
+ * <p>Note: For backwards compatibility this attribute is returned from {@link
+ * #getAttribute(String)} in decimal form (i.e. the format produced by {@link
+ * Double#toString(double)}). It is accepted into {@link #setAttribute(String, String)} in both
+ * rational (e.g. {@code "1/3"}) and decimal forms. The decimal format is anything accepted by
+ * {@link Double#parseDouble(String)}, e.g. {@code "0.125"}.
+ *
+ * <ul>
+ * <li>Tag = 41988
+ * <li>Type = Unsigned rational
+ * <li>Count = 1
+ * <li>Default = None
+ * </ul>
*/
public static final String TAG_DIGITAL_ZOOM_RATIO = "DigitalZoomRatio";
+
/**
* <p>This tag indicates the equivalent focal length assuming a 35mm film camera, in mm.
* A value of 0 means the focal length is unknown. Note that this tag differs from
@@ -1784,18 +1817,24 @@
* </ul>
*/
public static final String TAG_GPS_ALTITUDE = "GPSAltitude";
+
/**
- * <p>Indicates the time as UTC (Coordinated Universal Time). TimeStamp is expressed as three
- * unsigned rational values giving the hour, minute, and second.</p>
+ * Indicates the time as UTC (Coordinated Universal Time). TimeStamp is expressed as three
+ * unsigned rational values giving the hour, minute, and second.
*
- * <ul>
- * <li>Tag = 7</li>
- * <li>Type = Unsigned rational</li>
- * <li>Count = 3</li>
- * <li>Default = None</li>
- * </ul>
+ * <p>Note: This attribute is returned from {@link #getAttribute(String)} and accepted into
+ * {@link #setAttribute(String, String)} as 3 colon-separated integers, e.g. {@code "11:05:32"}.
+ * Decimal or rational hours, minutes or seconds parts are not supported.
+ *
+ * <ul>
+ * <li>Tag = 7
+ * <li>Type = Unsigned rational
+ * <li>Count = 3
+ * <li>Default = None
+ * </ul>
*/
public static final String TAG_GPS_TIMESTAMP = "GPSTimeStamp";
+
/**
* <p>Indicates the GPS satellites used for measurements. This tag may be used to describe
* the number of satellites, their ID number, angle of elevation, azimuth, SNR and other
@@ -2997,12 +3036,9 @@
(byte) 0x47, (byte) 0x0d, (byte) 0x0a, (byte) 0x1a, (byte) 0x0a};
// See "Extensions to the PNG 1.2 Specification, Version 1.5.0",
// 3.7. eXIf Exchangeable Image File (Exif) Profile
- private static final byte[] PNG_CHUNK_TYPE_EXIF = new byte[]{(byte) 0x65, (byte) 0x58,
- (byte) 0x49, (byte) 0x66};
- private static final byte[] PNG_CHUNK_TYPE_IHDR = new byte[]{(byte) 0x49, (byte) 0x48,
- (byte) 0x44, (byte) 0x52};
- private static final byte[] PNG_CHUNK_TYPE_IEND = new byte[]{(byte) 0x49, (byte) 0x45,
- (byte) 0x4e, (byte) 0x44};
+ private static final int PNG_CHUNK_TYPE_EXIF = intFromBytes('e', 'X', 'I', 'f');
+ private static final int PNG_CHUNK_TYPE_IHDR = intFromBytes('I', 'H', 'D', 'R');
+ private static final int PNG_CHUNK_TYPE_IEND = intFromBytes('I', 'E', 'N', 'D');
private static final int PNG_CHUNK_TYPE_BYTE_LENGTH = 4;
private static final int PNG_CHUNK_CRC_BYTE_LENGTH = 4;
@@ -3074,16 +3110,13 @@
};
// A class for indicating EXIF rational type.
- private static class Rational {
+ // TODO: b/308978831 - Migrate to android.util.Rational when the min API is 21.
+ @VisibleForTesting
+ static class Rational {
public final long numerator;
public final long denominator;
@SuppressWarnings("WeakerAccess") /* synthetic access */
- Rational(double value) {
- this((long) (value * 10000), 10000);
- }
-
- @SuppressWarnings("WeakerAccess") /* synthetic access */
Rational(long numerator, long denominator) {
// Handle erroneous case
if (denominator == 0) {
@@ -3095,6 +3128,43 @@
this.denominator = denominator;
}
+ /**
+ * Creates a new {@code Rational} which approximates the provided {@code double} value by
+ * using <a href="https://en.wikipedia.org/wiki/Continued_fraction">continued fractions</a>.
+ */
+ @NonNull
+ public static Rational createFromDouble(double value) {
+ if (value >= Long.MAX_VALUE || value <= Long.MIN_VALUE) {
+ // value is too large to represent as a long, so just return the max/min value.
+ return new Rational(
+ /* numerator= */ value > 0 ? Long.MAX_VALUE : Long.MIN_VALUE,
+ /* denominator= */ 1);
+ }
+
+ double absoluteValue = Math.abs(value);
+ double threshold = 0.00000001 * absoluteValue;
+ double remainingValue = absoluteValue;
+ long numerator = 1;
+ long previousNumerator = 0;
+ long denominator = 0;
+ long previousDenominator = 1;
+ do {
+ double remainder = remainingValue % 1;
+ long wholePart = (long) (remainingValue - remainder);
+ long tmp = numerator;
+ numerator = wholePart * numerator + previousNumerator;
+ previousNumerator = tmp;
+
+ tmp = denominator;
+ denominator = wholePart * denominator + previousDenominator;
+ previousDenominator = tmp;
+
+ remainingValue = 1 / remainder;
+ } while ((Math.abs(absoluteValue - numerator / (double) denominator) > threshold));
+
+ return new Rational(value < 0 ? -numerator : numerator, denominator);
+ }
+
@NonNull
@Override
public String toString() {
@@ -3793,9 +3863,30 @@
@SuppressWarnings("unchecked")
private static final HashMap<String, ExifTag>[] sExifTagMapsForWriting =
new HashMap[EXIF_TAGS.length];
- private static final HashSet<String> sTagSetForCompatibility = new HashSet<>(Arrays.asList(
- TAG_F_NUMBER, TAG_DIGITAL_ZOOM_RATIO, TAG_EXPOSURE_TIME, TAG_SUBJECT_DISTANCE,
- TAG_GPS_TIMESTAMP));
+
+ /**
+ * These are tags of type 'Unsigned rational' but which are handled in decimal form.
+ *
+ * <p>This means they are output from {@link #getAttribute(String)}, and accepted into {@link
+ * #setAttribute(String, String)}, as strings in decimal form (e.g. {@code "0.125"}, {@code
+ * "6.25E-4"}).
+ *
+ * <p>This is to maintain backwards compatibility with a previous implementation of the {@link
+ * android.media.ExifInterface} (the platform variant of this class).
+ *
+ * <p>See <a
+ * href="http://ag/c/platform/frameworks/base/+/909922/2..9/api/current.txt#b20093">this
+ * internal code review comment from 2016</a> for more details.
+ */
+ private static final Set<String> RATIONAL_TAGS_HANDLED_AS_DECIMALS_FOR_COMPATIBILITY =
+ Collections.unmodifiableSet(
+ new HashSet<>(
+ Arrays.asList(
+ TAG_F_NUMBER,
+ TAG_DIGITAL_ZOOM_RATIO,
+ TAG_EXPOSURE_TIME,
+ TAG_SUBJECT_DISTANCE)));
+
// Mappings from tag number to IFD type for pointer tags.
private static final HashMap<Integer, Integer> sExifPointerTagMap = new HashMap<>();
@@ -4116,34 +4207,35 @@
throw new NullPointerException("tag shouldn't be null");
}
ExifAttribute attribute = getExifAttribute(tag);
- if (attribute != null) {
- if (!sTagSetForCompatibility.contains(tag)) {
- return attribute.getStringValue(mExifByteOrder);
+ if (attribute == null) {
+ return null;
+ }
+ if (tag.equals(TAG_GPS_TIMESTAMP)) {
+ // Convert GPS timestamp value to a custom format for backwards compatibility.
+ if (attribute.format != IFD_FORMAT_URATIONAL
+ && attribute.format != IFD_FORMAT_SRATIONAL) {
+ Log.w(TAG, "GPS Timestamp format is not rational. format=" + attribute.format);
+ return null;
}
- if (tag.equals(TAG_GPS_TIMESTAMP)) {
- // Convert the rational values to the custom formats for backwards compatibility.
- if (attribute.format != IFD_FORMAT_URATIONAL
- && attribute.format != IFD_FORMAT_SRATIONAL) {
- Log.w(TAG, "GPS Timestamp format is not rational. format=" + attribute.format);
- return null;
- }
- Rational[] array = (Rational[]) attribute.getValue(mExifByteOrder);
- if (array == null || array.length != 3) {
- Log.w(TAG, "Invalid GPS Timestamp array. array=" + Arrays.toString(array));
- return null;
- }
- return String.format("%02d:%02d:%02d",
- (int) ((float) array[0].numerator / array[0].denominator),
- (int) ((float) array[1].numerator / array[1].denominator),
- (int) ((float) array[2].numerator / array[2].denominator));
+ Rational[] array = (Rational[]) attribute.getValue(mExifByteOrder);
+ if (array == null || array.length != 3) {
+ Log.w(TAG, "Invalid GPS Timestamp array. array=" + Arrays.toString(array));
+ return null;
}
+ return String.format("%02d:%02d:%02d",
+ (int) ((float) array[0].numerator / array[0].denominator),
+ (int) ((float) array[1].numerator / array[1].denominator),
+ (int) ((float) array[2].numerator / array[2].denominator));
+ } else if (RATIONAL_TAGS_HANDLED_AS_DECIMALS_FOR_COMPATIBILITY.contains(tag)) {
+ // Convert the rational values to the custom formats for backwards compatibility.
try {
return Double.toString(attribute.getDoubleValue(mExifByteOrder));
} catch (NumberFormatException e) {
return null;
}
+ } else {
+ return attribute.getStringValue(mExifByteOrder);
}
- return null;
}
/**
@@ -4205,10 +4297,45 @@
if (tag == null) {
throw new NullPointerException("tag shouldn't be null");
}
- // Validate and convert if necessary.
- if (TAG_DATETIME.equals(tag) || TAG_DATETIME_ORIGINAL.equals(tag)
- || TAG_DATETIME_DIGITIZED.equals(tag)) {
- if (value != null) {
+
+ // Maintain compatibility.
+ if (TAG_ISO_SPEED_RATINGS.equals(tag)) {
+ if (DEBUG) {
+ Log.d(TAG, "setAttribute: Replacing TAG_ISO_SPEED_RATINGS with "
+ + "TAG_PHOTOGRAPHIC_SENSITIVITY.");
+ }
+ tag = TAG_PHOTOGRAPHIC_SENSITIVITY;
+ }
+ // Maybe convert the given value for backwards compatibility.
+ if (value != null) {
+ if (RATIONAL_TAGS_HANDLED_AS_DECIMALS_FOR_COMPATIBILITY.contains(tag)
+ && !value.contains("/")) {
+ // Convert floating point values to rational for rational tags that are emitted and
+ // consumed as floating point values for backwards compatibility.
+ try {
+ double doubleValue = Double.parseDouble(value);
+ value = Rational.createFromDouble(doubleValue).toString();
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Invalid value for " + tag + " : " + value);
+ return;
+ }
+ } else if (tag.equals(TAG_GPS_TIMESTAMP)) {
+ Matcher m = GPS_TIMESTAMP_PATTERN.matcher(value);
+ if (!m.find()) {
+ Log.w(TAG, "Invalid value for " + tag + " : " + value);
+ return;
+ }
+ value =
+ Integer.parseInt(m.group(1))
+ + "/1,"
+ + Integer.parseInt(m.group(2))
+ + "/1,"
+ + Integer.parseInt(m.group(3))
+ + "/1";
+ } else if (TAG_DATETIME.equals(tag)
+ || TAG_DATETIME_ORIGINAL.equals(tag)
+ || TAG_DATETIME_DIGITIZED.equals(tag)) {
+ // Validate and convert datetime values if necessary.
boolean isPrimaryFormat = DATETIME_PRIMARY_FORMAT_PATTERN.matcher(value).find();
boolean isSecondaryFormat = DATETIME_SECONDARY_FORMAT_PATTERN.matcher(value).find();
// Validate
@@ -4218,8 +4345,8 @@
return;
}
// If datetime value has secondary format (e.g. 2020-01-01 00:00:00), convert it to
- // primary format (e.g. 2020:01:01 00:00:00) since it is the format in the
- // official documentation.
+ // primary format (e.g. 2020:01:01 00:00:00) since it is the format in the official
+ // documentation.
// See JEITA CP-3451C Section 4.6.4. D. Other Tags, DateTime
if (isSecondaryFormat) {
// Replace "-" with ":" to match the primary format.
@@ -4227,34 +4354,6 @@
}
}
}
- // Maintain compatibility.
- if (TAG_ISO_SPEED_RATINGS.equals(tag)) {
- if (DEBUG) {
- Log.d(TAG, "setAttribute: Replacing TAG_ISO_SPEED_RATINGS with "
- + "TAG_PHOTOGRAPHIC_SENSITIVITY.");
- }
- tag = TAG_PHOTOGRAPHIC_SENSITIVITY;
- }
- // Convert the given value to rational values for backwards compatibility.
- if (value != null && sTagSetForCompatibility.contains(tag)) {
- if (tag.equals(TAG_GPS_TIMESTAMP)) {
- Matcher m = GPS_TIMESTAMP_PATTERN.matcher(value);
- if (!m.find()) {
- Log.w(TAG, "Invalid value for " + tag + " : " + value);
- return;
- }
- value = Integer.parseInt(m.group(1)) + "/1," + Integer.parseInt(m.group(2)) + "/1,"
- + Integer.parseInt(m.group(3)) + "/1";
- } else {
- try {
- double doubleValue = Double.parseDouble(value);
- value = new Rational(doubleValue).toString();
- } catch (NumberFormatException e) {
- Log.w(TAG, "Invalid value for " + tag + " : " + value);
- return;
- }
- }
- }
for (int i = 0 ; i < EXIF_TAGS.length; ++i) {
if (i == IFD_TYPE_THUMBNAIL && !mHasThumbnail) {
@@ -5058,8 +5157,10 @@
setAltitude(location.getAltitude());
// Location objects store speeds in m/sec. Translates it to km/hr here.
setAttribute(TAG_GPS_SPEED_REF, "K");
- setAttribute(TAG_GPS_SPEED, new Rational(location.getSpeed()
- * TimeUnit.HOURS.toSeconds(1) / 1000).toString());
+ setAttribute(
+ TAG_GPS_SPEED,
+ Rational.createFromDouble(location.getSpeed() * TimeUnit.HOURS.toSeconds(1) / 1000)
+ .toString());
String[] dateTime = sFormatterPrimary.format(
new Date(location.getTime())).split("\\s+", -1);
setAttribute(ExifInterface.TAG_GPS_DATESTAMP, dateTime[0]);
@@ -5111,7 +5212,7 @@
*/
public void setAltitude(double altitude) {
String ref = altitude >= 0 ? "0" : "1";
- setAttribute(TAG_GPS_ALTITUDE, new Rational(Math.abs(altitude)).toString());
+ setAttribute(TAG_GPS_ALTITUDE, Rational.createFromDouble(Math.abs(altitude)).toString());
setAttribute(TAG_GPS_ALTITUDE_REF, ref);
}
@@ -5312,7 +5413,7 @@
}
} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
// Not valid
- throw new IllegalArgumentException();
+ throw new IllegalArgumentException(e);
}
}
@@ -5953,7 +6054,7 @@
}
} catch (RuntimeException e) {
throw new UnsupportedOperationException("Failed to read EXIF from HEIF file. "
- + "Given stream is either malformed or unsupported.");
+ + "Given stream is either malformed or unsupported.", e);
} finally {
try {
retriever.release();
@@ -6115,11 +6216,10 @@
// 2.1. Integers and byte order
in.setByteOrder(BIG_ENDIAN);
- int bytesRead = 0;
+ int startPosition = in.position();
// Skip the signature bytes
in.skipFully(PNG_SIGNATURE.length);
- bytesRead += PNG_SIGNATURE.length;
// Each chunk is made up of four parts:
// 1) Length: 4-byte unsigned integer indicating the number of bytes in the
@@ -6134,22 +6234,23 @@
try {
while (true) {
int length = in.readInt();
- bytesRead += 4;
- byte[] type = new byte[PNG_CHUNK_TYPE_BYTE_LENGTH];
- in.readFully(type);
- bytesRead += PNG_CHUNK_TYPE_BYTE_LENGTH;
+ int type = in.readInt();
// The first chunk must be the IHDR chunk
- if (bytesRead == 16 && !Arrays.equals(type, PNG_CHUNK_TYPE_IHDR)) {
- throw new IOException("Encountered invalid PNG file--IHDR chunk should appear"
- + "as the first chunk");
+ if (in.position() - startPosition == 16 && type != PNG_CHUNK_TYPE_IHDR) {
+ throw new IOException(
+ "Encountered invalid PNG file--IHDR chunk should appear as the first "
+ + "chunk");
}
- if (Arrays.equals(type, PNG_CHUNK_TYPE_IEND)) {
+ if (type == PNG_CHUNK_TYPE_IEND) {
// IEND marks the end of the image.
break;
- } else if (Arrays.equals(type, PNG_CHUNK_TYPE_EXIF)) {
+ } else if (type == PNG_CHUNK_TYPE_EXIF) {
+ // Save offset to EXIF data for handling thumbnail and attribute offsets.
+ mOffsetToExifData = in.position() - startPosition;
+
// TODO: Need to handle potential OutOfMemoryError
byte[] data = new byte[length];
in.readFully(data);
@@ -6158,15 +6259,13 @@
int dataCrcValue = in.readInt();
// Cyclic Redundancy Code used to check for corruption of the data
CRC32 crc = new CRC32();
- crc.update(type);
+ updateCrcWithInt(crc, type);
crc.update(data);
if ((int) crc.getValue() != dataCrcValue) {
throw new IOException("Encountered invalid CRC value for PNG-EXIF chunk."
+ "\n recorded CRC value: " + dataCrcValue + ", calculated CRC "
+ "value: " + crc.getValue());
}
- // Save offset to EXIF data for handling thumbnail and attribute offsets.
- mOffsetToExifData = bytesRead;
readExifSegment(data, IFD_TYPE_PRIMARY);
validateImages();
@@ -6175,16 +6274,22 @@
} else {
// Skip to next chunk
in.skipFully(length + PNG_CHUNK_CRC_BYTE_LENGTH);
- bytesRead += length + PNG_CHUNK_CRC_BYTE_LENGTH;
}
}
} catch (EOFException e) {
// Should not reach here. Will only reach here if the file is corrupted or
// does not follow the PNG specifications
- throw new IOException("Encountered corrupt PNG file.");
+ throw new IOException("Encountered corrupt PNG file.", e);
}
}
+ private static void updateCrcWithInt(CRC32 crc, int value) {
+ crc.update(value >>> 24);
+ crc.update(value >>> 16);
+ crc.update(value >>> 8);
+ crc.update(value);
+ }
+
// WebP contains EXIF data as a RIFF File Format Chunk
// All references below can be found in the following link.
// https://developers.google.com/speed/webp/docs/riff_container
@@ -6264,7 +6369,7 @@
} catch (EOFException e) {
// Should not reach here. Will only reach here if the file is corrupted or
// does not follow the WebP specifications
- throw new IOException("Encountered corrupt WebP file.");
+ throw new IOException("Encountered corrupt WebP file.", e);
}
}
@@ -7535,7 +7640,7 @@
case IMAGE_TYPE_PNG:
// Write PNG specific data (chunk size, chunk type)
dataOutputStream.writeInt(totalSize);
- dataOutputStream.write(PNG_CHUNK_TYPE_EXIF);
+ dataOutputStream.writeInt(PNG_CHUNK_TYPE_EXIF);
break;
case IMAGE_TYPE_WEBP:
// Write WebP specific data (chunk type, chunk size)
@@ -8149,4 +8254,12 @@
}
return false;
}
+
+ /*
+ * Combines the lower eight bits of each parameter into a 32-bit int. {@code b1} is the highest
+ * byte of the result, {@code b4} is the lowest.
+ */
+ private static int intFromBytes(int b1, int b2, int b3, int b4) {
+ return ((b1 & 0xFF) << 24) | ((b2 & 0xFF) << 16) | ((b3 & 0xFF) << 8) | (b4 & 0xFF);
+ }
}
diff --git a/libraryversions.toml b/libraryversions.toml
index 0616576..9d9a809 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -24,9 +24,9 @@
COLLECTION = "1.5.0-alpha01"
COMPOSE = "1.7.0-alpha08"
COMPOSE_COMPILER = "1.5.12" # Update when preparing for a release
-COMPOSE_MATERIAL3 = "1.3.0-alpha05"
-COMPOSE_MATERIAL3_ADAPTIVE = "1.0.0-alpha11"
-COMPOSE_MATERIAL3_ADAPTIVE_NAVIGATION_SUITE = "1.0.0-alpha06"
+COMPOSE_MATERIAL3 = "1.3.0-alpha06"
+COMPOSE_MATERIAL3_ADAPTIVE = "1.0.0-alpha12"
+COMPOSE_MATERIAL3_ADAPTIVE_NAVIGATION_SUITE = "1.0.0-alpha07"
COMPOSE_MATERIAL3_COMMON = "1.0.0-alpha01"
COMPOSE_RUNTIME_TRACING = "1.0.0-beta01"
CONSTRAINTLAYOUT = "2.2.0-alpha13"
@@ -157,8 +157,8 @@
VIEWPAGER = "1.1.0-alpha02"
VIEWPAGER2 = "1.1.0-rc01"
WEAR = "1.4.0-alpha01"
-WEAR_COMPOSE = "1.4.0-alpha07"
-WEAR_COMPOSE_MATERIAL3 = "1.0.0-alpha21"
+WEAR_COMPOSE = "1.4.0-alpha08"
+WEAR_COMPOSE_MATERIAL3 = "1.0.0-alpha22"
WEAR_CORE = "1.0.0-alpha01"
WEAR_INPUT = "1.2.0-alpha03"
WEAR_INPUT_TESTING = "1.2.0-alpha03"
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
index 68bdfd0..bf75d84 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
@@ -1065,8 +1065,10 @@
route.getProviderInstance()
.onCreateRouteController(
route.mDescriptorId, mSelectedRoute.mDescriptorId);
- controller.onSelect();
- mRouteControllerMap.put(route.mUniqueId, controller);
+ if (controller != null) {
+ controller.onSelect();
+ mRouteControllerMap.put(route.mUniqueId, controller);
+ }
}
}
}
diff --git a/navigation/navigation-common/api/current.txt b/navigation/navigation-common/api/current.txt
index 099e27d..d8aade0e 100644
--- a/navigation/navigation-common/api/current.txt
+++ b/navigation/navigation-common/api/current.txt
@@ -227,6 +227,7 @@
method public final String? getRoute();
method public boolean hasDeepLink(android.net.Uri deepLink);
method public boolean hasDeepLink(androidx.navigation.NavDeepLinkRequest deepLinkRequest);
+ method public static final <T> boolean hasRoute(androidx.navigation.NavDestination, kotlin.reflect.KClass<T> route);
method @CallSuper public void onInflate(android.content.Context context, android.util.AttributeSet attrs);
method protected static final <C> Class<? extends C?> parseClassFromName(android.content.Context context, String name, Class<? extends C?> expectedClassType);
method public final void putAction(@IdRes int actionId, androidx.navigation.NavAction action);
@@ -253,6 +254,7 @@
public static final class NavDestination.Companion {
method public kotlin.sequences.Sequence<androidx.navigation.NavDestination> getHierarchy(androidx.navigation.NavDestination);
method public inline <reified T> boolean hasRoute(androidx.navigation.NavDestination);
+ method public <T> boolean hasRoute(androidx.navigation.NavDestination, kotlin.reflect.KClass<T> route);
method protected <C> Class<? extends C?> parseClassFromName(android.content.Context context, String name, Class<? extends C?> expectedClassType);
}
diff --git a/navigation/navigation-common/api/restricted_current.txt b/navigation/navigation-common/api/restricted_current.txt
index 099e27d..d8aade0e 100644
--- a/navigation/navigation-common/api/restricted_current.txt
+++ b/navigation/navigation-common/api/restricted_current.txt
@@ -227,6 +227,7 @@
method public final String? getRoute();
method public boolean hasDeepLink(android.net.Uri deepLink);
method public boolean hasDeepLink(androidx.navigation.NavDeepLinkRequest deepLinkRequest);
+ method public static final <T> boolean hasRoute(androidx.navigation.NavDestination, kotlin.reflect.KClass<T> route);
method @CallSuper public void onInflate(android.content.Context context, android.util.AttributeSet attrs);
method protected static final <C> Class<? extends C?> parseClassFromName(android.content.Context context, String name, Class<? extends C?> expectedClassType);
method public final void putAction(@IdRes int actionId, androidx.navigation.NavAction action);
@@ -253,6 +254,7 @@
public static final class NavDestination.Companion {
method public kotlin.sequences.Sequence<androidx.navigation.NavDestination> getHierarchy(androidx.navigation.NavDestination);
method public inline <reified T> boolean hasRoute(androidx.navigation.NavDestination);
+ method public <T> boolean hasRoute(androidx.navigation.NavDestination, kotlin.reflect.KClass<T> route);
method protected <C> Class<? extends C?> parseClassFromName(android.content.Context context, String name, Class<? extends C?> expectedClassType);
}
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
index f1515cd..468e617 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
@@ -31,6 +31,7 @@
import androidx.navigation.common.R
import java.util.regex.Pattern
import kotlin.reflect.KClass
+import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.serializer
/**
@@ -828,7 +829,18 @@
* @param T the route from KClass
*/
@JvmStatic
- public inline fun <reified T : Any> NavDestination.hasRoute() =
- serializer<T>().hashCode() == id
+ public inline fun <reified T : Any> NavDestination.hasRoute() = hasRoute(T::class)
+
+ /**
+ * Checks if the NavDestination's route was generated from [T]
+ *
+ * Returns true if equal, false otherwise.
+ *
+ * @param route the route from KClass
+ */
+ @OptIn(InternalSerializationApi::class)
+ @JvmStatic
+ public fun <T : Any> NavDestination.hasRoute(route: KClass<T>) =
+ route.serializer().hashCode() == id
}
}
diff --git a/navigation/navigation-compose/integration-tests/navigation-demos/build.gradle b/navigation/navigation-compose/integration-tests/navigation-demos/build.gradle
index 140b0aa..03a6aca 100644
--- a/navigation/navigation-compose/integration-tests/navigation-demos/build.gradle
+++ b/navigation/navigation-compose/integration-tests/navigation-demos/build.gradle
@@ -28,10 +28,12 @@
id("com.android.library")
id("AndroidXComposePlugin")
id("org.jetbrains.kotlin.android")
+ alias(libs.plugins.kotlinSerialization)
}
dependencies {
implementation(libs.kotlinStdlib)
+ implementation(libs.kotlinSerializationCore)
implementation(project(":compose:integration-tests:demos:common"))
implementation(project(":compose:foundation:foundation"))
diff --git a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/BottomBarNavDemo.kt b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/BottomBarNavDemo.kt
index cc5f036..7c130b8 100644
--- a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/BottomBarNavDemo.kt
+++ b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/BottomBarNavDemo.kt
@@ -27,15 +27,18 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
+import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
+import androidx.navigation.compose.dialog
import androidx.navigation.compose.rememberNavController
import androidx.navigation.compose.samples.Dashboard
+import androidx.navigation.compose.samples.Destination
+import androidx.navigation.compose.samples.DialogContent
import androidx.navigation.compose.samples.Profile
-import androidx.navigation.compose.samples.Screen
import androidx.navigation.compose.samples.Scrollable
@Composable
@@ -43,9 +46,9 @@
val navController = rememberNavController()
val items = listOf(
- stringResource(R.string.profile) to Screen.Profile.route,
- stringResource(R.string.dashboard) to Screen.Dashboard.route,
- stringResource(R.string.scrollable) to Screen.Scrollable.route
+ stringResource(R.string.profile) to Destination.Profile,
+ stringResource(R.string.dashboard) to Destination.Dashboard(),
+ stringResource(R.string.scrollable) to Destination.Scrollable
)
Scaffold(
@@ -53,13 +56,15 @@
BottomNavigation {
val navBackStackEntry = navController.currentBackStackEntryAsState().value
val currentDestination = navBackStackEntry?.destination
- items.forEach { (name, route) ->
+ items.forEach { (name, destination) ->
BottomNavigationItem(
icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
label = { Text(name) },
- selected = currentDestination?.hierarchy?.any { it.route == route } == true,
+ selected = currentDestination?.hierarchy?.any {
+ it.hasRoute(destination.route)
+ } == true,
onClick = {
- navController.navigate(route) {
+ navController.navigate(destination) {
launchSingleTop = true
restoreState = true
popUpTo(navController.graph.findStartDestination().id) {
@@ -72,10 +77,11 @@
}
}
) { innerPadding ->
- NavHost(navController, Screen.Profile.route, Modifier.padding(innerPadding)) {
- composable(Screen.Profile.route) { Profile(navController) }
- composable(Screen.Dashboard.route) { Dashboard(navController) }
- composable(Screen.Scrollable.route) { Scrollable(navController) }
+ NavHost(navController, Destination.Profile.route, Modifier.padding(innerPadding)) {
+ composable<Destination.Profile> { Profile(navController) }
+ composable<Destination.Dashboard> { Dashboard(navController) }
+ composable<Destination.Scrollable> { Scrollable(navController) }
+ dialog<Destination.Dialog> { DialogContent(navController) }
}
}
}
diff --git a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavByDeepLinkDemo.kt b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavByDeepLinkDemo.kt
index 9951b1d..8eccc0d2 100644
--- a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavByDeepLinkDemo.kt
+++ b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavByDeepLinkDemo.kt
@@ -39,23 +39,27 @@
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.compose.samples.Dashboard
-import androidx.navigation.compose.samples.Screen
-import androidx.navigation.navArgument
+import androidx.navigation.compose.samples.Destination
import androidx.navigation.navDeepLink
+import androidx.navigation.toRoute
@Composable
-@Suppress("DEPRECATION")
fun NavByDeepLinkDemo() {
val navController = rememberNavController()
- val uri = "https://example.com/dashboard?userId="
- NavHost(navController, startDestination = Screen.Profile.route) {
- composable(Screen.Profile.route) { ProfileWithDeepLink(navController, uri) }
- composable(
- Screen.Dashboard.route,
- arguments = listOf(navArgument("userId") { defaultValue = "no value given" }),
- deepLinks = listOf(navDeepLink { uriPattern = "$uri{userId}" })
+ val basePath = "https://example.com"
+ NavHost(navController, startDestination = Destination.Profile.route) {
+ composable<Destination.Profile> {
+ ProfileWithDeepLink(
+ navController,
+ "$basePath?userId="
+ )
+ }
+ composable<Destination.Dashboard>(
+ // use the same args from Destination.Dashboard with custom uri base path
+ deepLinks = listOf(navDeepLink<Destination.Dashboard>(basePath))
) { backStackEntry ->
- Dashboard(navController, backStackEntry.arguments?.get("userId") as? String)
+ val dashboard = backStackEntry.toRoute<Destination.Dashboard>()
+ Dashboard(navController, dashboard.userId)
}
}
}
@@ -63,7 +67,7 @@
@Composable
fun ProfileWithDeepLink(navController: NavController, uri: String) {
Column(Modifier.fillMaxSize().then(Modifier.padding(8.dp))) {
- Text(text = stringResource(Screen.Profile.resourceId))
+ Text(text = stringResource(Destination.Profile.resourceId))
Divider(color = Color.Black)
val state = rememberSaveable { mutableStateOf("") }
Box {
@@ -75,6 +79,7 @@
}
Divider(color = Color.Black)
Button(
+ // navigate with deeplink
onClick = { navController.navigate(Uri.parse(uri + state.value)) },
colors = ButtonDefaults.buttonColors(backgroundColor = Color.LightGray),
modifier = Modifier.fillMaxWidth()
diff --git a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavPopUpToDemo.kt b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavPopUpToDemo.kt
index a39e8ab..eed030c 100644
--- a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavPopUpToDemo.kt
+++ b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavPopUpToDemo.kt
@@ -31,16 +31,19 @@
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
+import androidx.navigation.toRoute
+import kotlinx.serialization.Serializable
+
+@Serializable
+class NumberedDestination(val number: Int)
@Composable
fun NavPopUpToDemo() {
val navController = rememberNavController()
- NavHost(navController, startDestination = "1") {
- composable("1") { NumberedScreen(navController, 1) }
- composable("2") { NumberedScreen(navController, 2) }
- composable("3") { NumberedScreen(navController, 3) }
- composable("4") { NumberedScreen(navController, 4) }
- composable("5") { NumberedScreen(navController, 5) }
+ NavHost(navController, startDestination = NumberedDestination(1)) {
+ composable<NumberedDestination> {
+ NumberedScreen(navController, it.toRoute<NumberedDestination>().number)
+ }
}
}
@@ -50,7 +53,7 @@
val next = number + 1
if (number < 5) {
Button(
- onClick = { navController.navigate("$next") },
+ onClick = { navController.navigate(NumberedDestination(next)) },
colors = ButtonDefaults.buttonColors(backgroundColor = Color.LightGray),
modifier = Modifier.fillMaxWidth()
) {
@@ -59,8 +62,11 @@
}
Text("This is screen $number", Modifier.weight(1f))
if (navController.previousBackStackEntry != null) {
+ val firstScreen = NumberedDestination(1)
Button(
- onClick = { navController.navigate("1") { popUpTo("1") { inclusive = true } } },
+ onClick = { navController.navigate(firstScreen) {
+ popUpTo(firstScreen) { inclusive = true }
+ } },
colors = ButtonDefaults.buttonColors(backgroundColor = Color.LightGray),
modifier = Modifier.fillMaxWidth()
) {
diff --git a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavSingleTopDemo.kt b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavSingleTopDemo.kt
index 051d40b..6250ce4 100644
--- a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavSingleTopDemo.kt
+++ b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavSingleTopDemo.kt
@@ -32,6 +32,8 @@
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.compose.samples.NavigateButton
+import androidx.navigation.toRoute
+import kotlinx.serialization.Serializable
@Composable
fun NavSingleTopDemo() {
@@ -44,21 +46,28 @@
placeholder = { Text("Search") }
)
NavigateButton("Search") {
- navController.navigate("search/" + query.value) {
+ navController.navigate(SearchScreen(query.value)) {
launchSingleTop = true
}
}
- NavHost(navController, startDestination = "start") {
- composable("start") { StartScreen() }
- composable("search/{query}") { backStackEntry ->
+ NavHost(navController, startDestination = StartScreen::class) {
+ composable<StartScreen> { StartScreen() }
+ composable<SearchScreen> { backStackEntry ->
+ val args = backStackEntry.toRoute<SearchScreen>()
SearchResultScreen(
- backStackEntry.arguments!!.getString("query", "no query entered")
+ args.query
)
}
}
}
}
+@Serializable
+object StartScreen
+
+@Serializable
+data class SearchScreen(val query: String)
+
@Composable
fun StartScreen() {
Divider(color = Color.Black)
diff --git a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavWithArgsDemo.kt b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavWithArgsDemo.kt
index 3b6d9f1..025a5e1 100644
--- a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavWithArgsDemo.kt
+++ b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavWithArgsDemo.kt
@@ -16,56 +16,23 @@
package androidx.navigation.compose.demos
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material.Divider
-import androidx.compose.material.Text
-import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.compose.samples.Dashboard
-import androidx.navigation.compose.samples.NavigateButton
-import androidx.navigation.compose.samples.Screen
+import androidx.navigation.compose.samples.Destination
+import androidx.navigation.compose.samples.ProfileWithArgs
+import androidx.navigation.toRoute
@Composable
-@Suppress("DEPRECATION")
fun NavWithArgsDemo() {
val navController = rememberNavController()
- NavHost(navController, startDestination = Screen.Profile.route) {
- composable(Screen.Profile.route) { ProfileWithArgs(navController) }
- composable(Screen.Dashboard.route + "?userId={userId}") { backStackEntry ->
- Dashboard(navController, backStackEntry.arguments?.get("userId") as? String)
- }
- }
-}
-
-@Composable
-fun ProfileWithArgs(navController: NavController) {
- Column(Modifier.fillMaxSize().then(Modifier.padding(8.dp))) {
- Text(text = stringResource(Screen.Profile.resourceId))
- Divider(color = Color.Black)
- val state = rememberSaveable { mutableStateOf("") }
- Box {
- TextField(
- value = state.value,
- onValueChange = { state.value = it },
- placeholder = { Text("Enter userId here") }
- )
- }
- Divider(color = Color.Black)
- NavigateButton("Dashboard with userId") {
- navController.navigate(Screen.Dashboard.route + "?userId=" + state.value)
+ NavHost(navController, startDestination = Destination.Profile.route) {
+ composable<Destination.Profile> { ProfileWithArgs(navController) }
+ composable<Destination.Dashboard> { backStackEntry ->
+ val dashboard = backStackEntry.toRoute<Destination.Dashboard>()
+ Dashboard(navController, dashboard.userId)
}
}
}
diff --git a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavWithArgsInNestedGraphDemo.kt b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavWithArgsInNestedGraphDemo.kt
new file mode 100644
index 0000000..fe91ed6
--- /dev/null
+++ b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavWithArgsInNestedGraphDemo.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2024 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.navigation.compose.demos
+
+import androidx.compose.runtime.Composable
+import androidx.navigation.compose.samples.NavWithArgsInNestedGraph
+
+@Composable
+fun NavWithArgsInNestedGraphDemo() {
+ NavWithArgsInNestedGraph()
+}
diff --git a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavigationDemos.kt b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavigationDemos.kt
index 113d02d..6e11a87 100644
--- a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavigationDemos.kt
+++ b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavigationDemos.kt
@@ -27,6 +27,7 @@
ComposableDemo("Nested Nav In Graph Demo") { NestNavInGraphDemo() },
ComposableDemo("Bottom Bar Nav Demo") { BottomBarNavDemo() },
ComposableDemo("Navigation with Args") { NavWithArgsDemo() },
+ ComposableDemo("Navigation with Args in Nested Graph") { NavWithArgsInNestedGraphDemo() },
ComposableDemo("Navigation by DeepLink") { NavByDeepLinkDemo() },
ComposableDemo("Navigation PopUpTo") { NavPopUpToDemo() },
ComposableDemo("Navigation SingleTop") { NavSingleTopDemo() },
diff --git a/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/NavigationSamples.kt b/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/NavigationSamples.kt
index d2b50f3e..d2db6e0 100644
--- a/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/NavigationSamples.kt
+++ b/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/NavigationSamples.kt
@@ -19,7 +19,6 @@
import android.os.Bundle
import android.os.Parcelable
import androidx.annotation.Sampled
-import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@@ -36,8 +35,11 @@
import androidx.compose.material.Divider
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
+import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Color.Companion.LightGray
@@ -49,6 +51,7 @@
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.navigation.NavController
+import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
@@ -56,28 +59,53 @@
import androidx.navigation.compose.dialog
import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController
-import androidx.navigation.navArgument
+import androidx.navigation.toRoute
+import kotlin.reflect.KClass
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
-sealed class Screen(val route: String, @StringRes val resourceId: Int) {
- object Profile : Screen("profile", R.string.profile)
- object Dashboard : Screen("dashboard", R.string.dashboard)
- object Scrollable : Screen("scrollable", R.string.scrollable)
- object Dialog : Screen("dialog", R.string.dialog)
+interface Destination {
+ val route: KClass<out Destination>
+
+ @Serializable object Profile : Destination {
+ override val route = this::class
+ val resourceId: Int = R.string.profile
+ }
+ @Serializable object Scrollable : Destination {
+ override val route = this::class
+ val resourceId: Int = R.string.scrollable
+ }
+ @Serializable object Dialog : Destination {
+ override val route = this::class
+ val resourceId: Int = R.string.dialog
+ }
+ @Serializable data class Dashboard(val userId: String? = "no value given") : Destination {
+ override val route: KClass<out Destination>
+ get() = this::class
+ companion object {
+ val resourceId: Int = R.string.dashboard
+ }
+ }
+ @Serializable
+ object Nested : Destination { override val route = this::class }
+
+ @Serializable
+ data class NestedWithArg(val userId: String? = "default nested arg") : Destination {
+ override val route: KClass<out Destination>
+ get() = this::class
+ }
}
@Composable
fun BasicNav() {
val navController = rememberNavController()
- NavHost(navController, startDestination = Screen.Profile.route) {
- composable(Screen.Profile.route) { Profile(navController) }
- composable(
- Screen.Dashboard.route,
+ NavHost(navController, startDestination = Destination.Profile.route) {
+ composable<Destination.Profile> { Profile(navController) }
+ composable<Destination.Dashboard>(
enterTransition = {
- if (initialState.destination.route == Screen.Scrollable.route) {
+ if (initialState.destination.hasRoute<Destination.Scrollable>()) {
// Slide in when entering from Scrollable
slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Start)
} else {
@@ -85,7 +113,7 @@
}
},
popExitTransition = {
- if (targetState.destination.route == Screen.Scrollable.route) {
+ if (targetState.destination.hasRoute<Destination.Scrollable>()) {
// Slide out when popping back to Scrollable
slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End)
} else {
@@ -95,10 +123,9 @@
) {
Dashboard(navController)
}
- composable(
- Screen.Scrollable.route,
+ composable<Destination.Scrollable>(
exitTransition = {
- if (targetState.destination.route == Screen.Dashboard.route) {
+ if (targetState.destination.hasRoute<Destination.Dashboard>()) {
// Slide out when navigating to Dashboard
slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Start)
} else {
@@ -106,7 +133,7 @@
}
},
popEnterTransition = {
- if (initialState.destination.route == Screen.Dashboard.route) {
+ if (initialState.destination.hasRoute<Destination.Dashboard>()) {
// Slide back in when returning from Dashboard
slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.End)
} else {
@@ -116,33 +143,33 @@
) {
Scrollable(navController)
}
- dialog(Screen.Dialog.route) { DialogContent(navController) }
+ dialog<Destination.Dialog> { DialogContent(navController) }
}
}
@Composable
fun NestedNavStartDestination() {
val navController = rememberNavController()
- NavHost(navController, startDestination = "nested") {
- navigation(startDestination = Screen.Profile.route, route = "nested") {
- composable(Screen.Profile.route) { Profile(navController) }
+ NavHost(navController, startDestination = Destination.Nested.route) {
+ navigation<Destination.Nested>(startDestination = Destination.Profile.route) {
+ composable<Destination.Profile> { Profile(navController) }
}
- composable(Screen.Dashboard.route) { Dashboard(navController) }
- composable(Screen.Scrollable.route) { Scrollable(navController) }
- dialog(Screen.Dialog.route) { DialogContent(navController) }
+ composable<Destination.Dashboard> { Dashboard(navController) }
+ composable<Destination.Scrollable> { Scrollable(navController) }
+ dialog<Destination.Dialog> { DialogContent(navController) }
}
}
@Composable
fun NestedNavInGraph() {
val navController = rememberNavController()
- NavHost(navController, startDestination = Screen.Profile.route) {
- composable(Screen.Profile.route) { Profile(navController) }
- navigation(startDestination = "nested", route = Screen.Dashboard.route) {
- composable("nested") { Dashboard(navController) }
+ NavHost(navController, startDestination = Destination.Profile.route) {
+ composable<Destination.Profile> { Profile(navController) }
+ navigation<Destination.Dashboard>(startDestination = Destination.Nested.route) {
+ composable<Destination.Nested> { Dashboard(navController) }
}
- composable(Screen.Scrollable.route) { Scrollable(navController) }
- dialog(Screen.Dialog.route) { DialogContent(navController) }
+ composable<Destination.Scrollable> { Scrollable(navController) }
+ dialog<Destination.Dialog> { DialogContent(navController) }
}
}
@@ -151,49 +178,27 @@
fun NavScaffold() {
val navController = rememberNavController()
Scaffold { innerPadding ->
- NavHost(navController, Screen.Profile.route, Modifier.padding(innerPadding)) {
- composable(Screen.Profile.route) { Profile(navController) }
- composable(Screen.Dashboard.route) { Dashboard(navController) }
- composable(Screen.Scrollable.route) { Scrollable(navController) }
- dialog(Screen.Dialog.route) { DialogContent(navController) }
- }
- }
-}
-
-@Composable
-fun NavWithArgs() {
- val navController = rememberNavController()
- NavHost(navController, startDestination = Screen.Profile.route) {
- composable(Screen.Profile.route) { Profile(navController) }
- composable(
- Screen.Dashboard.route,
- arguments = listOf(navArgument("userId") { defaultValue = "no value given" })
- ) { backStackEntry ->
- Dashboard(navController, backStackEntry.arguments?.getString("userId"))
+ NavHost(navController, Destination.Profile.route, Modifier.padding(innerPadding)) {
+ composable<Destination.Profile> { Profile(navController) }
+ composable<Destination.Dashboard> { Dashboard(navController) }
+ composable<Destination.Scrollable> { Scrollable(navController) }
+ dialog<Destination.Dialog> { DialogContent(navController) }
}
}
}
@Sampled
@Composable
-fun NestedNavInGraphWithArgs() {
+fun NavWithArgsInNestedGraph() {
val navController = rememberNavController()
- NavHost(navController, startDestination = Screen.Profile.route) {
- composable(Screen.Profile.route) { Profile(navController) }
- navigation(
- startDestination = "nested",
- route = Screen.Dashboard.route,
- // This value will be sent to the start destination of the graph when you navigate to
- // this graph
- arguments = listOf(navArgument("userId") { defaultValue = "no value given" })
- ) {
- composable(
- "nested",
- // We don't need to set a default value here because the start destination will
- // automatically receive the arguments of its parent graph
- arguments = listOf(navArgument("userId") { })
- ) {
- Dashboard(navController)
+ NavHost(navController, startDestination = Destination.Profile.route) {
+ composable<Destination.Profile> { ProfileWithArgs(navController) }
+ navigation<Destination.Dashboard>(startDestination = Destination.NestedWithArg::class) {
+ composable<Destination.NestedWithArg> {
+ // argument from parent graph Destination.Dashboard will automatically be
+ // bundled into the start destination's arguments
+ val userId = it.toRoute<Destination.NestedWithArg>().userId
+ Dashboard(navController, userId)
}
}
}
@@ -202,17 +207,17 @@
@Composable
fun Profile(navController: NavHostController) {
Column(Modifier.fillMaxSize().then(Modifier.padding(8.dp))) {
- Text(text = stringResource(Screen.Profile.resourceId))
- NavigateButton(stringResource(Screen.Dashboard.resourceId)) {
- navController.navigate(Screen.Dashboard.route)
+ Text(text = stringResource(Destination.Profile.resourceId))
+ NavigateButton(stringResource(Destination.Dashboard.resourceId)) {
+ navController.navigate(Destination.Dashboard())
}
Divider(color = Color.Black)
- NavigateButton(stringResource(Screen.Scrollable.resourceId)) {
- navController.navigate(Screen.Scrollable.route)
+ NavigateButton(stringResource(Destination.Scrollable.resourceId)) {
+ navController.navigate(Destination.Scrollable)
}
Divider(color = Color.Black)
- NavigateButton(stringResource(Screen.Dialog.resourceId)) {
- navController.navigate(Screen.Dialog.route)
+ NavigateButton(stringResource(Destination.Dialog.resourceId)) {
+ navController.navigate(Destination.Dialog)
}
Spacer(Modifier.weight(1f))
NavigateBackButton(navController)
@@ -220,9 +225,29 @@
}
@Composable
+fun ProfileWithArgs(navController: NavController) {
+ Column(Modifier.fillMaxSize().then(Modifier.padding(8.dp))) {
+ Text(text = stringResource(Destination.Profile.resourceId))
+ Divider(color = Color.Black)
+ val state = rememberSaveable { mutableStateOf("") }
+ Box {
+ TextField(
+ value = state.value,
+ onValueChange = { state.value = it },
+ placeholder = { Text("Enter userId here") }
+ )
+ }
+ Divider(color = Color.Black)
+ NavigateButton("Dashboard with userId") {
+ navController.navigate(Destination.Dashboard(state.value))
+ }
+ }
+}
+
+@Composable
fun Dashboard(navController: NavController, title: String? = null) {
Column(Modifier.fillMaxSize().then(Modifier.padding(8.dp))) {
- Text(text = title ?: stringResource(Screen.Dashboard.resourceId))
+ Text(text = title ?: stringResource(Destination.Dashboard.resourceId))
Spacer(Modifier.weight(1f))
NavigateBackButton(navController)
}
@@ -231,8 +256,8 @@
@Composable
fun Scrollable(navController: NavController) {
Column(Modifier.fillMaxSize().then(Modifier.padding(8.dp))) {
- NavigateButton(stringResource(Screen.Dashboard.resourceId)) {
- navController.navigate(Screen.Dashboard.route)
+ NavigateButton(stringResource(Destination.Dashboard.resourceId)) {
+ navController.navigate(Destination.Dashboard())
}
LazyColumn(modifier = Modifier.weight(1f)) {
items(phrases) { phrase ->
diff --git a/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SizeTransformSample.kt b/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SizeTransformSample.kt
index ce55fdb..a8779ce 100644
--- a/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SizeTransformSample.kt
+++ b/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SizeTransformSample.kt
@@ -33,14 +33,15 @@
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
+import kotlinx.serialization.Serializable
@Sampled
@Composable
fun SizeTransformNav() {
val navController = rememberNavController()
Box {
- NavHost(navController, startDestination = "collapsed") {
- composable("collapsed",
+ NavHost(navController, startDestination = Collapsed) {
+ composable<Collapsed>(
enterTransition = { EnterTransition.None },
exitTransition = { ExitTransition.None },
sizeTransform = {
@@ -52,9 +53,9 @@
}
}
}) {
- CollapsedScreen { navController.navigate("expanded") }
+ CollapsedScreen { navController.navigate(Expanded) }
}
- composable("expanded",
+ composable<Expanded>(
enterTransition = { EnterTransition.None },
exitTransition = { ExitTransition.None },
sizeTransform = {
@@ -71,6 +72,12 @@
}
}
+@Serializable
+object Collapsed
+
+@Serializable
+object Expanded
+
@Composable
fun CollapsedScreen(onNavigate: () -> Unit) {
Box(Modifier.clickable { onNavigate() }.size(40.dp).background(Green))
diff --git a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavGraphBuilder.kt b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavGraphBuilder.kt
index 6c75048..fb4e5d5 100644
--- a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavGraphBuilder.kt
+++ b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavGraphBuilder.kt
@@ -223,7 +223,7 @@
/**
* Construct a nested [NavGraph]
*
- * @sample androidx.navigation.compose.samples.NestedNavInGraphWithArgs
+ * @sample androidx.navigation.compose.samples.NavWithArgsInNestedGraph
*
* @param startDestination the starting destination's route for this NavGraph
* @param route the destination's unique route
diff --git a/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt b/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt
index 2c07c85..11bf649 100644
--- a/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt
+++ b/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt
@@ -152,7 +152,7 @@
userSwipeEnabled: Boolean = true,
state: SwipeDismissableNavHostState = rememberSwipeDismissableNavHostState(),
) {
- val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
+ val lifecycleOwner = LocalLifecycleOwner.current
val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"SwipeDismissableNavHost requires a ViewModelStoreOwner to be provided " +
"via LocalViewModelStoreOwner"
@@ -272,11 +272,11 @@
} else {
Modifier.graphicsLayer {
val scaleProgression = NAV_HOST_ENTER_TRANSITION_EASING_STANDARD
- .transform((animationProgress.value / 0.75f).coerceAtMost(1f))
+ .transform((animationProgress.value / 0.75f))
val opacityProgression = NAV_HOST_ENTER_TRANSITION_EASING_STANDARD
- .transform((animationProgress.value / 0.25f).coerceAtMost(1f))
- val scale = lerp(0.75f, 1f, scaleProgression)
- val opacity = lerp(0.1f, 1f, opacityProgression)
+ .transform((animationProgress.value / 0.25f))
+ val scale = lerp(0.75f, 1f, scaleProgression).coerceAtMost(1f)
+ val opacity = lerp(0.1f, 1f, opacityProgression).coerceIn(0f, 1f)
scaleX = scale
scaleY = scale
alpha = opacity
@@ -491,10 +491,10 @@
if (layerColor != Color.Unspecified) {
Canvas(Modifier.fillMaxSize()) {
val absoluteProgression =
- ((animatable.value - 0.25f).coerceAtLeast(0f) / 0.75f).coerceAtMost(1f)
+ ((animatable.value - 0.25f) / 0.75f).coerceIn(0f, 1f)
val easedProgression = NAV_HOST_ENTER_TRANSITION_EASING_STANDARD
.transform(absoluteProgression)
- val alpha = lerp(0.07f, 0f, easedProgression)
+ val alpha = lerp(0.07f, 0f, easedProgression).coerceIn(0f, 1f)
if (isRoundDevice) {
drawCircle(color = layerColor.copy(alpha))
} else {
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index 9774ec0..28d0392 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -25,8 +25,8 @@
defaultConfig {
applicationId "androidx.wear.compose.integration.demos"
minSdk 25
- versionCode 22
- versionName "1.22"
+ versionCode 23
+ versionName "1.23"
}
buildTypes {
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/ProtoLayoutExtensionViewProvider.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/ProtoLayoutExtensionViewProvider.java
index ca4f923..42df804 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/ProtoLayoutExtensionViewProvider.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/ProtoLayoutExtensionViewProvider.java
@@ -1,5 +1,3 @@
-package androidx.wear.protolayout.renderer;
-
/*
* Copyright 2023 The Android Open Source Project
*
@@ -15,12 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package androidx.wear.protolayout.renderer;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
/**
* View provider for a View ExtensionLayoutElement. This should check that the given renderer
@@ -28,7 +28,7 @@
* payload. The returned View will be measured using the width/height from the {@link
* androidx.wear.protolayout.LayoutElementBuilders.ExtensionLayoutElement} message.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY)
+@RestrictTo(Scope.LIBRARY)
public interface ProtoLayoutExtensionViewProvider {
/**
* Return an Android View from the given renderer extension. In case of an error, this method
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutThemeImpl.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutThemeImpl.java
index 819b270..b540781 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutThemeImpl.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutThemeImpl.java
@@ -106,15 +106,6 @@
return new ProtoLayoutThemeImpl(context.getResources(), R.style.ProtoLayoutBaseTheme);
}
- /**
- * Creates a ProtoLayoutTheme for the default theme, based on R.style.ProtoLayoutBaseTheme and
- * R.attr.protoLayoutFallbackAppearance from the local package.
- */
- @NonNull
- public static ProtoLayoutTheme defaultTheme(@NonNull Resources resources) {
- return new ProtoLayoutThemeImpl(resources, R.style.ProtoLayoutBaseTheme);
- }
-
private final Map<Integer, FontSet> mVariantToFontSet = new ArrayMap<>();
private final Theme mTheme;
@AttrRes private final int mFallbackTextAppearanceAttrId;
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/TouchDelegateComposite.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/TouchDelegateComposite.java
index b5d91a6..e6bf97c 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/TouchDelegateComposite.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/TouchDelegateComposite.java
@@ -50,14 +50,13 @@
*/
class TouchDelegateComposite extends TouchDelegate {
- @NonNull
- private final WeakHashMap<View, DelegateInfo> mDelegates = new WeakHashMap<>();
+ @NonNull private final WeakHashMap<View, DelegateInfo> mDelegates = new WeakHashMap<>();
/**
* Constructor
*
- * @param delegateView The view that should receive motion events.
- * @param actualBounds The hit rect of the view.
+ * @param delegateView The view that should receive motion events.
+ * @param actualBounds The hit rect of the view.
* @param extendedBounds The hit rect to be delegated.
*/
TouchDelegateComposite(
@@ -110,13 +109,11 @@
}
return checkNotNull(mDelegates.get(view)).mTouchDelegate.onTouchEvent(event);
} else {
- // For other motion event, forward to ALL the delegate view whose extended bounds
- // with touch
- // slop contains the touch point.
+ // For other motion event, forward to ALL the delegate view whose extended bounds with
+ // touch slop contains the touch point.
for (DelegateInfo delegateInfo : mDelegates.values()) {
- // set the event location back to the original coordinates, which might get
- // offset by the
- // previous TouchDelegate#onTouchEvent call
+ // set the event location back to the original coordinates, which might get offset
+ // by the previous TouchDelegate#onTouchEvent call.
event.setLocation(x, y);
eventForwarded |= delegateInfo.mTouchDelegate.onTouchEvent(event);
}
@@ -151,15 +148,13 @@
}
private static final class DelegateInfo {
- @NonNull
- final Rect mActualBounds;
- @NonNull
- final Rect mExtendedBounds;
- @NonNull
- final TouchDelegate mTouchDelegate;
+ @NonNull final Rect mActualBounds;
+ @NonNull final Rect mExtendedBounds;
+ @NonNull final TouchDelegate mTouchDelegate;
DelegateInfo(
- @NonNull View delegateView, @NonNull Rect actualBounds,
+ @NonNull View delegateView,
+ @NonNull Rect actualBounds,
@NonNull Rect extendedBounds) {
mActualBounds = actualBounds;
mExtendedBounds = extendedBounds;
diff --git a/wear/tiles/tiles/api/current.txt b/wear/tiles/tiles/api/current.txt
index cbec097..7cd96a0 100644
--- a/wear/tiles/tiles/api/current.txt
+++ b/wear/tiles/tiles/api/current.txt
@@ -255,22 +255,22 @@
public final class EventBuilders {
}
- public static final class EventBuilders.TileAddEvent {
+ @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public static final class EventBuilders.TileAddEvent {
method public int getTileId();
}
public static final class EventBuilders.TileAddEvent.Builder {
- ctor public EventBuilders.TileAddEvent.Builder();
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public EventBuilders.TileAddEvent.Builder();
method public androidx.wear.tiles.EventBuilders.TileAddEvent build();
method public androidx.wear.tiles.EventBuilders.TileAddEvent.Builder setTileId(int);
}
- public static final class EventBuilders.TileEnterEvent {
+ @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public static final class EventBuilders.TileEnterEvent {
method public int getTileId();
}
public static final class EventBuilders.TileEnterEvent.Builder {
- ctor public EventBuilders.TileEnterEvent.Builder();
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public EventBuilders.TileEnterEvent.Builder();
method public androidx.wear.tiles.EventBuilders.TileEnterEvent build();
method public androidx.wear.tiles.EventBuilders.TileEnterEvent.Builder setTileId(int);
}
@@ -290,22 +290,22 @@
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public androidx.wear.tiles.EventBuilders.TileInteractionEvent.Builder setTimestamp(java.time.Instant);
}
- public static final class EventBuilders.TileLeaveEvent {
+ @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public static final class EventBuilders.TileLeaveEvent {
method public int getTileId();
}
public static final class EventBuilders.TileLeaveEvent.Builder {
- ctor public EventBuilders.TileLeaveEvent.Builder();
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public EventBuilders.TileLeaveEvent.Builder();
method public androidx.wear.tiles.EventBuilders.TileLeaveEvent build();
method public androidx.wear.tiles.EventBuilders.TileLeaveEvent.Builder setTileId(int);
}
- public static final class EventBuilders.TileRemoveEvent {
+ @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public static final class EventBuilders.TileRemoveEvent {
method public int getTileId();
}
public static final class EventBuilders.TileRemoveEvent.Builder {
- ctor public EventBuilders.TileRemoveEvent.Builder();
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public EventBuilders.TileRemoveEvent.Builder();
method public androidx.wear.tiles.EventBuilders.TileRemoveEvent build();
method public androidx.wear.tiles.EventBuilders.TileRemoveEvent.Builder setTileId(int);
}
@@ -914,7 +914,7 @@
public final class RequestBuilders {
}
- public static final class RequestBuilders.ResourcesRequest {
+ @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public static final class RequestBuilders.ResourcesRequest {
method public androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters getDeviceConfiguration();
method @Deprecated public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters? getDeviceParameters();
method public java.util.List<java.lang.String!> getResourceIds();
@@ -923,16 +923,16 @@
}
public static final class RequestBuilders.ResourcesRequest.Builder {
- ctor public RequestBuilders.ResourcesRequest.Builder();
- method public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder addResourceId(String);
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public RequestBuilders.ResourcesRequest.Builder();
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder addResourceId(String);
method public androidx.wear.tiles.RequestBuilders.ResourcesRequest build();
- method public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder setDeviceConfiguration(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder setDeviceConfiguration(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
method @Deprecated public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder setDeviceParameters(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
- method public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder setTileId(int);
- method public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder setVersion(String);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder setTileId(int);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder setVersion(String);
}
- public static final class RequestBuilders.TileRequest {
+ @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public static final class RequestBuilders.TileRequest {
method public androidx.wear.protolayout.StateBuilders.State getCurrentState();
method public androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters getDeviceConfiguration();
method @Deprecated public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters? getDeviceParameters();
@@ -941,13 +941,13 @@
}
public static final class RequestBuilders.TileRequest.Builder {
- ctor public RequestBuilders.TileRequest.Builder();
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public RequestBuilders.TileRequest.Builder();
method public androidx.wear.tiles.RequestBuilders.TileRequest build();
- method public androidx.wear.tiles.RequestBuilders.TileRequest.Builder setCurrentState(androidx.wear.protolayout.StateBuilders.State);
- method public androidx.wear.tiles.RequestBuilders.TileRequest.Builder setDeviceConfiguration(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public androidx.wear.tiles.RequestBuilders.TileRequest.Builder setCurrentState(androidx.wear.protolayout.StateBuilders.State);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public androidx.wear.tiles.RequestBuilders.TileRequest.Builder setDeviceConfiguration(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
method @Deprecated public androidx.wear.tiles.RequestBuilders.TileRequest.Builder setDeviceParameters(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
method @Deprecated public androidx.wear.tiles.RequestBuilders.TileRequest.Builder setState(androidx.wear.tiles.StateBuilders.State);
- method public androidx.wear.tiles.RequestBuilders.TileRequest.Builder setTileId(int);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public androidx.wear.tiles.RequestBuilders.TileRequest.Builder setTileId(int);
}
@Deprecated public final class ResourceBuilders {
@@ -1022,7 +1022,7 @@
public final class TileBuilders {
}
- public static final class TileBuilders.Tile {
+ @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public static final class TileBuilders.Tile {
method public long getFreshnessIntervalMillis();
method public String getResourcesVersion();
method public androidx.wear.protolayout.StateBuilders.State? getState();
@@ -1031,12 +1031,12 @@
}
public static final class TileBuilders.Tile.Builder {
- ctor public TileBuilders.Tile.Builder();
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public TileBuilders.Tile.Builder();
method public androidx.wear.tiles.TileBuilders.Tile build();
- method public androidx.wear.tiles.TileBuilders.Tile.Builder setFreshnessIntervalMillis(long);
- method public androidx.wear.tiles.TileBuilders.Tile.Builder setResourcesVersion(String);
- method public androidx.wear.tiles.TileBuilders.Tile.Builder setState(androidx.wear.protolayout.StateBuilders.State);
- method public androidx.wear.tiles.TileBuilders.Tile.Builder setTileTimeline(androidx.wear.protolayout.TimelineBuilders.Timeline);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public androidx.wear.tiles.TileBuilders.Tile.Builder setFreshnessIntervalMillis(long);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public androidx.wear.tiles.TileBuilders.Tile.Builder setResourcesVersion(String);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.tiles.TileBuilders.Tile.Builder setState(androidx.wear.protolayout.StateBuilders.State);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public androidx.wear.tiles.TileBuilders.Tile.Builder setTileTimeline(androidx.wear.protolayout.TimelineBuilders.Timeline);
method @Deprecated public androidx.wear.tiles.TileBuilders.Tile.Builder setTimeline(androidx.wear.tiles.TimelineBuilders.Timeline);
}
diff --git a/wear/tiles/tiles/api/restricted_current.txt b/wear/tiles/tiles/api/restricted_current.txt
index cbec097..7cd96a0 100644
--- a/wear/tiles/tiles/api/restricted_current.txt
+++ b/wear/tiles/tiles/api/restricted_current.txt
@@ -255,22 +255,22 @@
public final class EventBuilders {
}
- public static final class EventBuilders.TileAddEvent {
+ @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public static final class EventBuilders.TileAddEvent {
method public int getTileId();
}
public static final class EventBuilders.TileAddEvent.Builder {
- ctor public EventBuilders.TileAddEvent.Builder();
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public EventBuilders.TileAddEvent.Builder();
method public androidx.wear.tiles.EventBuilders.TileAddEvent build();
method public androidx.wear.tiles.EventBuilders.TileAddEvent.Builder setTileId(int);
}
- public static final class EventBuilders.TileEnterEvent {
+ @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public static final class EventBuilders.TileEnterEvent {
method public int getTileId();
}
public static final class EventBuilders.TileEnterEvent.Builder {
- ctor public EventBuilders.TileEnterEvent.Builder();
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public EventBuilders.TileEnterEvent.Builder();
method public androidx.wear.tiles.EventBuilders.TileEnterEvent build();
method public androidx.wear.tiles.EventBuilders.TileEnterEvent.Builder setTileId(int);
}
@@ -290,22 +290,22 @@
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public androidx.wear.tiles.EventBuilders.TileInteractionEvent.Builder setTimestamp(java.time.Instant);
}
- public static final class EventBuilders.TileLeaveEvent {
+ @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public static final class EventBuilders.TileLeaveEvent {
method public int getTileId();
}
public static final class EventBuilders.TileLeaveEvent.Builder {
- ctor public EventBuilders.TileLeaveEvent.Builder();
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public EventBuilders.TileLeaveEvent.Builder();
method public androidx.wear.tiles.EventBuilders.TileLeaveEvent build();
method public androidx.wear.tiles.EventBuilders.TileLeaveEvent.Builder setTileId(int);
}
- public static final class EventBuilders.TileRemoveEvent {
+ @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public static final class EventBuilders.TileRemoveEvent {
method public int getTileId();
}
public static final class EventBuilders.TileRemoveEvent.Builder {
- ctor public EventBuilders.TileRemoveEvent.Builder();
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public EventBuilders.TileRemoveEvent.Builder();
method public androidx.wear.tiles.EventBuilders.TileRemoveEvent build();
method public androidx.wear.tiles.EventBuilders.TileRemoveEvent.Builder setTileId(int);
}
@@ -914,7 +914,7 @@
public final class RequestBuilders {
}
- public static final class RequestBuilders.ResourcesRequest {
+ @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public static final class RequestBuilders.ResourcesRequest {
method public androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters getDeviceConfiguration();
method @Deprecated public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters? getDeviceParameters();
method public java.util.List<java.lang.String!> getResourceIds();
@@ -923,16 +923,16 @@
}
public static final class RequestBuilders.ResourcesRequest.Builder {
- ctor public RequestBuilders.ResourcesRequest.Builder();
- method public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder addResourceId(String);
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public RequestBuilders.ResourcesRequest.Builder();
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder addResourceId(String);
method public androidx.wear.tiles.RequestBuilders.ResourcesRequest build();
- method public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder setDeviceConfiguration(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder setDeviceConfiguration(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
method @Deprecated public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder setDeviceParameters(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
- method public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder setTileId(int);
- method public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder setVersion(String);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder setTileId(int);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder setVersion(String);
}
- public static final class RequestBuilders.TileRequest {
+ @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public static final class RequestBuilders.TileRequest {
method public androidx.wear.protolayout.StateBuilders.State getCurrentState();
method public androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters getDeviceConfiguration();
method @Deprecated public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters? getDeviceParameters();
@@ -941,13 +941,13 @@
}
public static final class RequestBuilders.TileRequest.Builder {
- ctor public RequestBuilders.TileRequest.Builder();
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public RequestBuilders.TileRequest.Builder();
method public androidx.wear.tiles.RequestBuilders.TileRequest build();
- method public androidx.wear.tiles.RequestBuilders.TileRequest.Builder setCurrentState(androidx.wear.protolayout.StateBuilders.State);
- method public androidx.wear.tiles.RequestBuilders.TileRequest.Builder setDeviceConfiguration(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public androidx.wear.tiles.RequestBuilders.TileRequest.Builder setCurrentState(androidx.wear.protolayout.StateBuilders.State);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public androidx.wear.tiles.RequestBuilders.TileRequest.Builder setDeviceConfiguration(androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
method @Deprecated public androidx.wear.tiles.RequestBuilders.TileRequest.Builder setDeviceParameters(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
method @Deprecated public androidx.wear.tiles.RequestBuilders.TileRequest.Builder setState(androidx.wear.tiles.StateBuilders.State);
- method public androidx.wear.tiles.RequestBuilders.TileRequest.Builder setTileId(int);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public androidx.wear.tiles.RequestBuilders.TileRequest.Builder setTileId(int);
}
@Deprecated public final class ResourceBuilders {
@@ -1022,7 +1022,7 @@
public final class TileBuilders {
}
- public static final class TileBuilders.Tile {
+ @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public static final class TileBuilders.Tile {
method public long getFreshnessIntervalMillis();
method public String getResourcesVersion();
method public androidx.wear.protolayout.StateBuilders.State? getState();
@@ -1031,12 +1031,12 @@
}
public static final class TileBuilders.Tile.Builder {
- ctor public TileBuilders.Tile.Builder();
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public TileBuilders.Tile.Builder();
method public androidx.wear.tiles.TileBuilders.Tile build();
- method public androidx.wear.tiles.TileBuilders.Tile.Builder setFreshnessIntervalMillis(long);
- method public androidx.wear.tiles.TileBuilders.Tile.Builder setResourcesVersion(String);
- method public androidx.wear.tiles.TileBuilders.Tile.Builder setState(androidx.wear.protolayout.StateBuilders.State);
- method public androidx.wear.tiles.TileBuilders.Tile.Builder setTileTimeline(androidx.wear.protolayout.TimelineBuilders.Timeline);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public androidx.wear.tiles.TileBuilders.Tile.Builder setFreshnessIntervalMillis(long);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public androidx.wear.tiles.TileBuilders.Tile.Builder setResourcesVersion(String);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.tiles.TileBuilders.Tile.Builder setState(androidx.wear.protolayout.StateBuilders.State);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=0) public androidx.wear.tiles.TileBuilders.Tile.Builder setTileTimeline(androidx.wear.protolayout.TimelineBuilders.Timeline);
method @Deprecated public androidx.wear.tiles.TileBuilders.Tile.Builder setTimeline(androidx.wear.tiles.TimelineBuilders.Timeline);
}
diff --git a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/ActionBuilders.java b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/ActionBuilders.java
index 0dd9ce1..37ad14f 100644
--- a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/ActionBuilders.java
+++ b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/ActionBuilders.java
@@ -1,19 +1,18 @@
/*
-* Copyright 2021 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.
-*/
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package androidx.wear.tiles;
diff --git a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/EventBuilders.java b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/EventBuilders.java
index b870020..7e72ffb 100644
--- a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/EventBuilders.java
+++ b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/EventBuilders.java
@@ -33,6 +33,7 @@
private EventBuilders() {}
/** Event fired when a tile has been added to the carousel. */
+ @RequiresSchemaVersion(major = 1, minor = 0)
public static final class TileAddEvent {
private final EventProto.TileAddEvent mImpl;
@@ -74,6 +75,8 @@
private final EventProto.TileAddEvent.Builder mImpl =
EventProto.TileAddEvent.newBuilder();
+ /** Creates an instance of {@link Builder}. */
+ @RequiresSchemaVersion(major = 1, minor = 0)
public Builder() {}
/**
@@ -96,6 +99,7 @@
}
/** Event fired when a tile has been removed from the carousel. */
+ @RequiresSchemaVersion(major = 1, minor = 0)
public static final class TileRemoveEvent {
private final EventProto.TileRemoveEvent mImpl;
@@ -137,6 +141,8 @@
private final EventProto.TileRemoveEvent.Builder mImpl =
EventProto.TileRemoveEvent.newBuilder();
+ /** Creates an instance of {@link Builder}. */
+ @RequiresSchemaVersion(major = 1, minor = 0)
public Builder() {}
/**
@@ -158,6 +164,7 @@
}
/** Event fired when a tile is swiped to by the user (i.e. it's visible on screen). */
+ @RequiresSchemaVersion(major = 1, minor = 0)
public static final class TileEnterEvent {
private final EventProto.TileEnterEvent mImpl;
@@ -199,6 +206,8 @@
private final EventProto.TileEnterEvent.Builder mImpl =
EventProto.TileEnterEvent.newBuilder();
+ /** Creates an instance of {@link Builder}. */
+ @RequiresSchemaVersion(major = 1, minor = 0)
public Builder() {}
/**
@@ -224,6 +233,7 @@
* Event fired when a tile is swiped away from by the user (i.e. it's no longer visible on
* screen).
*/
+ @RequiresSchemaVersion(major = 1, minor = 0)
public static final class TileLeaveEvent {
private final EventProto.TileLeaveEvent mImpl;
@@ -265,6 +275,8 @@
private final EventProto.TileLeaveEvent.Builder mImpl =
EventProto.TileLeaveEvent.newBuilder();
+ /** Creates an instance of {@link Builder}. */
+ @RequiresSchemaVersion(major = 1, minor = 0)
public Builder() {}
/**
diff --git a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/RequestBuilders.java b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/RequestBuilders.java
index a0b6980..b12b8ff2 100644
--- a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/RequestBuilders.java
+++ b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/RequestBuilders.java
@@ -22,6 +22,7 @@
import androidx.annotation.RestrictTo.Scope;
import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
import androidx.wear.protolayout.StateBuilders.State;
+import androidx.wear.protolayout.expression.RequiresSchemaVersion;
import androidx.wear.protolayout.proto.DeviceParametersProto;
import androidx.wear.protolayout.proto.StateProto;
import androidx.wear.tiles.proto.RequestProto;
@@ -36,6 +37,7 @@
* Parameters passed to a {@link androidx.wear.tiles.TileBuilders.Tile} Service when the
* renderer is requesting a new version of the tile.
*/
+ @RequiresSchemaVersion(major = 1, minor = 0)
public static final class TileRequest {
private final RequestProto.TileRequest mImpl;
@@ -147,6 +149,8 @@
private final RequestProto.TileRequest.Builder mImpl =
RequestProto.TileRequest.newBuilder();
+ /** Creates an instance of {@link Builder}. */
+ @RequiresSchemaVersion(major = 1, minor = 0)
public Builder() {}
/**
@@ -154,6 +158,7 @@
* object describing the device requesting the tile update. If not set, a default empty
* instance is used.
*/
+ @RequiresSchemaVersion(major = 1, minor = 0)
@NonNull
public Builder setDeviceConfiguration(@NonNull DeviceParameters deviceConfiguration) {
mImpl.setDeviceConfiguration(deviceConfiguration.toProto());
@@ -164,15 +169,15 @@
* Sets the {@link androidx.wear.protolayout.StateBuilders.State} that should be used
* when building the tile.
*/
+ @RequiresSchemaVersion(major = 1, minor = 0)
@NonNull
public Builder setCurrentState(@NonNull State currentState) {
mImpl.setCurrentState(currentState.toProto());
return this;
}
- /**
- * Sets the ID of the tile being requested.
- */
+ /** Sets the ID of the tile being requested. */
+ @RequiresSchemaVersion(major = 1, minor = 0)
@NonNull
public Builder setTileId(int tileId) {
mImpl.setTileId(tileId);
@@ -220,6 +225,7 @@
* Parameters passed to a {@link androidx.wear.tiles.TileBuilders.Tile} Service when the
* renderer is requesting a specific resource version.
*/
+ @RequiresSchemaVersion(major = 1, minor = 0)
public static final class ResourcesRequest {
private final RequestProto.ResourcesRequest mImpl;
@@ -325,13 +331,15 @@
private final RequestProto.ResourcesRequest.Builder mImpl =
RequestProto.ResourcesRequest.newBuilder();
+ /** Creates an instance of {@link Builder}. */
+ @RequiresSchemaVersion(major = 1, minor = 0)
public Builder() {}
/**
* Sets the version of the resources being fetched. This is the same as the requested
* resource version, passed in {@link androidx.wear.tiles.TileBuilders.Tile}.
- *
*/
+ @RequiresSchemaVersion(major = 1, minor = 0)
@NonNull
public Builder setVersion(@NonNull String version) {
mImpl.setVersion(version);
@@ -348,6 +356,7 @@
* used in {@link androidx.wear.protolayout.ResourceBuilders.Resources}.idToImage), not
* Android resource names or similar.
*/
+ @RequiresSchemaVersion(major = 1, minor = 0)
@NonNull
public Builder addResourceId(@NonNull String resourceId) {
mImpl.addResourceIds(resourceId);
@@ -358,15 +367,15 @@
* Sets the {@link androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters}
* object describing the device requesting the resources.
*/
+ @RequiresSchemaVersion(major = 1, minor = 0)
@NonNull
public Builder setDeviceConfiguration(@NonNull DeviceParameters deviceConfiguration) {
mImpl.setDeviceConfiguration(deviceConfiguration.toProto());
return this;
}
- /**
- * Sets the ID of the tile for which resources are being requested.
- */
+ /** Sets the ID of the tile for which resources are being requested. */
+ @RequiresSchemaVersion(major = 1, minor = 0)
@NonNull
public Builder setTileId(int tileId) {
mImpl.setTileId(tileId);
diff --git a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/TileBuilders.java b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/TileBuilders.java
index f81f833..7d9a635 100644
--- a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/TileBuilders.java
+++ b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/TileBuilders.java
@@ -22,6 +22,7 @@
import androidx.annotation.RestrictTo.Scope;
import androidx.wear.protolayout.StateBuilders.State;
import androidx.wear.protolayout.TimelineBuilders.Timeline;
+import androidx.wear.protolayout.expression.RequiresSchemaVersion;
import androidx.wear.protolayout.expression.proto.VersionProto.VersionInfo;
import androidx.wear.tiles.proto.TileProto;
@@ -33,6 +34,7 @@
* A holder for a tile. This specifies the resources to use for this delivery of the tile, and
* the timeline for the tile.
*/
+ @RequiresSchemaVersion(major = 1, minor = 0)
public static final class Tile {
private final TileProto.Tile mImpl;
@@ -81,9 +83,7 @@
return mImpl.getFreshnessIntervalMillis();
}
- /**
- * Gets {@link androidx.wear.protolayout.StateBuilders.State} for this tile.
- */
+ /** Gets {@link androidx.wear.protolayout.StateBuilders.State} for this tile. */
@Nullable
public State getState() {
if (mImpl.hasState()) {
@@ -144,6 +144,8 @@
public static final class Builder {
private final TileProto.Tile.Builder mImpl = TileProto.Tile.newBuilder();
+ /** Creates an instance of {@link Builder}. */
+ @RequiresSchemaVersion(major = 1, minor = 0)
public Builder() {}
/**
@@ -152,6 +154,7 @@
* androidx.wear.tiles.RequestBuilders.ResourcesRequest} if the system does not have a
* copy of the specified resource version.
*/
+ @RequiresSchemaVersion(major = 1, minor = 0)
@NonNull
public Builder setResourcesVersion(@NonNull String resourcesVersion) {
mImpl.setResourcesVersion(resourcesVersion);
@@ -162,6 +165,7 @@
* Sets the {@link androidx.wear.protolayout.TimelineBuilders.Timeline} containing the
* layouts for the tiles to show in the carousel, along with their validity periods.
*/
+ @RequiresSchemaVersion(major = 1, minor = 0)
@NonNull
public Builder setTileTimeline(@NonNull Timeline tileTimeline) {
mImpl.setTileTimeline(tileTimeline.toProto());
@@ -181,15 +185,15 @@
* tile if it is on-screen, or about to be on-screen, although this is not guaranteed
* due to system-level optimizations.
*/
+ @RequiresSchemaVersion(major = 1, minor = 0)
@NonNull
public Builder setFreshnessIntervalMillis(long freshnessIntervalMillis) {
mImpl.setFreshnessIntervalMillis(freshnessIntervalMillis);
return this;
}
- /**
- * Sets {@link androidx.wear.protolayout.StateBuilders.State} for this tile.
- */
+ /** Sets {@link androidx.wear.protolayout.StateBuilders.State} for this tile. */
+ @RequiresSchemaVersion(major = 1, minor = 200)
@NonNull
public Builder setState(@NonNull State state) {
mImpl.setState(state.toProto());
diff --git a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/TileLeaveEventData.java b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/TileLeaveEventData.java
index 97005c8..476cd9a 100644
--- a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/TileLeaveEventData.java
+++ b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/TileLeaveEventData.java
@@ -20,7 +20,7 @@
import androidx.annotation.RestrictTo;
/**
- * Holder for Tiles' TileLeaveEvent class, to be parceled and transferred to a tile service.
+ * Holder for Tiles' TileLeaveEvent class, to be parceled and transferred to a Tile Service.
*
* <p>All this does is to serialize TileLeaveEvent as a protobuf and transmit it.
*/
diff --git a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/TileUpdateRequestData.java b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/TileUpdateRequestData.java
index 3198815..1907b70 100644
--- a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/TileUpdateRequestData.java
+++ b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/TileUpdateRequestData.java
@@ -26,6 +26,7 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public final class TileUpdateRequestData extends ProtoParcelable {
public static final int VERSION_PLACEHOLDER = 1;
+
public static final Creator<TileUpdateRequestData> CREATOR =
newCreator(TileUpdateRequestData.class, TileUpdateRequestData::new);
diff --git a/wear/tiles/tiles/src/test/java/androidx/wear/tiles/CompositeTileUpdateRequesterTest.java b/wear/tiles/tiles/src/test/java/androidx/wear/tiles/CompositeTileUpdateRequesterTest.java
index 500a3ed..5c0133e 100644
--- a/wear/tiles/tiles/src/test/java/androidx/wear/tiles/CompositeTileUpdateRequesterTest.java
+++ b/wear/tiles/tiles/src/test/java/androidx/wear/tiles/CompositeTileUpdateRequesterTest.java
@@ -24,19 +24,18 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.concurrent.futures.ResolvableFuture;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.wear.protolayout.ResourceBuilders;
+import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ListenableFuture;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.internal.DoNotInstrument;
-import java.util.List;
-
-@RunWith(RobolectricTestRunner.class)
+@RunWith(AndroidJUnit4.class)
@DoNotInstrument
public class CompositeTileUpdateRequesterTest {
private FakeUpdateRequester mFakeUpdateRequester1;
@@ -51,7 +50,7 @@
mCompositeTileUpdateRequesterUnderTest =
new CompositeTileUpdateRequester(
- List.of(mFakeUpdateRequester1, mFakeUpdateRequester2));
+ ImmutableList.of(mFakeUpdateRequester1, mFakeUpdateRequester2));
}
@Test
@@ -62,7 +61,7 @@
assertThat(mFakeUpdateRequester2.mCalledService).isEqualTo(FakeService.class);
}
- private class FakeUpdateRequester implements TileUpdateRequester {
+ private static class FakeUpdateRequester implements TileUpdateRequester {
@Nullable Class<? extends TileService> mCalledService = null;
@Override
@@ -71,7 +70,7 @@
}
}
- private class FakeService extends TileService {
+ private static class FakeService extends TileService {
@NonNull
@Override
protected ListenableFuture<TileBuilders.Tile> onTileRequest(
diff --git a/wear/tiles/tiles/src/test/java/androidx/wear/tiles/ProtoParcelableTest.java b/wear/tiles/tiles/src/test/java/androidx/wear/tiles/ProtoParcelableTest.java
index 17a2330..d0032c7 100644
--- a/wear/tiles/tiles/src/test/java/androidx/wear/tiles/ProtoParcelableTest.java
+++ b/wear/tiles/tiles/src/test/java/androidx/wear/tiles/ProtoParcelableTest.java
@@ -20,15 +20,16 @@
import android.os.Parcel;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.wear.tiles.proto.RequestProto;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.internal.DoNotInstrument;
-@RunWith(RobolectricTestRunner.class)
-@DoNotInstrument
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument // See http://g/robolectric-users/fTi2FRXgyGA/m/PkB0wYuwBgAJ
public final class ProtoParcelableTest {
public static class Wrapper extends ProtoParcelable {
public static final int VERSION = 1;
diff --git a/wear/tiles/tiles/src/test/java/androidx/wear/tiles/ResourcesDataTest.java b/wear/tiles/tiles/src/test/java/androidx/wear/tiles/ResourcesDataTest.java
index cdbb5a9..7dfa440 100644
--- a/wear/tiles/tiles/src/test/java/androidx/wear/tiles/ResourcesDataTest.java
+++ b/wear/tiles/tiles/src/test/java/androidx/wear/tiles/ResourcesDataTest.java
@@ -20,14 +20,14 @@
import android.os.Parcel;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.wear.protolayout.proto.ResourceProto.Resources;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.internal.DoNotInstrument;
-@RunWith(RobolectricTestRunner.class)
+@RunWith(AndroidJUnit4.class)
@DoNotInstrument
public final class ResourcesDataTest {
@Test
diff --git a/wear/tiles/tiles/src/test/java/androidx/wear/tiles/ResourcesRequestDataTest.java b/wear/tiles/tiles/src/test/java/androidx/wear/tiles/ResourcesRequestDataTest.java
index 61dd9f1..3837ce0 100644
--- a/wear/tiles/tiles/src/test/java/androidx/wear/tiles/ResourcesRequestDataTest.java
+++ b/wear/tiles/tiles/src/test/java/androidx/wear/tiles/ResourcesRequestDataTest.java
@@ -20,14 +20,14 @@
import android.os.Parcel;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.wear.tiles.proto.RequestProto;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.internal.DoNotInstrument;
-@RunWith(RobolectricTestRunner.class)
+@RunWith(AndroidJUnit4.class)
@DoNotInstrument
public final class ResourcesRequestDataTest {
@Test
diff --git a/wear/tiles/tiles/src/test/java/androidx/wear/tiles/TileDataTest.java b/wear/tiles/tiles/src/test/java/androidx/wear/tiles/TileDataTest.java
index f1938b6..100c0d3 100644
--- a/wear/tiles/tiles/src/test/java/androidx/wear/tiles/TileDataTest.java
+++ b/wear/tiles/tiles/src/test/java/androidx/wear/tiles/TileDataTest.java
@@ -20,14 +20,14 @@
import android.os.Parcel;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.wear.tiles.proto.TileProto.Tile;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.internal.DoNotInstrument;
-@RunWith(RobolectricTestRunner.class)
+@RunWith(AndroidJUnit4.class)
@DoNotInstrument
public final class TileDataTest {
@Test
diff --git a/wear/tiles/tiles/src/test/java/androidx/wear/tiles/TileRequestDataTest.java b/wear/tiles/tiles/src/test/java/androidx/wear/tiles/TileRequestDataTest.java
index bf8ad22..6128a33 100644
--- a/wear/tiles/tiles/src/test/java/androidx/wear/tiles/TileRequestDataTest.java
+++ b/wear/tiles/tiles/src/test/java/androidx/wear/tiles/TileRequestDataTest.java
@@ -20,14 +20,14 @@
import android.os.Parcel;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.wear.tiles.proto.RequestProto;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.internal.DoNotInstrument;
-@RunWith(RobolectricTestRunner.class)
+@RunWith(AndroidJUnit4.class)
@DoNotInstrument
public final class TileRequestDataTest {
@Test