Merge "Update default alignment for base Button overload for consistency with other overloads." into androidx-main
diff --git a/annotation/annotation/build.gradle b/annotation/annotation/build.gradle
index 0b88e7c..2f5d5d7 100644
--- a/annotation/annotation/build.gradle
+++ b/annotation/annotation/build.gradle
@@ -42,7 +42,7 @@
wasmJsMain {
dependsOn(nonJvmMain)
dependencies {
- api(libs.kotlinStdlibJs)
+ implementation(libs.kotlinStdlibJs)
}
}
diff --git a/autofill/autofill/src/main/java/androidx/autofill/HintConstants.java b/autofill/autofill/src/main/java/androidx/autofill/HintConstants.java
index 12612a1..028e2dc 100644
--- a/autofill/autofill/src/main/java/androidx/autofill/HintConstants.java
+++ b/autofill/autofill/src/main/java/androidx/autofill/HintConstants.java
@@ -40,7 +40,7 @@
* should be <code>{@value #AUTOFILL_HINT_EMAIL_ADDRESS}</code>).
*
* <p>See {@link android.view.View#setAutofillHints(String...)} for more info about autofill
- * hints.3
+ * hints.
*/
public static final String AUTOFILL_HINT_EMAIL_ADDRESS = "emailAddress";
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt
index 7aa48d6..dc61707 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt
@@ -35,9 +35,13 @@
try {
streamConfigurationMap?.outputFormats
} catch (e: NullPointerException) {
- Logger.e(TAG, "Failed to get output formats from StreamConfigurationMap", e)
+ Logger.w(TAG, "Failed to get output formats from StreamConfigurationMap", e)
+ null
+ } catch (e: IllegalArgumentException) {
+ Logger.w(TAG, "Failed to get output formats from StreamConfigurationMap", e)
null
}
+
return outputFormats?.toTypedArray()
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/RequestProcessorAdapterTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/RequestProcessorAdapterTest.kt
index a933a0a..d9e49d0 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/RequestProcessorAdapterTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/RequestProcessorAdapterTest.kt
@@ -168,6 +168,8 @@
val callback: RequestProcessor.Callback = mock()
requestProcessorAdapter!!.setRepeating(requestToSet, callback)
+ advanceUntilIdle()
+
val frame = cameraGraphSimulator!!.simulateNextFrame()
val request = frame.request
assertThat(request.streams.size).isEqualTo(1)
@@ -209,6 +211,8 @@
val callback: RequestProcessor.Callback = mock()
requestProcessorAdapter!!.submit(mutableListOf(requestToSubmit), callback)
+ advanceUntilIdle()
+
val frame = cameraGraphSimulator!!.simulateNextFrame()
val request = frame.request
assertThat(request.streams.size).isEqualTo(1)
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/CameraGraphSimulator.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/CameraGraphSimulator.kt
index 63a3e39..78d0670 100644
--- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/CameraGraphSimulator.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/CameraGraphSimulator.kt
@@ -34,10 +34,8 @@
import androidx.camera.camera2.pipe.RequestFailure
import androidx.camera.camera2.pipe.StreamId
import androidx.camera.camera2.pipe.media.ImageSource
-import kotlin.collections.removeFirst as removeFirstKt
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.withTimeout
/**
* This class creates a [CameraPipe] and [CameraGraph] instance using a [FakeCameraBackend].
@@ -166,7 +164,7 @@
}
}
- public suspend fun simulateNextFrame(
+ public fun simulateNextFrame(
advanceClockByNanos: Long = 33_366_666 // (2_000_000_000 / (60 / 1.001))
): FrameSimulator =
generateNextFrame().also {
@@ -174,7 +172,7 @@
it.simulateStarted(clockNanos)
}
- private suspend fun generateNextFrame(): FrameSimulator {
+ private fun generateNextFrame(): FrameSimulator {
val captureSequenceProcessor = cameraController.currentCaptureSequenceProcessor
check(captureSequenceProcessor != null) {
"simulateCameraStarted() must be called before frames can be created!"
@@ -183,16 +181,19 @@
// This checks the pending frame queue and polls for the next request. If no request is
// available it will suspend until the next interaction with the request processor.
if (pendingFrameQueue.isEmpty()) {
- val requestSequence =
- withTimeout(timeMillis = 250) { captureSequenceProcessor.nextRequestSequence() }
+ val captureSequence = captureSequenceProcessor.nextCaptureSequence()
+ checkNotNull(captureSequence) {
+ "Failed to simulate a CaptureSequence from $captureSequenceProcessor! Make sure " +
+ "Requests have been submitted or that the repeating Request has been set."
+ }
// Each sequence is processed as a group, and if a sequence contains multiple requests
// the list of requests is processed in order before polling the next sequence.
- for (request in requestSequence.captureRequestList) {
- pendingFrameQueue.add(FrameSimulator(request, requestSequence))
+ for (request in captureSequence.captureRequestList) {
+ pendingFrameQueue.add(FrameSimulator(request, captureSequence))
}
}
- return pendingFrameQueue.removeFirstKt()
+ return pendingFrameQueue.removeAt(0)
}
/** Utility function to simulate the production of a [FakeImage]s for one or more streams. */
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraIds.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraIds.kt
new file mode 100644
index 0000000..7e718b1
--- /dev/null
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraIds.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.camera.camera2.pipe.testing
+
+import androidx.camera.camera2.pipe.CameraId
+import kotlinx.atomicfu.atomic
+
+/**
+ * Utility class for tracking and creating Fake [CameraId] instances for use in testing.
+ *
+ * These id's are intentionally non-numerical to help prevent code that may assume that camera2
+ * camera ids are parsable.
+ */
+public object FakeCameraIds {
+ private val fakeCameraIds = atomic(0)
+ public val default: CameraId = CameraId("FakeCamera-default")
+
+ public fun next(): CameraId = CameraId("FakeCamera-${fakeCameraIds.getAndIncrement()}")
+}
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraMetadata.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraMetadata.kt
index 1280187..38f6520 100644
--- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraMetadata.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraMetadata.kt
@@ -27,12 +27,6 @@
import androidx.camera.camera2.pipe.CameraMetadata
import androidx.camera.camera2.pipe.Metadata
import kotlin.reflect.KClass
-import kotlinx.atomicfu.atomic
-
-private val fakeCameraIds = atomic(0)
-
-internal fun nextFakeCameraId(): CameraId =
- CameraId("FakeCamera-${fakeCameraIds.incrementAndGet()}")
/** Utility class for interacting with objects that require pre-populated Metadata. */
public open class FakeMetadata(private val metadata: Map<Metadata.Key<*>, Any?> = emptyMap()) :
@@ -56,7 +50,7 @@
public class FakeCameraMetadata(
private val characteristics: Map<CameraCharacteristics.Key<*>, Any?> = emptyMap(),
metadata: Map<Metadata.Key<*>, Any?> = emptyMap(),
- cameraId: CameraId = nextFakeCameraId(),
+ cameraId: CameraId = FakeCameraIds.default,
override val keys: Set<CameraCharacteristics.Key<*>> = emptySet(),
override val requestKeys: Set<CaptureRequest.Key<*>> = emptySet(),
override val resultKeys: Set<CaptureResult.Key<*>> = emptySet(),
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequenceProcessor.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequenceProcessor.kt
index 5010c90..161d428 100644
--- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequenceProcessor.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCaptureSequenceProcessor.kt
@@ -25,48 +25,81 @@
import androidx.camera.camera2.pipe.RequestTemplate
import androidx.camera.camera2.pipe.StreamId
import kotlinx.atomicfu.atomic
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.Deferred
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.withTimeout
/**
- * Fake implementation of a [CaptureSequenceProcessor] that passes events to a [Channel].
+ * Fake implementation of a [CaptureSequenceProcessor] that records events and simulates some low
+ * level behavior.
*
* This allows kotlin tests to check sequences of interactions that dispatch in the background
* without blocking between events.
*/
public class FakeCaptureSequenceProcessor(
- private val cameraId: CameraId = CameraId("test-camera"),
+ private val cameraId: CameraId = FakeCameraIds.default,
private val defaultTemplate: RequestTemplate = RequestTemplate(1)
) : CaptureSequenceProcessor<Request, FakeCaptureSequence> {
+ private val debugId = debugIds.incrementAndGet()
private val lock = Any()
private val sequenceIds = atomic(0)
- private val eventChannel = Channel<Event>(Channel.UNLIMITED)
- @GuardedBy("lock") private var pendingSequence: CompletableDeferred<FakeCaptureSequence>? = null
+ @GuardedBy("lock") private val captureQueue = mutableListOf<FakeCaptureSequence>()
- @GuardedBy("lock") private val queue: MutableList<FakeCaptureSequence> = mutableListOf()
+ @GuardedBy("lock") private var repeatingCapture: FakeCaptureSequence? = null
- @GuardedBy("lock") private var repeatingRequestSequence: FakeCaptureSequence? = null
+ @GuardedBy("lock") private var shutdown = false
- @GuardedBy("lock") private var _rejectRequests = false
+ @GuardedBy("lock") private val _events = mutableListOf<Event>()
- public var rejectRequests: Boolean
- get() = synchronized(lock) { _rejectRequests }
- set(value) {
- synchronized(lock) { _rejectRequests = value }
+ @GuardedBy("lock") private var nextEventIndex = 0
+ public val events: List<Event>
+ get() = synchronized(lock) { _events }
+
+ /** Get the next event from queue with an option to specify a timeout for tests. */
+ public fun nextEvent(): Event {
+ synchronized(lock) {
+ val eventIdx = nextEventIndex++
+ check(_events.size > 0) {
+ "Failed to get next event for $this, there have been no interactions."
+ }
+ check(eventIdx < _events.size) {
+ "Failed to get next event. Last event was ${events[eventIdx - 1]}"
+ }
+ return events[eventIdx]
}
+ }
- private var _surfaceMap: Map<StreamId, Surface> = emptyMap()
- public var surfaceMap: Map<StreamId, Surface>
- get() = synchronized(lock) { _surfaceMap }
+ public fun clearEvents() {
+ synchronized(lock) {
+ _events.clear()
+ nextEventIndex = 0
+ }
+ }
+
+ public var rejectBuild: Boolean = false
+ get() = synchronized(lock) { field }
+ set(value) = synchronized(lock) { field = value }
+
+ public var rejectSubmit: Boolean = false
+ get() = synchronized(lock) { field }
+ set(value) = synchronized(lock) { field = value }
+
+ public var surfaceMap: Map<StreamId, Surface> = emptyMap()
+ get() = synchronized(lock) { field }
set(value) =
synchronized(lock) {
- _surfaceMap = value
+ field = value
println("Configured surfaceMap for $this")
}
+ @Volatile public var throwOnBuild: Boolean = false
+
+ @Volatile public var throwOnSubmit: Boolean = false
+
+ @Volatile public var throwOnStop: Boolean = false
+
+ @Volatile public var throwOnAbort: Boolean = false
+
+ @Volatile public var throwOnShutdown: Boolean = false
+
override fun build(
isRepeating: Boolean,
requests: List<Request>,
@@ -75,136 +108,157 @@
listeners: List<Request.Listener>,
sequenceListener: CaptureSequenceListener
): FakeCaptureSequence? {
- return FakeCaptureSequence.create(
- cameraId = cameraId,
- repeating = isRepeating,
- requests = requests,
- surfaceMap = surfaceMap,
- defaultTemplate = defaultTemplate,
- defaultParameters = defaultParameters,
- requiredParameters = requiredParameters,
- listeners = listeners,
- sequenceListener = sequenceListener
- )
+ throwTestExceptionIf(throwOnBuild)
+
+ val captureSequence =
+ FakeCaptureSequence.create(
+ cameraId,
+ isRepeating,
+ requests,
+ surfaceMap,
+ defaultTemplate,
+ defaultParameters,
+ requiredParameters,
+ listeners,
+ sequenceListener
+ )
+ synchronized(lock) {
+ if (rejectBuild || shutdown || captureSequence == null) {
+ println("$this: BuildRejected $captureSequence")
+ _events.add(BuildRejected(captureSequence))
+ return null
+ }
+ }
+ println("$this: Build $captureSequence")
+ return captureSequence
}
override fun submit(captureSequence: FakeCaptureSequence): Int {
- println("submit $captureSequence")
+ throwTestExceptionIf(throwOnSubmit)
synchronized(lock) {
- if (rejectRequests) {
- check(
- eventChannel
- .trySend(Event(requestSequence = captureSequence, rejected = true))
- .isSuccess
- )
+ if (rejectSubmit || shutdown) {
+ println("$this: SubmitRejected $captureSequence")
+ _events.add(SubmitRejected(captureSequence))
return -1
}
- queue.add(captureSequence)
+ captureQueue.add(captureSequence)
if (captureSequence.repeating) {
- repeatingRequestSequence = captureSequence
+ repeatingCapture = captureSequence
}
- check(
- eventChannel
- .trySend(Event(requestSequence = captureSequence, submit = true))
- .isSuccess
- )
- // If there is a non-null pending sequence, make sure we complete it here.
- pendingSequence?.also {
- pendingSequence = null
- it.complete(captureSequence)
- }
+ println("$this: Submit $captureSequence")
+ _events.add(Submit(captureSequence))
return sequenceIds.incrementAndGet()
}
}
override fun abortCaptures() {
+ throwTestExceptionIf(throwOnAbort)
+
val requestSequencesToAbort: List<FakeCaptureSequence>
synchronized(lock) {
- requestSequencesToAbort = queue.toList()
- queue.clear()
- check(eventChannel.trySend(Event(abort = true)).isSuccess)
+ println("$this: AbortCaptures")
+ _events.add(AbortCaptures)
+ requestSequencesToAbort = captureQueue.toList()
+ captureQueue.clear()
}
+
for (sequence in requestSequencesToAbort) {
sequence.invokeOnSequenceAborted()
}
}
override fun stopRepeating() {
- val requestSequence =
- synchronized(lock) {
- check(eventChannel.trySend(Event(stop = true)).isSuccess)
- repeatingRequestSequence.also { repeatingRequestSequence = null }
- }
- requestSequence?.invokeOnSequenceAborted()
+ throwTestExceptionIf(throwOnStop)
+ synchronized(lock) {
+ println("$this: StopRepeating")
+ _events.add(StopRepeating)
+ repeatingCapture = null
+ }
}
override suspend fun shutdown() {
+ throwTestExceptionIf(throwOnShutdown)
synchronized(lock) {
- rejectRequests = true
- check(eventChannel.trySend(Event(close = true)).isSuccess)
+ println("$this: Shutdown")
+ shutdown = true
+ _events.add(Shutdown)
}
}
- /** Get the next event from queue with an option to specify a timeout for tests. */
- public suspend fun nextEvent(timeMillis: Long = 500): Event =
- withTimeout(timeMillis) { eventChannel.receive() }
+ override fun toString(): String {
+ return "FakeCaptureSequenceProcessor-$debugId($cameraId)"
+ }
- public suspend fun nextRequestSequence(): FakeCaptureSequence {
- while (true) {
- val pending: Deferred<FakeCaptureSequence>
- synchronized(lock) {
- var sequence = queue.removeFirstOrNull()
- if (sequence == null) {
- sequence = repeatingRequestSequence
- }
- if (sequence != null) {
- return sequence
- }
+ /**
+ * Get the next CaptureSequence from this CaptureSequenceProcessor. If there are non-repeating
+ * capture requests in the queue, remove the first item from the queue. Otherwise, return the
+ * current repeating CaptureSequence, or null if there are no active CaptureSequences.
+ */
+ internal fun nextCaptureSequence(): FakeCaptureSequence? =
+ synchronized(lock) { captureQueue.removeFirstOrNull() ?: repeatingCapture }
- if (pendingSequence == null) {
- pendingSequence = CompletableDeferred()
- }
- pending = pendingSequence!!
- }
-
- pending.await()
+ private fun throwTestExceptionIf(condition: Boolean) {
+ if (condition) {
+ throw RuntimeException("Test Exception")
}
}
- /** TODO: It's probably better to model this as a sealed class. */
- public data class Event(
- val requestSequence: FakeCaptureSequence? = null,
- val rejected: Boolean = false,
- val abort: Boolean = false,
- val close: Boolean = false,
- val stop: Boolean = false,
- val submit: Boolean = false
- )
+ public open class Event
+
+ public object Shutdown : Event()
+
+ public object StopRepeating : Event()
+
+ public object AbortCaptures : Event()
+
+ public data class BuildRejected(val captureSequence: FakeCaptureSequence?) : Event()
+
+ public data class SubmitRejected(val captureSequence: FakeCaptureSequence) : Event()
+
+ public data class Submit(val captureSequence: FakeCaptureSequence) : Event()
public companion object {
- public suspend fun FakeCaptureSequenceProcessor.awaitEvent(
- request: Request? = null,
- filter: (event: Event) -> Boolean
- ): Event {
+ private val debugIds = atomic(0)
+ public val Event.requests: List<Request>
+ get() = checkNotNull(captureSequence).captureRequestList
- var event: Event
- var loopCount = 0
- while (loopCount < 10) {
- loopCount++
- event = this.nextEvent()
+ public val Event.requiredParameters: Map<*, Any?>
+ get() = checkNotNull(captureSequence).requiredParameters
- if (request != null) {
- val contains =
- event.requestSequence?.captureRequestList?.contains(request) ?: false
- if (filter(event) && contains) {
- return event
- }
- } else if (filter(event)) {
- return event
+ public val Event.defaultParameters: Map<*, Any?>
+ get() = checkNotNull(captureSequence).defaultParameters
+
+ // TODO: Decide if these should only work on successful submit or not.
+ public val Event.isRepeating: Boolean
+ get() = (this as? Submit)?.captureSequence?.repeating ?: false
+
+ public val Event.isCapture: Boolean
+ get() = (this as? Submit)?.captureSequence?.repeating == false
+
+ public val Event.isRejected: Boolean
+ get() =
+ when (this) {
+ is BuildRejected,
+ is SubmitRejected -> true
+ else -> false
}
- }
- throw IllegalStateException("Failed to observe a submit event containing $request")
- }
+ public val Event.isAbort: Boolean
+ get() = this is AbortCaptures
+
+ public val Event.isStopRepeating: Boolean
+ get() = this is StopRepeating
+
+ public val Event.isClose: Boolean
+ get() = this is Shutdown
+
+ public val Event.captureSequence: FakeCaptureSequence?
+ get() =
+ when (this) {
+ is Submit -> captureSequence
+ is BuildRejected -> captureSequence
+ is SubmitRejected -> captureSequence
+ else -> null
+ }
}
}
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeFrameMetadata.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeFrameMetadata.kt
index 90465f2..f24a252 100644
--- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeFrameMetadata.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeFrameMetadata.kt
@@ -35,7 +35,7 @@
public class FakeFrameMetadata(
private val resultMetadata: Map<CaptureResult.Key<*>, Any?> = emptyMap(),
extraResultMetadata: Map<Metadata.Key<*>, Any?> = emptyMap(),
- override val camera: CameraId = nextFakeCameraId(),
+ override val camera: CameraId = FakeCameraIds.default,
override val frameNumber: FrameNumber = nextFakeFrameNumber(),
override val extraMetadata: Map<*, Any?> = emptyMap<Any, Any>()
) : FakeMetadata(extraResultMetadata), FrameMetadata {
diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/CameraPipeSimulatorTest.kt b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/CameraPipeSimulatorTest.kt
index d7373f1..f1e3fd5 100644
--- a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/CameraPipeSimulatorTest.kt
+++ b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/CameraPipeSimulatorTest.kt
@@ -36,13 +36,17 @@
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
class CameraPipeSimulatorTest {
private val testScope = TestScope()
- private val frontCameraMetadata =
- FakeCameraMetadata(
- mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_FRONT)
- )
private val backCameraMetadata =
FakeCameraMetadata(
- mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK)
+ cameraId = FakeCameraIds.next(),
+ characteristics =
+ mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK)
+ )
+ private val frontCameraMetadata =
+ FakeCameraMetadata(
+ cameraId = FakeCameraIds.next(),
+ characteristics =
+ mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_FRONT)
)
private val streamConfig = CameraStream.Config.create(Size(640, 480), StreamFormat.YUV_420_888)
diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDevicesTest.kt b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDevicesTest.kt
index d91dd8e..fed7ff7 100644
--- a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDevicesTest.kt
+++ b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDevicesTest.kt
@@ -31,22 +31,30 @@
class FakeCameraDevicesTest {
private val EXTERNAL_BACKEND_ID =
CameraBackendId("androidx.camera.camera2.pipe.testing.FakeCameraDevicesTest")
- private val metadata1 =
+ private val frontMetadata =
FakeCameraMetadata(
- mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_FRONT)
+ cameraId = FakeCameraIds.next(),
+ characteristics =
+ mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_FRONT)
)
- private val metadata2 =
+ private val backMetadata =
FakeCameraMetadata(
- mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK)
+ cameraId = FakeCameraIds.next(),
+ characteristics =
+ mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK)
)
- private val metadata3 =
+ private val extMetadata =
FakeCameraMetadata(
- mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_EXTERNAL)
+ cameraId = FakeCameraIds.next(),
+ characteristics =
+ mapOf(
+ CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_EXTERNAL
+ )
)
private val cameraMetadataMap =
mapOf(
- FAKE_CAMERA_BACKEND_ID to listOf(metadata1, metadata2),
- EXTERNAL_BACKEND_ID to listOf(metadata3)
+ FAKE_CAMERA_BACKEND_ID to listOf(frontMetadata, backMetadata),
+ EXTERNAL_BACKEND_ID to listOf(extMetadata)
)
@Test
@@ -63,11 +71,13 @@
)
val devices = cameraDevices.getCameraIds()
assertThat(devices)
- .containsExactlyElementsIn(listOf(metadata1.camera, metadata2.camera))
+ .containsExactlyElementsIn(listOf(frontMetadata.camera, backMetadata.camera))
.inOrder()
- assertThat(cameraDevices.getCameraMetadata(metadata1.camera)).isSameInstanceAs(metadata1)
- assertThat(cameraDevices.getCameraMetadata(metadata2.camera)).isSameInstanceAs(metadata2)
+ assertThat(cameraDevices.getCameraMetadata(frontMetadata.camera))
+ .isSameInstanceAs(frontMetadata)
+ assertThat(cameraDevices.getCameraMetadata(backMetadata.camera))
+ .isSameInstanceAs(backMetadata)
}
@Test
@@ -86,13 +96,13 @@
assertThat(devices)
.containsExactlyElementsIn(
listOf(
- metadata3.camera,
+ extMetadata.camera,
)
)
.inOrder()
- assertThat(cameraDevices.getCameraMetadata(metadata3.camera)).isNull()
- assertThat(cameraDevices.getCameraMetadata(metadata3.camera, EXTERNAL_BACKEND_ID))
- .isSameInstanceAs(metadata3)
+ assertThat(cameraDevices.getCameraMetadata(extMetadata.camera)).isNull()
+ assertThat(cameraDevices.getCameraMetadata(extMetadata.camera, EXTERNAL_BACKEND_ID))
+ .isSameInstanceAs(extMetadata)
}
}
diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt
index e276d6c..d418346 100644
--- a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt
+++ b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt
@@ -62,7 +62,6 @@
)
assertThat(metadata1).isNotEqualTo(metadata2)
- assertThat(metadata1.camera).isNotEqualTo(metadata2.camera)
}
@Test
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt
index d75b9f9..89467a3 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt
@@ -18,6 +18,8 @@
import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CameraExtensionCharacteristics
+import android.hardware.camera2.CameraExtensionSession
import android.hardware.camera2.CaptureFailure
import android.hardware.camera2.CaptureRequest
import android.view.Surface
@@ -107,6 +109,25 @@
) {}
/**
+ * This event provides clients with an estimate of the post-processing progress of a capture
+ * which could take significantly more time relative to the rest of the
+ * [CameraExtensionSession.capture] sequence. The callback will be triggered only by
+ * extensions that return true from calls
+ * [CameraExtensionCharacteristics.isCaptureProcessProgressAvailable]. If support for this
+ * callback is present, then clients will be notified at least once with progress value 100.
+ * The callback will be triggered only for still capture requests
+ * [CameraExtensionSession.capture] and is not supported for repeating requests
+ * [CameraExtensionSession.setRepeatingRequest].
+ *
+ * @param requestMetadata the data about the camera2 request that was sent to the camera.
+ * @param progress the value indicating the current post-processing progress (between 0 and
+ * 100 inclusive)
+ * @see
+ * android.hardware.camera2.CameraExtensionSession.ExtensionCaptureCallback.onCaptureProcessProgressed
+ */
+ public fun onCaptureProgress(requestMetadata: RequestMetadata, progress: Int) {}
+
+ /**
* This event indicates that all of the metadata associated with this frame has been
* produced. If [onPartialCaptureResult] was invoked, the values returned in the
* totalCaptureResult map be a superset of the values produced from the
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureCallback.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureCallback.kt
index 9d10ac0..ebc244d 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureCallback.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureCallback.kt
@@ -46,6 +46,8 @@
frameNumber: FrameNumber
)
+ fun onCaptureProcessProgressed(captureRequest: CaptureRequest, progress: Int)
+
fun onCaptureFailed(captureRequest: CaptureRequest, frameNumber: FrameNumber)
fun onCaptureSequenceCompleted(captureSequenceId: Int, captureFrameNumber: Long)
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt
index 34a0a1d..cc5e292 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt
@@ -184,6 +184,15 @@
Debug.traceStop() // onCaptureCompleted
}
+ override fun onCaptureProcessProgressed(captureRequest: CaptureRequest, progress: Int) {
+ Debug.traceStart { "onCaptureProcessProgressed" }
+ // Load the request and throw if we are not able to find an associated request. Under
+ // normal circumstances this should never happen.
+ val request = readRequestMetadata(captureRequest)
+ invokeOnRequest(request) { it.onCaptureProgress(request, progress) }
+ Debug.traceStop()
+ }
+
override fun onCaptureFailed(
captureSession: CameraCaptureSession,
captureRequest: CaptureRequest,
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExtensionSessionWrapper.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExtensionSessionWrapper.kt
index 7fb4d94..608dc0f 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExtensionSessionWrapper.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExtensionSessionWrapper.kt
@@ -278,6 +278,14 @@
request: CaptureRequest
) {}
+ override fun onCaptureProcessProgressed(
+ session: CameraExtensionSession,
+ request: CaptureRequest,
+ progress: Int
+ ) {
+ captureCallback.onCaptureProcessProgressed(request, progress)
+ }
+
override fun onCaptureFailed(session: CameraExtensionSession, request: CaptureRequest) {
val frameNumber = frameQueue.remove()
captureCallback.onCaptureFailed(request, FrameNumber(frameNumber))
@@ -342,6 +350,14 @@
}
}
+ override fun onCaptureProcessProgressed(
+ session: CameraExtensionSession,
+ request: CaptureRequest,
+ progress: Int
+ ) {
+ captureCallback.onCaptureProcessProgressed(request, progress)
+ }
+
override fun onCaptureSequenceCompleted(session: CameraExtensionSession, sequenceId: Int) {
val frameNumber = extensionSessionMap[session]
captureCallback.onCaptureSequenceCompleted(sequenceId, frameNumber!!)
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphLoop.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphLoop.kt
index 80ce6b0f..cc5d57a 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphLoop.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphLoop.kt
@@ -373,8 +373,8 @@
if (success) {
lastRepeatingRequest = command.request
commands.removeAt(idx)
+ commands.removeUpTo(idx) { it is StartRepeating }
}
- commands.removeUpTo(idx) { it is StartRepeating }
}
is SubmitCapture -> {
if (!_captureProcessingEnabled.value) {
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
index 4b35f50..a9e597f 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
@@ -26,6 +26,7 @@
import androidx.camera.camera2.pipe.RequestNumber
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requiredParameters
import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
@@ -477,14 +478,20 @@
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
// We now check if the correct sequence of requests were submitted by unlock3APostCapture
- // call. There should be a request to cancel AF and AE precapture metering.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
+ // call. There should be a request to cancel AF and AE precapture metering
+ val event1 = captureSequenceProcessor.nextEvent()
if (cancelAf) {
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+ assertThat(event1.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+ )
}
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL)
+ assertThat(event1.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL
+ )
}
private fun testUnlock3APostCaptureAndroidLAndBelow(cancelAf: Boolean = true) = runTest {
@@ -509,31 +516,37 @@
// We now check if the correct sequence of requests were submitted by unlock3APostCapture
// call. There should be a request to cancel AF and lock ae.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
+ val event1 = captureSequenceProcessor.nextEvent()
if (cancelAf) {
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+ assertThat(event1.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+ )
}
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+
+ assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
// Then another request to unlock ae.
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(false)
+ val captureSequence2 = captureSequenceProcessor.nextEvent()
+ assertThat(captureSequence2.requiredParameters)
+ .containsEntry(CaptureRequest.CONTROL_AE_LOCK, false)
}
- private suspend fun assertCorrectCaptureSequenceInLock3AForCapture(
- isAfTriggered: Boolean = true
- ) {
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).apply {
+ private fun assertCorrectCaptureSequenceInLock3AForCapture(isAfTriggered: Boolean = true) {
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).apply {
if (isAfTriggered) {
isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
} else {
isNotEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_IDLE)
}
}
- assertThat(request1.requiredParameters[CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START)
+ assertThat(event1.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START
+ )
}
companion object {
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt
index 3085cc7..e9be1c9 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt
@@ -27,6 +27,10 @@
import androidx.camera.camera2.pipe.RequestNumber
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isCapture
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isRepeating
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requests
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requiredParameters
import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
@@ -147,14 +151,17 @@
// We not check if the correct sequence of requests were submitted by lock3A call. The
// request should be a repeating request to lock AE.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
// The second request should be a single request to lock AF.
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
- assertThat(request2.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event2 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
+ assertThat(event2.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
}
@Test
@@ -199,10 +206,12 @@
// Check the correctness of the requests submitted by lock3A.
// One repeating request was sent to monitor the state of AE to get converged.
- captureSequenceProcessor.nextEvent().requestSequence
- // Once AE is converged, another repeatingrequest is sent to lock AE.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.isRepeating).isTrue()
+
+ // Once AE is converged, another repeating request is sent to lock AE.
+ val event2 = captureSequenceProcessor.nextEvent()
+ assertThat(event2.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
globalScope.launch {
listener3A.onRequestSequenceCreated(
@@ -228,9 +237,12 @@
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
// A single request to lock AF must have been used as well.
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
+ val event3 = captureSequenceProcessor.nextEvent()
+ assertThat(event3.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
globalScope.cancel()
}
@@ -275,8 +287,8 @@
// For a new AE scan we first send a request to unlock AE just in case it was
// previously or internally locked.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(false)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, false)
globalScope.launch {
listener3A.onRequestSequenceCreated(
@@ -302,14 +314,17 @@
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
// There should be one more request to lock AE after new scan is done.
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event2 = captureSequenceProcessor.nextEvent()
+ assertThat(event2.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
// And one request to lock AF.
- val request3 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
- assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event3 = captureSequenceProcessor.nextEvent()
+ assertThat(event3.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
+ assertThat(event3.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
globalScope.cancel()
}
@@ -378,14 +393,17 @@
// There should be one request to monitor AF to finish it's scan.
captureSequenceProcessor.nextEvent()
// One request to lock AE
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event2 = captureSequenceProcessor.nextEvent()
+ assertThat(event2.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
// And one request to lock AF.
- val request3 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
- assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event3 = captureSequenceProcessor.nextEvent()
+ assertThat(event3.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
+ assertThat(event3.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
globalScope.cancel()
}
@@ -451,21 +469,27 @@
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
// One request to cancel AF to start a new scan.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+ )
// There should be one request to monitor AF to finish it's scan.
captureSequenceProcessor.nextEvent()
// There should be one request to monitor lock AE.
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event2 = captureSequenceProcessor.nextEvent()
+ assertThat(event2.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
// And one request to lock AF.
- val request3 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
- assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event3 = captureSequenceProcessor.nextEvent()
+ assertThat(event3.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
+ assertThat(event3.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
globalScope.cancel()
}
@@ -532,29 +556,26 @@
// There should be one request to monitor AF to finish it's scan.
val event = captureSequenceProcessor.nextEvent()
- assertThat(event.requestSequence!!.repeating).isTrue()
- assertThat(event.rejected).isFalse()
- assertThat(event.abort).isFalse()
- assertThat(event.close).isFalse()
- assertThat(event.submit).isTrue()
+ assertThat(event.isRepeating).isTrue()
- // One request to lock AE
+ // One request to lock AE (Repeating)
val request2Event = captureSequenceProcessor.nextEvent()
- assertThat(request2Event.requestSequence!!.repeating).isTrue()
- assertThat(request2Event.submit).isTrue()
- val request2 = request2Event.requestSequence!!
- assertThat(request2).isNotNull()
- assertThat(request2.requiredParameters).isNotEmpty()
- assertThat(request2.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ assertThat(request2Event.isRepeating).isTrue()
+ assertThat(request2Event.requests.size).isEqualTo(1)
+ assertThat(request2Event.requiredParameters)
+ .containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
// And one request to lock AF.
val request3Event = captureSequenceProcessor.nextEvent()
- assertThat(request3Event.requestSequence!!.repeating).isFalse()
- assertThat(request3Event.submit).isTrue()
- val request3 = request3Event.requestSequence!!
- assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
- assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ assertThat(request3Event.isCapture).isTrue()
+ assertThat(request3Event.requests.size).isEqualTo(1)
+ assertThat(request3Event.requiredParameters)
+ .containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
+ assertThat(request3Event.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
globalScope.cancel()
}
@@ -620,22 +641,29 @@
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
// One request to cancel AF to start a new scan.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+ )
+
// There should be one request to unlock AE and monitor the current AF scan to finish.
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(false)
+ val event2 = captureSequenceProcessor.nextEvent()
+ assertThat(event2.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, false)
// There should be one request to monitor lock AE.
- val request3 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event3 = captureSequenceProcessor.nextEvent()
+ assertThat(event3.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
// And one request to lock AF.
- val request4 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request4!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
- assertThat(request4.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event4 = captureSequenceProcessor.nextEvent()
+ assertThat(event4.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
+ assertThat(event4.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
globalScope.cancel()
}
@@ -713,14 +741,17 @@
// We not check if the correct sequence of requests were submitted by lock3A call. The
// request should be a repeating request to lock AE.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
// The second request should be a single request to lock AF.
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START)
- assertThat(request2.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(true)
+ val event2 = captureSequenceProcessor.nextEvent()
+ assertThat(event2.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
+ assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
}
@Test
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt
index e70b7a7..9305a51 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt
@@ -25,6 +25,7 @@
import androidx.camera.camera2.pipe.RequestNumber
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requiredParameters
import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
@@ -104,8 +105,8 @@
assertThat(result.isCompleted).isFalse()
// There should be one request to lock AE.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(false)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, false)
repeatingJob.cancel()
repeatingJob.join()
@@ -164,9 +165,12 @@
assertThat(result.isCompleted).isFalse()
// There should be one request to unlock AF.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+ )
repeatingJob.cancel()
repeatingJob.join()
@@ -225,8 +229,8 @@
assertThat(result.isCompleted).isFalse()
// There should be one request to lock AWB.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AWB_LOCK]).isEqualTo(false)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AWB_LOCK, false)
repeatingJob.cancel()
repeatingJob.join()
@@ -287,12 +291,15 @@
assertThat(result.isCompleted).isFalse()
// There should be one request to unlock AF.
- val request1 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
- .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+ val event1 = captureSequenceProcessor.nextEvent()
+ assertThat(event1.requiredParameters)
+ .containsEntry(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+ )
// Then request to unlock AE.
- val request2 = captureSequenceProcessor.nextEvent().requestSequence
- assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(false)
+ val event2 = captureSequenceProcessor.nextEvent()
+ assertThat(event2.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, false)
repeatingJob.cancel()
repeatingJob.join()
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphLoopTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphLoopTest.kt
index 728d859..711da09 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphLoopTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphLoopTest.kt
@@ -17,21 +17,25 @@
package androidx.camera.camera2.pipe.graph
import android.os.Build
-import android.view.Surface
import androidx.camera.camera2.pipe.CameraGraphId
-import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.CaptureSequence
-import androidx.camera.camera2.pipe.CaptureSequenceProcessor
import androidx.camera.camera2.pipe.Request
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.StreamId
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
-import androidx.camera.camera2.pipe.testing.FakeCaptureSequence
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.defaultParameters
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isAbort
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isCapture
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isClose
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isRejected
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isRepeating
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isStopRepeating
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requests
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requiredParameters
import androidx.camera.camera2.pipe.testing.FakeMetadata.Companion.TEST_KEY
import androidx.camera.camera2.pipe.testing.FakeSurfaces
import androidx.testutils.assertThrows
import com.google.common.truth.Truth.assertThat
-import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
@@ -73,8 +77,10 @@
stream2 to fakeSurfaces.createFakeSurface()
)
- private val csp1 = SimpleCSP(fakeCameraId, surfaceMap)
- private val csp2 = SimpleCSP(fakeCameraId, surfaceMap)
+ private val csp1 =
+ FakeCaptureSequenceProcessor(fakeCameraId).also { it.surfaceMap = surfaceMap }
+ private val csp2 =
+ FakeCaptureSequenceProcessor(fakeCameraId).also { it.surfaceMap = surfaceMap }
private val grp1 = GraphRequestProcessor.from(csp1)
private val grp2 = GraphRequestProcessor.from(csp2)
@@ -823,90 +829,32 @@
assertThat(csp2.events[0].isClose).isTrue()
}
- private val SimpleCSP.SimpleCSPEvent.requests: List<Request>
- get() = (this as SimpleCSP.Submit).captureSequence.captureRequestList
+ @Test
+ fun settingRepeatingRequestWhenRequestsAreRejectedDoesNotAttemptMultipleRepeatingRequests() =
+ testScope.runTest {
+ // Arrange
+ csp1.rejectSubmit = true
+ graphLoop.requestProcessor = grp1
+ graphLoop.repeatingRequest = request1
+ advanceUntilIdle()
- private val SimpleCSP.SimpleCSPEvent.requiredParameters: Map<*, Any?>
- get() = (this as SimpleCSP.Submit).captureSequence.requiredParameters
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isRejected).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
- private val SimpleCSP.SimpleCSPEvent.defaultParameters: Map<*, Any?>
- get() = (this as SimpleCSP.Submit).captureSequence.defaultParameters
+ graphLoop.repeatingRequest = request2
+ advanceUntilIdle()
- private val SimpleCSP.SimpleCSPEvent.isRepeating: Boolean
- get() = (this as? SimpleCSP.Submit)?.captureSequence?.repeating ?: false
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[1].isRejected).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request2)
- private val SimpleCSP.SimpleCSPEvent.isCapture: Boolean
- get() = (this as? SimpleCSP.Submit)?.captureSequence?.repeating == false
+ csp1.rejectSubmit = false
+ graphLoop.invalidate()
+ advanceUntilIdle()
- private val SimpleCSP.SimpleCSPEvent.isAbort: Boolean
- get() = this is SimpleCSP.AbortCaptures
-
- private val SimpleCSP.SimpleCSPEvent.isStopRepeating: Boolean
- get() = this is SimpleCSP.StopRepeating
-
- private val SimpleCSP.SimpleCSPEvent.isClose: Boolean
- get() = this is SimpleCSP.Close
-
- internal class SimpleCSP(
- private val cameraId: CameraId,
- private val surfaceMap: Map<StreamId, Surface>
- ) : CaptureSequenceProcessor<Request, FakeCaptureSequence> {
- val events = mutableListOf<SimpleCSPEvent>()
- var throwOnBuild = false
- private var closed = false
- private val sequenceIds = atomic(0)
-
- override fun build(
- isRepeating: Boolean,
- requests: List<Request>,
- defaultParameters: Map<*, Any?>,
- requiredParameters: Map<*, Any?>,
- listeners: List<Request.Listener>,
- sequenceListener: CaptureSequence.CaptureSequenceListener
- ): FakeCaptureSequence? {
- if (closed) return null
- if (throwOnBuild) throw RuntimeException("Test Exception")
- return FakeCaptureSequence.create(
- cameraId = cameraId,
- repeating = isRepeating,
- requests = requests,
- surfaceMap = surfaceMap,
- defaultParameters = defaultParameters,
- requiredParameters = requiredParameters,
- listeners = listeners,
- sequenceListener = sequenceListener
- )
+ assertThat(csp1.events.size).isEqualTo(3)
+ assertThat(csp1.events[2].isRepeating).isTrue()
+ assertThat(csp1.events[2].requests).containsExactly(request2)
}
-
- override fun abortCaptures() {
- events.add(AbortCaptures)
- }
-
- override fun stopRepeating() {
- events.add(StopRepeating)
- }
-
- override suspend fun shutdown() {
- closed = true
- events.add(Close)
- }
-
- override fun submit(captureSequence: FakeCaptureSequence): Int? {
- if (!closed) {
- events.add(Submit(captureSequence))
- return sequenceIds.incrementAndGet()
- }
- return null
- }
-
- sealed class SimpleCSPEvent
-
- object Close : SimpleCSPEvent()
-
- object StopRepeating : SimpleCSPEvent()
-
- object AbortCaptures : SimpleCSPEvent()
-
- data class Submit(val captureSequence: FakeCaptureSequence) : SimpleCSPEvent()
- }
}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
index 13e29b9..c648043 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
@@ -28,7 +28,12 @@
import androidx.camera.camera2.pipe.Request
import androidx.camera.camera2.pipe.StreamId
import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor
-import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.awaitEvent
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isCapture
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isClose
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isRejected
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.isRepeating
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requests
+import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceProcessor.Companion.requiredParameters
import androidx.camera.camera2.pipe.testing.FakeGraphConfigs
import androidx.camera.camera2.pipe.testing.FakeRequestListener
import androidx.camera.camera2.pipe.testing.FakeThreads
@@ -36,10 +41,9 @@
import androidx.testutils.assertThrows
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withTimeoutOrNull
@@ -52,17 +56,20 @@
@RunWith(RobolectricCameraPipeTestRunner::class)
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
internal class GraphProcessorTest {
+ private val testScope = TestScope()
+ private val fakeThreads = FakeThreads.fromTestScope(testScope)
+
private val globalListener = FakeRequestListener()
private val graphState3A = GraphState3A()
private val graphListener3A = Listener3A()
private val streamId = StreamId(0)
private val surfaceMap = mapOf(streamId to Surface(SurfaceTexture(1)))
- private val fakeProcessor1 = FakeCaptureSequenceProcessor().also { it.surfaceMap = surfaceMap }
- private val fakeProcessor2 = FakeCaptureSequenceProcessor().also { it.surfaceMap = surfaceMap }
+ private val csp1 = FakeCaptureSequenceProcessor().also { it.surfaceMap = surfaceMap }
+ private val csp2 = FakeCaptureSequenceProcessor().also { it.surfaceMap = surfaceMap }
- private val graphRequestProcessor1 = GraphRequestProcessor.from(fakeProcessor1)
- private val graphRequestProcessor2 = GraphRequestProcessor.from(fakeProcessor2)
+ private val grp1 = GraphRequestProcessor.from(csp1)
+ private val grp2 = GraphRequestProcessor.from(csp2)
private val requestListener1 = FakeRequestListener()
private val request1 = Request(listOf(StreamId(0)), listeners = listOf(requestListener1))
@@ -70,516 +77,375 @@
private val requestListener2 = FakeRequestListener()
private val request2 = Request(listOf(StreamId(0)), listeners = listOf(requestListener2))
+ private val graphProcessor =
+ GraphProcessorImpl(
+ fakeThreads,
+ CameraGraphId.nextId(),
+ FakeGraphConfigs.graphConfig,
+ graphState3A,
+ graphListener3A,
+ arrayListOf(globalListener)
+ )
+
@After
fun teardown() {
surfaceMap[streamId]?.release()
}
@Test
- fun graphProcessorSubmitsRequests() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.submit(request1)
- advanceUntilIdle()
+ fun graphProcessorSubmitsRequests() =
+ testScope.runTest {
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.submit(request1)
+ advanceUntilIdle()
- // Make sure the requests get submitted to the request processor
- val event = fakeProcessor1.nextEvent()
- assertThat(event.requestSequence!!.captureRequestList).containsExactly(request1)
- assertThat(event.requestSequence!!.requiredParameters)
- .containsEntry(CaptureRequest.JPEG_THUMBNAIL_QUALITY, 42)
- }
+ // Make sure the requests get submitted to the request processor
+ assertThat(csp1.events.size).isEqualTo(1)
- @Test
- fun graphProcessorSubmitsRequestsToMostRecentProcessor() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
-
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.onGraphStarted(graphRequestProcessor2)
- graphProcessor.submit(request1)
-
- val event1 = fakeProcessor1.nextEvent()
- assertThat(event1.close).isTrue()
-
- val event2 = fakeProcessor2.nextEvent()
- assertThat(event2.submit).isTrue()
- assertThat(event2.requestSequence!!.captureRequestList).containsExactly(request1)
- }
-
- @Test
- fun graphProcessorSubmitsQueuedRequests() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
-
- graphProcessor.submit(request1)
- graphProcessor.submit(request2)
-
- // Request1 and 2 should be queued and will be submitted even when the request
- // processor is set after the requests are submitted.
- graphProcessor.onGraphStarted(graphRequestProcessor1)
-
- val event1 = fakeProcessor1.awaitEvent(request = request1) { it.submit }
- assertThat(event1.requestSequence!!.captureRequestList).hasSize(1)
- assertThat(event1.requestSequence!!.captureRequestList).contains(request1)
-
- val event2 = fakeProcessor1.nextEvent()
- assertThat(event2.requestSequence!!.captureRequestList).hasSize(1)
- assertThat(event2.requestSequence!!.captureRequestList).contains(request2)
- }
-
- @Test
- fun graphProcessorSubmitsBurstsOfRequestsTogetherWithExtras() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
-
- graphProcessor.submit(listOf(request1, request2))
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- val event = fakeProcessor1.awaitEvent(request = request1) { it.submit }
- assertThat(event.requestSequence!!.captureRequestList).hasSize(2)
- assertThat(event.requestSequence!!.captureRequestList).contains(request1)
- assertThat(event.requestSequence!!.captureRequestList).contains(request2)
- }
-
- @Test
- fun graphProcessorDoesNotForgetRejectedRequests() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
-
- fakeProcessor1.rejectRequests = true
- graphProcessor.onGraphStarted(graphRequestProcessor1)
-
- graphProcessor.submit(request1)
- val event1 = fakeProcessor1.nextEvent()
- assertThat(event1.rejected).isTrue()
- assertThat(event1.requestSequence!!.captureRequestList[0]).isSameInstanceAs(request1)
-
- graphProcessor.submit(request2)
- val event2 = fakeProcessor1.nextEvent()
- assertThat(event2.rejected).isTrue()
- assertThat(event2.requestSequence!!.captureRequestList[0]).isSameInstanceAs(request1)
-
- graphProcessor.onGraphStarted(graphRequestProcessor2)
- assertThat(fakeProcessor2.nextEvent().requestSequence!!.captureRequestList[0])
- .isSameInstanceAs(request1)
- assertThat(fakeProcessor2.nextEvent().requestSequence!!.captureRequestList[0])
- .isSameInstanceAs(request2)
- }
-
- @Test
- fun graphProcessorContinuesSubmittingRequestsWhenFirstRequestIsRejected() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
-
- // Note: setting the requestProcessor, and calling submit() can both trigger a call
- // to submit a request.
- fakeProcessor1.rejectRequests = true
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.submit(request1)
-
- // Check to make sure that submit is called at least once, and that request1 is rejected
- // from the request processor.
- fakeProcessor1.awaitEvent(request = request1) { it.rejected }
-
- // Stop rejecting requests
- fakeProcessor1.rejectRequests = false
-
- graphProcessor.submit(request2)
- // Cycle events until we get a submitted event with request1
- val event2 = fakeProcessor1.awaitEvent(request = request1) { it.submit }
- assertThat(event2.rejected).isFalse()
-
- // Assert that immediately after we get a successfully submitted request, the
- // next request is also submitted.
- val event3 = fakeProcessor1.nextEvent()
- assertThat(event3.requestSequence!!.captureRequestList).contains(request2)
- assertThat(event3.submit).isTrue()
- assertThat(event3.rejected).isFalse()
- }
-
- @Test
- fun graphProcessorSetsRepeatingRequest() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
-
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.repeatingRequest = request1
- graphProcessor.repeatingRequest = request2
- advanceUntilIdle()
-
- val event =
- fakeProcessor1.awaitEvent(request = request2) {
- it.submit && it.requestSequence?.repeating == true
- }
- assertThat(event.requestSequence!!.requiredParameters)
- .containsEntry(CaptureRequest.JPEG_THUMBNAIL_QUALITY, 42)
- }
-
- @Test
- fun graphProcessorDoesNotForgetRejectedRepeatingRequests() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
-
- fakeProcessor1.rejectRequests = true
- graphProcessor.onGraphStarted(graphRequestProcessor1)
-
- graphProcessor.repeatingRequest = request1
- val event1 = fakeProcessor1.nextEvent()
- assertThat(event1.rejected).isTrue()
- assertThat(event1.requestSequence!!.captureRequestList[0]).isSameInstanceAs(request1)
-
- graphProcessor.repeatingRequest = request2
- val event2 = fakeProcessor1.nextEvent()
- assertThat(event2.rejected).isTrue()
- fakeProcessor1.awaitEvent(request = request2) {
- !it.submit && it.requestSequence?.repeating == true
+ assertThat(csp1.events[0].isCapture).isTrue()
+ assertThat(csp1.events[0].requiredParameters)
+ .containsEntry(CaptureRequest.JPEG_THUMBNAIL_QUALITY, 42)
}
- fakeProcessor1.rejectRequests = false
- graphProcessor.invalidate()
-
- fakeProcessor1.awaitEvent(request = request2) {
- it.submit && it.requestSequence?.repeating == true
- }
- }
-
@Test
- fun graphProcessorTracksRepeatingRequest() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
+ fun graphProcessorSubmitsRequestsToMostRecentProcessor() =
+ testScope.runTest {
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.onGraphStarted(grp2)
+ graphProcessor.submit(request1)
+ advanceUntilIdle()
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.repeatingRequest = request1
- advanceUntilIdle()
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isClose).isTrue()
- fakeProcessor1.awaitEvent(request = request1) {
- it.submit && it.requestSequence?.repeating == true
+ assertThat(csp2.events.size).isEqualTo(1)
+ assertThat(csp2.events[0].isCapture).isTrue()
+ assertThat(csp2.events[0].requests).containsExactly(request1)
}
- graphProcessor.onGraphStarted(graphRequestProcessor2)
- advanceUntilIdle()
+ @Test
+ fun graphProcessorSubmitsQueuedRequests() =
+ testScope.runTest {
+ graphProcessor.submit(request1)
+ graphProcessor.submit(request2)
- fakeProcessor2.awaitEvent(request = request1) {
- it.submit && it.requestSequence?.repeating == true
+ // Request1 and 2 should be queued and will be submitted even when the request
+ // processor is set after the requests are submitted.
+ graphProcessor.onGraphStarted(grp1)
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[0].isCapture).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
+
+ assertThat(csp1.events[1].isCapture).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request2)
}
- }
@Test
- fun graphProcessorTracksRejectedRepeatingRequests() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
+ fun graphProcessorSubmitsBurstsOfRequestsTogetherWithExtras() =
+ testScope.runTest {
+ graphProcessor.submit(listOf(request1, request2))
+ graphProcessor.onGraphStarted(grp1)
+ advanceUntilIdle()
- fakeProcessor1.rejectRequests = true
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.repeatingRequest = request1
- fakeProcessor1.awaitEvent(request = request1) { it.rejected }
-
- graphProcessor.onGraphStarted(graphRequestProcessor2)
- fakeProcessor2.awaitEvent(request = request1) {
- it.submit && it.requestSequence?.repeating == true
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isCapture).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1, request2).inOrder()
}
- }
@Test
- fun graphProcessorSubmitsRepeatingRequestAndQueuedRequests() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
+ fun graphProcessorDoesNotForgetRejectedRequests() =
+ testScope.runTest {
+ csp1.rejectSubmit = true
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.submit(request1)
+ advanceUntilIdle()
- graphProcessor.repeatingRequest = request1
- graphProcessor.submit(request2)
- delay(50)
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isRejected).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
- graphProcessor.onGraphStarted(graphRequestProcessor1)
+ graphProcessor.submit(request2)
+ advanceUntilIdle()
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[1].isRejected).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request1) // Re-attempt #1
- var hasRequest1Event = false
- var hasRequest2Event = false
+ graphProcessor.onGraphStarted(grp2)
+ advanceUntilIdle()
- // Loop until we see at least one repeating request, and one submit event.
- launch {
- while (!hasRequest1Event && !hasRequest2Event) {
- val event = fakeProcessor1.nextEvent()
- hasRequest1Event =
- hasRequest1Event ||
- event.requestSequence?.captureRequestList?.contains(request1) ?: false
- hasRequest2Event =
- hasRequest2Event ||
- event.requestSequence?.captureRequestList?.contains(request2) ?: false
- }
- }
- .join()
- }
+ // Assert that after a new request processor is set, it receives the queued up requests.
+ assertThat(csp2.events.size).isEqualTo(2)
+ assertThat(csp2.events[0].isCapture).isTrue()
+ assertThat(csp2.events[0].requests).containsExactly(request1)
+ assertThat(csp2.events[1].isCapture).isTrue()
+ assertThat(csp2.events[1].requests).containsExactly(request2).inOrder()
+ }
@Test
- fun graphProcessorAbortsQueuedRequests() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
+ fun graphProcessorContinuesSubmittingRequestsWhenFirstRequestIsRejected() =
+ testScope.runTest {
- graphProcessor.repeatingRequest = request1
- graphProcessor.submit(request2)
+ // Note: setting the requestProcessor, and calling submit() can both trigger a call
+ // to submit a request.
+ csp1.rejectSubmit = true
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.submit(request1)
+ advanceUntilIdle()
- // Abort queued and in-flight requests.
- graphProcessor.abort()
- graphProcessor.onGraphStarted(graphRequestProcessor1)
+ // Check to make sure that submit is called at least once, and that request1 is rejected
+ // from the request processor.
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isRejected).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
- val abortEvent1 =
- withTimeoutOrNull(timeMillis = 50L) { requestListener1.onAbortedFlow.firstOrNull() }
- val abortEvent2 = requestListener2.onAbortedFlow.first()
- val globalAbortEvent = globalListener.onAbortedFlow.first()
+ // Stop rejecting requests
+ csp1.rejectSubmit = false
- assertThat(abortEvent1).isNull()
- assertThat(abortEvent2.request).isSameInstanceAs(request2)
- assertThat(globalAbortEvent.request).isSameInstanceAs(request2)
+ graphProcessor.submit(request2)
+ advanceUntilIdle()
- val nextSequence = fakeProcessor1.nextRequestSequence()
- assertThat(nextSequence.captureRequestList.first()).isSameInstanceAs(request1)
- assertThat(nextSequence.requestMetadata[request1]!!.repeating).isTrue()
- }
+ // Assert that immediately after we get a successfully submitted request, the
+ // next request is also submitted.
+ assertThat(csp1.events.size).isEqualTo(3)
+ assertThat(csp1.events[1].isCapture).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request1)
+ assertThat(csp1.events[2].isCapture).isTrue()
+ assertThat(csp1.events[2].requests).containsExactly(request2)
+ }
@Test
- fun closingGraphProcessorAbortsSubsequentRequests() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
- graphProcessor.close()
- advanceUntilIdle()
+ fun graphProcessorSetsRepeatingRequest() =
+ testScope.runTest {
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.repeatingRequest = request1
+ graphProcessor.repeatingRequest = request2
+ advanceUntilIdle()
- // Abort queued and in-flight requests.
- // graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.repeatingRequest = request1
- graphProcessor.submit(request2)
-
- val abortEvent1 =
- withTimeoutOrNull(timeMillis = 50L) { requestListener1.onAbortedFlow.firstOrNull() }
- val abortEvent2 = requestListener2.onAbortedFlow.first()
- assertThat(abortEvent1).isNull()
- assertThat(abortEvent2.request).isSameInstanceAs(request2)
- }
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request2)
+ assertThat(csp1.events[0].requiredParameters)
+ .containsEntry(CaptureRequest.JPEG_THUMBNAIL_QUALITY, 42)
+ }
@Test
- fun graphProcessorResubmitsParametersAfterGraphStarts() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
+ fun graphProcessorDoesNotForgetRejectedRepeatingRequests() =
+ testScope.runTest {
+ csp1.rejectSubmit = true
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.repeatingRequest = request1
+ advanceUntilIdle()
- // Submit a repeating request first to make sure we have one in progress.
- graphProcessor.repeatingRequest = request1
- advanceUntilIdle()
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isRejected).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
- graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
- advanceUntilIdle()
+ graphProcessor.repeatingRequest = request2
+ advanceUntilIdle()
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- advanceUntilIdle()
- val event1 = fakeProcessor1.nextEvent()
- assertThat(event1.requestSequence?.repeating).isTrue()
- val event2 = fakeProcessor1.nextEvent()
- assertThat(event2.requestSequence?.repeating).isFalse()
- assertThat(event2.requestSequence?.requestMetadata?.get(request1)?.get(CONTROL_AE_LOCK))
- .isFalse()
- }
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[1].isRejected).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request2)
+
+ csp1.rejectSubmit = false
+ graphProcessor.invalidate()
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(3)
+ assertThat(csp1.events[2].isRepeating).isTrue()
+ assertThat(csp1.events[2].requests).containsExactly(request2)
+ }
@Test
- fun graphProcessorSubmitsLatestParametersWhenSubmittedTwiceBeforeGraphStarts() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
+ fun graphProcessorTracksRepeatingRequest() =
+ testScope.runTest {
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.repeatingRequest = request1
+ advanceUntilIdle()
- // Submit a repeating request first to make sure we have one in progress.
- graphProcessor.repeatingRequest = request1
- graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
- graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to true))
- advanceUntilIdle()
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- advanceUntilIdle()
+ graphProcessor.onGraphStarted(grp2)
+ advanceUntilIdle()
- val event1 = fakeProcessor1.nextEvent()
- assertThat(event1.requestSequence?.repeating).isTrue()
- val event2 = fakeProcessor1.nextEvent()
- assertThat(event2.requestSequence?.repeating).isFalse()
- assertThat(event2.requestSequence?.requestMetadata?.get(request1)?.get(CONTROL_AE_LOCK))
- .isFalse()
- val event3 = fakeProcessor1.nextEvent()
- assertThat(event3.requestSequence?.repeating).isFalse()
- assertThat(event3.requestSequence?.requestMetadata?.get(request1)?.get(CONTROL_AE_LOCK))
- .isTrue()
- }
+ assertThat(csp2.events.size).isEqualTo(1)
+ assertThat(csp2.events[0].isRepeating).isTrue()
+ assertThat(csp2.events[0].requests).containsExactly(request1)
+ }
@Test
- fun trySubmitShouldReturnFalseWhenNoRepeatingRequestIsQueued() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
+ fun graphProcessorTracksRejectedRepeatingRequests() =
+ testScope.runTest {
+ csp1.rejectSubmit = true
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.repeatingRequest = request1
+ advanceUntilIdle()
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- advanceUntilIdle()
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isRejected).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
- assertThrows<IllegalStateException> {
+ graphProcessor.onGraphStarted(grp2)
+ advanceUntilIdle()
+
+ assertThat(csp2.events.size).isEqualTo(1)
+ assertThat(csp2.events[0].isRepeating).isTrue()
+ assertThat(csp2.events[0].requests).containsExactly(request1)
+ }
+
+ @Test
+ fun graphProcessorSubmitsRepeatingRequestAndQueuedRequests() =
+ testScope.runTest {
+ graphProcessor.repeatingRequest = request1
+ graphProcessor.submit(request2)
+ advanceUntilIdle()
+
+ graphProcessor.onGraphStarted(grp1)
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
+ assertThat(csp1.events[1].isCapture).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request2)
+ }
+
+ @Test
+ fun graphProcessorAbortsQueuedRequests() =
+ testScope.runTest {
+ graphProcessor.repeatingRequest = request1
+ graphProcessor.submit(request2)
+
+ // Abort queued and in-flight requests.
+ graphProcessor.abort()
+ graphProcessor.onGraphStarted(grp1)
+
+ val abortEvent1 = requestListener2.onAbortedFlow.first()
+ val globalAbortEvent = globalListener.onAbortedFlow.first()
+
+ assertThat(abortEvent1.request).isSameInstanceAs(request2)
+ assertThat(globalAbortEvent.request).isSameInstanceAs(request2)
+
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(1)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
+ }
+
+ @Test
+ fun closingGraphProcessorAbortsSubsequentRequests() =
+ testScope.runTest {
+ graphProcessor.close()
+ advanceUntilIdle()
+
+ // Abort queued and in-flight requests.
+ // graphProcessor.onGraphStarted(graphRequestProcessor1)
+ graphProcessor.repeatingRequest = request1
+ graphProcessor.submit(request2)
+
+ val abortEvent1 =
+ withTimeoutOrNull(timeMillis = 50L) { requestListener1.onAbortedFlow.firstOrNull() }
+ val abortEvent2 = requestListener2.onAbortedFlow.first()
+ assertThat(abortEvent1).isNull()
+ assertThat(abortEvent2.request).isSameInstanceAs(request2)
+ }
+
+ @Test
+ fun graphProcessorResubmitsParametersAfterGraphStarts() =
+ testScope.runTest {
+ // Submit a repeating request first to make sure we have one in progress.
+ graphProcessor.repeatingRequest = request1
+ graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
+ graphProcessor.onGraphStarted(grp1)
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(2)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
+ assertThat(csp1.events[1].isCapture).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request1)
+ assertThat(csp1.events[1].requiredParameters).containsEntry(CONTROL_AE_LOCK, false)
+ }
+
+ @Test
+ fun graphProcessorSubmitsLatestParametersWhenSubmittedTwiceBeforeGraphStarts() =
+ testScope.runTest {
+
+ // Submit a repeating request first to make sure we have one in progress.
+ graphProcessor.repeatingRequest = request1
+ graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to true))
+ advanceUntilIdle()
+
+ graphProcessor.onGraphStarted(grp1)
+ advanceUntilIdle()
+
+ assertThat(csp1.events.size).isEqualTo(3)
+ assertThat(csp1.events[0].isRepeating).isTrue()
+ assertThat(csp1.events[0].requests).containsExactly(request1)
+
+ assertThat(csp1.events[1].isCapture).isTrue()
+ assertThat(csp1.events[1].requests).containsExactly(request1)
+ assertThat(csp1.events[1].requiredParameters).containsEntry(CONTROL_AE_LOCK, false)
+
+ assertThat(csp1.events[2].isCapture).isTrue()
+ assertThat(csp1.events[2].requests).containsExactly(request1)
+ assertThat(csp1.events[2].requiredParameters).containsEntry(CONTROL_AE_LOCK, true)
}
- }
@Test
- fun graphProcessorChangesGraphStateOnError() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
- )
- assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+ fun trySubmitShouldReturnFalseWhenNoRepeatingRequestIsQueued() =
+ testScope.runTest {
+ graphProcessor.onGraphStarted(grp1)
+ advanceUntilIdle()
- graphProcessor.onGraphStarted(graphRequestProcessor1)
- graphProcessor.onGraphError(
- GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
- )
- assertThat(graphProcessor.graphState.value).isInstanceOf(GraphStateError::class.java)
- }
+ assertThrows<IllegalStateException> {
+ graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to true))
+ }
+ }
@Test
- fun graphProcessorDropsStaleErrors() = runTest {
- val graphProcessor =
- GraphProcessorImpl(
- FakeThreads.fromTestScope(this),
- CameraGraphId.nextId(),
- FakeGraphConfigs.graphConfig,
- graphState3A,
- graphListener3A,
- arrayListOf(globalListener)
+ fun graphProcessorChangesGraphStateOnError() =
+ testScope.runTest {
+ assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+
+ graphProcessor.onGraphStarted(grp1)
+ graphProcessor.onGraphError(
+ GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
)
- assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+ assertThat(graphProcessor.graphState.value).isInstanceOf(GraphStateError::class.java)
+ }
- graphProcessor.onGraphError(
- GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
- )
- assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+ @Test
+ fun graphProcessorDropsStaleErrors() =
+ testScope.runTest {
+ assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
- graphProcessor.onGraphStarting()
- graphProcessor.onGraphStarted(graphRequestProcessor1)
+ graphProcessor.onGraphError(
+ GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
+ )
+ assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
- // GraphProcessor should drop errors while the camera graph is stopping.
- graphProcessor.onGraphStopping()
- graphProcessor.onGraphError(
- GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
- )
- assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+ graphProcessor.onGraphStarting()
+ graphProcessor.onGraphStarted(grp1)
- // GraphProcessor should also drop errors while the camera graph is stopped.
- graphProcessor.onGraphStopped(graphRequestProcessor1)
- graphProcessor.onGraphError(
- GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
- )
- assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
- }
+ // GraphProcessor should drop errors while the camera graph is stopping.
+ graphProcessor.onGraphStopping()
+ graphProcessor.onGraphError(
+ GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
+ )
+ assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+
+ // GraphProcessor should also drop errors while the camera graph is stopped.
+ graphProcessor.onGraphStopped(grp1)
+ graphProcessor.onGraphError(
+ GraphStateError(CameraError.ERROR_CAMERA_DEVICE, willAttemptRetry = true)
+ )
+ assertThat(graphProcessor.graphState.value).isEqualTo(GraphStateStopped)
+ }
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatBaseImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatBaseImpl.java
index ec4cc78..ace8b46 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatBaseImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatBaseImpl.java
@@ -45,8 +45,8 @@
// StreamConfigurationMap provided by Robolectric.
try {
return mStreamConfigurationMap.getOutputFormats();
- } catch (NullPointerException e) {
- Logger.e(TAG, "Failed to get output formats from StreamConfigurationMap", e);
+ } catch (NullPointerException | IllegalArgumentException e) {
+ Logger.w(TAG, "Failed to get output formats from StreamConfigurationMap", e);
return null;
}
}
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
index 263b7a5..b596010 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
@@ -124,7 +124,7 @@
FakeCameraControl fakeCameraControl =
getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
- fakeCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+ fakeCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
// Notify the cancel after the capture request has been successfully submitted
fakeCameraControl.notifyAllRequestsOnCaptureCancelled();
});
@@ -154,7 +154,7 @@
ImageCapture.OnImageCapturedCallback.class);
FakeCameraControl fakeCameraControl =
getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
- fakeCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+ fakeCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
// Notify the failure after the capture request has been successfully submitted
fakeCameraControl.notifyAllRequestsOnCaptureFailed();
});
@@ -302,7 +302,7 @@
getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
// Simulates the case that the capture request failed after running in 300 ms.
- fakeCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+ fakeCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
CameraXExecutors.mainThreadExecutor().schedule(() -> {
fakeCameraControl.notifyAllRequestsOnCaptureFailed();
}, 300, TimeUnit.MILLISECONDS);
@@ -395,7 +395,7 @@
getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
FakeCameraControl.OnNewCaptureRequestListener mockCaptureRequestListener =
mock(FakeCameraControl.OnNewCaptureRequestListener.class);
- fakeCameraControl.setOnNewCaptureRequestListener(mockCaptureRequestListener);
+ fakeCameraControl.addOnNewCaptureRequestListener(mockCaptureRequestListener);
// Act.
mInstrumentation.runOnMainSync(
@@ -463,7 +463,7 @@
FakeCameraControl fakeCameraControl =
getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
CountDownLatch latch = new CountDownLatch(1);
- fakeCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+ fakeCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
latch.countDown();
});
@@ -492,7 +492,7 @@
private void addExtraFailureNotificationsForRetry(FakeCameraControl cameraControl,
int retryCount) {
if (retryCount > 0) {
- cameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+ cameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
addExtraFailureNotificationsForRetry(cameraControl, retryCount - 1);
cameraControl.notifyAllRequestsOnCaptureFailed();
});
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index 20b2ecd..0873c4b 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -1312,7 +1312,8 @@
if (mTakePictureManager == null) {
// mTakePictureManager is reused when the Surface is reset.
- mTakePictureManager = new TakePictureManager(mImageCaptureControl);
+ mTakePictureManager = getCurrentConfig().getTakePictureManagerProvider().newInstance(
+ mImageCaptureControl);
}
mTakePictureManager.setImagePipeline(mImagePipeline);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
index b81133b..62245fd 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
@@ -49,10 +49,11 @@
public final class ImageProcessingUtil {
private static final String TAG = "ImageProcessingUtil";
+ public static final String JNI_LIB_NAME = "image_processing_util_jni";
private static int sImageCount = 0;
static {
- System.loadLibrary("image_processing_util_jni");
+ System.loadLibrary(JNI_LIB_NAME);
}
enum Result {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Bitmap2JpegBytes.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Bitmap2JpegBytes.java
index d4ab73e..ceec2f8 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Bitmap2JpegBytes.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Bitmap2JpegBytes.java
@@ -24,6 +24,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.processing.Operation;
import androidx.camera.core.processing.Packet;
@@ -37,7 +38,8 @@
*
* <p>The {@link Bitmap} will be recycled and should not be used after the processing.
*/
-class Bitmap2JpegBytes implements Operation<Bitmap2JpegBytes.In, Packet<byte[]>> {
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class Bitmap2JpegBytes implements Operation<Bitmap2JpegBytes.In, Packet<byte[]>> {
@NonNull
@Override
@@ -79,16 +81,16 @@
* Input of {@link Bitmap2JpegBytes} processor.
*/
@AutoValue
- abstract static class In {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public abstract static class In {
abstract Packet<Bitmap> getPacket();
abstract int getJpegQuality();
@NonNull
- static In of(@NonNull Packet<Bitmap> imagePacket, int jpegQuality) {
+ public static In of(@NonNull Packet<Bitmap> imagePacket, int jpegQuality) {
return new AutoValue_Bitmap2JpegBytes_In(imagePacket, jpegQuality);
}
}
}
-
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java
index 4dccd13..cb378f1 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java
@@ -27,6 +27,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.impl.utils.Exif;
@@ -48,7 +49,9 @@
/**
* Saves JPEG bytes to disk.
*/
-class JpegBytes2Disk implements Operation<JpegBytes2Disk.In, ImageCapture.OutputFileResults> {
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class JpegBytes2Disk implements
+ Operation<JpegBytes2Disk.In, ImageCapture.OutputFileResults> {
private static final String TEMP_FILE_PREFIX = "CameraX";
private static final String TEMP_FILE_SUFFIX = ".tmp";
@@ -287,7 +290,8 @@
* Input packet.
*/
@AutoValue
- abstract static class In {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public abstract static class In {
@NonNull
abstract Packet<byte[]> getPacket();
@@ -296,7 +300,7 @@
abstract ImageCapture.OutputFileOptions getOutputFileOptions();
@NonNull
- static In of(@NonNull Packet<byte[]> jpegBytes,
+ public static In of(@NonNull Packet<byte[]> jpegBytes,
@NonNull ImageCapture.OutputFileOptions outputFileOptions) {
return new AutoValue_JpegBytes2Disk_In(jpegBytes, outputFileOptions);
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
index fdf33ed..5b06259 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
@@ -16,36 +16,15 @@
package androidx.camera.core.imagecapture;
-import static androidx.camera.core.ImageCapture.ERROR_CAMERA_CLOSED;
-import static androidx.camera.core.ImageCapture.ERROR_CAPTURE_FAILED;
-import static androidx.camera.core.impl.utils.Threads.checkMainThread;
-import static androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor;
-import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
-import static androidx.core.util.Preconditions.checkState;
-
-import static java.util.Objects.requireNonNull;
-
-import android.util.Log;
-
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
-import androidx.camera.core.ForwardingImageProxy.OnImageCloseListener;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
-import androidx.camera.core.ImageProxy;
-import androidx.camera.core.Logger;
-import androidx.camera.core.impl.utils.futures.FutureCallback;
-import androidx.camera.core.impl.utils.futures.Futures;
-import androidx.core.util.Pair;
import com.google.auto.value.AutoValue;
-import com.google.common.util.concurrent.ListenableFuture;
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Deque;
import java.util.List;
/**
@@ -62,46 +41,13 @@
*
* <p>The thread safety is guaranteed by using the main thread.
*/
-public class TakePictureManager implements OnImageCloseListener, TakePictureRequest.RetryControl {
-
- private static final String TAG = "TakePictureManager";
-
- // Queue of new requests that have not been sent to the pipeline/camera.
- @VisibleForTesting
- final Deque<TakePictureRequest> mNewRequests = new ArrayDeque<>();
- final ImageCaptureControl mImageCaptureControl;
- ImagePipeline mImagePipeline;
-
- // The current request being processed by the camera. Only one request can be processed by
- // the camera at the same time. Null if the camera is idle.
- @Nullable
- private RequestWithCallback mCapturingRequest;
- // The current requests that have not received a result or an error.
- private final List<RequestWithCallback> mIncompleteRequests;
-
- // Once paused, the class waits until the class is resumed to handle new requests.
- boolean mPaused = false;
-
- /**
- * @param imageCaptureControl for controlling {@link ImageCapture}
- */
- @MainThread
- public TakePictureManager(@NonNull ImageCaptureControl imageCaptureControl) {
- checkMainThread();
- mImageCaptureControl = imageCaptureControl;
- mIncompleteRequests = new ArrayList<>();
- }
-
+public interface TakePictureManager {
/**
* Sets the {@link ImagePipeline} for building capture requests and post-processing camera
* output.
*/
@MainThread
- public void setImagePipeline(@NonNull ImagePipeline imagePipeline) {
- checkMainThread();
- mImagePipeline = imagePipeline;
- mImagePipeline.setOnImageCloseListener(this);
- }
+ void setImagePipeline(@NonNull ImagePipeline imagePipeline);
/**
* Adds requests to the queue.
@@ -109,201 +55,52 @@
* <p>The requests in the queue will be executed based on the order being added.
*/
@MainThread
- public void offerRequest(@NonNull TakePictureRequest takePictureRequest) {
- checkMainThread();
- mNewRequests.offer(takePictureRequest);
- issueNextRequest();
- }
-
- @MainThread
- @Override
- public void retryRequest(@NonNull TakePictureRequest request) {
- checkMainThread();
- Logger.d(TAG, "Add a new request for retrying.");
- // Insert the request to the front of the queue.
- mNewRequests.addFirst(request);
- // Try to issue the newly added request in case condition allows.
- issueNextRequest();
- }
+ void offerRequest(@NonNull TakePictureRequest takePictureRequest);
/**
* Pauses sending request to camera.
*/
@MainThread
- public void pause() {
- checkMainThread();
- mPaused = true;
-
- // Always retry because the camera may not send an error callback during the reset.
- if (mCapturingRequest != null) {
- mCapturingRequest.abortSilentlyAndRetry();
- }
- }
+ void pause();
/**
* Resumes sending request to camera.
*/
@MainThread
- public void resume() {
- checkMainThread();
- mPaused = false;
- issueNextRequest();
- }
+ void resume();
/**
* Clears the requests queue.
*/
@MainThread
- public void abortRequests() {
- checkMainThread();
- ImageCaptureException exception =
- new ImageCaptureException(ERROR_CAMERA_CLOSED, "Camera is closed.", null);
-
- // Clear pending request first so aborting in-flight request won't trigger another capture.
- for (TakePictureRequest request : mNewRequests) {
- request.onError(exception);
- }
- mNewRequests.clear();
-
- // Abort the in-flight request after clearing the pending requests.
- // Snapshot to avoid concurrent modification with the removal in getCompleteFuture().
- List<RequestWithCallback> requestsSnapshot = new ArrayList<>(mIncompleteRequests);
- for (RequestWithCallback request : requestsSnapshot) {
- // TODO: optimize the performance by not processing aborted requests.
- request.abortAndSendErrorToApp(exception);
- }
- }
+ void abortRequests();
/**
- * Issues the next request if conditions allow.
+ * Returns whether any capture request is being processed currently.
*/
- @MainThread
- void issueNextRequest() {
- checkMainThread();
- Log.d(TAG, "Issue the next TakePictureRequest.");
- if (hasCapturingRequest()) {
- Log.d(TAG, "There is already a request in-flight.");
- return;
- }
- if (mPaused) {
- Log.d(TAG, "The class is paused.");
- return;
- }
- if (mImagePipeline.getCapacity() == 0) {
- Log.d(TAG, "Too many acquire images. Close image to be able to process next.");
- return;
- }
- TakePictureRequest request = mNewRequests.poll();
- if (request == null) {
- Log.d(TAG, "No new request.");
- return;
- }
-
- RequestWithCallback requestWithCallback = new RequestWithCallback(request, this);
- trackCurrentRequests(requestWithCallback);
-
- // Send requests.
- Pair<CameraRequest, ProcessingRequest> requests =
- mImagePipeline.createRequests(request, requestWithCallback,
- requestWithCallback.getCaptureFuture());
- CameraRequest cameraRequest = requireNonNull(requests.first);
- ProcessingRequest processingRequest = requireNonNull(requests.second);
- mImagePipeline.submitProcessingRequest(processingRequest);
- ListenableFuture<Void> captureRequestFuture = submitCameraRequest(cameraRequest);
- requestWithCallback.setCaptureRequestFuture(captureRequestFuture);
- }
-
- /**
- * Waits for the request to finish before issuing the next.
- */
- private void trackCurrentRequests(@NonNull RequestWithCallback requestWithCallback) {
- checkState(!hasCapturingRequest());
- mCapturingRequest = requestWithCallback;
-
- // Waits for the capture to finish before issuing the next.
- mCapturingRequest.getCaptureFuture().addListener(() -> {
- mCapturingRequest = null;
- issueNextRequest();
- }, directExecutor());
-
- // Track all incomplete requests so we can abort them when UseCase is detached.
- mIncompleteRequests.add(requestWithCallback);
- requestWithCallback.getCompleteFuture().addListener(() -> {
- mIncompleteRequests.remove(requestWithCallback);
- }, directExecutor());
- }
-
- /**
- * Submit a request to camera and post-processing pipeline.
- *
- * <p>Flash is locked/unlocked during the flight of a {@link CameraRequest}.
- */
- @MainThread
- private ListenableFuture<Void> submitCameraRequest(
- @NonNull CameraRequest cameraRequest) {
- checkMainThread();
- mImageCaptureControl.lockFlashMode();
- ListenableFuture<Void> captureRequestFuture =
- mImageCaptureControl.submitStillCaptureRequests(cameraRequest.getCaptureConfigs());
- Futures.addCallback(captureRequestFuture, new FutureCallback<Void>() {
- @Override
- public void onSuccess(@Nullable Void result) {
- mImageCaptureControl.unlockFlashMode();
- }
-
- @Override
- public void onFailure(@NonNull Throwable throwable) {
- if (cameraRequest.isAborted()) {
- // When the pipeline is recreated, the in-flight request is aborted and
- // retried. On legacy devices, the camera may return CancellationException
- // for the aborted request which causes the retried request to fail. Return
- // early if the request has been aborted.
- return;
- } else {
- int requestId = cameraRequest.getCaptureConfigs().get(0).getId();
- if (throwable instanceof ImageCaptureException) {
- mImagePipeline.notifyCaptureError(
- CaptureError.of(requestId, (ImageCaptureException) throwable));
- } else {
- mImagePipeline.notifyCaptureError(
- CaptureError.of(requestId, new ImageCaptureException(
- ERROR_CAPTURE_FAILED,
- "Failed to submit capture request",
- throwable)));
- }
- }
- mImageCaptureControl.unlockFlashMode();
- }
- }, mainThreadExecutor());
- return captureRequestFuture;
- }
-
@VisibleForTesting
- boolean hasCapturingRequest() {
- return mCapturingRequest != null;
- }
+ boolean hasCapturingRequest();
+ /**
+ * Returns the capture request being processed currently.
+ */
@VisibleForTesting
@Nullable
- public RequestWithCallback getCapturingRequest() {
- return mCapturingRequest;
- }
+ RequestWithCallback getCapturingRequest();
+ /**
+ * Returns the requests that have not received a result or an error yet.
+ */
+ @NonNull
@VisibleForTesting
- List<RequestWithCallback> getIncompleteRequests() {
- return mIncompleteRequests;
- }
+ List<RequestWithCallback> getIncompleteRequests();
+ /**
+ * Returns the {@link ImagePipeline} instance used under the hood.
+ */
@VisibleForTesting
@NonNull
- public ImagePipeline getImagePipeline() {
- return mImagePipeline;
- }
-
- @Override
- public void onImageClose(@NonNull ImageProxy image) {
- mainThreadExecutor().execute(this::issueNextRequest);
- }
+ ImagePipeline getImagePipeline();
@AutoValue
abstract static class CaptureError {
@@ -318,4 +115,18 @@
}
}
+ /**
+ * Interface for deferring creation of a {@link TakePictureManager}.
+ */
+ interface Provider {
+ /**
+ * Creates a new, initialized instance of a {@link TakePictureManager}.
+ *
+ * @param imageCaptureControl Used by TakePictureManager to control an
+ * {@link ImageCapture} instance.
+ * @return The {@code TakePictureManager} instance.
+ */
+ @NonNull
+ TakePictureManager newInstance(@NonNull ImageCaptureControl imageCaptureControl);
+ }
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManagerImpl.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManagerImpl.java
new file mode 100644
index 0000000..e356a50
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManagerImpl.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.imagecapture;
+
+import static androidx.camera.core.ImageCapture.ERROR_CAMERA_CLOSED;
+import static androidx.camera.core.ImageCapture.ERROR_CAPTURE_FAILED;
+import static androidx.camera.core.impl.utils.Threads.checkMainThread;
+import static androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor;
+import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
+import static androidx.core.util.Preconditions.checkState;
+
+import static java.util.Objects.requireNonNull;
+
+import android.util.Log;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.ForwardingImageProxy.OnImageCloseListener;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.ImageCaptureException;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.core.Logger;
+import androidx.camera.core.impl.utils.futures.FutureCallback;
+import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.core.util.Pair;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.List;
+
+/**
+ * Manages {@link ImageCapture#takePicture} calls.
+ *
+ * <p>In coming requests are added to a queue and later sent to camera one at a time. Only one
+ * in-flight request is allowed at a time. The next request cannot be sent until the current one
+ * is completed by camera. However, it allows multiple concurrent requests for post-processing,
+ * as {@link ImagePipeline} supports parallel processing.
+ *
+ * <p>This class selectively propagates callbacks from camera and {@link ImagePipeline} to the
+ * app. e.g. it may choose to retry the request before sending the {@link ImageCaptureException}
+ * to the app.
+ *
+ * <p>The thread safety is guaranteed by using the main thread.
+ */
+public class TakePictureManagerImpl implements TakePictureManager, OnImageCloseListener,
+ TakePictureRequest.RetryControl {
+
+ private static final String TAG = "TakePictureManagerImpl";
+
+ // Queue of new requests that have not been sent to the pipeline/camera.
+ @VisibleForTesting
+ final Deque<TakePictureRequest> mNewRequests = new ArrayDeque<>();
+ final ImageCaptureControl mImageCaptureControl;
+ ImagePipeline mImagePipeline;
+
+ // The current request being processed by the camera. Only one request can be processed by
+ // the camera at the same time. Null if the camera is idle.
+ @Nullable
+ private RequestWithCallback mCapturingRequest;
+ // The current requests that have not received a result or an error.
+ private final List<RequestWithCallback> mIncompleteRequests;
+
+ // Once paused, the class waits until the class is resumed to handle new requests.
+ boolean mPaused = false;
+
+ /**
+ * @param imageCaptureControl for controlling {@link ImageCapture}
+ */
+ @MainThread
+ public TakePictureManagerImpl(@NonNull ImageCaptureControl imageCaptureControl) {
+ checkMainThread();
+ mImageCaptureControl = imageCaptureControl;
+ mIncompleteRequests = new ArrayList<>();
+ }
+
+ /**
+ * Sets the {@link ImagePipeline} for building capture requests and post-processing camera
+ * output.
+ */
+ @MainThread
+ @Override
+ public void setImagePipeline(@NonNull ImagePipeline imagePipeline) {
+ checkMainThread();
+ mImagePipeline = imagePipeline;
+ mImagePipeline.setOnImageCloseListener(this);
+ }
+
+ /**
+ * Adds requests to the queue.
+ *
+ * <p>The requests in the queue will be executed based on the order being added.
+ */
+ @MainThread
+ @Override
+ public void offerRequest(@NonNull TakePictureRequest takePictureRequest) {
+ checkMainThread();
+ mNewRequests.offer(takePictureRequest);
+ issueNextRequest();
+ }
+
+ @MainThread
+ @Override
+ public void retryRequest(@NonNull TakePictureRequest request) {
+ checkMainThread();
+ Logger.d(TAG, "Add a new request for retrying.");
+ // Insert the request to the front of the queue.
+ mNewRequests.addFirst(request);
+ // Try to issue the newly added request in case condition allows.
+ issueNextRequest();
+ }
+
+ /**
+ * Pauses sending request to camera.
+ */
+ @MainThread
+ @Override
+ public void pause() {
+ checkMainThread();
+ mPaused = true;
+
+ // Always retry because the camera may not send an error callback during the reset.
+ if (mCapturingRequest != null) {
+ mCapturingRequest.abortSilentlyAndRetry();
+ }
+ }
+
+ /**
+ * Resumes sending request to camera.
+ */
+ @MainThread
+ @Override
+ public void resume() {
+ checkMainThread();
+ mPaused = false;
+ issueNextRequest();
+ }
+
+ /**
+ * Clears the requests queue.
+ */
+ @MainThread
+ @Override
+ public void abortRequests() {
+ checkMainThread();
+ ImageCaptureException exception =
+ new ImageCaptureException(ERROR_CAMERA_CLOSED, "Camera is closed.", null);
+
+ // Clear pending request first so aborting in-flight request won't trigger another capture.
+ for (TakePictureRequest request : mNewRequests) {
+ request.onError(exception);
+ }
+ mNewRequests.clear();
+
+ // Abort the in-flight request after clearing the pending requests.
+ // Snapshot to avoid concurrent modification with the removal in getCompleteFuture().
+ List<RequestWithCallback> requestsSnapshot = new ArrayList<>(mIncompleteRequests);
+ for (RequestWithCallback request : requestsSnapshot) {
+ // TODO: optimize the performance by not processing aborted requests.
+ request.abortAndSendErrorToApp(exception);
+ }
+ }
+
+ /**
+ * Issues the next request if conditions allow.
+ */
+ @MainThread
+ void issueNextRequest() {
+ checkMainThread();
+ Log.d(TAG, "Issue the next TakePictureRequest.");
+ if (hasCapturingRequest()) {
+ Log.d(TAG, "There is already a request in-flight.");
+ return;
+ }
+ if (mPaused) {
+ Log.d(TAG, "The class is paused.");
+ return;
+ }
+ if (mImagePipeline.getCapacity() == 0) {
+ Log.d(TAG, "Too many acquire images. Close image to be able to process next.");
+ return;
+ }
+ TakePictureRequest request = mNewRequests.poll();
+ if (request == null) {
+ Log.d(TAG, "No new request.");
+ return;
+ }
+
+ RequestWithCallback requestWithCallback = new RequestWithCallback(request, this);
+ trackCurrentRequests(requestWithCallback);
+
+ // Send requests.
+ Pair<CameraRequest, ProcessingRequest> requests =
+ mImagePipeline.createRequests(request, requestWithCallback,
+ requestWithCallback.getCaptureFuture());
+ CameraRequest cameraRequest = requireNonNull(requests.first);
+ ProcessingRequest processingRequest = requireNonNull(requests.second);
+ mImagePipeline.submitProcessingRequest(processingRequest);
+ ListenableFuture<Void> captureRequestFuture = submitCameraRequest(cameraRequest);
+ requestWithCallback.setCaptureRequestFuture(captureRequestFuture);
+ }
+
+ /**
+ * Waits for the request to finish before issuing the next.
+ */
+ private void trackCurrentRequests(@NonNull RequestWithCallback requestWithCallback) {
+ checkState(!hasCapturingRequest());
+ mCapturingRequest = requestWithCallback;
+
+ // Waits for the capture to finish before issuing the next.
+ mCapturingRequest.getCaptureFuture().addListener(() -> {
+ mCapturingRequest = null;
+ issueNextRequest();
+ }, directExecutor());
+
+ // Track all incomplete requests so we can abort them when UseCase is detached.
+ mIncompleteRequests.add(requestWithCallback);
+ requestWithCallback.getCompleteFuture().addListener(() -> {
+ mIncompleteRequests.remove(requestWithCallback);
+ }, directExecutor());
+ }
+
+ /**
+ * Submit a request to camera and post-processing pipeline.
+ *
+ * <p>Flash is locked/unlocked during the flight of a {@link CameraRequest}.
+ */
+ @MainThread
+ private ListenableFuture<Void> submitCameraRequest(
+ @NonNull CameraRequest cameraRequest) {
+ checkMainThread();
+ mImageCaptureControl.lockFlashMode();
+ ListenableFuture<Void> captureRequestFuture =
+ mImageCaptureControl.submitStillCaptureRequests(cameraRequest.getCaptureConfigs());
+ Futures.addCallback(captureRequestFuture, new FutureCallback<Void>() {
+ @Override
+ public void onSuccess(@Nullable Void result) {
+ mImageCaptureControl.unlockFlashMode();
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ if (cameraRequest.isAborted()) {
+ // When the pipeline is recreated, the in-flight request is aborted and
+ // retried. On legacy devices, the camera may return CancellationException
+ // for the aborted request which causes the retried request to fail. Return
+ // early if the request has been aborted.
+ return;
+ } else {
+ int requestId = cameraRequest.getCaptureConfigs().get(0).getId();
+ if (throwable instanceof ImageCaptureException) {
+ mImagePipeline.notifyCaptureError(
+ CaptureError.of(requestId, (ImageCaptureException) throwable));
+ } else {
+ mImagePipeline.notifyCaptureError(
+ CaptureError.of(requestId, new ImageCaptureException(
+ ERROR_CAPTURE_FAILED,
+ "Failed to submit capture request",
+ throwable)));
+ }
+ }
+ mImageCaptureControl.unlockFlashMode();
+ }
+ }, mainThreadExecutor());
+ return captureRequestFuture;
+ }
+
+ @VisibleForTesting
+ @Override
+ public boolean hasCapturingRequest() {
+ return mCapturingRequest != null;
+ }
+
+ @VisibleForTesting
+ @Nullable
+ @Override
+ public RequestWithCallback getCapturingRequest() {
+ return mCapturingRequest;
+ }
+
+ @NonNull
+ @VisibleForTesting
+ @Override
+ public List<RequestWithCallback> getIncompleteRequests() {
+ return mIncompleteRequests;
+ }
+
+ @VisibleForTesting
+ @NonNull
+ @Override
+ public ImagePipeline getImagePipeline() {
+ return mImagePipeline;
+ }
+
+ @Override
+ public void onImageClose(@NonNull ImageProxy image) {
+ mainThreadExecutor().execute(this::issueNextRequest);
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java
index 4ef3f3b..d49c06b9 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java
@@ -82,14 +82,14 @@
* Gets the app provided options for on-disk capture.
*/
@Nullable
- abstract ImageCapture.OutputFileOptions getOutputFileOptions();
+ public abstract ImageCapture.OutputFileOptions getOutputFileOptions();
/**
* A snapshot of {@link ImageCapture#getViewPortCropRect()} when
* {@link ImageCapture#takePicture} is called.
*/
@NonNull
- abstract Rect getCropRect();
+ public abstract Rect getCropRect();
/**
* A snapshot of {@link ImageCapture#getSensorToBufferTransformMatrix()} when
@@ -102,14 +102,14 @@
* A snapshot of rotation degrees when {@link ImageCapture#takePicture} is called.
*/
@ImageOutputConfig.RotationValue
- abstract int getRotationDegrees();
+ public abstract int getRotationDegrees();
/**
* A snapshot of {@link ImageCaptureConfig#getJpegQuality()} when
* {@link ImageCapture#takePicture} is called.
*/
@IntRange(from = 1, to = 100)
- abstract int getJpegQuality();
+ public abstract int getJpegQuality();
/**
* Gets the capture mode of the request.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
index 347a25c..9d6f159 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
@@ -21,10 +21,16 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.core.ExtendableBuilder;
+import androidx.camera.core.ImageCapture;
import androidx.camera.core.UseCase;
+import androidx.camera.core.imagecapture.ImageCaptureControl;
+import androidx.camera.core.imagecapture.TakePictureManager;
+import androidx.camera.core.imagecapture.TakePictureManagerImpl;
import androidx.camera.core.impl.stabilization.StabilizationMode;
import androidx.camera.core.internal.TargetConfig;
+import java.util.Objects;
+
/**
* Configuration containing options for use cases.
*
@@ -108,6 +114,10 @@
Option<Integer> OPTION_VIDEO_STABILIZATION_MODE =
Option.create("camerax.core.useCase.videoStabilizationMode", int.class);
+ Option<TakePictureManager.Provider> OPTION_TAKE_PICTURE_MANAGER_PROVIDER =
+ Option.create("camerax.core.useCase.takePictureManagerProvider",
+ TakePictureManager.Provider.class);
+
// *********************************************************************************************
/**
@@ -329,6 +339,22 @@
}
/**
+ * @return The {@link TakePictureManager} implementation for {@link ImageCapture} use case.
+ */
+ @NonNull
+ default TakePictureManager.Provider getTakePictureManagerProvider() {
+ return Objects.requireNonNull(retrieveOption(OPTION_TAKE_PICTURE_MANAGER_PROVIDER,
+ new TakePictureManager.Provider() {
+ @NonNull
+ @Override
+ public TakePictureManager newInstance(
+ @NonNull ImageCaptureControl imageCaptureControl) {
+ return new TakePictureManagerImpl(imageCaptureControl);
+ }
+ }));
+ }
+
+ /**
* Builder for a {@link UseCase}.
*
* @param <T> The type of the object which will be built by {@link #build()}.
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureExtTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureExtTest.kt
index be859db..45ba186 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureExtTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureExtTest.kt
@@ -28,7 +28,6 @@
import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
-import java.io.File
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.MainScope
@@ -36,7 +35,9 @@
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
+import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows
@@ -47,9 +48,14 @@
@DoNotInstrument
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
class ImageCaptureExtTest {
+ @get:Rule
+ val temporaryFolder =
+ TemporaryFolder(ApplicationProvider.getApplicationContext<Context>().cacheDir)
+
private val context = ApplicationProvider.getApplicationContext<Context>()
- private val fakeOutputFileOptions =
- ImageCapture.OutputFileOptions.Builder(File("fake_path")).build()
+ private val fakeOutputFileOptions by lazy {
+ ImageCapture.OutputFileOptions.Builder(temporaryFolder.newFile("fake_path")).build()
+ }
private lateinit var cameraProvider: ProcessCameraProvider
private lateinit var imageCapture: ImageCapture
@@ -88,12 +94,11 @@
fun takePicture_inMemory_canGetImage(): Unit = runTest {
// Arrange
val imageProxy = FakeImageProxy(FakeImageInfo())
+ val fakeTakePictureManager = FakeAppConfig.getTakePictureManager()!!
+ fakeTakePictureManager.enqueueImageProxy(imageProxy)
// Arrange & Act.
val takePictureAsync = MainScope().async { imageCapture.takePicture() }
- Shadows.shadowOf(Looper.getMainLooper()).idle()
- val imageCaptureCallback = imageCapture.getTakePictureRequest()?.inMemoryCallback
- imageCaptureCallback?.onCaptureSuccess(imageProxy)
// Assert.
Shadows.shadowOf(Looper.getMainLooper()).idle()
@@ -135,6 +140,7 @@
var callbackCalled = false
val progress = 100
var resultProgress = 0
+ FakeAppConfig.getTakePictureManager()!!.disableAutoComplete = true
// Act.
val takePictureAsync =
@@ -163,6 +169,7 @@
var callbackCalled = false
val bitmap = Bitmap.createBitmap(800, 600, Bitmap.Config.ARGB_8888)
lateinit var resultBitmap: Bitmap
+ FakeAppConfig.getTakePictureManager()!!.disableAutoComplete = true
// Act.
val takePictureAsync =
@@ -189,15 +196,14 @@
fun takePicture_onDisk_canGetResult(): Unit = runTest {
// Arrange
val outputFileResults = ImageCapture.OutputFileResults(null)
+ val fakeTakePictureManager = FakeAppConfig.getTakePictureManager()!!
+ fakeTakePictureManager.enqueueOutputFileResults(outputFileResults)
// Arrange & Act.
val takePictureAsync =
MainScope().async {
imageCapture.takePicture(outputFileOptions = fakeOutputFileOptions)
}
- Shadows.shadowOf(Looper.getMainLooper()).idle()
- val imageCaptureCallback = imageCapture.getTakePictureRequest()?.onDiskCallback
- imageCaptureCallback?.onImageSaved(outputFileResults)
// Assert.
Shadows.shadowOf(Looper.getMainLooper()).idle()
@@ -245,6 +251,7 @@
var callbackCalled = false
val progress = 100
var resultProgress = 0
+ FakeAppConfig.getTakePictureManager()!!.disableAutoComplete = true
// Act.
val takePictureAsync =
@@ -274,6 +281,7 @@
var callbackCalled = false
val bitmap = Bitmap.createBitmap(800, 600, Bitmap.Config.ARGB_8888)
lateinit var resultBitmap: Bitmap
+ FakeAppConfig.getTakePictureManager()!!.disableAutoComplete = true
// Act.
val takePictureAsync =
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeTakePictureRequest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeTakePictureRequest.kt
index 7b775e8..87e05b0 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeTakePictureRequest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeTakePictureRequest.kt
@@ -112,7 +112,7 @@
return fileOptions
}
- internal override fun getCropRect(): Rect {
+ override fun getCropRect(): Rect {
return Rect(0, 0, 640, 480)
}
@@ -120,11 +120,11 @@
return Matrix()
}
- internal override fun getRotationDegrees(): Int {
+ override fun getRotationDegrees(): Int {
return ROTATION_DEGREES
}
- internal override fun getJpegQuality(): Int {
+ override fun getJpegQuality(): Int {
return JPEG_QUALITY
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
index db131ac..4f02ad7 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
@@ -50,7 +50,7 @@
private val imagePipeline = FakeImagePipeline()
private val imageCaptureControl = FakeImageCaptureControl()
private val takePictureManager =
- TakePictureManager(imageCaptureControl).also { it.imagePipeline = imagePipeline }
+ TakePictureManagerImpl(imageCaptureControl).also { it.imagePipeline = imagePipeline }
private val exception = ImageCaptureException(ImageCapture.ERROR_UNKNOWN, "", null)
@After
diff --git a/camera/camera-testing/api/current.txt b/camera/camera-testing/api/current.txt
index fbf779a..161b797 100644
--- a/camera/camera-testing/api/current.txt
+++ b/camera/camera-testing/api/current.txt
@@ -42,10 +42,12 @@
ctor public FakeCameraControl(androidx.camera.core.impl.CameraControlInternal.ControlUpdateCallback);
ctor public FakeCameraControl(java.util.concurrent.Executor, androidx.camera.core.impl.CameraControlInternal.ControlUpdateCallback);
method public void addInteropConfig(androidx.camera.core.impl.Config);
+ method public void addOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+ method public void addOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
method public void addZslConfig(androidx.camera.core.impl.SessionConfig.Builder);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelFocusAndMetering();
method public void clearInteropConfig();
- method public void clearNewCaptureRequestListener();
+ method @Deprecated public void clearNewCaptureRequestListener();
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
method public int getExposureCompensationIndex();
method public int getFlashMode();
@@ -62,11 +64,14 @@
method public void notifyAllRequestsOnCaptureCancelled();
method public void notifyAllRequestsOnCaptureCompleted(androidx.camera.core.impl.CameraCaptureResult);
method public void notifyAllRequestsOnCaptureFailed();
+ method public void notifyLastRequestOnCaptureCompleted(androidx.camera.core.impl.CameraCaptureResult);
+ method public void removeOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+ method public void removeOnNewCaptureRequestListeners(java.util.List<androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener!>);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> setExposureCompensationIndex(int);
method public void setFlashMode(int);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(float);
- method public void setOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
- method public void setOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+ method @Deprecated public void setOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+ method @Deprecated public void setOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
method public void setScreenFlash(androidx.camera.core.ImageCapture.ScreenFlash?);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
method public void setZslDisabledByUserCaseConfig(boolean);
diff --git a/camera/camera-testing/api/restricted_current.txt b/camera/camera-testing/api/restricted_current.txt
index fbf779a..161b797 100644
--- a/camera/camera-testing/api/restricted_current.txt
+++ b/camera/camera-testing/api/restricted_current.txt
@@ -42,10 +42,12 @@
ctor public FakeCameraControl(androidx.camera.core.impl.CameraControlInternal.ControlUpdateCallback);
ctor public FakeCameraControl(java.util.concurrent.Executor, androidx.camera.core.impl.CameraControlInternal.ControlUpdateCallback);
method public void addInteropConfig(androidx.camera.core.impl.Config);
+ method public void addOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+ method public void addOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
method public void addZslConfig(androidx.camera.core.impl.SessionConfig.Builder);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelFocusAndMetering();
method public void clearInteropConfig();
- method public void clearNewCaptureRequestListener();
+ method @Deprecated public void clearNewCaptureRequestListener();
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
method public int getExposureCompensationIndex();
method public int getFlashMode();
@@ -62,11 +64,14 @@
method public void notifyAllRequestsOnCaptureCancelled();
method public void notifyAllRequestsOnCaptureCompleted(androidx.camera.core.impl.CameraCaptureResult);
method public void notifyAllRequestsOnCaptureFailed();
+ method public void notifyLastRequestOnCaptureCompleted(androidx.camera.core.impl.CameraCaptureResult);
+ method public void removeOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+ method public void removeOnNewCaptureRequestListeners(java.util.List<androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener!>);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> setExposureCompensationIndex(int);
method public void setFlashMode(int);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(float);
- method public void setOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
- method public void setOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+ method @Deprecated public void setOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+ method @Deprecated public void setOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
method public void setScreenFlash(androidx.camera.core.ImageCapture.ScreenFlash?);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
method public void setZslDisabledByUserCaseConfig(boolean);
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
index 41958c4..c7f41a5 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
@@ -28,6 +28,10 @@
import androidx.camera.testing.impl.fakes.FakeCameraDeviceSurfaceManager;
import androidx.camera.testing.impl.fakes.FakeCameraFactory;
import androidx.camera.testing.impl.fakes.FakeUseCaseConfigFactory;
+import androidx.camera.testing.impl.wrappers.TakePictureManagerWrapper;
+
+import java.util.ArrayList;
+import java.util.List;
/**
* Convenience class for generating a fake {@link CameraXConfig}.
@@ -47,6 +51,9 @@
@Nullable
private static FakeCamera sFrontCamera = null;
+ @Nullable
+ private static FakeUseCaseConfigFactory sFakeUseCaseConfigFactory;
+
/** Generates a fake {@link CameraXConfig}. */
@NonNull
public static CameraXConfig create() {
@@ -59,28 +66,26 @@
*/
@NonNull
public static CameraXConfig create(@Nullable CameraSelector availableCamerasSelector) {
+ FakeCameraFactory cameraFactory = createCameraFactory(availableCamerasSelector);
+
final CameraFactory.Provider cameraFactoryProvider =
- (ignored1, ignored2, ignored3, ignore4) -> {
- final FakeCameraFactory cameraFactory = new FakeCameraFactory(
- availableCamerasSelector);
- cameraFactory.insertCamera(CameraSelector.LENS_FACING_BACK,
- DEFAULT_BACK_CAMERA_ID,
- FakeAppConfig::getBackCamera);
- cameraFactory.insertCamera(CameraSelector.LENS_FACING_FRONT,
- DEFAULT_FRONT_CAMERA_ID,
- FakeAppConfig::getFrontCamera);
- final CameraCoordinator cameraCoordinator = new FakeCameraCoordinator();
- cameraFactory.setCameraCoordinator(cameraCoordinator);
- return cameraFactory;
- };
+ (ignored1, ignored2, ignored3, ignore4) -> cameraFactory;
final CameraDeviceSurfaceManager.Provider surfaceManagerProvider =
(ignored1, ignored2, ignored3) -> new FakeCameraDeviceSurfaceManager();
+ List<FakeCamera> fakeCameras = new ArrayList<>();
+ for (String cameraId : cameraFactory.getAvailableCameraIds()) {
+ fakeCameras.add((FakeCamera) cameraFactory.getCamera(cameraId));
+ }
+
+ sFakeUseCaseConfigFactory = new FakeUseCaseConfigFactory(fakeCameras);
+
final CameraXConfig.Builder appConfigBuilder = new CameraXConfig.Builder()
.setCameraFactoryProvider(cameraFactoryProvider)
.setDeviceSurfaceManagerProvider(surfaceManagerProvider)
- .setUseCaseConfigFactoryProvider(ignored -> new FakeUseCaseConfigFactory());
+ .setUseCaseConfigFactoryProvider(
+ ignored -> sFakeUseCaseConfigFactory);
if (availableCamerasSelector != null) {
appConfigBuilder.setAvailableCamerasLimiter(availableCamerasSelector);
@@ -89,6 +94,21 @@
return appConfigBuilder.build();
}
+ private static FakeCameraFactory createCameraFactory(
+ @Nullable CameraSelector availableCamerasSelector) {
+ FakeCameraFactory cameraFactory = new FakeCameraFactory(availableCamerasSelector);
+ cameraFactory.insertCamera(
+ CameraSelector.LENS_FACING_BACK,
+ DEFAULT_BACK_CAMERA_ID,
+ FakeAppConfig::getBackCamera);
+ cameraFactory.insertCamera(CameraSelector.LENS_FACING_FRONT,
+ DEFAULT_FRONT_CAMERA_ID,
+ FakeAppConfig::getFrontCamera);
+ final CameraCoordinator cameraCoordinator = new FakeCameraCoordinator();
+ cameraFactory.setCameraCoordinator(cameraCoordinator);
+ return cameraFactory;
+ }
+
/**
* Returns the default fake back camera that is used internally by CameraX.
*/
@@ -126,4 +146,20 @@
return create();
}
}
+
+ /**
+ * Returns the {@link TakePictureManagerWrapper} being used for image capture.
+ *
+ * <p> Note that this may be null if {@link androidx.camera.core.ImageCapture} is still not set
+ * up and bound to a camera.
+ */
+ @Nullable
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static TakePictureManagerWrapper getTakePictureManager() {
+ if (sFakeUseCaseConfigFactory == null) {
+ return null;
+ }
+ return sFakeUseCaseConfigFactory.getTakePictureManager();
+ }
+
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
index a3b256b..b4d661b 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
@@ -21,6 +21,7 @@
import android.graphics.Rect;
+import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.core.FocusMeteringAction;
@@ -46,6 +47,8 @@
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
@@ -76,13 +79,18 @@
* <p> {@link CameraXExecutors#directExecutor} via default, unless some other executor is set
* via {@link #FakeCameraControl(Executor, CameraControlInternal.ControlUpdateCallback)}.
*/
- @NonNull private final Executor mExecutor;
+ @NonNull
+ private final Executor mExecutor;
private final ControlUpdateCallback mControlUpdateCallback;
private final SessionConfig.Builder mSessionConfigBuilder = new SessionConfig.Builder();
@ImageCapture.FlashMode
private int mFlashMode = FLASH_MODE_OFF;
private final ArrayList<CaptureConfig> mSubmittedCaptureRequests = new ArrayList<>();
+ @Deprecated
private Pair<Executor, OnNewCaptureRequestListener> mOnNewCaptureRequestListener;
+ @GuardedBy("mOnNewCaptureRequestListeners")
+ private final List<Pair<Executor, OnNewCaptureRequestListener>> mOnNewCaptureRequestListeners =
+ new ArrayList<>();
private MutableOptionsBundle mInteropConfig = MutableOptionsBundle.create();
private final ArrayList<CallbackToFutureAdapter.Completer<Void>> mSubmittedCompleterList =
new ArrayList<>();
@@ -127,7 +135,8 @@
* Constructs an instance of {@link FakeCameraControl} with the
* provided {@link ControlUpdateCallback}.
*
- * @param executor {@link Executor} used to invoke the {@code controlUpdateCallback}.
+ * @param executor {@link Executor} used to invoke the {@code
+ * controlUpdateCallback}.
* @param controlUpdateCallback {@link ControlUpdateCallback} to notify events.
*/
public FakeCameraControl(@NonNull Executor executor,
@@ -180,6 +189,38 @@
}
/**
+ * Notifies last submitted request using {@link CameraCaptureCallback#onCaptureCompleted},
+ * which is invoked in the thread denoted by {@link #mExecutor}.
+ *
+ * @param result The {@link CameraCaptureResult} which is notified to all the callbacks.
+ */
+ public void notifyLastRequestOnCaptureCompleted(@NonNull CameraCaptureResult result) {
+ if (mSubmittedCaptureRequests.isEmpty() || mSubmittedCompleterList.isEmpty()) {
+ Logger.e(TAG,
+ "notifyLastRequestOnCaptureCompleted: returning early since either "
+ + "mSubmittedCaptureRequests or mSubmittedCompleterList is empty, "
+ + "mSubmittedCaptureRequests = "
+ + mSubmittedCaptureRequests + ", mSubmittedCompleterList"
+ + mSubmittedCompleterList);
+ return;
+ }
+
+ CaptureConfig captureConfig = mSubmittedCaptureRequests.get(
+ mSubmittedCaptureRequests.size() - 1);
+ for (CameraCaptureCallback cameraCaptureCallback :
+ captureConfig.getCameraCaptureCallbacks()) {
+ mExecutor.execute(() -> cameraCaptureCallback.onCaptureCompleted(
+ captureConfig.getId(), result));
+ }
+ mSubmittedCaptureRequests.remove(captureConfig);
+
+ CallbackToFutureAdapter.Completer<Void> completer = mSubmittedCompleterList.get(
+ mSubmittedCompleterList.size() - 1);
+ completer.set(null);
+ mSubmittedCompleterList.remove(completer);
+ }
+
+ /**
* Notifies all submitted requests using {@link CameraCaptureCallback#onCaptureCompleted},
* which is invoked in the thread denoted by {@link #mExecutor}.
*
@@ -288,6 +329,7 @@
public ListenableFuture<List<Void>> submitStillCaptureRequests(
@NonNull List<CaptureConfig> captureConfigs,
int captureMode, int flashType) {
+ Logger.d(TAG, "submitStillCaptureRequests: captureConfigs = " + captureConfigs);
mSubmittedCaptureRequests.addAll(captureConfigs);
mExecutor.execute(
() -> mControlUpdateCallback.onCameraControlCaptureRequests(captureConfigs));
@@ -299,12 +341,16 @@
}));
}
- if (mOnNewCaptureRequestListener != null) {
- Executor executor = Objects.requireNonNull(mOnNewCaptureRequestListener.first);
- OnNewCaptureRequestListener listener =
- Objects.requireNonNull(mOnNewCaptureRequestListener.second);
+ synchronized (mOnNewCaptureRequestListeners) {
+ Logger.d(TAG, "submitStillCaptureRequests: mOnNewCaptureRequestListeners = "
+ + mOnNewCaptureRequestListeners);
- executor.execute(() -> listener.onNewCaptureRequests(captureConfigs));
+ for (Pair<Executor, FakeCameraControl.OnNewCaptureRequestListener> listenerPair :
+ mOnNewCaptureRequestListeners) {
+ Executor executor = Objects.requireNonNull(listenerPair.first);
+ OnNewCaptureRequestListener listener = Objects.requireNonNull(listenerPair.second);
+ executor.execute(() -> listener.onNewCaptureRequests(captureConfigs));
+ }
}
return Futures.allAsList(fakeFutures);
}
@@ -348,6 +394,58 @@
}
/**
+ * Adds a listener to be notified when there are new capture requests submitted.
+ *
+ * <p> Note that the listener will be executed on the calling thread directly using
+ * {@link CameraXExecutors#directExecutor}. To specify the execution thread, use
+ * {@link #setOnNewCaptureRequestListener(Executor, OnNewCaptureRequestListener)}.
+ *
+ * @param listener {@link OnNewCaptureRequestListener} that is notified with the submitted
+ * {@link CaptureConfig} parameters when new capture requests are submitted.
+ */
+ public void addOnNewCaptureRequestListener(@NonNull OnNewCaptureRequestListener listener) {
+ addOnNewCaptureRequestListener(CameraXExecutors.directExecutor(), listener);
+ }
+
+ /**
+ * Adds a listener to be notified when there are new capture requests submitted.
+ *
+ * @param executor {@link Executor} used to notify the {@code listener}.
+ * @param listener {@link OnNewCaptureRequestListener} that is notified with the submitted
+ * {@link CaptureConfig} parameters when new capture requests are submitted.
+ */
+ public void addOnNewCaptureRequestListener(@NonNull Executor executor,
+ @NonNull OnNewCaptureRequestListener listener) {
+ synchronized (mOnNewCaptureRequestListeners) {
+ mOnNewCaptureRequestListeners.add(new Pair<>(executor, listener));
+ }
+ }
+
+ /**
+ * Removes a listener set via {@link #addOnNewCaptureRequestListener}.
+ */
+ public void removeOnNewCaptureRequestListener(@NonNull OnNewCaptureRequestListener listener) {
+ removeOnNewCaptureRequestListeners(Collections.singletonList(listener));
+ }
+
+ /**
+ * Removes a listener set via {@link #addOnNewCaptureRequestListener}.
+ */
+ public void removeOnNewCaptureRequestListeners(
+ @NonNull List<OnNewCaptureRequestListener> listeners) {
+ synchronized (mOnNewCaptureRequestListeners) {
+ Iterator<Pair<Executor, OnNewCaptureRequestListener>> iterator =
+ mOnNewCaptureRequestListeners.iterator();
+ while (iterator.hasNext()) {
+ Pair<Executor, OnNewCaptureRequestListener> element = iterator.next();
+ if (listeners.contains(element.second)) {
+ iterator.remove();
+ }
+ }
+ }
+ }
+
+ /**
* Sets a listener to be notified when there are new capture requests submitted.
*
* <p> Note that the listener will be executed on the calling thread directly using
@@ -355,8 +453,10 @@
* {@link #setOnNewCaptureRequestListener(Executor, OnNewCaptureRequestListener)}.
*
* @param listener {@link OnNewCaptureRequestListener} that is notified with the submitted
- * {@link CaptureConfig} parameters when new capture requests are submitted.
+ * {@link CaptureConfig} parameters when new capture requests are submitted.
+ * @deprecated Use {@link #addOnNewCaptureRequestListener(OnNewCaptureRequestListener)} instead.
*/
+ @Deprecated // TODO: b/359458110 - Remove all usages
public void setOnNewCaptureRequestListener(@NonNull OnNewCaptureRequestListener listener) {
setOnNewCaptureRequestListener(CameraXExecutors.directExecutor(), listener);
}
@@ -366,17 +466,31 @@
*
* @param executor {@link Executor} used to notify the {@code listener}.
* @param listener {@link OnNewCaptureRequestListener} that is notified with the submitted
- * {@link CaptureConfig} parameters when new capture requests are submitted.
+ * {@link CaptureConfig} parameters when new capture requests are submitted.
+ * @deprecated Use
+ * {@link #addOnNewCaptureRequestListener(Executor, OnNewCaptureRequestListener)}
+ * instead.
*/
+ @Deprecated // TODO: b/359458110 - Remove all usages
public void setOnNewCaptureRequestListener(@NonNull Executor executor,
@NonNull OnNewCaptureRequestListener listener) {
mOnNewCaptureRequestListener = new Pair<>(executor, listener);
+ addOnNewCaptureRequestListener(executor, listener);
}
/**
* Clears any listener set via {@link #setOnNewCaptureRequestListener}.
+ *
+ * @deprecated Use {@link #removeOnNewCaptureRequestListener(OnNewCaptureRequestListener)}
+ * instead.
*/
+ @Deprecated // TODO: b/359458110 - Remove all usages
public void clearNewCaptureRequestListener() {
+ if (mOnNewCaptureRequestListener == null) {
+ return;
+ }
+ removeOnNewCaptureRequestListener(
+ Objects.requireNonNull(mOnNewCaptureRequestListener.second));
mOnNewCaptureRequestListener = null;
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CaptureSimulation.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CaptureSimulation.kt
index e960662..015bff9 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CaptureSimulation.kt
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CaptureSimulation.kt
@@ -16,8 +16,11 @@
package androidx.camera.testing.impl
+import android.graphics.Bitmap
import android.graphics.Rect
+import android.util.Size
import android.view.Surface
+import androidx.camera.core.Logger
import androidx.camera.core.impl.DeferrableSurface
import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.core.impl.utils.futures.FutureCallback
@@ -36,7 +39,7 @@
private const val TAG = "CaptureSimulation"
/** Simulates a capture frame being drawn on all of the provided surfaces. */
-public suspend fun List<DeferrableSurface>.simulateCaptureFrame(): Unit = forEach {
+internal suspend fun List<DeferrableSurface>.simulateCaptureFrame(): Unit = forEach {
it.simulateCaptureFrame()
}
@@ -45,7 +48,7 @@
*
* @throws IllegalStateException If [DeferrableSurface.getSurface] provides a null surface.
*/
-public suspend fun DeferrableSurface.simulateCaptureFrame() {
+internal suspend fun DeferrableSurface.simulateCaptureFrame() {
val deferred = CompletableDeferred<Unit>()
Futures.addCallback(
@@ -53,6 +56,7 @@
object : FutureCallback<Surface?> {
override fun onSuccess(surface: Surface?) {
if (surface == null) {
+ Logger.w(TAG, "simulateCaptureFrame: surface obtained from $this is null!")
deferred.completeExceptionally(
IllegalStateException(
"Null surface obtained from ${this@simulateCaptureFrame}"
@@ -60,10 +64,9 @@
)
return
}
- val canvas =
- surface.lockCanvas(Rect(0, 0, prescribedSize.width, prescribedSize.height))
// TODO: Draw something on the canvas (e.g. fake image bitmap or alternating color).
- surface.unlockCanvasAndPost(canvas)
+ surface.simulateCaptureFrame(prescribedSize)
+
deferred.complete(Unit)
}
@@ -77,6 +80,20 @@
deferred.await()
}
+/**
+ * Simulates a capture frame being drawn on a [Surface].
+ *
+ * @param canvasSize The canvas size for drawing.
+ * @param bitmap A bitmap to draw as the capture frame, if not null.
+ */
+internal fun Surface.simulateCaptureFrame(canvasSize: Size, bitmap: Bitmap? = null) {
+ val canvas = lockCanvas(Rect(0, 0, canvasSize.width, canvasSize.height))
+ if (bitmap != null) {
+ canvas.drawBitmap(bitmap, null, Rect(0, 0, canvasSize.width, canvasSize.height), null)
+ }
+ unlockCanvasAndPost(canvas)
+}
+
// The following methods are adapters for Java invocations.
/**
@@ -88,7 +105,7 @@
* @return A [ListenableFuture] representing when the operation has been completed.
*/
@JvmOverloads
-public fun List<DeferrableSurface>.simulateCaptureFrameAsync(
+internal fun List<DeferrableSurface>.simulateCaptureFrameAsync(
executor: Executor = Dispatchers.Default.asExecutor()
): ListenableFuture<Void> {
val scope = CoroutineScope(SupervisorJob() + executor.asCoroutineDispatcher())
@@ -104,7 +121,7 @@
* @return A [ListenableFuture] representing when the operation has been completed.
*/
@JvmOverloads
-public fun DeferrableSurface.simulateCaptureFrameAsync(
+internal fun DeferrableSurface.simulateCaptureFrameAsync(
executor: Executor = Dispatchers.Default.asExecutor()
): ListenableFuture<Void> {
val scope = CoroutineScope(SupervisorJob() + executor.asCoroutineDispatcher())
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageProxy.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageProxy.java
index 90c4bce..5acfa2bb 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageProxy.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageProxy.java
@@ -16,6 +16,7 @@
package androidx.camera.testing.impl.fakes;
+import android.graphics.Bitmap;
import android.graphics.Rect;
import android.media.Image;
@@ -47,6 +48,8 @@
@NonNull
private ImageInfo mImageInfo;
private Image mImage;
+ @Nullable
+ private Bitmap mBitmap;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
final Object mReleaseLock = new Object();
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@@ -60,6 +63,11 @@
mImageInfo = imageInfo;
}
+ public FakeImageProxy(@NonNull ImageInfo imageInfo, @NonNull Bitmap bitmap) {
+ mImageInfo = imageInfo;
+ mBitmap = bitmap;
+ }
+
@Override
public void close() {
synchronized (mReleaseLock) {
@@ -196,4 +204,13 @@
return mReleaseFuture;
}
}
+
+ @NonNull
+ @Override
+ public Bitmap toBitmap() {
+ if (mBitmap != null) {
+ return mBitmap;
+ }
+ return ImageProxy.super.toBitmap();
+ }
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfigFactory.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfigFactory.java
index f364c4e..9ccd9bd 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfigFactory.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfigFactory.java
@@ -19,6 +19,7 @@
import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_SESSION_CONFIG;
import static androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_TAKE_PICTURE_MANAGER_PROVIDER;
import android.annotation.SuppressLint;
import android.hardware.camera2.CameraDevice;
@@ -30,21 +31,47 @@
import androidx.camera.core.ExperimentalZeroShutterLag;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCapture.CaptureMode;
+import androidx.camera.core.imagecapture.ImageCaptureControl;
+import androidx.camera.core.imagecapture.TakePictureManager;
import androidx.camera.core.impl.Config;
import androidx.camera.core.impl.MutableOptionsBundle;
import androidx.camera.core.impl.OptionsBundle;
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.UseCaseConfigFactory;
+import androidx.camera.testing.fakes.FakeCamera;
+import androidx.camera.testing.impl.wrappers.TakePictureManagerWrapper;
+
+import java.util.ArrayList;
+import java.util.List;
/**
* A fake implementation of {@link UseCaseConfigFactory}.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public final class FakeUseCaseConfigFactory implements UseCaseConfigFactory {
-
@Nullable
private CaptureType mLastRequestedCaptureType;
+ @Nullable
+ private TakePictureManagerWrapper mTakePictureManager;
+
+ @NonNull
+ private final List<FakeCamera> mFakeCameras = new ArrayList<>();
+
+ /**
+ * Creates a {@link FakeUseCaseConfigFactory} instance.
+ */
+ public FakeUseCaseConfigFactory() {
+ }
+
+ /**
+ * Creates a {@link FakeUseCaseConfigFactory} instance with the available {@link FakeCamera}
+ * instances.
+ */
+ public FakeUseCaseConfigFactory(@NonNull List<FakeCamera> fakeCameras) {
+ mFakeCameras.addAll(fakeCameras);
+ }
+
/**
* Returns the configuration for the given capture type, or <code>null</code> if the
* configuration cannot be produced.
@@ -66,6 +93,20 @@
mutableConfig.insertOption(OPTION_SESSION_CONFIG_UNPACKER,
new FakeSessionConfigOptionUnpacker());
+ if (captureType == CaptureType.IMAGE_CAPTURE) {
+ mutableConfig.insertOption(OPTION_TAKE_PICTURE_MANAGER_PROVIDER,
+ new TakePictureManager.Provider() {
+ @NonNull
+ @Override
+ public TakePictureManager newInstance(
+ @NonNull ImageCaptureControl imageCaptureControl) {
+ mTakePictureManager = new TakePictureManagerWrapper(
+ imageCaptureControl, mFakeCameras);
+ return mTakePictureManager;
+ }
+ });
+ }
+
return OptionsBundle.from(mutableConfig);
}
@@ -97,4 +138,12 @@
return CameraDevice.TEMPLATE_PREVIEW;
}
}
+
+ /**
+ * Returns the last provided {@link TakePictureManagerWrapper} instance.
+ */
+ @Nullable
+ public TakePictureManagerWrapper getTakePictureManager() {
+ return mTakePictureManager;
+ }
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/wrappers/TakePictureManagerWrapper.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/wrappers/TakePictureManagerWrapper.kt
new file mode 100644
index 0000000..0de7dba
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/wrappers/TakePictureManagerWrapper.kt
@@ -0,0 +1,269 @@
+/*
+ * 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.camera.testing.impl.wrappers
+
+import android.graphics.Bitmap
+import android.graphics.Matrix
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageCapture.OutputFileOptions
+import androidx.camera.core.ImageCapture.OutputFileResults
+import androidx.camera.core.ImageProcessingUtil
+import androidx.camera.core.ImageProxy
+import androidx.camera.core.Logger
+import androidx.camera.core.imagecapture.Bitmap2JpegBytes
+import androidx.camera.core.imagecapture.ImageCaptureControl
+import androidx.camera.core.imagecapture.ImagePipeline
+import androidx.camera.core.imagecapture.JpegBytes2Disk
+import androidx.camera.core.imagecapture.JpegBytes2Image
+import androidx.camera.core.imagecapture.RequestWithCallback
+import androidx.camera.core.imagecapture.TakePictureManager
+import androidx.camera.core.imagecapture.TakePictureManagerImpl
+import androidx.camera.core.imagecapture.TakePictureRequest
+import androidx.camera.core.processing.Packet
+import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeCameraControl
+import androidx.camera.testing.impl.ExifUtil
+import androidx.camera.testing.impl.TestImageUtil
+import androidx.camera.testing.impl.fakes.FakeCameraCaptureResult
+import androidx.camera.testing.impl.fakes.FakeImageInfo
+import androidx.camera.testing.impl.fakes.FakeImageProxy
+
+/**
+ * A [TakePictureManager] implementation wrapped around the real implementation
+ * [TakePictureManagerImpl].
+ *
+ * It is used for fake cameras and provides fake image capture results when required from a camera.
+ */
+public class TakePictureManagerWrapper(
+ imageCaptureControl: ImageCaptureControl,
+ private val fakeCameras: List<FakeCamera>
+) : TakePictureManager {
+ // Try to keep the fake as close to real as possible
+ private val managerDelegate = TakePictureManagerImpl(imageCaptureControl)
+
+ private val bitmap2JpegBytes = Bitmap2JpegBytes()
+ private val jpegBytes2Disk = JpegBytes2Disk()
+ private val jpegBytes2Image = JpegBytes2Image()
+
+ private val imageProxyQueue = ArrayDeque<ImageProxy>()
+ private val outputFileResultsQueue = ArrayDeque<ImageCapture.OutputFileResults>()
+
+ /** Whether to disable auto capture completion. */
+ public var disableAutoComplete: Boolean = false
+
+ override fun setImagePipeline(imagePipeline: ImagePipeline) {
+ managerDelegate.imagePipeline = imagePipeline
+ }
+
+ override fun offerRequest(takePictureRequest: TakePictureRequest) {
+ val listeners = mutableListOf<FakeCameraControl.OnNewCaptureRequestListener>()
+
+ fakeCameras.forEach { camera ->
+ if (camera.cameraControlInternal is FakeCameraControl) {
+ (camera.cameraControlInternal as FakeCameraControl).apply {
+ val listener =
+ FakeCameraControl.OnNewCaptureRequestListener {
+ if (!disableAutoComplete) {
+ completeCapturingRequest(this)
+ }
+ }
+ listeners.add(listener)
+ addOnNewCaptureRequestListener(listener)
+ }
+ } else {
+ Logger.w(
+ TAG,
+ "Ignoring ${camera.cameraControlInternal} as it's not FakeCameraControl!"
+ )
+ }
+ }
+
+ managerDelegate.offerRequest(takePictureRequest)
+
+ fakeCameras.forEach { camera ->
+ if (camera.cameraControlInternal is FakeCameraControl) {
+ (camera.cameraControlInternal as FakeCameraControl)
+ .removeOnNewCaptureRequestListeners(listeners)
+ } else {
+ Logger.w(
+ TAG,
+ "Ignoring ${camera.cameraControlInternal} as it's not FakeCameraControl!"
+ )
+ }
+ }
+ }
+
+ override fun pause() {
+ managerDelegate.pause()
+ }
+
+ override fun resume() {
+ managerDelegate.resume()
+ }
+
+ override fun abortRequests() {
+ managerDelegate.abortRequests()
+ }
+
+ @VisibleForTesting
+ override fun hasCapturingRequest(): Boolean = managerDelegate.hasCapturingRequest()
+
+ @VisibleForTesting
+ override fun getCapturingRequest(): RequestWithCallback? = managerDelegate.capturingRequest
+
+ @VisibleForTesting
+ override fun getIncompleteRequests(): List<RequestWithCallback> =
+ managerDelegate.incompleteRequests
+
+ @VisibleForTesting
+ override fun getImagePipeline(): ImagePipeline = managerDelegate.imagePipeline
+
+ @VisibleForTesting
+ public fun completeCapturingRequest(fakeCameraControl: FakeCameraControl) {
+ Log.d(
+ TAG,
+ "completeCapturingRequest: capturingRequest = ${managerDelegate.capturingRequest}"
+ )
+ managerDelegate.capturingRequest?.apply {
+ onCaptureStarted()
+
+ // This ensures the future from CameraControlInternal#submitStillCaptureRequests() is
+ // completed and not garbage collected later
+ // TODO - notify all the new requests, not only the last one
+ fakeCameraControl.notifyLastRequestOnCaptureCompleted(FakeCameraCaptureResult())
+
+ onImageCaptured()
+
+ takePictureRequest.also { req ->
+ val outputFileOptions = req.outputFileOptions // enables smartcast for null check
+ if (req.onDiskCallback != null && outputFileOptions != null) {
+ if (outputFileResultsQueue.isEmpty()) {
+ onFinalResult(createOutputFileResults(req, outputFileOptions))
+ } else {
+ onFinalResult(outputFileResultsQueue.first())
+ outputFileResultsQueue.removeFirst()
+ }
+ } else {
+ if (imageProxyQueue.isEmpty()) {
+ onFinalResult(createImageProxy(req))
+ } else {
+ onFinalResult(imageProxyQueue.first())
+ imageProxyQueue.removeFirst()
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Enqueues an [ImageProxy] to be used as result for the next image capture with
+ * [ImageCapture.OnImageCapturedCallback].
+ *
+ * Note that the provided [ImageProxy] is consumed by next image capture and is not available
+ * for following captures. If no result is available during a capture, CameraX will create a
+ * fake image by itself and provide result based on that.
+ */
+ public fun enqueueImageProxy(imageProxy: ImageProxy) {
+ imageProxyQueue.add(imageProxy)
+ }
+
+ /**
+ * Enqueues an [OutputFileResults] to be used as result for the next image capture with
+ * [ImageCapture.OnImageSavedCallback].
+ *
+ * Note that the provided [OutputFileResults] is consumed by next image capture and is not
+ * available for following captures. If no result is available during a capture, CameraX will
+ * create a fake image by itself and provide result based on that.
+ */
+ public fun enqueueOutputFileResults(outputFileResults: ImageCapture.OutputFileResults) {
+ outputFileResultsQueue.add(outputFileResults)
+ }
+
+ private fun createOutputFileResults(
+ takePictureRequest: TakePictureRequest,
+ outputFileOptions: OutputFileOptions
+ ): ImageCapture.OutputFileResults {
+ // TODO - Take a bitmap as input and use that directly
+ val bytesPacket =
+ takePictureRequest.convertBitmapToBytes(
+ TestImageUtil.createBitmap(
+ takePictureRequest.cropRect.width(),
+ takePictureRequest.cropRect.height()
+ )
+ )
+ return jpegBytes2Disk.apply(JpegBytes2Disk.In.of(bytesPacket, outputFileOptions))
+ }
+
+ private fun createImageProxy(
+ takePictureRequest: TakePictureRequest,
+ ): ImageProxy {
+ // TODO - Take a bitmap as input and use that directly
+ val bitmap =
+ TestImageUtil.createBitmap(
+ takePictureRequest.cropRect.width(),
+ takePictureRequest.cropRect.height()
+ )
+ if (canLoadImageProcessingUtilJniLib()) {
+ val bytesPacket =
+ takePictureRequest.convertBitmapToBytes(
+ TestImageUtil.createBitmap(
+ takePictureRequest.cropRect.width(),
+ takePictureRequest.cropRect.height()
+ )
+ )
+ return jpegBytes2Image.apply(bytesPacket).data
+ } else {
+ return bitmap.toFakeImageProxy()
+ }
+ }
+
+ private fun Bitmap.toFakeImageProxy(): ImageProxy {
+ return FakeImageProxy(FakeImageInfo(), this)
+ }
+
+ private fun TakePictureRequest.convertBitmapToBytes(bitmap: Bitmap): Packet<ByteArray> {
+ val inputPacket =
+ Packet.of(
+ bitmap,
+ ExifUtil.createExif(
+ TestImageUtil.createJpegBytes(cropRect.width(), cropRect.height())
+ ),
+ cropRect,
+ rotationDegrees,
+ Matrix(),
+ FakeCameraCaptureResult()
+ )
+
+ return bitmap2JpegBytes.apply(Bitmap2JpegBytes.In.of(inputPacket, jpegQuality))
+ }
+
+ private fun canLoadImageProcessingUtilJniLib(): Boolean {
+ try {
+ System.loadLibrary(ImageProcessingUtil.JNI_LIB_NAME)
+ return true
+ } catch (e: UnsatisfiedLinkError) {
+ Logger.d(TAG, "canLoadImageProcessingUtilJniLib", e)
+ return false
+ }
+ }
+
+ private companion object {
+ private const val TAG = "TakePictureManagerWrap"
+ }
+}
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraControlTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraControlTest.java
index a61ada7..d7264b0 100644
--- a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraControlTest.java
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraControlTest.java
@@ -173,6 +173,42 @@
}
@Test
+ public void notifiesLastRequestOnCaptureCompleted() {
+ CameraCaptureResult captureResult = new FakeCameraCaptureResult();
+
+ CountDownLatch latch = new CountDownLatch(1);
+ List<CameraCaptureResult> resultList = new ArrayList<>();
+ CaptureConfig captureConfig1 = createCaptureConfig(new CameraCaptureCallback() {
+ @Override
+ public void onCaptureCompleted(int captureConfigId,
+ @NonNull CameraCaptureResult cameraCaptureResult) {
+ resultList.add(cameraCaptureResult);
+ }
+ }, new CameraCaptureCallback() {
+ @Override
+ public void onCaptureCompleted(int captureConfigId,
+ @NonNull CameraCaptureResult cameraCaptureResult) {
+ resultList.add(cameraCaptureResult);
+ }
+ });
+ CaptureConfig captureConfig2 = createCaptureConfig(new CameraCaptureCallback() {
+ @Override
+ public void onCaptureCompleted(int captureConfigId,
+ @NonNull CameraCaptureResult cameraCaptureResult) {
+ resultList.add(cameraCaptureResult);
+ latch.countDown();
+ }
+ });
+
+ mCameraControl.submitStillCaptureRequests(Arrays.asList(captureConfig1, captureConfig2),
+ ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY, ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH);
+ mCameraControl.notifyLastRequestOnCaptureCompleted(captureResult);
+
+ awaitLatch(latch);
+ assertThat(resultList).containsExactlyElementsIn(Collections.singletonList(captureResult));
+ }
+
+ @Test
public void canUpdateFlashModeToOff() {
mCameraControl.setFlashMode(ImageCapture.FLASH_MODE_OFF);
assertThat(mCameraControl.getFlashMode()).isEqualTo(ImageCapture.FLASH_MODE_OFF);
@@ -319,7 +355,7 @@
List<CaptureConfig> notifiedCaptureConfigs = new ArrayList<>();
CountDownLatch latch = new CountDownLatch(1);
- mCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+ mCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
notifiedCaptureConfigs.addAll(captureConfigs);
latch.countDown();
});
@@ -335,7 +371,7 @@
AtomicReference<Thread> listenerThread = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(1);
- mCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+ mCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
listenerThread.set(Thread.currentThread());
latch.countDown();
});
@@ -350,7 +386,7 @@
AtomicReference<Thread> listenerThread = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(1);
- mCameraControl.setOnNewCaptureRequestListener(CameraXExecutors.mainThreadExecutor(),
+ mCameraControl.addOnNewCaptureRequestListener(CameraXExecutors.mainThreadExecutor(),
captureConfigs -> {
listenerThread.set(Thread.currentThread());
latch.countDown();
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt
index c361fe0..3defd64b 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt
@@ -70,8 +70,6 @@
import com.google.common.util.concurrent.ListenableFuture
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executor
-import java.util.concurrent.ExecutorService
-import java.util.concurrent.Executors
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference
@@ -273,6 +271,8 @@
instrumentation.runOnMainSync {
val previewView = PreviewView(context)
+ // Specifies the content description and uses it to find the view to click
+ previewView.contentDescription = previewView.hashCode().toString()
clickEventHelper = ClickEventHelper(previewView)
previewView.setOnTouchListener(clickEventHelper)
previewView.controller = fakeController
@@ -311,7 +311,6 @@
private var uiDevice: UiDevice? = null
private var limitedRetryCount = 0
private var retriedCounter = 0
- private var executor: ExecutorService? = null
override fun onTouch(view: View, event: MotionEvent): Boolean {
if (view != targetView) {
@@ -337,7 +336,6 @@
uiDevice = null
limitedRetryCount = 0
retriedCounter = 0
- executor?.shutdown()
synchronized(lock) { isPerformingClick = false }
}
@@ -359,34 +357,16 @@
}
}
- executor = Executors.newSingleThreadExecutor()
limitedRetryCount = retryCount
retriedCounter = 0
this.uiDevice = uiDevice
performSingleClickInternal()
}
- private fun performSingleClickInternal() {
- executor!!.execute {
- var needClearContentDescription = false
- val originalContentDescription = targetView.contentDescription
-
- if (originalContentDescription == null || originalContentDescription.isEmpty()) {
- needClearContentDescription = true
- targetView.contentDescription = targetView.hashCode().toString()
- }
-
- uiDevice!!
- .findObject(
- UiSelector().descriptionContains(targetView.contentDescription.toString())
- )
- .click()
-
- if (needClearContentDescription) {
- targetView.contentDescription = originalContentDescription
- }
- }
- }
+ private fun performSingleClickInternal() =
+ uiDevice!!
+ .findObject(UiSelector().descriptionContains(targetView.hashCode().toString()))
+ .click()
}
@Test
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt
index af52c44..ef4fe9e 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt
@@ -42,7 +42,6 @@
import kotlinx.coroutines.withContext
import org.junit.After
import org.junit.Before
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
@@ -87,7 +86,7 @@
@Test
fun canSubmitTakePictureRequest(): Unit = runBlocking {
val countDownLatch = CountDownLatch(1)
- cameraControl.setOnNewCaptureRequestListener { countDownLatch.countDown() }
+ cameraControl.addOnNewCaptureRequestListener { countDownLatch.countDown() }
imageCapture.takePicture(CameraXExecutors.directExecutor(), FakeOnImageCapturedCallback())
@@ -96,10 +95,9 @@
// Duplicate to ImageCaptureTest on core-test-app JVM tests, any change here may need to be
// reflected there too
- @Ignore("b/318314454")
@Test
fun canCreateBitmapFromTakenImage_whenImageCapturedCallbackIsUsed(): Unit = runBlocking {
- val callback = FakeOnImageCapturedCallback()
+ val callback = FakeOnImageCapturedCallback(closeImageOnSuccess = false)
imageCapture.takePicture(CameraXExecutors.directExecutor(), callback)
callback.awaitCapturesAndAssert(capturedImagesCount = 1)
callback.results.first().image.toBitmap()
@@ -107,7 +105,6 @@
// Duplicate to ImageCaptureTest on core-test-app JVM tests, any change here may need to be
// reflected there too
- @Ignore("b/318314454")
@Test
fun canFindImage_whenFileStorageAndImageSavedCallbackIsUsed(): Unit = runBlocking {
val saveLocation = temporaryFolder.newFile()
@@ -126,7 +123,6 @@
// Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
// need to be reflected there too
- @Ignore("b/318314454")
@Test
fun canFindImage_whenMediaStoreAndImageSavedCallbackIsUsed(): Unit = runBlocking {
val initialCount = getMediaStoreCameraXImageCount()
diff --git a/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt
index 459187c..15cbefe 100644
--- a/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt
@@ -44,7 +44,6 @@
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
@@ -92,7 +91,7 @@
@Test
fun canSubmitTakePictureRequest(): Unit = runTest {
val countDownLatch = CountDownLatch(1)
- cameraControl.setOnNewCaptureRequestListener { countDownLatch.countDown() }
+ cameraControl.addOnNewCaptureRequestListener { countDownLatch.countDown() }
imageCapture.takePicture(CameraXExecutors.directExecutor(), FakeOnImageCapturedCallback())
@@ -101,7 +100,6 @@
// Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
// need to be reflected there too
- @Ignore("b/318314454")
@Test
fun canCreateBitmapFromTakenImage_whenImageCapturedCallbackIsUsed(): Unit = runTest {
val callback = FakeOnImageCapturedCallback()
@@ -112,7 +110,6 @@
// Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
// need to be reflected there too
- @Ignore("b/318314454")
@Test
fun canFindImage_whenFileStorageAndImageSavedCallbackIsUsed(): Unit = runTest {
val saveLocation = temporaryFolder.newFile()
@@ -131,7 +128,6 @@
// Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
// need to be reflected there too
- @Ignore("b/318314454")
@Test
fun canFindFakeImageUri_whenMediaStoreAndImageSavedCallbackIsUsed(): Unit = runBlocking {
val callback = FakeOnImageSavedCallback()
diff --git a/car/app/app/api/1.7.0-beta02.txt b/car/app/app/api/1.7.0-beta02.txt
index 2bc33d2..6939bce 100644
--- a/car/app/app/api/1.7.0-beta02.txt
+++ b/car/app/app/api/1.7.0-beta02.txt
@@ -900,6 +900,7 @@
field public static final String KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED";
field public static final String KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW";
field public static final String KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED";
+ field public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
field public static final String KEY_ROOT_HINT_MEDIA_SESSION_API = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_SESSION_API";
}
diff --git a/car/app/app/api/current.ignore b/car/app/app/api/current.ignore
index 0ba421d..e1d8307 100644
--- a/car/app/app/api/current.ignore
+++ b/car/app/app/api/current.ignore
@@ -1,3 +1,3 @@
// Baseline format: 1.0
-AddedClass: androidx.car.app.mediaextensions.MediaIntentExtras:
- Added class androidx.car.app.mediaextensions.MediaIntentExtras
+AddedField: androidx.car.app.mediaextensions.MediaBrowserExtras#KEY_ROOT_HINT_MEDIA_HOST_VERSION:
+ Added field androidx.car.app.mediaextensions.MediaBrowserExtras.KEY_ROOT_HINT_MEDIA_HOST_VERSION
diff --git a/car/app/app/api/current.txt b/car/app/app/api/current.txt
index 2bc33d2..6939bce 100644
--- a/car/app/app/api/current.txt
+++ b/car/app/app/api/current.txt
@@ -900,6 +900,7 @@
field public static final String KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED";
field public static final String KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW";
field public static final String KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED";
+ field public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
field public static final String KEY_ROOT_HINT_MEDIA_SESSION_API = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_SESSION_API";
}
diff --git a/car/app/app/api/restricted_1.7.0-beta02.txt b/car/app/app/api/restricted_1.7.0-beta02.txt
index 2bc33d2..6939bce 100644
--- a/car/app/app/api/restricted_1.7.0-beta02.txt
+++ b/car/app/app/api/restricted_1.7.0-beta02.txt
@@ -900,6 +900,7 @@
field public static final String KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED";
field public static final String KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW";
field public static final String KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED";
+ field public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
field public static final String KEY_ROOT_HINT_MEDIA_SESSION_API = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_SESSION_API";
}
diff --git a/car/app/app/api/restricted_current.ignore b/car/app/app/api/restricted_current.ignore
index 0ba421d..e1d8307 100644
--- a/car/app/app/api/restricted_current.ignore
+++ b/car/app/app/api/restricted_current.ignore
@@ -1,3 +1,3 @@
// Baseline format: 1.0
-AddedClass: androidx.car.app.mediaextensions.MediaIntentExtras:
- Added class androidx.car.app.mediaextensions.MediaIntentExtras
+AddedField: androidx.car.app.mediaextensions.MediaBrowserExtras#KEY_ROOT_HINT_MEDIA_HOST_VERSION:
+ Added field androidx.car.app.mediaextensions.MediaBrowserExtras.KEY_ROOT_HINT_MEDIA_HOST_VERSION
diff --git a/car/app/app/api/restricted_current.txt b/car/app/app/api/restricted_current.txt
index 2bc33d2..6939bce 100644
--- a/car/app/app/api/restricted_current.txt
+++ b/car/app/app/api/restricted_current.txt
@@ -900,6 +900,7 @@
field public static final String KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED";
field public static final String KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW";
field public static final String KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED";
+ field public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
field public static final String KEY_ROOT_HINT_MEDIA_SESSION_API = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_SESSION_API";
}
diff --git a/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaBrowserExtras.java b/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaBrowserExtras.java
index 473f447..413cd71 100644
--- a/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaBrowserExtras.java
+++ b/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaBrowserExtras.java
@@ -37,6 +37,17 @@
/**
* {@link Bundle} key used in the rootHints bundle passed to
+ * {@link androidx.media.MediaBrowserServiceCompat#onGetRoot(String, int, Bundle)}
+ * to indicate the version of the caller. Note that this should only be used for analytics and
+ * is different than {@link #KEY_ROOT_HINT_MEDIA_SESSION_API}.
+ *
+ * <p>TYPE: string - the version info.
+ */
+ public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION =
+ "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
+
+ /**
+ * {@link Bundle} key used in the rootHints bundle passed to
* {@link androidx.media.MediaBrowserServiceCompat#onGetRoot(String, int, Bundle)} to indicate
* which version of the media api is used by the caller
*
diff --git a/compose/animation/animation/build.gradle b/compose/animation/animation/build.gradle
index f968628..c7595d2 100644
--- a/compose/animation/animation/build.gradle
+++ b/compose/animation/animation/build.gradle
@@ -127,10 +127,6 @@
samples(project(":compose:animation:animation:animation-samples"))
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
android {
compileSdk 35
namespace "androidx.compose.animation"
diff --git a/compose/animation/animation/integration-tests/animation-demos/build.gradle b/compose/animation/animation/integration-tests/animation-demos/build.gradle
index 49c87a5..995c88c 100644
--- a/compose/animation/animation/integration-tests/animation-demos/build.gradle
+++ b/compose/animation/animation/integration-tests/animation-demos/build.gradle
@@ -36,10 +36,6 @@
debugImplementation(project(":compose:ui:ui-tooling"))
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
android {
compileSdk 35
namespace "androidx.compose.animation.demos"
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/CraneDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/CraneDemo.kt
index 375751a..803c694 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/CraneDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/CraneDemo.kt
@@ -70,7 +70,10 @@
val avatar = remember {
movableContentWithReceiverOf<SceneScope> {
Box(
- Modifier.sharedElementBasedOnProgress(progressProvider)
+ Modifier.sharedElementBasedOnProgress(
+ this@movableContentWithReceiverOf,
+ progressProvider
+ )
.background(Color(0xffff6f69), RoundedCornerShape(20))
.fillMaxSize()
)
@@ -81,7 +84,10 @@
movableContentWithReceiverOf<SceneScope, @Composable () -> Unit> { child ->
Surface(
modifier =
- Modifier.sharedElementBasedOnProgress(progressProvider)
+ Modifier.sharedElementBasedOnProgress(
+ this@movableContentWithReceiverOf,
+ progressProvider
+ )
.background(Color(0xfffdedac)),
color = Color(0xfffdedac),
shape = RoundedCornerShape(10.dp)
@@ -166,44 +172,53 @@
val progress: Float
}
-context(LookaheadScope)
@SuppressLint("PrimitiveInCollection")
-fun <T> Modifier.sharedElementBasedOnProgress(provider: ProgressProvider<T>) = composed {
- val sizeMap = remember { mutableMapOf<T, IntSize>() }
- val offsetMap = remember { mutableMapOf<T, Offset>() }
- val calculateSize: (IntSize) -> IntSize = {
- sizeMap[provider.targetState] = it
- val (width, height) =
- lerp(
- sizeMap[provider.initialState]!!.toSize(),
- sizeMap[provider.targetState]!!.toSize(),
- provider.progress
- )
- IntSize(width.roundToInt(), height.roundToInt())
- }
-
- val calculateOffset: Placeable.PlacementScope.(ApproachMeasureScope) -> IntOffset = {
- with(it) {
- coordinates?.let {
- offsetMap[provider.targetState] =
- lookaheadScopeCoordinates.localLookaheadPositionOf(it)
- val lerpedOffset =
+fun <T> Modifier.sharedElementBasedOnProgress(
+ lookaheadScope: LookaheadScope,
+ provider: ProgressProvider<T>
+) =
+ with(lookaheadScope) {
+ composed {
+ val sizeMap = remember { mutableMapOf<T, IntSize>() }
+ val offsetMap = remember { mutableMapOf<T, Offset>() }
+ val calculateSize: (IntSize) -> IntSize = {
+ sizeMap[provider.targetState] = it
+ val (width, height) =
lerp(
- offsetMap[provider.initialState]!!,
- offsetMap[provider.targetState]!!,
+ sizeMap[provider.initialState]!!.toSize(),
+ sizeMap[provider.targetState]!!.toSize(),
provider.progress
)
- val currentOffset = lookaheadScopeCoordinates.localPositionOf(it, Offset.Zero)
- (lerpedOffset - currentOffset).round()
- } ?: IntOffset(0, 0)
+ IntSize(width.roundToInt(), height.roundToInt())
+ }
+
+ val calculateOffset: Placeable.PlacementScope.(ApproachMeasureScope) -> IntOffset = {
+ with(it) {
+ coordinates?.let {
+ offsetMap[provider.targetState] =
+ lookaheadScopeCoordinates.localLookaheadPositionOf(it)
+ val lerpedOffset =
+ lerp(
+ offsetMap[provider.initialState]!!,
+ offsetMap[provider.targetState]!!,
+ provider.progress
+ )
+ val currentOffset =
+ lookaheadScopeCoordinates.localPositionOf(
+ it,
+ androidx.compose.ui.geometry.Offset.Zero
+ )
+ (lerpedOffset - currentOffset).round()
+ } ?: IntOffset(0, 0)
+ }
+ }
+ this.approachLayout({ provider.progress != 1f }) { measurable, _ ->
+ val (width, height) = calculateSize(lookaheadSize)
+ val animatedConstraints = androidx.compose.ui.unit.Constraints.fixed(width, height)
+ val placeable = measurable.measure(animatedConstraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(calculateOffset(this@approachLayout))
+ }
+ }
}
}
- this.approachLayout({ provider.progress != 1f }) { measurable, _ ->
- val (width, height) = calculateSize(lookaheadSize)
- val animatedConstraints = Constraints.fixed(width, height)
- val placeable = measurable.measure(animatedConstraints)
- layout(placeable.width, placeable.height) {
- placeable.place(calculateOffset(this@approachLayout))
- }
- }
-}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithDisappearingMoveableContentDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithDisappearingMoveableContentDemo.kt
index 5562c9c..0879163 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithDisappearingMoveableContentDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithDisappearingMoveableContentDemo.kt
@@ -71,7 +71,9 @@
Box(Modifier.padding(start = 50.dp, top = 200.dp, bottom = 100.dp)) {
val icon = remember { movableContentOf<Boolean> { MyIcon(it) } }
val title = remember {
- movableContentOf<Boolean> { Title(visible = it, Modifier.animatePosition()) }
+ movableContentOf<Boolean> {
+ Title(visible = it, Modifier.animatePosition(this@LookaheadScope))
+ }
}
val details = remember { movableContentOf<Boolean> { Details(visible = it) } }
@@ -129,39 +131,41 @@
}
}
-context(LookaheadScope)
@OptIn(ExperimentalAnimatableApi::class)
@SuppressLint("UnnecessaryComposedModifier")
-fun Modifier.animatePosition(): Modifier = composed {
- val offsetAnimation = remember { DeferredTargetAnimation(IntOffset.VectorConverter) }
- val coroutineScope = rememberCoroutineScope()
- this.approachLayout(
- isMeasurementApproachInProgress = { false },
- isPlacementApproachInProgress = {
- offsetAnimation.updateTarget(
- lookaheadScopeCoordinates.localLookaheadPositionOf(it).round(),
- coroutineScope,
- spring(stiffness = Spring.StiffnessMediumLow)
- )
- !offsetAnimation.isIdle
- }
- ) { measurable, constraints ->
- measurable.measure(constraints).run {
- layout(width, height) {
- val (x, y) =
- coordinates?.let { coordinates ->
- val origin = this.lookaheadScopeCoordinates
- val animOffset =
- offsetAnimation.updateTarget(
- origin.localLookaheadPositionOf(coordinates).round(),
- coroutineScope,
- spring(stiffness = Spring.StiffnessMediumLow),
- )
- val currentOffset = origin.localPositionOf(coordinates, Offset.Zero)
- animOffset - currentOffset.round()
- } ?: IntOffset.Zero
- place(x, y)
+fun Modifier.animatePosition(lookaheadScope: LookaheadScope): Modifier =
+ with(lookaheadScope) {
+ composed {
+ val offsetAnimation = remember { DeferredTargetAnimation(IntOffset.VectorConverter) }
+ val coroutineScope = rememberCoroutineScope()
+ this.approachLayout(
+ isMeasurementApproachInProgress = { false },
+ isPlacementApproachInProgress = {
+ offsetAnimation.updateTarget(
+ lookaheadScopeCoordinates.localLookaheadPositionOf(it).round(),
+ coroutineScope,
+ spring(stiffness = Spring.StiffnessMediumLow)
+ )
+ !offsetAnimation.isIdle
+ }
+ ) { measurable, constraints ->
+ measurable.measure(constraints).run {
+ layout(width, height) {
+ val (x, y) =
+ coordinates?.let { coordinates ->
+ val origin = this.lookaheadScopeCoordinates
+ val animOffset =
+ offsetAnimation.updateTarget(
+ origin.localLookaheadPositionOf(coordinates).round(),
+ coroutineScope,
+ spring(stiffness = Spring.StiffnessMediumLow),
+ )
+ val currentOffset = origin.localPositionOf(coordinates, Offset.Zero)
+ animOffset - currentOffset.round()
+ } ?: IntOffset.Zero
+ place(x, y)
+ }
+ }
}
}
}
-}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithMovableContentDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithMovableContentDemo.kt
index a02fc9a..d294736 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithMovableContentDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithMovableContentDemo.kt
@@ -18,10 +18,6 @@
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.animateBounds
-import androidx.compose.animation.core.DeferredTargetAnimation
-import androidx.compose.animation.core.ExperimentalAnimatableApi
-import androidx.compose.animation.core.VectorConverter
-import androidx.compose.animation.core.spring
import androidx.compose.animation.demos.fancy.AnimatedDotsDemo
import androidx.compose.animation.demos.statetransition.InfiniteProgress
import androidx.compose.animation.demos.statetransition.InfinitePulsingHeart
@@ -43,22 +39,14 @@
import androidx.compose.runtime.movableContentWithReceiverOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.LookaheadScope
-import androidx.compose.ui.layout.approachLayout
import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.round
@OptIn(ExperimentalSharedTransitionApi::class)
@Preview
@@ -157,37 +145,5 @@
}
}
-context(LookaheadScope)
-@OptIn(ExperimentalAnimatableApi::class)
-fun Modifier.animateBoundsInScope(): Modifier = composed {
- val sizeAnim = remember { DeferredTargetAnimation(IntSize.VectorConverter) }
- val offsetAnim = remember { DeferredTargetAnimation(IntOffset.VectorConverter) }
- val scope = rememberCoroutineScope()
- this.approachLayout(
- isMeasurementApproachInProgress = {
- sizeAnim.updateTarget(it, scope)
- !sizeAnim.isIdle
- },
- isPlacementApproachInProgress = {
- val target = lookaheadScopeCoordinates.localLookaheadPositionOf(it)
- offsetAnim.updateTarget(target.round(), scope, spring())
- !offsetAnim.isIdle
- }
- ) { measurable, _ ->
- val (animWidth, animHeight) = sizeAnim.updateTarget(lookaheadSize, scope, spring())
- measurable.measure(Constraints.fixed(animWidth, animHeight)).run {
- layout(width, height) {
- coordinates?.let {
- val target = lookaheadScopeCoordinates.localLookaheadPositionOf(it).round()
- val animOffset = offsetAnim.updateTarget(target, scope, spring())
- val current = lookaheadScopeCoordinates.localPositionOf(it, Offset.Zero).round()
- val (x, y) = animOffset - current
- place(x, y)
- } ?: place(0, 0)
- }
- }
- }
-}
-
private val colors =
listOf(Color(0xffff6f69), Color(0xffffcc5c), Color(0xff264653), Color(0xff2a9d84))
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithSubcompose.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithSubcompose.kt
index 2ea3bd2..ed920ea 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithSubcompose.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithSubcompose.kt
@@ -55,13 +55,15 @@
Text(if (shouldAnimate) "Stop animating bounds" else "Animate bounds")
}
SubcomposeLayout(
- Modifier.background(colors[3]).conditionallyAnimateBounds(shouldAnimate)
+ Modifier.background(colors[3])
+ .conditionallyAnimateBounds(this@LookaheadScope, shouldAnimate)
) {
val constraints = it.copy(minWidth = 0)
val placeable =
subcompose(0) {
Box(
Modifier.conditionallyAnimateBounds(
+ this@LookaheadScope,
shouldAnimate,
Modifier.width(if (isWide) 150.dp else 70.dp)
.requiredHeight(400.dp)
@@ -75,6 +77,7 @@
subcompose(1) {
Box(
Modifier.conditionallyAnimateBounds(
+ this@LookaheadScope,
shouldAnimate,
Modifier.width(if (isWide) 150.dp else 70.dp)
.requiredHeight(400.dp)
@@ -91,6 +94,7 @@
Box(
Modifier.width(totalWidth.toDp())
.conditionallyAnimateBounds(
+ this@LookaheadScope,
shouldAnimate,
Modifier.height(if (isWide) 150.dp else 70.dp)
)
@@ -108,12 +112,12 @@
}
}
-context(LookaheadScope)
@OptIn(ExperimentalSharedTransitionApi::class)
private fun Modifier.conditionallyAnimateBounds(
+ lookaheadScope: LookaheadScope,
shouldAnimate: Boolean,
modifier: Modifier = Modifier
-) = if (shouldAnimate) this.animateBounds(this@LookaheadScope, modifier) else this.then(modifier)
+) = if (shouldAnimate) this.animateBounds(lookaheadScope, modifier) else this.then(modifier)
private val colors =
listOf(Color(0xffff6f69), Color(0xffffcc5c), Color(0xff2a9d84), Color(0xff264653))
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/SceneHostExperiment.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/SceneHostExperiment.kt
index 1b94fc1..ecd48cd 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/SceneHostExperiment.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/SceneHostExperiment.kt
@@ -106,6 +106,9 @@
layout(placeable.width, placeable.height) {
val (x, y) =
offsetAnimation.updateTargetBasedOnCoordinates(
+ this@SceneScope,
+ this@layout,
+ this@with,
spring(stiffness = Spring.StiffnessMediumLow),
)
coordinates?.let {
@@ -153,25 +156,32 @@
}
}
-context(LookaheadScope, Placeable.PlacementScope, CoroutineScope)
@OptIn(ExperimentalAnimatableApi::class)
internal fun DeferredTargetAnimation<IntOffset, AnimationVector2D>.updateTargetBasedOnCoordinates(
+ lookaheadScope: LookaheadScope,
+ placementScope: Placeable.PlacementScope,
+ coroutineScope: CoroutineScope,
animationSpec: FiniteAnimationSpec<IntOffset>,
): IntOffset {
- coordinates?.let { coordinates ->
- with(this@PlacementScope) {
- val targetOffset = lookaheadScopeCoordinates.localLookaheadPositionOf(coordinates)
- val animOffset =
- updateTarget(
- targetOffset.round(),
- this@CoroutineScope,
- animationSpec,
- )
- val current =
- lookaheadScopeCoordinates.localPositionOf(coordinates, Offset.Zero).round()
- return (animOffset - current)
+ with(lookaheadScope) {
+ with(placementScope) {
+ coordinates?.let { coordinates ->
+ with(placementScope) {
+ val targetOffset =
+ lookaheadScopeCoordinates.localLookaheadPositionOf(coordinates)
+ val animOffset =
+ updateTarget(
+ targetOffset.round(),
+ coroutineScope,
+ animationSpec,
+ )
+ val current =
+ lookaheadScopeCoordinates.localPositionOf(coordinates, Offset.Zero).round()
+ return (animOffset - current)
+ }
+ }
+
+ return IntOffset.Zero
}
}
-
- return IntOffset.Zero
}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/ContainerTransformDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/ContainerTransformDemo.kt
index 25b32da..78916a7 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/ContainerTransformDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/ContainerTransformDemo.kt
@@ -90,180 +90,208 @@
) {
// TODO: Double check on container transform scrolling
if (it != null) {
- DetailView(model = model, selected = it, model.items[6])
+ DetailView(
+ this@AnimatedContent,
+ this@SharedTransitionLayout,
+ model = model,
+ selected = it,
+ model.items[6]
+ )
} else {
- GridView(model = model)
+ GridView(this@AnimatedContent, this@SharedTransitionLayout, model = model)
}
}
}
}
-context(SharedTransitionScope, AnimatedVisibilityScope)
@Composable
-fun Details(kitty: Kitty) {
- Column(
- Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp)
- .fillMaxHeight()
- .wrapContentHeight(Alignment.Top)
- .fillMaxWidth()
- .background(Color.White)
- .padding(start = 10.dp, end = 10.dp)
- ) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Column {
- Spacer(Modifier.size(20.dp))
- Text(
- kitty.name,
- fontSize = 25.sp,
- modifier =
- Modifier.padding(start = 10.dp)
- .sharedBounds(
- rememberSharedContentState(key = kitty.name + kitty.id),
- this@AnimatedVisibilityScope
- )
- )
- Text(
- kitty.breed,
- fontSize = 22.sp,
- color = Color.Gray,
- modifier =
- Modifier.padding(start = 10.dp)
- .sharedBounds(
- rememberSharedContentState(key = kitty.breed + kitty.id),
- this@AnimatedVisibilityScope
- )
+fun Details(
+ sharedTransitionScope: SharedTransitionScope,
+ animatedVisibilityScope: AnimatedVisibilityScope,
+ kitty: Kitty
+) {
+ with(sharedTransitionScope) {
+ Column(
+ Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp)
+ .fillMaxHeight()
+ .wrapContentHeight(Alignment.Top)
+ .fillMaxWidth()
+ .background(Color.White)
+ .padding(start = 10.dp, end = 10.dp)
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Column {
+ Spacer(Modifier.size(20.dp))
+ Text(
+ kitty.name,
+ fontSize = 25.sp,
+ modifier =
+ Modifier.padding(start = 10.dp)
+ .sharedBounds(
+ rememberSharedContentState(key = kitty.name + kitty.id),
+ animatedVisibilityScope
+ )
+ )
+ Text(
+ kitty.breed,
+ fontSize = 22.sp,
+ color = Color.Gray,
+ modifier =
+ Modifier.padding(start = 10.dp)
+ .sharedBounds(
+ rememberSharedContentState(key = kitty.breed + kitty.id),
+ animatedVisibilityScope
+ )
+ )
+ Spacer(Modifier.size(10.dp))
+ }
+ Spacer(Modifier.weight(1f))
+ Icon(
+ Icons.Outlined.Favorite,
+ contentDescription = null,
+ Modifier.background(Color(0xffffddee), CircleShape).padding(10.dp)
)
Spacer(Modifier.size(10.dp))
}
- Spacer(Modifier.weight(1f))
- Icon(
- Icons.Outlined.Favorite,
- contentDescription = null,
- Modifier.background(Color(0xffffddee), CircleShape).padding(10.dp)
+ Box(
+ modifier =
+ Modifier.padding(bottom = 10.dp)
+ .height(2.dp)
+ .fillMaxWidth()
+ .background(Color(0xffeeeeee))
)
- Spacer(Modifier.size(10.dp))
+ Text(
+ text =
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent fringilla" +
+ " mollis efficitur. Maecenas sit amet urna eu urna blandit suscipit efficitur" +
+ " eget mauris. Nullam eget aliquet ligula. Nunc id euismod elit. Morbi aliquam" +
+ " enim eros, eget consequat dolor consequat id. Quisque elementum faucibus" +
+ " congue. Curabitur mollis aliquet turpis, ut pellentesque justo eleifend nec.\n" +
+ "\n" +
+ "Suspendisse ac consequat turpis, euismod lacinia quam. Nulla lacinia tellus" +
+ " eu felis tristique ultricies. Vivamus et ultricies dolor. Orci varius" +
+ " natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus." +
+ " Ut gravida porttitor arcu elementum elementum. Phasellus ultrices vel turpis" +
+ " volutpat mollis. Vivamus leo diam, placerat quis leo efficitur, ultrices" +
+ " placerat ex. Nullam mollis et metus ac ultricies. Ut ligula metus, congue" +
+ " gravida metus in, vestibulum posuere velit. Sed et ex nisl. Fusce tempor" +
+ " odio eget sapien pellentesque, sed cursus velit fringilla. Nullam odio" +
+ " ipsum, eleifend non consectetur vitae, congue id libero. Etiam tincidunt" +
+ " mauris at urna dictum ornare.\n" +
+ "\n" +
+ "Etiam at facilisis ex. Sed quis arcu diam. Quisque semper pharetra leo eget" +
+ " fermentum. Nulla dapibus eget mi id porta. Nunc quis sodales nulla, eget" +
+ " commodo sem. Donec lacus enim, pharetra non risus nec, eleifend ultrices" +
+ " augue. Donec sit amet orci porttitor, auctor mauris et, facilisis dolor." +
+ " Nullam mattis luctus orci at pulvinar.\n" +
+ "\n" +
+ "Sed accumsan est massa, ut aliquam nulla dignissim id. Suspendisse in urna" +
+ " condimentum, convallis purus at, molestie nisi. In hac habitasse platea" +
+ " dictumst. Pellentesque id justo quam. Cras iaculis tellus libero, eu" +
+ " feugiat ex pharetra eget. Nunc ultrices, magna ut gravida egestas, mauris" +
+ " justo blandit sapien, eget congue nisi felis congue diam. Mauris at felis" +
+ " vitae erat porta auctor. Pellentesque iaculis sem metus. Phasellus quam" +
+ " neque, congue at est eget, sodales interdum justo. Aenean a pharetra dui." +
+ " Morbi odio nibh, hendrerit vulputate odio eget, sollicitudin egestas ex." +
+ " Fusce nisl ex, fermentum a ultrices id, rhoncus vitae urna. Aliquam quis" +
+ " lobortis turpis.\n" +
+ "\n",
+ color = Color.Gray,
+ fontSize = 15.sp,
+ )
}
- Box(
- modifier =
- Modifier.padding(bottom = 10.dp)
- .height(2.dp)
- .fillMaxWidth()
- .background(Color(0xffeeeeee))
- )
- Text(
- text =
- "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent fringilla" +
- " mollis efficitur. Maecenas sit amet urna eu urna blandit suscipit efficitur" +
- " eget mauris. Nullam eget aliquet ligula. Nunc id euismod elit. Morbi aliquam" +
- " enim eros, eget consequat dolor consequat id. Quisque elementum faucibus" +
- " congue. Curabitur mollis aliquet turpis, ut pellentesque justo eleifend nec.\n" +
- "\n" +
- "Suspendisse ac consequat turpis, euismod lacinia quam. Nulla lacinia tellus" +
- " eu felis tristique ultricies. Vivamus et ultricies dolor. Orci varius" +
- " natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus." +
- " Ut gravida porttitor arcu elementum elementum. Phasellus ultrices vel turpis" +
- " volutpat mollis. Vivamus leo diam, placerat quis leo efficitur, ultrices" +
- " placerat ex. Nullam mollis et metus ac ultricies. Ut ligula metus, congue" +
- " gravida metus in, vestibulum posuere velit. Sed et ex nisl. Fusce tempor" +
- " odio eget sapien pellentesque, sed cursus velit fringilla. Nullam odio" +
- " ipsum, eleifend non consectetur vitae, congue id libero. Etiam tincidunt" +
- " mauris at urna dictum ornare.\n" +
- "\n" +
- "Etiam at facilisis ex. Sed quis arcu diam. Quisque semper pharetra leo eget" +
- " fermentum. Nulla dapibus eget mi id porta. Nunc quis sodales nulla, eget" +
- " commodo sem. Donec lacus enim, pharetra non risus nec, eleifend ultrices" +
- " augue. Donec sit amet orci porttitor, auctor mauris et, facilisis dolor." +
- " Nullam mattis luctus orci at pulvinar.\n" +
- "\n" +
- "Sed accumsan est massa, ut aliquam nulla dignissim id. Suspendisse in urna" +
- " condimentum, convallis purus at, molestie nisi. In hac habitasse platea" +
- " dictumst. Pellentesque id justo quam. Cras iaculis tellus libero, eu" +
- " feugiat ex pharetra eget. Nunc ultrices, magna ut gravida egestas, mauris" +
- " justo blandit sapien, eget congue nisi felis congue diam. Mauris at felis" +
- " vitae erat porta auctor. Pellentesque iaculis sem metus. Phasellus quam" +
- " neque, congue at est eget, sodales interdum justo. Aenean a pharetra dui." +
- " Morbi odio nibh, hendrerit vulputate odio eget, sollicitudin egestas ex." +
- " Fusce nisl ex, fermentum a ultrices id, rhoncus vitae urna. Aliquam quis" +
- " lobortis turpis.\n" +
- "\n",
- color = Color.Gray,
- fontSize = 15.sp,
- )
}
}
-context(AnimatedVisibilityScope, SharedTransitionScope)
-@Suppress("UNUSED_PARAMETER")
@Composable
-fun DetailView(model: MyModel, selected: Kitty, next: Kitty?) {
- Column(
- Modifier.clickable(
- interactionSource = remember { MutableInteractionSource() },
- indication = null
- ) {
- model.selected = null
- }
- .sharedBounds(
- rememberSharedContentState(key = "container + ${selected.id}"),
- this@AnimatedVisibilityScope,
- fadeIn(),
- fadeOut(),
- resizeMode = ScaleToBounds(ContentScale.Crop),
- clipInOverlayDuringTransition = OverlayClip(RoundedCornerShape(20.dp)),
- )
- ) {
- Row(Modifier.fillMaxHeight(0.5f)) {
- Image(
- painter = painterResource(selected.photoResId),
- contentDescription = null,
- contentScale = ContentScale.Crop,
- modifier =
- Modifier.padding(10.dp)
- .sharedElement(
- rememberSharedContentState(key = selected.id),
- this@AnimatedVisibilityScope,
- placeHolderSize = animatedSize
- )
- .fillMaxHeight()
- .aspectRatio(1f)
- .clip(RoundedCornerShape(20.dp))
- )
- if (next != null) {
+fun DetailView(
+ animatedVisibilityScope: AnimatedVisibilityScope,
+ sharedTransitionScope: SharedTransitionScope,
+ model: MyModel,
+ selected: Kitty,
+ next: Kitty?
+) {
+ with(sharedTransitionScope) {
+ Column(
+ Modifier.clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = null
+ ) {
+ model.selected = null
+ }
+ .sharedBounds(
+ rememberSharedContentState(key = "container + ${selected.id}"),
+ animatedVisibilityScope,
+ fadeIn(),
+ fadeOut(),
+ resizeMode = ScaleToBounds(ContentScale.Crop),
+ clipInOverlayDuringTransition = OverlayClip(RoundedCornerShape(20.dp)),
+ )
+ ) {
+ Row(Modifier.fillMaxHeight(0.5f)) {
Image(
- painter = painterResource(next.photoResId),
+ painter = painterResource(selected.photoResId),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier =
- Modifier.padding(top = 10.dp, bottom = 10.dp, end = 10.dp)
- .fillMaxWidth()
+ Modifier.padding(10.dp)
+ .sharedElement(
+ rememberSharedContentState(key = selected.id),
+ animatedVisibilityScope,
+ placeHolderSize = animatedSize
+ )
.fillMaxHeight()
+ .aspectRatio(1f)
.clip(RoundedCornerShape(20.dp))
- .blur(10.dp)
)
+ if (next != null) {
+ Image(
+ painter = painterResource(next.photoResId),
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ modifier =
+ Modifier.padding(top = 10.dp, bottom = 10.dp, end = 10.dp)
+ .fillMaxWidth()
+ .fillMaxHeight()
+ .clip(RoundedCornerShape(20.dp))
+ .blur(10.dp)
+ )
+ }
}
+ Details(sharedTransitionScope, animatedVisibilityScope, kitty = selected)
}
- Details(kitty = selected)
}
}
-context(AnimatedVisibilityScope, SharedTransitionScope)
@Composable
-fun GridView(model: MyModel) {
- Box(Modifier.background(lessVibrantPurple)) {
- Box(
- Modifier.padding(20.dp)
- .renderInSharedTransitionScopeOverlay(zIndexInOverlay = 2f)
- .animateEnterExit(fadeIn(), fadeOut())
- ) {
- SearchBar()
- }
- LazyVerticalGrid(
- columns = GridCells.Fixed(2),
- contentPadding = PaddingValues(top = 90.dp)
- ) {
- items(6) {
- Box(modifier = Modifier.clickable { model.selected = model.items[it] }) {
- KittyItem(model.items[it])
+fun GridView(
+ animatedVisibilityScope: AnimatedVisibilityScope,
+ sharedTransitionScope: SharedTransitionScope,
+ model: MyModel
+) {
+ with(animatedVisibilityScope) {
+ with(sharedTransitionScope) {
+ Box(Modifier.background(lessVibrantPurple)) {
+ Box(
+ Modifier.padding(20.dp)
+ .renderInSharedTransitionScopeOverlay(zIndexInOverlay = 2f)
+ .animateEnterExit(fadeIn(), fadeOut())
+ ) {
+ SearchBar()
+ }
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(2),
+ contentPadding = PaddingValues(top = 90.dp)
+ ) {
+ items(6) {
+ Box(modifier = Modifier.clickable { model.selected = model.items[it] }) {
+ KittyItem(
+ animatedVisibilityScope,
+ sharedTransitionScope,
+ model.items[it]
+ )
+ }
+ }
}
}
}
@@ -284,54 +312,59 @@
var selected: Kitty? by mutableStateOf(null)
}
-context(AnimatedVisibilityScope, SharedTransitionScope)
@Composable
-fun KittyItem(kitty: Kitty) {
- Column(
- Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp)
- .sharedBounds(
- rememberSharedContentState(key = "container + ${kitty.id}"),
- this@AnimatedVisibilityScope,
+fun KittyItem(
+ animatedVisibilityScope: AnimatedVisibilityScope,
+ sharedTransitionScope: SharedTransitionScope,
+ kitty: Kitty
+) {
+ with(sharedTransitionScope) {
+ Column(
+ Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp)
+ .sharedBounds(
+ rememberSharedContentState(key = "container + ${kitty.id}"),
+ animatedVisibilityScope,
+ )
+ .background(Color.White, RoundedCornerShape(20.dp))
+ ) {
+ Image(
+ painter = painterResource(kitty.photoResId),
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ modifier =
+ Modifier.sharedElement(
+ rememberSharedContentState(key = kitty.id),
+ animatedVisibilityScope,
+ placeHolderSize = animatedSize
+ )
+ .aspectRatio(1f)
+ .clip(RoundedCornerShape(20.dp))
)
- .background(Color.White, RoundedCornerShape(20.dp))
- ) {
- Image(
- painter = painterResource(kitty.photoResId),
- contentDescription = null,
- contentScale = ContentScale.Crop,
- modifier =
- Modifier.sharedElement(
- rememberSharedContentState(key = kitty.id),
- this@AnimatedVisibilityScope,
- placeHolderSize = animatedSize
- )
- .aspectRatio(1f)
- .clip(RoundedCornerShape(20.dp))
- )
- Spacer(Modifier.size(10.dp))
- Text(
- kitty.name,
- fontSize = 18.sp,
- modifier =
- Modifier.padding(start = 10.dp)
- .sharedBounds(
- rememberSharedContentState(key = kitty.name + kitty.id),
- this@AnimatedVisibilityScope
- )
- )
- Spacer(Modifier.size(5.dp))
- Text(
- kitty.breed,
- fontSize = 15.sp,
- color = Color.Gray,
- modifier =
- Modifier.padding(start = 10.dp)
- .sharedBounds(
- rememberSharedContentState(key = kitty.breed + kitty.id),
- this@AnimatedVisibilityScope
- )
- )
- Spacer(Modifier.size(10.dp))
+ Spacer(Modifier.size(10.dp))
+ Text(
+ kitty.name,
+ fontSize = 18.sp,
+ modifier =
+ Modifier.padding(start = 10.dp)
+ .sharedBounds(
+ rememberSharedContentState(key = kitty.name + kitty.id),
+ animatedVisibilityScope
+ )
+ )
+ Spacer(Modifier.size(5.dp))
+ Text(
+ kitty.breed,
+ fontSize = 15.sp,
+ color = Color.Gray,
+ modifier =
+ Modifier.padding(start = 10.dp)
+ .sharedBounds(
+ rememberSharedContentState(key = kitty.breed + kitty.id),
+ animatedVisibilityScope
+ )
+ )
+ Spacer(Modifier.size(10.dp))
+ }
}
}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/SwitchBetweenCollapsedAndExpanded.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/SwitchBetweenCollapsedAndExpanded.kt
index 9a3c30d..3cd4c47 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/SwitchBetweenCollapsedAndExpanded.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/SwitchBetweenCollapsedAndExpanded.kt
@@ -83,111 +83,114 @@
)
) {
SharedTransitionLayout {
- HomePage(!showExpandedCard)
- ExpandedCard(showExpandedCard)
+ HomePage(this@SharedTransitionLayout, !showExpandedCard)
+ ExpandedCard(this@SharedTransitionLayout, showExpandedCard)
}
}
}
-context(SharedTransitionScope)
@Composable
-fun HomePage(showCard: Boolean) {
- Box(Modifier.fillMaxSize().background(Color.White)) {
- Column {
- SearchBarAndTabs()
- Box(Modifier.fillMaxWidth().aspectRatio(1.1f)) {
- androidx.compose.animation.AnimatedVisibility(visible = showCard) {
- Column(
- Modifier.padding(top = 10.dp, start = 10.dp, end = 10.dp)
- .sharedBounds(
- rememberSharedContentState(key = "container"),
- this@AnimatedVisibility,
- clipInOverlayDuringTransition =
- OverlayClip(RoundedCornerShape(20.dp))
- )
- .clip(shape = RoundedCornerShape(20.dp))
- .background(color = cardBackgroundColor),
- ) {
- Box {
- Column {
- Image(
- painterResource(R.drawable.quiet_night),
- contentDescription = null,
- modifier =
- Modifier.fillMaxWidth()
- .sharedElement(
- rememberSharedContentState(key = "quiet_night"),
- this@AnimatedVisibility,
- zIndexInOverlay = 0.5f,
- ),
- contentScale = ContentScale.FillWidth
+fun HomePage(sharedTransitionScope: SharedTransitionScope, showCard: Boolean) {
+ with(sharedTransitionScope) {
+ Box(Modifier.fillMaxSize().background(Color.White)) {
+ Column {
+ SearchBarAndTabs()
+ Box(Modifier.fillMaxWidth().aspectRatio(1.1f)) {
+ androidx.compose.animation.AnimatedVisibility(visible = showCard) {
+ Column(
+ Modifier.padding(top = 10.dp, start = 10.dp, end = 10.dp)
+ .sharedBounds(
+ rememberSharedContentState(key = "container"),
+ this@AnimatedVisibility,
+ clipInOverlayDuringTransition =
+ OverlayClip(RoundedCornerShape(20.dp))
)
- Text(
- text = longText,
- color = Color.Gray,
- fontSize = 15.sp,
- modifier =
- Modifier.fillMaxWidth()
- .padding(start = 20.dp, end = 20.dp, top = 20.dp)
- .height(14.dp)
- .sharedElement(
- rememberSharedContentState(key = "longText"),
- this@AnimatedVisibility,
- )
- .clipToBounds()
- .wrapContentHeight(
- align = Alignment.Top,
- unbounded = true
- )
- .skipToLookaheadSize(),
- )
- }
+ .clip(shape = RoundedCornerShape(20.dp))
+ .background(color = cardBackgroundColor),
+ ) {
+ Box {
+ Column {
+ Image(
+ painterResource(R.drawable.quiet_night),
+ contentDescription = null,
+ modifier =
+ Modifier.fillMaxWidth()
+ .sharedElement(
+ rememberSharedContentState(key = "quiet_night"),
+ this@AnimatedVisibility,
+ zIndexInOverlay = 0.5f,
+ ),
+ contentScale = ContentScale.FillWidth
+ )
+ Text(
+ text = longText,
+ color = Color.Gray,
+ fontSize = 15.sp,
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(start = 20.dp, end = 20.dp, top = 20.dp)
+ .height(14.dp)
+ .sharedElement(
+ rememberSharedContentState(key = "longText"),
+ this@AnimatedVisibility,
+ )
+ .clipToBounds()
+ .wrapContentHeight(
+ align = Alignment.Top,
+ unbounded = true
+ )
+ .skipToLookaheadSize(),
+ )
+ }
- Text(
- text = title,
- fontFamily = FontFamily.Default,
- color = Color.White,
- fontSize = 20.sp,
- modifier =
- Modifier.fillMaxWidth()
- .align(Alignment.BottomCenter)
- .renderInSharedTransitionScopeOverlay(zIndexInOverlay = 1f)
- .animateEnterExit(
- fadeIn(tween(1000)) + slideInVertically { -it / 3 },
- fadeOut(tween(50)) + slideOutVertically { -it / 3 }
- )
- .skipToLookaheadSize()
- .background(
- Brush.verticalGradient(
- listOf(
- Color.Transparent,
- Color.Black,
- Color.Transparent
+ Text(
+ text = title,
+ fontFamily = FontFamily.Default,
+ color = Color.White,
+ fontSize = 20.sp,
+ modifier =
+ Modifier.fillMaxWidth()
+ .align(Alignment.BottomCenter)
+ .renderInSharedTransitionScopeOverlay(
+ zIndexInOverlay = 1f
+ )
+ .animateEnterExit(
+ fadeIn(tween(1000)) + slideInVertically { -it / 3 },
+ fadeOut(tween(50)) + slideOutVertically { -it / 3 }
+ )
+ .skipToLookaheadSize()
+ .background(
+ Brush.verticalGradient(
+ listOf(
+ Color.Transparent,
+ Color.Black,
+ Color.Transparent
+ )
)
)
- )
- .padding(20.dp),
+ .padding(20.dp),
+ )
+ }
+ InstallBar(
+ Modifier.fillMaxWidth()
+ .zIndex(1f)
+ .sharedElementWithCallerManagedVisibility(
+ rememberSharedContentState(key = "install_bar"),
+ showCard,
+ )
)
}
- InstallBar(
- Modifier.fillMaxWidth()
- .zIndex(1f)
- .sharedElementWithCallerManagedVisibility(
- rememberSharedContentState(key = "install_bar"),
- showCard,
- )
- )
}
}
+ Cluster()
}
- Cluster()
+ Image(
+ painterResource(R.drawable.navigation_bar),
+ contentDescription = null,
+ Modifier.fillMaxWidth().align(Alignment.BottomCenter),
+ contentScale = ContentScale.FillWidth
+ )
}
- Image(
- painterResource(R.drawable.navigation_bar),
- contentDescription = null,
- Modifier.fillMaxWidth().align(Alignment.BottomCenter),
- contentScale = ContentScale.FillWidth
- )
}
}
@@ -221,97 +224,98 @@
}
}
-context(SharedTransitionScope)
@Composable
-fun ExpandedCard(visible: Boolean) {
- AnimatedVisibility(
- visible = visible,
- Modifier.fillMaxSize(),
- enter = fadeIn(),
- exit = fadeOut()
- ) {
- Box(Modifier.fillMaxSize().background(Color(0x55000000))) {
- Column(
- Modifier.align(Alignment.Center)
- .padding(20.dp)
- .sharedBounds(
- rememberSharedContentState(key = "container"),
- this@AnimatedVisibility,
- enter = EnterTransition.None,
- exit = ExitTransition.None,
- clipInOverlayDuringTransition = OverlayClip(RoundedCornerShape(20.dp))
- )
- .clip(shape = RoundedCornerShape(20.dp))
- .background(cardBackgroundColor)
- ) {
+fun ExpandedCard(sharedTransitionScope: SharedTransitionScope, visible: Boolean) {
+ with(sharedTransitionScope) {
+ AnimatedVisibility(
+ visible = visible,
+ Modifier.fillMaxSize(),
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ Box(Modifier.fillMaxSize().background(Color(0x55000000))) {
Column(
- Modifier.renderInSharedTransitionScopeOverlay(zIndexInOverlay = 1f)
- .animateEnterExit(
- fadeIn() + slideInVertically { it / 3 },
- fadeOut() + slideOutVertically { it / 3 }
+ Modifier.align(Alignment.Center)
+ .padding(20.dp)
+ .sharedBounds(
+ rememberSharedContentState(key = "container"),
+ this@AnimatedVisibility,
+ enter = EnterTransition.None,
+ exit = ExitTransition.None,
+ clipInOverlayDuringTransition = OverlayClip(RoundedCornerShape(20.dp))
)
- .skipToLookaheadSize()
- .background(
- Brush.verticalGradient(
- listOf(Color.Transparent, Color.Black, Color.Transparent)
- )
- )
- .padding(start = 20.dp, end = 20.dp),
+ .clip(shape = RoundedCornerShape(20.dp))
+ .background(cardBackgroundColor)
) {
- Text(
- text = "Lorem ipsum",
- Modifier.padding(top = 20.dp, bottom = 10.dp)
- .background(Color.LightGray, shape = RoundedCornerShape(15.dp))
- .padding(top = 8.dp, bottom = 8.dp, start = 15.dp, end = 15.dp),
- color = Color.Black,
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Bold,
- fontSize = 15.sp
+ Column(
+ Modifier.renderInSharedTransitionScopeOverlay(zIndexInOverlay = 1f)
+ .animateEnterExit(
+ fadeIn() + slideInVertically { it / 3 },
+ fadeOut() + slideOutVertically { it / 3 }
+ )
+ .skipToLookaheadSize()
+ .background(
+ Brush.verticalGradient(
+ listOf(Color.Transparent, Color.Black, Color.Transparent)
+ )
+ )
+ .padding(start = 20.dp, end = 20.dp),
+ ) {
+ Text(
+ text = "Lorem ipsum",
+ Modifier.padding(top = 20.dp, bottom = 10.dp)
+ .background(Color.LightGray, shape = RoundedCornerShape(15.dp))
+ .padding(top = 8.dp, bottom = 8.dp, start = 15.dp, end = 15.dp),
+ color = Color.Black,
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Bold,
+ fontSize = 15.sp
+ )
+ Text(
+ text = title,
+ color = Color.White,
+ fontSize = 30.sp,
+ modifier = Modifier.fillMaxWidth().padding(bottom = 20.dp)
+ )
+ }
+ Image(
+ painterResource(R.drawable.quiet_night),
+ contentDescription = null,
+ modifier =
+ Modifier.fillMaxWidth()
+ .sharedElement(
+ rememberSharedContentState("quiet_night"),
+ this@AnimatedVisibility,
+ ),
+ contentScale = ContentScale.FillWidth
)
+
Text(
- text = title,
- color = Color.White,
- fontSize = 30.sp,
- modifier = Modifier.fillMaxWidth().padding(bottom = 20.dp)
+ text = longText,
+ color = Color.Gray,
+ fontSize = 15.sp,
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(start = 15.dp, end = 10.dp, top = 10.dp)
+ .height(50.dp)
+ .sharedElement(
+ rememberSharedContentState("longText"),
+ this@AnimatedVisibility,
+ )
+ .clipToBounds()
+ .wrapContentHeight(align = Alignment.Top, unbounded = true)
+ .skipToLookaheadSize(),
+ )
+
+ InstallBar(
+ Modifier.fillMaxWidth()
+ .zIndex(1f)
+ .sharedElement(
+ rememberSharedContentState("install_bar"),
+ this@AnimatedVisibility,
+ )
)
}
- Image(
- painterResource(R.drawable.quiet_night),
- contentDescription = null,
- modifier =
- Modifier.fillMaxWidth()
- .sharedElement(
- rememberSharedContentState("quiet_night"),
- this@AnimatedVisibility,
- ),
- contentScale = ContentScale.FillWidth
- )
-
- Text(
- text = longText,
- color = Color.Gray,
- fontSize = 15.sp,
- modifier =
- Modifier.fillMaxWidth()
- .padding(start = 15.dp, end = 10.dp, top = 10.dp)
- .height(50.dp)
- .sharedElement(
- rememberSharedContentState("longText"),
- this@AnimatedVisibility,
- )
- .clipToBounds()
- .wrapContentHeight(align = Alignment.Top, unbounded = true)
- .skipToLookaheadSize(),
- )
-
- InstallBar(
- Modifier.fillMaxWidth()
- .zIndex(1f)
- .sharedElement(
- rememberSharedContentState("install_bar"),
- this@AnimatedVisibility,
- )
- )
}
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/Draggable2DTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/Draggable2DTest.kt
index cdd939a..5493358 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/Draggable2DTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/Draggable2DTest.kt
@@ -808,14 +808,12 @@
val moveAngle = Math.atan(moveOffset.x / moveOffset.y.toDouble())
rule.runOnIdle {
- assertEquals(
- downEventPosition.x + touchSlop * Math.cos(moveAngle).toFloat(),
- onDragStartedOffset.x
- )
- assertEquals(
- downEventPosition.y + touchSlop * Math.sin(moveAngle).toFloat(),
- onDragStartedOffset.y
- )
+ assertThat(downEventPosition.x + touchSlop * Math.cos(moveAngle).toFloat())
+ .isWithin(0.5f)
+ .of(onDragStartedOffset.x)
+ assertThat(downEventPosition.y + touchSlop * Math.sin(moveAngle).toFloat())
+ .isWithin(0.5f)
+ .of(onDragStartedOffset.y)
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
index 9d07996..6570da8 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
@@ -172,7 +172,7 @@
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
) =
detectDragGestures(
- onDragStart = { change, _ -> onDragStart(change.position) },
+ onDragStart = { _, slopTriggerChange, _ -> onDragStart(slopTriggerChange.position) },
onDragEnd = { onDragEnd.invoke() },
onDragCancel = onDragCancel,
shouldAwaitTouchSlop = { true },
@@ -200,7 +200,8 @@
*
* @param onDragStart A lambda to be called when the drag gesture starts, it contains information
* about the last known [PointerInputChange] relative to the containing element and the post slop
- * delta.
+ * delta, slopTriggerChange. It also contains information about the down event where this gesture
+ * started and the overSlopOffset.
* @param onDragEnd A lambda to be called when the gesture ends. It contains information about the
* up [PointerInputChange] that finished the gesture.
* @param onDragCancel A lambda to be called when the gesture is cancelled either by an error or
@@ -224,7 +225,10 @@
* @see detectDragGesturesAfterLongPress to detect gestures after long press
*/
internal suspend fun PointerInputScope.detectDragGestures(
- onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit,
+ onDragStart:
+ (
+ down: PointerInputChange, slopTriggerChange: PointerInputChange, overSlopOffset: Offset
+ ) -> Unit,
onDragEnd: (change: PointerInputChange) -> Unit,
onDragCancel: () -> Unit,
shouldAwaitTouchSlop: () -> Boolean,
@@ -242,7 +246,6 @@
}
val down = awaitFirstDown(requireUnconsumed = false)
var drag: PointerInputChange?
- var initialDelta = Offset.Zero
overSlop = Offset.Zero
if (awaitTouchSlop) {
@@ -257,13 +260,12 @@
overSlop = over
}
} while (drag != null && !drag.isConsumed)
- initialDelta = overSlop
} else {
drag = initialDown
}
if (drag != null) {
- onDragStart.invoke(drag, initialDelta)
+ onDragStart.invoke(down, drag, overSlop)
onDrag(drag, overSlop)
val upEvent =
drag(
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 f0490ff..82ad253 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
@@ -48,7 +48,6 @@
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.Velocity
import kotlin.coroutines.cancellation.CancellationException
-import kotlin.math.sign
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
@@ -463,23 +462,27 @@
// re-create tracker when pointer input block restarts. This lazily creates the tracker
// only when it is need.
val velocityTracker = VelocityTracker()
- val onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit =
- { startEvent, initialDelta ->
- if (canDrag.invoke(startEvent)) {
+
+ val onDragStart:
+ (
+ down: PointerInputChange,
+ slopTriggerChange: PointerInputChange,
+ postSlopOffset: Offset
+ ) -> Unit =
+ { down, slopTriggerChange, postSlopOffset ->
+ if (canDrag.invoke(down)) {
if (!isListeningForEvents) {
if (channel == null) {
channel = Channel(capacity = Channel.UNLIMITED)
}
startListeningForEvents()
}
- val overSlopOffset = initialDelta
- val xSign = sign(startEvent.position.x)
- val ySign = sign(startEvent.position.y)
- val adjustedStart =
- startEvent.position -
- Offset(overSlopOffset.x * xSign, overSlopOffset.y * ySign)
-
- channel?.trySend(DragStarted(adjustedStart))
+ velocityTracker.addPointerInputChange(down)
+ val dragStartedOffset = slopTriggerChange.position - postSlopOffset
+ // the drag start event offset is the down event + touch slop value
+ // or in this case the event that triggered the touch slop minus
+ // the post slop offset
+ channel?.trySend(DragStarted(dragStartedOffset))
}
}
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 f03d8b3..423483f 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
@@ -422,10 +422,17 @@
consumedScroll = consumedScroll,
measureResult =
layout(layoutWidth, layoutHeight) {
- // place normal items
- positionedItems.fastForEach { it.place(this, isLookingAhead) }
- // stickingItems should be placed after all other items
- stickingItems.fastForEach { it.place(this, isLookingAhead) }
+ // Tagging as motion frame of reference placement, meaning the placement
+ // contains scrolling. This allows the consumer of this placement offset to
+ // differentiate this offset vs. offsets from structural changes. Generally
+ // speaking, this signals a preference to directly apply changes rather than
+ // animating, to avoid a chasing effect to scrolling.
+ withMotionFrameOfReferencePlacement {
+ // place normal items
+ positionedItems.fastForEach { it.place(this, isLookingAhead) }
+ // stickingItems should be placed after all other items
+ stickingItems.fastForEach { it.place(this, isLookingAhead) }
+ }
// we attach it during the placement so LazyListState can trigger re-placement
placementScopeInvalidator.attachToScope()
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 22d00c9..66ed93c 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
@@ -390,8 +390,15 @@
consumedScroll = consumedScroll,
measureResult =
layout(layoutWidth, layoutHeight) {
- positionedItems.fastForEach { it.place(this) }
- stickingItems.fastForEach { it.place(this) }
+ // Tagging as motion frame of reference placement, meaning the placement
+ // contains scrolling. This allows the consumer of this placement offset to
+ // differentiate this offset vs. offsets from structural changes. Generally
+ // speaking, this signals a preference to directly apply changes rather than
+ // animating, to avoid a chasing effect to scrolling.
+ withMotionFrameOfReferencePlacement {
+ positionedItems.fastForEach { it.place(this) }
+ stickingItems.fastForEach { it.place(this) }
+ }
// we attach it during the placement so LazyGridState can trigger re-placement
placementScopeInvalidator.attachToScope()
},
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 15bba67..608b82b 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
@@ -897,8 +897,15 @@
consumedScroll = consumedScroll,
measureResult =
layout(layoutWidth, layoutHeight) {
- positionedItems.fastForEach { item ->
- item.place(scope = this, context = this@measure)
+ // Tagging as motion frame of reference placement, meaning the placement
+ // contains scrolling. This allows the consumer of this placement offset to
+ // differentiate this offset vs. offsets from structural changes. Generally
+ // speaking, this signals a preference to directly apply changes rather than
+ // animating, to avoid a chasing effect to scrolling.
+ withMotionFrameOfReferencePlacement {
+ positionedItems.fastForEach { item ->
+ item.place(scope = this, context = this@measure)
+ }
}
// we attach it during the placement so LazyStaggeredGridState can trigger
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt
index 1333583..a6f83cd 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt
@@ -465,7 +465,14 @@
firstVisiblePageScrollOffset = currentFirstPageScrollOffset,
measureResult =
layout(layoutWidth, layoutHeight) {
- positionedPages.fastForEach { it.place(this) }
+ // Tagging as motion frame of reference placement, meaning the placement
+ // contains scrolling. This allows the consumer of this placement offset to
+ // differentiate this offset vs. offsets from structural changes. Generally
+ // speaking, this signals a preference to directly apply changes rather than
+ // animating, to avoid a chasing effect to scrolling.
+ withMotionFrameOfReferencePlacement {
+ positionedPages.fastForEach { it.place(this) }
+ }
// we attach it during the placement so PagerState can trigger re-placement
placementScopeInvalidator.attachToScope()
},
diff --git a/compose/material3/adaptive/adaptive-layout/build.gradle b/compose/material3/adaptive/adaptive-layout/build.gradle
index f37fbdb..2f637a0 100644
--- a/compose/material3/adaptive/adaptive-layout/build.gradle
+++ b/compose/material3/adaptive/adaptive-layout/build.gradle
@@ -111,10 +111,6 @@
samples(project(":compose:material3:adaptive:adaptive-samples"))
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
// Screenshot tests related setup
android {
compileSdk 35
diff --git a/compose/material3/adaptive/adaptive-navigation/build.gradle b/compose/material3/adaptive/adaptive-navigation/build.gradle
index dbbcdc2..7992414 100644
--- a/compose/material3/adaptive/adaptive-navigation/build.gradle
+++ b/compose/material3/adaptive/adaptive-navigation/build.gradle
@@ -109,10 +109,6 @@
metalavaK2UastEnabled = false
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
// Screenshot tests related setup
android {
compileSdk 35
diff --git a/compose/material3/adaptive/adaptive/build.gradle b/compose/material3/adaptive/adaptive/build.gradle
index 1cb594e..24f0da1 100644
--- a/compose/material3/adaptive/adaptive/build.gradle
+++ b/compose/material3/adaptive/adaptive/build.gradle
@@ -109,10 +109,6 @@
metalavaK2UastEnabled = false
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
// Screenshot tests related setup
android {
compileSdk 35
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
index 61654d2..3e166ef 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
@@ -30,6 +30,7 @@
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.requiredHeight
import androidx.compose.foundation.layout.requiredSize
@@ -40,6 +41,7 @@
import androidx.compose.material3.internal.getString
import androidx.compose.material3.tokens.SheetBottomTokens
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -84,9 +86,11 @@
import androidx.compose.ui.test.swipeUp
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.coerceAtMost
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
import androidx.compose.ui.unit.width
import androidx.compose.ui.zIndex
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -96,6 +100,8 @@
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import junit.framework.TestCase
+import junit.framework.TestCase.assertEquals
+import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@@ -903,4 +909,121 @@
with(density!!) { rule.rootHeight().toPx() - peekHeight.toPx() - snackbarSize!!.height }
assertThat(snackbarBottomOffset).isWithin(1f).of(expectedSnackbarBottomOffset)
}
+
+ @Test
+ fun bottomSheetScaffold_bottomSheetOffsetTaggedAsMotionFrameOfReference() {
+ var offset by mutableStateOf(IntOffset(0, 0))
+ val offsets =
+ listOf(
+ IntOffset(0, 0),
+ IntOffset(5, 20),
+ IntOffset(25, 0),
+ IntOffset(100, 10),
+ )
+ var sheetCoords: LayoutCoordinates? = null
+ var rootCoords: LayoutCoordinates? = null
+ val state = SheetState(false, density = Density(1f))
+ var sheetValue by mutableStateOf(SheetValue.Hidden)
+ rule.setContent {
+ Box(Modifier.onGloballyPositioned { rootCoords = it }.offset { offset }) {
+ LaunchedEffect(sheetValue) {
+ if (sheetValue == SheetValue.Hidden) {
+ state.hide()
+ } else if (sheetValue == SheetValue.PartiallyExpanded) {
+ state.partialExpand()
+ } else {
+ state.expand()
+ }
+ }
+ BottomSheetScaffold(
+ sheetContent = {
+ Box(Modifier.fillMaxSize().onGloballyPositioned { sheetCoords = it })
+ },
+ scaffoldState =
+ BottomSheetScaffoldState(state, remember { SnackbarHostState() })
+ ) {
+ Box(Modifier.fillMaxSize())
+ }
+ }
+ }
+
+ SheetValue.values().forEach {
+ sheetValue = it
+ rule.waitForIdle()
+
+ repeat(4) {
+ offset = offsets[it]
+ rule.runOnIdle {
+ val excludeOffset =
+ rootCoords!!
+ .localPositionOf(sheetCoords!!, includeMotionFrameOfReference = false)
+ .round()
+ val includeSheetOffset =
+ rootCoords!!
+ .localPositionOf(sheetCoords!!, includeMotionFrameOfReference = true)
+ .round()
+ assertEquals(
+ includeSheetOffset - IntOffset(0, state.requireOffset().roundToInt()),
+ excludeOffset
+ )
+ }
+ }
+ }
+ }
+
+ @Test
+ fun modalBottomSheet_bottomSheetOffsetTaggedAsMotionFrameOfReference() {
+ var offset by mutableStateOf(IntOffset(0, 0))
+ val offsets =
+ listOf(
+ IntOffset(0, 0),
+ IntOffset(5, 20),
+ IntOffset(25, 0),
+ IntOffset(100, 10),
+ )
+ var sheetCoords: LayoutCoordinates? = null
+ val state = SheetState(false, density = Density(1f))
+ var sheetValue by mutableStateOf(SheetValue.Hidden)
+ rule.setContent {
+ LaunchedEffect(sheetValue) {
+ if (sheetValue == SheetValue.Hidden) {
+ state.hide()
+ } else if (sheetValue == SheetValue.PartiallyExpanded) {
+ state.partialExpand()
+ } else {
+ state.expand()
+ }
+ }
+ ModalBottomSheet({}, sheetState = state) {
+ Box(Modifier.fillMaxSize().onGloballyPositioned { sheetCoords = it })
+ }
+ }
+
+ fun LayoutCoordinates.root(): LayoutCoordinates =
+ if (parentLayoutCoordinates != null) parentLayoutCoordinates!!.root() else this
+
+ SheetValue.values().forEach {
+ sheetValue = it
+ rule.waitForIdle()
+ val rootCoords = sheetCoords!!.root()
+
+ repeat(4) {
+ offset = offsets[it]
+ rule.runOnIdle {
+ val excludeOffset =
+ rootCoords
+ .localPositionOf(sheetCoords!!, includeMotionFrameOfReference = false)
+ .round()
+ val includeSheetOffset =
+ rootCoords
+ .localPositionOf(sheetCoords!!, includeMotionFrameOfReference = true)
+ .round()
+ assertEquals(
+ includeSheetOffset - IntOffset(0, state.requireOffset().roundToInt()),
+ excludeOffset
+ )
+ }
+ }
+ }
+ }
}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt
index 883c847..bfe9fa0 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt
@@ -34,6 +34,7 @@
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.test.screenshot.AndroidXScreenshotTestRule
@@ -53,6 +54,8 @@
private val wrap = Modifier.wrapContentSize(Alignment.Center)
private val wrapperTestTag = "splitButtonWrapper"
+ private val leadingButtonTag = "leadingButton"
+ private val trailingButtonTag = "trailingButton"
@Test
fun splitButton() {
@@ -323,6 +326,82 @@
assertAgainstGolden("splitButton_textLeadingButton_${scheme.name}")
}
+ @Test
+ fun splitButton_leadingButton_pressed() {
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ SplitButton(
+ leadingButton = {
+ SplitButtonDefaults.LeadingButton(
+ onClick = { /* Do Nothing */ },
+ modifier = Modifier.testTag(leadingButtonTag),
+ ) {
+ Icon(
+ Icons.Filled.Edit,
+ modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
+ contentDescription = "Localized description",
+ )
+ Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+ Text("My Button")
+ }
+ },
+ trailingButton = {
+ SplitButtonDefaults.TrailingButton(
+ onClick = {},
+ checked = false,
+ ) {
+ Icon(
+ Icons.Outlined.KeyboardArrowDown,
+ contentDescription = "Localized description",
+ Modifier.size(SplitButtonDefaults.TrailingIconSize)
+ )
+ }
+ }
+ )
+ }
+ }
+
+ assertPressed(leadingButtonTag, "splitButton_leadingButton_pressed_${scheme.name}")
+ }
+
+ @Test
+ fun splitButton_trailingButton_pressed() {
+ rule.setMaterialContent(scheme.colorScheme) {
+ Box(wrap.testTag(wrapperTestTag)) {
+ SplitButton(
+ leadingButton = {
+ SplitButtonDefaults.LeadingButton(
+ onClick = { /* Do Nothing */ },
+ ) {
+ Icon(
+ Icons.Filled.Edit,
+ modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
+ contentDescription = "Localized description",
+ )
+ Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+ Text("My Button")
+ }
+ },
+ trailingButton = {
+ SplitButtonDefaults.TrailingButton(
+ onClick = {},
+ checked = false,
+ modifier = Modifier.testTag(trailingButtonTag),
+ ) {
+ Icon(
+ Icons.Outlined.KeyboardArrowDown,
+ contentDescription = "Localized description",
+ Modifier.size(SplitButtonDefaults.TrailingIconSize)
+ )
+ }
+ }
+ )
+ }
+ }
+
+ assertPressed(trailingButtonTag, "splitButton_trailingButton_pressed_${scheme.name}")
+ }
+
private fun assertAgainstGolden(goldenName: String) {
rule
.onNodeWithTag(wrapperTestTag)
@@ -330,6 +409,21 @@
.assertAgainstGolden(screenshotRule, goldenName)
}
+ private fun assertPressed(tag: String, goldenName: String) {
+ rule.mainClock.autoAdvance = false
+ rule.onNodeWithTag(tag).performTouchInput { down(center) }
+
+ rule.mainClock.advanceTimeByFrame()
+ rule.waitForIdle() // Wait for measure
+ rule.mainClock.advanceTimeBy(milliseconds = 200)
+
+ // Ripples are drawn on the RenderThread, not the main (UI) thread, so we can't wait for
+ // synchronization. Instead just wait until after the ripples are finished animating.
+ Thread.sleep(300)
+
+ assertAgainstGolden(goldenName)
+ }
+
// Provide the ColorScheme and their name parameter in a ColorSchemeWrapper.
// This makes sure that the default method name and the initial Scuba image generated
// name is as expected.
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt
index 2ca56a9..72c2356 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt
@@ -19,6 +19,7 @@
import androidx.compose.animation.core.FloatSpringSpec
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
@@ -39,16 +40,22 @@
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MonotonicFrameClock
import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.runtime.withFrameNanos
import androidx.compose.testutils.WithTouchSlop
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.junit4.StateRestorationTester
@@ -59,11 +66,13 @@
import androidx.compose.ui.test.swipeUp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import java.util.concurrent.TimeUnit
+import junit.framework.TestCase.assertEquals
import kotlin.math.roundToInt
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
@@ -697,7 +706,7 @@
initialValue = A,
defaultPositionalThreshold,
defaultVelocityThreshold,
- animationSpec = defaultAnimationSpec,
+ animationSpec = defaultAnimationSpec
)
anchoredDraggableState.updateAnchors(
DraggableAnchors {
@@ -735,40 +744,6 @@
dragJob.cancel()
}
- @Test
- fun anchoredDraggable_anchoredDrag_doesNotUpdateOnConfirmValueChange() = runTest {
- val anchoredDraggableState =
- AnchoredDraggableState(
- initialValue = B,
- defaultPositionalThreshold,
- defaultVelocityThreshold,
- animationSpec = defaultAnimationSpec,
- confirmValueChange = { false }
- )
- anchoredDraggableState.updateAnchors(
- DraggableAnchors {
- A at 0f
- B at 200f
- }
- )
-
- assertThat(anchoredDraggableState.targetValue).isEqualTo(B)
-
- val unexpectedTarget = A
- val targetUpdates = Channel<Float>()
- val dragJob =
- launch(Dispatchers.Unconfined) {
- anchoredDraggableState.anchoredDrag(unexpectedTarget) { anchors, latestTarget ->
- targetUpdates.send(anchors.positionOf(latestTarget))
- suspendIndefinitely()
- }
- }
-
- val firstTarget = targetUpdates.receive()
- assertThat(firstTarget).isEqualTo(200f)
- dragJob.cancel()
- }
-
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun anchoredDraggable_dragCompletesExceptionally_cleansUp() = runTest {
@@ -1019,6 +994,64 @@
)
}
+ @Test
+ fun draggableAnchors_draggableOffsetTaggedAsMotionFrameOfReference() {
+ var offset by mutableStateOf(IntOffset(0, 0))
+ val offsets =
+ listOf(
+ IntOffset(0, 0),
+ IntOffset(5, 20),
+ IntOffset(25, 0),
+ IntOffset(100, 10),
+ )
+ var coords: LayoutCoordinates? = null
+ var rootCoords: LayoutCoordinates? = null
+ val state =
+ AnchoredDraggableState(
+ initialValue = 0,
+ positionalThreshold = defaultPositionalThreshold,
+ velocityThreshold = defaultVelocityThreshold,
+ animationSpec = { spring() }
+ )
+ var value by mutableIntStateOf(0)
+ rule.setContent {
+ Box(Modifier.onGloballyPositioned { rootCoords = it }.offset { offset }) {
+ LaunchedEffect(value) { state.snapTo(value) }
+ Box(
+ Modifier.draggableAnchors(state, Orientation.Vertical) { _, _ ->
+ DraggableAnchors { repeat(5) { it at it * 100f } } to 0
+ }
+ .fillMaxSize()
+ ) {
+ Box(Modifier.fillMaxSize().onGloballyPositioned { coords = it })
+ }
+ }
+ }
+
+ repeat(5) {
+ value = it
+ rule.waitForIdle()
+
+ repeat(4) {
+ offset = offsets[it]
+ rule.runOnIdle {
+ val excludeOffset =
+ rootCoords!!
+ .localPositionOf(coords!!, includeMotionFrameOfReference = false)
+ .round()
+ val includeOffset =
+ rootCoords!!
+ .localPositionOf(coords!!, includeMotionFrameOfReference = true)
+ .round()
+ assertEquals(
+ includeOffset - IntOffset(0, state.requireOffset().roundToInt()),
+ excludeOffset
+ )
+ }
+ }
+ }
+ }
+
private suspend fun suspendIndefinitely() = suspendCancellableCoroutine<Unit> {}
private class HandPumpTestFrameClock : MonotonicFrameClock {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
index 4cf6819..63959a5 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
@@ -477,7 +477,7 @@
/** Default size for the leading button end corners and trailing button start corners */
// TODO update token to dp size and use it here
val InnerCornerSize = SplitButtonSmallTokens.InnerCornerSize
- private val InnerCornerSizePressed = ShapeDefaults.CornerSmall
+ private val InnerCornerSizePressed = ShapeDefaults.CornerMedium
/**
* Default percentage size for the leading button start corners and trailing button end corners
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
index 09b667b..1bf6f05 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
@@ -565,7 +565,7 @@
if (anchors.hasAnchorFor(targetValue)) {
try {
dragMutex.mutate(dragPriority) {
- dragTarget = if (confirmValueChange(targetValue)) targetValue else currentValue
+ dragTarget = targetValue
restartable(inputs = { anchors to [email protected] }) {
(latestAnchors, latestTarget) ->
anchoredDragScope.block(latestAnchors, latestTarget)
@@ -856,7 +856,14 @@
} else state.requireOffset()
val xOffset = if (orientation == Orientation.Horizontal) offset else 0f
val yOffset = if (orientation == Orientation.Vertical) offset else 0f
- placeable.place(xOffset.roundToInt(), yOffset.roundToInt())
+ // Tagging as motion frame of reference placement, meaning the placement
+ // contains scrolling. This allows the consumer of this placement offset to
+ // differentiate this offset vs. offsets from structural changes. Generally
+ // speaking, this signals a preference to directly apply changes rather than
+ // animating, to avoid a chasing effect to scrolling.
+ withMotionFrameOfReferencePlacement {
+ placeable.place(xOffset.roundToInt(), yOffset.roundToInt())
+ }
}
}
}
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt
index 7335b2c..d298a52 100644
--- a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt
+++ b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt
@@ -17,6 +17,7 @@
package androidx.compose.runtime.benchmark
import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
@@ -30,8 +31,13 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
+import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.text.drawText
+import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.dp
import androidx.test.annotation.UiThreadTest
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -236,6 +242,21 @@
fun benchmark_f_compose_Rect_100() = runBlockingTestWithFrameClock {
measureComposeFocused { repeat(100) { Rect() } }
}
+
+ @UiThreadTest
+ @Test
+ fun benchmark_g_group_eliding_focused_1000() = runBlockingTestWithFrameClock {
+ measureCompose { repeat(1000) { MyLayout { SimpleText("Value: $it") } } }
+ }
+}
+
+@Composable
+fun MyLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
+ Layout(content = content, measurePolicy = EmptyMeasurePolicy, modifier = modifier)
+}
+
+internal val EmptyMeasurePolicy = MeasurePolicy { _, constraints ->
+ layout(constraints.minWidth, constraints.minHeight) {}
}
class ColorModel(color: Color = Color.Black) {
@@ -254,6 +275,12 @@
}
@Composable
+private fun SimpleText(text: String) {
+ val measurer = rememberTextMeasurer()
+ Box(modifier = Modifier.drawBehind { drawText(measurer, text) })
+}
+
+@Composable
private fun Rect(color: Color) {
val modifier = remember(color) { Modifier.background(color) }
Column(modifier) {}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
index cce9013..fe4b799 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
@@ -18,6 +18,7 @@
import androidx.collection.MutableIntObjectMap
import androidx.collection.MutableIntSet
+import androidx.collection.MutableObjectList
import androidx.compose.runtime.snapshots.fastAny
import androidx.compose.runtime.snapshots.fastFilterIndexed
import androidx.compose.runtime.snapshots.fastForEach
@@ -1290,6 +1291,12 @@
/** This a count of the [nodeCount] of the explicitly started groups. */
private val nodeCountStack = IntStack()
+ /**
+ * Deferred slot writes for open groups to avoid thrashing the slot table when slots are added
+ * to parent group which already has children.
+ */
+ private var deferredSlotWrites: MutableIntObjectMap<MutableObjectList<Any?>>? = null
+
/** The current group that will be started by [startGroup] or skipped by [skipGroup] */
var currentGroup = 0
private set
@@ -1439,6 +1446,19 @@
* being inserted.
*/
fun update(value: Any?): Any? {
+ if (insertCount > 0 && currentSlot != slotsGapStart) {
+ // Defer write as doing it now would thrash the slot table.
+ val deferred =
+ (deferredSlotWrites ?: MutableIntObjectMap())
+ .also { deferredSlotWrites = it }
+ .getOrPut(parent) { MutableObjectList() }
+ deferred.add(value)
+ return Composer.Empty
+ }
+ return rawUpdate(value)
+ }
+
+ private fun rawUpdate(value: Any?): Any? {
val result = skip()
set(value)
return result
@@ -1664,7 +1684,7 @@
groups.dataIndex(groupIndexToAddress(groupIndex + groupSize(groupIndex)))
private val currentGroupSlotIndex: Int
- get() = currentSlot - slotsStartIndex(parent)
+ get() = currentSlot - slotsStartIndex(parent) + (deferredSlotWrites?.get(parent)?.size ?: 0)
/**
* Advance [currentGroup] by [amount]. The [currentGroup] group cannot be advanced outside the
@@ -1850,6 +1870,14 @@
val newGroupSize = currentGroup - groupIndex
val isNode = groups.isNode(groupAddress)
if (inserting) {
+ // Check for deferred slot writes
+ val deferredSlotWrites = deferredSlotWrites
+ deferredSlotWrites?.get(groupIndex)?.let {
+ it.forEach { value -> rawUpdate(value) }
+ deferredSlotWrites.remove(groupIndex)
+ }
+
+ // Close the group
groups.updateGroupSize(groupAddress, newGroupSize)
groups.updateNodeCount(groupAddress, newNodes)
nodeCount = nodeCountStack.pop() + if (isNode) 1 else newNodes
diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
index 9328a61..2d820aa 100644
--- a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
+++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
@@ -4608,6 +4608,19 @@
revalidate()
}
+ @Test // regression test for b/362291064
+ fun avoidsThrashingTheSlotTable() = compositionTest {
+ val count = 100
+ var data by mutableIntStateOf(0)
+ compose { repeat(count) { Linear { Text("Value: $it, data: $data") } } }
+
+ validate { repeat(count) { Linear { Text("Value: $it, data: $data") } } }
+
+ data++
+ advance()
+ revalidate()
+ }
+
private inline fun CoroutineScope.withGlobalSnapshotManager(block: CoroutineScope.() -> Unit) {
val channel = Channel<Unit>(Channel.CONFLATED)
val job = launch { channel.consumeEach { Snapshot.sendApplyNotifications() } }
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AccessibilityChecksTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AccessibilityChecksTest.kt
index e1b9966..30b464c 100644
--- a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AccessibilityChecksTest.kt
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AccessibilityChecksTest.kt
@@ -111,7 +111,7 @@
@Composable
private fun BoxWithMissingContentDescription() {
Box(
- Modifier.size(20.dp).semantics {
+ Modifier.size(48.dp).semantics {
// The SemanticsModifier will make this node importantForAccessibility
// Having no content description is now a violation
this.contentDescription = ""
diff --git a/compose/ui/ui/api/current.ignore b/compose/ui/ui/api/current.ignore
index 074fe37..8caaf36 100644
--- a/compose/ui/ui/api/current.ignore
+++ b/compose/ui/ui/api/current.ignore
@@ -13,5 +13,9 @@
Removed method androidx.compose.ui.semantics.SemanticsProperties.getInvisibleToUser() from compatibility checked API surface
+ChangedType: androidx.compose.ui.layout.MeasureResult#getAlignmentLines():
+ Method androidx.compose.ui.layout.MeasureResult.getAlignmentLines has changed return type from java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> to java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer>
+
+
RemovedMethod: androidx.compose.ui.layout.LayoutCoordinates#transformToScreen(float[]):
Removed method androidx.compose.ui.layout.LayoutCoordinates.transformToScreen(float[])
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index cc929b6..afed775 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -2338,20 +2338,20 @@
}
public interface MeasureResult {
- method public java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> getAlignmentLines();
+ method public java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> getAlignmentLines();
method public int getHeight();
method public default kotlin.jvm.functions.Function1<androidx.compose.ui.layout.RulerScope,kotlin.Unit>? getRulers();
method public int getWidth();
method public void placeChildren();
- property public abstract java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines;
+ property public abstract java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines;
property public abstract int height;
property public default kotlin.jvm.functions.Function1<androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers;
property public abstract int width;
}
@androidx.compose.ui.layout.MeasureScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureScope extends androidx.compose.ui.layout.IntrinsicMeasureScope {
- method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
- method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
+ method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
+ method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
}
@kotlin.DslMarker public @interface MeasureScopeMarker {
diff --git a/compose/ui/ui/api/restricted_current.ignore b/compose/ui/ui/api/restricted_current.ignore
index 074fe37..8caaf36 100644
--- a/compose/ui/ui/api/restricted_current.ignore
+++ b/compose/ui/ui/api/restricted_current.ignore
@@ -13,5 +13,9 @@
Removed method androidx.compose.ui.semantics.SemanticsProperties.getInvisibleToUser() from compatibility checked API surface
+ChangedType: androidx.compose.ui.layout.MeasureResult#getAlignmentLines():
+ Method androidx.compose.ui.layout.MeasureResult.getAlignmentLines has changed return type from java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> to java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer>
+
+
RemovedMethod: androidx.compose.ui.layout.LayoutCoordinates#transformToScreen(float[]):
Removed method androidx.compose.ui.layout.LayoutCoordinates.transformToScreen(float[])
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 2c9b7cf..29ad080 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -2341,20 +2341,20 @@
}
public interface MeasureResult {
- method public java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> getAlignmentLines();
+ method public java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> getAlignmentLines();
method public int getHeight();
method public default kotlin.jvm.functions.Function1<androidx.compose.ui.layout.RulerScope,kotlin.Unit>? getRulers();
method public int getWidth();
method public void placeChildren();
- property public abstract java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines;
+ property public abstract java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines;
property public abstract int height;
property public default kotlin.jvm.functions.Function1<androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers;
property public abstract int width;
}
@androidx.compose.ui.layout.MeasureScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureScope extends androidx.compose.ui.layout.IntrinsicMeasureScope {
- method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
- method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
+ method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
+ method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
}
@kotlin.DslMarker public @interface MeasureScopeMarker {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/PlacementScopeMotionFrameOfReferenceTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/PlacementScopeMotionFrameOfReferenceTest.kt
new file mode 100644
index 0000000..6f6e935
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/PlacementScopeMotionFrameOfReferenceTest.kt
@@ -0,0 +1,293 @@
+/*
+ * 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.ui.layout
+
+import androidx.activity.ComponentActivity
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.requiredWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState
+import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
+import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.PageSize
+import androidx.compose.foundation.pager.PagerState
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import kotlin.test.assertEquals
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class PlacementScopeMotionFrameOfReferenceTest {
+ @get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
+
+ @Test
+ fun testLazyList() {
+ var offset by mutableStateOf(IntOffset(0, 0))
+ val coords = arrayOfNulls<LayoutCoordinates>(30)
+ var rootCoords: LayoutCoordinates? = null
+ val state = LazyListState()
+ val offsets =
+ listOf(
+ IntOffset(0, 0),
+ IntOffset(5, 20),
+ IntOffset(25, 0),
+ IntOffset(100, 10),
+ )
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides Density(1f)) {
+ Box(
+ Modifier.layout { m, c ->
+ m.measure(c).run {
+ layout(width, height) {
+ rootCoords = coordinates
+ place(0, 0)
+ }
+ }
+ }
+ .offset { offset }
+ ) {
+ LazyColumn(state = state, modifier = Modifier.requiredHeight(100.dp)) {
+ items(30) { index ->
+ Box(Modifier.size(20.dp).onGloballyPositioned { coords[index] = it })
+ }
+ }
+ }
+ }
+ }
+ repeat(4) {
+ val itemId = it * 5
+ offset = offsets[it]
+ rule.runOnIdle { runBlocking { state.scrollToItem(itemId) } }
+ repeat(5) {
+ assertEquals(
+ offset,
+ coords[itemId + it]!!
+ .let {
+ rootCoords!!.localPositionOf(it, includeMotionFrameOfReference = false)
+ }
+ .round()
+ )
+ assertEquals(
+ offset + IntOffset(0, it * 20),
+ coords[itemId + it]!!
+ .let {
+ rootCoords!!.localPositionOf(it, includeMotionFrameOfReference = true)
+ }
+ .round()
+ )
+ }
+ }
+ }
+
+ @Test
+ fun testLazyGrid() {
+ var offset by mutableStateOf(IntOffset(0, 0))
+ val coords = arrayOfNulls<LayoutCoordinates>(60)
+ var rootCoords: LayoutCoordinates? = null
+ val state = LazyGridState()
+ val offsets =
+ listOf(
+ IntOffset(0, 0),
+ IntOffset(5, 20),
+ IntOffset(25, 0),
+ IntOffset(100, 10),
+ )
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides Density(1f)) {
+ Box(
+ Modifier.layout { m, c ->
+ m.measure(c).run {
+ layout(width, height) {
+ rootCoords = coordinates
+ place(0, 0)
+ }
+ }
+ }
+ .offset { offset }
+ ) {
+ LazyVerticalGrid(
+ GridCells.Fixed(2),
+ modifier = Modifier.requiredHeight(100.dp).requiredWidth(40.dp),
+ state = state
+ ) {
+ items(60) { index ->
+ Box(Modifier.size(20.dp).onGloballyPositioned { coords[index] = it })
+ }
+ }
+ }
+ }
+ }
+ repeat(4) {
+ val itemId = it * 5 * 2
+ offset = offsets[it]
+ rule.runOnIdle { runBlocking { state.scrollToItem(itemId) } }
+ rule.waitForIdle()
+ repeat(5) {
+ assertEquals(
+ offset,
+ coords[itemId + it]!!
+ .let {
+ rootCoords!!.localPositionOf(it, includeMotionFrameOfReference = false)
+ }
+ .round()
+ )
+ assertEquals(
+ offset + IntOffset(0 + it % 2 * 20, it / 2 * 20),
+ coords[itemId + it]!!
+ .let {
+ rootCoords!!.localPositionOf(it, includeMotionFrameOfReference = true)
+ }
+ .round()
+ )
+ }
+ }
+ }
+
+ @Test
+ fun testLazyStaggeredGrid() {
+ var offset by mutableStateOf(IntOffset(0, 0))
+ val coords = arrayOfNulls<LayoutCoordinates>(60)
+ var rootCoords: LayoutCoordinates? = null
+ val state = LazyStaggeredGridState()
+ val offsets =
+ listOf(
+ IntOffset(0, 0),
+ IntOffset(5, 20),
+ IntOffset(25, 0),
+ IntOffset(100, 10),
+ )
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides Density(1f)) {
+ Box(
+ Modifier.layout { m, c ->
+ m.measure(c).run {
+ layout(width, height) {
+ rootCoords = coordinates
+ place(0, 0)
+ }
+ }
+ }
+ .offset { offset }
+ ) {
+ LazyVerticalStaggeredGrid(
+ state = state,
+ columns = StaggeredGridCells.Fixed(2),
+ modifier = Modifier.requiredHeight(100.dp).requiredWidth(40.dp)
+ ) {
+ items(60) { index ->
+ Box(
+ Modifier.size(20.dp, ((index % 2) * 5).dp + 15.dp)
+ .onGloballyPositioned { coords[index] = it }
+ )
+ }
+ }
+ }
+ }
+ }
+ repeat(4) {
+ val itemId = it * 10
+ offset = offsets[it]
+ rule.runOnIdle { runBlocking { state.scrollToItem(itemId) } }
+ repeat(5) {
+ assertEquals(
+ offset,
+ coords[itemId + it]!!
+ .let {
+ rootCoords!!.localPositionOf(it, includeMotionFrameOfReference = false)
+ }
+ .round()
+ )
+ }
+ }
+ }
+
+ @Test
+ fun testPager() {
+ var offset by mutableStateOf(IntOffset(0, 0))
+ val coords = arrayOfNulls<LayoutCoordinates>(30)
+ var rootCoords: LayoutCoordinates? = null
+ val state = PagerState { 30 }
+ val offsets =
+ listOf(
+ IntOffset(0, 0),
+ IntOffset(5, 20),
+ IntOffset(25, 0),
+ IntOffset(100, 10),
+ )
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides Density(1f)) {
+ Box(
+ Modifier.layout { m, c ->
+ m.measure(c).run {
+ layout(width, height) {
+ rootCoords = coordinates
+ place(0, 0)
+ }
+ }
+ }
+ .offset { offset }
+ ) {
+ HorizontalPager(
+ state,
+ pageSize = PageSize.Fixed(20.dp),
+ modifier = Modifier.requiredHeight(20.dp).requiredWidth(100.dp)
+ ) { index ->
+ Box(Modifier.size(20.dp, 20.dp).onGloballyPositioned { coords[index] = it })
+ }
+ }
+ }
+ }
+ repeat(4) {
+ val itemId = it * 5
+ offset = offsets[it]
+ rule.runOnIdle { runBlocking { state.scrollToPage(itemId) } }
+ repeat(5) {
+ assertEquals(
+ offset,
+ requireNotNull(coords[itemId + it]) { "item $itemId, it = $it" }
+ .let {
+ rootCoords!!.localPositionOf(it, includeMotionFrameOfReference = false)
+ }
+ .round(),
+ )
+ }
+ }
+ }
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt
index 2273b6a..3ea0bed 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.zIndex
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
@@ -104,6 +105,52 @@
assertThat(visitedChildren).containsExactly(child1, child2, child3).inOrder()
}
+ @Test
+ fun visitChildrenInOtherLayoutNodesInDrawOrder_zIndex() {
+ // Arrange.
+ abstract class TrackedNode : Modifier.Node()
+ val (node, child1, child2, child3) = List(5) { object : TrackedNode() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ Box(Modifier.elementOf(node)) {
+ Box(Modifier.elementOf(child1).zIndex(10f))
+ Box(Modifier.elementOf(child2).zIndex(-10f))
+ Box(Modifier.elementOf(child3))
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ node.visitChildren(Nodes.Any, zOrder = true) {
+ @Suppress("KotlinConstantConditions") if (it is TrackedNode) visitedChildren.add(it)
+ }
+ }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(child2, child3, child1).inOrder()
+ }
+
+ @Test
+ fun visitChildrenInOtherLayoutNodesInDrawOrder_subcompose() {
+ // Arrange.
+ val (node, child1, child2, child3) = List(5) { object : Modifier.Node() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ ReverseMeasureLayout(
+ Modifier.elementOf(node),
+ { Box(Modifier.elementOf(child1)) },
+ { Box(Modifier.elementOf(child2)) },
+ { Box(Modifier.elementOf(child3)) }
+ )
+ }
+
+ // Act.
+ rule.runOnIdle { node.visitChildren(Nodes.Any, zOrder = true) { visitedChildren.add(it) } }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(child1, child2, child3).inOrder()
+ }
+
@Ignore("b/278765590")
@Test
fun skipsUnattachedLocalChild() {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt
index 63f82d9..6f1e610 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.zIndex
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
@@ -134,6 +135,58 @@
.inOrder()
}
+ @Test
+ fun visitsItemsAcrossLayoutNodesInDrawOrder_zIndex() {
+ // Arrange.
+ abstract class TrackedNode : Modifier.Node()
+ val (node, child1, child2, child3) = List(5) { object : TrackedNode() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ Box(Modifier.elementOf(node)) {
+ Box(Modifier.elementOf(child1).zIndex(10f))
+ Box(Modifier.elementOf(child2).zIndex(-10f))
+ Box(Modifier.elementOf(child3))
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ node.visitSubtreeIf(Nodes.Any, zOrder = true) {
+ @Suppress("KotlinConstantConditions") if (it is TrackedNode) visitedChildren.add(it)
+ true
+ }
+ }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(child2, child3, child1).inOrder()
+ }
+
+ @Test
+ fun visitsItemsAcrossLayoutNodesInDrawOrder_subcompose() {
+ // Arrange.
+ val (node, child1, child2, child3) = List(5) { object : Modifier.Node() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ ReverseMeasureLayout(
+ Modifier.elementOf(node),
+ { Box(Modifier.elementOf(child1)) },
+ { Box(Modifier.elementOf(child2)) },
+ { Box(Modifier.elementOf(child3)) }
+ )
+ }
+
+ // Act.
+ rule.runOnIdle {
+ node.visitSubtreeIf(Nodes.Any, zOrder = true) {
+ visitedChildren.add(it)
+ true
+ }
+ }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(child1, child2, child3).inOrder()
+ }
+
@Ignore("b/278765590")
@Test
fun skipsUnattachedItems() {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt
index 59d8aed..c2b585b5 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.zIndex
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
@@ -67,9 +68,6 @@
assertThat(visitedChildren).containsExactly(localChild1, localChild2).inOrder()
}
- // TODO(ralu): I feel that this order of visiting children is incorrect, and we should
- // visit children in the order of composition. So instead of a stack, we probably need
- // to use a queue to hold the intermediate nodes.
@Test
fun differentLayoutNodes() {
// Arrange.
@@ -79,10 +77,10 @@
val visitedChildren = mutableListOf<Modifier.Node>()
rule.setContent {
Box(Modifier.elementOf(node).elementOf(child1).elementOf(child2)) {
- Box(Modifier.elementOf(child5).elementOf(child6)) {
- Box(Modifier.elementOf(child7).elementOf(child8))
+ Box(Modifier.elementOf(child3).elementOf(child4)) {
+ Box(Modifier.elementOf(child5).elementOf(child6))
}
- Box { Box(Modifier.elementOf(child3).elementOf(child4)) }
+ Box { Box(Modifier.elementOf(child7).elementOf(child8)) }
}
}
@@ -95,6 +93,54 @@
.inOrder()
}
+ @Test
+ fun differentLayoutNodesInDrawOrder_zIndex() {
+ // Arrange.
+ abstract class TrackedNode : Modifier.Node()
+ val (node, child1, child2, child3, child4) = List(5) { object : TrackedNode() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ Box(Modifier.elementOf(node)) {
+ Box(Modifier.elementOf(child1))
+ Box(Modifier.elementOf(child2).zIndex(10f)) {
+ Box(Modifier.elementOf(child3).zIndex(-10f))
+ }
+ Box { Box(Modifier.elementOf(child4)) }
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ node.visitSubtree(Nodes.Any, zOrder = true) {
+ @Suppress("KotlinConstantConditions") if (it is TrackedNode) visitedChildren.add(it)
+ }
+ }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(child1, child4, child2, child3).inOrder()
+ }
+
+ @Test
+ fun differentLayoutNodesInDrawOrder_subcompose() {
+ // Arrange.
+ val (node, child1, child2, child3, child4) = List(5) { object : Modifier.Node() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ ReverseMeasureLayout(
+ Modifier.elementOf(node),
+ { Box(Modifier.elementOf(child1)) },
+ { Box(Modifier.elementOf(child2)) { Box(Modifier.elementOf(child3)) } },
+ { Box { Box(Modifier.elementOf(child4)) } }
+ )
+ }
+
+ // Act.
+ rule.runOnIdle { node.visitSubtree(Nodes.Any, zOrder = true) { visitedChildren.add(it) } }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(child1, child2, child3, child4).inOrder()
+ }
+
@Ignore("b/278765590")
@Test
fun skipsUnattached() {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NestedVectorStackTests.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NestedVectorStackTests.kt
deleted file mode 100644
index 74a3c0e..0000000
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NestedVectorStackTests.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.ui.node
-
-import androidx.compose.runtime.collection.mutableVectorOf
-import org.junit.Assert
-import org.junit.Test
-
-class NestedVectorStackTests {
-
- @Test
- fun testPushPopOrder() {
- val stack = NestedVectorStack<Int>()
- stack.push(mutableVectorOf(1, 2, 3))
- stack.push(mutableVectorOf(4, 5, 6))
- stack.push(mutableVectorOf())
- stack.push(mutableVectorOf(7))
- stack.push(mutableVectorOf(8, 9))
- val result = buildString {
- while (stack.isNotEmpty()) {
- append(stack.pop())
- }
- }
- Assert.assertEquals("987654321", result)
- }
-
- @Test
- fun testPopInBetweenPushes() {
- val stack = NestedVectorStack<Int>()
- stack.push(mutableVectorOf(1, 2, 3, 4))
- stack.pop()
- stack.push(mutableVectorOf(4, 5, 6))
- stack.pop()
- stack.pop()
- stack.push(mutableVectorOf())
- stack.push(mutableVectorOf(5, 6, 7))
- stack.push(mutableVectorOf(8, 9))
- val result = buildString {
- while (stack.isNotEmpty()) {
- append(stack.pop())
- }
- }
- Assert.assertEquals("987654321", result)
- }
-}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt
index 29b9c0a..e77e8d6 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt
@@ -16,7 +16,10 @@
package androidx.compose.ui.node
+import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.platform.InspectorInfo
/**
@@ -38,3 +41,30 @@
name = "testNode"
}
}
+
+@Composable
+internal fun ReverseMeasureLayout(modifier: Modifier, vararg contents: @Composable () -> Unit) =
+ SubcomposeLayout(modifier) { constraints ->
+ var layoutWidth = constraints.minWidth
+ var layoutHeight = constraints.minHeight
+ val subcomposes = mutableListOf<List<Placeable>>()
+
+ // Measure in reverse order
+ contents.reversed().forEachIndexed { index, content ->
+ subcomposes.add(
+ 0,
+ subcompose(index, content).map {
+ it.measure(constraints).also { placeable ->
+ layoutWidth = maxOf(layoutWidth, placeable.width)
+ layoutHeight = maxOf(layoutHeight, placeable.height)
+ }
+ }
+ )
+ }
+
+ layout(layoutWidth, layoutHeight) {
+
+ // But place in direct order - it sets direct draw order
+ subcomposes.forEach { placeables -> placeables.forEach { it.place(0, 0) } }
+ }
+ }
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingListParityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingListParityTest.kt
index 8d0b0ee..9ba98f3 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingListParityTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingListParityTest.kt
@@ -332,6 +332,90 @@
}
}
+ @Test
+ fun equalLists_withEqualFlings_shouldFinishAtTheSameItem_regularGestureOne() = runBlocking {
+ val state = LazyListState()
+
+ // starting with view
+ createActivity(state)
+ checkVisibility(composeView(), View.GONE)
+ checkVisibility(recyclerView(), View.VISIBLE)
+
+ regularGestureOne(R.id.view_list)
+ rule.waitForIdle()
+ recyclerView().awaitScrollIdle()
+
+ val childAtTheTopOfView = layoutManager?.findFirstVisibleItemPosition() ?: 0
+
+ // switch visibilities
+ rule.runOnUiThread {
+ rule.activity.findViewById<RecyclerView>(R.id.view_list).visibility = View.GONE
+ rule.activity.findViewById<ComposeView>(R.id.compose_view).visibility = View.VISIBLE
+ }
+
+ checkVisibility(composeView(), View.VISIBLE)
+ checkVisibility(recyclerView(), View.GONE)
+
+ assertTrue { isValidGesture(recyclerView().motionEvents.filterNotNull()) }
+
+ // Inject the same events in compose view
+ rule.runOnUiThread {
+ for (event in recyclerView().motionEvents) {
+ composeView().dispatchTouchEvent(event)
+ }
+ }
+
+ rule.runOnIdle {
+ val currentTopInCompose = state.firstVisibleItemIndex
+ val diff = (currentTopInCompose - childAtTheTopOfView).absoluteValue
+ val message =
+ "Compose=$currentTopInCompose View=$childAtTheTopOfView " + "Difference was=$diff"
+ assertTrue(message) { diff <= ItemDifferenceThreshold }
+ }
+ }
+
+ @Test
+ fun equalLists_withEqualFlings_shouldFinishAtTheSameItem_regularGestureTwo() = runBlocking {
+ val state = LazyListState()
+
+ // starting with view
+ createActivity(state)
+ checkVisibility(composeView(), View.GONE)
+ checkVisibility(recyclerView(), View.VISIBLE)
+
+ regularGestureTwo(R.id.view_list)
+ rule.waitForIdle()
+ recyclerView().awaitScrollIdle()
+
+ val childAtTheTopOfView = layoutManager?.findFirstVisibleItemPosition() ?: 0
+
+ // switch visibilities
+ rule.runOnUiThread {
+ rule.activity.findViewById<RecyclerView>(R.id.view_list).visibility = View.GONE
+ rule.activity.findViewById<ComposeView>(R.id.compose_view).visibility = View.VISIBLE
+ }
+
+ checkVisibility(composeView(), View.VISIBLE)
+ checkVisibility(recyclerView(), View.GONE)
+
+ assertTrue { isValidGesture(recyclerView().motionEvents.filterNotNull()) }
+
+ // Inject the same events in compose view
+ rule.runOnUiThread {
+ for (event in recyclerView().motionEvents) {
+ composeView().dispatchTouchEvent(event)
+ }
+ }
+
+ rule.runOnIdle {
+ val currentTopInCompose = state.firstVisibleItemIndex
+ val diff = (currentTopInCompose - childAtTheTopOfView).absoluteValue
+ val message =
+ "Compose=$currentTopInCompose View=$childAtTheTopOfView " + "Difference was=$diff"
+ assertTrue(message) { diff <= ItemDifferenceThreshold }
+ }
+ }
+
private fun createActivity(state: LazyListState) {
rule.activityRule.scenario.createActivityWithComposeContent(
R.layout.android_compose_lists_fling
@@ -385,7 +469,7 @@
@Composable
fun TestComposeList(state: LazyListState) {
LazyColumn(Modifier.fillMaxSize(), state = state) {
- items(1000) {
+ items(2000) {
Box(modifier = Modifier.fillMaxWidth().height(64.dp).background(Color.Black)) {
Text(text = it.toString(), color = Color.White)
}
@@ -394,7 +478,7 @@
}
private class ListAdapter : RecyclerView.Adapter<ListViewHolder>() {
- val items = (0 until 1000).map { it.toString() }
+ val items = (0 until 2000).map { it.toString() }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
return ListViewHolder(
@@ -451,4 +535,4 @@
}
}
-private const val ItemDifferenceThreshold = 3
+private const val ItemDifferenceThreshold = 1
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingParityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingParityTest.kt
index 4fef305..2e76112 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingParityTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingParityTest.kt
@@ -24,10 +24,9 @@
import androidx.activity.ComponentActivity
import androidx.annotation.LayoutRes
import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.awaitEachGesture
-import androidx.compose.foundation.gestures.awaitFirstDown
-import androidx.compose.foundation.gestures.awaitTouchSlopOrCancellation
import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.draggable2D
+import androidx.compose.foundation.gestures.rememberDraggable2DState
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@@ -38,14 +37,10 @@
import androidx.compose.ui.background
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.AwaitPointerEventScope
-import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerId
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.input.pointer.positionChangedIgnoreConsumed
import androidx.compose.ui.input.pointer.util.VelocityTrackerAddPointsFix
-import androidx.compose.ui.input.pointer.util.addPointerInputChange
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.ViewConfiguration
@@ -70,9 +65,7 @@
import com.google.errorprone.annotations.CanIgnoreReturnValue
import kotlin.math.absoluteValue
import kotlin.test.assertTrue
-import kotlinx.coroutines.coroutineScope
import org.junit.Before
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -272,7 +265,6 @@
}
@Test
- @Ignore("b/299092669")
fun equalDraggable_withEqualSwipes_shouldProduceSimilarVelocity_orthogonal() {
// Arrange
createActivity(true)
@@ -306,6 +298,70 @@
assertIsWithinTolerance(latestComposeVelocity.y, latestVelocityInViewY)
}
+ @Test
+ fun equalDraggable_withEqualSwipes_shouldProduceSimilarVelocity_regularSituationOne() {
+ // Arrange
+ createActivity()
+ checkVisibility(composeView, View.GONE)
+ checkVisibility(draggableView, View.VISIBLE)
+
+ // Act: Use system to send motion events and collect them.
+ regularGestureOne(R.id.draggable_view)
+
+ val latestVelocityInViewY = draggableView.latestVelocity.y
+
+ // switch visibility
+ rule.runOnUiThread {
+ composeView.visibility = View.VISIBLE
+ draggableView.visibility = View.GONE
+ }
+
+ checkVisibility(composeView, View.VISIBLE)
+ checkVisibility(draggableView, View.GONE)
+
+ assertTrue { isValidGesture(draggableView.motionEvents.filterNotNull()) }
+
+ // Inject the same events in compose view
+ for (event in draggableView.motionEvents) {
+ composeView.dispatchTouchEvent(event)
+ }
+
+ // assert
+ assertIsWithinTolerance(latestComposeVelocity.y, latestVelocityInViewY)
+ }
+
+ @Test
+ fun equalDraggable_withEqualSwipes_shouldProduceSimilarVelocity_regularSituationTwo() {
+ // Arrange
+ createActivity()
+ checkVisibility(composeView, View.GONE)
+ checkVisibility(draggableView, View.VISIBLE)
+
+ // Act: Use system to send motion events and collect them.
+ regularGestureTwo(R.id.draggable_view)
+
+ val latestVelocityInViewY = draggableView.latestVelocity.y
+
+ // switch visibility
+ rule.runOnUiThread {
+ composeView.visibility = View.VISIBLE
+ draggableView.visibility = View.GONE
+ }
+
+ checkVisibility(composeView, View.VISIBLE)
+ checkVisibility(draggableView, View.GONE)
+
+ assertTrue { isValidGesture(draggableView.motionEvents.filterNotNull()) }
+
+ // Inject the same events in compose view
+ for (event in draggableView.motionEvents) {
+ composeView.dispatchTouchEvent(event)
+ }
+
+ // assert
+ assertIsWithinTolerance(latestComposeVelocity.y, latestVelocityInViewY)
+ }
+
private fun createActivity(twoDimensional: Boolean = false) {
rule.activityRule.scenario.createActivityWithComposeContent(
R.layout.velocity_tracker_compose_vs_view
@@ -394,6 +450,24 @@
)
}
+internal fun regularGestureOne(id: Int) {
+ Espresso.onView(withId(id))
+ .perform(
+ espressoSwipe(
+ SwiperWithTime(100),
+ GeneralLocation.CENTER,
+ GeneralLocation.BOTTOM_CENTER
+ )
+ )
+}
+
+internal fun regularGestureTwo(id: Int) {
+ Espresso.onView(withId(id))
+ .perform(
+ espressoSwipe(SwiperWithTime(70), GeneralLocation.CENTER, GeneralLocation.TOP_CENTER)
+ )
+}
+
private fun espressoSwipe(
swiper: Swiper,
start: CoordinatesProvider,
@@ -418,7 +492,10 @@
.background(Color.Black)
.then(
if (twoDimensional) {
- Modifier.draggable2D(onDragStopped)
+ Modifier.draggable2D(
+ rememberDraggable2DState {},
+ onDragStopped = onDragStopped
+ )
} else {
Modifier.draggable(
rememberDraggableState(onDelta = {}),
@@ -431,32 +508,6 @@
}
}
-fun Modifier.draggable2D(onDragStopped: (Velocity) -> Unit) =
- this.pointerInput(Unit) {
- coroutineScope {
- awaitEachGesture {
- val tracker = androidx.compose.ui.input.pointer.util.VelocityTracker()
- val initialDown =
- awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
- tracker.addPointerInputChange(initialDown)
-
- awaitTouchSlopOrCancellation(initialDown.id) { change, _ ->
- tracker.addPointerInputChange(change)
- change.consume()
- }
-
- val lastEvent =
- awaitDragOrUp(initialDown.id) {
- tracker.addPointerInputChange(it)
- it.consume()
- it.positionChangedIgnoreConsumed()
- }
- lastEvent?.let { tracker.addPointerInputChange(it) }
- onDragStopped(tracker.calculateVelocity())
- }
- }
- }
-
private fun ActivityScenario<*>.createActivityWithComposeContent(
@LayoutRes layout: Int,
content: @Composable () -> Unit,
@@ -512,8 +563,8 @@
return down.size == 1 && move.isNotEmpty() && up.size == 1
}
-// 1% tolerance
-private const val VelocityDifferenceTolerance = 0.1f
+// 5% tolerance
+private const val VelocityDifferenceTolerance = 0.05f
/** Copied from androidx.test.espresso.action.Swipe */
internal data class SwiperWithTime(val gestureDurationMs: Int) : Swiper {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt
index 5abbb34..5906b9f 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt
@@ -15,8 +15,11 @@
*/
package androidx.compose.ui.window
-import android.content.res.Configuration
+import android.util.DisplayMetrics
import android.view.KeyEvent
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_UP
+import android.view.View
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
@@ -35,8 +38,11 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.lerp
+import androidx.compose.ui.gesture.MotionEvent
+import androidx.compose.ui.gesture.PointerProperties
+import androidx.compose.ui.input.pointer.PointerCoords
import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assertIsDisplayed
@@ -259,9 +265,9 @@
fun canFillScreenWidth_dependingOnProperty() {
var box1Width = 0
var box2Width = 0
- lateinit var configuration: Configuration
+ lateinit var displayMetrics: DisplayMetrics
rule.setContent {
- configuration = LocalConfiguration.current
+ displayMetrics = LocalView.current.context.resources.displayMetrics
Dialog(
onDismissRequest = {},
properties = DialogProperties(usePlatformDefaultWidth = false)
@@ -272,7 +278,7 @@
Box(Modifier.fillMaxSize().onSizeChanged { box2Width = it.width })
}
}
- val expectedWidth = with(rule.density) { configuration.screenWidthDp.dp.roundToPx() }
+ val expectedWidth = with(rule.density) { displayMetrics.widthPixels }
assertThat(box1Width).isEqualTo(expectedWidth)
assertThat(box2Width).isLessThan(box1Width)
}
@@ -313,6 +319,75 @@
}
}
+ @Test
+ fun dismissWhenClickingOutsideContent() {
+ var dismissed = false
+ var clicked = false
+ lateinit var composeView: View
+ val clickBoxTag = "clickBox"
+ rule.setContent {
+ Dialog(
+ onDismissRequest = { dismissed = true },
+ properties =
+ DialogProperties(
+ usePlatformDefaultWidth = false,
+ decorFitsSystemWindows = false
+ )
+ ) {
+ composeView = LocalView.current
+ Box(Modifier.size(10.dp).testTag(clickBoxTag).clickable { clicked = true })
+ }
+ }
+
+ // click inside the compose view
+ rule.onNodeWithTag(clickBoxTag).performClick()
+
+ rule.waitForIdle()
+
+ assertThat(dismissed).isFalse()
+ assertThat(clicked).isTrue()
+
+ clicked = false
+
+ // click outside the compose view
+ rule.waitForIdle()
+ var root = composeView
+ while (root.parent is View) {
+ root = root.parent as View
+ }
+
+ rule.runOnIdle {
+ val x = root.width / 4f
+ val y = root.height / 4f
+ val down =
+ MotionEvent(
+ eventTime = 0,
+ action = ACTION_DOWN,
+ numPointers = 1,
+ actionIndex = 0,
+ pointerProperties = arrayOf(PointerProperties(0)),
+ pointerCoords = arrayOf(PointerCoords(x, y)),
+ root
+ )
+ root.dispatchTouchEvent(down)
+ val up =
+ MotionEvent(
+ eventTime = 10,
+ action = ACTION_UP,
+ numPointers = 1,
+ actionIndex = 0,
+ pointerProperties = arrayOf(PointerProperties(0)),
+ pointerCoords = arrayOf(PointerCoords(x, y)),
+ root
+ )
+ root.dispatchTouchEvent(up)
+ }
+ rule.waitForIdle()
+
+ assertThat(dismissed).isTrue()
+ assertThat(clicked).isFalse()
+ }
+
private fun setupDialogTest(
closeDialogOnDismiss: Boolean = true,
dialogProperties: DialogProperties = DialogProperties(),
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt
index e85b14c..cde57e5 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt
@@ -15,12 +15,16 @@
*/
package androidx.compose.ui.window
+import android.content.res.Configuration
+import android.os.Build
import android.view.View
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.safeDrawing
+import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.material.TextField
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.Alignment
@@ -29,10 +33,18 @@
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.SoftwareKeyboardController
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.requestFocus
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.core.graphics.Insets
@@ -41,8 +53,10 @@
import androidx.core.view.WindowInsetsControllerCompat
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
+import kotlin.math.roundToInt
import org.junit.Assert.assertNotEquals
import org.junit.Rule
import org.junit.Test
@@ -129,6 +143,90 @@
assertNotEquals(Insets.NONE, imeInsets)
}
+ @Test
+ fun dialogCanTakeEntireScreen() {
+ var size = IntSize.Zero
+ var displayWidth = 0
+ var displayHeight = 0
+ var insetsLeft = 0
+ var insetsTop = 0
+ var insetsRight = 0
+ var insetsBottom = 0
+ var textTop = 0
+ var controller: SoftwareKeyboardController? = null
+ rule.setContent {
+ val displayMetrics = LocalView.current.resources.displayMetrics
+ controller = LocalSoftwareKeyboardController.current
+ displayWidth = displayMetrics.widthPixels
+ displayHeight = displayMetrics.heightPixels
+ Box(Modifier.fillMaxSize()) {
+ Dialog(
+ {},
+ properties =
+ DialogProperties(
+ decorFitsSystemWindows = false,
+ usePlatformDefaultWidth = false
+ )
+ ) {
+ val insets = WindowInsets.safeDrawing
+
+ Box(
+ Modifier.fillMaxSize()
+ .layout { m, c ->
+ val p = m.measure(c)
+ size = IntSize(p.width, p.height)
+ insetsTop = insets.getTop(this)
+ insetsLeft = insets.getLeft(this, layoutDirection)
+ insetsBottom = insets.getBottom(this)
+ insetsRight = insets.getRight(this, layoutDirection)
+ layout(p.width, p.height) { p.place(0, 0) }
+ }
+ .safeDrawingPadding()
+ ) {
+ TextField(
+ value = "Hello",
+ onValueChange = {},
+ Modifier.align(Alignment.BottomStart).testTag("textField").onPlaced {
+ layoutCoordinates ->
+ textTop = layoutCoordinates.positionInRoot().y.roundToInt()
+ }
+ )
+ }
+ }
+ }
+ }
+ rule.waitForIdle()
+
+ if (
+ Build.VERSION.SDK_INT >= 35 &&
+ rule.activity.applicationContext.applicationInfo.targetSdkVersion >= 35
+ ) {
+ // On SDK >= 35, the metrics is the size of the entire screen
+ assertThat(size.width).isEqualTo(displayWidth)
+ assertThat(size.height).isEqualTo(displayHeight)
+ } else {
+ // On SDK < 35, the metrics is the size of the screen with some insets removed
+ assertThat(size.width).isAtLeast(displayWidth)
+ assertThat(size.height).isAtLeast(displayHeight)
+ }
+ // There is going to be some insets
+ assertThat(maxOf(insetsLeft, insetsTop, insetsRight, insetsBottom)).isNotEqualTo(0)
+
+ val hardKeyboardHidden =
+ rule.runOnUiThread { rule.activity.resources.configuration.hardKeyboardHidden }
+ if (hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO) {
+ return // can't launch the IME when the hardware keyboard is up.
+ }
+ val bottomInsetsBeforeIme = insetsBottom
+ val textTopBeforeIme = textTop
+ rule.onNodeWithTag("textField").requestFocus()
+ rule.waitUntil {
+ controller?.show()
+ insetsBottom != bottomInsetsBeforeIme
+ }
+ rule.runOnIdle { assertThat(textTop).isLessThan(textTopBeforeIme) }
+ }
+
private fun findDialogWindowProviderInParent(view: View): DialogWindowProvider? {
if (view is DialogWindowProvider) {
return view
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
index 83c30cd..475f5dba 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
@@ -20,13 +20,16 @@
import android.graphics.Outline
import android.os.Build
import android.view.ContextThemeWrapper
+import android.view.Gravity
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
+import android.view.View.OnLayoutChangeListener
import android.view.ViewGroup
import android.view.ViewOutlineProvider
import android.view.Window
import android.view.WindowManager
+import android.widget.FrameLayout
import androidx.activity.ComponentDialog
import androidx.activity.addCallback
import androidx.compose.runtime.Composable
@@ -57,8 +60,11 @@
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastMaxBy
-import androidx.compose.ui.util.fastRoundToInt
+import androidx.core.view.OnApplyWindowInsetsListener
+import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsAnimationCompat
+import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.findViewTreeViewModelStoreOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
@@ -77,16 +83,19 @@
* @property securePolicy Policy for setting [WindowManager.LayoutParams.FLAG_SECURE] on the
* dialog's window.
* @property usePlatformDefaultWidth Whether the width of the dialog's content should be limited to
- * the platform default, which is smaller than the screen width.
+ * the platform default, which is smaller than the screen width. It is recommended to use
+ * [decorFitsSystemWindows] set to `false` when [usePlatformDefaultWidth] is false to support
+ * using the entire screen and avoiding UI glitches on some devices when the IME animates in.
* @property decorFitsSystemWindows Sets [WindowCompat.setDecorFitsSystemWindows] value. Set to
* `false` to use WindowInsets. If `false`, the
* [soft input mode][WindowManager.LayoutParams.softInputMode] will be changed to
* [WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE] and `android:windowIsFloating` is set to
- * `false` for Android [R][Build.VERSION_CODES.R] and earlier.
+ * `false` when [decorFitsSystemWindows] is false. When
+ * `targetSdk` >= [Build.VERSION_CODES.VANILLA_ICE_CREAM], [decorFitsSystemWindows] can only be
+ * `false` and this property doesn't have any effect.
*/
@Immutable
-actual class DialogProperties
-constructor(
+actual class DialogProperties(
actual val dismissOnBackPress: Boolean = true,
actual val dismissOnClickOutside: Boolean = true,
val securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit,
@@ -218,6 +227,7 @@
private var content: @Composable () -> Unit by mutableStateOf({})
var usePlatformDefaultWidth = false
+ var decorFitsSystemWindows = false
override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
private set
@@ -229,50 +239,16 @@
createComposition()
}
- override fun internalOnMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
- if (usePlatformDefaultWidth) {
- super.internalOnMeasure(widthMeasureSpec, heightMeasureSpec)
- } else {
- // usePlatformDefaultWidth false, so don't want to limit the dialog width to the Android
- // platform default. Therefore, we create a new measure spec for width, which
- // corresponds to the full screen width. We do the same for height, even if
- // ViewRootImpl gives it to us from the first measure.
- val displayWidthMeasureSpec =
- MeasureSpec.makeMeasureSpec(displayWidth, MeasureSpec.AT_MOST)
- val displayHeightMeasureSpec =
- MeasureSpec.makeMeasureSpec(displayHeight, MeasureSpec.AT_MOST)
- super.internalOnMeasure(displayWidthMeasureSpec, displayHeightMeasureSpec)
- }
- }
-
- override fun internalOnLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
- super.internalOnLayout(changed, left, top, right, bottom)
- // Now set the content size as fixed layout params, such that ViewRootImpl knows
- // the exact window size.
- if (!usePlatformDefaultWidth) {
- val child = getChildAt(0) ?: return
- window.setLayout(child.measuredWidth, child.measuredHeight)
- }
- }
-
- private val displayWidth: Int
- get() {
- val density = context.resources.displayMetrics.density
- return (context.resources.configuration.screenWidthDp * density).fastRoundToInt()
- }
-
- private val displayHeight: Int
- get() {
- val density = context.resources.displayMetrics.density
- return (context.resources.configuration.screenHeightDp * density).fastRoundToInt()
- }
-
@Composable
override fun Content() {
content()
}
}
+private fun adjustedDecorFitsSystemWindows(dialogProperties: DialogProperties, context: Context) =
+ dialogProperties.decorFitsSystemWindows &&
+ context.applicationInfo.targetSdkVersion < Build.VERSION_CODES.VANILLA_ICE_CREAM
+
private class DialogWrapper(
private var onDismissRequest: () -> Unit,
private var properties: DialogProperties,
@@ -288,16 +264,16 @@
*/
ContextThemeWrapper(
composeView.context,
- if (
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.S || properties.decorFitsSystemWindows
- ) {
+ if (adjustedDecorFitsSystemWindows(properties, composeView.context)) {
R.style.DialogWindowTheme
} else {
R.style.FloatingDialogWindowTheme
}
)
),
- ViewRootForInspector {
+ ViewRootForInspector,
+ OnApplyWindowInsetsListener,
+ OnLayoutChangeListener {
private val dialogLayout: DialogLayout
@@ -308,15 +284,12 @@
override val subCompositionView: AbstractComposeView
get() = dialogLayout
- private val defaultSoftInputMode: Int
-
init {
val window = window ?: error("Dialog has no window")
- defaultSoftInputMode =
- window.attributes.softInputMode and WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST
window.requestFeature(Window.FEATURE_NO_TITLE)
window.setBackgroundDrawableResource(android.R.color.transparent)
- WindowCompat.setDecorFitsSystemWindows(window, properties.decorFitsSystemWindows)
+ val decorFitsSystemWindows = adjustedDecorFitsSystemWindows(properties, context)
+ WindowCompat.setDecorFitsSystemWindows(window, decorFitsSystemWindows)
dialogLayout =
DialogLayout(context, window).apply {
// Set unique id for AbstractComposeView. This allows state restoration for the
@@ -336,10 +309,8 @@
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.
+ // draw its own shadow. This still enables us to get the extra space
+ // needed in the surface.
result.alpha = 0f
}
}
@@ -359,7 +330,38 @@
// Turn of all clipping so shadows can be drawn outside the window
(window.decorView as? ViewGroup)?.disableClipping()
- setContentView(dialogLayout)
+ // Center the ComposeView in a FrameLayout
+ val frameLayout = FrameLayout(context)
+ frameLayout.addView(
+ dialogLayout,
+ FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.WRAP_CONTENT,
+ FrameLayout.LayoutParams.WRAP_CONTENT
+ )
+ .also { it.gravity = Gravity.CENTER }
+ )
+ frameLayout.setOnClickListener { onDismissRequest() }
+ ViewCompat.setOnApplyWindowInsetsListener(frameLayout, this)
+ ViewCompat.setWindowInsetsAnimationCallback(
+ frameLayout,
+ object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
+ override fun onProgress(
+ insets: WindowInsetsCompat,
+ runningAnimations: MutableList<WindowInsetsAnimationCompat>
+ ): WindowInsetsCompat {
+ return insets.inset(
+ dialogLayout.left,
+ dialogLayout.top,
+ frameLayout.width - dialogLayout.right,
+ frameLayout.height - dialogLayout.bottom
+ )
+ }
+ }
+ )
+ dialogLayout.addOnLayoutChangeListener(this)
+ frameLayout.addOnLayoutChangeListener(this)
+
+ setContentView(frameLayout)
dialogLayout.setViewTreeLifecycleOwner(composeView.findViewTreeLifecycleOwner())
dialogLayout.setViewTreeViewModelStoreOwner(composeView.findViewTreeViewModelStoreOwner())
dialogLayout.setViewTreeSavedStateRegistryOwner(
@@ -430,21 +432,42 @@
this.properties = properties
setSecurePolicy(properties.securePolicy)
setLayoutDirection(layoutDirection)
- if (properties.usePlatformDefaultWidth && !dialogLayout.usePlatformDefaultWidth) {
- // Undo fixed size in internalOnLayout, which would suppress size changes when
- // usePlatformDefaultWidth is true.
- window?.setLayout(
- WindowManager.LayoutParams.WRAP_CONTENT,
- WindowManager.LayoutParams.WRAP_CONTENT
- )
- }
dialogLayout.usePlatformDefaultWidth = properties.usePlatformDefaultWidth
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
- if (properties.decorFitsSystemWindows) {
- window?.setSoftInputMode(defaultSoftInputMode)
- } else {
- @Suppress("DEPRECATION")
- window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
+ val decorFitsSystemWindows = adjustedDecorFitsSystemWindows(properties, context)
+ dialogLayout.decorFitsSystemWindows = decorFitsSystemWindows
+ val window = window
+ if (window != null) {
+ val softInput =
+ when {
+ decorFitsSystemWindows ->
+ WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.S ->
+ @Suppress("DEPRECATION") WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
+ else -> WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING
+ }
+ window.setSoftInputMode(softInput)
+ val attrs = window.attributes
+ val measurementWidth =
+ if (properties.usePlatformDefaultWidth) {
+ WindowManager.LayoutParams.WRAP_CONTENT
+ } else {
+ WindowManager.LayoutParams.MATCH_PARENT
+ }
+ val measurementHeight =
+ if (properties.usePlatformDefaultWidth || decorFitsSystemWindows) {
+ WindowManager.LayoutParams.WRAP_CONTENT
+ } else {
+ WindowManager.LayoutParams.MATCH_PARENT
+ }
+ if (
+ attrs.width != measurementWidth ||
+ attrs.height != measurementHeight ||
+ attrs.gravity != Gravity.CENTER
+ ) {
+ attrs.width = measurementWidth
+ attrs.height = measurementHeight
+ attrs.gravity = Gravity.CENTER
+ window.attributes = attrs
}
}
}
@@ -466,6 +489,28 @@
// Prevents the dialog from dismissing itself
return
}
+
+ override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
+ val left = dialogLayout.left
+ val top = dialogLayout.top
+ val right = v.width - dialogLayout.right
+ val bottom = v.height - dialogLayout.bottom
+ return insets.inset(left, top, right, bottom)
+ }
+
+ override fun onLayoutChange(
+ v: View,
+ left: Int,
+ top: Int,
+ right: Int,
+ bottom: Int,
+ oldLeft: Int,
+ oldTop: Int,
+ oldRight: Int,
+ oldBottom: Int
+ ) {
+ v.requestApplyInsets()
+ }
}
@Composable
diff --git a/compose/ui/ui/src/androidMain/res/values/styles.xml b/compose/ui/ui/src/androidMain/res/values/styles.xml
index e1211d4..d0e837b 100644
--- a/compose/ui/ui/src/androidMain/res/values/styles.xml
+++ b/compose/ui/ui/src/androidMain/res/values/styles.xml
@@ -19,11 +19,13 @@
<style name="DialogWindowTheme">
<item name="android:windowClipToOutline">false</item>
</style>
- <!-- Style for decorFitsSystemWindows = false on API 30 and earlier. WindowInsets won't
- be set on Dialogs without android:windowIsFloating set to false. -->
+ <!-- Style for decorFitsSystemWindows = false -->
<style name="FloatingDialogWindowTheme">
<item name="android:windowClipToOutline">false</item>
<item name="android:dialogTheme">@style/FloatingDialogTheme</item>
+ <item name="android:statusBarColor">@android:color/transparent</item>
+ <item name="android:navigationBarColor">@android:color/transparent</item>
+ <item name="android:backgroundDimEnabled">true</item>
</style>
<style name="FloatingDialogTheme">
<item name="android:windowIsFloating">false</item>
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
index c1363c7..0328f65 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
@@ -209,14 +209,14 @@
layout(d)
}
val recorder = Recorder()
- x.visitSubtree(Nodes.Draw, recorder)
+ x.visitSubtree(Nodes.Draw, block = recorder)
assertThat(recorder.recorded)
.isEqualTo(
listOf(
a.wrapped,
b,
- d,
c.wrapped,
+ d,
)
)
}
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/NestedVectorStackTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/NestedVectorStackTest.kt
deleted file mode 100644
index 951cb3d..0000000
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/NestedVectorStackTest.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * 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.compose.ui.node
-
-import androidx.compose.runtime.collection.mutableVectorOf
-import com.google.common.truth.Truth
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class NestedVectorStackTest {
-
- @Test
- fun testEnumerationOrder() {
- val stack = NestedVectorStack<Int>()
- stack.push(mutableVectorOf(1, 2, 3))
- stack.push(mutableVectorOf(4, 5, 6))
-
- Truth.assertThat(stack.enumerate()).isEqualTo(listOf(6, 5, 4, 3, 2, 1))
- }
-
- @Test
- fun testEnumerationOrderPartiallyPoppingMiddleVectors() {
- val stack = NestedVectorStack<Int>()
- stack.push(mutableVectorOf(1, 2, 3))
-
- Truth.assertThat(stack.pop()).isEqualTo(3)
-
- stack.push(mutableVectorOf(4, 5, 6))
-
- Truth.assertThat(stack.pop()).isEqualTo(6)
-
- Truth.assertThat(stack.enumerate()).isEqualTo(listOf(5, 4, 2, 1))
- }
-
- @Test
- fun testEnumerationOrderFullyPoppingMiddleVectors() {
- val stack = NestedVectorStack<Int>()
- stack.push(mutableVectorOf(1, 2, 3))
-
- Truth.assertThat(stack.pop()).isEqualTo(3)
- Truth.assertThat(stack.pop()).isEqualTo(2)
- Truth.assertThat(stack.pop()).isEqualTo(1)
-
- stack.push(mutableVectorOf(4, 5, 6))
-
- Truth.assertThat(stack.pop()).isEqualTo(6)
-
- Truth.assertThat(stack.enumerate()).isEqualTo(listOf(5, 4))
- }
-}
-
-internal fun <T> NestedVectorStack<T>.enumerate(): List<T> {
- val result = mutableListOf<T>()
- var item: T? = pop()
- while (item != null) {
- result.add(item)
- item = if (isNotEmpty()) pop() else null
- }
- return result
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt
index 8010fb9..d64d81b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt
@@ -90,7 +90,7 @@
override fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int>,
+ alignmentLines: Map<out AlignmentLine, Int>,
rulers: (RulerScope.() -> Unit)?,
placementBlock: Placeable.PlacementScope.() -> Unit
): MeasureResult {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
index dc853ae..1bdba48 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
@@ -351,7 +351,7 @@
override fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int>,
+ alignmentLines: Map<out AlignmentLine, Int>,
rulers: (RulerScope.() -> Unit)?,
placementBlock: Placeable.PlacementScope.() -> Unit
): MeasureResult {
@@ -365,7 +365,7 @@
override val height: Int
get() = h
- override val alignmentLines: Map<AlignmentLine, Int>
+ override val alignmentLines: Map<out AlignmentLine, Int>
get() = alignmentLines
override val rulers: (RulerScope.() -> Unit)?
@@ -385,7 +385,7 @@
override fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int>,
+ alignmentLines: Map<out AlignmentLine, Int>,
rulers: (RulerScope.() -> Unit)?,
placementBlock: Placeable.PlacementScope.() -> Unit
): MeasureResult {
@@ -399,7 +399,7 @@
override val height: Int
get() = h
- override val alignmentLines: Map<AlignmentLine, Int>
+ override val alignmentLines: Map<out AlignmentLine, Int>
get() = alignmentLines
override val rulers: (RulerScope.() -> Unit)?
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureResult.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureResult.kt
index a2b16cc..e7a3137 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureResult.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureResult.kt
@@ -19,7 +19,7 @@
* Alignment lines that can be used by parents to align this layout. This only includes the
* alignment lines of this layout and not children.
*/
- val alignmentLines: Map<AlignmentLine, Int>
+ val alignmentLines: Map<out AlignmentLine, Int>
/**
* An optional lambda function used to create [Ruler]s for child layout. This may be
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureScope.kt
index 36e7ea8..978165e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureScope.kt
@@ -47,7 +47,7 @@
fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int> = emptyMap(),
+ alignmentLines: Map<out AlignmentLine, Int> = emptyMap(),
placementBlock: Placeable.PlacementScope.() -> Unit
) = layout(width, height, alignmentLines, null, placementBlock)
@@ -69,7 +69,7 @@
fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int> = emptyMap(),
+ alignmentLines: Map<out AlignmentLine, Int> = emptyMap(),
rulers: (RulerScope.() -> Unit)? = null,
placementBlock: Placeable.PlacementScope.() -> Unit
): MeasureResult {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
index 442dada..08a5f0c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
@@ -911,7 +911,7 @@
override fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int>,
+ alignmentLines: Map<out AlignmentLine, Int>,
rulers: (RulerScope.() -> Unit)?,
placementBlock: Placeable.PlacementScope.() -> Unit
): MeasureResult {
@@ -923,7 +923,7 @@
override val height: Int
get() = height
- override val alignmentLines: Map<AlignmentLine, Int>
+ override val alignmentLines: Map<out AlignmentLine, Int>
get() = alignmentLines
override val rulers: (RulerScope.() -> Unit)?
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
index ffc88f5..f967cd8 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
@@ -99,48 +99,33 @@
return null
}
-internal inline fun DelegatableNode.visitSubtree(mask: Int, block: (Modifier.Node) -> Unit) {
- // TODO(lmr): we might want to add some safety wheels to prevent this from being called
- // while one of the chains is being diffed / updated.
- checkPrecondition(node.isAttached) { "visitSubtree called on an unattached node" }
- var node: Modifier.Node? = node.child
- var layout: LayoutNode? = requireLayoutNode()
- // we use this bespoke data structure here specifically for traversing children. In the
- // depth first traversal you would typically do a `stack.addAll(node.children)` type
- // call, but to avoid enumerating the vector and moving into our stack, we simply keep
- // a stack of vectors and keep track of where we are in each
- val nodes = NestedVectorStack<LayoutNode>()
- while (layout != null) {
- // NOTE: the ?: is important here for the starting condition, since we are starting
- // at THIS node, and not the head of this node chain.
- node = node ?: layout.nodes.head
- if (node.aggregateChildKindSet and mask != 0) {
- while (node != null) {
- if (node.kindSet and mask != 0) {
- block(node)
- }
- node = node.child
- }
- }
- node = null
- nodes.push(layout._children)
- layout = if (nodes.isNotEmpty()) nodes.pop() else null
+private fun LayoutNode.getChildren(zOrder: Boolean) =
+ if (zOrder) {
+ zSortedChildren
+ } else {
+ _children
}
+
+private fun MutableVector<Modifier.Node>.addLayoutNodeChildren(
+ node: Modifier.Node,
+ zOrder: Boolean,
+) {
+ node.requireLayoutNode().getChildren(zOrder).forEachReversed { add(it.nodes.head) }
}
-private fun MutableVector<Modifier.Node>.addLayoutNodeChildren(node: Modifier.Node) {
- node.requireLayoutNode()._children.forEachReversed { add(it.nodes.head) }
-}
-
-internal inline fun DelegatableNode.visitChildren(mask: Int, block: (Modifier.Node) -> Unit) {
+internal inline fun DelegatableNode.visitChildren(
+ mask: Int,
+ zOrder: Boolean,
+ block: (Modifier.Node) -> Unit
+) {
check(node.isAttached) { "visitChildren called on an unattached node" }
val branches = mutableVectorOf<Modifier.Node>()
val child = node.child
- if (child == null) branches.addLayoutNodeChildren(node) else branches.add(child)
+ if (child == null) branches.addLayoutNodeChildren(node, zOrder) else branches.add(child)
while (branches.isNotEmpty()) {
val branch = branches.removeAt(branches.lastIndex)
if (branch.aggregateChildKindSet and mask == 0) {
- branches.addLayoutNodeChildren(branch)
+ branches.addLayoutNodeChildren(branch, zOrder)
// none of these nodes match the mask, so don't bother traversing them
continue
}
@@ -159,11 +144,15 @@
* visit the shallow tree of children of a given mask, but if block returns true, we will continue
* traversing below it
*/
-internal inline fun DelegatableNode.visitSubtreeIf(mask: Int, block: (Modifier.Node) -> Boolean) {
+internal inline fun DelegatableNode.visitSubtreeIf(
+ mask: Int,
+ zOrder: Boolean,
+ block: (Modifier.Node) -> Boolean
+) {
checkPrecondition(node.isAttached) { "visitSubtreeIf called on an unattached node" }
val branches = mutableVectorOf<Modifier.Node>()
val child = node.child
- if (child == null) branches.addLayoutNodeChildren(node) else branches.add(child)
+ if (child == null) branches.addLayoutNodeChildren(node, zOrder) else branches.add(child)
outer@ while (branches.isNotEmpty()) {
val branch = branches.removeAt(branches.size - 1)
if (branch.aggregateChildKindSet and mask != 0) {
@@ -176,7 +165,7 @@
node = node.child
}
}
- branches.addLayoutNodeChildren(branch)
+ branches.addLayoutNodeChildren(branch, zOrder)
}
}
@@ -264,33 +253,41 @@
return null
}
-internal inline fun <reified T> DelegatableNode.visitSubtree(
- type: NodeKind<T>,
- block: (T) -> Unit
-) = visitSubtree(type.mask) { it.dispatchForKind(type, block) }
-
internal inline fun <reified T> DelegatableNode.visitChildren(
type: NodeKind<T>,
+ zOrder: Boolean = false,
block: (T) -> Unit
-) = visitChildren(type.mask) { it.dispatchForKind(type, block) }
+) = visitChildren(type.mask, zOrder) { it.dispatchForKind(type, block) }
internal inline fun <reified T> DelegatableNode.visitSelfAndChildren(
type: NodeKind<T>,
+ zOrder: Boolean = false,
block: (T) -> Unit
) {
node.dispatchForKind(type, block)
- visitChildren(type.mask) { it.dispatchForKind(type, block) }
+ visitChildren(type.mask, zOrder) { it.dispatchForKind(type, block) }
}
internal inline fun <reified T> DelegatableNode.visitSubtreeIf(
type: NodeKind<T>,
+ zOrder: Boolean = false,
block: (T) -> Boolean
) =
- visitSubtreeIf(type.mask) foo@{ node ->
+ visitSubtreeIf(type.mask, zOrder) foo@{ node ->
node.dispatchForKind(type) { if (!block(it)) return@foo false }
true
}
+internal inline fun <reified T> DelegatableNode.visitSubtree(
+ type: NodeKind<T>,
+ zOrder: Boolean = false,
+ block: (T) -> Unit
+) =
+ visitSubtreeIf(type.mask, zOrder) {
+ it.dispatchForKind(type, block)
+ true
+ }
+
internal fun DelegatableNode.has(type: NodeKind<*>): Boolean =
node.aggregateChildKindSet and type.mask != 0
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt
index a4d667fb..fe22dff 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt
@@ -94,9 +94,9 @@
/** The alignment lines of this layout, inherited + intrinsic */
private val alignmentLineMap: MutableMap<AlignmentLine, Int> = hashMapOf()
- fun getLastCalculation(): Map<AlignmentLine, Int> = alignmentLineMap
+ fun getLastCalculation(): Map<out AlignmentLine, Int> = alignmentLineMap
- protected abstract val NodeCoordinator.alignmentLinesMap: Map<AlignmentLine, Int>
+ protected abstract val NodeCoordinator.alignmentLinesMap: Map<out AlignmentLine, Int>
protected abstract fun NodeCoordinator.getPositionFor(alignmentLine: AlignmentLine): Int
@@ -201,7 +201,7 @@
internal class LayoutNodeAlignmentLines(alignmentLinesOwner: AlignmentLinesOwner) :
AlignmentLines(alignmentLinesOwner) {
- override val NodeCoordinator.alignmentLinesMap: Map<AlignmentLine, Int>
+ override val NodeCoordinator.alignmentLinesMap: Map<out AlignmentLine, Int>
get() = measureResult.alignmentLines
override fun NodeCoordinator.getPositionFor(alignmentLine: AlignmentLine): Int =
@@ -215,7 +215,7 @@
internal class LookaheadAlignmentLines(alignmentLinesOwner: AlignmentLinesOwner) :
AlignmentLines(alignmentLinesOwner) {
- override val NodeCoordinator.alignmentLinesMap: Map<AlignmentLine, Int>
+ override val NodeCoordinator.alignmentLinesMap: Map<out AlignmentLine, Int>
get() = lookaheadDelegate!!.measureResult.alignmentLines
override fun NodeCoordinator.getPositionFor(alignmentLine: AlignmentLine): Int =
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
index 3df4e68..bc03b51 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
@@ -950,7 +950,7 @@
return true
}
- override fun calculateAlignmentLines(): Map<AlignmentLine, Int> {
+ override fun calculateAlignmentLines(): Map<out AlignmentLine, Int> {
if (!duringAlignmentLinesQuery) {
// Mark alignments used by modifier
if (layoutState == LayoutState.Measuring) {
@@ -1278,7 +1278,7 @@
}
}
- override fun calculateAlignmentLines(): Map<AlignmentLine, Int> {
+ override fun calculateAlignmentLines(): Map<out AlignmentLine, Int> {
if (!duringAlignmentLinesQuery) {
if (layoutState == LayoutState.LookaheadMeasuring) {
// Mark alignments used by modifier
@@ -1894,7 +1894,7 @@
fun layoutChildren()
/** Recalculate the alignment lines if dirty, and layout children as needed. */
- fun calculateAlignmentLines(): Map<AlignmentLine, Int>
+ fun calculateAlignmentLines(): Map<out AlignmentLine, Int>
/**
* Parent [AlignmentLinesOwner]. This will be the AlignmentLinesOwner for the same pass but for
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
index f5e4439..8bf8d59 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
@@ -203,7 +203,7 @@
override fun layout(
width: Int,
height: Int,
- alignmentLines: Map<AlignmentLine, Int>,
+ alignmentLines: Map<out AlignmentLine, Int>,
rulers: (RulerScope.() -> Unit)?,
placementBlock: PlacementScope.() -> Unit
): MeasureResult {
@@ -215,7 +215,7 @@
override val height: Int
get() = height
- override val alignmentLines: Map<AlignmentLine, Int>
+ override val alignmentLines: Map<out AlignmentLine, Int>
get() = alignmentLines
override val rulers: (RulerScope.() -> Unit)?
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NestedVectorStack.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NestedVectorStack.kt
deleted file mode 100644
index 7f93d07..0000000
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NestedVectorStack.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.ui.node
-
-import androidx.compose.runtime.collection.MutableVector
-
-internal class NestedVectorStack<T> {
- // number of vectors in the stack
- private var size = 0
- // holds the current "top" index for each vector
- private var currentIndexes = IntArray(16)
- private var vectors = arrayOfNulls<MutableVector<T>>(16)
-
- fun isNotEmpty(): Boolean {
- return size > 0 && currentIndexes[size - 1] >= 0
- }
-
- fun pop(): T {
- check(size > 0) { "Cannot call pop() on an empty stack. Guard with a call to isNotEmpty()" }
- val indexOfVector = size - 1
- val indexOfItem = currentIndexes[indexOfVector]
- val vector = vectors[indexOfVector]!!
- if (indexOfItem > 0) currentIndexes[indexOfVector]--
- else if (indexOfItem == 0) {
- vectors[indexOfVector] = null
- size--
- }
- return vector[indexOfItem]
- }
-
- fun push(vector: MutableVector<T>) {
- // if the vector is empty there is no reason for us to add it
- if (vector.isEmpty()) return
- val nextIndex = size
- // check to see that we have capacity to add another vector
- if (nextIndex >= currentIndexes.size) {
- currentIndexes = currentIndexes.copyOf(currentIndexes.size * 2)
- vectors = vectors.copyOf(vectors.size * 2)
- }
- currentIndexes[nextIndex] = vector.size - 1
- vectors[nextIndex] = vector
- size++
- }
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
index b522a01..9a1ba69 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
@@ -1407,7 +1407,7 @@
@Suppress("PrimitiveInCollection")
private fun compareEquals(
a: MutableObjectIntMap<AlignmentLine>?,
- b: Map<AlignmentLine, Int>
+ b: Map<out AlignmentLine, Int>
): Boolean {
if (a == null) return false
if (a.size != b.size) return false
diff --git a/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java b/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java
index ed0ed4d..007491b 100644
--- a/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java
@@ -37,6 +37,7 @@
import androidx.test.filters.MediumTest;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -365,6 +366,7 @@
*/
@MediumTest
@Test
+ @Ignore("JobIntentService is deprecated and no longer maintained")
public void testEnqueueOne() throws Throwable {
initStatics();
@@ -386,6 +388,7 @@
*/
@MediumTest
@Test
+ @Ignore("JobIntentService is deprecated and no longer maintained")
public void testEnqueueMultiple() throws Throwable {
initStatics();
@@ -410,6 +413,7 @@
*/
@MediumTest
@Test
+ @Ignore("JobIntentService is deprecated and no longer maintained")
public void testEnqueueSubWork() throws Throwable {
initStatics();
@@ -439,6 +443,7 @@
*/
@MediumTest
@Test
+ @Ignore("JobIntentService is deprecated and no longer maintained")
@RequiresApi(26)
public void testStopWhileWorking() throws Throwable {
if (Build.VERSION.SDK_INT < 26) {
diff --git a/development/update_studio.sh b/development/update_studio.sh
index cdd888e..3fa7ebb 100755
--- a/development/update_studio.sh
+++ b/development/update_studio.sh
@@ -48,7 +48,7 @@
sed -i "s/androidStudio = .*/androidStudio = \"$STUDIO_VERSION\"/g" gradle/libs.versions.toml
# update settings.gradle
-sed -i "s/com.android.settings:com.android.settings.gradle.plugin:.*/com.android.settings:com.android.settings.gradle.plugin:$AGP_VERSION\")/g" settings.gradle
+sed -i "s/com.android.settings:com.android.settings.gradle.plugin:[0-9a-z\.\-]*/com.android.settings:com.android.settings.gradle.plugin:$AGP_VERSION\")/g" settings.gradle
# Pull all UTP artifacts for ADT version
ADT_VERSION=${3:-$LINT_VERSION}
diff --git a/ink/ink-brush/api/current.txt b/ink/ink-brush/api/current.txt
index 7582fcf..7599e2b 100644
--- a/ink/ink-brush/api/current.txt
+++ b/ink/ink-brush/api/current.txt
@@ -57,6 +57,16 @@
public static final class BrushFamily.Companion {
}
+ public final class BrushUtil {
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush copyWithAndroidColor(androidx.ink.brush.Brush, android.graphics.Color color, optional androidx.ink.brush.BrushFamily family, optional float size, optional float epsilon);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static android.graphics.Color createAndroidColor(androidx.ink.brush.Brush);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder createBuilderWithAndroidColor(android.graphics.Color color);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush createWithAndroidColor(androidx.ink.brush.Brush.Companion, androidx.ink.brush.BrushFamily family, android.graphics.Color color, float size, float epsilon);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush createWithAndroidColor(androidx.ink.brush.BrushFamily family, android.graphics.Color color, float size, float epsilon);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder setAndroidColor(androidx.ink.brush.Brush.Builder, android.graphics.Color color);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder toBuilderWithAndroidColor(androidx.ink.brush.Brush, android.graphics.Color color);
+ }
+
@SuppressCompatibility @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.TYPEALIAS}) public @interface ExperimentalInkCustomBrushApi {
}
diff --git a/ink/ink-brush/api/restricted_current.txt b/ink/ink-brush/api/restricted_current.txt
index 7582fcf..7599e2b 100644
--- a/ink/ink-brush/api/restricted_current.txt
+++ b/ink/ink-brush/api/restricted_current.txt
@@ -57,6 +57,16 @@
public static final class BrushFamily.Companion {
}
+ public final class BrushUtil {
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush copyWithAndroidColor(androidx.ink.brush.Brush, android.graphics.Color color, optional androidx.ink.brush.BrushFamily family, optional float size, optional float epsilon);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static android.graphics.Color createAndroidColor(androidx.ink.brush.Brush);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder createBuilderWithAndroidColor(android.graphics.Color color);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush createWithAndroidColor(androidx.ink.brush.Brush.Companion, androidx.ink.brush.BrushFamily family, android.graphics.Color color, float size, float epsilon);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush createWithAndroidColor(androidx.ink.brush.BrushFamily family, android.graphics.Color color, float size, float epsilon);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder setAndroidColor(androidx.ink.brush.Brush.Builder, android.graphics.Color color);
+ method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder toBuilderWithAndroidColor(androidx.ink.brush.Brush, android.graphics.Color color);
+ }
+
@SuppressCompatibility @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.TYPEALIAS}) public @interface ExperimentalInkCustomBrushApi {
}
diff --git a/ink/ink-brush/build.gradle b/ink/ink-brush/build.gradle
index 2483290..353b80f 100644
--- a/ink/ink-brush/build.gradle
+++ b/ink/ink-brush/build.gradle
@@ -50,9 +50,9 @@
jvmAndroidMain {
dependsOn(commonMain)
dependencies {
- implementation(libs.kotlinStdlib)
api(libs.androidx.annotation)
- implementation(project(":collection:collection"))
+ implementation(libs.kotlinStdlib)
+ implementation("androidx.collection:collection:1.4.3")
implementation(project(":ink:ink-geometry"))
implementation(project(":ink:ink-nativeloader"))
}
@@ -100,5 +100,4 @@
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2024"
description = "Define brushes for freehand input."
- metalavaK2UastEnabled = false
}
diff --git a/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt b/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt
index b23e689..1a277fe 100644
--- a/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt
+++ b/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt
@@ -41,7 +41,7 @@
private val testFamily = BrushFamily(uri = "/brush-family:pencil")
@Test
- fun brushGetAndroidColor_getsCorrectColor() {
+ fun brushCreateAndroidColor_getsCorrectColor() {
val brush = Brush.createWithColorLong(testFamily, testColorLong, 1f, 1f)
// Note that expectedColor is not necessarily the same as testColor, because of precision
@@ -50,7 +50,7 @@
// the
// color internally as a ColorLong anyway).
val expectedColor = AndroidColor.valueOf(testColorLong)
- assertThat(brush.getAndroidColor()).isEqualTo(expectedColor)
+ assertThat(brush.createAndroidColor()).isEqualTo(expectedColor)
}
@Test
@@ -97,7 +97,7 @@
}
@Test
- fun brushBuilderAndroidColor_setsColor() {
+ fun brushBuilderSetAndroidColor_setsColor() {
val brush =
Brush.builder()
.setFamily(testFamily)
@@ -110,7 +110,7 @@
}
@Test
- fun brushBuilderAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
+ fun brushBuilderSetAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
val brush =
Brush.builder()
@@ -126,13 +126,13 @@
}
@Test
- fun brushWithAndroidColor_createsBrushWithColor() {
+ fun brushCreateWithAndroidColor_createsBrushWithColor() {
val brush = Brush.createWithAndroidColor(testFamily, testColor, 1f, 1f)
assertThat(brush.colorLong).isEqualTo(testColorLong)
}
@Test
- fun brushWithAndroidColor_withUnsupportedColorSpace_createsBrushWithConvertedColor() {
+ fun brushCreateWithAndroidColor_withUnsupportedColorSpace_createsBrushWithConvertedColor() {
val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
val brush = Brush.createWithAndroidColor(testFamily, unsupportedColor, 1f, 1f)
@@ -142,21 +142,10 @@
}
@Test
- fun brushUtilGetAndroidColor_getsCorrectColor() {
- val brush = Brush.createWithColorLong(testFamily, testColorLong, 1f, 1f)
-
- // Note that expectedColor is not necessarily the same as testColor, because of precision
- // loss
- // when converting from testColor to testColorLong.
- val expectedColor = AndroidColor.valueOf(testColorLong)
- assertThat(BrushUtil.getAndroidColor(brush)).isEqualTo(expectedColor)
- }
-
- @Test
- fun brushUtilToBuilderWithAndroidColor_setsColor() {
+ fun brushToBuilderWithAndroidColor_setsColor() {
val brush = Brush.createWithColorIntArgb(testFamily, 0x4499bb66, 2f, 0.2f)
- val newBrush = BrushUtil.toBuilderWithAndroidColor(brush, testColor).build()
+ val newBrush = brush.toBuilderWithAndroidColor(testColor).build()
assertThat(newBrush.colorLong).isEqualTo(testColorLong)
assertThat(brush.family).isEqualTo(testFamily)
@@ -165,11 +154,11 @@
}
@Test
- fun brushUtilToBuilderWithAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
+ fun brushToBuilderWithAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
val brush = Brush.createWithColorIntArgb(testFamily, 0x4499bb66, 2f, 0.2f)
val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
- val newBrush = BrushUtil.toBuilderWithAndroidColor(brush, unsupportedColor).build()
+ val newBrush = brush.toBuilderWithAndroidColor(unsupportedColor).build()
// unsupportedColor gets converted to ColorLong (losing precision) and then to Display P3.
val expectedColor = AndroidColor.valueOf(unsupportedColor.pack()).convert(displayP3)
@@ -181,9 +170,9 @@
}
@Test
- fun brushUtilMakeBuilderWithAndroidColor_setsColor() {
+ fun createBrushBuilderWithAndroidColor_setsColor() {
val brush =
- BrushUtil.createBuilderWithAndroidColor(testColor)
+ createBrushBuilderWithAndroidColor(testColor)
.setFamily(testFamily)
.setSize(2f)
.setEpsilon(0.2f)
@@ -196,10 +185,10 @@
}
@Test
- fun brushUtilMakeBuilderAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
+ fun createBrushBuilderWithAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
val brush =
- BrushUtil.createBuilderWithAndroidColor(unsupportedColor)
+ createBrushBuilderWithAndroidColor(unsupportedColor)
.setFamily(testFamily)
.setSize(2f)
.setEpsilon(0.2f)
@@ -209,20 +198,4 @@
val expectedColor = AndroidColor.valueOf(unsupportedColor.pack()).convert(displayP3)
assertThat(brush.colorLong).isEqualTo(expectedColor.pack())
}
-
- @Test
- fun brushUtilMakeBrushWithAndroidColor_createsBrushWithColor() {
- val brush = BrushUtil.createWithAndroidColor(testFamily, testColor, 1f, 1f)
- assertThat(brush.colorLong).isEqualTo(testColorLong)
- }
-
- @Test
- fun brushUtilMakeBrushWithAndroidColor_withUnsupportedColorSpace_createsBrushWithConvertedColor() {
- val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
- val brush = BrushUtil.createWithAndroidColor(testFamily, unsupportedColor, 1f, 1f)
-
- // unsupportedColor gets converted to ColorLong (losing precision) and then to Display P3.
- val expectedColor = AndroidColor.valueOf(unsupportedColor.pack()).convert(displayP3)
- assertThat(brush.colorLong).isEqualTo(expectedColor.pack())
- }
}
diff --git a/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt b/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt
index a1a5bdc..ff89d66 100644
--- a/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt
+++ b/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+@file:JvmName("BrushUtil")
package androidx.ink.brush
@@ -22,17 +22,20 @@
import android.os.Build
import androidx.annotation.CheckResult
import androidx.annotation.RequiresApi
-import androidx.annotation.RestrictTo
/**
* The brush color as an [android.graphics.Color] instance, which can express colors in several
* different color spaces. sRGB and Display P3 are supported; a color in any other color space will
* be converted to Display P3.
+ *
+ * Unless an instance of [android.graphics.Color] is actually needed, prefer to use
+ * [Brush.colorLong] to get the color without causing an allocation, especially in
+ * performance-sensitive code. [Brush.colorLong] is fully compatible with the [Long] representation
+ * of [android.graphics.Color].
*/
-@JvmSynthetic
@CheckResult
@RequiresApi(Build.VERSION_CODES.O)
-public fun Brush.getAndroidColor(): AndroidColor = BrushUtil.getAndroidColor(this)
+public fun Brush.createAndroidColor(): AndroidColor = AndroidColor.valueOf(colorLong)
/**
* Creates a copy of `this` [Brush] and allows named properties to be altered while keeping the rest
@@ -40,7 +43,6 @@
* several different color spaces. sRGB and Display P3 are supported; a color in any other color
* space will be converted to Display P3.
*/
-@JvmSynthetic
@CheckResult
@RequiresApi(Build.VERSION_CODES.O)
public fun Brush.copyWithAndroidColor(
@@ -53,19 +55,53 @@
/**
* Set the color on a [Brush.Builder] as an [android.graphics.Color] instance. sRGB and Display P3
* are supported; a color in any other color space will be converted to Display P3.
+ *
+ * Java callers should prefer [toBuilderWithAndroidColor] or [createBrushBuilderWithAndroidColor] as
+ * a more fluent API.
*/
-@JvmSynthetic
@CheckResult
@RequiresApi(Build.VERSION_CODES.O)
public fun Brush.Builder.setAndroidColor(color: AndroidColor): Brush.Builder =
setColorLong(color.pack())
/**
+ * Returns a [Brush.Builder] with values set equivalent to the [Brush] and the color specified by an
+ * [android.graphics.Color] instance, which can encode several different color spaces. sRGB and
+ * Display P3 are supported; a color in any other color space will be converted to Display P3. Java
+ * developers, use the returned builder to build a copy of a Brush. Kotlin developers, see
+ * [copyWithAndroidColor] method.
+ *
+ * In Kotlin, calling this is equivalent to calling [Brush.toBuilder] followed by
+ * [Brush.Builder.setAndroidColor]. For Java callers, this function allows more fluent call
+ * chaining.
+ */
+@CheckResult
+@RequiresApi(Build.VERSION_CODES.O)
+public fun Brush.toBuilderWithAndroidColor(color: AndroidColor): Brush.Builder =
+ toBuilder().setAndroidColor(color)
+
+/**
+ * Returns a new, blank [Brush.Builder] with the color specified by an [android.graphics.Color]
+ * instance, which can encode several different color spaces. sRGB and Display P3 are supported; a
+ * color in any other color space will be converted to Display P3.
+ *
+ * In Kotlin, calling this is equivalent to calling [Brush.builder] followed by
+ * [Brush.Builder.setAndroidColor]. For Java callers, this function allows more fluent call
+ * chaining.
+ */
+@JvmName("createBuilderWithAndroidColor")
+@CheckResult
+@RequiresApi(Build.VERSION_CODES.O)
+public fun createBrushBuilderWithAndroidColor(color: AndroidColor): Brush.Builder =
+ Brush.Builder().setAndroidColor(color)
+
+/**
* Returns a new [Brush] with the color specified by an [android.graphics.Color] instance, which can
* encode several different color spaces. sRGB and Display P3 are supported; a color in any other
* color space will be converted to Display P3.
+ *
+ * Java callers should prefer `BrushUtil.createWithAndroidColor` ([createBrushWithAndroidColor]).
*/
-@JvmSynthetic
@CheckResult
@RequiresApi(Build.VERSION_CODES.O)
public fun Brush.Companion.createWithAndroidColor(
@@ -73,57 +109,21 @@
color: AndroidColor,
size: Float,
epsilon: Float,
-): Brush = BrushUtil.createWithAndroidColor(family, color, size, epsilon)
+): Brush = createWithColorLong(family, color.pack(), size, epsilon)
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
-public object BrushUtil {
-
- /**
- * The brush color as an [android.graphics.Color] instance, which can express colors in several
- * different color spaces. sRGB and Display P3 are supported; a color in any other color space
- * will be converted to Display P3.
- */
- @JvmStatic
- @CheckResult
- @RequiresApi(Build.VERSION_CODES.O)
- public fun getAndroidColor(brush: Brush): AndroidColor = AndroidColor.valueOf(brush.colorLong)
-
- /**
- * Returns a [Brush.Builder] with values set equivalent to [brush] and the color specified by an
- * [android.graphics.Color] instance, which can encode several different color spaces. sRGB and
- * Display P3 are supported; a color in any other color space will be converted to Display P3.
- * Java developers, use the returned builder to build a copy of a Brush. Kotlin developers, see
- * [copyWithAndroidColor] method.
- */
- @JvmStatic
- @CheckResult
- @RequiresApi(Build.VERSION_CODES.O)
- public fun toBuilderWithAndroidColor(brush: Brush, color: AndroidColor): Brush.Builder =
- brush.toBuilder().setAndroidColor(color)
-
- /**
- * Returns a new [Brush.Builder] with the color specified by an [android.graphics.Color]
- * instance, which can encode several different color spaces. sRGB and Display P3 are supported;
- * a color in any other color space will be converted to Display P3.
- */
- @JvmStatic
- @CheckResult
- @RequiresApi(Build.VERSION_CODES.O)
- public fun createBuilderWithAndroidColor(color: AndroidColor): Brush.Builder =
- Brush.Builder().setAndroidColor(color)
-
- /**
- * Returns a new [Brush] with the color specified by an [android.graphics.Color] instance, which
- * can encode several different color spaces. sRGB and Display P3 are supported; a color in any
- * other color space will be converted to Display P3.
- */
- @JvmStatic
- @CheckResult
- @RequiresApi(Build.VERSION_CODES.O)
- public fun createWithAndroidColor(
- family: BrushFamily,
- color: AndroidColor,
- size: Float,
- epsilon: Float,
- ): Brush = Brush.createWithColorLong(family, color.pack(), size, epsilon)
-}
+/**
+ * Returns a new [Brush] with the color specified by an [android.graphics.Color] instance, which can
+ * encode several different color spaces. sRGB and Display P3 are supported; a color in any other
+ * color space will be converted to Display P3.
+ *
+ * Kotlin callers should prefer [Brush.Companion.createWithAndroidColor].
+ */
+@JvmName("createWithAndroidColor")
+@CheckResult
+@RequiresApi(Build.VERSION_CODES.O)
+public fun createBrushWithAndroidColor(
+ family: BrushFamily,
+ color: AndroidColor,
+ size: Float,
+ epsilon: Float,
+): Brush = Brush.createWithAndroidColor(family, color, size, epsilon)
diff --git a/ink/ink-geometry/api/current.txt b/ink/ink-geometry/api/current.txt
index 7d352f46..8901f35 100644
--- a/ink/ink-geometry/api/current.txt
+++ b/ink/ink-geometry/api/current.txt
@@ -65,6 +65,7 @@
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Box? box);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.BoxAccumulator? other);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Parallelogram parallelogram);
+ method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.PartitionedMesh mesh);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Segment segment);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Triangle triangle);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Vec point);
@@ -172,26 +173,37 @@
public final class Intersection {
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Box other);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToBox);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Vec point);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Parallelogram other);
+ method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToParallelogram);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Vec point);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Box box, androidx.ink.geometry.AffineTransform meshToBox);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Parallelogram parallelogram, androidx.ink.geometry.AffineTransform meshToParallelogram);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.PartitionedMesh other, androidx.ink.geometry.AffineTransform thisToCommonTransForm, androidx.ink.geometry.AffineTransform otherToCommonTransform);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Segment segment, androidx.ink.geometry.AffineTransform meshToSegment);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Triangle triangle, androidx.ink.geometry.AffineTransform meshToTriangle);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Vec point, androidx.ink.geometry.AffineTransform meshToPoint);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToSegment);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Segment other);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Vec point);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToTriangle);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Triangle other);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Vec point);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToPoint);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Vec other);
@@ -313,6 +325,36 @@
public static final class Parallelogram.Companion {
}
+ public final class PartitionedMesh {
+ method public androidx.ink.geometry.Box? computeBoundingBox();
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Box box);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Box box, optional androidx.ink.geometry.AffineTransform boxToThis);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Parallelogram parallelogram);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Parallelogram parallelogram, optional androidx.ink.geometry.AffineTransform parallelogramToThis);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.PartitionedMesh other);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.PartitionedMesh other, optional androidx.ink.geometry.AffineTransform otherShapeToThis);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Triangle triangle);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Triangle triangle, optional androidx.ink.geometry.AffineTransform triangleToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Box box, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Box box, float coverageThreshold, optional androidx.ink.geometry.AffineTransform boxToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Parallelogram parallelogram, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Parallelogram parallelogram, float coverageThreshold, optional androidx.ink.geometry.AffineTransform parallelogramToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.PartitionedMesh other, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.PartitionedMesh other, float coverageThreshold, optional androidx.ink.geometry.AffineTransform otherShapeToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Triangle triangle, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Triangle triangle, float coverageThreshold, optional androidx.ink.geometry.AffineTransform triangleToThis);
+ method protected void finalize();
+ method @IntRange(from=0L) public int getOutlineCount(@IntRange(from=0L) int groupIndex);
+ method @IntRange(from=0L) public int getOutlineVertexCount(@IntRange(from=0L) int groupIndex, @IntRange(from=0L) int outlineIndex);
+ method @IntRange(from=0L) public int getRenderGroupCount();
+ method public void initializeSpatialIndex();
+ method public androidx.ink.geometry.MutableVec populateOutlinePosition(@IntRange(from=0L) int groupIndex, @IntRange(from=0L) int outlineIndex, @IntRange(from=0L) int outlineVertexIndex, androidx.ink.geometry.MutableVec outPosition);
+ field public static final androidx.ink.geometry.PartitionedMesh.Companion Companion;
+ }
+
+ public static final class PartitionedMesh.Companion {
+ }
+
public abstract class Segment {
method public final androidx.ink.geometry.ImmutableBox computeBoundingBox();
method public final androidx.ink.geometry.MutableBox computeBoundingBox(androidx.ink.geometry.MutableBox outBox);
diff --git a/ink/ink-geometry/api/restricted_current.txt b/ink/ink-geometry/api/restricted_current.txt
index 7d352f46..8901f35 100644
--- a/ink/ink-geometry/api/restricted_current.txt
+++ b/ink/ink-geometry/api/restricted_current.txt
@@ -65,6 +65,7 @@
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Box? box);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.BoxAccumulator? other);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Parallelogram parallelogram);
+ method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.PartitionedMesh mesh);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Segment segment);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Triangle triangle);
method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Vec point);
@@ -172,26 +173,37 @@
public final class Intersection {
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Box other);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToBox);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Vec point);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Parallelogram other);
+ method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToParallelogram);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Vec point);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Box box, androidx.ink.geometry.AffineTransform meshToBox);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Parallelogram parallelogram, androidx.ink.geometry.AffineTransform meshToParallelogram);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.PartitionedMesh other, androidx.ink.geometry.AffineTransform thisToCommonTransForm, androidx.ink.geometry.AffineTransform otherToCommonTransform);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Segment segment, androidx.ink.geometry.AffineTransform meshToSegment);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Triangle triangle, androidx.ink.geometry.AffineTransform meshToTriangle);
+ method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Vec point, androidx.ink.geometry.AffineTransform meshToPoint);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToSegment);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Segment other);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Vec point);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToTriangle);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Triangle other);
method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Vec point);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Box box);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Parallelogram parallelogram);
+ method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToPoint);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Segment segment);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Triangle triangle);
method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Vec other);
@@ -313,6 +325,36 @@
public static final class Parallelogram.Companion {
}
+ public final class PartitionedMesh {
+ method public androidx.ink.geometry.Box? computeBoundingBox();
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Box box);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Box box, optional androidx.ink.geometry.AffineTransform boxToThis);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Parallelogram parallelogram);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Parallelogram parallelogram, optional androidx.ink.geometry.AffineTransform parallelogramToThis);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.PartitionedMesh other);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.PartitionedMesh other, optional androidx.ink.geometry.AffineTransform otherShapeToThis);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Triangle triangle);
+ method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Triangle triangle, optional androidx.ink.geometry.AffineTransform triangleToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Box box, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Box box, float coverageThreshold, optional androidx.ink.geometry.AffineTransform boxToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Parallelogram parallelogram, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Parallelogram parallelogram, float coverageThreshold, optional androidx.ink.geometry.AffineTransform parallelogramToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.PartitionedMesh other, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.PartitionedMesh other, float coverageThreshold, optional androidx.ink.geometry.AffineTransform otherShapeToThis);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Triangle triangle, float coverageThreshold);
+ method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Triangle triangle, float coverageThreshold, optional androidx.ink.geometry.AffineTransform triangleToThis);
+ method protected void finalize();
+ method @IntRange(from=0L) public int getOutlineCount(@IntRange(from=0L) int groupIndex);
+ method @IntRange(from=0L) public int getOutlineVertexCount(@IntRange(from=0L) int groupIndex, @IntRange(from=0L) int outlineIndex);
+ method @IntRange(from=0L) public int getRenderGroupCount();
+ method public void initializeSpatialIndex();
+ method public androidx.ink.geometry.MutableVec populateOutlinePosition(@IntRange(from=0L) int groupIndex, @IntRange(from=0L) int outlineIndex, @IntRange(from=0L) int outlineVertexIndex, androidx.ink.geometry.MutableVec outPosition);
+ field public static final androidx.ink.geometry.PartitionedMesh.Companion Companion;
+ }
+
+ public static final class PartitionedMesh.Companion {
+ }
+
public abstract class Segment {
method public final androidx.ink.geometry.ImmutableBox computeBoundingBox();
method public final androidx.ink.geometry.MutableBox computeBoundingBox(androidx.ink.geometry.MutableBox outBox);
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt
index 79501d8..dde4d23 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt
@@ -227,8 +227,7 @@
*
* @return `this`
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
- public fun add(mesh: PartitionedMesh): BoxAccumulator = this.add(mesh.bounds)
+ public fun add(mesh: PartitionedMesh): BoxAccumulator = this.add(mesh.computeBoundingBox())
/**
* Compares this [BoxAccumulator] with [other], and returns true if either: Both this and
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt
index 1ee275d..fcdf2f5 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt
@@ -16,7 +16,6 @@
package androidx.ink.geometry
-import androidx.annotation.RestrictTo
import androidx.ink.nativeloader.NativeLoader
/**
@@ -116,7 +115,6 @@
* intersection of the point in [mesh]’s object coordinates.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun Vec.intersects(mesh: PartitionedMesh, meshToPoint: AffineTransform): Boolean {
return nativeMeshVecIntersects(
nativeMeshAddress = mesh.getNativeAddress(),
@@ -218,7 +216,6 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun Segment.intersects(mesh: PartitionedMesh, meshToSegment: AffineTransform): Boolean {
return nativeMeshSegmentIntersects(
nativeMeshAddress = mesh.getNativeAddress(),
@@ -311,7 +308,6 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun Triangle.intersects(
mesh: PartitionedMesh,
meshToTriangle: AffineTransform
@@ -382,7 +378,6 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun Box.intersects(mesh: PartitionedMesh, meshToBox: AffineTransform): Boolean {
return nativeMeshBoxIntersects(
nativeMeshAddress = mesh.getNativeAddress(),
@@ -436,7 +431,6 @@
* checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun Parallelogram.intersects(
mesh: PartitionedMesh,
meshToParallelogram: AffineTransform,
@@ -467,7 +461,6 @@
* coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun PartitionedMesh.intersects(
other: PartitionedMesh,
thisToCommonTransForm: AffineTransform,
@@ -566,7 +559,6 @@
* intersection of the point in [mesh]’s object coordinates.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun PartitionedMesh.intersects(point: Vec, meshToPoint: AffineTransform): Boolean =
point.intersects(this, meshToPoint)
@@ -578,7 +570,6 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun PartitionedMesh.intersects(
segment: Segment,
meshToSegment: AffineTransform
@@ -592,7 +583,6 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun PartitionedMesh.intersects(
triangle: Triangle,
meshToTriangle: AffineTransform,
@@ -606,7 +596,6 @@
* coordinate space to the coordinate space that the intersection should be checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun PartitionedMesh.intersects(box: Box, meshToBox: AffineTransform): Boolean =
box.intersects(this, meshToBox)
@@ -619,7 +608,6 @@
* checked in.
*/
@JvmStatic
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
public fun PartitionedMesh.intersects(
parallelogram: Parallelogram,
meshToParallelogram: AffineTransform,
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt
index d279937..6f186f6 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt
@@ -25,23 +25,22 @@
import androidx.ink.nativeloader.NativeLoader
/**
- * An immutable† complex shape expressed as a set of triangles. This is used to represent the shape
- * of a stroke or other complex objects see [MeshCreation]. The mesh may be divided into multiple
- * partitions, which enables certain brush effects (e.g. "multi-coat"), and allows ink to create
- * strokes requiring greater than 216 triangles (which must be rendered in multiple passes).
+ * An immutable** complex shape expressed as a set of triangles. This is used to represent the shape
+ * of a stroke or other complex objects. The mesh may be divided into multiple partitions, which
+ * enables certain brush effects (e.g. "multi-coat"), and allows strokes to be created using greater
+ * than 2^16 triangles (which must be rendered in multiple passes).
*
- * A PartitionedMesh may optionally have one or more "outlines", which are polylines that traverse
+ * A [PartitionedMesh] may optionally have one or more "outlines", which are polylines that traverse
* some or all of the vertices in the mesh; these are used for path-based rendering of strokes. This
* supports disjoint meshes such as dashed lines.
*
- * PartitionedMesh provides fast intersection and coverage testing by use of an internal spatial
+ * [PartitionedMesh] provides fast intersection and coverage testing by use of an internal spatial
* index.
*
- * † PartitionedMesh is technically not immutable, as the spatial index is lazily instantiated;
+ * ** [PartitionedMesh] is technically not immutable, as the spatial index is lazily instantiated;
* however, from the perspective of a caller, its properties do not change over the course of its
* lifetime. The entire object is thread-safe.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
@Suppress("NotCloseable") // Finalize is only used to free the native peer.
public class PartitionedMesh
/** Only for use within the ink library. Constructs a [PartitionedMesh] from native pointer. */
@@ -73,42 +72,53 @@
@VisibleForTesting internal constructor() : this(ModeledShapeNative.alloc())
/**
- * The number of render groups in this mesh. Each outline in the [PartitionedMesh] belongs to
- * exactly one render group, which are numbered in z-order: the group with index zero should be
- * rendered on bottom; the group with the highest index should be rendered on top.
+ * Returns the number of render groups in this mesh. Each outline in the [PartitionedMesh]
+ * belongs to exactly one render group, which are numbered in z-order: the group with index zero
+ * should be rendered on bottom; the group with the highest index should be rendered on top.
*/
@IntRange(from = 0)
- public val renderGroupCount: Int =
+ public fun getRenderGroupCount(): Int =
ModeledShapeNative.getRenderGroupCount(nativeAddress).also { check(it >= 0) }
/** The [Mesh] objects that make up this shape. */
private val meshesByGroup: List<List<Mesh>> = buildList {
- for (groupIndex in 0 until renderGroupCount) {
+ for (groupIndex in 0 until getRenderGroupCount()) {
val nativeAddressesOfMeshes =
ModeledShapeNative.getNativeAddressesOfMeshes(nativeAddress, groupIndex)
add(nativeAddressesOfMeshes.map(::Mesh))
}
}
+ private var _bounds: Box? = null
+
/**
- * The minimum bounding box of the [PartitionedMesh]. This will be null if the [PartitionedMesh]
- * is empty.
+ * Returns the minimum bounding box of the [PartitionedMesh]. This will be null if the
+ * [PartitionedMesh] is empty.
*/
- public val bounds: Box? = run {
+ public fun computeBoundingBox(): Box? {
+ // If we've already computed the bounding box, re-use it -- it won't change over the
+ // lifetime of
+ // this object.
+ if (_bounds != null) return _bounds
+
+ // If we have no meshes, then the bounding box is null.
+ if (meshesByGroup.isEmpty()) return null
+
val envelope = BoxAccumulator()
for (meshes in meshesByGroup) {
for (mesh in meshes) {
envelope.add(mesh.bounds)
}
}
- envelope.box
+ _bounds = envelope.box
+ return envelope.box
}
/** Returns the [MeshFormat] used for each [Mesh] in the specified render group. */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
public fun renderGroupFormat(@IntRange(from = 0) groupIndex: Int): MeshFormat {
- require(groupIndex >= 0 && groupIndex < renderGroupCount) {
- "groupIndex=$groupIndex must be between 0 and renderGroupCount=${renderGroupCount}"
+ require(groupIndex >= 0 && groupIndex < getRenderGroupCount()) {
+ "groupIndex=$groupIndex must be between 0 and renderGroupCount=${getRenderGroupCount()}"
}
return MeshFormat(ModeledShapeNative.getRenderGroupFormat(nativeAddress, groupIndex))
}
@@ -119,51 +129,51 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
public fun renderGroupMeshes(@IntRange(from = 0) groupIndex: Int): List<Mesh> {
- require(groupIndex >= 0 && groupIndex < renderGroupCount) {
- "groupIndex=$groupIndex must be between 0 and renderGroupCount=${renderGroupCount}"
+ require(groupIndex >= 0 && groupIndex < getRenderGroupCount()) {
+ "groupIndex=$groupIndex must be between 0 and renderGroupCount=${getRenderGroupCount()}"
}
return meshesByGroup[groupIndex]
}
- /** The number of outlines that comprise this shape. */
+ /** Returns the number of outlines that comprise this shape. */
@IntRange(from = 0)
- public fun outlineCount(@IntRange(from = 0) groupIndex: Int): Int {
- require(groupIndex >= 0 && groupIndex < renderGroupCount) {
- "groupIndex=$groupIndex must be between 0 and renderGroupCount=${renderGroupCount}"
+ public fun getOutlineCount(@IntRange(from = 0) groupIndex: Int): Int {
+ require(groupIndex >= 0 && groupIndex < getRenderGroupCount()) {
+ "groupIndex=$groupIndex must be between 0 and renderGroupCount=${getRenderGroupCount()}"
}
return ModeledShapeNative.getOutlineCount(nativeAddress, groupIndex).also { check(it >= 0) }
}
/**
- * The number of vertices that are in the outline at index [outlineIndex], and within the render
- * group at [groupIndex].
+ * Returns the number of vertices that are in the outline at index [outlineIndex], and within
+ * the render group at [groupIndex].
*/
@IntRange(from = 0)
- public fun outlineVertexCount(
+ public fun getOutlineVertexCount(
@IntRange(from = 0) groupIndex: Int,
@IntRange(from = 0) outlineIndex: Int,
): Int {
- require(outlineIndex >= 0 && outlineIndex < outlineCount(groupIndex)) {
- "outlineIndex=$outlineIndex must be between 0 and outlineCount=${outlineCount(groupIndex)}"
+ require(outlineIndex >= 0 && outlineIndex < getOutlineCount(groupIndex)) {
+ "outlineIndex=$outlineIndex must be between 0 and outlineCount=${getOutlineCount(groupIndex)}"
}
return ModeledShapeNative.getOutlineVertexCount(nativeAddress, groupIndex, outlineIndex)
.also { check(it >= 0) }
}
/**
- * Retrieve the outline vertex position from the outline at index [outlineIndex] (which can be
- * up to, but not including, [outlineCount]), and the vertex from within that outline at index
- * [outlineVertexIndex] (which can be up to, but not including, the result of calling
- * [outlineVertexCount] with [outlineIndex]). The resulting x/y position of that outline vertex
- * will be put into [outPosition], which can be pre-allocated and reused to avoid allocations.
+ * Populates [outPosition] with the position of the outline vertex at [outlineVertexIndex] in
+ * the outline at [outlineIndex] in the render group at [groupIndex], and returns [outPosition].
+ * [groupIndex] must be less than [getRenderGroupCount], [outlineIndex] must be less
+ * [getOutlineVertexCount] for [groupIndex], and [outlineVertexIndex] must be less than
+ * [getOutlineVertexCount] for [groupIndex] and [outlineIndex].
*/
public fun populateOutlinePosition(
@IntRange(from = 0) groupIndex: Int,
@IntRange(from = 0) outlineIndex: Int,
@IntRange(from = 0) outlineVertexIndex: Int,
outPosition: MutableVec,
- ) {
- val outlineVertexCount = outlineVertexCount(groupIndex, outlineIndex)
+ ): MutableVec {
+ val outlineVertexCount = getOutlineVertexCount(groupIndex, outlineIndex)
require(outlineVertexIndex >= 0 && outlineVertexIndex < outlineVertexCount) {
"outlineVertexIndex=$outlineVertexIndex must be between 0 and " +
"outlineVertexCount($outlineVertexIndex)=$outlineVertexCount"
@@ -178,6 +188,7 @@
val (meshIndex, meshVertexIndex) = scratchIntArray
val mesh = meshesByGroup[groupIndex][meshIndex]
mesh.fillPosition(meshVertexIndex, outPosition)
+ return outPosition
}
/**
@@ -187,18 +198,18 @@
* triangles in the [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space.
* Triangles in the [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that
* loops back over itself) are counted individually. Note that, if any triangles have negative
- * area (due to winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute
- * value of their area will be used instead.
+ * area (due to winding, see [Triangle.computeSignedArea]), the absolute value of their area
+ * will be used instead.
*
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [triangleToThis] contains the transform that maps from [triangle]'s
- * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
- * [IDENTITY].
+ * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to
+ * [AffineTransform.IDENTITY].
*/
@JvmOverloads
@FloatRange(from = 0.0, to = 1.0)
- public fun coverage(
+ public fun computeCoverage(
triangle: Triangle,
triangleToThis: AffineTransform = AffineTransform.IDENTITY,
): Float =
@@ -225,17 +236,20 @@
* [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space. Triangles in the
* [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that loops back over
* itself) are counted individually. Note that, if any triangles have negative area (due to
- * winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute value of their
- * area will be used instead.
+ * winding, see [Triangle.computeSignedArea]), the absolute value of their area will be used
+ * instead.
*
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [boxToThis] contains the transform that maps from [box]'s coordinate space
- * to this [PartitionedMesh]'s coordinate space, which defaults to the [IDENTITY].
+ * to this [PartitionedMesh]'s coordinate space, which defaults to [AffineTransform.IDENTITY].
*/
@JvmOverloads
@FloatRange(from = 0.0, to = 1.0)
- public fun coverage(box: Box, boxToThis: AffineTransform = AffineTransform.IDENTITY): Float =
+ public fun computeCoverage(
+ box: Box,
+ boxToThis: AffineTransform = AffineTransform.IDENTITY
+ ): Float =
ModeledShapeNative.modeledShapeBoxCoverage(
nativeAddress = nativeAddress,
boxXMin = box.xMin,
@@ -257,18 +271,18 @@
* of all triangles in the [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space.
* Triangles in the [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that
* loops back over itself) are counted individually. Note that, if any triangles have negative
- * area (due to winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute
- * value of their area will be used instead.
+ * area (due to winding, see [Triangle.computeSignedArea]), the absolute value of their area
+ * will be used instead.
*
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [parallelogramToThis] contains the transform that maps from
* [parallelogram]'s coordinate space to this [PartitionedMesh]'s coordinate space, which
- * defaults to the [IDENTITY].
+ * defaults to [AffineTransform.IDENTITY].
*/
@JvmOverloads
@FloatRange(from = 0.0, to = 1.0)
- public fun coverage(
+ public fun computeCoverage(
parallelogram: Parallelogram,
parallelogramToThis: AffineTransform = AffineTransform.IDENTITY,
): Float =
@@ -295,18 +309,18 @@
* triangles in the [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space.
* Triangles in the [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that
* loops back over itself) are counted individually. Note that, if any triangles have negative
- * area (due to winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute
- * value of their area will be used instead.t
+ * area (due to winding, see [Triangle.computeSignedArea]), the absolute value of their area
+ * will be used instead.
*
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [otherShapeToThis] contains the transform that maps from [other]'s
- * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
- * [IDENTITY].
+ * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to
+ * [AffineTransform.IDENTITY].
*/
@JvmOverloads
@FloatRange(from = 0.0, to = 1.0)
- public fun coverage(
+ public fun computeCoverage(
other: PartitionedMesh,
otherShapeToThis: AffineTransform = AffineTransform.IDENTITY,
): Float =
@@ -327,7 +341,7 @@
*
* This is equivalent to:
* ```
- * this.coverage(triangle, triangleToThis) > coverageThreshold
+ * computeCoverage(triangle, triangleToThis) > coverageThreshold
* ```
*
* but may be faster.
@@ -335,11 +349,11 @@
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [triangleToThis] contains the transform that maps from [triangle]'s
- * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
- * [IDENTITY].
+ * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to
+ * [AffineTransform.IDENTITY].
*/
@JvmOverloads
- public fun coverageIsGreaterThan(
+ public fun computeCoverageIsGreaterThan(
triangle: Triangle,
coverageThreshold: Float,
triangleToThis: AffineTransform = AffineTransform.IDENTITY,
@@ -375,10 +389,10 @@
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [boxToThis] contains the transform that maps from [box]'s coordinate space
- * to this [PartitionedMesh]'s coordinate space, which defaults to the [IDENTITY].
+ * to this [PartitionedMesh]'s coordinate space, which defaults to [AffineTransform.IDENTITY].
*/
@JvmOverloads
- public fun coverageIsGreaterThan(
+ public fun computeCoverageIsGreaterThan(
box: Box,
coverageThreshold: Float,
boxToThis: AffineTransform = AffineTransform.IDENTITY,
@@ -413,10 +427,10 @@
*
* Optional argument [parallelogramToThis] contains the transform that maps from
* [parallelogram]'s coordinate space to this [PartitionedMesh]'s coordinate space, which
- * defaults to the [IDENTITY].
+ * defaults to [AffineTransform.IDENTITY].
*/
@JvmOverloads
- public fun coverageIsGreaterThan(
+ public fun computeCoverageIsGreaterThan(
parallelogram: Parallelogram,
coverageThreshold: Float,
parallelogramToThis: AffineTransform = AffineTransform.IDENTITY,
@@ -452,11 +466,11 @@
* On an empty [PartitionedMesh], this will always return 0.
*
* Optional argument [otherShapeToThis] contains the transform that maps from [other]'s
- * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
- * [IDENTITY].
+ * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to
+ * [AffineTransform.IDENTITY].
*/
@JvmOverloads
- public fun coverageIsGreaterThan(
+ public fun computeCoverageIsGreaterThan(
other: PartitionedMesh,
coverageThreshold: Float,
otherShapeToThis: AffineTransform = AffineTransform.IDENTITY,
@@ -488,7 +502,7 @@
override fun toString(): String {
val address = java.lang.Long.toHexString(nativeAddress)
- return "PartitionedMesh(bounds=$bounds, meshesByGroup=$meshesByGroup, nativeAddress=$address)"
+ return "PartitionedMesh(bounds=${computeBoundingBox()}, meshesByGroup=$meshesByGroup, nativeAddress=$address)"
}
protected fun finalize() {
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt
index 10bf22b..50106fe 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt
@@ -31,35 +31,35 @@
class PartitionedMeshTest {
@Test
- fun bounds_shouldBeEmpty() {
+ fun computeBoundingBox_shouldBeEmpty() {
val partitionedMesh = PartitionedMesh()
- assertThat(partitionedMesh.bounds).isNull()
+ assertThat(partitionedMesh.computeBoundingBox()).isNull()
}
@Test
- fun renderGroupCount_whenEmptyShape_shouldBeZero() {
+ fun getRenderGroupCount_whenEmptyShape_shouldBeZero() {
val partitionedMesh = PartitionedMesh()
- assertThat(partitionedMesh.renderGroupCount).isEqualTo(0)
+ assertThat(partitionedMesh.getRenderGroupCount()).isEqualTo(0)
}
@Test
- fun outlineCount_whenEmptyShape_shouldThrow() {
+ fun getOutlineCount_whenEmptyShape_shouldThrow() {
val partitionedMesh = PartitionedMesh()
- assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineCount(-1) }
- assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineCount(0) }
- assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineCount(1) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineCount(-1) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineCount(0) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineCount(1) }
}
@Test
- fun outlineVertexCount_whenEmptyShape_shouldThrow() {
+ fun getOutlineVertexCount_whenEmptyShape_shouldThrow() {
val partitionedMesh = PartitionedMesh()
- assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineVertexCount(-1, 0) }
- assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineVertexCount(0, 0) }
- assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineVertexCount(1, 0) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineVertexCount(-1, 0) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineVertexCount(0, 0) }
+ assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineVertexCount(1, 0) }
}
@Test
@@ -92,14 +92,14 @@
fun populateOutlinePosition_withStrokeShape_shouldBeWithinBounds() {
val shape = buildTestStrokeShape()
- assertThat(shape.renderGroupCount).isEqualTo(1)
- assertThat(shape.outlineCount(0)).isEqualTo(1)
- assertThat(shape.outlineVertexCount(0, 0)).isGreaterThan(2)
+ assertThat(shape.getRenderGroupCount()).isEqualTo(1)
+ assertThat(shape.getOutlineCount(0)).isEqualTo(1)
+ assertThat(shape.getOutlineVertexCount(0, 0)).isGreaterThan(2)
- val bounds = assertNotNull(shape.bounds)
+ val bounds = assertNotNull(shape.computeBoundingBox())
val p = MutableVec()
- for (outlineVertexIndex in 0 until shape.outlineVertexCount(0, 0)) {
+ for (outlineVertexIndex in 0 until shape.getOutlineVertexCount(0, 0)) {
shape.populateOutlinePosition(groupIndex = 0, outlineIndex = 0, outlineVertexIndex, p)
assertThat(p.x).isAtLeast(bounds.xMin)
assertThat(p.y).isAtLeast(bounds.yMin)
@@ -124,7 +124,7 @@
@Test
fun meshFormat_forTestShape_isEquivalentToMeshFormatOfFirstMesh() {
val partitionedMesh = buildTestStrokeShape()
- assertThat(partitionedMesh.renderGroupCount).isEqualTo(1)
+ assertThat(partitionedMesh.getRenderGroupCount()).isEqualTo(1)
val shapeFormat = partitionedMesh.renderGroupFormat(0)
val meshes = partitionedMesh.renderGroupMeshes(0)
assertThat(meshes).isNotEmpty()
@@ -152,9 +152,9 @@
p2 = ImmutableVec(100f, 700f),
)
- assertThat(partitionedMesh.coverage(intersectingTriangle)).isGreaterThan(0f)
- assertThat(partitionedMesh.coverage(externalTriangle)).isEqualTo(0f)
- assertThat(partitionedMesh.coverage(externalTriangle, SCALE_TRANSFORM)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(intersectingTriangle)).isGreaterThan(0f)
+ assertThat(partitionedMesh.computeCoverage(externalTriangle)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(externalTriangle, SCALE_TRANSFORM)).isEqualTo(0f)
}
/**
@@ -169,9 +169,9 @@
val externalBox =
ImmutableBox.fromTwoPoints(ImmutableVec(100f, 200f), ImmutableVec(300f, 400f))
- assertThat(partitionedMesh.coverage(intersectingBox)).isGreaterThan(0f)
- assertThat(partitionedMesh.coverage(externalBox)).isEqualTo(0f)
- assertThat(partitionedMesh.coverage(externalBox, SCALE_TRANSFORM)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(intersectingBox)).isGreaterThan(0f)
+ assertThat(partitionedMesh.computeCoverage(externalBox)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(externalBox, SCALE_TRANSFORM)).isEqualTo(0f)
}
/**
@@ -196,9 +196,10 @@
shearFactor = 2f,
)
- assertThat(partitionedMesh.coverage(intersectingParallelogram)).isGreaterThan(0f)
- assertThat(partitionedMesh.coverage(externalParallelogram)).isEqualTo(0f)
- assertThat(partitionedMesh.coverage(externalParallelogram, SCALE_TRANSFORM)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(intersectingParallelogram)).isGreaterThan(0f)
+ assertThat(partitionedMesh.computeCoverage(externalParallelogram)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(externalParallelogram, SCALE_TRANSFORM))
+ .isEqualTo(0f)
}
/**
@@ -221,9 +222,9 @@
)
.shape
- assertThat(partitionedMesh.coverage(intersectingShape)).isGreaterThan(0f)
- assertThat(partitionedMesh.coverage(externalShape)).isEqualTo(0f)
- assertThat(partitionedMesh.coverage(externalShape, SCALE_TRANSFORM)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(intersectingShape)).isGreaterThan(0f)
+ assertThat(partitionedMesh.computeCoverage(externalShape)).isEqualTo(0f)
+ assertThat(partitionedMesh.computeCoverage(externalShape, SCALE_TRANSFORM)).isEqualTo(0f)
}
/**
@@ -246,9 +247,11 @@
p2 = ImmutableVec(100f, 700f),
)
- assertThat(partitionedMesh.coverageIsGreaterThan(intersectingTriangle, 0f)).isTrue()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalTriangle, 0f)).isFalse()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalTriangle, 0f, SCALE_TRANSFORM))
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingTriangle, 0f)).isTrue()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalTriangle, 0f)).isFalse()
+ assertThat(
+ partitionedMesh.computeCoverageIsGreaterThan(externalTriangle, 0f, SCALE_TRANSFORM)
+ )
.isFalse()
}
@@ -270,9 +273,9 @@
val externalBox =
ImmutableBox.fromTwoPoints(ImmutableVec(100f, 200f), ImmutableVec(300f, 400f))
- assertThat(partitionedMesh.coverageIsGreaterThan(intersectingBox, 0f)).isTrue()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalBox, 0f)).isFalse()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalBox, 0f, SCALE_TRANSFORM))
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingBox, 0f)).isTrue()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalBox, 0f)).isFalse()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalBox, 0f, SCALE_TRANSFORM))
.isFalse()
}
@@ -298,10 +301,16 @@
shearFactor = 2f,
)
- assertThat(partitionedMesh.coverageIsGreaterThan(intersectingParallelogram, 0f)).isTrue()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalParallelogram, 0f)).isFalse()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingParallelogram, 0f))
+ .isTrue()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalParallelogram, 0f))
+ .isFalse()
assertThat(
- partitionedMesh.coverageIsGreaterThan(externalParallelogram, 0f, SCALE_TRANSFORM)
+ partitionedMesh.computeCoverageIsGreaterThan(
+ externalParallelogram,
+ 0f,
+ SCALE_TRANSFORM
+ )
)
.isFalse()
}
@@ -333,9 +342,9 @@
)
.shape
- assertThat(partitionedMesh.coverageIsGreaterThan(intersectingShape, 0f)).isTrue()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalShape, 0f)).isFalse()
- assertThat(partitionedMesh.coverageIsGreaterThan(externalShape, 0f, SCALE_TRANSFORM))
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingShape, 0f)).isTrue()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalShape, 0f)).isFalse()
+ assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalShape, 0f, SCALE_TRANSFORM))
.isFalse()
}
@@ -360,7 +369,7 @@
)
assertThat(partitionedMesh.isSpatialIndexInitialized()).isFalse()
- assertThat(partitionedMesh.coverage(triangle)).isNotNaN()
+ assertThat(partitionedMesh.computeCoverage(triangle)).isNotNaN()
assertThat(partitionedMesh.isSpatialIndexInitialized()).isTrue()
}
diff --git a/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/Stroke.kt b/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/Stroke.kt
index 050e442..5b262d7 100644
--- a/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/Stroke.kt
+++ b/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/Stroke.kt
@@ -125,8 +125,8 @@
*/
internal constructor(nativeAddress: Long, brush: Brush) {
val shape = PartitionedMesh(StrokeJni.allocShallowCopyOfShape(nativeAddress))
- require(shape.renderGroupCount == brush.family.coats.size) {
- "The shape must have one render group per brush coat, but found ${shape.renderGroupCount} render groups in shape and ${brush.family.coats.size} brush coats in brush."
+ require(shape.getRenderGroupCount() == brush.family.coats.size) {
+ "The shape must have one render group per brush coat, but found ${shape.getRenderGroupCount()} render groups in shape and ${brush.family.coats.size} brush coats in brush."
}
this.nativeAddress = nativeAddress
this.brush = brush
@@ -142,8 +142,8 @@
* [PartitionedMesh] is being stored in addition to the [Brush] and [StrokeInputBatch].
*/
public constructor(brush: Brush, inputs: StrokeInputBatch, shape: PartitionedMesh) {
- require(shape.renderGroupCount == brush.family.coats.size) {
- "The shape must have one render group per brush coat, but found ${shape.renderGroupCount} render groups in shape and ${brush.family.coats.size} brush coats in brush."
+ require(shape.getRenderGroupCount() == brush.family.coats.size) {
+ "The shape must have one render group per brush coat, but found ${shape.getRenderGroupCount()} render groups in shape and ${brush.family.coats.size} brush coats in brush."
}
this.brush = brush
this.shape = shape
diff --git a/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeTest.kt b/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeTest.kt
index c730120..2155aff 100644
--- a/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeTest.kt
+++ b/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeTest.kt
@@ -68,7 +68,7 @@
// Create a [ModeledShape] with render group.
val inputs = makeTestInputs()
val shape = Stroke(buildTestBrush(), inputs).shape
- assertThat(shape.renderGroupCount).isEqualTo(1)
+ assertThat(shape.getRenderGroupCount()).isEqualTo(1)
// Create a brush with two brush coats.
val coat = BrushCoat(BrushTip(), BrushPaint())
diff --git a/libraryversions.toml b/libraryversions.toml
index c73bef8..75a598d 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -89,9 +89,9 @@
LEANBACK_TAB = "1.1.0-beta01"
LEGACY = "1.1.0-alpha01"
LIBYUV = "0.1.0-dev01"
-LIFECYCLE = "2.9.0-alpha01"
+LIFECYCLE = "2.9.0-alpha02"
LIFECYCLE_EXTENSIONS = "2.2.0"
-LINT = "1.0.0-alpha01"
+LINT = "1.0.0-alpha02"
LOADER = "1.2.0-alpha01"
MEDIA = "1.7.0-rc01"
MEDIAROUTER = "1.8.0-alpha01"
diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationAndroidTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationAndroidTest.kt
index 897a60c..10b89a5 100644
--- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationAndroidTest.kt
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationAndroidTest.kt
@@ -68,7 +68,7 @@
destination.id = 1
assertThat(destination.route).isEqualTo("route")
assertThat(destination.id).isEqualTo(1)
- assertThat(destination.hasDeepLink(createRoute("route").toUri())).isTrue()
+ assertThat(destination.hasDeepLink(createRoute("route").toUri())).isFalse()
destination.route = null
assertThat(destination.route).isNull()
@@ -682,4 +682,44 @@
assertThat(destination.hasRoute<TestClass>()).isFalse()
}
+
+ @Test
+ fun routeNotAddedToDeepLink() {
+ val destination = NoOpNavigator().createDestination()
+ assertThat(destination.route).isNull()
+
+ destination.route = "route"
+ assertThat(destination.route).isEqualTo("route")
+ assertThat(destination.hasDeepLink(createRoute("route").toUri())).isFalse()
+ }
+
+ @Test
+ fun matchRoute() {
+ val destination = NoOpNavigator().createDestination()
+
+ destination.route = "route"
+ assertThat(destination.route).isEqualTo("route")
+
+ val match = destination.matchRoute("route")
+ assertThat(match).isNotNull()
+ assertThat(match!!.destination).isEqualTo(destination)
+ }
+
+ @Test
+ fun matchRouteAfterSetNewRoute() {
+ val destination = NoOpNavigator().createDestination()
+
+ destination.route = "route"
+ assertThat(destination.route).isEqualTo("route")
+
+ val match = destination.matchRoute("route")
+ assertThat(match).isNotNull()
+ assertThat(match!!.destination).isEqualTo(destination)
+
+ destination.route = "newRoute"
+ assertThat(destination.route).isEqualTo("newRoute")
+ val match2 = destination.matchRoute("newRoute")
+ assertThat(match2).isNotNull()
+ assertThat(match2!!.destination).isEqualTo(destination)
+ }
}
diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt
index 5e1c141..64768f7 100644
--- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt
@@ -168,9 +168,7 @@
}
assertThat(expected.message)
.isEqualTo(
- "Deep link android-app://androidx.navigation/route can't be used to " +
- "open destination NavDestination(0xa2bd82dc).\n" +
- "Following required arguments are missing: [intArg]"
+ "Cannot set route \"route\" for destination NavDestination(0x0). Following required arguments are missing: [intArg]"
)
}
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 de01d95..4225f41 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
@@ -223,14 +223,36 @@
id = 0
} else {
require(route.isNotBlank()) { "Cannot have an empty route" }
- val internalRoute = createRoute(route)
- id = internalRoute.hashCode()
- addDeepLink(internalRoute)
+
+ // make sure the route contains all required arguments
+ val tempRoute = createRoute(route)
+ val tempDeepLink = NavDeepLink.Builder().setUriPattern(tempRoute).build()
+ val missingRequiredArguments =
+ _arguments.missingRequiredArguments { key ->
+ key !in tempDeepLink.argumentsNames
+ }
+ require(missingRequiredArguments.isEmpty()) {
+ "Cannot set route \"$route\" for destination $this. " +
+ "Following required arguments are missing: $missingRequiredArguments"
+ }
+
+ routeDeepLink = lazy { NavDeepLink.Builder().setUriPattern(tempRoute).build() }
+ id = tempRoute.hashCode()
}
- deepLinks.remove(deepLinks.firstOrNull { it.uriPattern == createRoute(field) })
field = route
}
+ /**
+ * This destination's unique route as a NavDeepLink.
+ *
+ * This deeplink must be kept private and segregated from the explicitly added public deeplinks
+ * to ensure that external users cannot deeplink into this destination with this routeDeepLink.
+ *
+ * This value is reassigned a new lazy value every time [route] is updated to ensure that any
+ * initialized lazy value is overwritten with the latest value.
+ */
+ private var routeDeepLink: Lazy<NavDeepLink>? = null
+
public open val displayName: String
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) get() = idName ?: id.toString()
@@ -346,26 +368,28 @@
}
/**
- * Determines if this NavDestination has a deep link of this route.
+ * Determines if this NavDestination's route matches the requested route.
*
* @param [route] The route to match against this [NavDestination.route]
* @return The matching [DeepLinkMatch], or null if no match was found.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public fun matchDeepLink(route: String): DeepLinkMatch? {
- val request = NavDeepLinkRequest.Builder.fromUri(createRoute(route).toUri()).build()
- val matchingDeepLink =
- if (this is NavGraph) {
- matchDeepLinkComprehensive(
- request,
- searchChildren = false,
- searchParent = false,
- lastVisited = this
- )
- } else {
- matchDeepLink(request)
- }
- return matchingDeepLink
+ public fun matchRoute(route: String): DeepLinkMatch? {
+ val routeDeepLink = this.routeDeepLink?.value ?: return null
+
+ val uri = createRoute(route).toUri()
+
+ // includes matching args for path, query, and fragment
+ val matchingArguments = routeDeepLink.getMatchingArguments(uri, _arguments) ?: return null
+ val matchingPathSegments = routeDeepLink.calculateMatchingPathSegments(uri)
+ return DeepLinkMatch(
+ this,
+ matchingArguments,
+ routeDeepLink.isExactDeepLink,
+ matchingPathSegments,
+ false,
+ -1
+ )
}
/**
@@ -481,7 +505,7 @@
// if no match based on routePattern, this means route contains filled in args or query
// params
- val matchingDeepLink = matchDeepLink(route)
+ val matchingDeepLink = matchRoute(route)
// if no matchingDeepLink or mismatching destination, return false directly
if (this != matchingDeepLink?.destination) return false
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
index 3a34696..5fbe69d 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
@@ -65,6 +65,50 @@
}
/**
+ * Matches route with all children and parents recursively.
+ *
+ * Does not revisit graphs (whether it's a child or parent) if it has already been visited.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public fun matchRouteComprehensive(
+ route: String,
+ searchChildren: Boolean,
+ searchParent: Boolean,
+ lastVisited: NavDestination
+ ): DeepLinkMatch? {
+ // First try to match with this graph's route
+ val bestMatch = matchRoute(route)
+ // If searchChildren is true, search through all child destinations for a matching route
+ val bestChildMatch =
+ if (searchChildren) {
+ mapNotNull { child ->
+ when (child) {
+ lastVisited -> null
+ is NavGraph ->
+ child.matchRouteComprehensive(
+ route,
+ searchChildren = true,
+ searchParent = false,
+ lastVisited = this
+ )
+ else -> child.matchRoute(route)
+ }
+ }
+ .maxOrNull()
+ } else null
+
+ // If searchParent is true, search through all parents (and their children) destinations
+ // for a matching route
+ val bestParentMatch =
+ parent?.let {
+ if (searchParent && it != lastVisited)
+ it.matchRouteComprehensive(route, searchChildren, true, this)
+ else null
+ }
+ return listOfNotNull(bestMatch, bestChildMatch, bestParentMatch).maxOrNull()
+ }
+
+ /**
* Matches deeplink with all children and parents recursively.
*
* Does not revisit graphs (whether it's a child or parent) if it has already been visited.
@@ -262,7 +306,7 @@
nodes.valueIterator().asSequence().firstOrNull {
// first try matching with routePattern
// if not found with routePattern, try matching with route args
- it.route.equals(route) || it.matchDeepLink(route) != null
+ it.route.equals(route) || it.matchRoute(route) != null
}
// Search the parent for the NavDestination if it is not a child of this navigation graph
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphNavigator.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphNavigator.kt
index 79035974..a8b4a4f 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphNavigator.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphNavigator.kt
@@ -84,7 +84,7 @@
)
}
if (startRoute != null && startRoute != startDestination.route) {
- val matchingArgs = startDestination.matchDeepLink(startRoute)?.matchingArgs
+ val matchingArgs = startDestination.matchRoute(startRoute)?.matchingArgs
if (matchingArgs != null && !matchingArgs.isEmpty) {
val bundle = Bundle()
// we need to add args from startRoute, but it should not override existing args
diff --git a/navigation/navigation-compose/samples/build.gradle b/navigation/navigation-compose/samples/build.gradle
index b672a68..eb7498d 100644
--- a/navigation/navigation-compose/samples/build.gradle
+++ b/navigation/navigation-compose/samples/build.gradle
@@ -57,10 +57,6 @@
kotlinTarget = KotlinTarget.KOTLIN_2_0
}
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
android {
compileSdk 35
namespace "androidx.navigation.compose.samples"
diff --git a/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SharedElementSample.kt b/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SharedElementSample.kt
index 838929a6..bfb1c33 100644
--- a/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SharedElementSample.kt
+++ b/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SharedElementSample.kt
@@ -103,65 +103,77 @@
SharedTransitionLayout {
val selectFirst = mutableStateOf(true)
NavHost(navController, startDestination = RedBox) {
- composable<RedBox> { RedBox(this, selectFirst) { navController.navigate(BlueBox) } }
- composable<BlueBox> { BlueBox(this, selectFirst) { navController.popBackStack() } }
+ composable<RedBox> {
+ RedBox(this@SharedTransitionLayout, this, selectFirst) {
+ navController.navigate(BlueBox)
+ }
+ }
+ composable<BlueBox> {
+ BlueBox(this@SharedTransitionLayout, this, selectFirst) {
+ navController.popBackStack()
+ }
+ }
}
}
}
-context(SharedTransitionScope)
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun RedBox(
+ sharedScope: SharedTransitionScope,
scope: AnimatedContentScope,
selectFirst: MutableState<Boolean>,
onNavigate: () -> Unit
) {
- Box(
- Modifier.sharedBounds(
- rememberSharedContentState("name"),
- scope,
- renderInOverlayDuringTransition = selectFirst.value
- )
- .clickable(
- onClick = {
- selectFirst.value = !selectFirst.value
- onNavigate()
- }
- )
- .background(Color.Red)
- .size(100.dp)
- ) {
- Text("start", color = Color.White)
+ with(sharedScope) {
+ Box(
+ Modifier.sharedBounds(
+ rememberSharedContentState("name"),
+ scope,
+ renderInOverlayDuringTransition = selectFirst.value
+ )
+ .clickable(
+ onClick = {
+ selectFirst.value = !selectFirst.value
+ onNavigate()
+ }
+ )
+ .background(Color.Red)
+ .size(100.dp)
+ ) {
+ Text("start", color = Color.White)
+ }
}
}
-context(SharedTransitionScope)
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun BlueBox(
+ sharedScope: SharedTransitionScope,
scope: AnimatedContentScope,
selectFirst: MutableState<Boolean>,
onPopBack: () -> Unit
) {
- Box(
- Modifier.offset(180.dp, 180.dp)
- .sharedBounds(
- rememberSharedContentState("name"),
- scope,
- renderInOverlayDuringTransition = !selectFirst.value
- )
- .clickable(
- onClick = {
- selectFirst.value = !selectFirst.value
- onPopBack()
- }
- )
- .alpha(0.5f)
- .background(Color.Blue)
- .size(180.dp)
- ) {
- Text("finish", color = Color.White)
+ with(sharedScope) {
+ Box(
+ Modifier.offset(180.dp, 180.dp)
+ .sharedBounds(
+ rememberSharedContentState("name"),
+ scope,
+ renderInOverlayDuringTransition = !selectFirst.value
+ )
+ .clickable(
+ onClick = {
+ selectFirst.value = !selectFirst.value
+ onPopBack()
+ }
+ )
+ .alpha(0.5f)
+ .background(Color.Blue)
+ .size(180.dp)
+ ) {
+ Text("finish", color = Color.White)
+ }
}
}
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt
index 986c95e..c05732dc 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt
@@ -29,6 +29,7 @@
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.testing.TestLifecycleOwner
+import androidx.navigation.NavController.Companion.KEY_DEEP_LINK_INTENT
import androidx.navigation.NavDestination.Companion.createRoute
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.serialization.generateHashCode
@@ -814,6 +815,30 @@
@UiThreadTest
@Test
+ fun testNavigateContainsIntent() {
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(startDestination = "start") {
+ test("start")
+ test("second")
+ }
+
+ val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+ assertThat(navController.currentDestination?.route).isEqualTo("start")
+ assertThat(navigator.backStack.size).isEqualTo(1)
+
+ navController.navigate("second")
+ assertThat(navController.currentDestination?.route).isEqualTo("second")
+ assertThat(navigator.backStack.size).isEqualTo(2)
+ val intent: Intent? =
+ @Suppress("DEPRECATION")
+ navController.currentBackStackEntry?.arguments?.getParcelable(KEY_DEEP_LINK_INTENT)
+ assertThat(intent).isNotNull()
+ assertThat(intent!!.data).isEqualTo(Uri.parse("android-app://androidx.navigation/second"))
+ }
+
+ @UiThreadTest
+ @Test
fun testNavigateNestedSharedDestination() {
val navController = createNavController()
navController.graph =
@@ -1524,7 +1549,6 @@
test<TestClass>()
}
assertThat(navController.currentDestination?.route).isEqualTo("start")
-
// passed in arg
navController.navigate(TestClass(TestTopLevelEnum.TWO))
assertThat(navController.currentDestination?.hasRoute(TestClass::class)).isTrue()
@@ -1725,12 +1749,10 @@
navController.graph = nav_singleArg_graph
// first nav with arg filed in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
// second nav with arg filled in
- val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
- navController.navigate(deepLink2)
+ navController.navigate("second_test/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13", "second_test/18"]
@@ -1761,12 +1783,10 @@
}
// fist nested graph
- val deepLink = Uri.parse("android-app://androidx.navigation/graph2/13")
- navController.navigate(deepLink)
+ navController.navigate("graph2/13")
// second nested graph
- val deepLink2 = Uri.parse("android-app://androidx.navigation/graph3/18")
- navController.navigate(deepLink2)
+ navController.navigate("graph3/18")
val navigator = navController.navigatorProvider.getNavigator(NavGraphNavigator::class.java)
// ["graph", "graph2/13", "graph3/18"]
@@ -1786,8 +1806,7 @@
navController.graph = nav_multiArg_graph
// navigate with both args filed in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/18"]
@@ -1804,8 +1823,7 @@
navController.graph = nav_multiArg_graph
// navigate with args partially filed in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/{arg2}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/{arg2}"]
@@ -1902,8 +1920,7 @@
}
// navigate with query param
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test?opt=13")
- navController.navigate(deepLink)
+ navController.navigate("second_test?opt=13")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -1937,8 +1954,7 @@
}
// navigate with query params
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test?opt=null&opt2=13")
- navController.navigate(deepLink)
+ navController.navigate("second_test?opt=null&opt2=13")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -1998,8 +2014,7 @@
navController.graph = nav_singleArg_graph
// navigate with arg filed in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13"]
@@ -2024,8 +2039,7 @@
navController.graph = nav_multiArg_graph
// navigate with args partially filed in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/18"]
@@ -2049,8 +2063,7 @@
navController.graph = nav_multiArg_graph
// navigate with args partially filed in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/{arg2}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/{arg2}"]
@@ -2073,8 +2086,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/18"]
@@ -2446,12 +2458,10 @@
navController.graph = nav_singleArg_graph
// first nav with arg filed in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
// second nav with arg filled in
- val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
- navController.navigate(deepLink2)
+ navController.navigate("second_test/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13", "second_test/18"]
@@ -2470,12 +2480,10 @@
navController.graph = nav_singleArg_graph
// first nav with arg filed in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
// second nav with arg filled in
- val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
- navController.navigate(deepLink2)
+ navController.navigate("second_test/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13", "second_test/18"]
@@ -2492,8 +2500,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/18"]
@@ -2510,8 +2517,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/{arg2}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/{arg2}"]
@@ -2539,8 +2545,7 @@
}
// navigate without filling query param
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test?{arg}")
- navController.navigate(deepLink)
+ navController.navigate("second_test?{arg}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -2569,8 +2574,7 @@
}
// navigate without query param
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test?opt={arg}")
- navController.navigate(deepLink)
+ navController.navigate("second_test?opt={arg}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -2596,8 +2600,7 @@
}
// navigate without query param
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test?opt=null")
- navController.navigate(deepLink)
+ navController.navigate("second_test?opt=null")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -2612,8 +2615,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/18"]
@@ -2630,8 +2632,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/{arg2}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/{arg2}"]
@@ -2648,8 +2649,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/18"]
@@ -2666,8 +2666,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
// ["start_test", "second_test/13/18"]
@@ -2989,11 +2988,9 @@
val navController = createNavController()
navController.graph = nav_singleArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
- val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
- navController.navigate(deepLink2)
+ navController.navigate("second_test/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3012,11 +3009,9 @@
val navController = createNavController()
navController.graph = nav_singleArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
- val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
- navController.navigate(deepLink2)
+ navController.navigate("second_test/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3036,11 +3031,9 @@
val navController = createNavController()
navController.graph = nav_singleArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
- val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18")
- navController.navigate(deepLink2)
+ navController.navigate("second_test/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3090,11 +3083,9 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/14")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/14")
- val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18/19")
- navController.navigate(deepLink2)
+ navController.navigate("second_test/18/19")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3111,11 +3102,9 @@
val navController = createNavController()
navController.graph = nav_singleArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
- val deepLink3 = Uri.parse("android-app://androidx.navigation/second_test/18")
- navController.navigate(deepLink3)
+ navController.navigate("second_test/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3133,11 +3122,9 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/14")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/14")
- val deepLink2 = Uri.parse("android-app://androidx.navigation/second_test/18/19")
- navController.navigate(deepLink2)
+ navController.navigate("second_test/18/19")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3155,11 +3142,9 @@
val navController = createNavController()
navController.graph = nav_singleArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
- val deepLink3 = Uri.parse("android-app://androidx.navigation/second_test/18")
- navController.navigate(deepLink3)
+ navController.navigate("second_test/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(3)
@@ -3179,8 +3164,7 @@
val navController = createNavController()
navController.graph = nav_singleArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3201,8 +3185,7 @@
navController.graph = nav_multiArg_graph
// navigate with partial args filled in
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/{arg2}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3229,8 +3212,7 @@
}
// navigate without filling query param
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test?{arg}")
- navController.navigate(deepLink)
+ navController.navigate("second_test?{arg}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3258,8 +3240,7 @@
}
// navigate without filling query param
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test?opt={arg}")
- navController.navigate(deepLink)
+ navController.navigate("second_test?opt={arg}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3287,8 +3268,7 @@
}
// navigate without filling query param
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test?opt=null")
- navController.navigate(deepLink)
+ navController.navigate("second_test?opt=null")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3305,8 +3285,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/{arg2}")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/{arg2}")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3323,8 +3302,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3341,8 +3319,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3359,8 +3336,7 @@
val navController = createNavController()
navController.graph = nav_multiArg_graph
- val deepLink = Uri.parse("android-app://androidx.navigation/second_test/13/18")
- navController.navigate(deepLink)
+ navController.navigate("second_test/13/18")
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
assertThat(navigator.backStack.size).isEqualTo(2)
@@ -3846,6 +3822,71 @@
assertThat(navigator.backStack.size).isEqualTo(1)
}
+ @UiThreadTest
+ @Test
+ fun testNavigateViaUriOnlyIfDeepLinkExplicitlyAdded() {
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(startDestination = "start") {
+ test("start") { deepLink { uriPattern = createRoute("explicit_start_deeplink") } }
+ }
+ val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+ assertThat(navController.currentDestination?.route).isEqualTo("start")
+ assertThat(navigator.backStack.size).isEqualTo(1)
+
+ val deepLink = Uri.parse(createRoute("explicit_start_deeplink"))
+
+ navController.navigate(deepLink)
+ assertThat(navController.currentDestination?.route).isEqualTo("start")
+ assertThat(navigator.backStack.size).isEqualTo(2)
+
+ // ensure can't deep link with destination's public route
+ val deepLink2 = Uri.parse(createRoute("start"))
+
+ val exception =
+ assertFailsWith<IllegalArgumentException> { navController.navigate(deepLink2) }
+ assertThat(exception.message)
+ .isEqualTo(
+ "Navigation destination that matches request " +
+ "NavDeepLinkRequest{ uri=android-app://androidx.navigation/start } " +
+ "cannot be found in the navigation graph NavGraph(0x0) " +
+ "startDestination={Destination(0xa2cd94f5) route=start}"
+ )
+ }
+
+ @UiThreadTest
+ @Test
+ fun testNavigateViaRequestOnlyIfDeepLinkExplicitlyAdded() {
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(startDestination = "start") {
+ test("start") { deepLink { uriPattern = createRoute("explicit_start_deeplink") } }
+ }
+ val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
+ assertThat(navController.currentDestination?.route).isEqualTo("start")
+ assertThat(navigator.backStack.size).isEqualTo(1)
+
+ val request =
+ NavDeepLinkRequest(Uri.parse(createRoute("explicit_start_deeplink")), null, null)
+
+ navController.navigate(request)
+ assertThat(navController.currentDestination?.route).isEqualTo("start")
+ assertThat(navigator.backStack.size).isEqualTo(2)
+
+ // ensure can't deep link with destination's public route
+ val request2 = NavDeepLinkRequest(Uri.parse(createRoute("start")), null, null)
+
+ val exception =
+ assertFailsWith<IllegalArgumentException> { navController.navigate(request2) }
+ assertThat(exception.message)
+ .isEqualTo(
+ "Navigation destination that matches request " +
+ "NavDeepLinkRequest{ uri=android-app://androidx.navigation/start } " +
+ "cannot be found in the navigation graph NavGraph(0x0) " +
+ "startDestination={Destination(0xa2cd94f5) route=start}"
+ )
+ }
+
@LargeTest
@Test
fun testNavigateViaImplicitDeepLink() {
@@ -5125,6 +5166,56 @@
@UiThreadTest
@Test
+ fun testHandleDeepLinkFromRouteOnlyIfExplicitlyAdded() {
+ val navController = createNavController()
+ navController.graph =
+ navController.createGraph(startDestination = "start") {
+ test("start") { deepLink { uriPattern = createRoute("explicit_start_deeplink") } }
+ }
+ val collectedDestinationRoutes = mutableListOf<String?>()
+ navController.addOnDestinationChangedListener { _, destination, _ ->
+ collectedDestinationRoutes.add(destination.route)
+ }
+
+ assertThat(collectedDestinationRoutes).containsExactly("start")
+
+ val intent =
+ Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse(createRoute("explicit_start_deeplink")),
+ ApplicationProvider.getApplicationContext() as Context,
+ TestActivity::class.java
+ )
+
+ assertWithMessage("handleDeepLink should return true with valid deep link")
+ .that(navController.handleDeepLink(intent))
+ .isTrue()
+
+ assertWithMessage("$collectedDestinationRoutes should have 2 destination id")
+ .that(collectedDestinationRoutes)
+ .hasSize(2)
+ assertThat(collectedDestinationRoutes).containsExactly("start", "start")
+
+ val intent2 =
+ Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse(createRoute("start")),
+ ApplicationProvider.getApplicationContext() as Context,
+ TestActivity::class.java
+ )
+
+ assertWithMessage("handleDeepLink should return false with invalid deep link")
+ .that(navController.handleDeepLink(intent2))
+ .isFalse()
+
+ assertWithMessage("$collectedDestinationRoutes should have 2 destination id")
+ .that(collectedDestinationRoutes)
+ .hasSize(2)
+ assertThat(collectedDestinationRoutes).containsExactly("start", "start")
+ }
+
+ @UiThreadTest
+ @Test
fun testHandleDeepLinkToRootInvalid() {
val navController = createNavController()
navController.graph = nav_simple_route_graph
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
index ae11abb..bcf2373 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
@@ -313,9 +313,9 @@
}
assertThat(expected.message)
.isEqualTo(
- "Deep link android-app://androidx.navigation/graph can't be used to open destination " +
- "NavGraph(0xa22391e1) startDestination=0x0.\n" +
- "Following required arguments are missing: [intArg]"
+ "Cannot set route \"graph\" for destination " +
+ "NavGraph(0x0) startDestination=0x0. Following required " +
+ "arguments are missing: [intArg]"
)
}
@@ -339,9 +339,9 @@
}
assertThat(expected.message)
.isEqualTo(
- "Deep link android-app://androidx.navigation/graph/{intArg} can't be used to " +
- "open destination NavGraph(0xf9423909) startDestination=0x0.\n" +
- "Following required arguments are missing: [longArg]"
+ "Cannot set route \"graph/{intArg}\" for destination " +
+ "NavGraph(0x0) startDestination=0x0. Following required " +
+ "arguments are missing: [longArg]"
)
}
@@ -365,9 +365,9 @@
}
assertThat(expected.message)
.isEqualTo(
- "Deep link android-app://androidx.navigation/graph can't be used to open " +
- "destination NavGraph(0xa22391e1) startDestination=0x0.\n" +
- "Following required arguments are missing: [intArg, longArg]"
+ "Cannot set route \"graph\" for destination NavGraph(0x0) " +
+ "startDestination=0x0. Following required arguments " +
+ "are missing: [intArg, longArg]"
)
}
@@ -389,9 +389,8 @@
}
assertThat(expected.message)
.isEqualTo(
- "Deep link android-app://androidx.navigation/dest1 can't be used to open " +
- "destination Destination(0xa1f3a662).\n" +
- "Following required arguments are missing: [intArg]"
+ "Cannot set route \"dest1\" for destination " +
+ "Destination(0x0). Following required arguments are missing: [intArg]"
)
}
@@ -420,9 +419,9 @@
}
assertThat(expected.message)
.isEqualTo(
- "Deep link android-app://androidx.navigation/dest1/{intArg} can't be used to " +
- "open destination Destination(0x994aa5a8).\n" +
- "Following required arguments are missing: [longArg]"
+ "Cannot set route \"dest1/{intArg}\" for " +
+ "destination Destination(0x0). Following required " +
+ "arguments are missing: [longArg]"
)
}
@@ -448,9 +447,7 @@
}
assertThat(expected.message)
.isEqualTo(
- "Deep link android-app://androidx.navigation/dest can't be used to open " +
- "destination Destination(0x78d64faf).\n" +
- "Following required arguments are missing: [intArg, longArg]"
+ "Cannot set route \"dest\" for destination Destination(0x0). Following required arguments are missing: [intArg, longArg]"
)
}
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
index 0075458..1d6d518 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
@@ -1683,7 +1683,7 @@
return null
}
// if not matched by routePattern, try matching with route args
- if (_graph!!.route == route || _graph!!.matchDeepLink(route) != null) {
+ if (_graph!!.route == route || _graph!!.matchRoute(route) != null) {
return _graph
}
return backQueue.getTopGraph().findNode(route)
@@ -2150,7 +2150,7 @@
backStackMap.values.removeAll { it == backStackId }
val backStackState = backStackStates.remove(backStackId)
- val matchingDeepLink = matchingDestination.matchDeepLink(route)
+ val matchingDeepLink = matchingDestination.matchRoute(route)
// check if the topmost NavBackStackEntryState contains the arguments in this
// matchingDeepLink. If not, we didn't find the correct stack.
val isCorrectStack =
@@ -2424,11 +2424,35 @@
navOptions: NavOptions? = null,
navigatorExtras: Navigator.Extras? = null
) {
- navigate(
- NavDeepLinkRequest.Builder.fromUri(createRoute(route).toUri()).build(),
- navOptions,
- navigatorExtras
- )
+ requireNotNull(_graph) {
+ "Cannot navigate to $route. Navigation graph has not been set for " +
+ "NavController $this."
+ }
+ val currGraph = backQueue.getTopGraph()
+ val deepLinkMatch =
+ currGraph.matchRouteComprehensive(
+ route,
+ searchChildren = true,
+ searchParent = true,
+ lastVisited = currGraph
+ )
+ if (deepLinkMatch != null) {
+ val destination = deepLinkMatch.destination
+ val args = destination.addInDefaultArgs(deepLinkMatch.matchingArgs) ?: Bundle()
+ val node = deepLinkMatch.destination
+ val intent =
+ Intent().apply {
+ setDataAndType(createRoute(destination.route).toUri(), null)
+ action = null
+ }
+ args.putParcelable(KEY_DEEP_LINK_INTENT, intent)
+ navigate(node, args, navOptions, navigatorExtras)
+ } else {
+ throw IllegalArgumentException(
+ "Navigation destination that matches route $route cannot be found in the " +
+ "navigation graph $_graph"
+ )
+ }
}
/**
@@ -2470,12 +2494,7 @@
navOptions: NavOptions? = null,
navigatorExtras: Navigator.Extras? = null
) {
- val finalRoute = generateRouteFilled(route)
- navigate(
- NavDeepLinkRequest.Builder.fromUri(createRoute(finalRoute).toUri()).build(),
- navOptions,
- navigatorExtras
- )
+ navigate(generateRouteFilled(route), navOptions, navigatorExtras)
}
/**
diff --git a/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt b/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
index d31fa8e..63fe0d1 100644
--- a/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
+++ b/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
@@ -18,11 +18,13 @@
import android.app.Activity
import android.content.ContentResolver
+import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.view.WindowManager
import android.widget.FrameLayout
import androidx.core.os.BundleCompat
import androidx.core.view.WindowInsetsCompat
@@ -66,6 +68,7 @@
import androidx.pdf.widget.ZoomView
import androidx.pdf.widget.ZoomView.ZoomScroll
import com.google.android.material.floatingactionbutton.FloatingActionButton
+import java.io.IOException
import kotlinx.coroutines.launch
/**
@@ -235,7 +238,6 @@
): View? {
super.onCreateView(inflater, container, savedInstanceState)
this.container = container
-
if (!hasContents && delayedContentsAvailable == null) {
if (savedInstanceState != null) {
restoreContents(savedInstanceState)
@@ -298,7 +300,7 @@
}
}
},
- onDocumentLoadFailure = { thrown -> onLoadDocumentError(thrown) }
+ onDocumentLoadFailure = { thrown -> showLoadingErrorView(thrown) }
)
setUpEditFab()
@@ -362,20 +364,25 @@
/** Adjusts the [FindInFileView] to be displayed on top of the keyboard. */
private fun adjustInsetsForSearchMenu(findInFileView: FindInFileView, activity: Activity) {
- val screenHeight = activity.resources.displayMetrics.heightPixels
+ val containerLocation = IntArray(2)
+ container!!.getLocationInWindow(containerLocation)
+
+ val windowManager = activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+ val screenHeight = windowManager.currentWindowMetrics.bounds.height()
+
val imeInsets =
activity.window.decorView.rootWindowInsets.getInsets(WindowInsetsCompat.Type.ime())
- var menuMargin = 0
val keyboardTop = screenHeight - imeInsets.bottom
- if (container!!.bottom >= keyboardTop) {
- menuMargin = container!!.bottom - keyboardTop
- }
+ val absoluteContainerBottom = container!!.height + containerLocation[1]
+ var menuMargin = 0
+ if (absoluteContainerBottom >= keyboardTop) {
+ menuMargin = absoluteContainerBottom - keyboardTop
+ }
findInFileView.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = menuMargin
}
-
isSearchMenuAdjusted = true
}
@@ -494,8 +501,11 @@
savedState?.let { state ->
if (isFileRestoring) {
state.containsKey(KEY_LAYOUT_REACH).let {
- val layoutReach = state.getInt(KEY_LAYOUT_REACH)
- layoutHandler?.setInitialPageLayoutReachWithMax(layoutReach)
+ val layoutReach = state.getInt(KEY_LAYOUT_REACH, -1)
+ if (layoutReach != -1) {
+ layoutHandler?.pageLayoutReach = layoutReach
+ layoutHandler?.setInitialPageLayoutReachWithMax(layoutReach)
+ }
}
// Restore page selection from saved state if it exists
@@ -581,6 +591,7 @@
pdfLoaderCallbacks?.pdfLoader = pdfLoader
layoutHandler = LayoutHandler(pdfLoader)
+ paginatedView?.model?.size?.let { layoutHandler!!.pageLayoutReach = it }
val updatedSelectionModel = PdfSelectionModel(pdfLoader)
updateSelectionModel(updatedSelectionModel)
@@ -630,7 +641,7 @@
// app that owns it has been killed by the system. We will still recover,
// but log this.
viewState.set(ViewState.ERROR)
- onLoadDocumentError(e)
+ showLoadingErrorView(e)
}
}
}
@@ -654,9 +665,7 @@
}
private fun destroyContentModel() {
-
pdfLoader?.cancelAll()
-
paginationModel = null
selectionHandles?.destroy()
@@ -737,6 +746,13 @@
)
}
+ private fun showLoadingErrorView(error: Throwable) {
+ context?.resources?.getString(R.string.error_cannot_open_pdf)?.let {
+ loadingView?.showErrorView(it)
+ }
+ onLoadDocumentError(error)
+ }
+
private fun loadFile(fileUri: Uri) {
Preconditions.checkNotNull(fileUri)
Preconditions.checkArgument(
@@ -759,8 +775,13 @@
try {
validateFileUri(fileUri)
fetchFile(fileUri)
- } catch (e: SecurityException) {
- onLoadDocumentError(e)
+ } catch (error: Exception) {
+ when (error) {
+ is IOException,
+ is SecurityException,
+ is NullPointerException -> showLoadingErrorView(error)
+ else -> throw error
+ }
}
if (localUri != null && localUri != fileUri) {
annotationButton?.hide()
@@ -787,7 +808,7 @@
}
override fun failed(thrown: Throwable) {
- onLoadDocumentError(thrown)
+ showLoadingErrorView(thrown)
}
override fun progress(progress: Float) {}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
index 3cdd35d..ea847f0 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
@@ -18,6 +18,8 @@
import android.content.Context;
import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.View;
@@ -27,6 +29,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
+import androidx.core.os.ParcelCompat;
import androidx.pdf.ViewState;
import androidx.pdf.data.Range;
import androidx.pdf.util.PaginationUtils;
@@ -168,6 +171,22 @@
}
}
+ @Nullable
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ return new SavedState(superState, mModel);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ SavedState savedState = (SavedState) state;
+ super.onRestoreInstanceState(((SavedState) state).getSuperState());
+ mModel = savedState.mModel;
+ mPageRangeHandler = new PageRangeHandler(mModel);
+ requestLayout();
+ }
+
/**
* Returns the current viewport in content coordinates
*/
@@ -522,4 +541,24 @@
public boolean isConfigurationChanged() {
return mIsConfigurationChanged;
}
+
+ static class SavedState extends View.BaseSavedState {
+ final PaginationModel mModel;
+
+ SavedState(Parcelable superState, PaginationModel model) {
+ super(superState);
+ mModel = model;
+ }
+
+ SavedState(Parcel source, ClassLoader loader) {
+ super(source);
+ mModel = ParcelCompat.readParcelable(source, loader, PaginationModel.class);
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeParcelable(mModel, flags);
+ }
+ }
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java
index 8407a743..3279dfe 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java
@@ -16,8 +16,11 @@
package androidx.pdf.viewer;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
@@ -57,7 +60,8 @@
* pages are added
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class PaginationModel {
+@SuppressLint("BanParcelableUsage")
+public class PaginationModel implements Parcelable {
/**
* The spacing added before and after each page (the actual space between 2 consecutive pages is
* twice this distance), in pixels.
@@ -87,6 +91,28 @@
mPageSpacingPx = PaginationUtils.getPageSpacingInPixels(context);
}
+ protected PaginationModel(@NonNull Parcel in) {
+ mPageSpacingPx = in.readInt();
+ mMaxPages = in.readInt();
+ mPages = in.createTypedArray(Dimensions.CREATOR);
+ mPageStops = in.createIntArray();
+ mSize = in.readInt();
+ mEstimatedPageHeight = in.readFloat();
+ mAccumulatedPageSize = in.readInt();
+ }
+
+ public static final Creator<PaginationModel> CREATOR = new Creator<PaginationModel>() {
+ @Override
+ public PaginationModel createFromParcel(Parcel in) {
+ return new PaginationModel(in);
+ }
+
+ @Override
+ public PaginationModel[] newArray(int size) {
+ return new PaginationModel[size];
+ }
+ };
+
/**
* Initializes the model.
*
@@ -264,7 +290,6 @@
}
-
/**
* Returns the location of the page in the model.
*
@@ -277,7 +302,7 @@
* maximizes the portion of that view that is visible on the screen
* </ul>
*
- * @param pageNum - index of requested page
+ * @param pageNum - index of requested page
* @param viewArea - the current viewport in content coordinates
* @return - coordinates of the page within this model
*/
@@ -391,4 +416,20 @@
mObservers.clear();
super.finalize();
}
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mPageSpacingPx);
+ dest.writeInt(mMaxPages);
+ dest.writeTypedArray(mPages, flags);
+ dest.writeIntArray(mPageStops);
+ dest.writeInt(mSize);
+ dest.writeFloat(mEstimatedPageHeight);
+ dest.writeInt(mAccumulatedPageSize);
+ }
}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt
index b5fbe27..963a8cd 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt
@@ -228,9 +228,6 @@
"Document not loaded but status " + status.number
)
PdfStatus.PDF_ERROR -> {
- loadingView.showErrorView(
- context.resources.getString(R.string.error_cannot_open_pdf)
- )
handleError(status)
}
PdfStatus.FILE_ERROR,
@@ -259,9 +256,7 @@
override fun setPageDimensions(pageNum: Int, dimensions: Dimensions) {
if (viewState.get() != ViewState.NO_VIEW) {
-
paginatedView.model.addPage(pageNum, dimensions)
-
layoutHandler!!.pageLayoutReach = paginatedView.model.size
if (
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
index 86fed41..79fdec1 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
@@ -17,8 +17,11 @@
package androidx.privacysandbox.ui.integration.testapp
import android.app.Activity
+import android.graphics.Color
+import android.graphics.Typeface
import android.os.Bundle
import android.util.Log
+import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
@@ -59,6 +62,10 @@
}
}
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ getSandboxedSdkViews().forEach { it.addStateChangedListener() }
+ }
+
/** Returns a handle to the already loaded SDK. */
fun getSdkApi(): ISdkApi {
return sdkApi
@@ -119,6 +126,8 @@
val parent = view.parent as ViewGroup
val index = parent.indexOfChild(view)
val textView = TextView(requireActivity())
+ textView.setTypeface(null, Typeface.BOLD_ITALIC)
+ textView.setTextColor(Color.RED)
textView.text = state.throwable.message
requireActivity().runOnUiThread {
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt
index 4a9ccf7..d3eba3b 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt
@@ -104,6 +104,7 @@
} catch (e: Exception) {
Log.w(TAG, "Ad not loaded $e")
}
+ childSandboxedSdkView.addStateChangedListener()
sandboxedSdkViewSet.add(childSandboxedSdkView)
}
}
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeFragment.kt
index 3a3f32b..166d109 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeFragment.kt
@@ -67,7 +67,6 @@
}
private fun loadResizableBannerAd() {
- resizableBannerView.addStateChangedListener()
loadBannerAd(
currentAdType,
currentMediationOption,
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollFragment.kt
index e49be51..2d38fe7 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollFragment.kt
@@ -61,7 +61,6 @@
}
private fun loadBottomBannerAd() {
- bottomBannerView.addStateChangedListener()
bottomBannerView.layoutParams =
inflatedView.findViewById<LinearLayout>(R.id.bottom_banner_container).layoutParams
requireActivity().runOnUiThread {
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt
index 5115d6f..0deb550 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt
@@ -22,8 +22,8 @@
import androidx.sqlite.SQLiteConnection
import java.util.concurrent.Callable
import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
/**
* A LiveData implementation that closely works with [InvalidationTracker] to implement database
@@ -53,6 +53,17 @@
private val computing = AtomicBoolean(false)
private val registeredObserver = AtomicBoolean(false)
+ private val launchContext =
+ if (database.inCompatibilityMode()) {
+ if (inTransaction) {
+ database.getTransactionContext()
+ } else {
+ database.getQueryContext()
+ }
+ } else {
+ EmptyCoroutineContext
+ }
+
private suspend fun refresh() {
if (registeredObserver.compareAndSet(false, true)) {
database.invalidationTracker.subscribe(
@@ -105,7 +116,7 @@
val isActive = hasActiveObservers()
if (invalid.compareAndSet(false, true)) {
if (isActive) {
- database.getCoroutineScope().launch { refresh() }
+ database.getCoroutineScope().launch(launchContext) { refresh() }
}
}
}
@@ -115,7 +126,7 @@
override fun onActive() {
super.onActive()
container.onActive(this)
- database.getCoroutineScope().launch { refresh() }
+ database.getCoroutineScope().launch(launchContext) { refresh() }
}
override fun onInactive() {
@@ -132,13 +143,7 @@
private val callableFunction: Callable<T?>
) : RoomTrackingLiveData<T>(database, container, inTransaction, tableNames) {
override suspend fun compute(): T? {
- val queryContext =
- if (inTransaction) {
- database.getTransactionContext()
- } else {
- database.getQueryContext()
- }
- return withContext(queryContext) { callableFunction.call() }
+ return callableFunction.call()
}
}
diff --git a/settings.gradle b/settings.gradle
index 988cca5..ca5d8f6 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -29,7 +29,12 @@
classpath("com.gradle:develocity-gradle-plugin:3.18")
classpath("com.gradle:common-custom-user-data-gradle-plugin:2.0.1")
classpath("androidx.build.gradle.gcpbuildcache:gcpbuildcache:1.0.0-beta10")
- classpath("com.android.settings:com.android.settings.gradle.plugin:8.7.0-alpha02")
+ def agpOverride = System.getenv("GRADLE_PLUGIN_VERSION")
+ if (agpOverride != null) {
+ classpath("com.android.settings:com.android.settings.gradle.plugin:$agpOverride")
+ } else {
+ classpath("com.android.settings:com.android.settings.gradle.plugin:8.7.0-alpha02")
+ }
}
}
diff --git a/wear/compose/compose-foundation/api/current.txt b/wear/compose/compose-foundation/api/current.txt
index ea38e1a..596925c 100644
--- a/wear/compose/compose-foundation/api/current.txt
+++ b/wear/compose/compose-foundation/api/current.txt
@@ -388,8 +388,10 @@
public sealed interface LazyColumnLayoutInfo {
method public int getTotalItemsCount();
+ method public long getViewportSize();
method public java.util.List<androidx.wear.compose.foundation.lazy.LazyColumnVisibleItemInfo> getVisibleItems();
property public abstract int totalItemsCount;
+ property public abstract long viewportSize;
property public abstract java.util.List<androidx.wear.compose.foundation.lazy.LazyColumnVisibleItemInfo> visibleItems;
}
diff --git a/wear/compose/compose-foundation/api/restricted_current.txt b/wear/compose/compose-foundation/api/restricted_current.txt
index ea38e1a..596925c 100644
--- a/wear/compose/compose-foundation/api/restricted_current.txt
+++ b/wear/compose/compose-foundation/api/restricted_current.txt
@@ -388,8 +388,10 @@
public sealed interface LazyColumnLayoutInfo {
method public int getTotalItemsCount();
+ method public long getViewportSize();
method public java.util.List<androidx.wear.compose.foundation.lazy.LazyColumnVisibleItemInfo> getVisibleItems();
property public abstract int totalItemsCount;
+ property public abstract long viewportSize;
property public abstract java.util.List<androidx.wear.compose.foundation.lazy.LazyColumnVisibleItemInfo> visibleItems;
}
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfoTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfoTest.kt
new file mode 100644
index 0000000..f07a6b7
--- /dev/null
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfoTest.kt
@@ -0,0 +1,196 @@
+/*
+ * 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.wear.compose.foundation.lazy
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class LazyColumnLayoutInfoTest {
+ @get:Rule val rule = createComposeRule()
+
+ private var itemSizePx: Int = 50
+ private var itemSizeDp: Dp = Dp.Infinity
+
+ @Before
+ fun before() {
+ with(rule.density) { itemSizeDp = itemSizePx.toDp() }
+ }
+
+ @Test
+ fun visibleItemsAreCorrect() {
+ lateinit var state: LazyColumnState
+
+ rule.setContent {
+ LazyColumn(
+ state = rememberLazyColumnState().also { state = it },
+ // Viewport take 4 items, item 0 is exactly above the center and there is space for
+ // two more items below the center line.
+ modifier = Modifier.requiredSize(itemSizeDp * 4f),
+ verticalArrangement = Arrangement.spacedBy(0.dp)
+ ) {
+ items((0..5).toList()) { Box(Modifier.requiredSize(itemSizeDp)) }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.viewportSize.height).isEqualTo(itemSizePx * 4)
+ // Start offset compensates for the layout where the first item is exactly above the
+ // center line.
+ state.layoutInfo.assertVisibleItems(count = 3, startOffset = itemSizePx)
+ }
+ }
+
+ @Test
+ fun visibleItemsAreCorrectWithSpacing() {
+ lateinit var state: LazyColumnState
+
+ rule.setContent {
+ LazyColumn(
+ state = rememberLazyColumnState().also { state = it },
+ // Viewport take 4 items, item 0 is exactly above the center and there is space for
+ // two more items below the center line.
+ modifier = Modifier.requiredSize(itemSizeDp * 4f),
+ verticalArrangement = Arrangement.spacedBy(itemSizeDp),
+ ) {
+ items((0..5).toList()) { Box(Modifier.requiredSize(itemSizeDp)) }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.viewportSize.height).isEqualTo(itemSizePx * 4)
+ // Start offset compensates for the layout where the first item is exactly above the
+ // center line.
+ state.layoutInfo.assertVisibleItems(
+ count = 2,
+ spacing = itemSizePx,
+ startOffset = itemSizePx
+ )
+ }
+ }
+
+ @Test
+ fun visibleItemsAreObservableWhenResize() {
+ lateinit var state: LazyColumnState
+ var size by mutableStateOf(itemSizeDp * 2)
+ var currentInfo: LazyColumnLayoutInfo? = null
+ @Composable
+ fun observingFun() {
+ currentInfo = state.layoutInfo
+ }
+ rule.setContent {
+ LazyColumn(
+ state = rememberLazyColumnState().also { state = it },
+ modifier = Modifier.requiredSize(itemSizeDp * 4f)
+ ) {
+ item { Box(Modifier.requiredSize(size)) }
+ }
+ observingFun()
+ }
+
+ rule.runOnIdle {
+ assertThat(currentInfo).isNotNull()
+ currentInfo!!.assertVisibleItems(count = 1, expectedSize = itemSizePx * 2)
+ currentInfo = null
+ size = itemSizeDp
+ }
+
+ rule.runOnIdle {
+ assertThat(currentInfo).isNotNull()
+ currentInfo!!.assertVisibleItems(count = 1, expectedSize = itemSizePx)
+ }
+ }
+
+ @Test
+ fun totalCountIsCorrect() {
+ var count by mutableStateOf(10)
+ lateinit var state: LazyColumnState
+ rule.setContent {
+ LazyColumn(state = rememberLazyColumnState().also { state = it }) {
+ items((0 until count).toList()) { Box(Modifier.requiredSize(10.dp)) }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.totalItemsCount).isEqualTo(10)
+ count = 20
+ }
+
+ rule.runOnIdle { assertThat(state.layoutInfo.totalItemsCount).isEqualTo(20) }
+ }
+
+ @Test
+ fun viewportOffsetsAndSizeAreCorrect() {
+ val sizePx = 45
+ val sizeDp = with(rule.density) { sizePx.toDp() }
+ lateinit var state: LazyColumnState
+ rule.setContent {
+ LazyColumn(
+ Modifier.height(sizeDp).width(sizeDp * 2),
+ state = rememberLazyColumnState().also { state = it }
+ ) {
+ items((0..3).toList()) { Box(Modifier.requiredSize(sizeDp)) }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.viewportSize).isEqualTo(IntSize(sizePx * 2, sizePx))
+ }
+ }
+
+ private fun LazyColumnLayoutInfo.assertVisibleItems(
+ count: Int,
+ startIndex: Int = 0,
+ startOffset: Int = 0,
+ expectedSize: Int = itemSizePx,
+ spacing: Int = 0
+ ) {
+ assertThat(visibleItems.size).isEqualTo(count)
+ var currentIndex = startIndex
+ var currentOffset = startOffset
+ visibleItems.forEach {
+ assertThat(it.index).isEqualTo(currentIndex)
+ assertWithMessage("Offset of item $currentIndex")
+ .that(it.offset)
+ .isEqualTo(currentOffset)
+ assertThat(it.height).isEqualTo(expectedSize)
+ currentIndex++
+ currentOffset += it.height + spacing
+ }
+ }
+}
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfo.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfo.kt
index c5002a0..14fc4c4 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfo.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfo.kt
@@ -16,6 +16,8 @@
package androidx.wear.compose.foundation.lazy
+import androidx.compose.ui.unit.IntSize
+
/**
* Scroll progress of an item in a [LazyColumn] before any modifications to the item's height are
* applied (using [LazyColumnItemScope.transformedHeight] modifier).
@@ -44,21 +46,26 @@
sealed interface LazyColumnVisibleItemInfo {
/** The index of the item in the underlying data source. */
val index: Int
+
/** The offset of the item from the start of the visible area. */
val offset: Int
+
/** The height of the item after applying any height changes. */
val height: Int
+
/** The scroll progress of the item, indicating its position within the visible area. */
val scrollProgress: LazyColumnItemScrollProgress
}
/** Holds the layout information for a [LazyColumn]. */
sealed interface LazyColumnLayoutInfo {
+
/** A list of [LazyColumnVisibleItemInfo] objects representing the visible items in the list. */
val visibleItems: List<LazyColumnVisibleItemInfo>
/** The total count of items passed to [LazyColumn]. */
val totalItemsCount: Int
- // TODO: b/352686661 - Expose more properties related to layout.
+ /** The size of the viewport in pixels. */
+ val viewportSize: IntSize
}
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt
index 3f316f2..1bd6f3e 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt
@@ -17,6 +17,7 @@
package androidx.wear.compose.foundation.lazy
import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.unit.IntSize
/** The result of the measure pass of the [LazyColumn]. */
internal class LazyColumnMeasureResult(
@@ -32,4 +33,8 @@
override val visibleItems: List<LazyColumnVisibleItemInfo>,
/** see [LazyColumnLayoutInfo.totalItemsCount] */
override val totalItemsCount: Int,
-) : LazyColumnLayoutInfo, MeasureResult by measureResult
+) : LazyColumnLayoutInfo, MeasureResult by measureResult {
+ /** see [LazyColumnLayoutInfo.viewportSize] */
+ override val viewportSize: IntSize
+ get() = IntSize(width = width, height = height)
+}
diff --git a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/dialog/DialogTest.kt b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/dialog/DialogTest.kt
index bd54d7f..9be2bf8 100644
--- a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/dialog/DialogTest.kt
+++ b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/dialog/DialogTest.kt
@@ -385,32 +385,6 @@
rule.onNodeWithTag(TEST_TAG).performTouchInput({ swipeRight() })
rule.onNodeWithText(dismissedText).assertExists()
}
-
- @Test
- fun calls_ondismissrequest_when_dialog_becomes_hidden() {
- val show = mutableStateOf(true)
- var dismissed = false
- rule.setContentWithTheme {
- Box {
- Dialog(
- showDialog = show.value,
- onDismissRequest = { dismissed = true },
- ) {
- Alert(
- icon = {},
- title = {},
- message = { Text("Text", modifier = Modifier.testTag(TEST_TAG)) },
- content = {},
- )
- }
- }
- }
- rule.waitForIdle()
- show.value = false
-
- rule.waitForIdle()
- assert(dismissed)
- }
}
class DialogContentSizeAndPositionTest {
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/dialog/Dialog.android.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/dialog/Dialog.android.kt
index 697583e..b07f490 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/dialog/Dialog.android.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/dialog/Dialog.android.kt
@@ -233,17 +233,18 @@
transitionState.targetState = DialogVisibility.Hide
}
}
- }
- }
- LaunchedEffect(transitionState.currentState) {
- if (
- pendingOnDismissCall &&
- transitionState.currentState == DialogVisibility.Hide &&
- transitionState.isIdle
- ) {
- // After the outro animation, leave the dialog & reset alpha/scale transitions.
- onDismissRequest()
- pendingOnDismissCall = false
+
+ LaunchedEffect(transitionState.currentState) {
+ if (
+ pendingOnDismissCall &&
+ transitionState.currentState == DialogVisibility.Hide &&
+ transitionState.isIdle
+ ) {
+ // After the outro animation, leave the dialog & reset alpha/scale transitions.
+ onDismissRequest()
+ pendingOnDismissCall = false
+ }
+ }
}
}
}
diff --git a/wear/compose/compose-material3/benchmark/src/androidTest/java/androidx/wear/compose/material3/benchmark/ProgressIndicatorBenchmark.kt b/wear/compose/compose-material3/benchmark/src/androidTest/java/androidx/wear/compose/material3/benchmark/ProgressIndicatorBenchmark.kt
new file mode 100644
index 0000000..e9e624a
--- /dev/null
+++ b/wear/compose/compose-material3/benchmark/src/androidTest/java/androidx/wear/compose/material3/benchmark/ProgressIndicatorBenchmark.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.wear.compose.material3.benchmark
+
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkToFirstPixel
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.wear.compose.material3.CircularProgressIndicator
+import androidx.wear.compose.material3.MaterialTheme
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class ProgressIndicatorBenchmark {
+ @get:Rule val benchmarkRule = ComposeBenchmarkRule()
+
+ private val testCaseFactory = { ProgressIndicatorTestCase() }
+
+ @Test
+ fun first_pixel() {
+ benchmarkRule.benchmarkToFirstPixel(testCaseFactory)
+ }
+}
+
+internal class ProgressIndicatorTestCase : LayeredComposeTestCase() {
+ @Composable
+ override fun MeasuredContent() {
+ CircularProgressIndicator(progress = { 0.5f })
+ }
+
+ @Composable
+ override fun ContentWrappers(content: @Composable () -> Unit) {
+ MaterialTheme { content() }
+ }
+}
diff --git a/wear/compose/compose-navigation/build.gradle b/wear/compose/compose-navigation/build.gradle
index bb35fa2..1e61bdbc 100644
--- a/wear/compose/compose-navigation/build.gradle
+++ b/wear/compose/compose-navigation/build.gradle
@@ -45,7 +45,6 @@
androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
- androidTestImplementation(project(":navigation:navigation-common"))
androidTestImplementation(libs.testRunner)
androidTestImplementation(project(":wear:compose:compose-material"))
androidTestImplementation(project(":wear:compose:compose-navigation-samples"))
diff --git a/wear/wear_sdk/README.txt b/wear/wear_sdk/README.txt
index 26122f9..224156b 100644
--- a/wear/wear_sdk/README.txt
+++ b/wear/wear_sdk/README.txt
@@ -5,5 +5,5 @@
"preinstalled on WearOS devices."
gerrit source: "vendor/google_clockwork/sdk/lib"
API version: 35.1
-Build ID: 12239970
-Last updated: Fri Aug 16 06:57:41 PM UTC 2024
+Build ID: 12295646
+Last updated: Thu Aug 29 08:37:30 PM UTC 2024
diff --git a/wear/wear_sdk/wear-sdk.jar b/wear/wear_sdk/wear-sdk.jar
index 36f61d4..43b434d 100644
--- a/wear/wear_sdk/wear-sdk.jar
+++ b/wear/wear_sdk/wear-sdk.jar
Binary files differ