Merge "Fix missing invalidations while movable content is moving" into androidx-main
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/Release.kt b/buildSrc/private/src/main/kotlin/androidx/build/Release.kt
index 46c80b0..a6afb13 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/Release.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/Release.kt
@@ -88,7 +88,9 @@
onRegister = {
}
)
- finalizedBy(verifyTask)
+ if (!isPresubmitBuild()) {
+ finalizedBy(verifyTask)
+ }
}
/**
@@ -122,7 +124,7 @@
include(inclusion)
}
}
- verifyTask.get().addFile(fromDir)
+ verifyTask.get().addFile(File(fromDir, "${artifact.version}"))
}
/**
* Config action that configures the task when necessary.
@@ -396,21 +398,27 @@
@TaskAction
fun execute() {
val missingFiles = mutableListOf<String>()
+ val emptyDirs = mutableListOf<String>()
filesToVerify.forEach { file ->
if (!file.exists()) {
missingFiles.add(file.path)
+ } else {
+ if (file.isDirectory) {
+ if (file.listFiles().isEmpty()) {
+ emptyDirs.add(file.path)
+ }
+ }
}
}
- if (missingFiles.isNotEmpty()) {
- val checkedFilesString = filesToVerify.map {
- it -> it.toString()
- }.reduce {
- acc, s -> "$acc, $s"
- }
- val missingFileString = missingFiles.reduce { acc, s -> "$acc, $s" }
+ if (missingFiles.isNotEmpty() || emptyDirs.isNotEmpty()) {
+ val checkedFilesString = filesToVerify.toString()
+ val missingFileString = missingFiles.toString()
+ val emptyDirsString = emptyDirs.toString()
throw FileNotFoundException(
- "GMavenZip file missing: $missingFileString. Checked files: $checkedFilesString"
+ "GMavenZip ${missingFiles.size} missing files: $missingFileString, " +
+ "${emptyDirs.size} empty dirs: $emptyDirsString. " +
+ "Checked files: $checkedFilesString"
)
}
}
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
index 7bbc024..02991ed 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
@@ -58,7 +58,7 @@
@Test
fun previewSizeAreaIsWithinMaxPreviewArea() {
// Act & Assert
- val previewSize = displayInfoManager.previewSize
+ val previewSize = displayInfoManager.getPreviewSize()
assertTrue("$previewSize has larger area than 1920 * 1080",
previewSize.width * previewSize.height <= 1920 * 1080)
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
index 91b1c4f..5ef866d 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
@@ -129,7 +129,7 @@
if (captureType == CaptureType.PREVIEW) {
mutableConfig.insertOption(
ImageOutputConfig.OPTION_MAX_RESOLUTION,
- displayInfoManager.previewSize
+ displayInfoManager.getPreviewSize()
)
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
index c8a98dc0..e8e8998 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
@@ -30,7 +30,10 @@
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.CameraMetadata
import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
+import androidx.camera.camera2.pipe.integration.compat.workaround.ExtraSupportedSurfaceCombinationsContainer
import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
+import androidx.camera.camera2.pipe.integration.compat.workaround.ResolutionCorrector
+import androidx.camera.camera2.pipe.integration.impl.DisplayInfoManager
import androidx.camera.core.impl.AttachedSurfaceInfo
import androidx.camera.core.impl.EncoderProfilesProxy
import androidx.camera.core.impl.ImageFormatConstants
@@ -76,6 +79,10 @@
private val displayManager: DisplayManager =
(context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager)
private val streamConfigurationMapCompat = getStreamConfigurationMapCompat()
+ private val extraSupportedSurfaceCombinationsContainer =
+ ExtraSupportedSurfaceCombinationsContainer()
+ private val displayInfoManager = DisplayInfoManager(context)
+ private val resolutionCorrector = ResolutionCorrector()
init {
checkCapabilities()
@@ -124,8 +131,10 @@
): SurfaceConfig {
val maxOutputSizeForConcurrentMode = if (isConcurrentCameraModeOn)
getMaxOutputSizeByFormat(imageFormat) else null
- return SurfaceConfig.transformSurfaceConfig(isConcurrentCameraModeOn,
- imageFormat, size, surfaceSizeDefinition, maxOutputSizeForConcurrentMode)
+ return SurfaceConfig.transformSurfaceConfig(
+ isConcurrentCameraModeOn,
+ imageFormat, size, surfaceSizeDefinition, maxOutputSizeForConcurrentMode
+ )
}
/**
@@ -182,8 +191,12 @@
// Collect supported output sizes for all use cases
for (index in useCasesPriorityOrder) {
- val supportedOutputSizes: List<Size> =
+ var supportedOutputSizes: List<Size> =
newUseCaseConfigsSupportedSizeMap[newUseCaseConfigs[index]]!!
+ supportedOutputSizes = resolutionCorrector.insertOrPrioritize(
+ SurfaceConfig.getConfigType(newUseCaseConfigs[index].inputFormat),
+ supportedOutputSizes
+ )
supportedOutputSizesList.add(supportedOutputSizes)
}
// Get all possible size arrangements
@@ -223,9 +236,11 @@
for (useCaseConfig in newUseCaseConfigs) {
suggestedStreamSpecMap.put(
useCaseConfig,
- StreamSpec.builder(possibleSizeList[useCasesPriorityOrder.indexOf(
- newUseCaseConfigs.indexOf(useCaseConfig)
- )]).build()
+ StreamSpec.builder(
+ possibleSizeList[useCasesPriorityOrder.indexOf(
+ newUseCaseConfigs.indexOf(useCaseConfig)
+ )]
+ ).build()
)
}
break
@@ -250,14 +265,19 @@
* Refresh Preview Size based on current display configurations.
*/
private fun refreshPreviewSize() {
- val previewSize: Size = calculatePreviewSize()
- surfaceSizeDefinition = SurfaceSizeDefinition.create(
- surfaceSizeDefinition.analysisSize,
- surfaceSizeDefinition.s720pSize,
- previewSize,
- surfaceSizeDefinition.s1440pSize,
- surfaceSizeDefinition.recordSize
- )
+ displayInfoManager.refresh()
+ if (!::surfaceSizeDefinition.isInitialized) {
+ generateSurfaceSizeDefinition()
+ } else {
+ val previewSize: Size = displayInfoManager.getPreviewSize()
+ surfaceSizeDefinition = SurfaceSizeDefinition.create(
+ surfaceSizeDefinition.analysisSize,
+ surfaceSizeDefinition.s720pSize,
+ previewSize,
+ surfaceSizeDefinition.s1440pSize,
+ surfaceSizeDefinition.recordSize
+ )
+ }
}
/**
@@ -284,13 +304,15 @@
isRawSupported, isBurstCaptureSupported
)
)
- // TODO(b/246609101): ExtraSupportedSurfaceCombinationsQuirk is supposed to be here to add additional
- // surface combinations to the list
+ surfaceCombinations.addAll(
+ extraSupportedSurfaceCombinationsContainer[cameraId, hardwareLevel]
+ )
}
private fun generateConcurrentSupportedCombinationList() {
concurrentSurfaceCombinations.addAll(
- GuaranteedConfigurationsUtil.generateConcurrentSupportedCombinationList())
+ GuaranteedConfigurationsUtil.generateConcurrentSupportedCombinationList()
+ )
}
/**
@@ -304,10 +326,12 @@
// Same for s1440p.
val s720pSize = Size(1280, 720)
val s1440pSize = Size(1920, 1440)
- val previewSize: Size = calculatePreviewSize()
+ val previewSize: Size = displayInfoManager.getPreviewSize()
val recordSize: Size = getRecordSize()
- surfaceSizeDefinition = SurfaceSizeDefinition.create(vgaSize, s720pSize, previewSize,
- s1440pSize, recordSize)
+ surfaceSizeDefinition = SurfaceSizeDefinition.create(
+ vgaSize, s720pSize, previewSize,
+ s1440pSize, recordSize
+ )
}
/**
@@ -337,7 +361,7 @@
private fun getStreamConfigurationMapCompat(): StreamConfigurationMapCompat {
val map = cameraMetadata[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]
?: throw IllegalArgumentException("Cannot retrieve SCALER_STREAM_CONFIGURATION_MAP")
- return StreamConfigurationMapCompat(map, OutputSizesCorrector(cameraMetadata))
+ return StreamConfigurationMapCompat(map, OutputSizesCorrector(cameraMetadata, map))
}
/**
@@ -347,7 +371,9 @@
* @return Maximum supported video size.
*/
private fun getRecordSizeFromStreamConfigurationMapCompat(): Size {
- val videoSizeArr = streamConfigurationMapCompat.getOutputSizes(
+ val map: StreamConfigurationMap =
+ streamConfigurationMapCompat.toStreamConfigurationMap()
+ val videoSizeArr = map.getOutputSizes(
MediaRecorder::class.java
) ?: return RESOLUTION_480P
Arrays.sort(videoSizeArr, CompareSizesByArea(true))
@@ -394,31 +420,6 @@
}
/**
- * Calculates the size for preview. If the max size is larger than 1080p, use 1080p.
- */
- @SuppressWarnings("deprecation")
- /* getRealSize */
- private fun calculatePreviewSize(): Size {
- val displaySize = Point()
- val display: Display = getMaxSizeDisplay()
- display.getRealSize(displaySize)
- var displayViewSize: Size
- displayViewSize = if (displaySize.x > displaySize.y) {
- Size(displaySize.x, displaySize.y)
- } else {
- Size(displaySize.y, displaySize.x)
- }
- if (displayViewSize.width * displayViewSize.height
- > RESOLUTION_1080P.width * RESOLUTION_1080P.height
- ) {
- displayViewSize = RESOLUTION_1080P
- }
- // TODO(b/245619094): Use ExtraCroppingQuirk to potentially override this with select
- // resolution
- return displayViewSize
- }
-
- /**
* Retrieves the display which has the max size among all displays.
*/
private fun getMaxSizeDisplay(): Display {
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/AspectRatioLegacyApi21Quirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/AspectRatioLegacyApi21Quirk.kt
new file mode 100644
index 0000000..c46b711
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/AspectRatioLegacyApi21Quirk.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.quirk
+
+import android.hardware.camera2.CameraCharacteristics
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.integration.compat.workaround.TargetAspectRatio
+import androidx.camera.core.impl.Quirk
+
+/**
+ *
+ * QuirkSummary
+ * Bug Id: b/128924712
+ * Description: Quirk that produces stretched use cases on all the legacy API 21 devices. If
+ * the device is LEGACY + Android 5.0, then return the same aspect ratio as maximum JPEG
+ * resolution. The Camera2 LEGACY mode API always sends the HAL a configure call with the
+ * same aspect ratio as the maximum JPEG resolution, and do the cropping/scaling before
+ * returning the output. There is a bug because of a flipped scaling factor in the
+ * intermediate texture transform matrix, and it was fixed in L MR1.
+ * Device(s): All the legacy API 21 devices
+ * @see androidx.camera.camera2.internal.compat.workaround.TargetAspectRatio
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class AspectRatioLegacyApi21Quirk : Quirk {
+ /**
+ * Get the corrected aspect ratio.
+ */
+ @TargetAspectRatio.Ratio
+ fun getCorrectedAspectRatio(): Int {
+ return TargetAspectRatio.RATIO_MAX_JPEG
+ }
+
+ companion object {
+ fun load(cameraMetadata: CameraMetadata): Boolean {
+ val level: Int? = cameraMetadata[CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL]
+ return level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY &&
+ Build.VERSION.SDK_INT == 21
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt
index c0c7158..08310aa 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt
@@ -45,6 +45,9 @@
if (AfRegionFlipHorizontallyQuirk.isEnabled(cameraMetadata)) {
quirks.add(AfRegionFlipHorizontallyQuirk())
}
+ if (AspectRatioLegacyApi21Quirk.load(cameraMetadata)) {
+ quirks.add(AspectRatioLegacyApi21Quirk())
+ }
if (CamcorderProfileResolutionQuirk.isEnabled(cameraMetadata)) {
quirks.add(CamcorderProfileResolutionQuirk(streamConfigurationMapCompat))
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/DeviceQuirksLoader.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/DeviceQuirksLoader.kt
index 4508e2f..24cdf62 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/DeviceQuirksLoader.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/DeviceQuirksLoader.kt
@@ -55,9 +55,18 @@
if (ExcludedSupportedSizesQuirk.load()) {
quirks.add(ExcludedSupportedSizesQuirk())
}
+ if (ExtraCroppingQuirk.load()) {
+ quirks.add(ExtraCroppingQuirk())
+ }
if (ExtraSupportedOutputSizeQuirk.load()) {
quirks.add(ExtraSupportedOutputSizeQuirk())
}
+ if (ExtraSupportedSurfaceCombinationsQuirk.load()) {
+ quirks.add(ExtraSupportedSurfaceCombinationsQuirk())
+ }
+ if (Nexus4AndroidLTargetAspectRatioQuirk.load()) {
+ quirks.add(Nexus4AndroidLTargetAspectRatioQuirk())
+ }
if (PreviewPixelHDRnetQuirk.isEnabled()) {
quirks.add(PreviewPixelHDRnetQuirk())
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraCroppingQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraCroppingQuirk.kt
new file mode 100644
index 0000000..f391079
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraCroppingQuirk.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.quirk
+
+import android.os.Build
+import android.util.Range
+import android.util.Size
+import androidx.annotation.RequiresApi
+import androidx.camera.core.impl.Quirk
+import androidx.camera.core.impl.SurfaceConfig.ConfigType
+
+/**
+ * Quirk that requires specific resolutions as the workaround.
+ *
+ * QuirkSummary
+ * Bug Id: 190203334
+ * Description: The symptom of these devices is that the output of one or many streams,
+ * including PRIV, JPEG and/or YUV, can have an unintended 25% crop, and the
+ * cropped image is stretched to fill the Surface, which results in a distorted
+ * output. The streams can also have an unintended 25% double crop, in which
+ * case the stretched image will not be distorted, but the FOV is smaller than
+ * it should be. The behavior is inconsistent in a way that the extra cropping
+ * depends on the resolution of the streams. The existence of the issue also
+ * depends on API level and/or build number. See discussion in
+ * go/samsung-camera-distortion.
+ * Device(s): Samsung Galaxy Tab A (2016) SM-T580, Samsung Galaxy J7 (2016) SM-J710MN,
+ * Samsung Galaxy A3 (2017) SM-A320FL, Samsung Galaxy J5 Prime SM-G570M,
+ * Samsung Galaxy J7 Prime SM-G610F, Samsung Galaxy J7 Prime SM-G610M
+ */
+@RequiresApi(21)
+class ExtraCroppingQuirk : Quirk {
+ /**
+ * Get a verified resolution that is guaranteed to work.
+ *
+ * The selected resolution have been manually tested by CameraX team. It is known to
+ * work for the given device/stream.
+ *
+ * @return null if no resolution provided, in which case the calling code should fallback to
+ * user provided target resolution.
+ */
+ fun getVerifiedResolution(configType: ConfigType): Size? {
+ return if (isSamsungDistortion) {
+ // The following resolutions are needed for both the front and the back camera.
+ when (configType) {
+ ConfigType.PRIV -> Size(1920, 1080)
+ ConfigType.YUV -> Size(1280, 720)
+ ConfigType.JPEG -> Size(3264, 1836)
+ else -> null
+ }
+ } else null
+ }
+
+ companion object {
+ private val SAMSUNG_DISTORTION_MODELS_TO_API_LEVEL_MAP: MutableMap<String, Range<Int>?> =
+ mutableMapOf(
+ "SM-T580" to null,
+ "SM-J710MN" to Range(21, 26),
+ "SM-A320FL" to null,
+ "SM-G570M" to null,
+ "SM-G610F" to null,
+ "SM-G610M" to Range(21, 26)
+ )
+
+ fun load(): Boolean {
+ return isSamsungDistortion
+ }
+
+ /**
+ * Checks for device model with Samsung output distortion bug (b/190203334).
+ */
+ internal val isSamsungDistortion: Boolean
+ get() {
+ val isDeviceModelContained = ("samsung".equals(Build.BRAND, ignoreCase = true) &&
+ SAMSUNG_DISTORTION_MODELS_TO_API_LEVEL_MAP.containsKey(
+ Build.MODEL.uppercase()
+ ))
+ if (!isDeviceModelContained) {
+ return false
+ }
+ val apiLevelRange =
+ SAMSUNG_DISTORTION_MODELS_TO_API_LEVEL_MAP[Build.MODEL.uppercase()]
+ return apiLevelRange?.contains(Build.VERSION.SDK_INT) ?: true
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraSupportedSurfaceCombinationsQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraSupportedSurfaceCombinationsQuirk.kt
new file mode 100644
index 0000000..5a63102
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraSupportedSurfaceCombinationsQuirk.kt
@@ -0,0 +1,352 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.quirk
+
+import android.hardware.camera2.CameraCharacteristics
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.core.impl.Quirk
+import androidx.camera.core.impl.SurfaceCombination
+import androidx.camera.core.impl.SurfaceConfig
+
+/**
+ *
+ * QuirkSummary
+ * Bug Id: b/194149215
+ * Description: Quirk required to include extra supported surface combinations which are
+ * additional to the guaranteed supported configurations. An example is the
+ * Samsung S7's LIMITED-level camera device can support additional YUV/640x480 +
+ * PRIV/PREVIEW + YUV/MAXIMUM combination. Some other Samsung devices can
+ * support additional YUV/640x480 + PRIV/PREVIEW + YUV/MAXIMUM and YUV/640x480 +
+ * YUV/PREVIEW + YUV/MAXIMUM configurations.
+ * Device(s): Some Samsung devices
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+
+class ExtraSupportedSurfaceCombinationsQuirk : Quirk {
+ /**
+ * Returns the extra supported surface combinations for specific camera on the device.
+ */
+ fun getExtraSupportedSurfaceCombinations(
+ cameraId: String,
+ hardwareLevel: Int
+ ): List<SurfaceCombination> {
+ if (isSamsungS7) {
+ return getSamsungS7ExtraCombinations(cameraId)
+ }
+ if (supportExtraFullConfigurationsSamsungDevice()) {
+ return getLimitedDeviceExtraSupportedFullConfigurations(hardwareLevel)
+ }
+ return if (supportExtraLevel3ConfigurationsGoogleDevice()) {
+ listOf(LEVEL_3_LEVEL_PRIV_PRIV_YUV_RAW_CONFIGURATION)
+ } else emptyList()
+ }
+
+ private fun getSamsungS7ExtraCombinations(cameraId: String): List<SurfaceCombination> {
+ val extraCombinations: MutableList<SurfaceCombination> = ArrayList()
+ if (cameraId == "1") {
+ // (YUV, ANALYSIS) + (PRIV, PREVIEW) + (YUV, MAXIMUM)
+ extraCombinations.add(FULL_LEVEL_YUV_PRIV_YUV_CONFIGURATION)
+ }
+ return extraCombinations
+ }
+
+ private fun getLimitedDeviceExtraSupportedFullConfigurations(
+ hardwareLevel: Int
+ ): List<SurfaceCombination> {
+ val extraCombinations: MutableList<SurfaceCombination> = ArrayList()
+ if (hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED) {
+ // (YUV, ANALYSIS) + (PRIV, PREVIEW) + (YUV, MAXIMUM)
+ extraCombinations.add(FULL_LEVEL_YUV_PRIV_YUV_CONFIGURATION)
+ // (YUV, ANALYSIS) + (YUV, PREVIEW) + (YUV, MAXIMUM)
+ extraCombinations.add(FULL_LEVEL_YUV_YUV_YUV_CONFIGURATION)
+ }
+ return extraCombinations
+ }
+
+ companion object {
+ private const val TAG = "ExtraSupportedSurfaceCombinationsQuirk"
+ private val FULL_LEVEL_YUV_PRIV_YUV_CONFIGURATION = createFullYuvPrivYuvConfiguration()
+ private val FULL_LEVEL_YUV_YUV_YUV_CONFIGURATION = createFullYuvYuvYuvConfiguration()
+ private val LEVEL_3_LEVEL_PRIV_PRIV_YUV_RAW_CONFIGURATION =
+ createLevel3PrivPrivYuvRawConfiguration()
+ private val SUPPORT_EXTRA_FULL_CONFIGURATIONS_SAMSUNG_MODELS: Set<String> =
+ setOf(
+ "SM-A515F", // Galaxy A51
+ "SM-A515U", // Galaxy A51
+ "SM-A515U1", // Galaxy A51
+ "SM-A515W", // Galaxy A51
+ "SM-S515DL", // Galaxy A51
+ "SC-54A", // Galaxy A51 5G
+ "SCG07", // Galaxy A51 5G
+ "SM-A5160", // Galaxy A51 5G
+ "SM-A516B", // Galaxy A51 5G
+ "SM-A516N", // Galaxy A51 5G
+ "SM-A516U", // Galaxy A51 5G
+ "SM-A516U1", // Galaxy A51 5G
+ "SM-A516V", // Galaxy A51 5G
+ "SM-A715F", // Galaxy A71
+ "SM-A715W", // Galaxy A71
+ "SM-A7160", // Galaxy A71 5G
+ "SM-A716B", // Galaxy A71 5G
+ "SM-A716U", // Galaxy A71 5G
+ "SM-A716U1", // Galaxy A71 5G
+ "SM-A716V", // Galaxy A71 5G
+ "SM-A8050", // Galaxy A80
+ "SM-A805F", // Galaxy A80
+ "SM-A805N", // Galaxy A80
+ "SCV44", // Galaxy Fold
+ "SM-F9000", // Galaxy Fold
+ "SM-F900F", // Galaxy Fold
+ "SM-F900U", // Galaxy Fold
+ "SM-F900U1", // Galaxy Fold
+ "SM-F900W", // Galaxy Fold
+ "SM-F907B", // Galaxy Fold 5G
+ "SM-F907N", // Galaxy Fold 5G
+ "SM-N970F", // Galaxy Note10
+ "SM-N9700", // Galaxy Note10
+ "SM-N970U", // Galaxy Note10
+ "SM-N970U1", // Galaxy Note10
+ "SM-N970W", // Galaxy Note10
+ "SM-N971N", // Galaxy Note10 5G
+ "SM-N770F", // Galaxy Note10 Lite
+ "SC-01M", // Galaxy Note10+
+ "SCV45", // Galaxy Note10+
+ "SM-N9750", // Galaxy Note10+
+ "SM-N975C", // Galaxy Note10+
+ "SM-N975U", // Galaxy Note10+
+ "SM-N975U1", // Galaxy Note10+
+ "SM-N975W", // Galaxy Note10+
+ "SM-N975F", // Galaxy Note10+
+ "SM-N976B", // Galaxy Note10+ 5G
+ "SM-N976N", // Galaxy Note10+ 5G
+ "SM-N9760", // Galaxy Note10+ 5G
+ "SM-N976Q", // Galaxy Note10+ 5G
+ "SM-N976V", // Galaxy Note10+ 5G
+ "SM-N976U", // Galaxy Note10+ 5G
+ "SM-N9810", // Galaxy Note20 5G
+ "SM-N981N", // Galaxy Note20 5G
+ "SM-N981U", // Galaxy Note20 5G
+ "SM-N981U1", // Galaxy Note20 5G
+ "SM-N981W", // Galaxy Note20 5G
+ "SM-N981B", // Galaxy Note20 5G
+ "SC-53A", // Galaxy Note20 Ultra 5G
+ "SCG06", // Galaxy Note20 Ultra 5G
+ "SM-N9860", // Galaxy Note20 Ultra 5G
+ "SM-N986N", // Galaxy Note20 Ultra 5G
+ "SM-N986U", // Galaxy Note20 Ultra 5G
+ "SM-N986U1", // Galaxy Note20 Ultra 5G
+ "SM-N986W", // Galaxy Note20 Ultra 5G
+ "SM-N986B", // Galaxy Note20 Ultra 5G
+ "SC-03L", // Galaxy S10
+ "SCV41", // Galaxy S10
+ "SM-G973F", // Galaxy S10
+ "SM-G973N", // Galaxy S10
+ "SM-G9730", // Galaxy S10
+ "SM-G9738", // Galaxy S10
+ "SM-G973C", // Galaxy S10
+ "SM-G973U", // Galaxy S10
+ "SM-G973U1", // Galaxy S10
+ "SM-G973W", // Galaxy S10
+ "SM-G977B", // Galaxy S10 5G
+ "SM-G977N", // Galaxy S10 5G
+ "SM-G977P", // Galaxy S10 5G
+ "SM-G977T", // Galaxy S10 5G
+ "SM-G977U", // Galaxy S10 5G
+ "SM-G770F", // Galaxy S10 Lite
+ "SM-G770U1", // Galaxy S10 Lite
+ "SC-04L", // Galaxy S10+
+ "SCV42", // Galaxy S10+
+ "SM-G975F", // Galaxy S10+
+ "SM-G975N", // Galaxy S10+
+ "SM-G9750", // Galaxy S10+
+ "SM-G9758", // Galaxy S10+
+ "SM-G975U", // Galaxy S10+
+ "SM-G975U1", // Galaxy S10+
+ "SM-G975W", // Galaxy S10+
+ "SC-05L", // Galaxy S10+ Olympic Games Edition
+ "SM-G970F", // Galaxy S10e
+ "SM-G970N", // Galaxy S10e
+ "SM-G9700", // Galaxy S10e
+ "SM-G9708", // Galaxy S10e
+ "SM-G970U", // Galaxy S10e
+ "SM-G970U1", // Galaxy S10e
+ "SM-G970W", // Galaxy S10e
+ "SC-51A", // Galaxy S20 5G
+ "SC51Aa", // Galaxy S20 5G
+ "SCG01", // Galaxy S20 5G
+ "SM-G9810", // Galaxy S20 5G
+ "SM-G981N", // Galaxy S20 5G
+ "SM-G981U", // Galaxy S20 5G
+ "SM-G981U1", // Galaxy S20 5G
+ "SM-G981V", // Galaxy S20 5G
+ "SM-G981W", // Galaxy S20 5G
+ "SM-G981B", // Galaxy S20 5G
+ "SCG03", // Galaxy S20 Ultra 5G
+ "SM-G9880", // Galaxy S20 Ultra 5G
+ "SM-G988N", // Galaxy S20 Ultra 5G
+ "SM-G988Q", // Galaxy S20 Ultra 5G
+ "SM-G988U", // Galaxy S20 Ultra 5G
+ "SM-G988U1", // Galaxy S20 Ultra 5G
+ "SM-G988W", // Galaxy S20 Ultra 5G
+ "SM-G988B", // Galaxy S20 Ultra 5G
+ "SC-52A", // Galaxy S20+ 5G
+ "SCG02", // Galaxy S20+ 5G
+ "SM-G9860", // Galaxy S20+ 5G
+ "SM-G986N", // Galaxy S20+ 5G
+ "SM-G986U", // Galaxy S20+ 5G
+ "SM-G986U1", // Galaxy S20+ 5G
+ "SM-G986W", // Galaxy S20+ 5G
+ "SM-G986B", // Galaxy S20+ 5G
+ "SCV47", // Galaxy Z Flip
+ "SM-F7000", // Galaxy Z Flip
+ "SM-F700F", // Galaxy Z Flip
+ "SM-F700N", // Galaxy Z Flip
+ "SM-F700U", // Galaxy Z Flip
+ "SM-F700U1", // Galaxy Z Flip
+ "SM-F700W", // Galaxy Z Flip
+ "SCG04", // Galaxy Z Flip 5G
+ "SM-F7070", // Galaxy Z Flip 5G
+ "SM-F707B", // Galaxy Z Flip 5G
+ "SM-F707N", // Galaxy Z Flip 5G
+ "SM-F707U", // Galaxy Z Flip 5G
+ "SM-F707U1", // Galaxy Z Flip 5G
+ "SM-F707W", // Galaxy Z Flip 5G
+ "SM-F9160", // Galaxy Z Fold2 5G
+ "SM-F916B", // Galaxy Z Fold2 5G
+ "SM-F916N", // Galaxy Z Fold2 5G
+ "SM-F916Q", // Galaxy Z Fold2 5G
+ "SM-F916U", // Galaxy Z Fold2 5G
+ "SM-F916U1", // Galaxy Z Fold2 5G
+ "SM-F916W" // Galaxy Z Fold2 5G
+ )
+ private val SUPPORT_EXTRA_LEVEL_3_CONFIGURATIONS_GOOGLE_MODELS: Set<String> =
+ setOf(
+ "PIXEL 6",
+ "PIXEL 6 PRO",
+ "PIXEL 7",
+ "PIXEL 7 PRO"
+ )
+
+ fun load(): Boolean {
+ return (isSamsungS7 || supportExtraFullConfigurationsSamsungDevice() ||
+ supportExtraLevel3ConfigurationsGoogleDevice())
+ }
+
+ internal val isSamsungS7: Boolean
+ get() = "heroqltevzw".equals(
+ Build.DEVICE,
+ ignoreCase = true
+ ) || "heroqltetmo".equals(
+ Build.DEVICE, ignoreCase = true
+ )
+
+ internal fun supportExtraFullConfigurationsSamsungDevice(): Boolean {
+ if (!"samsung".equals(Build.BRAND, ignoreCase = true)) {
+ return false
+ }
+ val capitalModelName = Build.MODEL.uppercase()
+ return SUPPORT_EXTRA_FULL_CONFIGURATIONS_SAMSUNG_MODELS.contains(capitalModelName)
+ }
+
+ internal fun supportExtraLevel3ConfigurationsGoogleDevice(): Boolean {
+ if (!"google".equals(Build.BRAND, ignoreCase = true)) {
+ return false
+ }
+ val capitalModelName = Build.MODEL.uppercase()
+ return SUPPORT_EXTRA_LEVEL_3_CONFIGURATIONS_GOOGLE_MODELS.contains(capitalModelName)
+ }
+
+ internal fun createFullYuvPrivYuvConfiguration(): SurfaceCombination {
+ // (YUV, ANALYSIS) + (PRIV, PREVIEW) + (YUV, MAXIMUM)
+ val surfaceCombination = SurfaceCombination()
+ surfaceCombination.addSurfaceConfig(
+ SurfaceConfig.create(
+ SurfaceConfig.ConfigType.YUV,
+ SurfaceConfig.ConfigSize.VGA
+ )
+ )
+ surfaceCombination.addSurfaceConfig(
+ SurfaceConfig.create(
+ SurfaceConfig.ConfigType.PRIV,
+ SurfaceConfig.ConfigSize.PREVIEW
+ )
+ )
+ surfaceCombination.addSurfaceConfig(
+ SurfaceConfig.create(
+ SurfaceConfig.ConfigType.YUV,
+ SurfaceConfig.ConfigSize.MAXIMUM
+ )
+ )
+ return surfaceCombination
+ }
+
+ internal fun createFullYuvYuvYuvConfiguration(): SurfaceCombination {
+ // (YUV, ANALYSIS) + (YUV, PREVIEW) + (YUV, MAXIMUM)
+ val surfaceCombination = SurfaceCombination()
+ surfaceCombination.addSurfaceConfig(
+ SurfaceConfig.create(
+ SurfaceConfig.ConfigType.YUV,
+ SurfaceConfig.ConfigSize.VGA
+ )
+ )
+ surfaceCombination.addSurfaceConfig(
+ SurfaceConfig.create(
+ SurfaceConfig.ConfigType.YUV,
+ SurfaceConfig.ConfigSize.PREVIEW
+ )
+ )
+ surfaceCombination.addSurfaceConfig(
+ SurfaceConfig.create(
+ SurfaceConfig.ConfigType.YUV,
+ SurfaceConfig.ConfigSize.MAXIMUM
+ )
+ )
+ return surfaceCombination
+ }
+
+ internal fun createLevel3PrivPrivYuvRawConfiguration(): SurfaceCombination {
+ // (PRIV, PREVIEW) + (PRIV, ANALYSIS) + (YUV, MAXIMUM) + (RAW, MAXIMUM)
+ val surfaceCombination = SurfaceCombination()
+ surfaceCombination.addSurfaceConfig(
+ SurfaceConfig.create(
+ SurfaceConfig.ConfigType.PRIV,
+ SurfaceConfig.ConfigSize.PREVIEW
+ )
+ )
+ surfaceCombination.addSurfaceConfig(
+ SurfaceConfig.create(
+ SurfaceConfig.ConfigType.PRIV,
+ SurfaceConfig.ConfigSize.VGA
+ )
+ )
+ surfaceCombination.addSurfaceConfig(
+ SurfaceConfig.create(
+ SurfaceConfig.ConfigType.YUV,
+ SurfaceConfig.ConfigSize.MAXIMUM
+ )
+ )
+ surfaceCombination.addSurfaceConfig(
+ SurfaceConfig.create(
+ SurfaceConfig.ConfigType.RAW,
+ SurfaceConfig.ConfigSize.MAXIMUM
+ )
+ )
+ return surfaceCombination
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/Nexus4AndroidLTargetAspectRatioQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/Nexus4AndroidLTargetAspectRatioQuirk.kt
new file mode 100644
index 0000000..7147ca8
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/Nexus4AndroidLTargetAspectRatioQuirk.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.quirk
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.integration.compat.workaround.TargetAspectRatio
+import androidx.camera.core.impl.Quirk
+
+/**
+ *
+ * QuirkSummary
+ * Bug Id: b/19606058
+ * Description: Quirk that produces stretched preview on Nexus 4 devices running Android L
+ * (API levels 21 and 22). There is a Camera1/HAL1 issue on the Nexus 4. The preview will be
+ * stretched when configuring a JPEG that doesn't actually have the same aspect ratio as the
+ * maximum JPEG resolution.
+ * Device(s): Google Nexus 4
+ * @see androidx.camera.camera2.internal.compat.workaround.TargetAspectRatio
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+
+class Nexus4AndroidLTargetAspectRatioQuirk : Quirk {
+ /**
+ * Get the corrected aspect ratio.
+ */
+ @TargetAspectRatio.Ratio
+ fun getCorrectedAspectRatio(): Int {
+ return TargetAspectRatio.RATIO_MAX_JPEG
+ }
+
+ companion object {
+ // List of devices with the issue.
+ private val DEVICE_MODELS = listOf(
+ "NEXUS 4" // b/158749159
+ )
+
+ fun load(): Boolean {
+ return "GOOGLE".equals(
+ Build.BRAND,
+ ignoreCase = true
+ ) && Build.VERSION.SDK_INT < 23 && DEVICE_MODELS.contains(
+ Build.MODEL.uppercase()
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/ExtraSupportedSurfaceCombinationsContainer.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/ExtraSupportedSurfaceCombinationsContainer.kt
new file mode 100644
index 0000000..b56f416
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/ExtraSupportedSurfaceCombinationsContainer.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.workaround
+
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks
+import androidx.camera.camera2.pipe.integration.compat.quirk.ExtraSupportedSurfaceCombinationsQuirk
+import androidx.camera.core.impl.SurfaceCombination
+
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class ExtraSupportedSurfaceCombinationsContainer {
+ private val quirk: ExtraSupportedSurfaceCombinationsQuirk? =
+ DeviceQuirks[ExtraSupportedSurfaceCombinationsQuirk::class.java]
+
+ /**
+ * Retrieves the extra surface combinations which can be supported on the device.
+ */
+ operator fun get(cameraId: String, hardwareLevel: Int): List<SurfaceCombination> {
+ return quirk?.getExtraSupportedSurfaceCombinations(cameraId, hardwareLevel) ?: listOf()
+ }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/MaxPreviewSize.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/MaxPreviewSize.kt
new file mode 100644
index 0000000..93a724e
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/MaxPreviewSize.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.workaround
+
+import android.util.Size
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks
+import androidx.camera.camera2.pipe.integration.compat.quirk.ExtraCroppingQuirk
+import androidx.camera.core.impl.SurfaceConfig
+
+/**
+ * Helper class that overrides the maximum preview size used in surface combination check.
+ */
+@RequiresApi(21)
+class MaxPreviewSize constructor(
+ private val extraCroppingQuirk: ExtraCroppingQuirk? =
+ DeviceQuirks[ExtraCroppingQuirk::class.java]
+) {
+
+ /**
+ * Gets the max preview resolution based on the default preview max resolution.
+ *
+ *
+ * If select resolution is larger than the default resolution, return the select
+ * resolution. The select resolution has been manually tested on the device. Otherwise,
+ * return the default max resolution.
+ */
+ fun getMaxPreviewResolution(defaultMaxPreviewResolution: Size): Size {
+ if (extraCroppingQuirk == null) {
+ return defaultMaxPreviewResolution
+ }
+ val selectResolution: Size = extraCroppingQuirk.getVerifiedResolution(
+ SurfaceConfig.ConfigType.PRIV
+ ) ?: return defaultMaxPreviewResolution
+ val isSelectResolutionLarger = (selectResolution.width * selectResolution.height >
+ defaultMaxPreviewResolution.width * defaultMaxPreviewResolution.height)
+ return if (isSelectResolutionLarger) selectResolution else defaultMaxPreviewResolution
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/OutputSizesCorrector.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/OutputSizesCorrector.kt
index a873e6b..723c840 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/OutputSizesCorrector.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/OutputSizesCorrector.kt
@@ -16,31 +16,40 @@
package androidx.camera.camera2.pipe.integration.compat.workaround
+import android.hardware.camera2.params.StreamConfigurationMap
+import android.util.Rational
import android.util.Size
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.CameraMetadata
-import androidx.camera.camera2.pipe.integration.config.CameraScope
-import javax.inject.Inject
+import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
import androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks
import androidx.camera.camera2.pipe.integration.compat.quirk.ExcludedSupportedSizesQuirk
import androidx.camera.camera2.pipe.integration.compat.quirk.ExtraSupportedOutputSizeQuirk
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import androidx.camera.core.impl.utils.AspectRatioUtil
+import androidx.camera.core.impl.utils.CompareSizesByArea
+import java.util.Collections
+import javax.inject.Inject
/**
* Helper class to provide the StreamConfigurationMap output sizes related correction functions.
*
* 1. ExtraSupportedOutputSizeQuirk
* 2. ExcludedSupportedSizesContainer
- * 3. TargetAspectRatio
+ * 3. Nexus4AndroidLTargetAspectRatioQuirk
+ * 4. AspectRatioLegacyApi21Quirk
*/
@CameraScope
@RequiresApi(21)
class OutputSizesCorrector @Inject constructor(
- private val cameraMetadata: CameraMetadata
+ private val cameraMetadata: CameraMetadata,
+ private val streamConfigurationMap: StreamConfigurationMap
) {
private val excludedSupportedSizesQuirk: ExcludedSupportedSizesQuirk? =
DeviceQuirks[ExcludedSupportedSizesQuirk::class.java]
private val extraSupportedOutputSizeQuirk: ExtraSupportedOutputSizeQuirk? =
DeviceQuirks[ExtraSupportedOutputSizeQuirk::class.java]
+ private val targetAspectRatio: TargetAspectRatio = TargetAspectRatio()
/**
* Applies the output sizes related quirks onto the input sizes array.
@@ -132,8 +141,51 @@
* Excludes output sizes by TargetAspectRatio.
*/
private fun excludeOutputSizesByTargetAspectRatioWorkaround(sizes: Array<Size>?): Array<Size>? {
- // TODO(b/245622117): Nexus4AndroidLTargetAspectRatioQuirk and AspectRatioLegacyApi21Quirk
- return sizes
+ if (sizes == null) {
+ return null
+ }
+
+ val targetAspectRatio: Int =
+ targetAspectRatio[
+ cameraMetadata,
+ StreamConfigurationMapCompat(streamConfigurationMap, this)
+ ]
+
+ var ratio: Rational? = null
+
+ when (targetAspectRatio) {
+ TargetAspectRatio.RATIO_4_3 -> ratio =
+ AspectRatioUtil.ASPECT_RATIO_4_3
+
+ TargetAspectRatio.RATIO_16_9 -> ratio =
+ AspectRatioUtil.ASPECT_RATIO_16_9
+
+ TargetAspectRatio.RATIO_MAX_JPEG -> {
+ val maxJpegSize = Collections.max(sizes.asList(), CompareSizesByArea())
+ ratio = Rational(maxJpegSize.width, maxJpegSize.height)
+ }
+
+ TargetAspectRatio.RATIO_ORIGINAL -> ratio =
+ null
+ }
+
+ if (ratio == null) {
+ return sizes
+ }
+
+ val resultList: MutableList<Size> = java.util.ArrayList()
+
+ for (size in sizes) {
+ if (AspectRatioUtil.hasMatchingAspectRatio(size, ratio)) {
+ resultList.add(size)
+ }
+ }
+
+ return if (resultList.isEmpty()) {
+ null
+ } else {
+ resultList.toTypedArray()
+ }
}
private fun concatNullableSizeLists(
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/ResolutionCorrector.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/ResolutionCorrector.kt
new file mode 100644
index 0000000..c3cee29
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/ResolutionCorrector.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.workaround
+
+import android.util.Size
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks
+import androidx.camera.camera2.pipe.integration.compat.quirk.ExtraCroppingQuirk
+import androidx.camera.core.impl.SurfaceConfig.ConfigType
+
+/**
+ * Helper class that overrides user configured resolution with resolution selected based on device
+ * quirks.
+ */
+@RequiresApi(21)
+class ResolutionCorrector {
+ private val extraCroppingQuirk = DeviceQuirks[ExtraCroppingQuirk::class.java]
+ /**
+ * Returns a new list of resolution with the selected resolution inserted or prioritized.
+ *
+ *
+ * If the list contains the selected resolution, move it to be the first element; if it
+ * does not contain the selected resolution, insert it as the first element; if there is no
+ * device quirk, return the original list.
+ *
+ * @param configType the config type based on which the supported resolution is
+ * calculated.
+ * @param supportedResolutions a ordered list of resolutions calculated by CameraX.
+ */
+ fun insertOrPrioritize(
+ configType: ConfigType,
+ supportedResolutions: List<Size>,
+
+ ): List<Size> {
+ if (extraCroppingQuirk == null) {
+ return supportedResolutions
+ }
+ val selectResolution: Size = extraCroppingQuirk.getVerifiedResolution(configType)
+ ?: return supportedResolutions
+ val newResolutions: MutableList<Size> = mutableListOf()
+ newResolutions.add(selectResolution)
+ for (size in supportedResolutions) {
+ if (size != selectResolution) {
+ newResolutions.add(size)
+ }
+ }
+ return newResolutions
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/TargetAspectRatio.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/TargetAspectRatio.kt
new file mode 100644
index 0000000..99c6d19
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/TargetAspectRatio.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.workaround
+
+import androidx.annotation.IntDef
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
+import androidx.camera.camera2.pipe.integration.compat.quirk.AspectRatioLegacyApi21Quirk
+import androidx.camera.camera2.pipe.integration.compat.quirk.CameraQuirks
+import androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks
+import androidx.camera.camera2.pipe.integration.compat.quirk.Nexus4AndroidLTargetAspectRatioQuirk
+
+/**
+ * Workaround to get corrected target aspect ratio.
+ *
+ * @see Nexus4AndroidLTargetAspectRatioQuirk
+ *
+ * @see AspectRatioLegacyApi21Quirk
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+
+class TargetAspectRatio {
+ /**
+ * Gets corrected target aspect ratio based on device and camera quirks.
+ */
+ @Ratio
+ operator fun get(
+ cameraMetadata: CameraMetadata,
+ streamConfigurationMapCompat: StreamConfigurationMapCompat
+ ): Int {
+ val cameraQuirks = CameraQuirks(
+ cameraMetadata,
+ streamConfigurationMapCompat
+ )
+ val nexus4AndroidLTargetAspectRatioQuirk =
+ DeviceQuirks[Nexus4AndroidLTargetAspectRatioQuirk::class.java]
+ if (nexus4AndroidLTargetAspectRatioQuirk != null) {
+ return nexus4AndroidLTargetAspectRatioQuirk.getCorrectedAspectRatio()
+ }
+
+ val aspectRatioLegacyApi21Quirk =
+ cameraQuirks.quirks[AspectRatioLegacyApi21Quirk::class.java]
+ return aspectRatioLegacyApi21Quirk?.getCorrectedAspectRatio() ?: RATIO_ORIGINAL
+ }
+
+ /**
+ */
+ @IntDef(RATIO_4_3, RATIO_16_9, RATIO_MAX_JPEG, RATIO_ORIGINAL)
+ @Retention(AnnotationRetention.SOURCE)
+ @RestrictTo(
+ RestrictTo.Scope.LIBRARY_GROUP
+ )
+ annotation class Ratio
+ companion object {
+ /** 4:3 standard aspect ratio. */
+ const val RATIO_4_3 = 0
+
+ /** 16:9 standard aspect ratio. */
+ const val RATIO_16_9 = 1
+
+ /** The same aspect ratio as the maximum JPEG resolution. */
+ const val RATIO_MAX_JPEG = 2
+
+ /** No correction is needed. */
+ const val RATIO_ORIGINAL = 3
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt
index c7745f6..c1ea143 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt
@@ -25,6 +25,7 @@
import android.util.Size
import android.view.Display
import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.integration.compat.workaround.MaxPreviewSize
import javax.inject.Inject
import javax.inject.Singleton
@@ -33,6 +34,7 @@
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
class DisplayInfoManager @Inject constructor(context: Context) {
private val MAX_PREVIEW_SIZE = Size(1920, 1080)
+ private val maxPreviewSize: MaxPreviewSize = MaxPreviewSize()
companion object {
private var lazyMaxDisplay: Display? = null
@@ -69,8 +71,27 @@
val defaultDisplay: Display
get() = getMaxSizeDisplay()
- val previewSize: Size
- get() = calculatePreviewSize()
+ private var previewSize: Size? = null
+
+ /**
+ * Update the preview size according to current display size.
+ */
+ fun refresh() {
+ previewSize = calculatePreviewSize()
+ }
+
+ /**
+ * PREVIEW refers to the best size match to the device's screen resolution, or to 1080p
+ * (1920x1080), whichever is smaller.
+ */
+ fun getPreviewSize(): Size {
+ // Use cached value to speed up since this would be called multiple times.
+ if (previewSize != null) {
+ return previewSize as Size
+ }
+ previewSize = calculatePreviewSize()
+ return previewSize as Size
+ }
private fun getMaxSizeDisplay(): Display {
lazyMaxDisplay?.let { return it }
@@ -130,7 +151,7 @@
) {
displayViewSize = MAX_PREVIEW_SIZE
}
- // TODO(b/230402463): Migrate extra cropping quirk from CameraX.
+ displayViewSize = maxPreviewSize.getMaxPreviewResolution(displayViewSize)
return displayViewSize.also { lazyPreviewSize = displayViewSize }
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
index 9f2844a..00d5f2c4 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
@@ -166,7 +166,7 @@
// Find maximum supported resolution that is <= min(VGA, display resolution)
// Using minimum supported size could cause some issue on certain devices.
- val previewSize = displayInfoManager.previewSize
+ val previewSize = displayInfoManager.getPreviewSize()
val maxSizeProduct =
min(640L * 480L, previewSize.width.toLong() * previewSize.height.toLong())
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
index 77f4fe0..9898814 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
@@ -1272,11 +1272,13 @@
// camera 5 supports 1 AF and 0 AE/AWB regions
focusMeteringControl = initFocusMeteringControl(cameraId = CAMERA_ID_5)
- startFocusMeteringAndAwait(FocusMeteringAction.Builder(
- point1,
- FocusMeteringAction.FLAG_AF or FocusMeteringAction.FLAG_AE or
- FocusMeteringAction.FLAG_AWB
- ).build())
+ startFocusMeteringAndAwait(
+ FocusMeteringAction.Builder(
+ point1,
+ FocusMeteringAction.FLAG_AF or FocusMeteringAction.FLAG_AE or
+ FocusMeteringAction.FLAG_AWB
+ ).build()
+ )
with(fakeRequestControl.focusMeteringCalls.last()) {
assertWithMessage("Wrong number of AE regions").that(aeRegions).isNull()
@@ -1398,7 +1400,10 @@
cameraPropertiesMap[cameraId]!!.metadata,
StreamConfigurationMapCompat(
StreamConfigurationMapBuilder.newBuilder().build(),
- OutputSizesCorrector(cameraPropertiesMap[cameraId]!!.metadata)
+ OutputSizesCorrector(
+ cameraPropertiesMap[cameraId]!!.metadata,
+ StreamConfigurationMapBuilder.newBuilder().build()
+ ),
)
)
),
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
index 2f82f0a..5074974 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
@@ -180,7 +180,8 @@
for (combination in combinationList) {
val isSupported =
supportedSurfaceCombination.checkSupported(
- false, combination.surfaceConfigList)
+ false, combination.surfaceConfigList
+ )
assertThat(isSupported).isTrue()
}
}
@@ -194,7 +195,8 @@
)
val combinationList = getLegacySupportedCombinationList()
val isSupported = isAllSubConfigListSupported(
- false, supportedSurfaceCombination, combinationList)
+ false, supportedSurfaceCombination, combinationList
+ )
assertThat(isSupported).isTrue()
}
@@ -209,7 +211,8 @@
for (combination in combinationList) {
val isSupported =
supportedSurfaceCombination.checkSupported(
- false, combination.surfaceConfigList)
+ false, combination.surfaceConfigList
+ )
assertThat(isSupported).isFalse()
}
}
@@ -225,7 +228,8 @@
for (combination in combinationList) {
val isSupported =
supportedSurfaceCombination.checkSupported(
- false, combination.surfaceConfigList)
+ false, combination.surfaceConfigList
+ )
assertThat(isSupported).isFalse()
}
}
@@ -241,7 +245,8 @@
for (combination in combinationList) {
val isSupported =
supportedSurfaceCombination.checkSupported(
- false, combination.surfaceConfigList)
+ false, combination.surfaceConfigList
+ )
assertThat(isSupported).isFalse()
}
}
@@ -257,7 +262,8 @@
for (combination in combinationList) {
val isSupported =
supportedSurfaceCombination.checkSupported(
- false, combination.surfaceConfigList)
+ false, combination.surfaceConfigList
+ )
assertThat(isSupported).isTrue()
}
}
@@ -271,7 +277,8 @@
)
val combinationList = getLimitedSupportedCombinationList()
val isSupported = isAllSubConfigListSupported(
- false, supportedSurfaceCombination, combinationList)
+ false, supportedSurfaceCombination, combinationList
+ )
assertThat(isSupported).isTrue()
}
@@ -286,7 +293,8 @@
for (combination in combinationList) {
val isSupported =
supportedSurfaceCombination.checkSupported(
- false, combination.surfaceConfigList)
+ false, combination.surfaceConfigList
+ )
assertThat(isSupported).isFalse()
}
}
@@ -302,7 +310,8 @@
for (combination in combinationList) {
val isSupported =
supportedSurfaceCombination.checkSupported(
- false, combination.surfaceConfigList)
+ false, combination.surfaceConfigList
+ )
assertThat(isSupported).isFalse()
}
}
@@ -318,7 +327,8 @@
for (combination in combinationList) {
val isSupported =
supportedSurfaceCombination.checkSupported(
- false, combination.surfaceConfigList)
+ false, combination.surfaceConfigList
+ )
assertThat(isSupported).isTrue()
}
}
@@ -332,7 +342,8 @@
)
val combinationList = getFullSupportedCombinationList()
val isSupported = isAllSubConfigListSupported(
- false, supportedSurfaceCombination, combinationList)
+ false, supportedSurfaceCombination, combinationList
+ )
assertThat(isSupported).isTrue()
}
@@ -347,7 +358,8 @@
for (combination in combinationList) {
val isSupported =
supportedSurfaceCombination.checkSupported(
- false, combination.surfaceConfigList)
+ false, combination.surfaceConfigList
+ )
assertThat(isSupported).isFalse()
}
}
@@ -367,7 +379,8 @@
for (combination in combinationList) {
val isSupported =
supportedSurfaceCombination.checkSupported(
- false, combination.surfaceConfigList)
+ false, combination.surfaceConfigList
+ )
assertThat(isSupported).isTrue()
}
}
@@ -387,7 +400,8 @@
for (combination in combinationList) {
val isSupported =
supportedSurfaceCombination.checkSupported(
- false, combination.surfaceConfigList)
+ false, combination.surfaceConfigList
+ )
assertThat(isSupported).isTrue()
}
}
@@ -407,7 +421,8 @@
for (combination in combinationList) {
val isSupported =
supportedSurfaceCombination.checkSupported(
- false, combination.surfaceConfigList)
+ false, combination.surfaceConfigList
+ )
assertThat(isSupported).isTrue()
}
}
@@ -427,7 +442,8 @@
for (combination in combinationList) {
val isSupported =
supportedSurfaceCombination.checkSupported(
- false, combination.surfaceConfigList)
+ false, combination.surfaceConfigList
+ )
assertThat(isSupported).isTrue()
}
}
@@ -443,7 +459,8 @@
for (combination in combinationList) {
val isSupported =
supportedSurfaceCombination.checkSupported(
- false, combination.surfaceConfigList)
+ false, combination.surfaceConfigList
+ )
assertThat(isSupported).isTrue()
}
}
@@ -457,14 +474,16 @@
)
val combinationList = getLevel3SupportedCombinationList()
val isSupported = isAllSubConfigListSupported(
- false, supportedSurfaceCombination, combinationList)
+ false, supportedSurfaceCombination, combinationList
+ )
assertThat(isSupported).isTrue()
}
@Test
fun checkConcurrentSurfaceCombinationSupportedInConcurrentCameraMode() {
Shadows.shadowOf(context.packageManager).setSystemFeature(
- PackageManager.FEATURE_CAMERA_CONCURRENT, true)
+ PackageManager.FEATURE_CAMERA_CONCURRENT, true
+ )
setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3)
val supportedSurfaceCombination = SupportedSurfaceCombination(
context, fakeCameraMetadata,
@@ -474,7 +493,8 @@
for (combination in combinationList) {
val isSupported =
supportedSurfaceCombination.checkSupported(
- true, combination.surfaceConfigList)
+ true, combination.surfaceConfigList
+ )
assertThat(isSupported).isTrue()
}
}
@@ -482,7 +502,8 @@
@Test
fun checkConcurrentSurfaceCombinationSubListSupportedInConcurrentCameraMode() {
Shadows.shadowOf(context.packageManager).setSystemFeature(
- PackageManager.FEATURE_CAMERA_CONCURRENT, true)
+ PackageManager.FEATURE_CAMERA_CONCURRENT, true
+ )
setupCamera(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3)
val supportedSurfaceCombination = SupportedSurfaceCombination(
context, fakeCameraMetadata,
@@ -490,7 +511,8 @@
)
val combinationList = getConcurrentSupportedCombinationList()
val isSupported = isAllSubConfigListSupported(
- true, supportedSurfaceCombination, combinationList)
+ true, supportedSurfaceCombination, combinationList
+ )
assertThat(isSupported).isTrue()
}
@@ -1666,7 +1688,8 @@
val subConfigurationList: MutableList<SurfaceConfig> = ArrayList(configList)
subConfigurationList.removeAt(index)
val isSupported = supportedSurfaceCombination.checkSupported(
- isConcurrentCameraModeOn, subConfigurationList)
+ isConcurrentCameraModeOn, subConfigurationList
+ )
if (!isSupported) {
return false
}
@@ -1679,11 +1702,13 @@
captureType: UseCaseConfigFactory.CaptureType,
targetFrameRate: Range<Int>? = null
): UseCase {
- val builder = FakeUseCaseConfig.Builder(captureType, when (captureType) {
- UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE -> ImageFormat.JPEG
- UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS -> ImageFormat.YUV_420_888
- else -> ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
- })
+ val builder = FakeUseCaseConfig.Builder(
+ captureType, when (captureType) {
+ UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE -> ImageFormat.JPEG
+ UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS -> ImageFormat.YUV_420_888
+ else -> ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
+ }
+ )
targetFrameRate?.let {
builder.mutableConfig.insertOption(UseCaseConfig.OPTION_TARGET_FRAME_RATE, it)
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/OutputSizesCorrectorTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/OutputSizesCorrectorTest.kt
index b81db73..63199bb 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/OutputSizesCorrectorTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/OutputSizesCorrectorTest.kt
@@ -40,6 +40,8 @@
private const val MOTOROLA_E5_PLAY_MODEL_NAME = "moto e5 play"
private const val SAMSUNG_BRAND_NAME = "SAMSUNG"
private const val SAMSUNG_J7_DEVICE_NAME = "J7XELTE"
+private const val FAKE_BRAND_NAME = "Fake-Brand"
+private const val FAKE_DEVICE_NAME = "Fake-Device"
private val outputSizes = arrayOf(
// Samsung J7 API 27 above excluded sizes
@@ -206,6 +208,98 @@
).inOrder()
}
+ @Test
+ fun canExcludeApi21LegacyLevelProblematicSizesByFormat() {
+ val outputSizesCorrector = createOutputSizesCorrector(
+ FAKE_BRAND_NAME,
+ FAKE_DEVICE_NAME,
+ null,
+ CAMERA_ID_0,
+ CameraCharacteristics.LENS_FACING_BACK,
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
+ )
+
+ val resultList = outputSizesCorrector.applyQuirks(
+ outputSizes,
+ ImageFormat.YUV_420_888
+ )!!.toList()
+
+ val expectedList = if (Build.VERSION.SDK_INT == 21) {
+ // non-4:3 sizes are removed
+ listOf(
+ Size(4128, 3096),
+ Size(3264, 2448),
+ Size(2048, 1536),
+ Size(1280, 960),
+ Size(640, 480),
+ Size(320, 240),
+ )
+ } else {
+ listOf(
+ Size(4128, 3096),
+ Size(4128, 2322),
+ Size(3088, 3088),
+ Size(3264, 2448),
+ Size(3264, 1836),
+ Size(2048, 1536),
+ Size(2048, 1152),
+ Size(1920, 1080),
+ Size(1280, 960),
+ Size(1280, 720),
+ Size(640, 480),
+ Size(320, 240),
+ )
+ }
+
+ Truth.assertThat(resultList).containsExactlyElementsIn(expectedList).inOrder()
+ }
+
+ @Test
+ fun canExcludeApi21LegacyLevelProblematicSizesByClass() {
+ val outputSizesCorrector = createOutputSizesCorrector(
+ FAKE_BRAND_NAME,
+ FAKE_DEVICE_NAME,
+ null,
+ CAMERA_ID_0,
+ CameraCharacteristics.LENS_FACING_BACK,
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
+ )
+
+ val resultList = outputSizesCorrector.applyQuirks(
+ outputSizes,
+ SurfaceTexture::class.java
+ )!!.toList()
+
+ val expectedList = if (Build.VERSION.SDK_INT == 21) {
+ // non-4:3 sizes are removed
+ listOf(
+ Size(4128, 3096),
+ Size(3264, 2448),
+ Size(2048, 1536),
+ Size(1280, 960),
+ Size(640, 480),
+ Size(320, 240),
+ )
+ } else {
+ listOf(
+ Size(4128, 3096),
+ Size(4128, 2322),
+ Size(3088, 3088),
+ Size(3264, 2448),
+ Size(3264, 1836),
+ Size(2048, 1536),
+ Size(2048, 1152),
+ Size(1920, 1080),
+ Size(1280, 960),
+ Size(1280, 720),
+ Size(640, 480),
+ Size(320, 240),
+ )
+ }
+
+ Truth.assertThat(resultList).containsExactlyElementsIn(expectedList).inOrder()
+ }
+
private fun createOutputSizesCorrector(
brand: String,
device: String?,
@@ -222,20 +316,22 @@
ReflectionHelpers.setStaticField(Build::class.java, "MODEL", it)
}
+ val map = StreamConfigurationMapBuilder.newBuilder()
+ .apply {
+ outputSizes.forEach { outputSize ->
+ addOutputSize(outputSize)
+ }
+ }.build()
+
return OutputSizesCorrector(
FakeCameraMetadata(
cameraId = CameraId(cameraId), characteristics = mapOf(
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL to hardwareLevel,
CameraCharacteristics.LENS_FACING to lensFacing,
- CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP to
- StreamConfigurationMapBuilder.newBuilder()
- .apply {
- outputSizes.forEach { outputSize ->
- addOutputSize(outputSize)
- }
- }.build()
+ CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP to map
)
- )
+ ),
+ map
)
}
}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatTest.kt
index f972ee6..ef595571 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatTest.kt
@@ -60,7 +60,7 @@
streamConfigurationMapCompat =
StreamConfigurationMapCompat(
builder.build(),
- OutputSizesCorrector(FakeCameraMetadata())
+ OutputSizesCorrector(FakeCameraMetadata(), builder.build())
)
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/AfRegionFlipHorizontallyQuirkTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/AfRegionFlipHorizontallyQuirkTest.kt
index 5f4c468..9e3a6be 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/AfRegionFlipHorizontallyQuirkTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/AfRegionFlipHorizontallyQuirkTest.kt
@@ -73,7 +73,10 @@
cameraMetadata,
StreamConfigurationMapCompat(
StreamConfigurationMapBuilder.newBuilder().build(),
- OutputSizesCorrector(cameraMetadata)
+ OutputSizesCorrector(
+ cameraMetadata,
+ StreamConfigurationMapBuilder.newBuilder().build()
+ )
)
).quirks
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/CamcorderProfileResolutionQuirkTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/CamcorderProfileResolutionQuirkTest.kt
index 7b2f9e2..5f0a226 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/CamcorderProfileResolutionQuirkTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/CamcorderProfileResolutionQuirkTest.kt
@@ -74,7 +74,10 @@
val quirk = CamcorderProfileResolutionQuirk(
StreamConfigurationMapCompat(
cameraMetadata[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]!!,
- OutputSizesCorrector(cameraMetadata)
+ OutputSizesCorrector(
+ cameraMetadata,
+ cameraMetadata[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]!!
+ )
)
)
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraCroppingQuirkTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraCroppingQuirkTest.kt
new file mode 100644
index 0000000..42b703a
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraCroppingQuirkTest.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.quirk
+
+import android.os.Build
+import android.util.Size
+import androidx.camera.core.impl.SurfaceConfig
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.util.ReflectionHelpers
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class ExtraCroppingQuirkTest {
+ private val quirk = ExtraCroppingQuirk()
+
+ @Test
+ fun deviceModelNotContained_returnsNull() {
+ ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "non-existent")
+ assert(!ExtraCroppingQuirk.load())
+ Truth.assertThat(quirk.getVerifiedResolution(SurfaceConfig.ConfigType.PRIV)).isNull()
+ }
+
+ @Test
+ fun deviceBrandNotSamsung_returnsNull() {
+ ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "non-existent")
+ assert(!ExtraCroppingQuirk.load())
+ Truth.assertThat(quirk.getVerifiedResolution(SurfaceConfig.ConfigType.PRIV)).isNull()
+ }
+
+ @Test
+ fun osVersionNotInRange_returnsNull() {
+ ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "samsung")
+ ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "SM-J710MN")
+ ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 20)
+ assert(!ExtraCroppingQuirk.load())
+ Truth.assertThat(quirk.getVerifiedResolution(SurfaceConfig.ConfigType.PRIV)).isNull()
+ }
+
+ @Test
+ fun rawConfigType_returnsNull() {
+ ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "samsung")
+ ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "SM-J710MN")
+ ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 22)
+ assert(ExtraCroppingQuirk.load())
+ Truth.assertThat(quirk.getVerifiedResolution(SurfaceConfig.ConfigType.RAW)).isNull()
+ }
+
+ @Test
+ fun privConfigType_returnsSize() {
+ ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "samsung")
+ ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "SM-J710MN")
+ ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 22)
+ assert(ExtraCroppingQuirk.load())
+ Truth.assertThat(quirk.getVerifiedResolution(SurfaceConfig.ConfigType.PRIV))
+ .isEqualTo(Size(1920, 1080))
+ }
+
+ @Test
+ fun yuvConfigType_returnsSize() {
+ ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "samsung")
+ ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "SM-J710MN")
+ ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 22)
+ assert(ExtraCroppingQuirk.load())
+ Truth.assertThat(quirk.getVerifiedResolution(SurfaceConfig.ConfigType.YUV))
+ .isEqualTo(Size(1280, 720))
+ }
+
+ @Test
+ fun jpegConfigType_returnsSize() {
+ ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "samsung")
+ ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "SM-J710MN")
+ ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 22)
+ assert(ExtraCroppingQuirk.load())
+ Truth.assertThat(quirk.getVerifiedResolution(SurfaceConfig.ConfigType.JPEG))
+ .isEqualTo(Size(3264, 1836))
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraSupportedSurfaceCombinationsQuirkTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraSupportedSurfaceCombinationsQuirkTest.kt
new file mode 100644
index 0000000..88b54d5
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/ExtraSupportedSurfaceCombinationsQuirkTest.kt
@@ -0,0 +1,314 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.quirk
+
+import android.hardware.camera2.CameraCharacteristics
+import android.os.Build
+import androidx.camera.core.impl.SurfaceCombination
+import androidx.camera.core.impl.SurfaceConfig
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.ParameterizedRobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.util.ReflectionHelpers
+
+@RunWith(ParameterizedRobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class ExtraSupportedSurfaceCombinationsQuirkTest(private val config: Config) {
+
+ @Test
+ fun checkExtraSupportedSurfaceCombinations() {
+ // Set up brand properties
+ if (config.brand != null) {
+ ReflectionHelpers.setStaticField(Build::class.java, "BRAND", config.brand)
+ }
+
+ // Set up device properties
+ if (config.device != null) {
+ ReflectionHelpers.setStaticField(Build::class.java, "DEVICE", config.device)
+ }
+
+ // Set up model properties
+ if (config.model != null) {
+ ReflectionHelpers.setStaticField(Build::class.java, "MODEL", config.model)
+ }
+
+ // Initializes ExtraSupportedSurfaceCombinationsContainer instance with camera id
+ val quirk =
+ ExtraSupportedSurfaceCombinationsQuirk()
+
+ // Gets the extra supported surface combinations on the device
+ val extraSurfaceCombinations: List<SurfaceCombination> =
+ quirk.getExtraSupportedSurfaceCombinations(
+ config.cameraId,
+ config.hardwareLevel
+ )
+ for (expectedSupportedSurfaceCombination in config.expectedSupportedSurfaceCombinations) {
+ var isSupported = false
+
+ // Checks the combination is supported by the list retrieved from the
+ // ExtraSupportedSurfaceCombinationsContainer.
+ for (extraSurfaceCombination in extraSurfaceCombinations) {
+ if (extraSurfaceCombination.isSupported(
+ expectedSupportedSurfaceCombination.surfaceConfigList
+ )
+ ) {
+ isSupported = true
+ break
+ }
+ }
+ Truth.assertThat(isSupported).isTrue()
+ }
+ }
+
+ companion object {
+ @JvmStatic
+ @ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
+ fun data() = listOf(
+ Config(
+ null,
+ "heroqltevzw",
+ null,
+ "0",
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+ ),
+ Config(
+ null,
+ "heroqltevzw",
+ null,
+ "1",
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ createFullLevelYPYSupportedCombinations()
+ ),
+ // Tests for Samsung S7 case
+ Config(
+ null,
+ "heroqltetmo",
+ null,
+ "0",
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+ ),
+ Config(
+ null,
+ "heroqltetmo",
+ null, "1",
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ createFullLevelYPYSupportedCombinations()
+ ),
+ // Tests for Samsung limited device case
+ Config(
+ "samsung",
+ null,
+ "sm-g9860",
+ "0",
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+ ),
+ Config(
+ "samsung",
+ null,
+ "sm-g9860",
+ "1",
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+ createFullLevelYPYAndYYYSupportedCombinations()
+ ),
+ // Tests for FULL Pixel devices
+ Config(
+ "Google",
+ null,
+ "Pixel 6",
+ "0",
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+ createLevel3PrivPrivYuvRawConfiguration()
+ ),
+ Config(
+ "Google",
+ null,
+ "Pixel 6",
+ "1",
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+ createLevel3PrivPrivYuvRawConfiguration()
+ ),
+ Config(
+ "Google",
+ null,
+ "Pixel 6 Pro",
+ "0",
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+ createLevel3PrivPrivYuvRawConfiguration()
+ ),
+ Config(
+ "Google",
+ null,
+ "Pixel 6 Pro",
+ "1",
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+ createLevel3PrivPrivYuvRawConfiguration()
+ ),
+ Config(
+ "Google",
+ null,
+ "Pixel 7",
+ "0",
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+ createLevel3PrivPrivYuvRawConfiguration()
+ ),
+ Config(
+ "Google",
+ null,
+ "Pixel 6 Pro",
+ "1",
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+ createLevel3PrivPrivYuvRawConfiguration()
+ ),
+ Config(
+ "Google",
+ null,
+ "Pixel 7 Pro",
+ "0",
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+ createLevel3PrivPrivYuvRawConfiguration()
+ ),
+ Config(
+ "Google",
+ null,
+ "Pixel 7 Pro",
+ "1",
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+ createLevel3PrivPrivYuvRawConfiguration()
+ ),
+ // Other cases
+ Config(
+ null,
+ null,
+ null,
+ "0",
+ CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+ )
+ )
+
+ private fun createFullLevelYPYSupportedCombinations(): Array<SurfaceCombination> {
+ // (YUV, ANALYSIS) + (PRIV, PREVIEW) + (YUV, MAXIMUM)
+ val surfaceCombination = SurfaceCombination()
+ surfaceCombination.addSurfaceConfig(
+ SurfaceConfig.create(
+ SurfaceConfig.ConfigType.YUV,
+ SurfaceConfig.ConfigSize.VGA
+ )
+ )
+ surfaceCombination.addSurfaceConfig(
+ SurfaceConfig.create(
+ SurfaceConfig.ConfigType.PRIV,
+ SurfaceConfig.ConfigSize.PREVIEW
+ )
+ )
+ surfaceCombination.addSurfaceConfig(
+ SurfaceConfig.create(
+ SurfaceConfig.ConfigType.YUV,
+ SurfaceConfig.ConfigSize.MAXIMUM
+ )
+ )
+ return arrayOf(surfaceCombination)
+ }
+
+ private fun createFullLevelYPYAndYYYSupportedCombinations(): Array<SurfaceCombination> {
+ // (YUV, ANALYSIS) + (PRIV, PREVIEW) + (YUV, MAXIMUM)
+ val surfaceCombination1 = SurfaceCombination()
+ surfaceCombination1.addSurfaceConfig(
+ SurfaceConfig.create(
+ SurfaceConfig.ConfigType.YUV,
+ SurfaceConfig.ConfigSize.VGA
+ )
+ )
+ surfaceCombination1.addSurfaceConfig(
+ SurfaceConfig.create(
+ SurfaceConfig.ConfigType.PRIV,
+ SurfaceConfig.ConfigSize.PREVIEW
+ )
+ )
+ surfaceCombination1.addSurfaceConfig(
+ SurfaceConfig.create(
+ SurfaceConfig.ConfigType.YUV,
+ SurfaceConfig.ConfigSize.MAXIMUM
+ )
+ )
+
+ // (YUV, ANALYSIS) + (YUV, PREVIEW) + (YUV, MAXIMUM)
+ val surfaceCombination2 = SurfaceCombination()
+ surfaceCombination2.addSurfaceConfig(
+ SurfaceConfig.create(
+ SurfaceConfig.ConfigType.YUV,
+ SurfaceConfig.ConfigSize.VGA
+ )
+ )
+ surfaceCombination2.addSurfaceConfig(
+ SurfaceConfig.create(
+ SurfaceConfig.ConfigType.YUV,
+ SurfaceConfig.ConfigSize.PREVIEW
+ )
+ )
+ surfaceCombination2.addSurfaceConfig(
+ SurfaceConfig.create(
+ SurfaceConfig.ConfigType.YUV,
+ SurfaceConfig.ConfigSize.MAXIMUM
+ )
+ )
+ return arrayOf(surfaceCombination1, surfaceCombination2)
+ }
+
+ private fun createLevel3PrivPrivYuvRawConfiguration(): Array<SurfaceCombination> {
+ // (PRIV, PREVIEW) + (PRIV, ANALYSIS) + (YUV, MAXIMUM) + (RAW, MAXIMUM)
+ val surfaceCombination = SurfaceCombination()
+ surfaceCombination.addSurfaceConfig(
+ SurfaceConfig.create(
+ SurfaceConfig.ConfigType.PRIV,
+ SurfaceConfig.ConfigSize.PREVIEW
+ )
+ )
+ surfaceCombination.addSurfaceConfig(
+ SurfaceConfig.create(
+ SurfaceConfig.ConfigType.PRIV,
+ SurfaceConfig.ConfigSize.VGA
+ )
+ )
+ surfaceCombination.addSurfaceConfig(
+ SurfaceConfig.create(
+ SurfaceConfig.ConfigType.YUV,
+ SurfaceConfig.ConfigSize.MAXIMUM
+ )
+ )
+ surfaceCombination.addSurfaceConfig(
+ SurfaceConfig.create(
+ SurfaceConfig.ConfigType.RAW,
+ SurfaceConfig.ConfigSize.MAXIMUM
+ )
+ )
+ return arrayOf(surfaceCombination)
+ }
+ }
+
+ class Config(
+ val brand: String?,
+ val device: String?,
+ val model: String?,
+ val cameraId: String,
+ val hardwareLevel: Int,
+ val expectedSupportedSurfaceCombinations: Array<SurfaceCombination> = arrayOf()
+ )
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/JpegHalCorruptImageQuirkTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/JpegHalCorruptImageQuirkTest.kt
index bc371e4..edd60eb 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/JpegHalCorruptImageQuirkTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/JpegHalCorruptImageQuirkTest.kt
@@ -55,7 +55,10 @@
FakeCameraMetadata(),
StreamConfigurationMapCompat(
StreamConfigurationMapBuilder.newBuilder().build(),
- OutputSizesCorrector(FakeCameraMetadata())
+ OutputSizesCorrector(
+ FakeCameraMetadata(),
+ StreamConfigurationMapBuilder.newBuilder().build()
+ )
)
).quirks
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/YuvImageOnePixelShiftQuirkTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/YuvImageOnePixelShiftQuirkTest.kt
index 43d8e7f..c7b452a 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/YuvImageOnePixelShiftQuirkTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/quirk/YuvImageOnePixelShiftQuirkTest.kt
@@ -46,7 +46,10 @@
FakeCameraMetadata(),
StreamConfigurationMapCompat(
StreamConfigurationMapBuilder.newBuilder().build(),
- OutputSizesCorrector(FakeCameraMetadata())
+ OutputSizesCorrector(
+ FakeCameraMetadata(),
+ StreamConfigurationMapBuilder.newBuilder().build()
+ )
)
).quirks
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/AutoFlashAEModeDisablerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/AutoFlashAEModeDisablerTest.kt
index 04025fc..f7ad78c 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/AutoFlashAEModeDisablerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/AutoFlashAEModeDisablerTest.kt
@@ -113,7 +113,10 @@
CameraQuirks(
metadata, StreamConfigurationMapCompat(
StreamConfigurationMapBuilder.newBuilder().build(),
- OutputSizesCorrector(FakeCameraMetadata())
+ OutputSizesCorrector(
+ FakeCameraMetadata(),
+ StreamConfigurationMapBuilder.newBuilder().build()
+ )
)
)
)
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/ResolutionCorrectorTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/ResolutionCorrectorTest.kt
new file mode 100644
index 0000000..ca3adf66
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/ResolutionCorrectorTest.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.workaround
+
+import android.os.Build
+import android.util.Size
+import androidx.camera.core.impl.SurfaceConfig
+import com.google.common.truth.Truth
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.util.ReflectionHelpers
+
+private val RESOLUTION_1 = Size(101, 100)
+private val RESOLUTION_2 = Size(102, 100)
+
+private val SELECT_RESOLUTION_PRIV = Size(1920, 1080)
+private val SELECT_RESOLUTION_YUV = Size(1280, 720)
+private val SELECT_RESOLUTION_JPEG = Size(3264, 1836)
+
+private val SUPPORTED_RESOLUTIONS = listOf(RESOLUTION_1, RESOLUTION_2)
+
+/**
+ * Unit test for [ResolutionCorrector].
+ */
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class ResolutionCorrectorTest {
+
+ private var mResolutionCorrector: ResolutionCorrector? = null
+
+ @Before
+ fun setup() {
+ ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "samsung")
+ ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "SM-J710MN")
+ ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 22)
+ mResolutionCorrector = ResolutionCorrector()
+ }
+
+ @Test
+ fun hasPrivResolution_prioritized() {
+ hasResolution_prioritized(SurfaceConfig.ConfigType.PRIV, SELECT_RESOLUTION_PRIV)
+ }
+
+ @Test
+ fun hasYuvResolution_prioritized() {
+ hasResolution_prioritized(SurfaceConfig.ConfigType.YUV, SELECT_RESOLUTION_YUV)
+ }
+
+ @Test
+ fun hasJpegResolution_prioritized() {
+ hasResolution_prioritized(SurfaceConfig.ConfigType.JPEG, SELECT_RESOLUTION_JPEG)
+ }
+
+ private fun hasResolution_prioritized(
+ configType: SurfaceConfig.ConfigType,
+ resolution: Size
+ ) {
+ val resolutions: MutableList<Size> = ArrayList<Size>(SUPPORTED_RESOLUTIONS)
+ resolutions.add(resolution)
+ Truth.assertThat(mResolutionCorrector!!.insertOrPrioritize(configType, resolutions))
+ .containsExactly(resolution, RESOLUTION_1, RESOLUTION_2).inOrder()
+ }
+
+ @Test
+ fun noPrivResolution_inserted() {
+ noResolution_inserted(SurfaceConfig.ConfigType.PRIV, SELECT_RESOLUTION_PRIV)
+ }
+
+ @Test
+ fun noYuvResolution_inserted() {
+ noResolution_inserted(SurfaceConfig.ConfigType.YUV, SELECT_RESOLUTION_YUV)
+ }
+
+ @Test
+ fun noJpegResolution_inserted() {
+ noResolution_inserted(SurfaceConfig.ConfigType.JPEG, SELECT_RESOLUTION_JPEG)
+ }
+
+ private fun noResolution_inserted(
+ configType: SurfaceConfig.ConfigType,
+ resolution: Size
+ ) {
+ Truth.assertThat(
+ mResolutionCorrector!!.insertOrPrioritize(
+ configType,
+ SUPPORTED_RESOLUTIONS
+ )
+ )
+ .containsExactly(resolution, RESOLUTION_1, RESOLUTION_2).inOrder()
+ }
+
+ @Test
+ fun noQuirk_returnsOriginalSupportedResolutions() {
+ ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "notsamsung")
+ val resolutionCorrector = ResolutionCorrector()
+ val result = resolutionCorrector.insertOrPrioritize(
+ SurfaceConfig.ConfigType.PRIV,
+ SUPPORTED_RESOLUTIONS
+ )
+ Truth.assertThat(result).containsExactlyElementsIn(SUPPORTED_RESOLUTIONS)
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
index 1d12d9b..02a742b 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
@@ -198,7 +198,7 @@
addDisplay(480, 640)
// Act & Assert
- assertEquals(Size(640, 480), displayInfoManager.previewSize)
+ assertEquals(Size(640, 480), displayInfoManager.getPreviewSize())
}
@Test
@@ -207,7 +207,7 @@
addDisplay(2000, 3000)
// Act & Assert
- assertEquals(Size(1920, 1080), displayInfoManager.previewSize)
+ assertEquals(Size(1920, 1080), displayInfoManager.getPreviewSize())
}
@Test
@@ -216,10 +216,11 @@
addDisplay(480, 640)
// Act
- displayInfoManager.previewSize
+ displayInfoManager.getPreviewSize()
addDisplay(2000, 3000)
+ displayInfoManager.refresh()
// Assert
- assertEquals(Size(1920, 1080), displayInfoManager.previewSize)
+ assertEquals(Size(1920, 1080), displayInfoManager.getPreviewSize())
}
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
index b687bf0..f0b520c 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
@@ -100,7 +100,7 @@
}
val fakeStreamConfigurationMap = StreamConfigurationMapCompat(
streamConfigurationMap,
- OutputSizesCorrector(cameraProperties.metadata)
+ OutputSizesCorrector(cameraProperties.metadata, streamConfigurationMap)
)
val fakeCameraQuirks = CameraQuirks(
cameraProperties.metadata,
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
index 2d941f0..973ad8b 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
@@ -56,7 +56,6 @@
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.InitializationException;
-import androidx.camera.core.ResolutionSelector;
import androidx.camera.core.UseCase;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.CameraCaptureResult;
@@ -69,6 +68,8 @@
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.StreamSpec;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
+import androidx.camera.core.resolutionselector.HighResolution;
+import androidx.camera.core.resolutionselector.ResolutionSelector;
import androidx.camera.testing.CameraUtil;
import androidx.camera.testing.HandlerUtil;
import androidx.camera.testing.fakes.FakeCamera;
@@ -965,7 +966,8 @@
// Creates a test use case with high resolution enabled.
ResolutionSelector highResolutionSelector =
- new ResolutionSelector.Builder().setHighResolutionEnabled(true).build();
+ new ResolutionSelector.Builder().setHighResolutionEnabledFlags(
+ HighResolution.FLAG_DEFAULT_MODE_ON).build();
FakeUseCaseConfig.Builder configBuilder =
new FakeUseCaseConfig.Builder().setSessionOptionUnpacker(
new Camera2SessionOptionUnpacker()).setTargetName(
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2UseCaseConfigFactory.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2UseCaseConfigFactory.java
index fe5cae3..66ce64d 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2UseCaseConfigFactory.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2UseCaseConfigFactory.java
@@ -17,6 +17,7 @@
package androidx.camera.camera2.internal;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_MAX_RESOLUTION;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ROTATION;
import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_CAPTURE_CONFIG;
@@ -26,6 +27,7 @@
import android.content.Context;
import android.hardware.camera2.CameraDevice;
+import android.util.Size;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
@@ -37,6 +39,8 @@
import androidx.camera.core.impl.OptionsBundle;
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.UseCaseConfigFactory;
+import androidx.camera.core.resolutionselector.ResolutionSelector;
+import androidx.camera.core.resolutionselector.ResolutionStrategy;
/**
* Implementation of UseCaseConfigFactory to provide the default camera2 configurations for use
@@ -90,7 +94,7 @@
captureBuilder.setTemplateType(
captureMode == ImageCapture.CAPTURE_MODE_ZERO_SHUTTER_LAG
? CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG :
- CameraDevice.TEMPLATE_STILL_CAPTURE);
+ CameraDevice.TEMPLATE_STILL_CAPTURE);
break;
case PREVIEW:
case IMAGE_ANALYSIS:
@@ -109,8 +113,13 @@
: Camera2CaptureOptionUnpacker.INSTANCE);
if (captureType == CaptureType.PREVIEW) {
- mutableConfig.insertOption(OPTION_MAX_RESOLUTION,
- mDisplayInfoManager.getPreviewSize());
+ Size previewSize = mDisplayInfoManager.getPreviewSize();
+ mutableConfig.insertOption(OPTION_MAX_RESOLUTION, previewSize);
+ ResolutionStrategy resolutionStrategy = ResolutionStrategy.create(previewSize,
+ ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER);
+ mutableConfig.insertOption(OPTION_RESOLUTION_SELECTOR,
+ new ResolutionSelector.Builder().setResolutionStrategy(
+ resolutionStrategy).build());
}
int targetRotation = mDisplayInfoManager.getMaxSizeDisplay().getRotation();
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
index 59a07ee4..573a0d7 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
@@ -23,6 +23,7 @@
import static androidx.camera.core.impl.ImageAnalysisConfig.OPTION_OUTPUT_IMAGE_FORMAT;
import static androidx.camera.core.impl.ImageAnalysisConfig.OPTION_OUTPUT_IMAGE_ROTATION_ENABLED;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_CUSTOM_ORDERED_RESOLUTIONS;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_DEFAULT_RESOLUTION;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_MAX_RESOLUTION;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_SUPPORTED_RESOLUTIONS;
@@ -82,6 +83,10 @@
import androidx.camera.core.internal.TargetConfig;
import androidx.camera.core.internal.ThreadConfig;
import androidx.camera.core.internal.compat.quirk.OnePixelShiftQuirk;
+import androidx.camera.core.internal.utils.SizeUtil;
+import androidx.camera.core.resolutionselector.AspectRatioStrategy;
+import androidx.camera.core.resolutionselector.ResolutionSelector;
+import androidx.camera.core.resolutionselector.ResolutionStrategy;
import androidx.core.util.Preconditions;
import androidx.lifecycle.LifecycleOwner;
@@ -265,35 +270,38 @@
? mSubscribedAnalyzer.getDefaultTargetResolution() : null;
}
- if (analyzerResolution != null) {
- if (!builder.getMutableConfig().containsOption(OPTION_RESOLUTION_SELECTOR)) {
- int targetRotation = builder.getMutableConfig().retrieveOption(
- OPTION_TARGET_ROTATION, Surface.ROTATION_0);
- // analyzerResolution is a size in the sensor coordinate system, but the legacy
- // target resolution setting is in the view coordinate system. Flips the
- // analyzerResolution according to the sensor rotation degrees.
- if (cameraInfo.getSensorRotationDegrees(targetRotation) % 180 == 90) {
- analyzerResolution = new Size(/* width= */ analyzerResolution.getHeight(),
- /* height= */ analyzerResolution.getWidth());
- }
+ if (analyzerResolution == null) {
+ return builder.getUseCaseConfig();
+ }
- if (!builder.getUseCaseConfig().containsOption(OPTION_TARGET_RESOLUTION)) {
- builder.getMutableConfig().insertOption(OPTION_TARGET_RESOLUTION,
- analyzerResolution);
- }
- } else {
- // Merges analyzerResolution or default resolution to ResolutionSelector.
- ResolutionSelector resolutionSelector =
- builder.getMutableConfig().retrieveOption(OPTION_RESOLUTION_SELECTOR);
+ int targetRotation = builder.getMutableConfig().retrieveOption(
+ OPTION_TARGET_ROTATION, Surface.ROTATION_0);
+ // analyzerResolution is a size in the sensor coordinate system, but the legacy
+ // target resolution setting is in the view coordinate system. Flips the
+ // analyzerResolution according to the sensor rotation degrees.
+ if (cameraInfo.getSensorRotationDegrees(targetRotation) % 180 == 90) {
+ analyzerResolution = new Size(/* width= */ analyzerResolution.getHeight(),
+ /* height= */ analyzerResolution.getWidth());
+ }
- if (resolutionSelector.getPreferredResolution() == null) {
- ResolutionSelector.Builder resolutionSelectorBuilder =
- ResolutionSelector.Builder.fromSelector(resolutionSelector);
- resolutionSelectorBuilder.setPreferredResolution(analyzerResolution);
- builder.getMutableConfig().insertOption(OPTION_RESOLUTION_SELECTOR,
- resolutionSelectorBuilder.build());
- }
- }
+ // Merges the analyzerResolution as legacy target resolution setting so that it can take
+ // effect when running the legacy resolution selection logic flow.
+ if (!builder.getUseCaseConfig().containsOption(OPTION_TARGET_RESOLUTION)) {
+ builder.getMutableConfig().insertOption(OPTION_TARGET_RESOLUTION,
+ analyzerResolution);
+ }
+
+ // Merges the analyzerResolution to ResolutionSelector.
+ ResolutionSelector resolutionSelector =
+ builder.getMutableConfig().retrieveOption(OPTION_RESOLUTION_SELECTOR, null);
+ if (resolutionSelector != null && resolutionSelector.getResolutionStrategy() == null) {
+ ResolutionSelector.Builder resolutionSelectorBuilder =
+ ResolutionSelector.Builder.fromResolutionSelector(resolutionSelector);
+ resolutionSelectorBuilder.setResolutionStrategy(
+ ResolutionStrategy.create(analyzerResolution,
+ ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER));
+ builder.getMutableConfig().insertOption(OPTION_RESOLUTION_SELECTOR,
+ resolutionSelectorBuilder.build());
}
return builder.getUseCaseConfig();
@@ -721,6 +729,18 @@
return super.getResolutionInfo();
}
+ /**
+ * Returns the resolution selector setting.
+ *
+ * <p>This setting is set when constructing an ImageCapture using
+ * {@link Builder#setResolutionSelector(ResolutionSelector)}.
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ public ResolutionSelector getResolutionSelector() {
+ return ((ImageOutputConfig) getCurrentConfig()).getResolutionSelector(null);
+ }
+
@Override
@NonNull
public String toString() {
@@ -985,13 +1005,21 @@
private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 1;
private static final int DEFAULT_ASPECT_RATIO = AspectRatio.RATIO_4_3;
+ private static final ResolutionSelector DEFAULT_RESOLUTION_SELECTOR =
+ new ResolutionSelector.Builder().setAspectRatioStrategy(
+ AspectRatioStrategy.RATIO_4_3_FALLBACK_AUTO_STRATEGY).setResolutionStrategy(
+ ResolutionStrategy.create(SizeUtil.RESOLUTION_VGA,
+ ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER))
+ .build();
+
private static final ImageAnalysisConfig DEFAULT_CONFIG;
static {
Builder builder = new Builder()
.setDefaultResolution(DEFAULT_TARGET_RESOLUTION)
.setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY)
- .setTargetAspectRatio(DEFAULT_ASPECT_RATIO);
+ .setTargetAspectRatio(DEFAULT_ASPECT_RATIO)
+ .setResolutionSelector(DEFAULT_RESOLUTION_SELECTOR);
DEFAULT_CONFIG = builder.getUseCaseConfig();
}
@@ -1383,7 +1411,7 @@
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
public Builder setDefaultResolution(@NonNull Size resolution) {
- getMutableConfig().insertOption(ImageOutputConfig.OPTION_DEFAULT_RESOLUTION,
+ getMutableConfig().insertOption(OPTION_DEFAULT_RESOLUTION,
resolution);
return this;
}
@@ -1415,14 +1443,14 @@
/**
* Sets the resolution selector to select the preferred supported resolution.
*
- * <p>ImageAnalysis has a default minimal bounding size as 640x480. The input
- * {@link ResolutionSelector}'s' preferred resolution can override the minimal bounding
- * size to find the best resolution.
+ * <p>ImageAnalysis has a default {@link ResolutionStrategy} with bound size as 640x480
+ * and fallback rule of {@link ResolutionStrategy#FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER}.
+ * Applications can override this default strategy with a different resolution strategy.
*
- * <p>When using the {@code camera-camera2} CameraX implementation, which resolution will
- * be finally selected will depend on the camera device's hardware level, capabilities
- * and the bound use cases combination. The device hardware level and capabilities
- * information can be retrieved via the interop class
+ * <p>When using the {@code camera-camera2} CameraX implementation, which resolution is
+ * finally selected depends on the camera device's hardware level, capabilities and the
+ * bound use cases combination. The device hardware level and capabilities information
+ * can be retrieved via the interop class
* {@link androidx.camera.camera2.interop.Camera2CameraInfo#getCameraCharacteristic(android.hardware.camera2.CameraCharacteristics.Key)}
* with
* {@link android.hardware.camera2.CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL} and
@@ -1431,13 +1459,14 @@
* <p>A {@code LIMITED-level} above device can support a {@code RECORD} size resolution
* for {@link ImageAnalysis} when it is bound together with {@link Preview} and
* {@link ImageCapture}. The trade-off is the selected resolution for the
- * {@link ImageCapture} will also be restricted by the {@code RECORD} size. To
- * successfully select a {@code RECORD} size resolution for {@link ImageAnalysis}, a
- * {@code RECORD} size preferred resolution should be set on both {@link ImageCapture} and
- * {@link ImageAnalysis}. This indicates that the application clearly understand the
- * trade-off and prefer the {@link ImageAnalysis} to have a larger resolution rather than
- * the {@link ImageCapture} to have a {@code MAXIMUM} size resolution. For the
- * definitions of {@code RECORD}, {@code MAXIMUM} sizes and more details see the
+ * {@link ImageCapture} is also restricted by the {@code RECORD} size. To successfully
+ * select a {@code RECORD} size resolution for {@link ImageAnalysis}, a
+ * {@link ResolutionStrategy} of selecting {@code RECORD} size resolution should be set
+ * on both {@link ImageCapture} and {@link ImageAnalysis}. This indicates that the
+ * application clearly understand the trade-off and prefer the {@link ImageAnalysis} to
+ * have a larger resolution rather than the {@link ImageCapture} to have a {@code MAXIMUM
+ * } size resolution. For the definitions of {@code RECORD}, {@code MAXIMUM} sizes and
+ * more details see the
* <a href="https://developer.android.com/reference/android/hardware/camera2/CameraDevice#regular-capture">Regular capture</a>
* section in {@link android.hardware.camera2.CameraDevice}'s. The {@code RECORD} size
* refers to the camera device's maximum supported recording resolution, as determined by
@@ -1447,11 +1476,13 @@
*
* <p>The existing {@link #setTargetResolution(Size)} and
* {@link #setTargetAspectRatio(int)} APIs are deprecated and are not compatible with
- * {@link ResolutionSelector}. Calling any of these APIs together with
- * {@link ResolutionSelector} will throw an {@link IllegalArgumentException} while
- * {@link #build()} is called to create the {@link ImageAnalysis} instance.
+ * {@link #setResolutionSelector(ResolutionSelector)}. Calling either of these APIs
+ * together with {@link #setResolutionSelector(ResolutionSelector)} will result in an
+ * {@link IllegalArgumentException} being thrown when you attempt to build the
+ * {@link ImageAnalysis} instance.
*
- **/
+ * @return The current Builder.
+ */
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@NonNull
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 ebae469..159f7ff 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
@@ -119,6 +119,9 @@
import androidx.camera.core.internal.compat.quirk.SoftwareJpegEncodingPreferredQuirk;
import androidx.camera.core.internal.compat.workaround.ExifRotationAvailability;
import androidx.camera.core.internal.utils.ImageUtil;
+import androidx.camera.core.resolutionselector.AspectRatioStrategy;
+import androidx.camera.core.resolutionselector.ResolutionSelector;
+import androidx.camera.core.resolutionselector.ResolutionStrategy;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.core.util.Preconditions;
import androidx.lifecycle.LifecycleOwner;
@@ -933,6 +936,18 @@
}
/**
+ * Returns the resolution selector setting.
+ *
+ * <p>This setting is set when constructing an ImageCapture using
+ * {@link Builder#setResolutionSelector(ResolutionSelector)}.
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ public ResolutionSelector getResolutionSelector() {
+ return ((ImageOutputConfig) getCurrentConfig()).getResolutionSelector(null);
+ }
+
+ /**
* Captures a new still image for in memory access.
*
* <p>The callback will be called only once for every invocation of this method. The listener
@@ -2017,11 +2032,18 @@
private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 4;
private static final int DEFAULT_ASPECT_RATIO = AspectRatio.RATIO_4_3;
+ private static final ResolutionSelector DEFAULT_RESOLUTION_SELECTOR =
+ new ResolutionSelector.Builder().setAspectRatioStrategy(
+ AspectRatioStrategy.RATIO_4_3_FALLBACK_AUTO_STRATEGY).setResolutionStrategy(
+ ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY).build();
+
private static final ImageCaptureConfig DEFAULT_CONFIG;
static {
- Builder builder = new Builder().setSurfaceOccupancyPriority(
- DEFAULT_SURFACE_OCCUPANCY_PRIORITY).setTargetAspectRatio(DEFAULT_ASPECT_RATIO);
+ Builder builder = new Builder()
+ .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY)
+ .setTargetAspectRatio(DEFAULT_ASPECT_RATIO)
+ .setResolutionSelector(DEFAULT_RESOLUTION_SELECTOR);
DEFAULT_CONFIG = builder.getUseCaseConfig();
}
@@ -2885,21 +2907,20 @@
/**
* Sets the resolution selector to select the preferred supported resolution.
*
- * <p>If no resolution selector is set, the largest available resolution will be selected
- * to use. Usually, users will intend to get the largest still image that the camera
- * device can support. Unlike {@link Builder#setTargetResolution(Size)},
- * {@link #setCropAspectRatio(Rational)} won't be automatically called to set the
- * corresponding value and crop the output image when a target resolution is set. Use
- * {@link ViewPort} instead if the output images need to be cropped in a specific
- * aspect ratio.
+ * <p>The default resolution strategy for ImageCapture is
+ * {@link ResolutionStrategy#HIGHEST_AVAILABLE_STRATEGY}, which will select the largest
+ * available resolution to use. Applications can override this default strategy with a
+ * different resolution strategy.
*
* <p>The existing {@link #setTargetResolution(Size)} and
* {@link #setTargetAspectRatio(int)} APIs are deprecated and are not compatible with
- * {@link ResolutionSelector}. Calling any of these APIs together with
- * {@link ResolutionSelector} will throw an {@link IllegalArgumentException} while
- * {@link #build()} is called to create the {@link ImageCapture} instance.
+ * {@link #setResolutionSelector(ResolutionSelector)}. Calling either of these APIs
+ * together with {@link #setResolutionSelector(ResolutionSelector)} will result in an
+ * {@link IllegalArgumentException} being thrown when you attempt to build the
+ * {@link ImageCapture} instance.
*
- **/
+ * @return The current Builder.
+ */
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@NonNull
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index 862dce9..f994a2d 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -89,6 +89,9 @@
import androidx.camera.core.processing.Node;
import androidx.camera.core.processing.SurfaceEdge;
import androidx.camera.core.processing.SurfaceProcessorNode;
+import androidx.camera.core.resolutionselector.AspectRatioStrategy;
+import androidx.camera.core.resolutionselector.ResolutionSelector;
+import androidx.camera.core.resolutionselector.ResolutionStrategy;
import androidx.core.util.Consumer;
import androidx.lifecycle.LifecycleOwner;
@@ -534,6 +537,18 @@
return super.getResolutionInfo();
}
+ /**
+ * Returns the resolution selector setting.
+ *
+ * <p>This setting is set when constructing an ImageCapture using
+ * {@link Builder#setResolutionSelector(ResolutionSelector)}.
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ public ResolutionSelector getResolutionSelector() {
+ return ((ImageOutputConfig) getCurrentConfig()).getResolutionSelector(null);
+ }
+
@NonNull
@Override
public String toString() {
@@ -573,20 +588,6 @@
builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT,
INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE);
- // Merges Preview's default max resolution setting when resolution selector is used
- ResolutionSelector resolutionSelector =
- builder.getMutableConfig().retrieveOption(OPTION_RESOLUTION_SELECTOR, null);
- if (resolutionSelector != null && resolutionSelector.getMaxResolution() == null) {
- Size maxResolution = builder.getMutableConfig().retrieveOption(OPTION_MAX_RESOLUTION);
- if (maxResolution != null) {
- ResolutionSelector.Builder resolutionSelectorBuilder =
- ResolutionSelector.Builder.fromSelector(resolutionSelector);
- resolutionSelectorBuilder.setMaxResolution(maxResolution);
- builder.getMutableConfig().insertOption(OPTION_RESOLUTION_SELECTOR,
- resolutionSelectorBuilder.build());
- }
- }
-
return builder.getUseCaseConfig();
}
@@ -738,11 +739,18 @@
private static final int DEFAULT_ASPECT_RATIO = AspectRatio.RATIO_4_3;
private static final int DEFAULT_MIRROR_MODE = MIRROR_MODE_FRONT_ON;
+ private static final ResolutionSelector DEFAULT_RESOLUTION_SELECTOR =
+ new ResolutionSelector.Builder().setAspectRatioStrategy(
+ AspectRatioStrategy.RATIO_4_3_FALLBACK_AUTO_STRATEGY).setResolutionStrategy(
+ ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY).build();
+
private static final PreviewConfig DEFAULT_CONFIG;
static {
- Builder builder = new Builder().setSurfaceOccupancyPriority(
- DEFAULT_SURFACE_OCCUPANCY_PRIORITY).setTargetAspectRatio(DEFAULT_ASPECT_RATIO);
+ Builder builder = new Builder()
+ .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY)
+ .setTargetAspectRatio(DEFAULT_ASPECT_RATIO)
+ .setResolutionSelector(DEFAULT_RESOLUTION_SELECTOR);
DEFAULT_CONFIG = builder.getUseCaseConfig();
}
@@ -1061,21 +1069,23 @@
* size match to the device's screen resolution, or to 1080p (1920x1080), whichever is
* smaller. See the
* <a href="https://developer.android.com/reference/android/hardware/camera2/CameraDevice#regular-capture">Regular capture</a>
- * section in {@link android.hardware.camera2.CameraDevice}'. If the
- * {@link ResolutionSelector} contains the max resolution setting larger than the {@code
- * PREVIEW} size, a size larger than the device's screen resolution or 1080p can be
- * selected to use for {@link Preview}.
+ * section in {@link android.hardware.camera2.CameraDevice}'. {@link Preview} has a
+ * default {@link ResolutionStrategy} with the {@code PREVIEW} bound size and
+ * {@link ResolutionStrategy#FALLBACK_RULE_CLOSEST_LOWER} to achieve this. Applications
+ * can override this default strategy with a different resolution strategy.
*
* <p>Note that due to compatibility reasons, CameraX may select a resolution that is
* larger than the default screen resolution on certain devices.
*
* <p>The existing {@link #setTargetResolution(Size)} and
* {@link #setTargetAspectRatio(int)} APIs are deprecated and are not compatible with
- * {@link ResolutionSelector}. Calling any of these APIs together with
- * {@link ResolutionSelector} will throw an {@link IllegalArgumentException} while
- * {@link #build()} is called to create the {@link Preview} instance.
+ * {@link #setResolutionSelector(ResolutionSelector)}. Calling either of these APIs
+ * together with {@link #setResolutionSelector(ResolutionSelector)} will result in an
+ * {@link IllegalArgumentException} being thrown when you attempt to build the
+ * {@link Preview} instance.
*
- **/
+ * @return The current Builder.
+ */
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
@NonNull
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ResolutionSelector.java b/camera/camera-core/src/main/java/androidx/camera/core/ResolutionSelector.java
deleted file mode 100644
index 9d18df4..0000000
--- a/camera/camera-core/src/main/java/androidx/camera/core/ResolutionSelector.java
+++ /dev/null
@@ -1,381 +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.camera.core;
-
-import android.util.Size;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-import androidx.camera.core.impl.SizeCoordinate;
-
-/**
- * A set of requirements and priorities used to select a resolution for the use case.
- *
- * <p>The resolution selection mechanism is determined by the following three steps:
- * <ol>
- * <li> Collect supported output sizes to the candidate resolution list
- * <li> Determine the selecting priority of the candidate resolution list by the preference
- * settings
- * <li> Consider all the resolution selector settings of bound use cases to find the best
- * resolution for each use case
- * </ol>
- *
- * <p>For the first step, all supported resolution output sizes are put into the candidate
- * resolution list as the base in the beginning.
- *
- * <p>ResolutionSelector provides the following two functions for applications to adjust the
- * conditions of the candidate resolutions.
- * <ul>
- * <li> {@link Builder#setMaxResolution(Size)}
- * <li> {@link Builder#setHighResolutionEnabled(boolean)}
- * </ul>
- *
- * <p>For the second step, ResolutionSelector provides the following three functions for
- * applications to determine which resolution has higher priority to be selected.
- * <ul>
- * <li> {@link Builder#setPreferredResolution(Size)}
- * <li> {@link Builder#setPreferredResolutionByViewSize(Size)}
- * <li> {@link Builder#setPreferredAspectRatio(int)}
- * </ul>
- *
- * <p>The resolution that exactly matches the preferred resolution is selected in first priority.
- * If the resolution can't be found, CameraX falls back to use the sizes of the preferred aspect
- * ratio. In this case, the preferred resolution is treated as the minimal bounding size to find
- * the best resolution.
- *
- * <p>Different types of use cases might have their own additional conditions. Please see the use
- * case config builders’ {@code setResolutionSelector()} function to know the condition details
- * for each type of use case.
- *
- * <p>For the third step, CameraX selects the final resolution for the use case based on the
- * camera device's hardware level, capabilities and the bound use case combination. Applications
- * can check which resolution is finally selected by using the use case's {@code
- * getResolutionInfo()} function.
- *
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class ResolutionSelector {
- @Nullable
- private final Size mPreferredResolution;
-
- private final SizeCoordinate mSizeCoordinate;
-
- private final int mPreferredAspectRatio;
-
- @Nullable
- private final Size mMaxResolution;
-
- private final boolean mIsHighResolutionEnabled;
-
- ResolutionSelector(int preferredAspectRatio,
- @Nullable Size preferredResolution,
- @NonNull SizeCoordinate sizeCoordinate,
- @Nullable Size maxResolution,
- boolean isHighResolutionEnabled) {
- mPreferredAspectRatio = preferredAspectRatio;
- mPreferredResolution = preferredResolution;
- mSizeCoordinate = sizeCoordinate;
- mMaxResolution = maxResolution;
- mIsHighResolutionEnabled = isHighResolutionEnabled;
- }
-
- /**
- * Retrieves the preferred aspect ratio in the ResolutionSelector.
- *
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @AspectRatio.Ratio
- public int getPreferredAspectRatio() {
- return mPreferredAspectRatio;
- }
-
- /**
- * Retrieves the preferred resolution in the ResolutionSelector.
- *
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @Nullable
- public Size getPreferredResolution() {
- return mPreferredResolution;
- }
-
- /**
- * Retrieves the size coordinate of the preferred resolution in the ResolutionSelector.
- *
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @NonNull
- public SizeCoordinate getSizeCoordinate() {
- return mSizeCoordinate;
- }
-
- /**
- * Returns the max resolution in the ResolutionSelector.
- *
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @Nullable
- public Size getMaxResolution() {
- return mMaxResolution;
- }
-
- /**
- * Returns {@code true} if high resolutions are allowed to be selected, otherwise {@code false}.
- *
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public boolean isHighResolutionEnabled() {
- return mIsHighResolutionEnabled;
- }
-
- /**
- * Builder for a {@link ResolutionSelector}.
- */
- public static final class Builder {
- @AspectRatio.Ratio
- private int mPreferredAspectRatio = AspectRatio.RATIO_4_3;
- @Nullable
- private Size mPreferredResolution = null;
- @NonNull
- private SizeCoordinate mSizeCoordinate = SizeCoordinate.CAMERA_SENSOR;
- @Nullable
- private Size mMaxResolution = null;
- private boolean mIsHighResolutionEnabled = false;
-
- /**
- * Creates a new Builder object.
- */
- public Builder() {
- }
-
- private Builder(@NonNull ResolutionSelector selector) {
- mPreferredAspectRatio = selector.getPreferredAspectRatio();
- mPreferredResolution = selector.getPreferredResolution();
- mSizeCoordinate = selector.getSizeCoordinate();
- mMaxResolution = selector.getMaxResolution();
- mIsHighResolutionEnabled = selector.isHighResolutionEnabled();
- }
-
- /**
- * Generates a Builder from another {@link ResolutionSelector} object.
- *
- * @param selector an existing {@link ResolutionSelector}.
- * @return the new Builder.
- */
- @NonNull
- public static Builder fromSelector(@NonNull ResolutionSelector selector) {
- return new Builder(selector);
- }
-
- /**
- * Sets the preferred aspect ratio that the output images are expected to have.
- *
- * <p>The aspect ratio is the ratio of width to height in the camera sensor's natural
- * orientation. If set, CameraX finds the sizes that match the aspect ratio with priority
- * . Among the sizes that match the aspect ratio, the larger the size, the higher the
- * priority.
- *
- * <p>If CameraX can't find any available sizes that match the preferred aspect ratio,
- * CameraX falls back to select the sizes with the nearest aspect ratio that can contain
- * the full field of view of the sizes with preferred aspect ratio.
- *
- * <p>If preferred aspect ratio is not set, the default aspect ratio is
- * {@link AspectRatio#RATIO_4_3}, which usually has largest field of view because most
- * camera sensor are {@code 4:3}.
- *
- * <p>This API is useful for apps that want to capture images matching the {@code 16:9}
- * display aspect ratio. Apps can set preferred aspect ratio as
- * {@link AspectRatio#RATIO_16_9} to achieve this.
- *
- * <p>The actual aspect ratio of the output may differ from the specified preferred
- * aspect ratio value. Application code should check the resulting output's resolution.
- *
- * @param preferredAspectRatio the aspect ratio you prefer to use.
- * @return the current Builder.
- */
- @NonNull
- public Builder setPreferredAspectRatio(@AspectRatio.Ratio int preferredAspectRatio) {
- mPreferredAspectRatio = preferredAspectRatio;
- return this;
- }
-
- /**
- * Sets the preferred resolution you expect to select. The resolution is expressed in the
- * camera sensor's natural orientation (landscape), which means you can set the size
- * retrieved from
- * {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputSizes} directly.
- *
- * <p>Once the preferred resolution is set, CameraX finds exactly matched size first
- * regardless of the preferred aspect ratio. This API is useful for apps that want to
- * select an exact size retrieved from
- * {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputSizes}.
- *
- * <p>If CameraX can't find the size that matches the preferred resolution, it attempts
- * to establish a minimal bound for the given resolution. The actual resolution is the
- * closest available resolution that is not smaller than the preferred resolution.
- * However, if no resolution exists that is equal to or larger than the preferred
- * resolution, the nearest available resolution smaller than the preferred resolution is
- * chosen.
- *
- * <p>When the preferred resolution is used as a minimal bound, CameraX also considers
- * the preferred aspect ratio to find the sizes that either match it or are close to it.
- * Using preferred resolution as the minimal bound is useful for apps that want to shrink
- * the size for the surface. For example, for apps that just show the camera preview in a
- * small view, apps can specify a size smaller than display size. CameraX can effectively
- * select a smaller size for better efficiency.
- *
- * <p>If both {@link Builder#setPreferredResolution(Size)} and
- * {@link Builder#setPreferredResolutionByViewSize(Size)} are invoked, which one set
- * later overrides the one set before.
- *
- * @param preferredResolution the preferred resolution expressed in the orientation of
- * the device camera sensor coordinate to choose the preferred
- * resolution from supported output sizes list.
- * @return the current Builder.
- */
- @NonNull
- public Builder setPreferredResolution(@NonNull Size preferredResolution) {
- mPreferredResolution = preferredResolution;
- mSizeCoordinate = SizeCoordinate.CAMERA_SENSOR;
- return this;
- }
-
- /**
- * Sets the preferred resolution you expect to select. The resolution is expressed in the
- * Android {@link View} coordinate system.
- *
- * <p>For phone devices, the sensor coordinate orientation usually has 90 degrees
- * difference from the phone device display’s natural orientation. Depending on the
- * display rotation value when the use case is bound, CameraX transforms the input
- * resolution into the camera sensor's natural orientation to find the best suitable
- * resolution.
- *
- * <p>Once the preferred resolution is set, CameraX finds the size that exactly matches
- * the preferred resolution first regardless of the preferred aspect ratio.
- *
- * <p>If CameraX can't find the size that matches the preferred resolution, it attempts
- * to establish a minimal bound for the given resolution. The actual resolution is the
- * closest available resolution that is not smaller than the preferred resolution.
- * However, if no resolution exists that is equal to or larger than the preferred
- * resolution, the nearest available resolution smaller than the preferred resolution is
- * chosen.
- *
- * <p>When the preferred resolution is used as a minimal bound, CameraX also considers
- * the preferred aspect ratio to find the sizes that either match it or are close to it.
- * Using Android {@link View} size as preferred resolution is useful for apps that want
- * to shrink the size for the surface. For example, for apps that just show the camera
- * preview in a small view, apps can specify the small size of Android {@link View}.
- * CameraX can effectively select a smaller size for better efficiency.
- *
- * <p>If both {@link Builder#setPreferredResolution(Size)} and
- * {@link Builder#setPreferredResolutionByViewSize(Size)} are invoked, the later setting
- * overrides the former one.
- *
- * @param preferredResolutionByViewSize the preferred resolution expressed in the
- * orientation of the app layout's Android
- * {@link View} to choose the preferred resolution
- * from supported output sizes list.
- * @return the current Builder.
- */
- @NonNull
- public Builder setPreferredResolutionByViewSize(
- @NonNull Size preferredResolutionByViewSize) {
- mPreferredResolution = preferredResolutionByViewSize;
- mSizeCoordinate = SizeCoordinate.ANDROID_VIEW;
- return this;
- }
-
- /**
- * Sets the max resolution condition for the use case.
- *
- * <p>The max resolution prevents the use case to select the sizes which either width or
- * height exceeds the specified resolution.
- *
- * <p>The resolution should be expressed in the camera sensor's natural orientation
- * (landscape).
- *
- * <p>For example, if applications want to select a resolution smaller than a specific
- * resolution to have better performance, a {@link ResolutionSelector} which sets this
- * specific resolution as the max resolution can be used. Or, if applications want to
- * select a larger resolution for a {@link Preview} which has the default max resolution
- * of the small one of device's screen size and 1080p (1920x1080), use a
- * {@link ResolutionSelector} with max resolution.
- *
- * @param resolution the max resolution limitation to choose from supported output sizes
- * list.
- * @return the current Builder.
- */
- @NonNull
- public Builder setMaxResolution(@NonNull Size resolution) {
- mMaxResolution = resolution;
- return this;
- }
-
- /**
- * Sets whether high resolutions are allowed to be selected for the use cases.
- *
- * <p>Calling this function allows the use case to select the high resolution output
- * sizes if it is supported for the camera device.
- *
- * <p>When high resolution is enabled, if an {@link ImageCapture} with
- * {@link ImageCapture#CAPTURE_MODE_ZERO_SHUTTER_LAG} mode is bound, the
- * {@link ImageCapture#CAPTURE_MODE_ZERO_SHUTTER_LAG} mode is forced disabled.
- *
- * <p>When using the {@code camera-extensions} to enable an extension mode, even if high
- * resolution is enabled, the supported high resolution output sizes are still excluded
- * from the candidate resolution list.
- *
- * <p>When using the {@code camera-camera2} CameraX implementation, the supported
- * high resolutions are retrieved from
- * {@link android.hardware.camera2.params.StreamConfigurationMap#getHighResolutionOutputSizes(int)}.
- * Be noticed that the high resolution sizes might cause the entire capture session to
- * not meet the 20 fps frame rate. Even if only an ImageCapture use case selects a high
- * resolution, it might still impact the FPS of the Preview, ImageAnalysis or
- * VideoCapture use cases which are bound together. This function only takes effect on
- * devices with
- * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE}
- * capability. For devices without
- * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE}
- * capability, all resolutions can be retrieved from
- * {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputSizes(int)},
- * but it is not guaranteed to meet >= 20 fps for any resolution in the list.
- *
- * @param enabled {@code true} to allow to select high resolution for the use case.
- * @return the current Builder.
- */
- @NonNull
- public Builder setHighResolutionEnabled(boolean enabled) {
- mIsHighResolutionEnabled = enabled;
- return this;
- }
-
- /**
- * Builds the {@link ResolutionSelector}.
- *
- * @return the {@link ResolutionSelector} built with the specified resolution settings.
- */
- @NonNull
- public ResolutionSelector build() {
- return new ResolutionSelector(mPreferredAspectRatio, mPreferredResolution,
- mSizeCoordinate, mMaxResolution, mIsHighResolutionEnabled);
- }
- }
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
index 362e463..3257ece 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
@@ -19,6 +19,9 @@
import static androidx.camera.core.MirrorMode.MIRROR_MODE_FRONT_ON;
import static androidx.camera.core.MirrorMode.MIRROR_MODE_OFF;
import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_RESOLUTION;
import static androidx.camera.core.impl.utils.TransformUtils.within360;
import static androidx.camera.core.processing.TargetUtils.isSuperset;
import static androidx.core.util.Preconditions.checkArgument;
@@ -209,6 +212,16 @@
mergedConfig = MutableOptionsBundle.create();
}
+ // Removes the default resolution selector setting to go for the legacy resolution
+ // selection logic flow if applications call the legacy setTargetAspectRatio and
+ // setTargetResolution APIs to do the setting.
+ if (mUseCaseConfig.containsOption(OPTION_TARGET_ASPECT_RATIO)
+ || mUseCaseConfig.containsOption(OPTION_TARGET_RESOLUTION)) {
+ if (mergedConfig.containsOption(OPTION_RESOLUTION_SELECTOR)) {
+ mergedConfig.removeOption(OPTION_RESOLUTION_SELECTOR);
+ }
+ }
+
// If any options need special handling, this is the place to do it. For now we'll just copy
// over all options.
for (Option<?> opt : mUseCaseConfig.listOptions()) {
@@ -247,7 +260,8 @@
// Forces disable ZSL when high resolution is enabled.
if (mergedConfig.containsOption(ImageOutputConfig.OPTION_RESOLUTION_SELECTOR)
&& mergedConfig.retrieveOption(
- ImageOutputConfig.OPTION_RESOLUTION_SELECTOR).isHighResolutionEnabled()) {
+ ImageOutputConfig.OPTION_RESOLUTION_SELECTOR).getHighResolutionEnabledFlags()
+ != 0) {
mergedConfig.insertOption(UseCaseConfig.OPTION_ZSL_DISABLED, true);
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/Config.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/Config.java
index 2d3ae77..67493a2 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/Config.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/Config.java
@@ -19,9 +19,12 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.utils.ResolutionSelectorUtil;
+import androidx.camera.core.resolutionselector.ResolutionSelector;
import com.google.auto.value.AutoValue;
+import java.util.Objects;
import java.util.Set;
/**
@@ -321,11 +324,23 @@
// just copy over all options.
for (Config.Option<?> opt : extendedConfig.listOptions()) {
@SuppressWarnings("unchecked") // Options/values are being copied directly
- Config.Option<Object> objectOpt = (Config.Option<Object>) opt;
+ Config.Option<Object> objectOpt = (Config.Option<Object>) opt;
- mergedConfig.insertOption(objectOpt,
- extendedConfig.getOptionPriority(opt),
- extendedConfig.retrieveOption(objectOpt));
+ // ResolutionSelector needs special handling to merge the underlying settings.
+ if (Objects.equals(objectOpt, ImageOutputConfig.OPTION_RESOLUTION_SELECTOR)) {
+ ResolutionSelector resolutionSelectorToOverride =
+ (ResolutionSelector) extendedConfig.retrieveOption(objectOpt);
+ ResolutionSelector baseResolutionSelector =
+ (ResolutionSelector) baseConfig.retrieveOption(objectOpt);
+ mergedConfig.insertOption(objectOpt,
+ extendedConfig.getOptionPriority(opt),
+ ResolutionSelectorUtil.overrideResolutionSelectors(
+ baseResolutionSelector, resolutionSelectorToOverride));
+ } else {
+ mergedConfig.insertOption(objectOpt,
+ extendedConfig.getOptionPriority(opt),
+ extendedConfig.retrieveOption(objectOpt));
+ }
}
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java
index a2168c3..390e103 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java
@@ -29,7 +29,7 @@
import androidx.annotation.RequiresApi;
import androidx.camera.core.AspectRatio;
import androidx.camera.core.MirrorMode;
-import androidx.camera.core.ResolutionSelector;
+import androidx.camera.core.resolutionselector.ResolutionSelector;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ResolutionSelectorUtil.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ResolutionSelectorUtil.java
new file mode 100644
index 0000000..da7aaa3
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ResolutionSelectorUtil.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.impl.utils;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.resolutionselector.ResolutionSelector;
+
+/**
+ * Utility class for resolution selector related operations.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class ResolutionSelectorUtil {
+ private ResolutionSelectorUtil() {
+ }
+
+ /**
+ * Merges two resolution selectors.
+ *
+ * @param baseResolutionSelector the base resolution selector.
+ * @param resolutionSelectorToOverride the resolution selector that the inside settings can
+ * override the corresponding setting in the base
+ * resolution selector.
+ * @return {@code null} if both the input resolution selectors are null. Otherwise, returns
+ * the merged resolution selector.
+ */
+ @Nullable
+ public static ResolutionSelector overrideResolutionSelectors(
+ @Nullable ResolutionSelector baseResolutionSelector,
+ @Nullable ResolutionSelector resolutionSelectorToOverride) {
+ if (resolutionSelectorToOverride == null) {
+ return baseResolutionSelector;
+ } else if (baseResolutionSelector == null) {
+ return resolutionSelectorToOverride;
+ }
+
+ ResolutionSelector.Builder builder =
+ ResolutionSelector.Builder.fromResolutionSelector(baseResolutionSelector);
+
+ if (resolutionSelectorToOverride.getAspectRatioStrategy() != null) {
+ builder.setAspectRatioStrategy(resolutionSelectorToOverride.getAspectRatioStrategy());
+ }
+
+ if (resolutionSelectorToOverride.getResolutionStrategy() != null) {
+ builder.setResolutionStrategy(resolutionSelectorToOverride.getResolutionStrategy());
+ }
+
+ if (resolutionSelectorToOverride.getResolutionFilter() != null) {
+ builder.setResolutionFilter(resolutionSelectorToOverride.getResolutionFilter());
+ }
+
+ if (resolutionSelectorToOverride.getHighResolutionEnabledFlags() != 0) {
+ builder.setHighResolutionEnabledFlags(
+ resolutionSelectorToOverride.getHighResolutionEnabledFlags());
+ }
+
+ return builder.build();
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java
index ad15a23..4c44a62 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java
@@ -26,41 +26,58 @@
import android.util.Pair;
import android.util.Rational;
import android.util.Size;
+import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.camera.core.AspectRatio;
+import androidx.camera.core.CameraSelector;
import androidx.camera.core.Logger;
-import androidx.camera.core.ResolutionSelector;
import androidx.camera.core.impl.CameraInfoInternal;
import androidx.camera.core.impl.ImageOutputConfig;
import androidx.camera.core.impl.UseCaseConfig;
import androidx.camera.core.impl.utils.AspectRatioUtil;
+import androidx.camera.core.impl.utils.CameraOrientationUtil;
import androidx.camera.core.impl.utils.CompareSizesByArea;
-import androidx.camera.core.internal.utils.SizeUtil;
-import androidx.core.util.Preconditions;
+import androidx.camera.core.resolutionselector.AspectRatioStrategy;
+import androidx.camera.core.resolutionselector.ResolutionFilter;
+import androidx.camera.core.resolutionselector.ResolutionSelector;
+import androidx.camera.core.resolutionselector.ResolutionStrategy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
- * A class used to sort the supported output sizes according to the use case configs
+ * The supported output sizes collector to help collect the available resolution candidate list
+ * according to the use case config and the following settings in {@link ResolutionSelector}:
+ *
+ * <ul>
+ * <li>Aspect ratio strategy
+ * <li>Resolution strategy
+ * <li>Custom resolution filter
+ * <li>High resolution enabled flags
+ * </ul>
*/
@RequiresApi(21)
public class SupportedOutputSizesSorter {
private static final String TAG = "SupportedOutputSizesCollector";
private final CameraInfoInternal mCameraInfoInternal;
+ private final int mSensorOrientation;
+ private final int mLensFacing;
private final Rational mFullFovRatio;
private final boolean mIsSensorLandscapeResolution;
private final SupportedOutputSizesSorterLegacy mSupportedOutputSizesSorterLegacy;
public SupportedOutputSizesSorter(@NonNull CameraInfoInternal cameraInfoInternal) {
mCameraInfoInternal = cameraInfoInternal;
+ mSensorOrientation = mCameraInfoInternal.getSensorRotationDegrees();
+ mLensFacing = mCameraInfoInternal.getLensFacing();
mFullFovRatio = calculateFullFovRatio(mCameraInfoInternal);
// Determines the sensor resolution orientation info by the full FOV ratio.
mIsSensorLandscapeResolution = mFullFovRatio != null ? mFullFovRatio.getNumerator()
@@ -86,6 +103,13 @@
return new Rational(maxSize.getWidth(), maxSize.getHeight());
}
+ /**
+ * Returns the sorted output sizes according to the use case config.
+ *
+ * <p>If ResolutionSelector is specified in the use case config, the output sizes will be
+ * sorted according to the ResolutionSelector setting and logic. Otherwise, the output sizes
+ * will be sorted according to the legacy resolution API settings and logic.
+ */
@NonNull
public List<Size> getSortedSupportedOutputSizes(@NonNull UseCaseConfig<?> useCaseConfig) {
ImageOutputConfig imageOutputConfig = (ImageOutputConfig) useCaseConfig;
@@ -96,50 +120,16 @@
return customOrderedResolutions;
}
- // Retrieves the resolution candidate list according to the use case config if
- List<Size> resolutionCandidateList = getResolutionCandidateList(useCaseConfig);
-
ResolutionSelector resolutionSelector = imageOutputConfig.getResolutionSelector(null);
if (resolutionSelector == null) {
return mSupportedOutputSizesSorterLegacy.sortSupportedOutputSizes(
- resolutionCandidateList, useCaseConfig);
+ getResolutionCandidateList(useCaseConfig), useCaseConfig);
} else {
- Size miniBoundingSize = resolutionSelector.getPreferredResolution();
- if (miniBoundingSize == null) {
- miniBoundingSize = imageOutputConfig.getDefaultResolution(null);
- }
- return sortSupportedOutputSizesByResolutionSelector(resolutionCandidateList,
- resolutionSelector, miniBoundingSize);
+ return sortSupportedOutputSizesByResolutionSelector(useCaseConfig);
}
}
- @NonNull
- private List<Size> getResolutionCandidateList(@NonNull UseCaseConfig<?> useCaseConfig) {
- int imageFormat = useCaseConfig.getInputFormat();
- ImageOutputConfig imageOutputConfig = (ImageOutputConfig) useCaseConfig;
- // Tries to get the custom supported resolutions list if it is set
- List<Size> resolutionCandidateList = getCustomizedSupportedResolutionsFromConfig(
- imageFormat, imageOutputConfig);
-
- // Tries to get the supported output sizes from the CameraInfoInternal if both custom
- // ordered and supported resolutions lists are not set.
- if (resolutionCandidateList == null) {
- resolutionCandidateList = mCameraInfoInternal.getSupportedResolutions(imageFormat);
- }
-
- // Appends high resolution output sizes if high resolution is enabled by ResolutionSelector
- if (imageOutputConfig.getResolutionSelector(null) != null
- && imageOutputConfig.getResolutionSelector().isHighResolutionEnabled()) {
- List<Size> allSizesList = new ArrayList<>();
- allSizesList.addAll(resolutionCandidateList);
- allSizesList.addAll(mCameraInfoInternal.getSupportedHighResolutions(imageFormat));
- return allSizesList;
- }
-
- return resolutionCandidateList;
- }
-
/**
* Retrieves the customized supported resolutions from the use case config.
*
@@ -171,148 +161,56 @@
}
/**
- * Sorts the resolution candidate list by the following steps:
+ * Sorts the resolution candidate list according to the ResolutionSelector API logic.
*
- * 1. Filters out the candidate list according to the max resolution.
- * 2. Sorts the candidate list according to ResolutionSelector strategies.
+ * <ol>
+ * <li>Collects the output sizes
+ * <ul>
+ * <li>Applies the high resolution settings
+ * </ul>
+ * <li>Applies the aspect ratio strategy
+ * <ul>
+ * <li>Applies the aspect ratio strategy fallback rule
+ * </ul>
+ * <li>Applies the resolution strategy
+ * <ul>
+ * <li>Applies the resolution strategy fallback rule
+ * </ul>
+ * <li>Applies the resolution filter
+ * </ol>
+ *
+ * @return a size list which has been filtered and sorted by the specified resolution
+ * selector settings.
+ * @throws IllegalArgumentException if the specified resolution filter returns any size which
+ * is not included in the provided supported size list.
*/
@NonNull
private List<Size> sortSupportedOutputSizesByResolutionSelector(
- @NonNull List<Size> resolutionCandidateList,
- @NonNull ResolutionSelector resolutionSelector,
- @Nullable Size miniBoundingSize) {
- if (resolutionCandidateList.isEmpty()) {
- return resolutionCandidateList;
+ @NonNull UseCaseConfig<?> useCaseConfig) {
+ ResolutionSelector resolutionSelector =
+ ((ImageOutputConfig) useCaseConfig).getResolutionSelector();
+
+ // Retrieves the normal supported output sizes.
+ List<Size> resolutionCandidateList = getResolutionCandidateList(useCaseConfig);
+
+ // Applies the high resolution settings onto the resolution candidate list.
+ if (!useCaseConfig.isHigResolutionDisabled(false)) {
+ resolutionCandidateList = applyHighResolutionSettings(resolutionCandidateList,
+ resolutionSelector, useCaseConfig.getInputFormat());
}
- List<Size> descendingSizeList = new ArrayList<>(resolutionCandidateList);
+ // Applies the aspect ratio strategy onto the resolution candidate list.
+ LinkedHashMap<Rational, List<Size>> aspectRatioSizeListMap =
+ applyAspectRatioStrategy(resolutionCandidateList,
+ resolutionSelector.getAspectRatioStrategy());
- // Sort the result sizes. The Comparator result must be reversed to have a descending
- // order result.
- Collections.sort(descendingSizeList, new CompareSizesByArea(true));
+ // Applies the resolution strategy onto the resolution candidate list.
+ applyResolutionStrategy(aspectRatioSizeListMap, resolutionSelector.getResolutionStrategy());
- // 1. Filters out the candidate list according to the min size bound and max resolution.
- List<Size> filteredSizeList = filterOutResolutionCandidateListByMaxResolutionSetting(
- descendingSizeList, resolutionSelector);
-
- // 2. Sorts the candidate list according to the rules of new Resolution API.
- return sortResolutionCandidateListByTargetAspectRatioAndResolutionSettings(
- filteredSizeList, resolutionSelector, miniBoundingSize);
-
- }
-
- /**
- * Filters out the resolution candidate list by the max resolution setting.
- *
- * The input size list should have been sorted in descending order.
- */
- private static List<Size> filterOutResolutionCandidateListByMaxResolutionSetting(
- @NonNull List<Size> resolutionCandidateList,
- @NonNull ResolutionSelector resolutionSelector) {
- // Retrieves the max resolution setting. When ResolutionSelector is used, all resolution
- // selection logic should depend on ResolutionSelector's settings.
- Size maxResolution = resolutionSelector.getMaxResolution();
-
- if (maxResolution == null) {
- return resolutionCandidateList;
- }
-
- // Filter out the resolution candidate list by the max resolution. Sizes that any edge
- // exceeds the max resolution will be filtered out.
+ // Collects all sizes from the sorted aspect ratio size groups into the final sorted list.
List<Size> resultList = new ArrayList<>();
- for (Size outputSize : resolutionCandidateList) {
- if (!SizeUtil.isLongerInAnyEdge(outputSize, maxResolution)) {
- resultList.add(outputSize);
- }
- }
-
- if (resultList.isEmpty()) {
- throw new IllegalArgumentException(
- "Resolution candidate list is empty after filtering out by the settings!");
- }
-
- return resultList;
- }
-
- /**
- * Sorts the resolution candidate list according to the new ResolutionSelector API logic.
- *
- * The list will be sorted by the following order:
- * 1. size of preferred resolution
- * 2. a resolution with preferred aspect ratio, is not smaller than, and is closest to the
- * preferred resolution.
- * 3. resolutions with preferred aspect ratio and is smaller than the preferred resolution
- * size in descending order of resolution area size.
- * 4. Other sizes sorted by CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace and
- * area size.
- */
- @NonNull
- private List<Size> sortResolutionCandidateListByTargetAspectRatioAndResolutionSettings(
- @NonNull List<Size> resolutionCandidateList,
- @NonNull ResolutionSelector resolutionSelector,
- @Nullable Size miniBoundingSize) {
- Rational aspectRatio = getTargetAspectRatioRationalValue(
- resolutionSelector.getPreferredAspectRatio(), mIsSensorLandscapeResolution);
- Preconditions.checkNotNull(aspectRatio, "ResolutionSelector should also have aspect ratio"
- + " value.");
-
- Size targetSize = resolutionSelector.getPreferredResolution();
- List<Size> resultList = sortResolutionCandidateListByTargetAspectRatioAndSize(
- resolutionCandidateList, aspectRatio, miniBoundingSize);
-
- // Moves the target size to the first position if it exists in the resolution candidate
- // list.
- if (resultList.contains(targetSize)) {
- resultList.remove(targetSize);
- resultList.add(0, targetSize);
- }
-
- return resultList;
- }
-
- /**
- * Sorts the resolution candidate list according to the target aspect ratio and size settings.
- *
- * 1. The resolution candidate list will be grouped by aspect ratio.
- * 2. Moves the smallest size larger than the mini bounding size to the first position for each
- * aspect ratio sizes group.
- * 3. The aspect ratios of groups will be sorted against to the target aspect ratio setting by
- * CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace.
- * 4. Concatenate all sizes as the result list
- */
- @NonNull
- private List<Size> sortResolutionCandidateListByTargetAspectRatioAndSize(
- @NonNull List<Size> resolutionCandidateList, @NonNull Rational aspectRatio,
- @Nullable Size miniBoundingSize) {
- // Rearrange the supported size to put the ones with the same aspect ratio in the front
- // of the list and put others in the end from large to small. Some low end devices may
- // not able to get an supported resolution that match the preferred aspect ratio.
-
- // Group output sizes by aspect ratio.
- Map<Rational, List<Size>> aspectRatioSizeListMap =
- groupSizesByAspectRatio(resolutionCandidateList);
-
- // If the target resolution is set, use it to remove unnecessary larger sizes.
- if (miniBoundingSize != null) {
- // Sorts sizes from each aspect ratio size list
- for (Rational key : aspectRatioSizeListMap.keySet()) {
- List<Size> sortedResult = sortSupportedSizesByMiniBoundingSize(
- aspectRatioSizeListMap.get(key), miniBoundingSize);
- aspectRatioSizeListMap.put(key, sortedResult);
- }
- }
-
- // Sort the aspect ratio key set by the target aspect ratio.
- List<Rational> aspectRatios = new ArrayList<>(aspectRatioSizeListMap.keySet());
- Collections.sort(aspectRatios,
- new AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
- aspectRatio, mFullFovRatio));
-
- List<Size> resultList = new ArrayList<>();
-
- // Put available sizes into final result list by aspect ratio distance to target ratio.
- for (Rational rational : aspectRatios) {
- for (Size size : aspectRatioSizeListMap.get(rational)) {
+ for (List<Size> sortedSizes : aspectRatioSizeListMap.values()) {
+ for (Size size : sortedSizes) {
// A size may exist in multiple groups in mod16 condition. Keep only one in
// the final list.
if (!resultList.contains(size)) {
@@ -321,7 +219,322 @@
}
}
- return resultList;
+ // Applies the resolution filter onto the resolution candidate list.
+ return applyResolutionFilter(resultList, resolutionSelector.getResolutionFilter(),
+ ((ImageOutputConfig) useCaseConfig).getTargetRotation(Surface.ROTATION_0));
+ }
+
+ /**
+ * Returns the normal supported output sizes.
+ *
+ * <p>When using camera-camera2 implementation, the output sizes are retrieved via
+ * StreamConfigurationMap#getOutputSizes().
+ *
+ * @return the resolution candidate list sorted in descending order.
+ */
+ @NonNull
+ private List<Size> getResolutionCandidateList(@NonNull UseCaseConfig<?> useCaseConfig) {
+ int imageFormat = useCaseConfig.getInputFormat();
+ ImageOutputConfig imageOutputConfig = (ImageOutputConfig) useCaseConfig;
+ // Tries to get the custom supported resolutions list if it is set
+ List<Size> resolutionCandidateList = getCustomizedSupportedResolutionsFromConfig(
+ imageFormat, imageOutputConfig);
+
+ // Tries to get the supported output sizes from the CameraInfoInternal if both custom
+ // ordered and supported resolutions lists are not set.
+ if (resolutionCandidateList == null) {
+ resolutionCandidateList = mCameraInfoInternal.getSupportedResolutions(imageFormat);
+ }
+
+ Collections.sort(resolutionCandidateList, new CompareSizesByArea(true));
+
+ return resolutionCandidateList;
+ }
+
+ /**
+ * Appends the high resolution supported output sizes according to the high resolution settings.
+ *
+ * <p>When using camera-camera2 implementation, the output sizes are retrieved via
+ * StreamConfigurationMap#getHighResolutionOutputSizes().
+ *
+ * @param resolutionCandidateList the supported size list which contains only normal output
+ * sizes.
+ * @param resolutionSelector the specified resolution selector.
+ * @param imageFormat the desired image format for the target use case.
+ * @return the resolution candidate list including the high resolution output sizes sorted in
+ * descending order.
+ */
+ @NonNull
+ private List<Size> applyHighResolutionSettings(@NonNull List<Size> resolutionCandidateList,
+ @NonNull ResolutionSelector resolutionSelector, int imageFormat) {
+ // Appends high resolution output sizes if high resolution is enabled by ResolutionSelector
+ if (resolutionSelector.getHighResolutionEnabledFlags() != 0) {
+ List<Size> allSizesList = new ArrayList<>();
+ allSizesList.addAll(resolutionCandidateList);
+ allSizesList.addAll(mCameraInfoInternal.getSupportedHighResolutions(imageFormat));
+ Collections.sort(allSizesList, new CompareSizesByArea(true));
+ return allSizesList;
+ }
+
+ return resolutionCandidateList;
+ }
+
+ /**
+ * Applies the aspect ratio strategy onto the input resolution candidate list.
+ *
+ * @param resolutionCandidateList the supported sizes list which has been sorted in
+ * descending order.
+ * @param aspectRatioStrategy the specified aspect ratio strategy.
+ * @return an aspect ratio to size list linked hash map which the aspect ratio fallback rule
+ * is applied and is sorted against the preferred aspect ratio.
+ */
+ @NonNull
+ private LinkedHashMap<Rational, List<Size>> applyAspectRatioStrategy(
+ @NonNull List<Size> resolutionCandidateList,
+ @NonNull AspectRatioStrategy aspectRatioStrategy) {
+ // Group output sizes by aspect ratio.
+ Map<Rational, List<Size>> aspectRatioSizeListMap =
+ groupSizesByAspectRatio(resolutionCandidateList);
+
+ // Applies the aspect ratio fallback rule
+ return applyAspectRatioStrategyFallbackRule(aspectRatioSizeListMap, aspectRatioStrategy);
+ }
+
+ /**
+ * Applies the aspect ratio strategy fallback rule to the aspect ratio to size list map.
+ *
+ * @param sizeGroupsMap the aspect ratio to size list map. The size list should have been
+ * sorted in descending order.
+ * @param aspectRatioStrategy the specified aspect ratio strategy.
+ * @return an aspect ratio to size list linked hash map which the aspect ratio fallback rule
+ * is applied and is sorted against the preferred aspect ratio.
+ */
+ private LinkedHashMap<Rational, List<Size>> applyAspectRatioStrategyFallbackRule(
+ @NonNull Map<Rational, List<Size>> sizeGroupsMap,
+ @NonNull AspectRatioStrategy aspectRatioStrategy) {
+ Rational aspectRatio = getTargetAspectRatioRationalValue(
+ aspectRatioStrategy.getPreferredAspectRatio(), mIsSensorLandscapeResolution);
+
+ // Remove items of all other aspect ratios if the fallback rule is AspectRatioStrategy
+ // .FALLBACK_RULE_NONE
+ if (aspectRatioStrategy.getFallbackRule() == AspectRatioStrategy.FALLBACK_RULE_NONE) {
+ Rational preferredAspectRatio = getTargetAspectRatioRationalValue(
+ aspectRatioStrategy.getPreferredAspectRatio(), mIsSensorLandscapeResolution);
+ for (Rational ratio : new ArrayList<>(sizeGroupsMap.keySet())) {
+ if (!ratio.equals(preferredAspectRatio)) {
+ sizeGroupsMap.remove(ratio);
+ }
+ }
+ }
+
+ // Sorts the aspect ratio key set by the preferred aspect ratio.
+ List<Rational> aspectRatios = new ArrayList<>(sizeGroupsMap.keySet());
+ Collections.sort(aspectRatios,
+ new AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
+ aspectRatio, mFullFovRatio));
+
+ // Stores the size groups into LinkedHashMap to keep the order
+ LinkedHashMap<Rational, List<Size>> sortedAspectRatioSizeListMap = new LinkedHashMap<>();
+ for (Rational ratio : aspectRatios) {
+ sortedAspectRatioSizeListMap.put(ratio, sizeGroupsMap.get(ratio));
+ }
+
+ return sortedAspectRatioSizeListMap;
+ }
+
+ /**
+ * Applies the resolution strategy onto the aspect ratio to size list linked hash map.
+ *
+ * <p>The resolution fallback rule is applied to filter out and sort the sizes in the
+ * underlying size list.
+ *
+ * @param sortedAspectRatioSizeListMap the aspect ratio to size list linked hash map. The
+ * entries order should not be changed.
+ * @param resolutionStrategy the resolution strategy to sort the candidate
+ * resolutions.
+ */
+ private static void applyResolutionStrategy(
+ @NonNull LinkedHashMap<Rational, List<Size>> sortedAspectRatioSizeListMap,
+ @Nullable ResolutionStrategy resolutionStrategy) {
+ if (resolutionStrategy == null) {
+ return;
+ }
+
+ // Applies the resolution strategy with the specified fallback rule
+ for (Rational key : sortedAspectRatioSizeListMap.keySet()) {
+ applyResolutionStrategyFallbackRule(sortedAspectRatioSizeListMap.get(key),
+ resolutionStrategy);
+ }
+ }
+
+ /**
+ * Applies the resolution strategy fallback rule to the size list.
+ *
+ * @param supportedSizesList the supported sizes list which has been sorted in descending order.
+ * @param resolutionStrategy the resolution strategy to sort the candidate resolutions.
+ */
+ private static void applyResolutionStrategyFallbackRule(
+ @NonNull List<Size> supportedSizesList,
+ @NonNull ResolutionStrategy resolutionStrategy) {
+ if (supportedSizesList.isEmpty()) {
+ return;
+ }
+ Integer fallbackRule = resolutionStrategy.getFallbackRule();
+
+ if (fallbackRule == null) {
+ // Do nothing for HIGHEST_AVAILABLE_STRATEGY case.
+ return;
+ }
+
+ Size boundSize = resolutionStrategy.getBoundSize();
+
+ switch (fallbackRule) {
+ case ResolutionStrategy.FALLBACK_RULE_NONE:
+ sortSupportedSizesByFallbackRuleNone(supportedSizesList, boundSize);
+ break;
+ case ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER:
+ sortSupportedSizesByFallbackRuleClosestHigherThenLower(supportedSizesList,
+ boundSize, true);
+ break;
+ case ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER:
+ sortSupportedSizesByFallbackRuleClosestHigherThenLower(supportedSizesList,
+ boundSize, false);
+ break;
+ case ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER:
+ sortSupportedSizesByFallbackRuleClosestLowerThenHigher(supportedSizesList,
+ boundSize, true);
+ break;
+ case ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER:
+ sortSupportedSizesByFallbackRuleClosestLowerThenHigher(supportedSizesList,
+ boundSize, false);
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Applies the resolution filtered to the sorted output size list.
+ *
+ * @param sizeList the supported size list which has been filtered and sorted by the
+ * specified aspect ratio, resolution strategies.
+ * @param resolutionFilter the specified resolution filter.
+ * @param targetRotation the use case target rotation info
+ * @return the result size list applied the specified resolution filter.
+ * @throws IllegalArgumentException if the specified resolution filter returns any size which
+ * is not included in the provided supported size list.
+ */
+ @NonNull
+ private List<Size> applyResolutionFilter(@NonNull List<Size> sizeList,
+ @Nullable ResolutionFilter resolutionFilter,
+ @ImageOutputConfig.RotationValue int targetRotation) {
+ if (resolutionFilter == null) {
+ return sizeList;
+ }
+
+ // Invokes ResolutionFilter#filter() to filter/sort and return the result if it is
+ // specified.
+ int destRotationDegrees = CameraOrientationUtil.surfaceRotationToDegrees(
+ targetRotation);
+ int rotationDegrees =
+ CameraOrientationUtil.getRelativeImageRotation(destRotationDegrees,
+ mSensorOrientation,
+ mLensFacing == CameraSelector.LENS_FACING_BACK);
+ List<Size> filteredResultList = resolutionFilter.filter(new ArrayList<>(sizeList),
+ rotationDegrees);
+ if (sizeList.containsAll(filteredResultList)) {
+ return filteredResultList;
+ } else {
+ throw new IllegalArgumentException("The returned sizes list of the resolution "
+ + "filter must be a subset of the provided sizes list.");
+ }
+ }
+
+ /**
+ * Sorts the size list for {@link ResolutionStrategy#FALLBACK_RULE_NONE}.
+ *
+ * @param supportedSizesList the supported sizes list which has been sorted in descending order.
+ * @param boundSize the resolution strategy bound size.
+ */
+ private static void sortSupportedSizesByFallbackRuleNone(
+ @NonNull List<Size> supportedSizesList, @NonNull Size boundSize) {
+ boolean containsBoundSize = supportedSizesList.contains(boundSize);
+ supportedSizesList.clear();
+ if (containsBoundSize) {
+ supportedSizesList.add(boundSize);
+ }
+ }
+
+ /**
+ * Sorts the size list for {@link ResolutionStrategy#FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER}
+ * or {@link ResolutionStrategy#FALLBACK_RULE_CLOSEST_HIGHER}.
+ *
+ * @param supportedSizesList the supported sizes list which has been sorted in descending order.
+ * @param boundSize the resolution strategy bound size.
+ * @param keepLowerSizes keeps the sizes lower than the bound size in the result list if
+ * this is {@code true}.
+ */
+ static void sortSupportedSizesByFallbackRuleClosestHigherThenLower(
+ @NonNull List<Size> supportedSizesList, @NonNull Size boundSize,
+ boolean keepLowerSizes) {
+ List<Size> lowerSizes = new ArrayList<>();
+
+ for (int i = supportedSizesList.size() - 1; i >= 0; i--) {
+ Size outputSize = supportedSizesList.get(i);
+ if (outputSize.getWidth() < boundSize.getWidth()
+ || outputSize.getHeight() < boundSize.getHeight()) {
+ // The supportedSizesList is in descending order. Checking and put the
+ // bounding-below size at position 0 so that the largest smaller resolution
+ // will be put in the first position finally.
+ lowerSizes.add(0, outputSize);
+ } else {
+ break;
+ }
+ }
+ // Removes the lower sizes from the list
+ supportedSizesList.removeAll(lowerSizes);
+ // Reverses the list so that the smallest larger resolution will be put in the first
+ // position.
+ Collections.reverse(supportedSizesList);
+ if (keepLowerSizes) {
+ // Appends the lower sizes to the tail
+ supportedSizesList.addAll(lowerSizes);
+ }
+ }
+
+ /**
+ * Sorts the size list for {@link ResolutionStrategy#FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER}
+ * or {@link ResolutionStrategy#FALLBACK_RULE_CLOSEST_LOWER}.
+ *
+ * @param supportedSizesList the supported sizes list which has been sorted in descending order.
+ * @param boundSize the resolution strategy bound size.
+ * @param keepHigherSizes keeps the sizes higher than the bound size in the result list if
+ * this is {@code true}.
+ */
+ private static void sortSupportedSizesByFallbackRuleClosestLowerThenHigher(
+ @NonNull List<Size> supportedSizesList, @NonNull Size boundSize,
+ boolean keepHigherSizes) {
+ List<Size> higherSizes = new ArrayList<>();
+
+ for (int i = 0; i < supportedSizesList.size(); i++) {
+ Size outputSize = supportedSizesList.get(i);
+ if (outputSize.getWidth() > boundSize.getWidth()
+ || outputSize.getHeight() > boundSize.getHeight()) {
+ // The supportedSizesList is in descending order. Checking and put the
+ // bounding-above size at position 0 so that the smallest larger resolution
+ // will be put in the first position finally.
+ higherSizes.add(0, outputSize);
+ } else {
+ // Breaks the for-loop to keep the equal-to or lower sizes in the list.
+ break;
+ }
+ }
+ // Removes the higher sizes from the list
+ supportedSizesList.removeAll(higherSizes);
+ if (keepHigherSizes) {
+ // Appends the higher sizes to the tail
+ supportedSizesList.addAll(higherSizes);
+ }
}
/**
@@ -391,42 +604,8 @@
}
/**
- * Removes unnecessary sizes by target size.
- *
- * <p>If the target resolution is set, a size that is equal to or closest to the target
- * resolution will be selected. If the list includes more than one size equal to or larger
- * than the target resolution, only one closest size needs to be kept. The other larger sizes
- * can be removed so that they won't be selected to use.
- *
- * @param supportedSizesList The list should have been sorted in descending order.
- * @param miniBoundingSize The target size used to remove unnecessary sizes.
+ * Groups the input sizes into an aspect ratio to size list map.
*/
- static List<Size> sortSupportedSizesByMiniBoundingSize(@NonNull List<Size> supportedSizesList,
- @NonNull Size miniBoundingSize) {
- if (supportedSizesList.isEmpty()) {
- return supportedSizesList;
- }
-
- List<Size> resultList = new ArrayList<>();
-
- // Get the index of the item that is equal to or closest to the target size.
- for (int i = 0; i < supportedSizesList.size(); i++) {
- Size outputSize = supportedSizesList.get(i);
- if (outputSize.getWidth() >= miniBoundingSize.getWidth()
- && outputSize.getHeight() >= miniBoundingSize.getHeight()) {
- // The supportedSizesList is in descending order. Checking and put the
- // mini-bounding-above size at position 0 so that the smallest larger resolution
- // will be put in the first position finally.
- resultList.add(0, outputSize);
- } else {
- // Appends the remaining smaller sizes in descending order.
- resultList.add(outputSize);
- }
- }
-
- return resultList;
- }
-
static Map<Rational, List<Size>> groupSizesByAspectRatio(@NonNull List<Size> sizes) {
Map<Rational, List<Size>> aspectRatioSizeListMap = new HashMap<>();
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorterLegacy.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorterLegacy.java
index 2329794..7483407 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorterLegacy.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorterLegacy.java
@@ -19,7 +19,7 @@
import static androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio;
import static androidx.camera.core.internal.SupportedOutputSizesSorter.getResolutionListGroupingAspectRatioKeys;
import static androidx.camera.core.internal.SupportedOutputSizesSorter.groupSizesByAspectRatio;
-import static androidx.camera.core.internal.SupportedOutputSizesSorter.sortSupportedSizesByMiniBoundingSize;
+import static androidx.camera.core.internal.SupportedOutputSizesSorter.sortSupportedSizesByFallbackRuleClosestHigherThenLower;
import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_VGA;
import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_ZERO;
import static androidx.camera.core.internal.utils.SizeUtil.getArea;
@@ -141,7 +141,8 @@
// If the target resolution is set, use it to sort the sizes list.
if (targetSize != null) {
- resultSizeList = sortSupportedSizesByMiniBoundingSize(resultSizeList, targetSize);
+ sortSupportedSizesByFallbackRuleClosestHigherThenLower(resultSizeList, targetSize,
+ true);
}
} else {
// Rearrange the supported size to put the ones with the same aspect ratio in the front
@@ -151,13 +152,11 @@
// Group output sizes by aspect ratio.
aspectRatioSizeListMap = groupSizesByAspectRatio(filteredSizeList);
- // If the target resolution is set, use it to remove unnecessary larger sizes.
+ // If the target resolution is set, sort the sizes against it.
if (targetSize != null) {
- // Remove unnecessary larger sizes from each aspect ratio size list
for (Rational key : aspectRatioSizeListMap.keySet()) {
- List<Size> sortedResult = sortSupportedSizesByMiniBoundingSize(
- aspectRatioSizeListMap.get(key), targetSize);
- aspectRatioSizeListMap.put(key, sortedResult);
+ sortSupportedSizesByFallbackRuleClosestHigherThenLower(
+ aspectRatioSizeListMap.get(key), targetSize, true);
}
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
index e71e058..77a1cc3 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
@@ -315,7 +315,7 @@
@NonNull
public ListenableFuture<SurfaceOutput> createSurfaceOutputFuture(@NonNull Size inputSize,
@CameraEffect.Formats int format, @NonNull Rect cropRect, int rotationDegrees,
- boolean mirroring, @NonNull CameraInternal cameraInternal) {
+ boolean mirroring, @Nullable CameraInternal cameraInternal) {
checkMainThread();
checkNotClosed();
checkAndSetHasConsumer();
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java
index 177090e..17715d6 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java
@@ -20,6 +20,7 @@
import static androidx.camera.core.impl.utils.TransformUtils.getRectToRect;
import static androidx.camera.core.impl.utils.TransformUtils.rotateSize;
import static androidx.camera.core.impl.utils.TransformUtils.sizeToRectF;
+import static androidx.core.util.Preconditions.checkState;
import android.graphics.Rect;
import android.graphics.RectF;
@@ -76,7 +77,7 @@
private final float[] mAdditionalTransform = new float[16];
// The inverted value of SurfaceTexture#getTransformMatrix()
@NonNull
- private final float[] mInvertedCameraTransform = new float[16];
+ private final float[] mInvertedTextureTransform = new float[16];
@GuardedBy("mLock")
@Nullable
private Consumer<Event> mEventListener;
@@ -91,6 +92,8 @@
@NonNull
private final ListenableFuture<Void> mCloseFuture;
private CallbackToFutureAdapter.Completer<Void> mCloseFutureCompleter;
+ @Nullable
+ private CameraInternal mCameraInternal;
SurfaceOutputImpl(
@NonNull Surface surface,
@@ -101,7 +104,7 @@
@NonNull Rect inputCropRect,
int rotationDegree,
boolean mirroring,
- @NonNull CameraInternal cameraInternal) {
+ @Nullable CameraInternal cameraInternal) {
mSurface = surface;
mTargets = targets;
mFormat = format;
@@ -110,7 +113,8 @@
mInputCropRect = new Rect(inputCropRect);
mMirroring = mirroring;
mRotationDegrees = rotationDegree;
- calculateAdditionalTransform(cameraInternal);
+ mCameraInternal = cameraInternal;
+ calculateAdditionalTransform();
mCloseFuture = CallbackToFutureAdapter.getFuture(
completer -> {
mCloseFutureCompleter = completer;
@@ -211,6 +215,11 @@
return mMirroring;
}
+ @VisibleForTesting
+ public CameraInternal getCamera() {
+ return mCameraInternal;
+ }
+
/**
* This method can be invoked by the processor implementation on any thread.
*
@@ -229,7 +238,6 @@
/**
* Returns the close state.
- *
*/
@RestrictTo(RestrictTo.Scope.TESTS)
public boolean isClosed() {
@@ -258,22 +266,22 @@
/**
* Calculates the additional GL transform and saves it to {@link #mAdditionalTransform}.
*
- * <p>The effect implementation needs to apply this value on top of camera transform obtained
+ * <p>The effect implementation needs to apply this value on top of texture transform obtained
* from {@link SurfaceTexture#getTransformMatrix}.
*
- * <p>The overall transformation (A * B) is a concatenation of 2 values: A) the camera/GL
+ * <p>The overall transformation (A * B) is a concatenation of 2 values: A) the texture
* transform (value of SurfaceTexture#getTransformMatrix), and B) CameraX's additional
* transform based on user config such as the ViewPort API and UseCase#targetRotation. To
* calculate B, we do it in 3 steps:
* <ol>
* <li>1. Calculate A * B by using CameraX transformation value such as crop rect, relative
- * rotation, and mirroring. These info already contain the camera transform(A) in it.
- * <li>2. Calculate A^-1 by predicating the camera transform(A) based on camera
+ * rotation, and mirroring. It already contains the texture transform(A).
+ * <li>2. Calculate A^-1 by predicating the texture transform(A) based on camera
* characteristics then inverting it.
* <li>3. Calculate B by multiplying A^-1 * A * B.
* </ol>
*/
- private void calculateAdditionalTransform(@NonNull CameraInternal cameraInternal) {
+ private void calculateAdditionalTransform() {
Matrix.setIdentityM(mAdditionalTransform, 0);
// Step 1, calculate the overall transformation(A * B) with the following steps:
@@ -313,47 +321,48 @@
Matrix.translateM(mAdditionalTransform, 0, offsetX, offsetY, 0f);
Matrix.scaleM(mAdditionalTransform, 0, scaleX, scaleY, 1f);
- // Step 2: calculate the inverted camera/GL transform: A^-1
- calculateInvertedCameraTransform(cameraInternal);
+ // Step 2: calculate the inverted texture transform: A^-1
+ calculateInvertedTextureTransform();
// Step 3: calculate the additional transform: B = A^-1 * A * B
- Matrix.multiplyMM(mAdditionalTransform, 0, mInvertedCameraTransform, 0,
+ Matrix.multiplyMM(mAdditionalTransform, 0, mInvertedTextureTransform, 0,
mAdditionalTransform, 0);
}
/**
- * Calculates the inverted camera/GL transform and saves it to
- * {@link #mInvertedCameraTransform}.
+ * Calculates the inverted texture transform and saves it to
+ * {@link #mInvertedTextureTransform}.
*
* <p>This method predicts the value of {@link SurfaceTexture#getTransformMatrix} based on
- * camera characteristics then invert it. The result is used to remove the camera transform
+ * camera characteristics then invert it. The result is used to remove the texture transform
* from overall transformation.
*/
- private void calculateInvertedCameraTransform(@NonNull CameraInternal cameraInternal) {
- Matrix.setIdentityM(mInvertedCameraTransform, 0);
+ private void calculateInvertedTextureTransform() {
+ Matrix.setIdentityM(mInvertedTextureTransform, 0);
// Flip for GL. SurfaceTexture#getTransformMatrix always contains this flipping regardless
// of whether it has the camera transform.
- Matrix.translateM(mInvertedCameraTransform, 0, 0f, 1f, 0f);
- Matrix.scaleM(mInvertedCameraTransform, 0, 1f, -1f, 1f);
+ Matrix.translateM(mInvertedTextureTransform, 0, 0f, 1f, 0f);
+ Matrix.scaleM(mInvertedTextureTransform, 0, 1f, -1f, 1f);
- // Applies the transform from CameraInfo if the camera has transformation.
- if (cameraInternal.getHasTransform()) {
+ // Applies the camera sensor orientation if the input surface contains camera transform.
+ if (mCameraInternal != null) {
+ checkState(mCameraInternal.getHasTransform(), "Camera has no transform.");
// Rotation
- preRotate(mInvertedCameraTransform,
- cameraInternal.getCameraInfo().getSensorRotationDegrees(),
+ preRotate(mInvertedTextureTransform,
+ mCameraInternal.getCameraInfo().getSensorRotationDegrees(),
0.5f,
0.5f);
// Mirroring
- if (cameraInternal.isFrontFacing()) {
- Matrix.translateM(mInvertedCameraTransform, 0, 1, 0f, 0f);
- Matrix.scaleM(mInvertedCameraTransform, 0, -1, 1f, 1f);
+ if (mCameraInternal.isFrontFacing()) {
+ Matrix.translateM(mInvertedTextureTransform, 0, 1, 0f, 0f);
+ Matrix.scaleM(mInvertedTextureTransform, 0, -1, 1f, 1f);
}
}
// Invert the matrix so it can be used to "undo" the SurfaceTexture#getTransformMatrix.
- Matrix.invertM(mInvertedCameraTransform, 0, mInvertedCameraTransform, 0);
+ Matrix.invertM(mInvertedTextureTransform, 0, mInvertedTextureTransform, 0);
}
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
index 66ca7ab4..040006d 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
@@ -200,7 +200,7 @@
output.getKey().getCropRect(),
output.getKey().getRotationDegrees(),
output.getKey().getMirroring(),
- mCameraInternal);
+ input.hasCameraTransform() ? mCameraInternal : null);
Futures.addCallback(future, new FutureCallback<SurfaceOutput>() {
@Override
public void onSuccess(@Nullable SurfaceOutput output) {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/AspectRatioStrategy.java b/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/AspectRatioStrategy.java
new file mode 100644
index 0000000..bf36dfde
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/AspectRatioStrategy.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.resolutionselector;
+
+import static androidx.camera.core.AspectRatio.RATIO_4_3;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.camera.core.AspectRatio;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.UseCase;
+import androidx.lifecycle.LifecycleOwner;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * The aspect ratio strategy defines the sequence of aspect ratios that are used to select the
+ * best size for a particular image.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class AspectRatioStrategy {
+ /**
+ * CameraX doesn't fall back to select sizes of any other aspect ratio when this fallback
+ * rule is used.
+ */
+ public static final int FALLBACK_RULE_NONE = 0;
+ /**
+ * CameraX automatically chooses the next best aspect ratio which contains the closest field
+ * of view (FOV) of the camera sensor, from the remaining options.
+ */
+ public static final int FALLBACK_RULE_AUTO = 1;
+
+ /**
+ * Defines the available fallback rules for AspectRatioStrategy.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({FALLBACK_RULE_NONE,
+ FALLBACK_RULE_AUTO
+ })
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public @interface AspectRatioFallbackRule {
+ }
+
+ /**
+ * The pre-defined default aspect ratio strategy that selects sizes with
+ * {@link AspectRatio#RATIO_4_3} in priority. Then, selects sizes with other aspect ratios
+ * according to which aspect ratio can contain the closest FOV of the camera sensor.
+ *
+ * <p>Please see the
+ * <a href="https://source.android.com/docs/core/camera/camera3_crop_reprocess">Output streams,
+ * Cropping, and Zoom</a> introduction to know more about the camera FOV.
+ */
+ @NonNull
+ public static final AspectRatioStrategy RATIO_4_3_FALLBACK_AUTO_STRATEGY =
+ AspectRatioStrategy.create(RATIO_4_3, FALLBACK_RULE_AUTO);
+
+ @AspectRatio.Ratio
+ private final int mPreferredAspectRatio;
+ @AspectRatioFallbackRule
+ private final int mFallbackRule;
+
+ private AspectRatioStrategy(@AspectRatio.Ratio int preferredAspectRatio,
+ @AspectRatioFallbackRule int fallbackRule) {
+ mPreferredAspectRatio = preferredAspectRatio;
+ mFallbackRule = fallbackRule;
+ }
+
+ /**
+ * Returns the specified preferred aspect ratio.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @AspectRatio.Ratio
+ public int getPreferredAspectRatio() {
+ return mPreferredAspectRatio;
+ }
+
+ /**
+ * Returns the specified fallback rule for choosing the aspect ratio when the preferred aspect
+ * ratio is not available.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @AspectRatioFallbackRule
+ public int getFallbackRule() {
+ return mFallbackRule;
+ }
+
+ /**
+ * Creates a new AspectRatioStrategy instance, configured with the specified preferred aspect
+ * ratio and fallback rule.
+ *
+ * <p>OEMs might make the width or height of the supported output sizes be mod 16 aligned for
+ * performance reasons. This means that the device might support 1920x1088 instead of
+ * 1920x1080, even though a 16:9 aspect ratio size is 1920x1080. CameraX can select these mod
+ * 16 aligned sizes when applications specify the preferred aspect ratio as
+ * {@link AspectRatio#RATIO_16_9}.
+ *
+ * <p>Some devices may have issues using sizes of the preferred aspect ratios. CameraX
+ * recommends that applications use the {@link #FALLBACK_RULE_AUTO} setting to avoid no
+ * resolution being available, as an {@link IllegalArgumentException} may be thrown when
+ * calling
+ * {@link androidx.camera.lifecycle.ProcessCameraProvider#bindToLifecycle(LifecycleOwner, CameraSelector, UseCase...)}
+ * to bind {@link UseCase}s with the AspectRatioStrategy specified in the
+ * {@link ResolutionSelector}.
+ *
+ * @param preferredAspectRatio the preferred aspect ratio to select first.
+ * @param fallbackRule the rule to follow when the preferred aspect ratio is not available.
+ */
+ @NonNull
+ public static AspectRatioStrategy create(
+ @AspectRatio.Ratio int preferredAspectRatio,
+ @AspectRatioFallbackRule int fallbackRule) {
+ return new AspectRatioStrategy(preferredAspectRatio, fallbackRule);
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/HighResolution.java b/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/HighResolution.java
new file mode 100644
index 0000000..f9c0941
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/HighResolution.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.resolutionselector;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+/**
+ * The flags that are available in high resolution.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class HighResolution {
+ /**
+ * This flag enables high resolution in the default sensor pixel mode.
+ *
+ * <p>When using the <code>camera-camera2</code> CameraX implementation, please see
+ * {@link android.hardware.camera2.CaptureRequest#SENSOR_PIXEL_MODE} to know more information
+ * about the default and maximum resolution sensor pixel mode.
+ *
+ * <p>When this high resolution flag is set, the high resolution is retrieved via the
+ * {@link android.hardware.camera2.params.StreamConfigurationMap#getHighResolutionOutputSizes(int)}
+ * from the stream configuration map obtained with the
+ * {@link android.hardware.camera2.CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP}
+ * camera characteristics.
+ *
+ * <p>Since Android S, some devices might support a maximum resolution sensor pixel mode,
+ * which allows them to capture additional ultra high resolutions retrieved from
+ * {@link android.hardware.camera2.CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION}
+ * . Enabling high resolution with this flag does not allow applications to select those
+ * ultra high resolutions.
+ */
+ public static final int FLAG_DEFAULT_MODE_ON = 0x1;
+
+ private HighResolution() {
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/ResolutionFilter.java b/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/ResolutionFilter.java
new file mode 100644
index 0000000..0756560
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/ResolutionFilter.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.resolutionselector;
+
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.camera.core.AspectRatio;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.UseCase;
+
+import java.util.List;
+
+/**
+ * Applications can filter out unsuitable sizes and sort the resolution list in the preferred
+ * order by implementing the resolution filter interface. The preferred order is the order in
+ * which the resolutions should be tried first.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface ResolutionFilter {
+ /**
+ * Removes unsuitable sizes and sorts the resolution list in the preferred order.
+ *
+ * <p>OEMs might make the width or height of the supported output sizes be mod 16 aligned for
+ * performance reasons. This means that the device might support 1920x1088 instead of
+ * 1920x1080, even though a 16:9 aspect ratio size is 1920x1080. Therefore, the input
+ * supported sizes list also contains these aspect ratio sizes when applications specify
+ * an {@link AspectRatioStrategy} with {@link AspectRatio#RATIO_16_9} and then also specify a
+ * ResolutionFilter to apply their own selection logic.
+ *
+ * @param supportedSizes the supported output sizes which have been filtered and sorted
+ * according to the other resolution selector settings.
+ * @param rotationDegrees the rotation degrees to rotate the image to the desired
+ * orientation, matching the {@link UseCase}’s target rotation setting
+ * . For example, the target rotation set via
+ * {@link ImageCapture.Builder#setTargetRotation(int)} or
+ * {@link ImageCapture#setTargetRotation(int)}.
+ * @return the desired ordered sizes list for resolution selection. The returned list should
+ * only include sizes in the provided input supported sizes list.
+ */
+ @NonNull
+ List<Size> filter(@NonNull List<Size> supportedSizes, int rotationDegrees);
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/ResolutionSelector.java b/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/ResolutionSelector.java
new file mode 100644
index 0000000..006cb53
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/ResolutionSelector.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.resolutionselector;
+
+import static androidx.camera.core.resolutionselector.AspectRatioStrategy.RATIO_4_3_FALLBACK_AUTO_STRATEGY;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.camera.core.UseCase;
+
+/**
+ * A set of requirements and priorities used to select a resolution for the {@link UseCase}.
+ *
+ * <p>The resolution selection mechanism is determined by the following three steps:
+ * <ol>
+ * <li> Collect the supported output sizes and add them to the candidate resolution list.
+ * <li> Filter and sort the candidate resolution list according to the preference settings.
+ * <li> Consider all the resolution selector settings of bound {@link UseCase}s to find the
+ * resolution that best suits each {@link UseCase}.
+ * </ol>
+ *
+ * <p>For the first step, all supported resolution output sizes are added to the candidate
+ * resolution list as the starting point.
+ *
+ * <p>ResolutionSelector provides the following function for applications to adjust the candidate
+ * resolution settings.
+ * <ul>
+ * <li> {@link Builder#setHighResolutionEnabledFlags(int)}
+ * </ul>
+ *
+ * <p>For the second step, ResolutionSelector provides the following three functions for
+ * applications to determine which resolution should be selected with higher priority.
+ * <ul>
+ * <li> {@link Builder#setAspectRatioStrategy(AspectRatioStrategy)}
+ * <li> {@link Builder#setResolutionStrategy(ResolutionStrategy)}
+ * <li> {@link Builder#setResolutionFilter(ResolutionFilter)}
+ * </ul>
+ *
+ * <p>CameraX sorts the collected sizes according to the specified aspect ratio and resolution
+ * strategies. The aspect ratio strategy has precedence over the resolution strategy for sorting
+ * the resolution candidate list. If applications specify a custom resolution filter, CameraX
+ * passes the resulting sizes list, sorted by the specified aspect ratio and resolution
+ * strategies, to the resolution filter to get the final desired list.
+ *
+ * <p>Different types of {@link UseCase}s might have their own default settings. You can see the
+ * {@link UseCase} builders’ {@code setResolutionSelector()} function to know the details for each
+ * type of {@link UseCase}.
+ *
+ * <p>In the third step, CameraX selects the final resolution for the {@link UseCase} based on the
+ * camera device's hardware level, capabilities, and the bound {@link UseCase} combination.
+ * Applications can check which resolution is finally selected by using the {@link UseCase}'s
+ * {@code getResolutionInfo()} function.
+ *
+ * <p>Note that a ResolutionSelector with more restricted settings may result in that no
+ * resolution can be selected to use. Applications will receive {@link IllegalArgumentException}
+ * when binding the {@link UseCase}s with such kind of ResolutionSelector. Applications can
+ * specify the {@link AspectRatioStrategy} and {@link ResolutionStrategy} with proper fallback
+ * rules to avoid the {@link IllegalArgumentException} or try-catch it and show a proper message
+ * to the end users.
+ *
+ * <p>When creating a ResolutionSelector instance, the
+ * {@link AspectRatioStrategy#RATIO_4_3_FALLBACK_AUTO_STRATEGY} will be the default
+ * {@link AspectRatioStrategy} if it is not set. However, if neither the
+ * {@link ResolutionStrategy} nor the high resolution enabled flags are set, there will be no
+ * default value specified.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class ResolutionSelector {
+ @Nullable
+ private final AspectRatioStrategy mAspectRatioStrategy;
+ @Nullable
+ private final ResolutionStrategy mResolutionStrategy;
+ @Nullable
+ private final ResolutionFilter mResolutionFilter;
+ private final int mHighResolutionEnabledFlags;
+
+ ResolutionSelector(
+ @Nullable AspectRatioStrategy aspectRatioStrategy,
+ @Nullable ResolutionStrategy resolutionStrategy,
+ @Nullable ResolutionFilter resolutionFilter,
+ int highResolutionEnabledFlags) {
+ mAspectRatioStrategy = aspectRatioStrategy;
+ mResolutionStrategy = resolutionStrategy;
+ mResolutionFilter = resolutionFilter;
+ mHighResolutionEnabledFlags = highResolutionEnabledFlags;
+ }
+
+ /**
+ * Returns the specified {@link AspectRatioStrategy}, or null if not specified.
+ */
+ @Nullable
+ public AspectRatioStrategy getAspectRatioStrategy() {
+ return mAspectRatioStrategy;
+ }
+
+ /**
+ * Returns the specified {@link ResolutionStrategy}, or null if not specified.
+ */
+ @Nullable
+ public ResolutionStrategy getResolutionStrategy() {
+ return mResolutionStrategy;
+ }
+
+ /**
+ * Returns the specified {@link ResolutionFilter} implementation, or null if not specified.
+ */
+ @Nullable
+ public ResolutionFilter getResolutionFilter() {
+ return mResolutionFilter;
+ }
+
+ /**
+ * Returns the specified high resolution enabled flags.
+ */
+ public int getHighResolutionEnabledFlags() {
+ return mHighResolutionEnabledFlags;
+ }
+
+ /**
+ * Builder for a {@link ResolutionSelector}.
+ */
+ public static final class Builder {
+ @Nullable
+ private AspectRatioStrategy mAspectRatioStrategy = RATIO_4_3_FALLBACK_AUTO_STRATEGY;
+ @Nullable
+ private ResolutionStrategy mResolutionStrategy = null;
+ @Nullable
+ private ResolutionFilter mResolutionFilter = null;
+ private int mHighResolutionEnabledFlags = 0;
+
+ /**
+ * Creates a Builder instance.
+ */
+ public Builder() {
+ }
+
+ private Builder(@NonNull ResolutionSelector resolutionSelector) {
+ mAspectRatioStrategy = resolutionSelector.getAspectRatioStrategy();
+ mResolutionStrategy = resolutionSelector.getResolutionStrategy();
+ mResolutionFilter = resolutionSelector.getResolutionFilter();
+ mHighResolutionEnabledFlags = resolutionSelector.getHighResolutionEnabledFlags();
+ }
+
+ /**
+ * Creates a Builder from an existing resolution selector.
+ */
+ @NonNull
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static Builder fromResolutionSelector(
+ @NonNull ResolutionSelector resolutionSelector) {
+ return new Builder(resolutionSelector);
+ }
+
+ /**
+ * Sets the aspect ratio selection strategy for the {@link UseCase}. The aspect ratio
+ * selection strategy determines how the {@link UseCase} will choose the aspect ratio of
+ * the captured image.
+ */
+ @NonNull
+ public Builder setAspectRatioStrategy(@NonNull AspectRatioStrategy aspectRatioStrategy) {
+ mAspectRatioStrategy = aspectRatioStrategy;
+ return this;
+ }
+
+ /**
+ * Sets the resolution selection strategy for the {@link UseCase}. The resolution selection
+ * strategy determines how the {@link UseCase} will choose the resolution of the captured
+ * image.
+ */
+ @NonNull
+ public Builder setResolutionStrategy(@NonNull ResolutionStrategy resolutionStrategy) {
+ mResolutionStrategy = resolutionStrategy;
+ return this;
+ }
+
+ /**
+ * Sets the resolution filter to output the final desired sizes list. The resolution
+ * filter will filter out unsuitable sizes and sort the resolution list in the preferred
+ * order. The preferred order is the order in which the resolutions should be tried first.
+ */
+ @NonNull
+ public Builder setResolutionFilter(@NonNull ResolutionFilter resolutionFilter) {
+ mResolutionFilter = resolutionFilter;
+ return this;
+ }
+
+ /**
+ * Sets high resolutions enabled flags to allow the application to select high
+ * resolutions for the {@link UseCase}s. This will enable the application to choose high
+ * resolutions for the captured image, which may result in better quality images.
+ *
+ * <p>Now, only {@link HighResolution#FLAG_DEFAULT_MODE_ON} is allowed for this function.
+ */
+ @NonNull
+ public Builder setHighResolutionEnabledFlags(int flags) {
+ mHighResolutionEnabledFlags = flags;
+ return this;
+ }
+
+ /**
+ * Builds the resolution selector. This will create a resolution selector that can be
+ * used to select the desired resolution for the captured image.
+ */
+ @NonNull
+ public ResolutionSelector build() {
+ return new ResolutionSelector(mAspectRatioStrategy, mResolutionStrategy,
+ mResolutionFilter, mHighResolutionEnabledFlags);
+ }
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/ResolutionStrategy.java b/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/ResolutionStrategy.java
new file mode 100644
index 0000000..b0282e1
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/resolutionselector/ResolutionStrategy.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.resolutionselector;
+
+import android.util.Size;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.UseCase;
+import androidx.lifecycle.LifecycleOwner;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * The resolution strategy defines the resolution selection sequence to select the best size.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class ResolutionStrategy {
+ /**
+ * A resolution strategy chooses the highest available resolution. This strategy does not
+ * have a bound size or fallback rule. When using this strategy, CameraX selects the
+ * available resolutions to use in descending order, starting with the highest quality
+ * resolution available.
+ */
+ @NonNull
+ public static final ResolutionStrategy HIGHEST_AVAILABLE_STRATEGY = new ResolutionStrategy();
+
+ /**
+ * CameraX doesn't select an alternate size when the specified bound size is unavailable.
+ */
+ public static final int FALLBACK_RULE_NONE = 0;
+ /**
+ * When the specified bound size is unavailable, CameraX falls back to select the closest
+ * higher resolution size. If CameraX still cannot find any available resolution, it will
+ * fallback to select other lower resolutions.
+ */
+ public static final int FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER = 1;
+ /**
+ * When the specified bound size is unavailable, CameraX falls back to the closest higher
+ * resolution size.
+ */
+ public static final int FALLBACK_RULE_CLOSEST_HIGHER = 2;
+ /**
+ * When the specified bound size is unavailable, CameraX falls back to select the closest
+ * lower resolution size. If CameraX still cannot find any available resolution, it will
+ * fallback to select other higher resolutions.
+ */
+ public static final int FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER = 3;
+ /**
+ * When the specified bound size is unavailable, CameraX falls back to the closest lower
+ * resolution size.
+ */
+ public static final int FALLBACK_RULE_CLOSEST_LOWER = 4;
+
+ /**
+ * Defines the available fallback rules for ResolutionStrategy.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({FALLBACK_RULE_NONE,
+ FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER,
+ FALLBACK_RULE_CLOSEST_HIGHER,
+ FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER,
+ FALLBACK_RULE_CLOSEST_LOWER
+ })
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public @interface ResolutionFallbackRule {
+ }
+
+ private boolean mIsHighestAvailableStrategy = false;
+ @Nullable
+ private Size mBoundSize = null;
+ @Nullable
+ private Integer mFallbackRule = null;
+
+ /**
+ * Creates a default ResolutionStrategy instance to select the highest available resolution.
+ */
+ private ResolutionStrategy() {
+ mIsHighestAvailableStrategy = true;
+ }
+
+ /**
+ * Creates a ResolutionStrategy instance to select resolution according to the specified bound
+ * size and fallback rule.
+ */
+ private ResolutionStrategy(@NonNull Size boundSize,
+ @NonNull Integer fallbackRule) {
+ mBoundSize = boundSize;
+ mFallbackRule = fallbackRule;
+ }
+
+ /**
+ * Returns {@code true} if the instance is a highest available resolution strategy.
+ * Otherwise, returns {@code false}.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public boolean isHighestAvailableStrategy() {
+ return mIsHighestAvailableStrategy;
+ }
+
+ /**
+ * Returns the specified bound size.
+ *
+ * @return the specified bound size or {@code null} if this is instance of
+ * {@link #HIGHEST_AVAILABLE_STRATEGY}.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @Nullable
+ public Size getBoundSize() {
+ return mBoundSize;
+ }
+
+ /**
+ * Returns the fallback rule for choosing an alternate size when the specified bound size is
+ * unavailable.
+ */
+ @Nullable
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public Integer getFallbackRule() {
+ return mFallbackRule;
+ }
+
+ /**
+ * Creates a new ResolutionStrategy instance, configured with the specified bound size and
+ * fallback rule.
+ *
+ * <p>If the resolution candidate list contains the bound size and the bound size can fulfill
+ * all resolution selector settings, CameraX can also select the specified bound size as the
+ * result for the {@link UseCase}.
+ *
+ * <p>Some devices may have issues using sizes of the preferred aspect ratios. CameraX
+ * recommends that applications use the following fallback rule setting to avoid no
+ * resolution being available, as an {@link IllegalArgumentException} may be thrown when
+ * calling
+ * {@link androidx.camera.lifecycle.ProcessCameraProvider#bindToLifecycle(LifecycleOwner, CameraSelector, UseCase...)}
+ * to bind {@link UseCase}s with the ResolutionStrategy specified in the
+ * {@link ResolutionSelector}.
+ * <ul>
+ * <li> {@link #FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER}
+ * <li> {@link #FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER}
+ * </ul>
+ *
+ * @param boundSize the bound size to select the best resolution with the fallback rule.
+ * @param fallbackRule the rule to follow when the specified bound size is not available.
+ */
+ @NonNull
+ public static ResolutionStrategy create(
+ @NonNull Size boundSize,
+ @ResolutionFallbackRule int fallbackRule) {
+ return new ResolutionStrategy(boundSize, fallbackRule);
+ }
+}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java b/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
index 30de277..36df55c 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
@@ -42,6 +42,9 @@
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.internal.CameraUseCaseAdapter;
import androidx.camera.core.internal.utils.SizeUtil;
+import androidx.camera.core.resolutionselector.AspectRatioStrategy;
+import androidx.camera.core.resolutionselector.ResolutionSelector;
+import androidx.camera.core.resolutionselector.ResolutionStrategy;
import androidx.camera.testing.CameraUtil;
import androidx.camera.testing.CameraXUtil;
import androidx.camera.testing.fakes.FakeAppConfig;
@@ -197,7 +200,7 @@
public void setAnalyzerWithResolution_doesNotOverridesUseCaseResolution_resolutionSelector() {
ImageAnalysisConfig config = getMergedImageAnalysisConfig(APP_RESOLUTION,
ANALYZER_RESOLUTION, -1, true);
- assertThat(config.getResolutionSelector().getPreferredResolution()).isEqualTo(
+ assertThat(config.getResolutionSelector().getResolutionStrategy().getBoundSize()).isEqualTo(
APP_RESOLUTION);
}
@@ -212,8 +215,8 @@
public void setAnalyzerWithResolution_usedAsDefaultUseCaseResolution_resolutionSelector() {
ImageAnalysisConfig config = getMergedImageAnalysisConfig(null,
ANALYZER_RESOLUTION, -1, true);
- assertThat(config.getResolutionSelector().getPreferredResolution()).isEqualTo(
- ANALYZER_RESOLUTION);
+ assertThat(config.getResolutionSelector().getResolutionStrategy().getBoundSize()).isEqualTo(
+ FLIPPED_ANALYZER_RESOLUTION);
}
@Test(expected = IllegalArgumentException.class)
@@ -232,11 +235,15 @@
// Sets preferred resolution by ResolutionSelector or legacy API
if (useResolutionSelector) {
- ResolutionSelector.Builder resolutionSelectorBuilder = new ResolutionSelector.Builder();
+ ResolutionSelector.Builder selectorBuilder = new ResolutionSelector.Builder();
if (appResolution != null) {
- resolutionSelectorBuilder.setPreferredResolution(appResolution);
+ selectorBuilder.setResolutionStrategy(ResolutionStrategy.create(appResolution,
+ ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER));
+ } else {
+ selectorBuilder.setAspectRatioStrategy(
+ AspectRatioStrategy.RATIO_4_3_FALLBACK_AUTO_STRATEGY);
}
- builder.setResolutionSelector(resolutionSelectorBuilder.build());
+ builder.setResolutionSelector(selectorBuilder.build());
} else {
if (appResolution != null) {
builder.setTargetResolution(appResolution);
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
index de6014c..bff9468 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
@@ -48,6 +48,7 @@
import androidx.camera.core.impl.utils.futures.Futures
import androidx.camera.core.internal.CameraUseCaseAdapter
import androidx.camera.core.internal.utils.SizeUtil
+import androidx.camera.core.resolutionselector.ResolutionSelector
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CameraXUtil
import androidx.camera.testing.fakes.FakeAppConfig
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
index 6ef3e65..2968588 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
@@ -44,6 +44,7 @@
import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
import androidx.camera.core.internal.CameraUseCaseAdapter
import androidx.camera.core.internal.utils.SizeUtil
+import androidx.camera.core.resolutionselector.ResolutionSelector
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CameraXUtil
import androidx.camera.testing.fakes.FakeAppConfig
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt
index c18ffdc..e7265d0 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt
@@ -20,15 +20,31 @@
import android.os.Build
import android.util.Pair
import android.util.Size
+import androidx.camera.core.AspectRatio
+import androidx.camera.core.impl.UseCaseConfig
import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType
+import androidx.camera.core.impl.utils.AspectRatioUtil
+import androidx.camera.core.resolutionselector.AspectRatioStrategy
+import androidx.camera.core.resolutionselector.HighResolution
+import androidx.camera.core.resolutionselector.ResolutionFilter
+import androidx.camera.core.resolutionselector.ResolutionSelector
+import androidx.camera.core.resolutionselector.ResolutionStrategy
+import androidx.camera.core.resolutionselector.ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER
+import androidx.camera.core.resolutionselector.ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
+import androidx.camera.core.resolutionselector.ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER
+import androidx.camera.core.resolutionselector.ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER
import androidx.camera.testing.fakes.FakeCameraInfoInternal
import androidx.camera.testing.fakes.FakeUseCaseConfig
import com.google.common.truth.Truth.assertThat
+import java.util.Collections
+import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
import org.robolectric.annotation.internal.DoNotInstrument
+private const val TARGET_ASPECT_RATIO_NONE = -99
private val DEFAULT_SUPPORTED_SIZES = listOf(
Size(4032, 3024), // 4:3
Size(3840, 2160), // 16:9
@@ -36,6 +52,7 @@
Size(1920, 1080), // 16:9
Size(1280, 960), // 4:3
Size(1280, 720), // 16:9
+ Size(960, 960), // 1:1
Size(960, 544), // a mod16 version of resolution with 16:9 aspect ratio.
Size(800, 450), // 16:9
Size(640, 480), // 4:3
@@ -43,14 +60,39 @@
Size(320, 180), // 16:9
Size(256, 144) // 16:9
)
+private val HIGH_RESOLUTION_SUPPORTED_SIZES = listOf(
+ Size(8000, 6000),
+ Size(8000, 4500),
+)
+private val CUSTOM_SUPPORTED_SIZES = listOf(
+ Size(1920, 1080),
+ Size(720, 480),
+ Size(640, 480)
+)
+private val PORTRAIT_SUPPORTED_SIZES = listOf(
+ Size(1440, 1920),
+ Size(1080, 1920),
+ Size(1080, 1440),
+ Size(960, 1280),
+ Size(720, 1280),
+ Size(960, 540),
+ Size(480, 640),
+ Size(640, 480),
+ Size(360, 480)
+)
/**
* Unit tests for [SupportedOutputSizesSorter].
*/
@RunWith(RobolectricTestRunner::class)
@DoNotInstrument
-@org.robolectric.annotation.Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
class SupportedOutputSizesSorterTest {
+ private val cameraInfoInternal = FakeCameraInfoInternal().apply {
+ setSupportedResolutions(ImageFormat.JPEG, DEFAULT_SUPPORTED_SIZES)
+ setSupportedHighResolutions(ImageFormat.JPEG, HIGH_RESOLUTION_SUPPORTED_SIZES)
+ }
+ private val supportedOutputSizesSorter = SupportedOutputSizesSorter(cameraInfoInternal)
@Test
fun canSelectCustomOrderedResolutions() {
@@ -63,26 +105,14 @@
// Sets up the custom ordered resolutions
val useCaseConfig =
FakeUseCaseConfig.Builder(CaptureType.IMAGE_CAPTURE, imageFormat).apply {
- setCustomOrderedResolutions(
- listOf(
- Size(1920, 1080),
- Size(720, 480),
- Size(640, 480)
- )
- )
+ setCustomOrderedResolutions(CUSTOM_SUPPORTED_SIZES)
}.useCaseConfig
// Act
val sortedResult = supportedOutputSizesSorter.getSortedSupportedOutputSizes(useCaseConfig)
// Assert
- assertThat(sortedResult).containsExactlyElementsIn(
- listOf(
- Size(1920, 1080),
- Size(720, 480),
- Size(640, 480)
- )
- ).inOrder()
+ assertThat(sortedResult).containsExactlyElementsIn(CUSTOM_SUPPORTED_SIZES).inOrder()
}
@Test
@@ -97,15 +127,7 @@
val useCaseConfig =
FakeUseCaseConfig.Builder(CaptureType.IMAGE_CAPTURE, imageFormat).apply {
setSupportedResolutions(
- listOf(
- Pair.create(
- imageFormat, arrayOf(
- Size(1920, 1080),
- Size(720, 480),
- Size(640, 480)
- )
- )
- )
+ listOf(Pair.create(imageFormat, CUSTOM_SUPPORTED_SIZES.toTypedArray()))
)
}.useCaseConfig
@@ -113,12 +135,460 @@
val sortedResult = supportedOutputSizesSorter.getSortedSupportedOutputSizes(useCaseConfig)
// Assert
- assertThat(sortedResult).containsExactlyElementsIn(
- listOf(
- Size(1920, 1080),
- Size(720, 480),
- Size(640, 480)
- )
- ).inOrder()
+ assertThat(sortedResult).containsExactlyElementsIn(CUSTOM_SUPPORTED_SIZES).inOrder()
}
-}
\ No newline at end of file
+
+ @Test
+ fun getSupportedOutputSizes_aspectRatio4x3_fallbackRuleNone() {
+ verifySupportedOutputSizesWithResolutionSelectorSettings(
+ preferredAspectRatio = AspectRatio.RATIO_4_3,
+ aspectRatioFallbackRule = AspectRatioStrategy.FALLBACK_RULE_NONE,
+ expectedList = listOf(
+ // Only returns preferred AspectRatio matched items, sorted by area size.
+ Size(4032, 3024),
+ Size(1920, 1440),
+ Size(1280, 960),
+ Size(640, 480),
+ Size(320, 240),
+ )
+ )
+ }
+
+ @Test
+ fun getSupportedOutputSizes_aspectRatio4x3_fallbackRuleAuto() {
+ verifySupportedOutputSizesWithResolutionSelectorSettings(
+ preferredAspectRatio = AspectRatio.RATIO_4_3,
+ aspectRatioFallbackRule = AspectRatioStrategy.FALLBACK_RULE_AUTO,
+ expectedList = listOf(
+ // Matched preferred AspectRatio items, sorted by area size.
+ Size(4032, 3024), // 4:3
+ Size(1920, 1440),
+ Size(1280, 960),
+ Size(640, 480),
+ Size(320, 240),
+ // Mismatched preferred AspectRatio items, sorted by FOV and area size.
+ Size(960, 960), // 1:1
+ Size(3840, 2160), // 16:9
+ Size(1920, 1080),
+ Size(1280, 720),
+ Size(960, 544),
+ Size(800, 450),
+ Size(320, 180),
+ Size(256, 144)
+ )
+ )
+ }
+
+ @Test
+ fun getSupportedOutputSizes_aspectRatio16x9_fallbackRuleNone() {
+ verifySupportedOutputSizesWithResolutionSelectorSettings(
+ preferredAspectRatio = AspectRatio.RATIO_16_9,
+ aspectRatioFallbackRule = AspectRatioStrategy.FALLBACK_RULE_NONE,
+ expectedList = listOf(
+ // Only returns preferred AspectRatio matched items, sorted by area size.
+ Size(3840, 2160),
+ Size(1920, 1080),
+ Size(1280, 720),
+ Size(960, 544),
+ Size(800, 450),
+ Size(320, 180),
+ Size(256, 144),
+ )
+ )
+ }
+
+ @Test
+ fun getSupportedOutputSizes_aspectRatio16x9_fallbackRuleAuto() {
+ verifySupportedOutputSizesWithResolutionSelectorSettings(
+ preferredAspectRatio = AspectRatio.RATIO_16_9,
+ aspectRatioFallbackRule = AspectRatioStrategy.FALLBACK_RULE_AUTO,
+ expectedList = listOf(
+ // Matched preferred AspectRatio items, sorted by area size.
+ Size(3840, 2160), // 16:9
+ Size(1920, 1080),
+ Size(1280, 720),
+ Size(960, 544),
+ Size(800, 450),
+ Size(320, 180),
+ Size(256, 144),
+ // Mismatched preferred AspectRatio items, sorted by FOV and area size.
+ Size(4032, 3024), // 4:3
+ Size(1920, 1440),
+ Size(1280, 960),
+ Size(640, 480),
+ Size(320, 240),
+ Size(960, 960), // 1:1
+ )
+ )
+ }
+
+ @Test
+ fun getSupportedOutputSizes_boundSize1920x1080_withResolutionFallbackRuleNone() {
+ verifySupportedOutputSizesWithResolutionSelectorSettings(
+ boundSize = Size(1280, 960),
+ resolutionFallbackRule = ResolutionStrategy.FALLBACK_RULE_NONE,
+ // Only returns preferred AspectRatio matched item.
+ expectedList = listOf(Size(1280, 960))
+ )
+ }
+
+ @Test
+ fun getSupportedOutputSizes_boundSize1920x1080_withBothAspectRatioResolutionFallbackRuleNone() {
+ verifySupportedOutputSizesWithResolutionSelectorSettings(
+ preferredAspectRatio = AspectRatio.RATIO_4_3,
+ aspectRatioFallbackRule = AspectRatioStrategy.FALLBACK_RULE_NONE,
+ boundSize = Size(1920, 1080),
+ resolutionFallbackRule = ResolutionStrategy.FALLBACK_RULE_NONE,
+ // No size is returned since only 1920x1080 is allowed but it is not a 4:3 size
+ expectedList = Collections.emptyList()
+ )
+ }
+
+ @Test
+ fun getSupportedOutputSizes_withHighestAvailableResolutionStrategy() {
+ // Bound size will be ignored when fallback rule is HIGHEST_AVAILABLE
+ verifySupportedOutputSizesWithResolutionSelectorSettings(
+ resolutionStrategy = ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY,
+ // The 4:3 default aspect ratio will make sizes of 4/3 have the highest priority
+ expectedList = listOf(
+ // Matched default preferred AspectRatio items, sorted by area size.
+ Size(4032, 3024),
+ Size(1920, 1440),
+ Size(1280, 960),
+ Size(640, 480),
+ Size(320, 240),
+ // Mismatched default preferred AspectRatio items, sorted by FOV and area size.
+ Size(960, 960), // 1:1
+ Size(3840, 2160), // 16:9
+ Size(1920, 1080),
+ Size(1280, 720),
+ Size(960, 544),
+ Size(800, 450),
+ Size(320, 180),
+ Size(256, 144),
+ )
+ )
+ }
+
+ @Test
+ fun getSupportedOutputSizes_boundSize1920x1080_withClosestHigherThenLowerRule() {
+ verifySupportedOutputSizesWithResolutionSelectorSettings(
+ boundSize = Size(1920, 1080),
+ resolutionFallbackRule = FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER,
+ // The 4:3 default aspect ratio will make sizes of 4/3 have the highest priority
+ expectedList = listOf(
+ // Matched default preferred AspectRatio items, sorted by area size.
+ Size(1920, 1440), // 4:3 smallest larger size has highest priority
+ Size(4032, 3024), // the remaining 4:3 larger sizes
+ Size(1280, 960), // the remaining 4:3 smaller sizes
+ Size(640, 480),
+ Size(320, 240),
+ // Mismatched default preferred AspectRatio items, sorted by FOV and area size.
+ Size(960, 960), // 1:1
+ Size(1920, 1080), // 16:9 smallest larger size
+ Size(3840, 2160), // the remaining 16:9 larger sizes
+ Size(1280, 720), // the remaining 16:9 smaller sizes
+ Size(960, 544),
+ Size(800, 450),
+ Size(320, 180),
+ Size(256, 144),
+ )
+ )
+ }
+
+ @Test
+ fun getSupportedOutputSizes_boundSize1920x1080_withClosestHigherRule() {
+ verifySupportedOutputSizesWithResolutionSelectorSettings(
+ boundSize = Size(1920, 1080),
+ resolutionFallbackRule = FALLBACK_RULE_CLOSEST_HIGHER,
+ // The 4:3 default aspect ratio will make sizes of 4/3 have the highest priority
+ expectedList = listOf(
+ // Matched default preferred AspectRatio items, sorted by area size.
+ Size(1920, 1440), // 4:3 smallest larger size has highest priority
+ Size(4032, 3024), // the remaining 4:3 larger sizes
+ // Mismatched default preferred AspectRatio items, sorted by FOV and area size.
+ Size(1920, 1080), // 16:9 smallest larger size
+ Size(3840, 2160), // the remaining 16:9 larger sizes
+ )
+ )
+ }
+
+ @Test
+ fun getSupportedOutputSizes_boundSize1920x1080_withClosestLowerThenHigherRule() {
+ verifySupportedOutputSizesWithResolutionSelectorSettings(
+ boundSize = Size(1920, 1080),
+ resolutionFallbackRule = FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER,
+ // The 4:3 default aspect ratio will make sizes of 4/3 have the highest priority
+ expectedList = listOf(
+ // Matched default preferred AspectRatio items, sorted by area size.
+ Size(1280, 960), // 4:3 largest smaller size has highest priority
+ Size(640, 480), // the remaining 4:3 smaller sizes
+ Size(320, 240),
+ Size(1920, 1440), // the remaining 4:3 larger sizes
+ Size(4032, 3024),
+ // Mismatched default preferred AspectRatio items, sorted by FOV and area size.
+ Size(960, 960), // 1:1
+ Size(1920, 1080), // 16:9 largest smaller size
+ Size(1280, 720), // the remaining 16:9 smaller sizes
+ Size(960, 544),
+ Size(800, 450),
+ Size(320, 180),
+ Size(256, 144),
+ Size(3840, 2160), // the remaining 16:9 larger sizes
+ )
+ )
+ }
+
+ @Test
+ fun getSupportedOutputSizes_boundSize1920x1080_withClosestLowerRule() {
+ verifySupportedOutputSizesWithResolutionSelectorSettings(
+ boundSize = Size(1920, 1080),
+ resolutionFallbackRule = FALLBACK_RULE_CLOSEST_LOWER,
+ // The 4:3 default aspect ratio will make sizes of 4/3 have the highest priority
+ expectedList = listOf(
+ // Matched default preferred AspectRatio items, sorted by area size.
+ Size(1280, 960), // 4:3 largest smaller size has highest priority
+ Size(640, 480), // the remaining 4:3 smaller sizes
+ Size(320, 240),
+ // Mismatched default preferred AspectRatio items, sorted by FOV and area size.
+ Size(960, 960), // 1:1
+ Size(1920, 1080), // 16:9 largest smaller size
+ Size(1280, 720), // the remaining 16:9 smaller sizes
+ Size(960, 544),
+ Size(800, 450),
+ Size(320, 180),
+ Size(256, 144),
+ )
+ )
+ }
+
+ @Test
+ fun getSupportedOutputSizesWithPortraitPixelArraySize_aspectRatio16x9() {
+ val cameraInfoInternal = FakeCameraInfoInternal().apply {
+ setSupportedResolutions(ImageFormat.JPEG, PORTRAIT_SUPPORTED_SIZES)
+ }
+ val supportedOutputSizesSorter = SupportedOutputSizesSorter(cameraInfoInternal)
+ verifySupportedOutputSizesWithResolutionSelectorSettings(
+ outputSizesSorter = supportedOutputSizesSorter,
+ preferredAspectRatio = AspectRatio.RATIO_16_9,
+ expectedList = listOf(
+ // Matched preferred AspectRatio items, sorted by area size.
+ Size(1080, 1920),
+ Size(720, 1280),
+ // Mismatched preferred AspectRatio items, sorted by area size.
+ Size(1440, 1920),
+ Size(1080, 1440),
+ Size(960, 1280),
+ Size(480, 640),
+ Size(360, 480),
+ Size(640, 480),
+ Size(960, 540)
+ )
+ )
+ }
+
+ @Config(minSdk = Build.VERSION_CODES.M)
+ @Test
+ fun getSupportedOutputSizes_whenHighResolutionIsEnabled_aspectRatio16x9() {
+ verifySupportedOutputSizesWithResolutionSelectorSettings(
+ preferredAspectRatio = AspectRatio.RATIO_16_9,
+ highResolutionEnabledFlags = HighResolution.FLAG_DEFAULT_MODE_ON,
+ expectedList = listOf(
+ // Matched preferred AspectRatio items, sorted by area size.
+ Size(8000, 4500), // 16:9 high resolution size
+ Size(3840, 2160),
+ Size(1920, 1080),
+ Size(1280, 720),
+ Size(960, 544),
+ Size(800, 450),
+ Size(320, 180),
+ Size(256, 144),
+ // Mismatched preferred AspectRatio items, sorted by area size.
+ Size(8000, 6000), // 4:3 high resolution size
+ Size(4032, 3024),
+ Size(1920, 1440),
+ Size(1280, 960),
+ Size(640, 480),
+ Size(320, 240),
+ Size(960, 960), // 1:1
+ )
+ )
+ }
+
+ @Config(minSdk = Build.VERSION_CODES.M)
+ @Test
+ fun highResolutionCanNotBeSelected_whenHighResolutionForceDisabled() {
+ verifySupportedOutputSizesWithResolutionSelectorSettings(
+ preferredAspectRatio = AspectRatio.RATIO_16_9,
+ highResolutionEnabledFlags = HighResolution.FLAG_DEFAULT_MODE_ON,
+ highResolutionForceDisabled = true,
+ expectedList = listOf(
+ // Matched preferred AspectRatio items, sorted by area size.
+ Size(3840, 2160),
+ Size(1920, 1080),
+ Size(1280, 720),
+ Size(960, 544),
+ Size(800, 450),
+ Size(320, 180),
+ Size(256, 144),
+ // Mismatched preferred AspectRatio items, sorted by area size.
+ Size(4032, 3024), // 4:3
+ Size(1920, 1440),
+ Size(1280, 960),
+ Size(640, 480),
+ Size(320, 240),
+ Size(960, 960), // 1:1
+ )
+ )
+ }
+
+ @Test
+ fun checkAllSupportedSizesCanBeSelected() {
+ // For 4:3 and 16:9 sizes, sets each of supported size as bound size and also calculates
+ // its ratio to set as preferred aspect ratio, so that the size can be put in the first
+ // priority position. For sizes of other aspect ratio, creates a ResolutionFilter to select
+ // it.
+ DEFAULT_SUPPORTED_SIZES.forEach { supportedSize ->
+ var resolutionFilter: ResolutionFilter? = null
+ val preferredAspectRatio =
+ if (AspectRatioUtil.hasMatchingAspectRatio(
+ supportedSize,
+ AspectRatioUtil.ASPECT_RATIO_4_3
+ )
+ ) {
+ AspectRatio.RATIO_4_3
+ } else if (AspectRatioUtil.hasMatchingAspectRatio(
+ supportedSize,
+ AspectRatioUtil.ASPECT_RATIO_16_9
+ )
+ ) {
+ AspectRatio.RATIO_16_9
+ } else {
+ // Sizes of special aspect ratios as 1:1 or 18:9 need to implement
+ // ResolutionFilter to select them.
+ resolutionFilter = ResolutionFilter { supportedSizes, _ ->
+ supportedSizes.remove(supportedSize)
+ supportedSizes.add(0, supportedSize)
+ supportedSizes
+ }
+ TARGET_ASPECT_RATIO_NONE
+ }
+
+ val useCaseConfig = createUseCaseConfig(
+ preferredAspectRatio = preferredAspectRatio,
+ boundSize = supportedSize,
+ resolutionFilter = resolutionFilter
+ )
+
+ val resultList = supportedOutputSizesSorter.getSortedSupportedOutputSizes(useCaseConfig)
+ assertThat(resultList[0]).isEqualTo(supportedSize)
+ }
+ }
+
+ @Test
+ fun getSupportedOutputSizes_withResolutionFilter() {
+ val filteredSizesList = arrayListOf(
+ Size(1280, 720),
+ Size(4032, 3024),
+ Size(960, 960)
+ )
+ val resolutionFilter = ResolutionFilter { _, _ -> filteredSizesList }
+ verifySupportedOutputSizesWithResolutionSelectorSettings(
+ resolutionFilter = resolutionFilter,
+ expectedList = filteredSizesList
+ )
+ }
+
+ @Test
+ fun getSupportedOutputSizes_resolutionFilterReturnsAdditionalItem() {
+ val resolutionFilter = ResolutionFilter { supportedSizes, _ ->
+ supportedSizes.add(Size(1599, 899)) // Adds an additional size item
+ supportedSizes
+ }
+ // Throws exception when additional items is added in the returned list
+ assertThrows(IllegalArgumentException::class.java) {
+ verifySupportedOutputSizesWithResolutionSelectorSettings(
+ resolutionFilter = resolutionFilter
+ )
+ }
+ }
+
+ private fun verifySupportedOutputSizesWithResolutionSelectorSettings(
+ outputSizesSorter: SupportedOutputSizesSorter = supportedOutputSizesSorter,
+ captureType: CaptureType = CaptureType.IMAGE_CAPTURE,
+ preferredAspectRatio: Int = TARGET_ASPECT_RATIO_NONE,
+ aspectRatioFallbackRule: Int = AspectRatioStrategy.FALLBACK_RULE_AUTO,
+ resolutionStrategy: ResolutionStrategy? = null,
+ boundSize: Size? = null,
+ resolutionFallbackRule: Int = FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER,
+ resolutionFilter: ResolutionFilter? = null,
+ highResolutionEnabledFlags: Int = 0,
+ highResolutionForceDisabled: Boolean = false,
+ expectedList: List<Size> = Collections.emptyList(),
+ ) {
+ val useCaseConfig = createUseCaseConfig(
+ captureType,
+ preferredAspectRatio,
+ aspectRatioFallbackRule,
+ resolutionStrategy,
+ boundSize,
+ resolutionFallbackRule,
+ resolutionFilter,
+ highResolutionEnabledFlags,
+ highResolutionForceDisabled
+ )
+ val resultList = outputSizesSorter.getSortedSupportedOutputSizes(useCaseConfig)
+ assertThat(resultList).containsExactlyElementsIn(expectedList).inOrder()
+ }
+
+ private fun createUseCaseConfig(
+ captureType: CaptureType = CaptureType.IMAGE_CAPTURE,
+ preferredAspectRatio: Int = TARGET_ASPECT_RATIO_NONE,
+ aspectRatioFallbackRule: Int = AspectRatioStrategy.FALLBACK_RULE_AUTO,
+ resolutionStrategy: ResolutionStrategy? = null,
+ boundSize: Size? = null,
+ resolutionFallbackRule: Int = FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER,
+ resolutionFilter: ResolutionFilter? = null,
+ highResolutionEnabledFlags: Int = 0,
+ highResolutionForceDisabled: Boolean = false,
+ ): UseCaseConfig<*> {
+ val useCaseConfigBuilder = FakeUseCaseConfig.Builder(captureType, ImageFormat.JPEG)
+ val resolutionSelectorBuilder = ResolutionSelector.Builder()
+
+ // Creates aspect ratio strategy and sets to resolution selector
+ val aspectRatioStrategy = if (preferredAspectRatio != TARGET_ASPECT_RATIO_NONE) {
+ AspectRatioStrategy.create(preferredAspectRatio, aspectRatioFallbackRule)
+ } else {
+ null
+ }
+
+ aspectRatioStrategy?.let { resolutionSelectorBuilder.setAspectRatioStrategy(it) }
+
+ // Creates resolution strategy and sets to resolution selector
+ if (resolutionStrategy != null) {
+ resolutionSelectorBuilder.setResolutionStrategy(resolutionStrategy)
+ } else {
+ boundSize?.let {
+ resolutionSelectorBuilder.setResolutionStrategy(
+ ResolutionStrategy.create(
+ boundSize,
+ resolutionFallbackRule
+ )
+ )
+ }
+ }
+
+ // Sets the resolution filter to resolution selector
+ resolutionFilter?.let { resolutionSelectorBuilder.setResolutionFilter(it) }
+ // Sets the high resolution enabled flags to resolution selector
+ resolutionSelectorBuilder.setHighResolutionEnabledFlags(highResolutionEnabledFlags)
+
+ // Sets the custom resolution selector to use case config
+ useCaseConfigBuilder.setResolutionSelector(resolutionSelectorBuilder.build())
+
+ // Sets the high resolution force disabled setting
+ useCaseConfigBuilder.setHighResolutionDisabled(highResolutionForceDisabled)
+
+ return useCaseConfigBuilder.useCaseConfig
+ }
+}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceOutputImplTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceOutputImplTest.kt
index eb809d7..bc76e238 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceOutputImplTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceOutputImplTest.kt
@@ -125,9 +125,7 @@
@Test
fun updateMatrixWithoutCameraTransform_noCameraTransform() {
// Arrange.
- val cameraInfo = FakeCameraInfoInternal(180, LENS_FACING_FRONT)
- val camera = FakeCamera(null, cameraInfo).apply { hasTransform = false }
- val surfaceOut = createFakeSurfaceOutputImpl(camera)
+ val surfaceOut = createFakeSurfaceOutputImpl(null)
val input = FloatArray(16).also {
Matrix.setIdentityM(it, 0)
}
@@ -179,7 +177,7 @@
assertThat(hasRequestedClose).isFalse()
}
- private fun createFakeSurfaceOutputImpl(camera: FakeCamera = FakeCamera()) = SurfaceOutputImpl(
+ private fun createFakeSurfaceOutputImpl(camera: FakeCamera? = FakeCamera()) = SurfaceOutputImpl(
fakeSurface,
TARGET,
INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
index 1107797..f6287b4 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
@@ -116,6 +116,36 @@
}
@Test
+ fun inputHasNoCameraTransform_surfaceOutputReceivesNullCamera() {
+ // Arrange: configure node to produce JPEG output.
+ createSurfaceProcessorNode()
+ createInputEdge(
+ inputEdge = SurfaceEdge(
+ PREVIEW,
+ INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
+ StreamSpec.builder(INPUT_SIZE).build(),
+ Matrix.IDENTITY_MATRIX,
+ false,
+ PREVIEW_CROP_RECT,
+ 0,
+ false
+ )
+ )
+
+ // Act.
+ val nodeOutput = node.transform(nodeInput)
+ provideSurfaces(nodeOutput)
+ shadowOf(getMainLooper()).idle()
+
+ val previewSurfaceOutput =
+ surfaceProcessorInternal.surfaceOutputs[PREVIEW]!! as SurfaceOutputImpl
+ assertThat(previewSurfaceOutput.camera).isNull()
+ val videoSurfaceOutput =
+ surfaceProcessorInternal.surfaceOutputs[VIDEO_CAPTURE]!! as SurfaceOutputImpl
+ assertThat(videoSurfaceOutput.camera).isNull()
+ }
+
+ @Test
fun configureJpegOutput_returnsJpegFormat() {
// Arrange: configure node to produce JPEG output.
createSurfaceProcessorNode()
@@ -288,6 +318,7 @@
assertThat(previewTransformInfo.rotationDegrees).isEqualTo(0)
assertThat(previewSurfaceOutput.inputSize).isEqualTo(INPUT_SIZE)
assertThat(previewSurfaceOutput.mirroring).isFalse()
+ assertThat(previewSurfaceOutput.camera).isNotNull()
val videoSurfaceOutput =
surfaceProcessorInternal.surfaceOutputs[VIDEO_CAPTURE]!! as SurfaceOutputImpl
@@ -298,6 +329,7 @@
assertThat(videoTransformInfo.rotationDegrees).isEqualTo(270)
assertThat(videoSurfaceOutput.inputSize).isEqualTo(INPUT_SIZE)
assertThat(videoSurfaceOutput.mirroring).isTrue()
+ assertThat(videoSurfaceOutput.camera).isNotNull()
}
@Test
@@ -421,9 +453,8 @@
inputRotationDegrees: Int = INPUT_ROTATION_DEGREES,
mirroring: Boolean = MIRRORING,
videoOutputSize: Size = VIDEO_SIZE,
- frameRateRange: Range<Int> = StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED
- ) {
- val inputEdge = SurfaceEdge(
+ frameRateRange: Range<Int> = StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED,
+ inputEdge: SurfaceEdge = SurfaceEdge(
previewTarget,
INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
StreamSpec.builder(previewSize).setExpectedFrameRateRange(frameRateRange).build(),
@@ -432,7 +463,8 @@
previewCropRect,
inputRotationDegrees,
mirroring,
- )
+ ),
+ ) {
videoOutConfig = OutConfig.of(
VIDEO_CAPTURE,
INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/CameraUtil.java b/camera/camera-testing/src/main/java/androidx/camera/testing/CameraUtil.java
index a87cc14..198e8c24 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/CameraUtil.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/CameraUtil.java
@@ -1040,6 +1040,7 @@
public static TestRule grantCameraPermissionAndPreTest(@Nullable PreTestCamera cameraTestRule,
@Nullable PreTestCameraIdList cameraIdListTestRule) {
RuleChain rule = RuleChain.outerRule(GrantPermissionRule.grant(Manifest.permission.CAMERA));
+ rule = rule.around(new IgnoreProblematicDeviceRule());
if (cameraIdListTestRule != null) {
rule = rule.around(cameraIdListTestRule);
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/IgnoreProblematicDeviceRule.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/IgnoreProblematicDeviceRule.kt
new file mode 100644
index 0000000..32d1274
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/IgnoreProblematicDeviceRule.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.testing
+
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import org.junit.AssumptionViolatedException
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * Test class to set the TestRule should not be run on the problematic devices.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class IgnoreProblematicDeviceRule : TestRule {
+ private var avdName: String = try {
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ device.executeShellCommand("getprop ro.kernel.qemu.avd_name").filterNot {
+ it == '_' || it == '-' || it == ' '
+ }
+ } catch (e: Exception) {
+ Log.d("ProblematicDeviceRule", "Cannot get avd name", e)
+ ""
+ }
+
+ private val pixel2Api26Emulator = isEmulator && avdName.contains(
+ "Pixel2", ignoreCase = true
+ ) && Build.VERSION.SDK_INT == Build.VERSION_CODES.O
+
+ private val api21Emulator = isEmulator && Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP
+
+ private val isProblematicDevices = pixel2Api26Emulator || api21Emulator
+
+ override fun apply(base: Statement, description: Description): Statement {
+ return object : Statement() {
+ override fun evaluate() {
+ if (isProblematicDevices) {
+ throw AssumptionViolatedException(
+ "CameraDevice of the emulator may not be well prepared for camera" +
+ " related tests. Ignore the test: " + description.displayName +
+ ". To test on emulator devices, please remove the " +
+ "IgnoreProblematicDeviceRule from the test class."
+ )
+ } else {
+ base.evaluate()
+ }
+ }
+ }
+ }
+
+ companion object {
+ // Sync from TestRequestBuilder.RequiresDeviceFilter
+ private const val EMULATOR_HARDWARE_GOLDFISH = "goldfish"
+ private const val EMULATOR_HARDWARE_RANCHU = "ranchu"
+ private const val EMULATOR_HARDWARE_GCE = "gce_x86"
+ private val emulatorHardwareNames: Set<String> = setOf(
+ EMULATOR_HARDWARE_GOLDFISH,
+ EMULATOR_HARDWARE_RANCHU,
+ EMULATOR_HARDWARE_GCE
+ )
+
+ private val isEmulator = emulatorHardwareNames.contains(Build.HARDWARE.lowercase())
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java
index 2267928..88ae896 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java
@@ -25,7 +25,6 @@
import androidx.annotation.RequiresApi;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.MirrorMode;
-import androidx.camera.core.ResolutionSelector;
import androidx.camera.core.UseCase;
import androidx.camera.core.impl.CaptureConfig;
import androidx.camera.core.impl.Config;
@@ -36,6 +35,7 @@
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.UseCaseConfig;
import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType;
+import androidx.camera.core.resolutionselector.ResolutionSelector;
import java.util.List;
import java.util.UUID;
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index 7dc5a90..3eb0bf2 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -72,7 +72,6 @@
import androidx.camera.core.ImageCapture;
import androidx.camera.core.Logger;
import androidx.camera.core.MirrorMode;
-import androidx.camera.core.ResolutionSelector;
import androidx.camera.core.SurfaceRequest;
import androidx.camera.core.UseCase;
import androidx.camera.core.ViewPort;
@@ -104,6 +103,7 @@
import androidx.camera.core.processing.DefaultSurfaceProcessor;
import androidx.camera.core.processing.SurfaceEdge;
import androidx.camera.core.processing.SurfaceProcessorNode;
+import androidx.camera.core.resolutionselector.ResolutionSelector;
import androidx.camera.video.StreamInfo.StreamState;
import androidx.camera.video.impl.VideoCaptureConfig;
import androidx.camera.video.internal.VideoValidatedEncoderProfilesProxy;
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageAnalysisTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageAnalysisTest.kt
index 17d87da..e8e96f9 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageAnalysisTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageAnalysisTest.kt
@@ -32,9 +32,10 @@
import androidx.camera.core.ImageAnalysis.BackpressureStrategy
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageProxy
-import androidx.camera.core.ResolutionSelector
import androidx.camera.core.impl.ImageOutputConfig
import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.core.resolutionselector.HighResolution
+import androidx.camera.core.resolutionselector.ResolutionSelector
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.testing.CameraPipeConfigTestRule
import androidx.camera.testing.CameraUtil
@@ -428,9 +429,11 @@
assumeTrue(maxHighResolutionOutputSize != null)
val resolutionSelector = ResolutionSelector.Builder()
- .setPreferredResolution(maxHighResolutionOutputSize!!)
- .setHighResolutionEnabled(true)
- .build()
+ .setHighResolutionEnabledFlags(HighResolution.FLAG_DEFAULT_MODE_ON)
+ .setResolutionFilter { _, _ ->
+ listOf(maxHighResolutionOutputSize)
+ }
+ .build()
val imageAnalysis = ImageAnalysis.Builder()
.setResolutionSelector(resolutionSelector)
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
index 119b3a0..129edde 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
@@ -48,7 +48,6 @@
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
-import androidx.camera.core.ResolutionSelector
import androidx.camera.core.UseCase
import androidx.camera.core.UseCaseGroup
import androidx.camera.core.ViewPort
@@ -63,6 +62,8 @@
import androidx.camera.core.impl.utils.CameraOrientationUtil
import androidx.camera.core.impl.utils.Exif
import androidx.camera.core.internal.compat.workaround.ExifRotationAvailability
+import androidx.camera.core.resolutionselector.HighResolution
+import androidx.camera.core.resolutionselector.ResolutionSelector
import androidx.camera.integration.core.util.CameraPipeUtil
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.testing.CameraPipeConfigTestRule
@@ -1582,11 +1583,12 @@
// Only runs the test when the device has high resolution output sizes
assumeTrue(maxHighResolutionOutputSize != null)
- val resolutionSelector =
- ResolutionSelector.Builder()
- .setPreferredResolution(maxHighResolutionOutputSize!!)
- .setHighResolutionEnabled(true)
- .build()
+ val resolutionSelector = ResolutionSelector.Builder()
+ .setHighResolutionEnabledFlags(HighResolution.FLAG_DEFAULT_MODE_ON)
+ .setResolutionFilter { _, _ ->
+ listOf(maxHighResolutionOutputSize)
+ }
+ .build()
val sensorOrientation = CameraUtil.getSensorOrientation(BACK_SELECTOR.lensFacing!!)
// Sets the target rotation to the camera sensor orientation to avoid the captured image
// buffer data rotated by the HAL and impact the final image resolution check
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
index 8348922..b744eea 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
@@ -29,11 +29,12 @@
import androidx.camera.core.CameraXConfig
import androidx.camera.core.ImageCapture
import androidx.camera.core.Preview
-import androidx.camera.core.ResolutionSelector
import androidx.camera.core.UseCase
import androidx.camera.core.impl.ImageOutputConfig
import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.core.internal.CameraUseCaseAdapter
+import androidx.camera.core.resolutionselector.HighResolution
+import androidx.camera.core.resolutionselector.ResolutionSelector
import androidx.camera.testing.CameraPipeConfigTestRule
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CameraUtil.PreTestCameraIdList
@@ -111,7 +112,9 @@
CameraXUtil.initialize(context!!, cameraConfig).get()
// init CameraX before creating Preview to get preview size with CameraX's context
- defaultBuilder = Preview.Builder.fromConfig(Preview.DEFAULT_CONFIG.config)
+ defaultBuilder = Preview.Builder.fromConfig(Preview.DEFAULT_CONFIG.config).also {
+ it.mutableConfig.removeOption(ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO)
+ }
surfaceFutureSemaphore = Semaphore( /*permits=*/0)
safeToReleaseSemaphore = Semaphore( /*permits=*/0)
}
@@ -548,9 +551,10 @@
// Arrange.
val resolutionSelector =
ResolutionSelector.Builder()
- .setMaxResolution(maxHighResolutionOutputSize!!)
- .setPreferredResolution(maxHighResolutionOutputSize)
- .setHighResolutionEnabled(true)
+ .setHighResolutionEnabledFlags(HighResolution.FLAG_DEFAULT_MODE_ON)
+ .setResolutionFilter { _, _ ->
+ listOf(maxHighResolutionOutputSize)
+ }
.build()
val preview = Preview.Builder().setResolutionSelector(resolutionSelector).build()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerStateTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerStateTest.kt
index 8aa3c7a..f979d1a 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerStateTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerStateTest.kt
@@ -34,6 +34,7 @@
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
import kotlin.test.assertFalse
+import kotlin.test.assertTrue
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@@ -464,7 +465,7 @@
// Act
// Moving more than threshold
val backwardDelta = scrollForwardSign.toFloat() * with(rule.density) {
- -DefaultPositionThreshold.toPx() / 2
+ -DefaultPositionThreshold.toPx() / 2
}
previousTargetPage = state.targetPage
@@ -622,6 +623,48 @@
}
@Test
+ fun settledPage_shouldChangeOnScrollFinished() {
+ // Arrange
+ val state = PagerState()
+ var settledPageChanges = 0
+ createPager(
+ state = state,
+ modifier = Modifier.fillMaxSize(),
+ effects = {
+ LaunchedEffect(key1 = state.settledPage) {
+ settledPageChanges++
+ }
+ }
+ )
+ rule.runOnIdle {
+ assertThat(state.settledPage).isEqualTo(state.currentPage)
+ assertTrue { settledPageChanges == 1 }
+ }
+
+ settledPageChanges = 0
+ val previousSettled = state.settledPage
+ rule.mainClock.autoAdvance = false
+ // Act
+ // Moving forward
+ rule.runOnIdle {
+ scope.launch {
+ state.animateScrollToPage(DefaultPageCount - 1)
+ }
+ }
+
+ assertTrue { state.isScrollInProgress }
+ assertTrue { settledPageChanges == 0 }
+ assertThat(state.settledPage).isEqualTo(previousSettled)
+
+ rule.mainClock.advanceTimeUntil { settledPageChanges != 0 }
+
+ rule.runOnIdle {
+ assertTrue { !state.isScrollInProgress }
+ assertThat(state.settledPage).isEqualTo(state.currentPage)
+ }
+ }
+
+ @Test
fun currentPageOffset_shouldReflectScrollingOfCurrentPage() {
// Arrange
val state = PagerState(DefaultPageCount / 2)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerFocusTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerFocusTest.kt
index 1552480..0fe45bd 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerFocusTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerFocusTest.kt
@@ -26,20 +26,26 @@
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.TEST_FONT_FAMILY
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.geometry.Offset
+import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalTextToolbar
+import androidx.compose.ui.platform.TextToolbar
+import androidx.compose.ui.platform.TextToolbarStatus
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.click
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.longClick
+import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
@@ -54,10 +60,10 @@
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
+import java.util.concurrent.CountDownLatch
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
@LargeTest
@RunWith(AndroidJUnit4::class)
@@ -139,6 +145,52 @@
}
}
+ @Test
+ fun leavingComposition_hidesTextToolbar() {
+ // null -> nothing got called, true -> show, false -> hide
+ var lastShowCalled: Boolean? = null
+ val fakeTextToolbar = FakeTextToolbar(
+ onShowMenu = { _, _, _, _, _ -> lastShowCalled = true },
+ onHideMenu = { lastShowCalled = false }
+ )
+
+ val tag = "SelectionContainer"
+
+ var inComposition by mutableStateOf(true)
+
+ rule.setContent {
+ CompositionLocalProvider(
+ LocalTextToolbar provides fakeTextToolbar
+ ) {
+ if (inComposition) {
+ SelectionContainer(modifier = Modifier.testTag("SelectionContainer")) {
+ BasicText(
+ AnnotatedString(textContent),
+ Modifier.fillMaxWidth(),
+ style = TextStyle(fontFamily = fontFamily, fontSize = fontSize),
+ softWrap = true,
+ overflow = TextOverflow.Clip,
+ maxLines = Int.MAX_VALUE,
+ inlineContent = mapOf(),
+ onTextLayout = {}
+ )
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag(tag).performTouchInput { longClick() }
+ rule.runOnIdle {
+ assertThat(lastShowCalled).isTrue()
+ }
+
+ inComposition = false
+
+ rule.runOnIdle {
+ assertThat(lastShowCalled).isFalse()
+ }
+ }
+
private fun createSelectionContainer(isRtl: Boolean = false) {
val measureLatch = CountDownLatch(1)
@@ -151,9 +203,11 @@
) {
Column {
SelectionContainer(
- modifier = Modifier.onGloballyPositioned {
- measureLatch.countDown()
- }.testTag("selectionContainer1"),
+ modifier = Modifier
+ .onGloballyPositioned {
+ measureLatch.countDown()
+ }
+ .testTag("selectionContainer1"),
selection = selection1.value,
onSelectionChange = {
selection1.value = it
@@ -170,14 +224,19 @@
inlineContent = mapOf(),
onTextLayout = {}
)
- Box(Modifier.size(boxSize, boxSize).testTag("box"))
+ Box(
+ Modifier
+ .size(boxSize, boxSize)
+ .testTag("box"))
}
}
SelectionContainer(
- modifier = Modifier.onGloballyPositioned {
- measureLatch.countDown()
- }.testTag("selectionContainer2"),
+ modifier = Modifier
+ .onGloballyPositioned {
+ measureLatch.countDown()
+ }
+ .testTag("selectionContainer2"),
selection = selection2.value,
onSelectionChange = {
selection2.value = it
@@ -201,4 +260,44 @@
view = it.findViewById<ViewGroup>(android.R.id.content)
}
}
+}
+
+internal fun FakeTextToolbar(
+ onShowMenu: (
+ rect: Rect,
+ onCopyRequested: (() -> Unit)?,
+ onPasteRequested: (() -> Unit)?,
+ onCutRequested: (() -> Unit)?,
+ onSelectAllRequested: (() -> Unit)?
+ ) -> Unit,
+ onHideMenu: () -> Unit
+): TextToolbar {
+ return object : TextToolbar {
+ private var _status: TextToolbarStatus = TextToolbarStatus.Hidden
+
+ override fun showMenu(
+ rect: Rect,
+ onCopyRequested: (() -> Unit)?,
+ onPasteRequested: (() -> Unit)?,
+ onCutRequested: (() -> Unit)?,
+ onSelectAllRequested: (() -> Unit)?
+ ) {
+ onShowMenu(
+ rect,
+ onCopyRequested,
+ onPasteRequested,
+ onCutRequested,
+ onSelectAllRequested
+ )
+ _status = TextToolbarStatus.Shown
+ }
+
+ override fun hide() {
+ onHideMenu()
+ _status = TextToolbarStatus.Hidden
+ }
+
+ override val status: TextToolbarStatus
+ get() = _status
+ }
}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt
index 3d8a7782..ef5913b 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt
@@ -14,9 +14,6 @@
* limitations under the License.
*/
-// TODO(b/160821157): Replace FocusState with FocusState2.isFocused
-@file:Suppress("DEPRECATION")
-
package androidx.compose.foundation.textfield
import android.os.Build
@@ -1373,8 +1370,8 @@
)
}
val textNode = rule.onNodeWithTag(Tag)
- textNode.performTouchInput { longClick() }
- textNode.performTextInputSelection(TextRange(0, 4))
+ textNode.performSemanticsAction(SemanticsActions.RequestFocus)
+ textNode.performTextInputSelection(TextRange(0, 5))
textNode.performKeyPress(
KeyEvent(
NativeKeyEvent(
@@ -1392,14 +1389,18 @@
)
)
+ rule.waitForIdle()
+ textNode.assertTextEquals("")
+ val selection = textNode.fetchSemanticsNode().config
+ .getOrNull(SemanticsProperties.TextSelectionRange)
+ assertThat(selection).isEqualTo(TextRange(0))
+
textFieldValue.value = "Hello"
rule.waitForIdle()
-
- val expected = TextRange(0, 0)
val actual = textNode.fetchSemanticsNode().config
.getOrNull(SemanticsProperties.TextSelectionRange)
- assertThat(actual).isEqualTo(expected)
+ assertThat(actual).isEqualTo(TextRange(0))
}
@Test
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemanticState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyLayoutSemanticState.kt
similarity index 92%
rename from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemanticState.kt
rename to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyLayoutSemanticState.kt
index c4e0403..5709700 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemanticState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyLayoutSemanticState.kt
@@ -14,10 +14,10 @@
* limitations under the License.
*/
-package androidx.compose.foundation.lazy.layout
+package androidx.compose.foundation.lazy
import androidx.compose.foundation.gestures.animateScrollBy
-import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.layout.LazyLayoutSemanticState
import androidx.compose.ui.semantics.CollectionInfo
internal fun LazyLayoutSemanticState(
@@ -44,4 +44,4 @@
} else {
CollectionInfo(rowCount = 1, columnCount = -1)
}
-}
\ No newline at end of file
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
index 664ac94..31cbd85 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
@@ -92,9 +92,9 @@
* We use the idea of sliding window as an optimization, so user can scroll up to this number of
* items until we have to regenerate the key to index map.
*/
-private const val NearestItemsSlidingWindowSize = 30
+internal const val NearestItemsSlidingWindowSize = 30
/**
* The minimum amount of items near the current first visible item we want to have mapping for.
*/
-private const val NearestItemsExtraItemCount = 100
\ No newline at end of file
+internal const val NearestItemsExtraItemCount = 100
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/LazyLayoutPager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/LazyLayoutPager.kt
new file mode 100644
index 0000000..1fcdf60
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/LazyLayoutPager.kt
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager.lazy
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.clipScrollableContainer
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollableDefaults
+import androidx.compose.foundation.gestures.scrollable
+import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.lazy.LazyListBeyondBoundsInfo
+import androidx.compose.foundation.lazy.NearestItemsExtraItemCount
+import androidx.compose.foundation.lazy.NearestItemsSlidingWindowSize
+import androidx.compose.foundation.lazy.layout.IntervalList
+import androidx.compose.foundation.lazy.layout.LazyLayout
+import androidx.compose.foundation.lazy.layout.LazyLayoutIntervalContent
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.LazyLayoutKeyIndexMap
+import androidx.compose.foundation.lazy.layout.MutableIntervalList
+import androidx.compose.foundation.lazy.layout.NearestRangeKeyIndexMapState
+import androidx.compose.foundation.lazy.layout.PinnableItem
+import androidx.compose.foundation.lazy.layout.lazyLayoutSemantics
+import androidx.compose.foundation.overscroll
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.flow.drop
+import kotlinx.coroutines.flow.filter
+
+@ExperimentalFoundationApi
+@Composable
+internal fun Pager(
+ /** Modifier to be applied for the inner layout */
+ modifier: Modifier,
+ /** The amount of Pages that will be present in this Pager **/
+ pageCount: Int,
+ /** State controlling the scroll position */
+ state: PagerState,
+ /** The inner padding to be added for the whole content(not for each individual page) */
+ contentPadding: PaddingValues,
+ /** reverse the direction of scrolling and layout */
+ reverseLayout: Boolean,
+ /** The layout orientation of the Pager */
+ orientation: Orientation,
+ /** fling behavior to be used for flinging */
+ flingBehavior: SnapFlingBehavior,
+ /** Whether scrolling via the user gestures is allowed. */
+ userScrollEnabled: Boolean,
+ /** Number of pages to layout before and after the visible pages */
+ beyondBoundsPageCount: Int = 0,
+ /** Space between pages **/
+ pageSpacing: Dp = 0.dp,
+ /** Allows to change how to calculate the Page size **/
+ pageSize: PageSize,
+ /** A [NestedScrollConnection] that dictates how this [Pager] behaves with nested lists. **/
+ pageNestedScrollConnection: NestedScrollConnection,
+ /** a stable and unique key representing the Page **/
+ key: ((index: Int) -> Any)?,
+ /** The alignment to align pages horizontally. Required when isVertical is true */
+ horizontalAlignment: Alignment.Horizontal,
+ /** The alignment to align pages vertically. Required when isVertical is false */
+ verticalAlignment: Alignment.Vertical,
+ /** The content of the list */
+ pageContent: @Composable (page: Int) -> Unit
+) {
+ require(beyondBoundsPageCount >= 0) {
+ "beyondBoundsPageCount should be greater than or equal to 0, " +
+ "you selected $beyondBoundsPageCount"
+ }
+
+ val overscrollEffect = ScrollableDefaults.overscrollEffect()
+
+ // TODO(levima) Add test for pageContent change
+ val pagerItemProvider = remember(state, pageCount, key, pageContent as Any?) {
+ val intervalList = MutableIntervalList<PagerIntervalContent>().apply {
+ addInterval(pageCount, PagerIntervalContent(key = key, item = pageContent))
+ }
+ PagerLazyLayoutItemProvider(
+ state = state,
+ intervals = intervalList
+ )
+ }
+
+ val beyondBoundsInfo = remember { LazyListBeyondBoundsInfo() }
+
+ val measurePolicy = rememberPagerMeasurePolicy(
+ state = state,
+ contentPadding = contentPadding,
+ reverseLayout = reverseLayout,
+ orientation = orientation,
+ beyondBoundsPageCount = beyondBoundsPageCount,
+ pageSpacing = pageSpacing,
+ pageSize = pageSize,
+ horizontalAlignment = horizontalAlignment,
+ verticalAlignment = verticalAlignment,
+ itemProvider = pagerItemProvider,
+ pageCount = pageCount,
+ beyondBoundsInfo = beyondBoundsInfo
+ )
+
+ val pagerFlingBehavior = remember(flingBehavior, state) {
+ PagerWrapperFlingBehavior(flingBehavior, state)
+ }
+
+ // TODO(levima) Move this logic to measure pass
+ LaunchedEffect(state) {
+ snapshotFlow { state.isScrollInProgress }
+ .filter { !it }
+ .drop(1) // Initial scroll is false
+ .collect { state.updateOnScrollStopped() }
+ }
+
+ val pagerSemantics = if (userScrollEnabled) {
+ Modifier.pagerSemantics(state, orientation == Orientation.Vertical)
+ } else {
+ Modifier
+ }
+
+ val semanticState = rememberPagerSemanticState(
+ state,
+ pagerItemProvider,
+ reverseLayout,
+ orientation == Orientation.Vertical
+ )
+
+ LazyLayout(
+ modifier = modifier
+ .then(state.remeasurementModifier)
+ .then(state.awaitLayoutModifier)
+ .then(pagerSemantics)
+ .lazyLayoutSemantics(
+ itemProvider = pagerItemProvider,
+ state = semanticState,
+ orientation = orientation,
+ userScrollEnabled = userScrollEnabled,
+ reverseScrolling = reverseLayout
+ )
+ .clipScrollableContainer(orientation)
+ .pagerBeyondBoundsModifier(state, beyondBoundsInfo, reverseLayout, orientation)
+ .overscroll(overscrollEffect)
+ .scrollable(
+ orientation = orientation,
+ reverseDirection = ScrollableDefaults.reverseDirection(
+ LocalLayoutDirection.current,
+ orientation,
+ reverseLayout
+ ),
+ interactionSource = state.internalInteractionSource,
+ flingBehavior = pagerFlingBehavior,
+ state = state,
+ overscrollEffect = overscrollEffect,
+ enabled = userScrollEnabled
+ )
+ .nestedScroll(pageNestedScrollConnection),
+ measurePolicy = measurePolicy,
+ prefetchState = state.prefetchState,
+ itemProvider = pagerItemProvider
+ )
+}
+
+@ExperimentalFoundationApi
+internal class PagerLazyLayoutItemProvider(
+ intervals: IntervalList<PagerIntervalContent>,
+ val state: PagerState
+) : LazyLayoutItemProvider {
+ private val pagerContent = PagerLayoutIntervalContent(intervals)
+ private val keyToIndexMap: LazyLayoutKeyIndexMap by NearestRangeKeyIndexMapState(
+ firstVisibleItemIndex = { state.firstVisiblePage },
+ slidingWindowSize = { NearestItemsSlidingWindowSize },
+ extraItemCount = { NearestItemsExtraItemCount },
+ content = { pagerContent }
+ )
+ override val itemCount: Int
+ get() = pagerContent.itemCount
+
+ @Composable
+ override fun Item(index: Int) {
+ pagerContent.PinnableItem(index, state.pinnedPages) { localIndex ->
+ item(localIndex)
+ }
+ }
+
+ override fun getKey(index: Int): Any = pagerContent.getKey(index)
+
+ override fun getIndex(key: Any): Int = keyToIndexMap[key]
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private class PagerLayoutIntervalContent(
+ override val intervals: IntervalList<PagerIntervalContent>
+) : LazyLayoutIntervalContent<PagerIntervalContent>()
+
+@OptIn(ExperimentalFoundationApi::class)
+internal class PagerIntervalContent(
+ override val key: ((page: Int) -> Any)?,
+ val item: @Composable (page: Int) -> Unit
+) : LazyLayoutIntervalContent.Interval
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemanticState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/LazyLayoutSemanticState.kt
similarity index 79%
copy from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemanticState.kt
copy to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/LazyLayoutSemanticState.kt
index c4e0403..1e8af8c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemanticState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/LazyLayoutSemanticState.kt
@@ -14,19 +14,21 @@
* limitations under the License.
*/
-package androidx.compose.foundation.lazy.layout
+package androidx.compose.foundation.pager.lazy
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.animateScrollBy
-import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.layout.LazyLayoutSemanticState
import androidx.compose.ui.semantics.CollectionInfo
+@OptIn(ExperimentalFoundationApi::class)
internal fun LazyLayoutSemanticState(
- state: LazyListState,
+ state: PagerState,
isVertical: Boolean
): LazyLayoutSemanticState = object : LazyLayoutSemanticState {
override val currentPosition: Float
- get() = state.firstVisibleItemIndex + state.firstVisibleItemScrollOffset / 100_000f
+ get() = state.firstVisiblePage + state.firstVisiblePageOffset / 100_000f
override val canScrollForward: Boolean
get() = state.canScrollForward
@@ -35,7 +37,7 @@
}
override suspend fun scrollToItem(index: Int) {
- state.scrollToItem(index)
+ state.scrollToPage(index)
}
override fun collectionInfo(): CollectionInfo =
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/MeasuredPage.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/MeasuredPage.kt
new file mode 100644
index 0000000..aedd26b
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/MeasuredPage.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager.lazy
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.util.fastForEach
+
+internal class MeasuredPage(
+ val index: Int,
+ val size: Int,
+ val placeables: List<Placeable>,
+ val visualOffset: IntOffset,
+ val key: Any,
+ val orientation: Orientation,
+ val horizontalAlignment: Alignment.Horizontal?,
+ val verticalAlignment: Alignment.Vertical?,
+ val layoutDirection: LayoutDirection,
+ val reverseLayout: Boolean,
+ val beforeContentPadding: Int,
+ val afterContentPadding: Int,
+) {
+
+ val crossAxisSize: Int
+
+ init {
+ var maxCrossAxis = 0
+ placeables.fastForEach {
+ maxCrossAxis = maxOf(
+ maxCrossAxis,
+ if (orientation != Orientation.Vertical) it.height else it.width
+ )
+ }
+ crossAxisSize = maxCrossAxis
+ }
+
+ fun position(
+ offset: Int,
+ layoutWidth: Int,
+ layoutHeight: Int
+ ): PositionedPage {
+ val wrappers = mutableListOf<PagerPlaceableWrapper>()
+ val mainAxisLayoutSize =
+ if (orientation == Orientation.Vertical) layoutHeight else layoutWidth
+ var mainAxisOffset = if (reverseLayout) {
+ mainAxisLayoutSize - offset - size
+ } else {
+ offset
+ }
+ var index = if (reverseLayout) placeables.lastIndex else 0
+ while (if (reverseLayout) index >= 0 else index < placeables.size) {
+ val it = placeables[index]
+ val addIndex = if (reverseLayout) 0 else wrappers.size
+ val placeableOffset = if (orientation == Orientation.Vertical) {
+ val x = requireNotNull(horizontalAlignment)
+ .align(it.width, layoutWidth, layoutDirection)
+ IntOffset(x, mainAxisOffset)
+ } else {
+ val y = requireNotNull(verticalAlignment).align(it.height, layoutHeight)
+ IntOffset(mainAxisOffset, y)
+ }
+ mainAxisOffset += if (orientation == Orientation.Vertical) it.height else it.width
+ wrappers.add(
+ addIndex,
+ PagerPlaceableWrapper(placeableOffset, it, placeables[index].parentData)
+ )
+ if (reverseLayout) index-- else index++
+ }
+ return PositionedPage(
+ offset = offset,
+ index = this.index,
+ key = key,
+ orientation = orientation,
+ wrappers = wrappers,
+ visualOffset = visualOffset,
+ )
+ }
+}
+
+internal class PagerPlaceableWrapper(
+ val offset: IntOffset,
+ val placeable: Placeable,
+ val parentData: Any?
+)
\ No newline at end of file
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/SizeCoordinate.java b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PageInfo.kt
similarity index 65%
rename from camera/camera-core/src/main/java/androidx/camera/core/impl/SizeCoordinate.java
rename to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PageInfo.kt
index b1cfbf1..c1b85f1 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/SizeCoordinate.java
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PageInfo.kt
@@ -14,19 +14,12 @@
* limitations under the License.
*/
-package androidx.camera.core.impl;
+package androidx.compose.foundation.pager.lazy
-/**
- * The size coordinate system enum.
- */
-public enum SizeCoordinate {
- /**
- * Size is expressed in the camera sensor's natural orientation (landscape).
- */
- CAMERA_SENSOR,
+import androidx.compose.foundation.ExperimentalFoundationApi
- /**
- * Size is expressed in the Android View's orientation.
- */
- ANDROID_VIEW
-}
+@ExperimentalFoundationApi
+internal interface PageInfo {
+ val index: Int
+ val offset: Int
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/Pager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/Pager.kt
new file mode 100644
index 0000000..e12d3f8
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/Pager.kt
@@ -0,0 +1,631 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager.lazy
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.DecayAnimationSpec
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.calculateTargetValue
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.rememberSplineBasedDecay
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.FlingBehavior
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior
+import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.semantics.pageDown
+import androidx.compose.ui.semantics.pageLeft
+import androidx.compose.ui.semantics.pageRight
+import androidx.compose.ui.semantics.pageUp
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastFirstOrNull
+import androidx.compose.ui.util.fastForEach
+import kotlin.math.absoluteValue
+import kotlin.math.ceil
+import kotlin.math.floor
+import kotlin.math.sign
+import kotlinx.coroutines.launch
+
+/**
+ * A Pager that scrolls horizontally. Pages are lazily placed in accordance to the available
+ * viewport size. By definition, pages in a [Pager] have the same size, defined by [pageSize] and
+ * use a snap animation (provided by [flingBehavior] to scroll pages into a specific position). You
+ * can use [beyondBoundsPageCount] to place more pages before and after the visible pages.
+ *
+ * If you need snapping with pages of different size, you can use a [SnapFlingBehavior] with a
+ * [SnapLayoutInfoProvider] adapted to a LazyList.
+ * @see androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider for the implementation
+ * of a [SnapLayoutInfoProvider] that uses [androidx.compose.foundation.lazy.LazyListState].
+ *
+ * Please refer to the sample to learn how to use this API.
+ * @sample androidx.compose.foundation.samples.SimpleHorizontalPagerSample
+ *
+ * @param pageCount The number of pages this Pager will contain
+ * @param modifier A modifier instance to be applied to this Pager outer layout
+ * @param state The state to control this pager
+ * @param contentPadding a padding around the whole content. This will add padding for the
+ * content after it has been clipped, which is not possible via [modifier] param. You can use it
+ * to add a padding before the first page or after the last one. Use [pageSpacing] to add spacing
+ * between the pages.
+ * @param pageSize Use this to change how the pages will look like inside this pager.
+ * @param beyondBoundsPageCount Pages to load before and after the list of visible
+ * pages. Note: Be aware that using a large value for [beyondBoundsPageCount] will cause a lot of
+ * pages to be composed, measured and placed which will defeat the purpose of using lazy loading.
+ * This should be used as an optimization to pre-load a couple of pages before and after the visible
+ * ones.
+ * @param pageSpacing The amount of space to be used to separate the pages in this Pager
+ * @param verticalAlignment How pages are aligned vertically in this Pager.
+ * @param flingBehavior The [FlingBehavior] to be used for post scroll gestures.
+ * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions
+ * is allowed. You can still scroll programmatically using [PagerState.scroll] even when it is
+ * disabled.
+ * @param reverseLayout reverse the direction of scrolling and layout.
+ * @param key a stable and unique key representing the page. When you specify the key the scroll
+ * position will be maintained based on the key, which means if you add/remove pages before the
+ * current visible page the page with the given key will be kept as the first visible one.
+ * @param pageNestedScrollConnection A [NestedScrollConnection] that dictates how this [Pager]
+ * behaves with nested lists. The default behavior will see [Pager] to consume all nested deltas.
+ * @param pageContent This Pager's page Composable.
+ */
+@Composable
+@ExperimentalFoundationApi
+internal fun HorizontalPager(
+ pageCount: Int,
+ modifier: Modifier = Modifier,
+ state: PagerState = rememberPagerState(),
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+ pageSize: PageSize = PageSize.Fill,
+ beyondBoundsPageCount: Int = 0,
+ pageSpacing: Dp = 0.dp,
+ verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
+ flingBehavior: SnapFlingBehavior = PagerDefaults.flingBehavior(state = state),
+ userScrollEnabled: Boolean = true,
+ reverseLayout: Boolean = false,
+ key: ((index: Int) -> Any)? = null,
+ pageNestedScrollConnection: NestedScrollConnection = PagerDefaults.pageNestedScrollConnection(
+ Orientation.Horizontal
+ ),
+ pageContent: @Composable (page: Int) -> Unit
+) {
+ Pager(
+ modifier = modifier,
+ pageCount = pageCount,
+ state = state,
+ contentPadding = contentPadding,
+ reverseLayout = reverseLayout,
+ orientation = Orientation.Horizontal,
+ flingBehavior = flingBehavior,
+ userScrollEnabled = userScrollEnabled,
+ pageSize = pageSize,
+ beyondBoundsPageCount = beyondBoundsPageCount,
+ pageSpacing = pageSpacing,
+ pageContent = pageContent,
+ pageNestedScrollConnection = pageNestedScrollConnection,
+ verticalAlignment = verticalAlignment,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ key = key
+ )
+}
+
+/**
+ * A Pager that scrolls vertically. Pages are lazily placed in accordance to the available
+ * viewport size. By definition, pages in a [Pager] have the same size, defined by [pageSize] and
+ * use a snap animation (provided by [flingBehavior] to scroll pages into a specific position). You
+ * can use [beyondBoundsPageCount] to place more pages before and after the visible pages.
+ *
+ * If you need snapping with pages of different size, you can use a [SnapFlingBehavior] with a
+ * [SnapLayoutInfoProvider] adapted to a LazyList.
+ * @see androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider for the implementation
+ * of a [SnapLayoutInfoProvider] that uses [androidx.compose.foundation.lazy.LazyListState].
+ *
+ * Please refer to the sample to learn how to use this API.
+ * @sample androidx.compose.foundation.samples.SimpleVerticalPagerSample
+ *
+ * @param pageCount The number of pages this Pager will contain
+ * @param modifier A modifier instance to be apply to this Pager outer layout
+ * @param state The state to control this pager
+ * @param contentPadding a padding around the whole content. This will add padding for the
+ * content after it has been clipped, which is not possible via [modifier] param. You can use it
+ * to add a padding before the first page or after the last one. Use [pageSpacing] to add spacing
+ * between the pages.
+ * @param pageSize Use this to change how the pages will look like inside this pager.
+ * @param beyondBoundsPageCount Pages to load before and after the list of visible
+ * pages. Note: Be aware that using a large value for [beyondBoundsPageCount] will cause a lot of
+ * pages to be composed, measured and placed which will defeat the purpose of using lazy loading.
+ * This should be used as an optimization to pre-load a couple of pages before and after the visible
+ * ones.
+ * @param pageSpacing The amount of space to be used to separate the pages in this Pager
+ * @param horizontalAlignment How pages are aligned horizontally in this Pager.
+ * @param flingBehavior The [FlingBehavior] to be used for post scroll gestures.
+ * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions
+ * is allowed. You can still scroll programmatically using [PagerState.scroll] even when it is
+ * disabled.
+ * @param reverseLayout reverse the direction of scrolling and layout.
+ * @param key a stable and unique key representing the page. When you specify the key the scroll
+ * position will be maintained based on the key, which means if you add/remove pages before the
+ * current visible page the page with the given key will be kept as the first visible one.
+ * @param pageNestedScrollConnection A [NestedScrollConnection] that dictates how this [Pager] behaves
+ * with nested lists. The default behavior will see [Pager] to consume all nested deltas.
+ * @param pageContent This Pager's page Composable.
+ */
+@Composable
+@ExperimentalFoundationApi
+internal fun VerticalPager(
+ pageCount: Int,
+ modifier: Modifier = Modifier,
+ state: PagerState = rememberPagerState(),
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+ pageSize: PageSize = PageSize.Fill,
+ beyondBoundsPageCount: Int = 0,
+ pageSpacing: Dp = 0.dp,
+ horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
+ flingBehavior: SnapFlingBehavior = PagerDefaults.flingBehavior(state = state),
+ userScrollEnabled: Boolean = true,
+ reverseLayout: Boolean = false,
+ key: ((index: Int) -> Any)? = null,
+ pageNestedScrollConnection: NestedScrollConnection = PagerDefaults.pageNestedScrollConnection(
+ Orientation.Vertical
+ ),
+ pageContent: @Composable (page: Int) -> Unit
+) {
+ Pager(
+ modifier = modifier,
+ pageCount = pageCount,
+ state = state,
+ contentPadding = contentPadding,
+ reverseLayout = reverseLayout,
+ orientation = Orientation.Vertical,
+ flingBehavior = flingBehavior,
+ userScrollEnabled = userScrollEnabled,
+ pageSize = pageSize,
+ beyondBoundsPageCount = beyondBoundsPageCount,
+ pageSpacing = pageSpacing,
+ pageContent = pageContent,
+ pageNestedScrollConnection = pageNestedScrollConnection,
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalAlignment = horizontalAlignment,
+ key = key
+ )
+}
+
+/**
+ * This is used to determine how Pages are laid out in [Pager]. By changing the size of the pages
+ * one can change how many pages are shown.
+ *
+ * Please refer to the sample to learn how to use this API.
+ * @sample androidx.compose.foundation.samples.CustomPageSizeSample
+ *
+ */
+@ExperimentalFoundationApi
+@Stable
+internal interface PageSize {
+
+ /**
+ * Based on [availableSpace] pick a size for the pages
+ * @param availableSpace The amount of space the pages in this Pager can use.
+ * @param pageSpacing The amount of space used to separate pages.
+ */
+ fun Density.calculateMainAxisPageSize(availableSpace: Int, pageSpacing: Int): Int
+
+ /**
+ * Pages take up the whole Pager size.
+ */
+ @ExperimentalFoundationApi
+ object Fill : PageSize {
+ override fun Density.calculateMainAxisPageSize(availableSpace: Int, pageSpacing: Int): Int {
+ return availableSpace
+ }
+ }
+
+ /**
+ * Multiple pages in a viewport
+ * @param pageSize A fixed size for pages
+ */
+ @ExperimentalFoundationApi
+ class Fixed(val pageSize: Dp) : PageSize {
+ override fun Density.calculateMainAxisPageSize(availableSpace: Int, pageSpacing: Int): Int {
+ return pageSize.roundToPx()
+ }
+ }
+}
+
+/**
+ * Contains the default values used by [Pager].
+ */
+@ExperimentalFoundationApi
+internal object PagerDefaults {
+
+ /**
+ * A [SnapFlingBehavior] that will snap pages to the start of the layout. One can use the
+ * given parameters to control how the snapping animation will happen.
+ * @see androidx.compose.foundation.gestures.snapping.SnapFlingBehavior for more information
+ * on what which parameter controls in the overall snapping animation.
+ *
+ * @param state The [PagerState] that controls the which to which this FlingBehavior will
+ * be applied to.
+ * @param pagerSnapDistance A way to control the snapping destination for this [Pager].
+ * The default behavior will result in any fling going to the next page in the direction of the
+ * fling (if the fling has enough velocity, otherwise we'll bounce back). Use
+ * [PagerSnapDistance.atMost] to define a maximum number of pages this [Pager] is allowed to
+ * fling after scrolling is finished and fling has started.
+ * @param lowVelocityAnimationSpec The animation spec used to approach the target offset. When
+ * the fling velocity is not large enough. Large enough means large enough to naturally decay.
+ * @param highVelocityAnimationSpec The animation spec used to approach the target offset. When
+ * the fling velocity is large enough. Large enough means large enough to naturally decay.
+ * @param snapAnimationSpec The animation spec used to finally snap to the position.
+ *
+ * @return An instance of [FlingBehavior] that will perform Snapping to the next page by
+ * default. The animation will be governed by the post scroll velocity and we'll use either
+ * [lowVelocityAnimationSpec] or [highVelocityAnimationSpec] to approach the snapped position
+ * and the last step of the animation will be performed by [snapAnimationSpec].
+ */
+ @Composable
+ fun flingBehavior(
+ state: PagerState,
+ pagerSnapDistance: PagerSnapDistance = PagerSnapDistance.atMost(1),
+ lowVelocityAnimationSpec: AnimationSpec<Float> = tween(
+ easing = LinearEasing,
+ durationMillis = LowVelocityAnimationDefaultDuration
+ ),
+ highVelocityAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
+ snapAnimationSpec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
+ ): SnapFlingBehavior {
+ val density = LocalDensity.current
+
+ return remember(
+ lowVelocityAnimationSpec,
+ highVelocityAnimationSpec,
+ snapAnimationSpec,
+ pagerSnapDistance,
+ density
+ ) {
+ val snapLayoutInfoProvider =
+ SnapLayoutInfoProvider(
+ state,
+ pagerSnapDistance,
+ highVelocityAnimationSpec
+ )
+ SnapFlingBehavior(
+ snapLayoutInfoProvider = snapLayoutInfoProvider,
+ lowVelocityAnimationSpec = lowVelocityAnimationSpec,
+ highVelocityAnimationSpec = highVelocityAnimationSpec,
+ snapAnimationSpec = snapAnimationSpec,
+ density = density
+ )
+ }
+ }
+
+ /**
+ * The default implementation of Pager's pageNestedScrollConnection. All fling scroll deltas
+ * will be consumed by the Pager.
+ *
+ * @param orientation The orientation of the pager. This will be used to determine which
+ * direction it will consume everything. The other direction will not be consumed.
+ */
+ fun pageNestedScrollConnection(orientation: Orientation): NestedScrollConnection {
+ return if (orientation == Orientation.Horizontal) {
+ ConsumeHorizontalFlingNestedScrollConnection
+ } else {
+ ConsumeVerticalFlingNestedScrollConnection
+ }
+ }
+}
+
+/**
+ * [PagerSnapDistance] defines the way the [Pager] will treat the distance between the current
+ * page and the page where it will settle.
+ */
+@ExperimentalFoundationApi
+@Stable
+internal interface PagerSnapDistance {
+
+ /** Provides a chance to change where the [Pager] fling will settle.
+ *
+ * @param startPage The current page right before the fling starts.
+ * @param suggestedTargetPage The proposed target page where this fling will stop. This target
+ * will be the page that will be correctly positioned (snapped) after naturally decaying with
+ * [velocity] using a [DecayAnimationSpec].
+ * @param velocity The initial fling velocity.
+ * @param pageSize The page size for this [Pager].
+ * @param pageSpacing The spacing used between pages.
+ *
+ * @return An updated target page where to settle. Note that this value needs to be between 0
+ * and the total count of pages in this pager. If an invalid value is passed, the pager will
+ * coerce within the valid values.
+ */
+ fun calculateTargetPage(
+ startPage: Int,
+ suggestedTargetPage: Int,
+ velocity: Float,
+ pageSize: Int,
+ pageSpacing: Int
+ ): Int
+
+ companion object {
+ /**
+ * Limits the maximum number of pages that can be flung per fling gesture.
+ * @param pages The maximum number of extra pages that can be flung at once.
+ */
+ fun atMost(pages: Int): PagerSnapDistance {
+ require(pages >= 0) {
+ "pages should be greater than or equal to 0. You have used $pages."
+ }
+ return PagerSnapDistanceMaxPages(pages)
+ }
+ }
+}
+
+/**
+ * Limits the maximum number of pages that can be flung per fling gesture.
+ * @param pagesLimit The maximum number of extra pages that can be flung at once.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal class PagerSnapDistanceMaxPages(private val pagesLimit: Int) : PagerSnapDistance {
+ override fun calculateTargetPage(
+ startPage: Int,
+ suggestedTargetPage: Int,
+ velocity: Float,
+ pageSize: Int,
+ pageSpacing: Int,
+ ): Int {
+ return suggestedTargetPage.coerceIn(startPage - pagesLimit, startPage + pagesLimit)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return if (other is PagerSnapDistanceMaxPages) {
+ this.pagesLimit == other.pagesLimit
+ } else {
+ false
+ }
+ }
+
+ override fun hashCode(): Int {
+ return pagesLimit.hashCode()
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun SnapLayoutInfoProvider(
+ pagerState: PagerState,
+ pagerSnapDistance: PagerSnapDistance,
+ decayAnimationSpec: DecayAnimationSpec<Float>
+): SnapLayoutInfoProvider {
+ return object : SnapLayoutInfoProvider {
+ val layoutInfo: PagerLayoutInfo
+ get() = pagerState.layoutInfo
+
+ override fun Density.calculateSnappingOffsetBounds(): ClosedFloatingPointRange<Float> {
+ var lowerBoundOffset = Float.NEGATIVE_INFINITY
+ var upperBoundOffset = Float.POSITIVE_INFINITY
+
+ layoutInfo.visiblePagesInfo.fastForEach { page ->
+ val offset = calculateDistanceToDesiredSnapPosition(
+ layoutInfo,
+ page,
+ SnapAlignmentStartToStart
+ )
+
+ // Find page that is closest to the snap position, but before it
+ if (offset <= 0 && offset > lowerBoundOffset) {
+ lowerBoundOffset = offset
+ }
+
+ // Find page that is closest to the snap position, but after it
+ if (offset >= 0 && offset < upperBoundOffset) {
+ upperBoundOffset = offset
+ }
+ }
+
+ return lowerBoundOffset.rangeTo(upperBoundOffset)
+ }
+
+ override fun Density.calculateSnapStepSize(): Float = layoutInfo.pageSize.toFloat()
+
+ override fun Density.calculateApproachOffset(initialVelocity: Float): Float {
+ val effectivePageSizePx = pagerState.pageSize + pagerState.pageSpacing
+ val animationOffsetPx =
+ decayAnimationSpec.calculateTargetValue(0f, initialVelocity)
+ val startPage = pagerState.firstVisiblePageInfo?.let {
+ if (initialVelocity < 0) it.index + 1 else it.index
+ } ?: pagerState.currentPage
+
+ val scrollOffset =
+ layoutInfo.visiblePagesInfo.fastFirstOrNull { it.index == startPage }?.offset ?: 0
+
+ debugLog {
+ "Initial Offset=$scrollOffset " +
+ "\nAnimation Offset=$animationOffsetPx " +
+ "\nFling Start Page=$startPage " +
+ "\nEffective Page Size=$effectivePageSizePx"
+ }
+
+ val targetOffsetPx = startPage * effectivePageSizePx + animationOffsetPx
+
+ val targetPageValue = targetOffsetPx / effectivePageSizePx
+ val targetPage = if (initialVelocity > 0) {
+ ceil(targetPageValue)
+ } else {
+ floor(targetPageValue)
+ }.toInt().coerceIn(0, pagerState.pageCount)
+
+ debugLog { "Fling Target Page=$targetPage" }
+
+ val correctedTargetPage = pagerSnapDistance.calculateTargetPage(
+ startPage,
+ targetPage,
+ initialVelocity,
+ pagerState.pageSize,
+ pagerState.pageSpacing
+ ).coerceIn(0, pagerState.pageCount)
+
+ debugLog { "Fling Corrected Target Page=$correctedTargetPage" }
+
+ val proposedFlingOffset = (correctedTargetPage - startPage) * effectivePageSizePx
+
+ debugLog { "Proposed Fling Approach Offset=$proposedFlingOffset" }
+
+ val flingApproachOffsetPx =
+ (proposedFlingOffset.absoluteValue - scrollOffset.absoluteValue).coerceAtLeast(0)
+
+ return if (flingApproachOffsetPx == 0) {
+ flingApproachOffsetPx.toFloat()
+ } else {
+ flingApproachOffsetPx * initialVelocity.sign
+ }.also {
+ debugLog { "Fling Approach Offset=$it" }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+internal class PagerWrapperFlingBehavior(
+ val originalFlingBehavior: SnapFlingBehavior,
+ val pagerState: PagerState
+) : FlingBehavior {
+ override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
+ return with(originalFlingBehavior) {
+ performFling(initialVelocity) { remainingScrollOffset ->
+ pagerState.snapRemainingScrollOffset = remainingScrollOffset
+ }
+ }
+ }
+}
+
+private val ConsumeHorizontalFlingNestedScrollConnection =
+ ConsumeAllFlingOnDirection(Orientation.Horizontal)
+private val ConsumeVerticalFlingNestedScrollConnection =
+ ConsumeAllFlingOnDirection(Orientation.Vertical)
+
+private class ConsumeAllFlingOnDirection(val orientation: Orientation) : NestedScrollConnection {
+
+ fun Velocity.consumeOnOrientation(orientation: Orientation): Velocity {
+ return if (orientation == Orientation.Vertical) {
+ copy(x = 0f)
+ } else {
+ copy(y = 0f)
+ }
+ }
+
+ fun Offset.consumeOnOrientation(orientation: Orientation): Offset {
+ return if (orientation == Orientation.Vertical) {
+ copy(x = 0f)
+ } else {
+ copy(y = 0f)
+ }
+ }
+
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ return when (source) {
+ NestedScrollSource.Fling -> available.consumeOnOrientation(orientation)
+ else -> Offset.Zero
+ }
+ }
+
+ override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+ return available.consumeOnOrientation(orientation)
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Suppress("ComposableModifierFactory")
+@Composable
+internal fun Modifier.pagerSemantics(state: PagerState, isVertical: Boolean): Modifier {
+ val scope = rememberCoroutineScope()
+ fun performForwardPaging(): Boolean {
+ return if (state.canScrollForward) {
+ scope.launch {
+ state.animateToNextPage()
+ }
+ true
+ } else {
+ false
+ }
+ }
+
+ fun performBackwardPaging(): Boolean {
+ return if (state.canScrollBackward) {
+ scope.launch {
+ state.animateToPreviousPage()
+ }
+ true
+ } else {
+ false
+ }
+ }
+
+ return this.then(Modifier.semantics {
+ if (isVertical) {
+ pageUp { performBackwardPaging() }
+ pageDown { performForwardPaging() }
+ } else {
+ pageLeft { performBackwardPaging() }
+ pageRight { performForwardPaging() }
+ }
+ })
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+internal fun Density.calculateDistanceToDesiredSnapPosition(
+ layoutInfo: PagerLayoutInfo,
+ page: PageInfo,
+ positionInLayout: Density.(layoutSize: Float, itemSize: Float) -> Float
+): Float {
+ val containerSize =
+ with(layoutInfo) { singleAxisViewportSize - beforeContentPadding - afterContentPadding }
+
+ val desiredDistance =
+ positionInLayout(containerSize.toFloat(), layoutInfo.pageSize.toFloat())
+
+ val itemCurrentPosition = page.offset
+ return itemCurrentPosition - desiredDistance
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private val PagerLayoutInfo.singleAxisViewportSize: Int
+ get() = if (orientation == Orientation.Vertical) viewportSize.height else viewportSize.width
+
+private const val LowVelocityAnimationDefaultDuration = 500
+
+private const val DEBUG = false
+private inline fun debugLog(generateMsg: () -> String) {
+ if (DEBUG) {
+ println("Pager: ${generateMsg()}")
+ }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerBeyondBoundsModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerBeyondBoundsModifier.kt
new file mode 100644
index 0000000..b55d013
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerBeyondBoundsModifier.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.compose.foundation.pager.lazy
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.lazy.LazyListBeyondBoundsInfo
+import androidx.compose.foundation.lazy.layout.BeyondBoundsState
+import androidx.compose.foundation.lazy.layout.LazyLayoutBeyondBoundsModifierLocal
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalLayoutDirection
+
+/**
+ * This modifier is used to measure and place additional pages when the Pager receives a
+ * request to layout pages beyond the visible bounds.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+@Suppress("ComposableModifierFactory")
+@Composable
+internal fun Modifier.pagerBeyondBoundsModifier(
+ state: PagerState,
+ beyondBoundsInfo: LazyListBeyondBoundsInfo,
+ reverseLayout: Boolean,
+ orientation: Orientation
+): Modifier {
+ val layoutDirection = LocalLayoutDirection.current
+ return this then remember(
+ state,
+ beyondBoundsInfo,
+ reverseLayout,
+ layoutDirection,
+ orientation
+ ) {
+ LazyLayoutBeyondBoundsModifierLocal(
+ PagerBeyondBoundsState(state),
+ beyondBoundsInfo,
+ reverseLayout,
+ layoutDirection,
+ orientation
+ )
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+internal class PagerBeyondBoundsState(val state: PagerState) : BeyondBoundsState {
+ override fun remeasure() {
+ state.remeasurement?.forceRemeasure()
+ }
+
+ override val itemCount: Int
+ get() = state.layoutInfo.pagesCount
+ override val hasVisibleItems: Boolean
+ get() = state.layoutInfo.visiblePagesInfo.isNotEmpty()
+ override val firstVisibleIndex: Int
+ get() = state.firstVisiblePage
+ override val lastVisibleIndex: Int
+ get() = state.layoutInfo.visiblePagesInfo.last().index
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerLayoutInfo.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerLayoutInfo.kt
new file mode 100644
index 0000000..d482850
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerLayoutInfo.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager.lazy
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.IntSize
+
+@ExperimentalFoundationApi
+internal interface PagerLayoutInfo {
+ val visiblePagesInfo: List<PageInfo>
+ val pagesCount: Int
+ val pageSize: Int
+ val pageSpacing: Int
+ val viewportStartOffset: Int
+ val viewportEndOffset: Int
+ val beforeContentPadding: Int
+ val afterContentPadding: Int
+ val viewportSize: IntSize
+ val orientation: Orientation
+ val reverseLayout: Boolean
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerMeasure.kt
new file mode 100644
index 0000000..1d01a66
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerMeasure.kt
@@ -0,0 +1,645 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager.lazy
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.fastFilter
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
+import androidx.compose.foundation.lazy.LazyListBeyondBoundsInfo
+import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
+import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastMaxBy
+import kotlin.math.abs
+import kotlin.math.min
+import kotlin.math.roundToInt
+import kotlin.math.sign
+
+@OptIn(ExperimentalFoundationApi::class)
+internal fun LazyLayoutMeasureScope.measurePager(
+ pageCount: Int,
+ pagerItemProvider: PagerLazyLayoutItemProvider,
+ mainAxisAvailableSize: Int,
+ beforeContentPadding: Int,
+ afterContentPadding: Int,
+ spaceBetweenPages: Int,
+ firstVisiblePage: Int,
+ firstVisiblePageOffset: Int,
+ scrollToBeConsumed: Float,
+ constraints: Constraints,
+ orientation: Orientation,
+ verticalAlignment: Alignment.Vertical?,
+ horizontalAlignment: Alignment.Horizontal?,
+ reverseLayout: Boolean,
+ visualPageOffset: IntOffset,
+ pageAvailableSize: Int,
+ beyondBoundsPageCount: Int,
+ beyondBoundsInfo: LazyListBeyondBoundsInfo,
+ pinnedPages: LazyLayoutPinnedItemList,
+ layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
+): PagerMeasureResult {
+ require(beforeContentPadding >= 0)
+ require(afterContentPadding >= 0)
+
+ val pageSizeWithSpacing = (pageAvailableSize + spaceBetweenPages).coerceAtLeast(0)
+
+ debugLog { "Remeasuring..." }
+
+ return if (pageCount <= 0) {
+ PagerMeasureResult(
+ visiblePagesInfo = emptyList(),
+ pagesCount = 0,
+ pageSize = pageAvailableSize,
+ pageSpacing = spaceBetweenPages,
+ afterContentPadding = afterContentPadding,
+ orientation = orientation,
+ viewportStartOffset = -beforeContentPadding,
+ viewportEndOffset = mainAxisAvailableSize + afterContentPadding,
+ measureResult = layout(constraints.minWidth, constraints.minHeight) {},
+ consumedScroll = 0f,
+ closestPageToSnapPosition = null,
+ firstVisiblePage = null,
+ firstVisiblePageOffset = 0,
+ reverseLayout = false,
+ canScrollForward = false
+ )
+ } else {
+
+ val childConstraints = Constraints(
+ maxWidth = if (orientation == Orientation.Vertical) {
+ constraints.maxWidth
+ } else {
+ pageAvailableSize
+ },
+ maxHeight = if (orientation != Orientation.Vertical) {
+ constraints.maxHeight
+ } else {
+ pageAvailableSize
+ }
+ )
+
+ var currentFirstPage = firstVisiblePage
+ var currentFirstPageScrollOffset = firstVisiblePageOffset
+ if (currentFirstPage >= pageCount) {
+ // the data set has been updated and now we have less pages that we were
+ // scrolled to before
+ currentFirstPage = pageCount - 1
+ currentFirstPageScrollOffset = 0
+ }
+
+ // represents the real amount of scroll we applied as a result of this measure pass.
+ var scrollDelta = scrollToBeConsumed.roundToInt()
+
+ // applying the whole requested scroll offset. we will figure out if we can't consume
+ // all of it later
+ currentFirstPageScrollOffset -= scrollDelta
+
+ // if the current scroll offset is less than minimally possible
+ if (currentFirstPage == 0 && currentFirstPageScrollOffset < 0) {
+ scrollDelta += currentFirstPageScrollOffset
+ currentFirstPageScrollOffset = 0
+ }
+
+ // this will contain all the measured pages representing the visible pages
+ val visiblePages = mutableListOf<MeasuredPage>()
+
+ // define min and max offsets
+ val minOffset = -beforeContentPadding + if (spaceBetweenPages < 0) spaceBetweenPages else 0
+ val maxOffset = mainAxisAvailableSize
+
+ // include the start padding so we compose pages in the padding area and neutralise page
+ // spacing (if the spacing is negative this will make sure the previous page is composed)
+ // before starting scrolling forward we will remove it back
+ currentFirstPageScrollOffset += minOffset
+
+ // max of cross axis sizes of all visible pages
+ var maxCrossAxis = 0
+
+ // we had scrolled backward or we compose pages in the start padding area, which means
+ // pages before current firstPageScrollOffset should be visible. compose them and update
+ // firstPageScrollOffset
+ while (currentFirstPageScrollOffset < 0 && currentFirstPage > 0) {
+ val previous = currentFirstPage - 1
+ val measuredPage = getAndMeasure(
+ index = previous,
+ childConstraints = childConstraints,
+ pagerItemProvider = pagerItemProvider,
+ visualPageOffset = visualPageOffset,
+ orientation = orientation,
+ horizontalAlignment = horizontalAlignment,
+ verticalAlignment = verticalAlignment,
+ afterContentPadding = afterContentPadding,
+ beforeContentPadding = beforeContentPadding,
+ layoutDirection = layoutDirection,
+ reverseLayout = reverseLayout,
+ pageAvailableSize = pageAvailableSize
+ )
+ visiblePages.add(0, measuredPage)
+ maxCrossAxis = maxOf(maxCrossAxis, measuredPage.crossAxisSize)
+ currentFirstPageScrollOffset += pageSizeWithSpacing
+ currentFirstPage = previous
+ }
+
+ // if we were scrolled backward, but there were not enough pages before. this means
+ // not the whole scroll was consumed
+ if (currentFirstPageScrollOffset < minOffset) {
+ scrollDelta += currentFirstPageScrollOffset
+ currentFirstPageScrollOffset = minOffset
+ }
+
+ // neutralize previously added padding as we stopped filling the before content padding
+ currentFirstPageScrollOffset -= minOffset
+
+ var index = currentFirstPage
+ val maxMainAxis = (maxOffset + afterContentPadding).coerceAtLeast(0)
+ var currentMainAxisOffset = -currentFirstPageScrollOffset
+
+ // first we need to skip pages we already composed while composing backward
+ visiblePages.fastForEach {
+ index++
+ currentMainAxisOffset += pageSizeWithSpacing
+ }
+
+ // then composing visible pages forward until we fill the whole viewport.
+ // we want to have at least one page in visiblePages even if in fact all the pages are
+ // offscreen, this can happen if the content padding is larger than the available size.
+ while (index < pageCount &&
+ (currentMainAxisOffset < maxMainAxis ||
+ currentMainAxisOffset <= 0 || // filling beforeContentPadding area
+ visiblePages.isEmpty())
+ ) {
+ val measuredPage = getAndMeasure(
+ index = index,
+ childConstraints = childConstraints,
+ pagerItemProvider = pagerItemProvider,
+ visualPageOffset = visualPageOffset,
+ orientation = orientation,
+ horizontalAlignment = horizontalAlignment,
+ verticalAlignment = verticalAlignment,
+ afterContentPadding = afterContentPadding,
+ beforeContentPadding = beforeContentPadding,
+ layoutDirection = layoutDirection,
+ reverseLayout = reverseLayout,
+ pageAvailableSize = pageAvailableSize
+ )
+ currentMainAxisOffset += pageSizeWithSpacing
+
+ if (currentMainAxisOffset <= minOffset && index != pageCount - 1) {
+ // this page is offscreen and will not be placed. advance firstVisiblePage
+ currentFirstPage = index + 1
+ currentFirstPageScrollOffset -= pageSizeWithSpacing
+ } else {
+ maxCrossAxis = maxOf(maxCrossAxis, measuredPage.crossAxisSize)
+ visiblePages.add(measuredPage)
+ }
+
+ index++
+ }
+
+ // we didn't fill the whole viewport with pages starting from firstVisiblePage.
+ // lets try to scroll back if we have enough pages before firstVisiblePage.
+ if (currentMainAxisOffset < maxOffset) {
+ val toScrollBack = maxOffset - currentMainAxisOffset
+ currentFirstPageScrollOffset -= toScrollBack
+ currentMainAxisOffset += toScrollBack
+ while (currentFirstPageScrollOffset < beforeContentPadding &&
+ currentFirstPage > 0
+ ) {
+ val previousIndex = currentFirstPage - 1
+ val measuredPage = getAndMeasure(
+ index = previousIndex,
+ childConstraints = childConstraints,
+ pagerItemProvider = pagerItemProvider,
+ visualPageOffset = visualPageOffset,
+ orientation = orientation,
+ horizontalAlignment = horizontalAlignment,
+ verticalAlignment = verticalAlignment,
+ afterContentPadding = afterContentPadding,
+ beforeContentPadding = beforeContentPadding,
+ layoutDirection = layoutDirection,
+ reverseLayout = reverseLayout,
+ pageAvailableSize = pageAvailableSize
+ )
+ visiblePages.add(0, measuredPage)
+ maxCrossAxis = maxOf(maxCrossAxis, measuredPage.crossAxisSize)
+ currentFirstPageScrollOffset += pageSizeWithSpacing
+ currentFirstPage = previousIndex
+ }
+ scrollDelta += toScrollBack
+ if (currentFirstPageScrollOffset < 0) {
+ scrollDelta += currentFirstPageScrollOffset
+ currentMainAxisOffset += currentFirstPageScrollOffset
+ currentFirstPageScrollOffset = 0
+ }
+ }
+
+ // report the amount of pixels we consumed. scrollDelta can be smaller than
+ // scrollToBeConsumed if there were not enough pages to fill the offered space or it
+ // can be larger if pages were resized, or if, for example, we were previously
+ // displaying the page 15, but now we have only 10 pages in total in the data set.
+ val consumedScroll = if (scrollToBeConsumed.roundToInt().sign == scrollDelta.sign &&
+ abs(scrollToBeConsumed.roundToInt()) >= abs(scrollDelta)
+ ) {
+ scrollDelta.toFloat()
+ } else {
+ scrollToBeConsumed
+ }
+
+ // the initial offset for pages from visiblePages list
+ require(currentFirstPageScrollOffset >= 0)
+ val visiblePagesScrollOffset = -currentFirstPageScrollOffset
+ var firstPage = visiblePages.first()
+
+ // even if we compose pages to fill before content padding we should ignore pages fully
+ // located there for the state's scroll position calculation (first page + first offset)
+ if (beforeContentPadding > 0 || spaceBetweenPages < 0) {
+ for (i in visiblePages.indices) {
+ val size = pageSizeWithSpacing
+ if (currentFirstPageScrollOffset != 0 && size <= currentFirstPageScrollOffset &&
+ i != visiblePages.lastIndex
+ ) {
+ currentFirstPageScrollOffset -= size
+ firstPage = visiblePages[i + 1]
+ } else {
+ break
+ }
+ }
+ }
+
+ // Compose extra pages before
+ val extraPagesBefore = createPagesBeforeList(
+ beyondBoundsInfo = beyondBoundsInfo,
+ currentFirstPage = currentFirstPage,
+ pagesCount = pageCount,
+ beyondBoundsPageCount = beyondBoundsPageCount,
+ pinnedPages = pinnedPages
+ ) {
+ getAndMeasure(
+ index = it,
+ childConstraints = childConstraints,
+ pagerItemProvider = pagerItemProvider,
+ visualPageOffset = visualPageOffset,
+ orientation = orientation,
+ horizontalAlignment = horizontalAlignment,
+ verticalAlignment = verticalAlignment,
+ afterContentPadding = afterContentPadding,
+ beforeContentPadding = beforeContentPadding,
+ layoutDirection = layoutDirection,
+ reverseLayout = reverseLayout,
+ pageAvailableSize = pageAvailableSize
+ )
+ }
+
+ // Update maxCrossAxis with extra pages
+ extraPagesBefore.fastForEach {
+ maxCrossAxis = maxOf(maxCrossAxis, it.crossAxisSize)
+ }
+
+ // Compose pages after last page
+ val extraPagesAfter = createPagesAfterList(
+ beyondBoundsInfo = beyondBoundsInfo,
+ visiblePages = visiblePages,
+ pagesCount = pageCount,
+ beyondBoundsPageCount = beyondBoundsPageCount,
+ pinnedPages = pinnedPages
+ ) {
+ getAndMeasure(
+ index = it,
+ childConstraints = childConstraints,
+ pagerItemProvider = pagerItemProvider,
+ visualPageOffset = visualPageOffset,
+ orientation = orientation,
+ horizontalAlignment = horizontalAlignment,
+ verticalAlignment = verticalAlignment,
+ afterContentPadding = afterContentPadding,
+ beforeContentPadding = beforeContentPadding,
+ layoutDirection = layoutDirection,
+ reverseLayout = reverseLayout,
+ pageAvailableSize = pageAvailableSize
+ )
+ }
+
+ // Update maxCrossAxis with extra pages
+ extraPagesAfter.fastForEach {
+ maxCrossAxis = maxOf(maxCrossAxis, it.crossAxisSize)
+ }
+
+ val noExtraPages = firstPage == visiblePages.first() &&
+ extraPagesBefore.isEmpty() &&
+ extraPagesAfter.isEmpty()
+
+ val layoutWidth = constraints
+ .constrainWidth(
+ if (orientation == Orientation.Vertical)
+ maxCrossAxis
+ else
+ currentMainAxisOffset
+ )
+ val layoutHeight = constraints
+ .constrainHeight(
+ if (orientation == Orientation.Vertical)
+ currentMainAxisOffset
+ else
+ maxCrossAxis
+ )
+
+ val positionedPages = calculatePagesOffsets(
+ pages = visiblePages,
+ extraPagesBefore = extraPagesBefore,
+ extraPagesAfter = extraPagesAfter,
+ layoutWidth = layoutWidth,
+ layoutHeight = layoutHeight,
+ finalMainAxisOffset = currentMainAxisOffset,
+ maxOffset = maxOffset,
+ pagesScrollOffset = visiblePagesScrollOffset,
+ orientation = orientation,
+ reverseLayout = reverseLayout,
+ density = this,
+ pageAvailableSize = pageAvailableSize,
+ spaceBetweenPages = spaceBetweenPages
+ )
+
+ val visiblePagesInfo = if (noExtraPages) positionedPages else positionedPages.fastFilter {
+ (it.index >= visiblePages.first().index && it.index <= visiblePages.last().index)
+ }
+ val viewPortSize = if (orientation == Orientation.Vertical) layoutHeight else layoutWidth
+
+ val closestPageToSnapPosition = visiblePagesInfo.fastMaxBy {
+ -abs(
+ calculateDistanceToDesiredSnapPosition(
+ viewPortSize,
+ beforeContentPadding,
+ afterContentPadding,
+ pageAvailableSize,
+ it,
+ SnapAlignmentStartToStart
+ )
+ )
+ }
+
+ return PagerMeasureResult(
+ firstVisiblePage = firstPage,
+ firstVisiblePageOffset = currentFirstPageScrollOffset,
+ closestPageToSnapPosition = closestPageToSnapPosition,
+ consumedScroll = consumedScroll,
+ measureResult = layout(layoutWidth, layoutHeight) {
+ positionedPages.fastForEach {
+ it.place(this)
+ }
+ },
+ viewportStartOffset = -beforeContentPadding,
+ viewportEndOffset = maxOffset + afterContentPadding,
+ visiblePagesInfo = visiblePagesInfo,
+ pagesCount = pageCount,
+ reverseLayout = reverseLayout,
+ orientation = orientation,
+ pageSize = pageAvailableSize,
+ pageSpacing = spaceBetweenPages,
+ afterContentPadding = afterContentPadding,
+ canScrollForward = index < pageCount || currentMainAxisOffset > maxOffset
+ )
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun Density.calculateDistanceToDesiredSnapPosition(
+ axisViewPortSize: Int,
+ beforeContentPadding: Int,
+ afterContentPadding: Int,
+ pageSize: Int,
+ page: PageInfo,
+ positionInLayout: Density.(layoutSize: Float, itemSize: Float) -> Float
+): Float {
+ val containerSize = axisViewPortSize - beforeContentPadding - afterContentPadding
+
+ val desiredDistance =
+ positionInLayout(containerSize.toFloat(), pageSize.toFloat())
+
+ val itemCurrentPosition = page.offset
+ return itemCurrentPosition - desiredDistance
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun createPagesAfterList(
+ beyondBoundsInfo: LazyListBeyondBoundsInfo,
+ visiblePages: MutableList<MeasuredPage>,
+ pagesCount: Int,
+ beyondBoundsPageCount: Int,
+ pinnedPages: LazyLayoutPinnedItemList,
+ getAndMeasure: (Int) -> MeasuredPage
+): List<MeasuredPage> {
+ fun LazyListBeyondBoundsInfo.endIndex() = min(end, pagesCount - 1)
+
+ var list: MutableList<MeasuredPage>? = null
+
+ var end = visiblePages.last().index
+
+ fun addPage(index: Int) {
+ if (list == null) list = mutableListOf()
+ requireNotNull(list).add(getAndMeasure(index))
+ }
+
+ if (beyondBoundsInfo.hasIntervals()) {
+ end = maxOf(beyondBoundsInfo.endIndex(), end)
+ }
+
+ end = minOf(end + beyondBoundsPageCount, pagesCount - 1)
+
+ for (i in visiblePages.last().index + 1..end) {
+ addPage(i)
+ }
+
+ pinnedPages.fastForEach { page ->
+ if (page.index in (end + 1) until pagesCount) {
+ addPage(page.index)
+ }
+ }
+
+ return list ?: emptyList()
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun createPagesBeforeList(
+ beyondBoundsInfo: LazyListBeyondBoundsInfo,
+ currentFirstPage: Int,
+ pagesCount: Int,
+ beyondBoundsPageCount: Int,
+ pinnedPages: LazyLayoutPinnedItemList,
+ getAndMeasure: (Int) -> MeasuredPage
+): List<MeasuredPage> {
+ fun LazyListBeyondBoundsInfo.startIndex() = min(start, pagesCount - 1)
+
+ var list: MutableList<MeasuredPage>? = null
+
+ var start = currentFirstPage
+
+ fun addPage(index: Int) {
+ if (list == null) list = mutableListOf()
+ requireNotNull(list).add(
+ getAndMeasure(index)
+ )
+ }
+
+ if (beyondBoundsInfo.hasIntervals()) {
+ start = minOf(beyondBoundsInfo.startIndex(), start)
+ }
+
+ start = maxOf(0, start - beyondBoundsPageCount)
+
+ for (i in currentFirstPage - 1 downTo start) {
+ addPage(i)
+ }
+
+ pinnedPages.fastForEach { page ->
+ if (page.index < start) {
+ addPage(page.index)
+ }
+ }
+
+ return list ?: emptyList()
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun LazyLayoutMeasureScope.getAndMeasure(
+ index: Int,
+ childConstraints: Constraints,
+ pagerItemProvider: PagerLazyLayoutItemProvider,
+ visualPageOffset: IntOffset,
+ orientation: Orientation,
+ horizontalAlignment: Alignment.Horizontal?,
+ verticalAlignment: Alignment.Vertical?,
+ afterContentPadding: Int,
+ beforeContentPadding: Int,
+ layoutDirection: LayoutDirection,
+ reverseLayout: Boolean,
+ pageAvailableSize: Int
+): MeasuredPage {
+ val key = pagerItemProvider.getKey(index)
+ val placeable = measure(index, childConstraints)
+
+ return MeasuredPage(
+ index = index,
+ placeables = placeable,
+ visualOffset = visualPageOffset,
+ horizontalAlignment = horizontalAlignment,
+ verticalAlignment = verticalAlignment,
+ afterContentPadding = afterContentPadding,
+ beforeContentPadding = beforeContentPadding,
+ layoutDirection = layoutDirection,
+ reverseLayout = reverseLayout,
+ size = pageAvailableSize,
+ orientation = orientation,
+ key = key
+ )
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun LazyLayoutMeasureScope.calculatePagesOffsets(
+ pages: List<MeasuredPage>,
+ extraPagesBefore: List<MeasuredPage>,
+ extraPagesAfter: List<MeasuredPage>,
+ layoutWidth: Int,
+ layoutHeight: Int,
+ finalMainAxisOffset: Int,
+ maxOffset: Int,
+ pagesScrollOffset: Int,
+ orientation: Orientation,
+ reverseLayout: Boolean,
+ density: Density,
+ spaceBetweenPages: Int,
+ pageAvailableSize: Int
+): MutableList<PositionedPage> {
+ val pageSizeWithSpacing = (pageAvailableSize + spaceBetweenPages)
+ val mainAxisLayoutSize = if (orientation == Orientation.Vertical) layoutHeight else layoutWidth
+ val hasSpareSpace = finalMainAxisOffset < minOf(mainAxisLayoutSize, maxOffset)
+ if (hasSpareSpace) {
+ check(pagesScrollOffset == 0)
+ }
+ val positionedPages =
+ ArrayList<PositionedPage>(pages.size + extraPagesBefore.size + extraPagesAfter.size)
+
+ if (hasSpareSpace) {
+ require(extraPagesBefore.isEmpty() && extraPagesAfter.isEmpty())
+
+ val pagesCount = pages.size
+ fun Int.reverseAware() =
+ if (!reverseLayout) this else pagesCount - this - 1
+
+ val sizes = IntArray(pagesCount) { pageAvailableSize }
+ val offsets = IntArray(pagesCount) { 0 }
+
+ val arrangement = spacedBy(pageAvailableSize.toDp())
+ if (orientation == Orientation.Vertical) {
+ with(arrangement) { density.arrange(mainAxisLayoutSize, sizes, offsets) }
+ } else {
+ with(arrangement) {
+ // Enforces Ltr layout direction as it is mirrored with placeRelative later.
+ density.arrange(mainAxisLayoutSize, sizes, LayoutDirection.Ltr, offsets)
+ }
+ }
+
+ val reverseAwareOffsetIndices =
+ if (!reverseLayout) offsets.indices else offsets.indices.reversed()
+ for (index in reverseAwareOffsetIndices) {
+ val absoluteOffset = offsets[index]
+ // when reverseLayout == true, offsets are stored in the reversed order to pages
+ val page = pages[index.reverseAware()]
+ val relativeOffset = if (reverseLayout) {
+ // inverse offset to align with scroll direction for positioning
+ mainAxisLayoutSize - absoluteOffset - page.size
+ } else {
+ absoluteOffset
+ }
+ positionedPages.add(page.position(relativeOffset, layoutWidth, layoutHeight))
+ }
+ } else {
+ var currentMainAxis = pagesScrollOffset
+ extraPagesBefore.fastForEach {
+ currentMainAxis -= pageSizeWithSpacing
+ positionedPages.add(it.position(currentMainAxis, layoutWidth, layoutHeight))
+ }
+
+ currentMainAxis = pagesScrollOffset
+ pages.fastForEach {
+ positionedPages.add(it.position(currentMainAxis, layoutWidth, layoutHeight))
+ currentMainAxis += pageSizeWithSpacing
+ }
+
+ extraPagesAfter.fastForEach {
+ positionedPages.add(it.position(currentMainAxis, layoutWidth, layoutHeight))
+ currentMainAxis += pageSizeWithSpacing
+ }
+ }
+ return positionedPages
+}
+
+private const val DEBUG = true
+private inline fun debugLog(generateMsg: () -> String) {
+ if (DEBUG) {
+ println("PagerMeasure: ${generateMsg()}")
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerMeasurePolicy.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerMeasurePolicy.kt
new file mode 100644
index 0000000..1747273
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerMeasurePolicy.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager.lazy
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.checkScrollableContainerConstraints
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.calculateEndPadding
+import androidx.compose.foundation.layout.calculateStartPadding
+import androidx.compose.foundation.lazy.LazyListBeyondBoundsInfo
+import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
+import androidx.compose.ui.unit.offset
+import kotlin.math.roundToInt
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+internal fun rememberPagerMeasurePolicy(
+ itemProvider: PagerLazyLayoutItemProvider,
+ state: PagerState,
+ contentPadding: PaddingValues,
+ reverseLayout: Boolean,
+ orientation: Orientation,
+ beyondBoundsPageCount: Int,
+ pageSpacing: Dp,
+ pageSize: PageSize,
+ horizontalAlignment: Alignment.Horizontal?,
+ verticalAlignment: Alignment.Vertical?,
+ pageCount: Int,
+ beyondBoundsInfo: LazyListBeyondBoundsInfo
+) = remember<LazyLayoutMeasureScope.(Constraints) -> MeasureResult>(
+ contentPadding,
+ pageSpacing,
+ pageSize,
+ state,
+ contentPadding,
+ reverseLayout,
+ orientation,
+ horizontalAlignment,
+ verticalAlignment,
+ pageCount,
+ beyondBoundsInfo
+) {
+ { containerConstraints ->
+ val isVertical = orientation == Orientation.Vertical
+ checkScrollableContainerConstraints(
+ containerConstraints,
+ if (isVertical) Orientation.Vertical else Orientation.Horizontal
+ )
+
+ // resolve content paddings
+ val startPadding =
+ if (isVertical) {
+ contentPadding.calculateLeftPadding(layoutDirection).roundToPx()
+ } else {
+ // in horizontal configuration, padding is reversed by placeRelative
+ contentPadding.calculateStartPadding(layoutDirection).roundToPx()
+ }
+
+ val endPadding =
+ if (isVertical) {
+ contentPadding.calculateRightPadding(layoutDirection).roundToPx()
+ } else {
+ // in horizontal configuration, padding is reversed by placeRelative
+ contentPadding.calculateEndPadding(layoutDirection).roundToPx()
+ }
+ val topPadding = contentPadding.calculateTopPadding().roundToPx()
+ val bottomPadding = contentPadding.calculateBottomPadding().roundToPx()
+ val totalVerticalPadding = topPadding + bottomPadding
+ val totalHorizontalPadding = startPadding + endPadding
+ val totalMainAxisPadding = if (isVertical) totalVerticalPadding else totalHorizontalPadding
+ val beforeContentPadding = when {
+ isVertical && !reverseLayout -> topPadding
+ isVertical && reverseLayout -> bottomPadding
+ !isVertical && !reverseLayout -> startPadding
+ else -> endPadding // !isVertical && reverseLayout
+ }
+ val afterContentPadding = totalMainAxisPadding - beforeContentPadding
+ val contentConstraints =
+ containerConstraints.offset(-totalHorizontalPadding, -totalVerticalPadding)
+
+ state.density = this
+
+ val spaceBetweenPages = pageSpacing.roundToPx()
+
+ // can be negative if the content padding is larger than the max size from constraints
+ val mainAxisAvailableSize = if (isVertical) {
+ containerConstraints.maxHeight - totalVerticalPadding
+ } else {
+ containerConstraints.maxWidth - totalHorizontalPadding
+ }
+ val visualItemOffset = if (!reverseLayout || mainAxisAvailableSize > 0) {
+ IntOffset(startPadding, topPadding)
+ } else {
+ // When layout is reversed and paddings together take >100% of the available space,
+ // layout size is coerced to 0 when positioning. To take that space into account,
+ // we offset start padding by negative space between paddings.
+ IntOffset(
+ if (isVertical) startPadding else startPadding + mainAxisAvailableSize,
+ if (isVertical) topPadding + mainAxisAvailableSize else topPadding
+ )
+ }
+
+ val pageAvailableSize =
+ with(pageSize) { calculateMainAxisPageSize(mainAxisAvailableSize, spaceBetweenPages) }
+
+ state.premeasureConstraints = Constraints(
+ maxWidth = if (orientation == Orientation.Vertical) {
+ contentConstraints.maxWidth
+ } else {
+ pageAvailableSize
+ },
+ maxHeight = if (orientation != Orientation.Vertical) {
+ contentConstraints.maxHeight
+ } else {
+ pageAvailableSize
+ }
+ )
+
+ val firstVisiblePage: Int
+ val firstVisiblePageOffset: Int
+ Snapshot.withoutReadObservation {
+ firstVisiblePage = state.firstVisiblePage
+ firstVisiblePageOffset = if (state.layoutInfo == EmptyLayoutInfo) {
+ (state.initialPageOffsetFraction * pageAvailableSize).roundToInt()
+ } else {
+ state.firstVisiblePageOffset
+ }
+ }
+
+ measurePager(
+ beforeContentPadding = beforeContentPadding,
+ afterContentPadding = afterContentPadding,
+ constraints = contentConstraints,
+ pageCount = pageCount,
+ spaceBetweenPages = spaceBetweenPages,
+ mainAxisAvailableSize = mainAxisAvailableSize,
+ visualPageOffset = visualItemOffset,
+ pageAvailableSize = pageAvailableSize,
+ beyondBoundsPageCount = beyondBoundsPageCount,
+ orientation = orientation,
+ firstVisiblePage = firstVisiblePage,
+ firstVisiblePageOffset = firstVisiblePageOffset,
+ horizontalAlignment = horizontalAlignment,
+ verticalAlignment = verticalAlignment,
+ pagerItemProvider = itemProvider,
+ reverseLayout = reverseLayout,
+ scrollToBeConsumed = state.scrollToBeConsumed,
+ beyondBoundsInfo = beyondBoundsInfo,
+ pinnedPages = state.pinnedPages,
+ layout = { width, height, placement ->
+ layout(
+ containerConstraints.constrainWidth(width + totalHorizontalPadding),
+ containerConstraints.constrainHeight(height + totalVerticalPadding),
+ emptyMap(),
+ placement
+ )
+ }
+ ).also {
+ state.applyMeasureResult(it)
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerMeasureResult.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerMeasureResult.kt
new file mode 100644
index 0000000..20409e1
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerMeasureResult.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager.lazy
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.unit.IntSize
+
+@OptIn(ExperimentalFoundationApi::class)
+internal class PagerMeasureResult(
+ override val visiblePagesInfo: List<PageInfo>,
+ override val pagesCount: Int,
+ override val pageSize: Int,
+ override val pageSpacing: Int,
+ override val afterContentPadding: Int,
+ override val orientation: Orientation,
+ override val viewportStartOffset: Int,
+ override val viewportEndOffset: Int,
+ override val reverseLayout: Boolean,
+ val consumedScroll: Float,
+ val firstVisiblePage: MeasuredPage?,
+ val closestPageToSnapPosition: PageInfo?,
+ val firstVisiblePageOffset: Int,
+ val canScrollForward: Boolean,
+ measureResult: MeasureResult,
+) : PagerLayoutInfo, MeasureResult by measureResult {
+ override val viewportSize: IntSize
+ get() = IntSize(width, height)
+ override val beforeContentPadding: Int get() = -viewportStartOffset
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerScrollPosition.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerScrollPosition.kt
new file mode 100644
index 0000000..8d96e99
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerScrollPosition.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager.lazy
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.DataIndex
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.Snapshot
+
+/**
+ * Contains the current scroll position represented by the first visible page and the first
+ * visible page scroll offset.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal class PagerScrollPosition(
+ initialPage: Int = 0,
+ initialScrollOffset: Int = 0
+) {
+ var firstVisiblePage by mutableStateOf(DataIndex(initialPage))
+ var currentPage by mutableStateOf(DataIndex(initialPage))
+
+ var scrollOffset by mutableStateOf(initialScrollOffset)
+ private set
+
+ private var hadFirstNotEmptyLayout = false
+
+ /** The last know key of the page at [firstVisiblePage] position. */
+ private var lastKnownFirstPageKey: Any? = null
+
+ /**
+ * Updates the current scroll position based on the results of the last measurement.
+ */
+ fun updateFromMeasureResult(measureResult: PagerMeasureResult) {
+ lastKnownFirstPageKey = measureResult.firstVisiblePage?.key
+ // we ignore the index and offset from measureResult until we get at least one
+ // measurement with real pages. otherwise the initial index and scroll passed to the
+ // state would be lost and overridden with zeros.
+ if (hadFirstNotEmptyLayout || measureResult.pagesCount > 0) {
+ hadFirstNotEmptyLayout = true
+ val scrollOffset = measureResult.firstVisiblePageOffset
+ check(scrollOffset >= 0f) { "scrollOffset should be non-negative ($scrollOffset)" }
+
+ Snapshot.withoutReadObservation {
+ update(
+ DataIndex(measureResult.firstVisiblePage?.index ?: 0),
+ scrollOffset
+ )
+ measureResult.closestPageToSnapPosition?.index?.let {
+ val currentPage = DataIndex(it)
+ if (currentPage != this.currentPage) {
+ this.currentPage = currentPage
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates the scroll position - the passed values will be used as a start position for
+ * composing the pages during the next measure pass and will be updated by the real
+ * position calculated during the measurement. This means that there is no guarantee that
+ * exactly this index and offset will be applied as it is possible that:
+ * a) there will be no page at this index in reality
+ * b) page at this index will be smaller than the asked scrollOffset, which means we would
+ * switch to the next page
+ * c) there will be not enough pages to fill the viewport after the requested index, so we
+ * would have to compose few elements before the asked index, changing the first visible page.
+ */
+ fun requestPosition(index: DataIndex, scrollOffset: Int) {
+ update(index, scrollOffset)
+ // clear the stored key as we have a direct request to scroll to [index] position and the
+ // next [checkIfFirstVisibleItemWasMoved] shouldn't override this.
+ lastKnownFirstPageKey = null
+ }
+
+ private fun update(index: DataIndex, scrollOffset: Int) {
+ require(index.value >= 0f) { "Index should be non-negative (${index.value})" }
+ if (index != this.firstVisiblePage) {
+ this.firstVisiblePage = index
+ }
+ if (scrollOffset != this.scrollOffset) {
+ this.scrollOffset = scrollOffset
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerSemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerSemantics.kt
new file mode 100644
index 0000000..e47f801
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerSemantics.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager.lazy
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.LazyLayoutSemanticState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+internal fun rememberPagerSemanticState(
+ state: PagerState,
+ itemProvider: LazyLayoutItemProvider,
+ reverseScrolling: Boolean,
+ isVertical: Boolean
+): LazyLayoutSemanticState {
+ return remember(state, itemProvider, reverseScrolling, isVertical) {
+ LazyLayoutSemanticState(state, isVertical)
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerState.kt
new file mode 100644
index 0000000..f550e81
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PagerState.kt
@@ -0,0 +1,594 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager.lazy
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.interaction.InteractionSource
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.lazy.AwaitFirstLayoutModifier
+import androidx.compose.foundation.lazy.DataIndex
+import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
+import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.structuralEqualityPolicy
+import androidx.compose.ui.layout.Remeasurement
+import androidx.compose.ui.layout.RemeasurementModifier
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastFirstOrNull
+import kotlin.math.abs
+import kotlin.math.roundToInt
+import kotlin.math.sign
+
+/**
+ * Creates and remember a [PagerState] to be used with a [Pager]
+ *
+ * Please refer to the sample to learn how to use this API.
+ * @sample androidx.compose.foundation.samples.PagerWithStateSample
+ *
+ * @param initialPage The pager that should be shown first.
+ * @param initialPageOffsetFraction The offset of the initial page as a fraction of the page size.
+ * This should vary between -0.5 and 0.5 and indicates how to offset the initial page from the
+ * snapped position.
+ */
+@ExperimentalFoundationApi
+@Composable
+internal fun rememberPagerState(
+ initialPage: Int = 0,
+ initialPageOffsetFraction: Float = 0f
+): PagerState {
+ return rememberSaveable(saver = PagerState.Saver) {
+ PagerState(initialPage = initialPage, initialPageOffsetFraction = initialPageOffsetFraction)
+ }
+}
+
+/**
+ * The state that can be used to control [VerticalPager] and [HorizontalPager]
+ * @param initialPage The initial page to be displayed
+ * @param initialPageOffsetFraction The offset of the initial page with respect to the start of
+ * the layout.
+ */
+@ExperimentalFoundationApi
+@Stable
+internal class PagerState(
+ val initialPage: Int = 0,
+ val initialPageOffsetFraction: Float = 0f
+) : ScrollableState {
+
+ init {
+ require(initialPageOffsetFraction in -0.5..0.5) {
+ "initialPageOffsetFraction $initialPageOffsetFraction is " +
+ "not within the range -0.5 to 0.5"
+ }
+ }
+
+ internal var snapRemainingScrollOffset by mutableStateOf(0f)
+
+ // TODO(levima) Use a Pager specific scroll position that will be based on
+ // currentPage/currentPageOffsetFraction
+ private val scrollPosition = PagerScrollPosition(initialPage, 0)
+
+ internal val firstVisiblePage: Int get() = scrollPosition.firstVisiblePage.value
+
+ internal val firstVisiblePageOffset: Int get() = scrollPosition.scrollOffset
+
+ internal var scrollToBeConsumed = 0f
+ private set
+
+ /**
+ * The ScrollableController instance. We keep it as we need to call stopAnimation on it once
+ * we reached the end of the list.
+ */
+ private val scrollableState = ScrollableState { -performScroll(-it) }
+
+ /**
+ * Only used for testing to confirm that we're not making too many measure passes
+ */
+ internal var numMeasurePasses: Int = 0
+ private set
+
+ /**
+ * Only used for testing to disable prefetching when needed to test the main logic.
+ */
+ internal var prefetchingEnabled: Boolean = true
+
+ /**
+ * The index scheduled to be prefetched (or the last prefetched index if the prefetch is done).
+ */
+ private var indexToPrefetch = -1
+
+ /**
+ * The handle associated with the current index from [indexToPrefetch].
+ */
+ private var currentPrefetchHandle: LazyLayoutPrefetchState.PrefetchHandle? = null
+
+ /**
+ * Keeps the scrolling direction during the previous calculation in order to be able to
+ * detect the scrolling direction change.
+ */
+ private var wasScrollingForward = false
+
+ /** Backing state for PagerLayoutInfo **/
+ private var pagerLayoutInfoState = mutableStateOf(EmptyLayoutInfo)
+
+ internal val layoutInfo: PagerLayoutInfo get() = pagerLayoutInfoState.value
+
+ internal val pageSpacing: Int
+ get() = pagerLayoutInfoState.value.pageSpacing
+
+ internal val pageSize: Int
+ get() = pagerLayoutInfoState.value.pageSize
+
+ internal var density: Density by mutableStateOf(UnitDensity)
+
+ private val visiblePages: List<PageInfo>
+ get() = pagerLayoutInfoState.value.visiblePagesInfo
+
+ private val pageAvailableSpace: Int
+ get() = pageSize + pageSpacing
+
+ /**
+ * How far the current page needs to scroll so the target page is considered to be the next
+ * page.
+ */
+ private val positionThresholdFraction: Float
+ get() = with(density) {
+ val minThreshold = minOf(DefaultPositionThreshold.toPx(), pageSize / 2f)
+ minThreshold / pageSize.toFloat()
+ }
+
+ internal val pageCount: Int
+ get() = pagerLayoutInfoState.value.pagesCount
+
+ internal val firstVisiblePageInfo: PageInfo?
+ get() = visiblePages.lastOrNull {
+ density.calculateDistanceToDesiredSnapPosition(
+ pagerLayoutInfoState.value,
+ it,
+ SnapAlignmentStartToStart
+ ) <= 0
+ }
+
+ private val distanceToSnapPosition: Float
+ get() = layoutInfo.visiblePagesInfo.fastFirstOrNull { it.index == currentPage }?.let {
+ density.calculateDistanceToDesiredSnapPosition(
+ layoutInfo,
+ it,
+ SnapAlignmentStartToStart
+ )
+ } ?: 0f
+
+ internal val internalInteractionSource: MutableInteractionSource = MutableInteractionSource()
+
+ /**
+ * [InteractionSource] that will be used to dispatch drag events when this
+ * list is being dragged. If you want to know whether the fling (or animated scroll) is in
+ * progress, use [isScrollInProgress].
+ */
+ val interactionSource: InteractionSource
+ get() = internalInteractionSource
+
+ /**
+ * The page that sits closest to the snapped position. This is an observable value and will
+ * change as the pager scrolls either by gesture or animation.
+ *
+ * Please refer to the sample to learn how to use this API.
+ * @sample androidx.compose.foundation.samples.ObservingStateChangesInPagerStateSample
+ *
+ */
+ val currentPage: Int get() = scrollPosition.currentPage.value
+
+ private var animationTargetPage by mutableStateOf(-1)
+
+ private var settledPageState = mutableStateOf(initialPage)
+
+ /**
+ * The page that is currently "settled". This is an animation/gesture unaware page in the sense
+ * that it will not be updated while the pages are being scrolled, but rather when the
+ * animation/scroll settles.
+ *
+ * Please refer to the sample to learn how to use this API.
+ * @sample androidx.compose.foundation.samples.ObservingStateChangesInPagerStateSample
+ */
+ val settledPage: Int get() = settledPageState.value.coerceInPageRange()
+
+ /**
+ * The page this [Pager] intends to settle to.
+ * During fling or animated scroll (from [animateScrollToPage] this will represent the page
+ * this pager intends to settle to. When no scroll is ongoing, this will be equal to
+ * [currentPage].
+ *
+ * Please refer to the sample to learn how to use this API.
+ * @sample androidx.compose.foundation.samples.ObservingStateChangesInPagerStateSample
+ */
+ val targetPage: Int by derivedStateOf(structuralEqualityPolicy()) {
+ val finalPage = if (!isScrollInProgress) {
+ currentPage
+ } else if (animationTargetPage != -1) {
+ animationTargetPage
+ } else if (snapRemainingScrollOffset == 0.0f) {
+ // act on scroll only
+ if (abs(currentPageOffsetFraction) >= abs(positionThresholdFraction)) {
+ currentPage + currentPageOffsetFraction.sign.toInt()
+ } else {
+ currentPage
+ }
+ } else {
+ // act on flinging
+ val pageDisplacement = snapRemainingScrollOffset / pageAvailableSpace
+ (currentPage + pageDisplacement.roundToInt())
+ }
+ finalPage.coerceInPageRange()
+ }
+
+ /**
+ * Indicates how far the current page is to the snapped position, this will vary from
+ * -0.5 (page is offset towards the start of the layout) to 0.5 (page is offset towards the end
+ * of the layout). This is 0.0 if the [currentPage] is in the snapped position. The value will
+ * flip once the current page changes.
+ *
+ * This property is observable and shouldn't be used as is in a composable function due to
+ * potential performance issues. To use it in the composition, please consider using a
+ * derived state (e.g [derivedStateOf]) to only have recompositions when the derived
+ * value changes.
+ *
+ * Please refer to the sample to learn how to use this API.
+ * @sample androidx.compose.foundation.samples.ObservingStateChangesInPagerStateSample
+ */
+ val currentPageOffsetFraction: Float by derivedStateOf(structuralEqualityPolicy()) {
+ val currentPagePositionOffset =
+ layoutInfo.visiblePagesInfo.fastFirstOrNull { it.index == currentPage }?.offset ?: 0
+ val pageUsedSpace = pageAvailableSpace.toFloat()
+ if (pageUsedSpace == 0f) {
+ // Default to 0 when there's no info about the page size yet.
+ initialPageOffsetFraction
+ } else {
+ ((-currentPagePositionOffset) / (pageUsedSpace)).coerceIn(
+ MinPageOffset, MaxPageOffset
+ )
+ }
+ }
+
+ internal val prefetchState = LazyLayoutPrefetchState()
+
+ /**
+ * Provides a modifier which allows to delay some interactions (e.g. scroll)
+ * until layout is ready.
+ */
+ internal val awaitLayoutModifier = AwaitFirstLayoutModifier()
+
+ /**
+ * The [Remeasurement] object associated with our layout. It allows us to remeasure
+ * synchronously during scroll.
+ */
+ internal var remeasurement: Remeasurement? by mutableStateOf(null)
+ private set
+
+ /**
+ * The modifier which provides [remeasurement].
+ */
+ internal val remeasurementModifier = object : RemeasurementModifier {
+ override fun onRemeasurementAvailable(remeasurement: Remeasurement) {
+ this@PagerState.remeasurement = remeasurement
+ }
+ }
+
+ /**
+ * Constraints passed to the prefetcher for premeasuring the prefetched items.
+ */
+ internal var premeasureConstraints by mutableStateOf(Constraints())
+
+ /**
+ * Stores currently pinned pages which are always composed, used by for beyond bound pages.
+ */
+ internal val pinnedPages = LazyLayoutPinnedItemList()
+
+ /**
+ * Scroll (jump immediately) to a given [page].
+ *
+ * Please refer to the sample to learn how to use this API.
+ * @sample androidx.compose.foundation.samples.ScrollToPageSample
+ *
+ * @param page The destination page to scroll to
+ * @param pageOffsetFraction A fraction of the page size that indicates the offset the
+ * destination page will be offset from its snapped position.
+ */
+ suspend fun scrollToPage(page: Int, pageOffsetFraction: Float = 0f) {
+ debugLog { "Scroll from page=$currentPage to page=$page" }
+ awaitScrollDependencies()
+ require(pageOffsetFraction in -0.5..0.5) {
+ "pageOffsetFraction $pageOffsetFraction is not within the range -0.5 to 0.5"
+ }
+ val targetPage = page.coerceInPageRange()
+ scrollPosition.requestPosition(
+ DataIndex(targetPage),
+ (pageAvailableSpace * pageOffsetFraction).roundToInt()
+ )
+ remeasurement?.forceRemeasure()
+ }
+
+ /**
+ * Scroll animate to a given [page]. If the [page] is too far away from [currentPage] we will
+ * not compose all pages in the way. We will pre-jump to a nearer page, compose and animate
+ * the rest of the pages until [page].
+ *
+ * Please refer to the sample to learn how to use this API.
+ * @sample androidx.compose.foundation.samples.AnimateScrollPageSample
+ *
+ * @param page The destination page to scroll to
+ * @param pageOffsetFraction A fraction of the page size that indicates the offset the
+ * destination page will be offset from its snapped position.
+ * @param animationSpec An [AnimationSpec] to move between pages. We'll use a [spring] as the
+ * default animation.
+ */
+ suspend fun animateScrollToPage(
+ page: Int,
+ pageOffsetFraction: Float = 0f,
+ animationSpec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow)
+ ) {
+ if (page == currentPage) return
+ awaitScrollDependencies()
+ require(pageOffsetFraction in -0.5..0.5) {
+ "pageOffsetFraction $pageOffsetFraction is not within the range -0.5 to 0.5"
+ }
+ var currentPosition = currentPage
+ val targetPage = page.coerceInPageRange()
+ animationTargetPage = targetPage
+ // If our future page is too far off, that is, outside of the current viewport
+ val firstVisiblePageIndex = visiblePages.first().index
+ val lastVisiblePageIndex = visiblePages.last().index
+ if (((page > currentPage && page > lastVisiblePageIndex) ||
+ (page < currentPage && page < firstVisiblePageIndex)) &&
+ abs(page - currentPage) >= MaxPagesForAnimateScroll
+ ) {
+ val preJumpPosition = if (page > currentPage) {
+ (page - visiblePages.size).coerceAtLeast(currentPosition)
+ } else {
+ page + visiblePages.size.coerceAtMost(currentPosition)
+ }
+
+ debugLog {
+ "animateScrollToPage with pre-jump to position=$preJumpPosition"
+ }
+
+ // Pre-jump to 1 viewport away from destination page, if possible
+ scrollToPage(preJumpPosition)
+ currentPosition = preJumpPosition
+ }
+
+ val targetOffset = targetPage * pageAvailableSpace
+ val currentOffset = currentPosition * pageAvailableSpace
+ val pageOffsetToSnappedPosition =
+ distanceToSnapPosition + pageOffsetFraction * pageAvailableSpace
+
+ val displacement = targetOffset - currentOffset + pageOffsetToSnappedPosition
+
+ debugLog { "animateScrollToPage $displacement pixels" }
+ animateScrollBy(displacement, animationSpec)
+ animationTargetPage = -1
+ }
+
+ private suspend fun awaitScrollDependencies() {
+ awaitLayoutModifier.waitForFirstLayout()
+ }
+
+ override suspend fun scroll(
+ scrollPriority: MutatePriority,
+ block: suspend ScrollScope.() -> Unit
+ ) {
+ awaitLayoutModifier.waitForFirstLayout()
+ scrollableState.scroll(scrollPriority, block)
+ }
+
+ override fun dispatchRawDelta(delta: Float): Float {
+ return scrollableState.dispatchRawDelta(delta)
+ }
+
+ override val isScrollInProgress: Boolean
+ get() = scrollableState.isScrollInProgress
+
+ override var canScrollForward: Boolean by mutableStateOf(false)
+ private set
+ override var canScrollBackward: Boolean by mutableStateOf(false)
+ private set
+
+ /**
+ * Updates the state with the new calculated scroll position and consumed scroll.
+ */
+ internal fun applyMeasureResult(result: PagerMeasureResult) {
+ scrollPosition.updateFromMeasureResult(result)
+ scrollToBeConsumed -= result.consumedScroll
+ pagerLayoutInfoState.value = result
+ canScrollForward = result.canScrollForward
+ canScrollBackward = (result.firstVisiblePage?.index ?: 0) != 0 ||
+ result.firstVisiblePageOffset != 0
+ cancelPrefetchIfVisibleItemsChanged(result)
+ }
+
+ private fun Int.coerceInPageRange() = if (pageCount > 0) {
+ coerceIn(0, pageCount - 1)
+ } else {
+ 0
+ }
+
+ internal fun updateOnScrollStopped() {
+ settledPageState.value = currentPage
+ }
+
+ private fun performScroll(distance: Float): Float {
+ if (distance < 0 && !canScrollForward || distance > 0 && !canScrollBackward) {
+ return 0f
+ }
+ check(abs(scrollToBeConsumed) <= 0.5f) {
+ "entered drag with non-zero pending scroll: $scrollToBeConsumed"
+ }
+ scrollToBeConsumed += distance
+
+ // scrollToBeConsumed will be consumed synchronously during the forceRemeasure invocation
+ // inside measuring we do scrollToBeConsumed.roundToInt() so there will be no scroll if
+ // we have less than 0.5 pixels
+ if (abs(scrollToBeConsumed) > 0.5f) {
+ val preScrollToBeConsumed = scrollToBeConsumed
+ remeasurement?.forceRemeasure()
+ if (prefetchingEnabled) {
+ notifyPrefetch(preScrollToBeConsumed - scrollToBeConsumed)
+ }
+ }
+
+ // here scrollToBeConsumed is already consumed during the forceRemeasure invocation
+ if (abs(scrollToBeConsumed) <= 0.5f) {
+ // We consumed all of it - we'll hold onto the fractional scroll for later, so report
+ // that we consumed the whole thing
+ return distance
+ } else {
+ val scrollConsumed = distance - scrollToBeConsumed
+ // We did not consume all of it - return the rest to be consumed elsewhere (e.g.,
+ // nested scrolling)
+ scrollToBeConsumed = 0f // We're not consuming the rest, give it back
+ return scrollConsumed
+ }
+ }
+
+ private fun notifyPrefetch(delta: Float) {
+ if (!prefetchingEnabled) {
+ return
+ }
+ val info = layoutInfo
+ if (info.visiblePagesInfo.isNotEmpty()) {
+ val scrollingForward = delta < 0
+ val indexToPrefetch = if (scrollingForward) {
+ info.visiblePagesInfo.last().index + 1
+ } else {
+ info.visiblePagesInfo.first().index - 1
+ }
+ if (indexToPrefetch != this.indexToPrefetch &&
+ indexToPrefetch in 0 until info.pagesCount
+ ) {
+ if (wasScrollingForward != scrollingForward) {
+ // the scrolling direction has been changed which means the last prefetched
+ // is not going to be reached anytime soon so it is safer to dispose it.
+ // if this item is already visible it is safe to call the method anyway
+ // as it will be no-op
+ currentPrefetchHandle?.cancel()
+ }
+ this.wasScrollingForward = scrollingForward
+ this.indexToPrefetch = indexToPrefetch
+ currentPrefetchHandle = prefetchState.schedulePrefetch(
+ indexToPrefetch, premeasureConstraints
+ )
+ }
+ }
+ }
+
+ private fun cancelPrefetchIfVisibleItemsChanged(info: PagerLayoutInfo) {
+ if (indexToPrefetch != -1 && info.visiblePagesInfo.isNotEmpty()) {
+ val expectedPrefetchIndex = if (wasScrollingForward) {
+ info.visiblePagesInfo.last().index + 1
+ } else {
+ info.visiblePagesInfo.first().index - 1
+ }
+ if (indexToPrefetch != expectedPrefetchIndex) {
+ indexToPrefetch = -1
+ currentPrefetchHandle?.cancel()
+ currentPrefetchHandle = null
+ }
+ }
+ }
+
+ companion object {
+ /**
+ * To keep current page and current page offset saved
+ */
+ val Saver: Saver<PagerState, *> = listSaver(
+ save = {
+ listOf(
+ it.currentPage,
+ it.currentPageOffsetFraction
+ )
+ },
+ restore = {
+ PagerState(
+ initialPage = it[0] as Int,
+ initialPageOffsetFraction = it[1] as Float
+ )
+ }
+ )
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+internal suspend fun PagerState.animateToNextPage() {
+ if (currentPage + 1 < pageCount) animateScrollToPage(currentPage + 1)
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+internal suspend fun PagerState.animateToPreviousPage() {
+ if (currentPage - 1 >= 0) animateScrollToPage(currentPage - 1)
+}
+
+private const val MinPageOffset = -0.5f
+private const val MaxPageOffset = 0.5f
+internal val DefaultPositionThreshold = 56.dp
+private const val MaxPagesForAnimateScroll = 3
+
+@OptIn(ExperimentalFoundationApi::class)
+internal val EmptyLayoutInfo = object : PagerLayoutInfo {
+ override val visiblePagesInfo: List<PageInfo> = emptyList()
+ override val pagesCount: Int = 0
+ override val pageSize: Int = 0
+ override val pageSpacing: Int = 0
+ override val beforeContentPadding: Int = 0
+ override val afterContentPadding: Int = 0
+ override val viewportSize: IntSize = IntSize.Zero
+ override val orientation: Orientation = Orientation.Horizontal
+ override val viewportStartOffset: Int = 0
+ override val viewportEndOffset: Int = 0
+ override val reverseLayout: Boolean = false
+}
+
+private val UnitDensity = object : Density {
+ override val density: Float = 1f
+ override val fontScale: Float = 1f
+}
+
+internal val SnapAlignmentStartToStart: Density.(layoutSize: Float, itemSize: Float) -> Float =
+ { _, _ -> 0f }
+
+private const val DEBUG = false
+private inline fun debugLog(generateMsg: () -> String) {
+ if (DEBUG) {
+ println("PagerState: ${generateMsg()}")
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PositionedPage.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PositionedPage.kt
new file mode 100644
index 0000000..4f1b26f9
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/lazy/PositionedPage.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.pager.lazy
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.IntOffset
+
+@OptIn(ExperimentalFoundationApi::class)
+internal class PositionedPage(
+ override val index: Int,
+ override val offset: Int,
+ val key: Any,
+ val orientation: Orientation,
+ val wrappers: MutableList<PagerPlaceableWrapper>,
+ val visualOffset: IntOffset
+) : PageInfo {
+ fun place(scope: Placeable.PlacementScope) = with(scope) {
+ repeat(wrappers.size) { index ->
+ val placeable = wrappers[index].placeable
+ val offset = wrappers[index].offset
+ if (orientation == Orientation.Vertical) {
+ placeable.placeWithLayer(offset + visualOffset)
+ } else {
+ placeable.placeRelativeWithLayer(offset + visualOffset)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
index 0d4733d..e2e8484 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
@@ -137,7 +137,8 @@
DisposableEffect(manager) {
onDispose {
- manager.hideSelectionToolbar()
+ manager.onRelease()
+ manager.hasFocus = false
}
}
}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTest.kt
index 98399ac..9055b1d 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTest.kt
@@ -16,6 +16,8 @@
package androidx.compose.material.pullrefresh
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@@ -26,6 +28,9 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
@@ -33,8 +38,10 @@
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onChild
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeDown
+import androidx.compose.ui.unit.IntSize
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
@@ -162,6 +169,50 @@
assertThat(indicatorNode.getUnclippedBoundsInRoot()).isEqualTo(restingBounds)
}
+ // Regression test for b/271777421
+ @Test
+ fun indicatorDoesNotCapturePointerEvents() {
+ var indicatorSize: IntSize? = null
+ lateinit var state: PullRefreshState
+ var downEvent: PointerInputChange? = null
+
+ rule.setContent {
+ state = rememberPullRefreshState(false, {})
+
+ Box {
+ Box(Modifier.fillMaxSize().pointerInput(Unit) {
+ awaitEachGesture {
+ downEvent = awaitFirstDown()
+ }
+ })
+ PullRefreshIndicator(
+ refreshing = false,
+ state = state,
+ modifier = Modifier.onSizeChanged {
+ // The indicator starts as offset by its negative height in the y direction,
+ // so work out its height so we can place it inside its normal layout
+ // bounds
+ indicatorSize = it
+ }.testTag(IndicatorTag)
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ // Pull by twice the indicator height (since pull delta is halved) - this will make the
+ // indicator fully visible in its layout bounds, so when we performClick() the indicator
+ // will be visibly inside those coordinates.
+ state.onPull(indicatorSize!!.height.toFloat() * 2)
+ }
+
+ rule.onNodeWithTag(IndicatorTag).performClick()
+ rule.runOnIdle {
+ // The indicator should not have blocked its sibling (placed first, so below) from
+ // seeing touch events.
+ assertThat(downEvent).isNotNull()
+ }
+ }
+
private val pullRefreshNode get() = rule.onNodeWithTag(PullRefreshTag)
private val indicatorNode get() = rule.onNodeWithTag(IndicatorTag).onChild()
}
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicator.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicator.kt
index 082cd36..e8c1323 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicator.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicator.kt
@@ -21,14 +21,15 @@
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.LocalElevationOverlay
import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Surface
import androidx.compose.material.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
@@ -37,6 +38,7 @@
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.center
@@ -82,13 +84,19 @@
derivedStateOf { refreshing || state.position > 0.5f }
}
- Surface(
+ // Apply an elevation overlay if needed. Note that we aren't using Surface here, as we do not
+ // want its input-blocking behaviour, since the indicator is typically displayed above other
+ // (possibly) interactive content.
+ val elevationOverlay = LocalElevationOverlay.current
+ val color = elevationOverlay?.apply(color = backgroundColor, elevation = Elevation)
+ ?: backgroundColor
+
+ Box(
modifier = modifier
.size(IndicatorSize)
- .pullRefreshIndicatorTransform(state, scale),
- shape = SpinnerShape,
- color = backgroundColor,
- elevation = if (showElevation) Elevation else 0.dp,
+ .pullRefreshIndicatorTransform(state, scale)
+ .shadow(if (showElevation) Elevation else 0.dp, SpinnerShape, clip = true)
+ .background(color = color, shape = SpinnerShape)
) {
Crossfade(
targetState = refreshing,
diff --git a/compose/runtime/runtime/api/current.ignore b/compose/runtime/runtime/api/current.ignore
index e28d869..1589837 100644
--- a/compose/runtime/runtime/api/current.ignore
+++ b/compose/runtime/runtime/api/current.ignore
@@ -3,3 +3,7 @@
Added method androidx.compose.runtime.Composer.getCurrentCompositionLocalMap()
AddedAbstractMethod: androidx.compose.runtime.CompositionContext#getEffectCoroutineContext():
Added method androidx.compose.runtime.CompositionContext.getEffectCoroutineContext()
+
+
+RemovedClass: androidx.compose.runtime.SnapshotStateKt:
+ Removed class androidx.compose.runtime.SnapshotStateKt
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index e6b9f52..1291a3c 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -448,110 +448,6 @@
method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
}
- public final class SnapshotStateKt {
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsState(kotlinx.coroutines.flow.StateFlow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
- method @androidx.compose.runtime.Composable public static <T extends R, R> androidx.compose.runtime.State<R> collectAsState(kotlinx.coroutines.flow.Flow<? extends T>, R? initial, optional kotlin.coroutines.CoroutineContext context);
- method public static <T> androidx.compose.runtime.State<T> derivedStateOf(kotlin.jvm.functions.Function0<? extends T> calculation);
- method public static <T> androidx.compose.runtime.State<T> derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy<T> policy, kotlin.jvm.functions.Function0<? extends T> calculation);
- method public static inline operator <T> T! getValue(androidx.compose.runtime.State<? extends T>, Object? thisObj, kotlin.reflect.KProperty<?> property);
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf();
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf(T?... elements);
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf();
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
- method public static <T> androidx.compose.runtime.MutableState<T> mutableStateOf(T? value, optional androidx.compose.runtime.SnapshotMutationPolicy<T> policy);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> neverEqualPolicy();
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, Object? key3, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object![]? keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> referentialEqualityPolicy();
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> rememberUpdatedState(T? newValue);
- method public static inline operator <T> void setValue(androidx.compose.runtime.MutableState<T>, Object? thisObj, kotlin.reflect.KProperty<?> property, T? value);
- method public static <T> kotlinx.coroutines.flow.Flow<T> snapshotFlow(kotlin.jvm.functions.Function0<? extends T> block);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> structuralEqualityPolicy();
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> toMutableStateList(java.util.Collection<? extends T>);
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
- }
-
- public final class SnapshotStateKt {
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsState(kotlinx.coroutines.flow.StateFlow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
- method @androidx.compose.runtime.Composable public static <T extends R, R> androidx.compose.runtime.State<R> collectAsState(kotlinx.coroutines.flow.Flow<? extends T>, R? initial, optional kotlin.coroutines.CoroutineContext context);
- method public static <T> androidx.compose.runtime.State<T> derivedStateOf(kotlin.jvm.functions.Function0<? extends T> calculation);
- method public static <T> androidx.compose.runtime.State<T> derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy<T> policy, kotlin.jvm.functions.Function0<? extends T> calculation);
- method public static inline operator <T> T! getValue(androidx.compose.runtime.State<? extends T>, Object? thisObj, kotlin.reflect.KProperty<?> property);
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf();
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf(T?... elements);
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf();
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
- method public static <T> androidx.compose.runtime.MutableState<T> mutableStateOf(T? value, optional androidx.compose.runtime.SnapshotMutationPolicy<T> policy);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> neverEqualPolicy();
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, Object? key3, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object![]? keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> referentialEqualityPolicy();
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> rememberUpdatedState(T? newValue);
- method public static inline operator <T> void setValue(androidx.compose.runtime.MutableState<T>, Object? thisObj, kotlin.reflect.KProperty<?> property, T? value);
- method public static <T> kotlinx.coroutines.flow.Flow<T> snapshotFlow(kotlin.jvm.functions.Function0<? extends T> block);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> structuralEqualityPolicy();
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> toMutableStateList(java.util.Collection<? extends T>);
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
- }
-
- public final class SnapshotStateKt {
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsState(kotlinx.coroutines.flow.StateFlow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
- method @androidx.compose.runtime.Composable public static <T extends R, R> androidx.compose.runtime.State<R> collectAsState(kotlinx.coroutines.flow.Flow<? extends T>, R? initial, optional kotlin.coroutines.CoroutineContext context);
- method public static <T> androidx.compose.runtime.State<T> derivedStateOf(kotlin.jvm.functions.Function0<? extends T> calculation);
- method public static <T> androidx.compose.runtime.State<T> derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy<T> policy, kotlin.jvm.functions.Function0<? extends T> calculation);
- method public static inline operator <T> T! getValue(androidx.compose.runtime.State<? extends T>, Object? thisObj, kotlin.reflect.KProperty<?> property);
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf();
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf(T?... elements);
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf();
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
- method public static <T> androidx.compose.runtime.MutableState<T> mutableStateOf(T? value, optional androidx.compose.runtime.SnapshotMutationPolicy<T> policy);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> neverEqualPolicy();
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, Object? key3, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object![]? keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> referentialEqualityPolicy();
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> rememberUpdatedState(T? newValue);
- method public static inline operator <T> void setValue(androidx.compose.runtime.MutableState<T>, Object? thisObj, kotlin.reflect.KProperty<?> property, T? value);
- method public static <T> kotlinx.coroutines.flow.Flow<T> snapshotFlow(kotlin.jvm.functions.Function0<? extends T> block);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> structuralEqualityPolicy();
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> toMutableStateList(java.util.Collection<? extends T>);
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
- }
-
- public final class SnapshotStateKt {
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsState(kotlinx.coroutines.flow.StateFlow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
- method @androidx.compose.runtime.Composable public static <T extends R, R> androidx.compose.runtime.State<R> collectAsState(kotlinx.coroutines.flow.Flow<? extends T>, R? initial, optional kotlin.coroutines.CoroutineContext context);
- method public static <T> androidx.compose.runtime.State<T> derivedStateOf(kotlin.jvm.functions.Function0<? extends T> calculation);
- method public static <T> androidx.compose.runtime.State<T> derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy<T> policy, kotlin.jvm.functions.Function0<? extends T> calculation);
- method public static inline operator <T> T! getValue(androidx.compose.runtime.State<? extends T>, Object? thisObj, kotlin.reflect.KProperty<?> property);
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf();
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf(T?... elements);
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf();
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
- method public static <T> androidx.compose.runtime.MutableState<T> mutableStateOf(T? value, optional androidx.compose.runtime.SnapshotMutationPolicy<T> policy);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> neverEqualPolicy();
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, Object? key3, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object![]? keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> referentialEqualityPolicy();
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> rememberUpdatedState(T? newValue);
- method public static inline operator <T> void setValue(androidx.compose.runtime.MutableState<T>, Object? thisObj, kotlin.reflect.KProperty<?> property, T? value);
- method public static <T> kotlinx.coroutines.flow.Flow<T> snapshotFlow(kotlin.jvm.functions.Function0<? extends T> block);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> structuralEqualityPolicy();
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> toMutableStateList(java.util.Collection<? extends T>);
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
- }
-
@androidx.compose.runtime.StableMarker @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface Stable {
}
diff --git a/compose/runtime/runtime/api/public_plus_experimental_current.txt b/compose/runtime/runtime/api/public_plus_experimental_current.txt
index 892930c..b6000ab 100644
--- a/compose/runtime/runtime/api/public_plus_experimental_current.txt
+++ b/compose/runtime/runtime/api/public_plus_experimental_current.txt
@@ -497,110 +497,6 @@
method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
}
- public final class SnapshotStateKt {
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsState(kotlinx.coroutines.flow.StateFlow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
- method @androidx.compose.runtime.Composable public static <T extends R, R> androidx.compose.runtime.State<R> collectAsState(kotlinx.coroutines.flow.Flow<? extends T>, R? initial, optional kotlin.coroutines.CoroutineContext context);
- method public static <T> androidx.compose.runtime.State<T> derivedStateOf(kotlin.jvm.functions.Function0<? extends T> calculation);
- method public static <T> androidx.compose.runtime.State<T> derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy<T> policy, kotlin.jvm.functions.Function0<? extends T> calculation);
- method public static inline operator <T> T! getValue(androidx.compose.runtime.State<? extends T>, Object? thisObj, kotlin.reflect.KProperty<?> property);
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf();
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf(T?... elements);
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf();
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
- method public static <T> androidx.compose.runtime.MutableState<T> mutableStateOf(T? value, optional androidx.compose.runtime.SnapshotMutationPolicy<T> policy);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> neverEqualPolicy();
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, Object? key3, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object![]? keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> referentialEqualityPolicy();
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> rememberUpdatedState(T? newValue);
- method public static inline operator <T> void setValue(androidx.compose.runtime.MutableState<T>, Object? thisObj, kotlin.reflect.KProperty<?> property, T? value);
- method public static <T> kotlinx.coroutines.flow.Flow<T> snapshotFlow(kotlin.jvm.functions.Function0<? extends T> block);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> structuralEqualityPolicy();
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> toMutableStateList(java.util.Collection<? extends T>);
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
- }
-
- public final class SnapshotStateKt {
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsState(kotlinx.coroutines.flow.StateFlow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
- method @androidx.compose.runtime.Composable public static <T extends R, R> androidx.compose.runtime.State<R> collectAsState(kotlinx.coroutines.flow.Flow<? extends T>, R? initial, optional kotlin.coroutines.CoroutineContext context);
- method public static <T> androidx.compose.runtime.State<T> derivedStateOf(kotlin.jvm.functions.Function0<? extends T> calculation);
- method public static <T> androidx.compose.runtime.State<T> derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy<T> policy, kotlin.jvm.functions.Function0<? extends T> calculation);
- method public static inline operator <T> T! getValue(androidx.compose.runtime.State<? extends T>, Object? thisObj, kotlin.reflect.KProperty<?> property);
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf();
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf(T?... elements);
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf();
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
- method public static <T> androidx.compose.runtime.MutableState<T> mutableStateOf(T? value, optional androidx.compose.runtime.SnapshotMutationPolicy<T> policy);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> neverEqualPolicy();
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, Object? key3, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object![]? keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> referentialEqualityPolicy();
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> rememberUpdatedState(T? newValue);
- method public static inline operator <T> void setValue(androidx.compose.runtime.MutableState<T>, Object? thisObj, kotlin.reflect.KProperty<?> property, T? value);
- method public static <T> kotlinx.coroutines.flow.Flow<T> snapshotFlow(kotlin.jvm.functions.Function0<? extends T> block);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> structuralEqualityPolicy();
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> toMutableStateList(java.util.Collection<? extends T>);
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
- }
-
- public final class SnapshotStateKt {
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsState(kotlinx.coroutines.flow.StateFlow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
- method @androidx.compose.runtime.Composable public static <T extends R, R> androidx.compose.runtime.State<R> collectAsState(kotlinx.coroutines.flow.Flow<? extends T>, R? initial, optional kotlin.coroutines.CoroutineContext context);
- method public static <T> androidx.compose.runtime.State<T> derivedStateOf(kotlin.jvm.functions.Function0<? extends T> calculation);
- method public static <T> androidx.compose.runtime.State<T> derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy<T> policy, kotlin.jvm.functions.Function0<? extends T> calculation);
- method public static inline operator <T> T! getValue(androidx.compose.runtime.State<? extends T>, Object? thisObj, kotlin.reflect.KProperty<?> property);
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf();
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf(T?... elements);
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf();
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
- method public static <T> androidx.compose.runtime.MutableState<T> mutableStateOf(T? value, optional androidx.compose.runtime.SnapshotMutationPolicy<T> policy);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> neverEqualPolicy();
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, Object? key3, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object![]? keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> referentialEqualityPolicy();
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> rememberUpdatedState(T? newValue);
- method public static inline operator <T> void setValue(androidx.compose.runtime.MutableState<T>, Object? thisObj, kotlin.reflect.KProperty<?> property, T? value);
- method public static <T> kotlinx.coroutines.flow.Flow<T> snapshotFlow(kotlin.jvm.functions.Function0<? extends T> block);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> structuralEqualityPolicy();
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> toMutableStateList(java.util.Collection<? extends T>);
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
- }
-
- public final class SnapshotStateKt {
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsState(kotlinx.coroutines.flow.StateFlow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
- method @androidx.compose.runtime.Composable public static <T extends R, R> androidx.compose.runtime.State<R> collectAsState(kotlinx.coroutines.flow.Flow<? extends T>, R? initial, optional kotlin.coroutines.CoroutineContext context);
- method public static <T> androidx.compose.runtime.State<T> derivedStateOf(kotlin.jvm.functions.Function0<? extends T> calculation);
- method public static <T> androidx.compose.runtime.State<T> derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy<T> policy, kotlin.jvm.functions.Function0<? extends T> calculation);
- method public static inline operator <T> T! getValue(androidx.compose.runtime.State<? extends T>, Object? thisObj, kotlin.reflect.KProperty<?> property);
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf();
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf(T?... elements);
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf();
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
- method public static <T> androidx.compose.runtime.MutableState<T> mutableStateOf(T? value, optional androidx.compose.runtime.SnapshotMutationPolicy<T> policy);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> neverEqualPolicy();
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, Object? key3, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object![]? keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> referentialEqualityPolicy();
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> rememberUpdatedState(T? newValue);
- method public static inline operator <T> void setValue(androidx.compose.runtime.MutableState<T>, Object? thisObj, kotlin.reflect.KProperty<?> property, T? value);
- method public static <T> kotlinx.coroutines.flow.Flow<T> snapshotFlow(kotlin.jvm.functions.Function0<? extends T> block);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> structuralEqualityPolicy();
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> toMutableStateList(java.util.Collection<? extends T>);
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
- }
-
@androidx.compose.runtime.StableMarker @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface Stable {
}
diff --git a/compose/runtime/runtime/api/restricted_current.ignore b/compose/runtime/runtime/api/restricted_current.ignore
index e28d869..1589837 100644
--- a/compose/runtime/runtime/api/restricted_current.ignore
+++ b/compose/runtime/runtime/api/restricted_current.ignore
@@ -3,3 +3,7 @@
Added method androidx.compose.runtime.Composer.getCurrentCompositionLocalMap()
AddedAbstractMethod: androidx.compose.runtime.CompositionContext#getEffectCoroutineContext():
Added method androidx.compose.runtime.CompositionContext.getEffectCoroutineContext()
+
+
+RemovedClass: androidx.compose.runtime.SnapshotStateKt:
+ Removed class androidx.compose.runtime.SnapshotStateKt
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index 39b95b6..8af600b 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -484,110 +484,6 @@
method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
}
- public final class SnapshotStateKt {
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsState(kotlinx.coroutines.flow.StateFlow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
- method @androidx.compose.runtime.Composable public static <T extends R, R> androidx.compose.runtime.State<R> collectAsState(kotlinx.coroutines.flow.Flow<? extends T>, R? initial, optional kotlin.coroutines.CoroutineContext context);
- method public static <T> androidx.compose.runtime.State<T> derivedStateOf(kotlin.jvm.functions.Function0<? extends T> calculation);
- method public static <T> androidx.compose.runtime.State<T> derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy<T> policy, kotlin.jvm.functions.Function0<? extends T> calculation);
- method public static inline operator <T> T! getValue(androidx.compose.runtime.State<? extends T>, Object? thisObj, kotlin.reflect.KProperty<?> property);
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf();
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf(T?... elements);
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf();
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
- method public static <T> androidx.compose.runtime.MutableState<T> mutableStateOf(T? value, optional androidx.compose.runtime.SnapshotMutationPolicy<T> policy);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> neverEqualPolicy();
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, Object? key3, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object![]? keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> referentialEqualityPolicy();
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> rememberUpdatedState(T? newValue);
- method public static inline operator <T> void setValue(androidx.compose.runtime.MutableState<T>, Object? thisObj, kotlin.reflect.KProperty<?> property, T? value);
- method public static <T> kotlinx.coroutines.flow.Flow<T> snapshotFlow(kotlin.jvm.functions.Function0<? extends T> block);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> structuralEqualityPolicy();
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> toMutableStateList(java.util.Collection<? extends T>);
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
- }
-
- public final class SnapshotStateKt {
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsState(kotlinx.coroutines.flow.StateFlow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
- method @androidx.compose.runtime.Composable public static <T extends R, R> androidx.compose.runtime.State<R> collectAsState(kotlinx.coroutines.flow.Flow<? extends T>, R? initial, optional kotlin.coroutines.CoroutineContext context);
- method public static <T> androidx.compose.runtime.State<T> derivedStateOf(kotlin.jvm.functions.Function0<? extends T> calculation);
- method public static <T> androidx.compose.runtime.State<T> derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy<T> policy, kotlin.jvm.functions.Function0<? extends T> calculation);
- method public static inline operator <T> T! getValue(androidx.compose.runtime.State<? extends T>, Object? thisObj, kotlin.reflect.KProperty<?> property);
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf();
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf(T?... elements);
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf();
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
- method public static <T> androidx.compose.runtime.MutableState<T> mutableStateOf(T? value, optional androidx.compose.runtime.SnapshotMutationPolicy<T> policy);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> neverEqualPolicy();
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, Object? key3, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object![]? keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> referentialEqualityPolicy();
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> rememberUpdatedState(T? newValue);
- method public static inline operator <T> void setValue(androidx.compose.runtime.MutableState<T>, Object? thisObj, kotlin.reflect.KProperty<?> property, T? value);
- method public static <T> kotlinx.coroutines.flow.Flow<T> snapshotFlow(kotlin.jvm.functions.Function0<? extends T> block);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> structuralEqualityPolicy();
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> toMutableStateList(java.util.Collection<? extends T>);
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
- }
-
- public final class SnapshotStateKt {
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsState(kotlinx.coroutines.flow.StateFlow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
- method @androidx.compose.runtime.Composable public static <T extends R, R> androidx.compose.runtime.State<R> collectAsState(kotlinx.coroutines.flow.Flow<? extends T>, R? initial, optional kotlin.coroutines.CoroutineContext context);
- method public static <T> androidx.compose.runtime.State<T> derivedStateOf(kotlin.jvm.functions.Function0<? extends T> calculation);
- method public static <T> androidx.compose.runtime.State<T> derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy<T> policy, kotlin.jvm.functions.Function0<? extends T> calculation);
- method public static inline operator <T> T! getValue(androidx.compose.runtime.State<? extends T>, Object? thisObj, kotlin.reflect.KProperty<?> property);
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf();
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf(T?... elements);
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf();
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
- method public static <T> androidx.compose.runtime.MutableState<T> mutableStateOf(T? value, optional androidx.compose.runtime.SnapshotMutationPolicy<T> policy);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> neverEqualPolicy();
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, Object? key3, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object![]? keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> referentialEqualityPolicy();
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> rememberUpdatedState(T? newValue);
- method public static inline operator <T> void setValue(androidx.compose.runtime.MutableState<T>, Object? thisObj, kotlin.reflect.KProperty<?> property, T? value);
- method public static <T> kotlinx.coroutines.flow.Flow<T> snapshotFlow(kotlin.jvm.functions.Function0<? extends T> block);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> structuralEqualityPolicy();
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> toMutableStateList(java.util.Collection<? extends T>);
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
- }
-
- public final class SnapshotStateKt {
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> collectAsState(kotlinx.coroutines.flow.StateFlow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
- method @androidx.compose.runtime.Composable public static <T extends R, R> androidx.compose.runtime.State<R> collectAsState(kotlinx.coroutines.flow.Flow<? extends T>, R? initial, optional kotlin.coroutines.CoroutineContext context);
- method public static <T> androidx.compose.runtime.State<T> derivedStateOf(kotlin.jvm.functions.Function0<? extends T> calculation);
- method public static <T> androidx.compose.runtime.State<T> derivedStateOf(androidx.compose.runtime.SnapshotMutationPolicy<T> policy, kotlin.jvm.functions.Function0<? extends T> calculation);
- method public static inline operator <T> T! getValue(androidx.compose.runtime.State<? extends T>, Object? thisObj, kotlin.reflect.KProperty<?> property);
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf();
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> mutableStateListOf(T?... elements);
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf();
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> mutableStateMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
- method public static <T> androidx.compose.runtime.MutableState<T> mutableStateOf(T? value, optional androidx.compose.runtime.SnapshotMutationPolicy<T> policy);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> neverEqualPolicy();
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? key1, Object? key2, Object? key3, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object![]? keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> referentialEqualityPolicy();
- method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> rememberUpdatedState(T? newValue);
- method public static inline operator <T> void setValue(androidx.compose.runtime.MutableState<T>, Object? thisObj, kotlin.reflect.KProperty<?> property, T? value);
- method public static <T> kotlinx.coroutines.flow.Flow<T> snapshotFlow(kotlin.jvm.functions.Function0<? extends T> block);
- method public static <T> androidx.compose.runtime.SnapshotMutationPolicy<T> structuralEqualityPolicy();
- method public static <T> androidx.compose.runtime.snapshots.SnapshotStateList<T> toMutableStateList(java.util.Collection<? extends T>);
- method public static <K, V> androidx.compose.runtime.snapshots.SnapshotStateMap<K,V> toMutableStateMap(Iterable<? extends kotlin.Pair<? extends K,? extends V>>);
- }
-
@androidx.compose.runtime.StableMarker @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface Stable {
}
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml b/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml
index 60f3519..149bfbb 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml
@@ -151,6 +151,19 @@
</receiver>
<receiver
+ android:name="androidx.glance.appwidget.demos.RippleAppWidgetReceiver"
+ android:label="@string/ripple_widget_name"
+ android:enabled="@bool/glance_appwidget_available"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+ </intent-filter>
+ <meta-data
+ android:name="android.appwidget.provider"
+ android:resource="@xml/default_app_widget_info" />
+ </receiver>
+
+ <receiver
android:name="androidx.glance.appwidget.demos.VerticalGridAppWidgetReceiver"
android:label="@string/grid_widget_name"
android:enabled="@bool/glance_appwidget_available"
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/RippleAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/RippleAppWidget.kt
new file mode 100644
index 0000000..8202282
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/RippleAppWidget.kt
@@ -0,0 +1,189 @@
+/*
+ * 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.glance.appwidget.demos
+
+import android.content.Context
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.glance.GlanceId
+import androidx.glance.GlanceModifier
+import androidx.glance.Image
+import androidx.glance.ImageProvider
+import androidx.glance.action.clickable
+import androidx.glance.appwidget.GlanceAppWidget
+import androidx.glance.appwidget.GlanceAppWidgetReceiver
+import androidx.glance.appwidget.cornerRadius
+import androidx.glance.appwidget.provideContent
+import androidx.glance.background
+import androidx.glance.color.ColorProvider
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.Box
+import androidx.glance.layout.Column
+import androidx.glance.layout.ContentScale
+import androidx.glance.layout.Spacer
+import androidx.glance.layout.fillMaxSize
+import androidx.glance.layout.fillMaxWidth
+import androidx.glance.layout.height
+import androidx.glance.layout.padding
+import androidx.glance.layout.size
+import androidx.glance.text.FontWeight
+import androidx.glance.text.Text
+import androidx.glance.text.TextAlign
+import androidx.glance.text.TextStyle
+
+/**
+ * Sample AppWidget to showcase the ripples.
+ * Note: Rounded corners are supported in S+
+ */
+class RippleAppWidget : GlanceAppWidget() {
+ private val columnBgColorsA = listOf(Color(0xffA2BDF2), Color(0xff5087EF))
+ private val columnBgColorsB = listOf(Color(0xFFBD789C), Color(0xFF880E4F))
+ private val boxColors = listOf(Color(0xffF7A998), Color(0xffFA5F3D))
+
+ override suspend fun provideGlance(
+ context: Context,
+ id: GlanceId
+ ) = provideContent {
+ RippleDemoContent()
+ }
+
+ @Composable
+ private fun RippleDemoContent() {
+ var count by remember { mutableStateOf(0) }
+ var type by remember { mutableStateOf(ContentScale.Fit) }
+ var columnBgColors by remember { mutableStateOf(columnBgColorsA) }
+
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = GlanceModifier.fillMaxSize().padding(8.dp)
+ .cornerRadius(20.dp)
+ .background(ColorProvider(day = columnBgColors[0], night = columnBgColors[1]))
+ .clickable {
+ columnBgColors = when (columnBgColors[0]) {
+ columnBgColorsA[0] -> columnBgColorsB
+ else -> columnBgColorsA
+ }
+ }
+ ) {
+ Text(
+ text = "Content Scale: ${type.asString()}, Image / Box click count: $count",
+ modifier = GlanceModifier.padding(5.dp)
+ )
+ // A drawable image with rounded corners and a click modifier.
+ OutlinedButtonUsingImage(text = "Toggle content scale", onClick = {
+ type = when (type) {
+ ContentScale.Crop -> ContentScale.FillBounds
+ ContentScale.FillBounds -> ContentScale.Fit
+ else -> ContentScale.Crop
+ }
+ })
+ Spacer(GlanceModifier.size(5.dp))
+ Text(
+ text = "Image in a clickable box with rounded corners",
+ modifier = GlanceModifier.padding(5.dp)
+ )
+ ImageInClickableBoxWithRoundedCorners(contentScale = type, onClick = { count++ })
+ Spacer(GlanceModifier.size(5.dp))
+ Text(
+ text = "Rounded corner image in a clickable box",
+ modifier = GlanceModifier.padding(5.dp)
+ )
+ RoundedImageInClickableBox(contentScale = type, onClick = { count++ })
+ }
+ }
+ @Composable
+ private fun ImageInClickableBoxWithRoundedCorners(
+ contentScale: ContentScale,
+ onClick: () -> Unit
+ ) {
+ Box(
+ modifier = GlanceModifier
+ .height(100.dp)
+ .background(ColorProvider(day = boxColors[0], night = boxColors[1]))
+ .cornerRadius(25.dp)
+ .clickable(onClick)
+ ) {
+ Image(
+ provider = ImageProvider(R.drawable.compose),
+ contentDescription = "Image sample in a box with rounded corners",
+ contentScale = contentScale,
+ modifier = GlanceModifier.fillMaxSize()
+ )
+ }
+ }
+
+ @Composable
+ private fun RoundedImageInClickableBox(contentScale: ContentScale, onClick: () -> Unit) {
+ Box(
+ modifier = GlanceModifier
+ .height(100.dp)
+ .background(ColorProvider(day = boxColors[0], night = boxColors[1]))
+ .clickable(onClick)
+ ) {
+ Image(
+ provider = ImageProvider(R.drawable.compose),
+ contentDescription = "Image sample with rounded corners",
+ contentScale = contentScale,
+ modifier = GlanceModifier.fillMaxSize().cornerRadius(25.dp)
+ )
+ }
+ }
+
+ @Composable
+ fun OutlinedButtonUsingImage(
+ text: String,
+ onClick: () -> Unit,
+ ) {
+ Box(
+ modifier = GlanceModifier.height(40.dp).fillMaxWidth(),
+ contentAlignment = Alignment.Center
+ ) {
+ // Demonstrates a button with rounded outline using a clickable image. Alternatively,
+ // such button can also be created using Box + Text by adding background image, corner
+ // radius and click modifiers to the box.
+ Image(
+ provider = ImageProvider(R.drawable.ic_outlined_button),
+ contentDescription = "Outlined button sample",
+ // Radius value matched with the border in the outline image so that the ripple
+ // matches it (in versions that support cornerRadius modifier).
+ modifier = GlanceModifier.fillMaxSize().cornerRadius(20.dp).clickable(onClick)
+ )
+ Text(
+ text = text,
+ style = TextStyle(fontWeight = FontWeight.Medium, textAlign = TextAlign.Center),
+ modifier = GlanceModifier.background(Color.Transparent)
+ )
+ }
+ }
+
+ private fun ContentScale.asString(): String =
+ when (this) {
+ ContentScale.Fit -> "Fit"
+ ContentScale.FillBounds -> "Fill Bounds"
+ ContentScale.Crop -> "Crop"
+ else -> "Unknown content scale"
+ }
+}
+
+class RippleAppWidgetReceiver : GlanceAppWidgetReceiver() {
+ override val glanceAppWidget: GlanceAppWidget = RippleAppWidget()
+}
\ No newline at end of file
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/ic_outlined_button.xml b/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/ic_outlined_button.xml
new file mode 100644
index 0000000..ee58ce0
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/ic_outlined_button.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@android:color/transparent" />
+ <corners android:radius="20dp"/>
+ <stroke android:width="1dp" android:color="#000000" />
+ <padding android:left="1dp" android:top="1dp" android:right="1dp"
+ android:bottom="1dp" />
+</shape>
\ No newline at end of file
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/res/values/strings.xml b/glance/glance-appwidget/integration-tests/demos/src/main/res/values/strings.xml
index 3f8a5df..1bf0d9a 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/res/values/strings.xml
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/res/values/strings.xml
@@ -29,6 +29,7 @@
<string name="error_widget_name">Error UI Widget</string>
<string name="scrollable_widget_name">Scrollable Widget</string>
<string name="image_widget_name">Image Widget</string>
+ <string name="ripple_widget_name">Ripple Widget</string>
<string name="grid_widget_name">Vertical Grid Widget</string>
<string name="default_state_widget_name">Default State Widget</string>
<string name="progress_indicator_widget_name">ProgressBar Widget</string>
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
index 8f20d20..818ac07 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
@@ -32,6 +32,7 @@
import android.widget.CompoundButton
import android.widget.FrameLayout
import android.widget.ImageView
+import android.widget.ImageView.ScaleType
import android.widget.LinearLayout
import android.widget.RadioButton
import android.widget.TextView
@@ -488,8 +489,14 @@
mHostRule.startHost()
- mHostRule.onUnboxedHostView<TextView> { textView ->
- assertThat(textView.background).isNotNull()
+ mHostRule.onUnboxedHostView<FrameLayout> { box ->
+ assertThat(box.notGoneChildCount).isEqualTo(2)
+ val (boxedImage, boxedText) = box.notGoneChildren.toList()
+ val image = boxedImage.getTargetView<ImageView>()
+ val text = boxedText.getTargetView<TextView>()
+ assertThat(image.drawable).isNotNull()
+ assertThat(image.scaleType).isEqualTo(ScaleType.FIT_XY)
+ assertThat(text.background).isNull()
}
}
@@ -511,6 +518,30 @@
val image = boxedImage.getTargetView<ImageView>()
val text = boxedText.getTargetView<TextView>()
assertThat(image.drawable).isNotNull()
+ assertThat(image.scaleType).isEqualTo(ScaleType.FIT_CENTER)
+ assertThat(text.background).isNull()
+ }
+ }
+
+ @Test
+ fun drawableCropBackground() {
+ TestGlanceAppWidget.uiDefinition = {
+ Text(
+ "Some useful text",
+ modifier = GlanceModifier.fillMaxWidth().height(220.dp)
+ .background(ImageProvider(R.drawable.oval), contentScale = ContentScale.Crop)
+ )
+ }
+
+ mHostRule.startHost()
+
+ mHostRule.onUnboxedHostView<FrameLayout> { box ->
+ assertThat(box.notGoneChildCount).isEqualTo(2)
+ val (boxedImage, boxedText) = box.notGoneChildren.toList()
+ val image = boxedImage.getTargetView<ImageView>()
+ val text = boxedText.getTargetView<TextView>()
+ assertThat(image.drawable).isNotNull()
+ assertThat(image.scaleType).isEqualTo(ScaleType.CENTER_CROP)
assertThat(text.background).isNull()
}
}
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/NormalizeCompositionTree.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/NormalizeCompositionTree.kt
index 5336388..40a865e 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/NormalizeCompositionTree.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/NormalizeCompositionTree.kt
@@ -17,7 +17,6 @@
import android.util.Log
import androidx.compose.ui.unit.dp
-import androidx.glance.AndroidResourceImageProvider
import androidx.glance.BackgroundModifier
import androidx.glance.Emittable
import androidx.glance.EmittableButton
@@ -33,7 +32,6 @@
import androidx.glance.extractModifier
import androidx.glance.findModifier
import androidx.glance.layout.Alignment
-import androidx.glance.layout.ContentScale
import androidx.glance.layout.EmittableBox
import androidx.glance.layout.HeightModifier
import androidx.glance.layout.WidthModifier
@@ -189,36 +187,63 @@
if (this is EmittableLazyListItem || this is EmittableSizeBox) return this
var target = this
-
- // We only need to add a background image view if the background is a Bitmap, or a
- // drawable resource with non-default content scale. Otherwise, we can set the background
- // directly on the target element in ApplyModifiers.kt.
- val (bgModifier, notBgModifier) = target.modifier.extractModifier<BackgroundModifier>()
- val addBackground = bgModifier?.imageProvider != null &&
- (bgModifier.imageProvider !is AndroidResourceImageProvider ||
- bgModifier.contentScale != ContentScale.FillBounds)
-
- // Add a ripple for every element with an action that does not have already have a built in
- // ripple.
- notBgModifier.warnIfMultipleClickableActions()
- val (actionModifier, notBgOrActionModifier) = notBgModifier.extractModifier<ActionModifier>()
- val addRipple = actionModifier != null && !hasBuiltinRipple()
val isButton = target is EmittableButton
- if (!addBackground && !addRipple && !isButton) return target
- // Hoist the size and action modifiers to the wrapping Box, then set the target element to fill
- // the given space. doNotUnsetAction() prevents the views within the Box from being made
- // clickable.
- val (sizeModifiers, nonSizeModifiers) = notBgOrActionModifier.extractSizeModifiers()
- val boxModifiers = mutableListOf<GlanceModifier?>(sizeModifiers, actionModifier)
- val targetModifiers = mutableListOf<GlanceModifier?>(
- nonSizeModifiers.fillMaxSize()
- )
-
- // If we don't need to emulate the background, add the background modifier back to the target.
- if (!addBackground) {
- targetModifiers += bgModifier
+ val shouldWrapTargetInABox = target.modifier.any {
+ // Background images (i.e. BitMap or drawable resources) are emulated by placing the image
+ // before the target in the wrapper box. This allows us to support content scale as well as
+ // additional image features (e.g. color filters) on background images.
+ (it is BackgroundModifier && it.imageProvider != null) ||
+ // Ripples are implemented by placing a drawable after the target in the wrapper box.
+ (it is ActionModifier && !hasBuiltinRipple()) ||
+ // Buttons are implemented using a background drawable with rounded corners and an
+ // EmittableText.
+ isButton
}
+ if (!shouldWrapTargetInABox) return target
+
+ // Hoisted modifiers are subtracted from the target one by one and added to the box and the
+ // remaining modifiers are applied to the target.
+ val boxModifiers = mutableListOf<GlanceModifier?>()
+ val targetModifiers = mutableListOf<GlanceModifier?>()
+ var backgroundImage: EmittableImage? = null
+ var rippleImage: EmittableImage? = null
+
+ // bgModifier.imageProvider is converted to an actual image but bgModifier.colorProvider is
+ // applied back to the target. Note: We could have hoisted the bg color to box instead of
+ // adding it back to the target, but for buttons, we also add an outline background to the box.
+ val (bgModifier, targetModifiersMinusBg) = target.modifier.extractModifier<BackgroundModifier>()
+ if (bgModifier != null) {
+ if (bgModifier.imageProvider != null) {
+ backgroundImage = EmittableImage().apply {
+ modifier = GlanceModifier.fillMaxSize()
+ provider = bgModifier.imageProvider
+ contentScale = bgModifier.contentScale
+ }
+ } else { // is a background color modifier
+ targetModifiers += bgModifier
+ }
+ }
+
+ // Action modifiers are hoisted on the wrapping box and a ripple image is added to the
+ // foreground if the target doesn't have it built-in.
+ targetModifiersMinusBg.warnIfMultipleClickableActions()
+ val (actionModifier, targetModifiersMinusAction) =
+ targetModifiersMinusBg.extractModifier<ActionModifier>()
+ boxModifiers += actionModifier
+ if (actionModifier != null && !hasBuiltinRipple()) {
+ rippleImage = EmittableImage().apply {
+ modifier = GlanceModifier.fillMaxSize()
+ provider = ImageProvider(R.drawable.glance_ripple)
+ }
+ }
+
+ // Hoist the size and corner radius modifiers to the wrapping Box, then set the target element
+ // to fill the given space.
+ val (sizeAndCornerModifiers, targetModifiersMinusSizeAndCornerRadius) =
+ targetModifiersMinusAction.extractSizeAndCornerRadiusModifiers()
+ boxModifiers += sizeAndCornerModifiers
+ targetModifiers += targetModifiersMinusSizeAndCornerRadius.fillMaxSize()
// If this is a button, set the necessary modifiers on the wrapping Box.
if (target is EmittableButton) {
@@ -234,20 +259,9 @@
modifier = boxModifiers.collect()
if (isButton) contentAlignment = Alignment.Center
- if (addBackground && bgModifier != null) {
- children += EmittableImage().apply {
- modifier = GlanceModifier.fillMaxSize()
- provider = bgModifier.imageProvider
- contentScale = bgModifier.contentScale
- }
- }
+ backgroundImage?.let { children += it }
children += target.apply { modifier = targetModifiers.collect() }
- if (addRipple) {
- children += EmittableImage().apply {
- modifier = GlanceModifier.fillMaxSize()
- provider = ImageProvider(R.drawable.glance_ripple)
- }
- }
+ rippleImage?.let { children += it }
}
}
@@ -262,13 +276,15 @@
)
/**
- * Split the [GlanceModifier] into one that contains the [WidthModifier]s and [HeightModifier]s and
- * one that contains the rest.
+ * Split the [GlanceModifier] into one that contains the [WidthModifier]s, [HeightModifier]s and
+ * and [CornerRadiusModifier]s and one that contains the rest.
*/
-private fun GlanceModifier.extractSizeModifiers() =
- if (any { it is WidthModifier || it is HeightModifier }) {
+private fun GlanceModifier.extractSizeAndCornerRadiusModifiers() =
+ if (any { it is WidthModifier || it is HeightModifier || it is CornerRadiusModifier }) {
foldIn(ExtractedSizeModifiers()) { acc, modifier ->
- if (modifier is WidthModifier || modifier is HeightModifier) {
+ if (modifier is WidthModifier ||
+ modifier is HeightModifier ||
+ modifier is CornerRadiusModifier) {
acc.copy(sizeModifiers = acc.sizeModifiers.then(modifier))
} else {
acc.copy(nonSizeModifiers = acc.nonSizeModifiers.then(modifier))
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 2383b5d9..31189e2 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -98,7 +98,7 @@
checkerframework = { module = "org.checkerframework:checker-qual", version = "2.5.3" }
checkmark = { module = "net.saff.checkmark:checkmark", version = "0.1.6" }
constraintLayout = { module = "androidx.constraintlayout:constraintlayout", version = "2.0.1"}
-dackka = { module = "com.google.devsite:dackka", version = "1.3.0" }
+dackka = { module = "com.google.devsite:dackka", version = "1.3.1" }
dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" }
daggerCompiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" }
dexmakerMockito = { module = "com.linkedin.dexmaker:dexmaker-mockito", version.ref = "dexmaker" }
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/SingleBufferedCanvasRendererV29Test.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/SingleBufferedCanvasRendererV29Test.kt
index 736b93e..5ecc43e 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/SingleBufferedCanvasRendererV29Test.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/SingleBufferedCanvasRendererV29Test.kt
@@ -41,6 +41,7 @@
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
@@ -352,6 +353,7 @@
}
}
+ @Ignore("b/274099885")
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
@Test
fun testBatchedRenders() {
diff --git a/paging/paging-rxjava2/api/current.ignore b/paging/paging-rxjava2/api/current.ignore
index 4c496cc..0e1e678 100644
--- a/paging/paging-rxjava2/api/current.ignore
+++ b/paging/paging-rxjava2/api/current.ignore
@@ -1,3 +1,7 @@
// Baseline format: 1.0
ParameterNameChange: androidx.paging.rxjava2.RxPagingSource#load(androidx.paging.PagingSource.LoadParams<Key>, kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>>) parameter #1:
Attempted to remove parameter name from parameter arg2 in androidx.paging.rxjava2.RxPagingSource.load
+
+
+RemovedClass: androidx.paging.rxjava2.PagingRx:
+ Removed class androidx.paging.rxjava2.PagingRx
diff --git a/paging/paging-rxjava2/api/current.txt b/paging/paging-rxjava2/api/current.txt
index 0b95aeb..9a9398b 100644
--- a/paging/paging-rxjava2/api/current.txt
+++ b/paging/paging-rxjava2/api/current.txt
@@ -38,15 +38,6 @@
method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.Single<R>> transform);
}
- public final class PagingRx {
- method @CheckResult public static <T> androidx.paging.PagingData<T> filter(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.Single<java.lang.Boolean>> predicate);
- method @CheckResult public static <T, R> androidx.paging.PagingData<R> flatMap(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.Single<java.lang.Iterable<R>>> transform);
- method public static <Key, Value> io.reactivex.Flowable<androidx.paging.PagingData<Value>> getFlowable(androidx.paging.Pager<Key,Value>);
- method public static <Key, Value> io.reactivex.Observable<androidx.paging.PagingData<Value>> getObservable(androidx.paging.Pager<Key,Value>);
- method @CheckResult public static <T extends R, R> androidx.paging.PagingData<R> insertSeparators(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function2<? super T,? super T,? extends io.reactivex.Maybe<R>> generator);
- method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.Single<R>> transform);
- }
-
public abstract class RxPagingSource<Key, Value> extends androidx.paging.PagingSource<Key,Value> {
ctor public RxPagingSource();
method public final suspend Object? load(androidx.paging.PagingSource.LoadParams<Key> params, kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>>);
diff --git a/paging/paging-rxjava2/api/public_plus_experimental_current.txt b/paging/paging-rxjava2/api/public_plus_experimental_current.txt
index 7179ce2..acadd7c 100644
--- a/paging/paging-rxjava2/api/public_plus_experimental_current.txt
+++ b/paging/paging-rxjava2/api/public_plus_experimental_current.txt
@@ -40,17 +40,6 @@
method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.Single<R>> transform);
}
- public final class PagingRx {
- method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.Observable<androidx.paging.PagingData<T>> cachedIn(io.reactivex.Observable<androidx.paging.PagingData<T>>, kotlinx.coroutines.CoroutineScope scope);
- method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.Flowable<androidx.paging.PagingData<T>> cachedIn(io.reactivex.Flowable<androidx.paging.PagingData<T>>, kotlinx.coroutines.CoroutineScope scope);
- method @CheckResult public static <T> androidx.paging.PagingData<T> filter(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.Single<java.lang.Boolean>> predicate);
- method @CheckResult public static <T, R> androidx.paging.PagingData<R> flatMap(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.Single<java.lang.Iterable<R>>> transform);
- method public static <Key, Value> io.reactivex.Flowable<androidx.paging.PagingData<Value>> getFlowable(androidx.paging.Pager<Key,Value>);
- method public static <Key, Value> io.reactivex.Observable<androidx.paging.PagingData<Value>> getObservable(androidx.paging.Pager<Key,Value>);
- method @CheckResult public static <T extends R, R> androidx.paging.PagingData<R> insertSeparators(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function2<? super T,? super T,? extends io.reactivex.Maybe<R>> generator);
- method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.Single<R>> transform);
- }
-
public abstract class RxPagingSource<Key, Value> extends androidx.paging.PagingSource<Key,Value> {
ctor public RxPagingSource();
method public final suspend Object? load(androidx.paging.PagingSource.LoadParams<Key> params, kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>>);
diff --git a/paging/paging-rxjava2/api/restricted_current.ignore b/paging/paging-rxjava2/api/restricted_current.ignore
index 4c496cc..0e1e678 100644
--- a/paging/paging-rxjava2/api/restricted_current.ignore
+++ b/paging/paging-rxjava2/api/restricted_current.ignore
@@ -1,3 +1,7 @@
// Baseline format: 1.0
ParameterNameChange: androidx.paging.rxjava2.RxPagingSource#load(androidx.paging.PagingSource.LoadParams<Key>, kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>>) parameter #1:
Attempted to remove parameter name from parameter arg2 in androidx.paging.rxjava2.RxPagingSource.load
+
+
+RemovedClass: androidx.paging.rxjava2.PagingRx:
+ Removed class androidx.paging.rxjava2.PagingRx
diff --git a/paging/paging-rxjava2/api/restricted_current.txt b/paging/paging-rxjava2/api/restricted_current.txt
index 0b95aeb..9a9398b 100644
--- a/paging/paging-rxjava2/api/restricted_current.txt
+++ b/paging/paging-rxjava2/api/restricted_current.txt
@@ -38,15 +38,6 @@
method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.Single<R>> transform);
}
- public final class PagingRx {
- method @CheckResult public static <T> androidx.paging.PagingData<T> filter(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.Single<java.lang.Boolean>> predicate);
- method @CheckResult public static <T, R> androidx.paging.PagingData<R> flatMap(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.Single<java.lang.Iterable<R>>> transform);
- method public static <Key, Value> io.reactivex.Flowable<androidx.paging.PagingData<Value>> getFlowable(androidx.paging.Pager<Key,Value>);
- method public static <Key, Value> io.reactivex.Observable<androidx.paging.PagingData<Value>> getObservable(androidx.paging.Pager<Key,Value>);
- method @CheckResult public static <T extends R, R> androidx.paging.PagingData<R> insertSeparators(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function2<? super T,? super T,? extends io.reactivex.Maybe<R>> generator);
- method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.Single<R>> transform);
- }
-
public abstract class RxPagingSource<Key, Value> extends androidx.paging.PagingSource<Key,Value> {
ctor public RxPagingSource();
method public final suspend Object? load(androidx.paging.PagingSource.LoadParams<Key> params, kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>>);
diff --git a/paging/paging-rxjava3/api/current.ignore b/paging/paging-rxjava3/api/current.ignore
index 6159e34..451c35d 100644
--- a/paging/paging-rxjava3/api/current.ignore
+++ b/paging/paging-rxjava3/api/current.ignore
@@ -1,3 +1,7 @@
// Baseline format: 1.0
ParameterNameChange: androidx.paging.rxjava3.RxPagingSource#load(androidx.paging.PagingSource.LoadParams<Key>, kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>>) parameter #1:
Attempted to remove parameter name from parameter arg2 in androidx.paging.rxjava3.RxPagingSource.load
+
+
+RemovedClass: androidx.paging.rxjava3.PagingRx:
+ Removed class androidx.paging.rxjava3.PagingRx
diff --git a/paging/paging-rxjava3/api/current.txt b/paging/paging-rxjava3/api/current.txt
index 7af17b8..b77cbbe 100644
--- a/paging/paging-rxjava3/api/current.txt
+++ b/paging/paging-rxjava3/api/current.txt
@@ -10,15 +10,6 @@
method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.rxjava3.core.Single<R>> transform);
}
- public final class PagingRx {
- method @CheckResult public static <T> androidx.paging.PagingData<T> filter(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.rxjava3.core.Single<java.lang.Boolean>> predicate);
- method @CheckResult public static <T, R> androidx.paging.PagingData<R> flatMap(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.rxjava3.core.Single<java.lang.Iterable<R>>> transform);
- method public static <Key, Value> io.reactivex.rxjava3.core.Flowable<androidx.paging.PagingData<Value>> getFlowable(androidx.paging.Pager<Key,Value>);
- method public static <Key, Value> io.reactivex.rxjava3.core.Observable<androidx.paging.PagingData<Value>> getObservable(androidx.paging.Pager<Key,Value>);
- method @CheckResult public static <T extends R, R> androidx.paging.PagingData<R> insertSeparators(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function2<? super T,? super T,? extends io.reactivex.rxjava3.core.Maybe<R>> generator);
- method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.rxjava3.core.Single<R>> transform);
- }
-
@Deprecated public final class RxPagedListBuilder<Key, Value> {
ctor @Deprecated public RxPagedListBuilder(kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory, androidx.paging.PagedList.Config config);
ctor @Deprecated public RxPagedListBuilder(kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory, int pageSize);
diff --git a/paging/paging-rxjava3/api/public_plus_experimental_current.txt b/paging/paging-rxjava3/api/public_plus_experimental_current.txt
index 6ed617d..68770f0 100644
--- a/paging/paging-rxjava3/api/public_plus_experimental_current.txt
+++ b/paging/paging-rxjava3/api/public_plus_experimental_current.txt
@@ -12,17 +12,6 @@
method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.rxjava3.core.Single<R>> transform);
}
- public final class PagingRx {
- method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.rxjava3.core.Observable<androidx.paging.PagingData<T>> cachedIn(io.reactivex.rxjava3.core.Observable<androidx.paging.PagingData<T>>, kotlinx.coroutines.CoroutineScope scope);
- method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.rxjava3.core.Flowable<androidx.paging.PagingData<T>> cachedIn(io.reactivex.rxjava3.core.Flowable<androidx.paging.PagingData<T>>, kotlinx.coroutines.CoroutineScope scope);
- method @CheckResult public static <T> androidx.paging.PagingData<T> filter(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.rxjava3.core.Single<java.lang.Boolean>> predicate);
- method @CheckResult public static <T, R> androidx.paging.PagingData<R> flatMap(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.rxjava3.core.Single<java.lang.Iterable<R>>> transform);
- method public static <Key, Value> io.reactivex.rxjava3.core.Flowable<androidx.paging.PagingData<Value>> getFlowable(androidx.paging.Pager<Key,Value>);
- method public static <Key, Value> io.reactivex.rxjava3.core.Observable<androidx.paging.PagingData<Value>> getObservable(androidx.paging.Pager<Key,Value>);
- method @CheckResult public static <T extends R, R> androidx.paging.PagingData<R> insertSeparators(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function2<? super T,? super T,? extends io.reactivex.rxjava3.core.Maybe<R>> generator);
- method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.rxjava3.core.Single<R>> transform);
- }
-
@Deprecated public final class RxPagedListBuilder<Key, Value> {
ctor @Deprecated public RxPagedListBuilder(kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory, androidx.paging.PagedList.Config config);
ctor @Deprecated public RxPagedListBuilder(kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory, int pageSize);
diff --git a/paging/paging-rxjava3/api/restricted_current.ignore b/paging/paging-rxjava3/api/restricted_current.ignore
index 6159e34..451c35d 100644
--- a/paging/paging-rxjava3/api/restricted_current.ignore
+++ b/paging/paging-rxjava3/api/restricted_current.ignore
@@ -1,3 +1,7 @@
// Baseline format: 1.0
ParameterNameChange: androidx.paging.rxjava3.RxPagingSource#load(androidx.paging.PagingSource.LoadParams<Key>, kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>>) parameter #1:
Attempted to remove parameter name from parameter arg2 in androidx.paging.rxjava3.RxPagingSource.load
+
+
+RemovedClass: androidx.paging.rxjava3.PagingRx:
+ Removed class androidx.paging.rxjava3.PagingRx
diff --git a/paging/paging-rxjava3/api/restricted_current.txt b/paging/paging-rxjava3/api/restricted_current.txt
index 7af17b8..b77cbbe 100644
--- a/paging/paging-rxjava3/api/restricted_current.txt
+++ b/paging/paging-rxjava3/api/restricted_current.txt
@@ -10,15 +10,6 @@
method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.rxjava3.core.Single<R>> transform);
}
- public final class PagingRx {
- method @CheckResult public static <T> androidx.paging.PagingData<T> filter(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.rxjava3.core.Single<java.lang.Boolean>> predicate);
- method @CheckResult public static <T, R> androidx.paging.PagingData<R> flatMap(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.rxjava3.core.Single<java.lang.Iterable<R>>> transform);
- method public static <Key, Value> io.reactivex.rxjava3.core.Flowable<androidx.paging.PagingData<Value>> getFlowable(androidx.paging.Pager<Key,Value>);
- method public static <Key, Value> io.reactivex.rxjava3.core.Observable<androidx.paging.PagingData<Value>> getObservable(androidx.paging.Pager<Key,Value>);
- method @CheckResult public static <T extends R, R> androidx.paging.PagingData<R> insertSeparators(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function2<? super T,? super T,? extends io.reactivex.rxjava3.core.Maybe<R>> generator);
- method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function1<? super T,? extends io.reactivex.rxjava3.core.Single<R>> transform);
- }
-
@Deprecated public final class RxPagedListBuilder<Key, Value> {
ctor @Deprecated public RxPagedListBuilder(kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory, androidx.paging.PagedList.Config config);
ctor @Deprecated public RxPagedListBuilder(kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory, int pageSize);
diff --git a/room/room-ktx/build.gradle b/room/room-ktx/build.gradle
index 466e365..5e4a868 100644
--- a/room/room-ktx/build.gradle
+++ b/room/room-ktx/build.gradle
@@ -30,12 +30,12 @@
api(libs.kotlinCoroutinesAndroid)
testImplementation(libs.junit)
testImplementation(libs.mockitoCore4)
- testImplementation(libs.truth)
+ testImplementation(project(":internal-testutils-kmp"))
testImplementation("androidx.lifecycle:lifecycle-livedata-core:2.0.0")
testImplementation(libs.testRunner)
testImplementation(libs.kotlinCoroutinesTest)
- androidTestImplementation(libs.truth)
+ androidTestImplementation(project(":internal-testutils-kmp"))
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.kotlinCoroutinesTest)
}
diff --git a/room/room-ktx/src/androidTest/java/androidx/room/CoroutineRoomCancellationTest.kt b/room/room-ktx/src/androidTest/java/androidx/room/CoroutineRoomCancellationTest.kt
index aaa8ba9..97dcc0d 100644
--- a/room/room-ktx/src/androidTest/java/androidx/room/CoroutineRoomCancellationTest.kt
+++ b/room/room-ktx/src/androidTest/java/androidx/room/CoroutineRoomCancellationTest.kt
@@ -18,10 +18,10 @@
import android.database.sqlite.SQLiteException
import android.os.CancellationSignal
+import androidx.kruth.assertThat
import androidx.sqlite.db.SupportSQLiteOpenHelper
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -32,7 +32,6 @@
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
-import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Test
import java.util.concurrent.Callable
@@ -135,7 +134,7 @@
}
)
} catch (exception: Throwable) {
- assertTrue(exception is SQLiteException)
+ assertThat(exception).isInstanceOf<SQLiteException>()
}
}
diff --git a/room/room-ktx/src/test/java/androidx/room/CoroutinesRoomTest.kt b/room/room-ktx/src/test/java/androidx/room/CoroutinesRoomTest.kt
index 46e43ec..08dc8c5 100644
--- a/room/room-ktx/src/test/java/androidx/room/CoroutinesRoomTest.kt
+++ b/room/room-ktx/src/test/java/androidx/room/CoroutinesRoomTest.kt
@@ -16,8 +16,8 @@
package androidx.room
+import androidx.kruth.assertThat
import androidx.sqlite.db.SupportSQLiteOpenHelper
-import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.first
@@ -56,11 +56,11 @@
}
yield(); yield() // yield for async and flow
- assertThat(invalidationTracker.observers.size).isEqualTo(1)
+ assertThat(invalidationTracker.observers).hasSize(1)
assertThat(callableExecuted).isTrue()
assertThat(job.await()).isEqualTo(expectedResult)
- assertThat(invalidationTracker.observers.isEmpty()).isTrue()
+ assertThat(invalidationTracker.observers).isEmpty()
}
// Use runBlocking dispatcher as query dispatchers, keeps the tests consistent.
diff --git a/room/room-ktx/src/test/java/androidx/room/MigrationTest.kt b/room/room-ktx/src/test/java/androidx/room/MigrationTest.kt
index 162f50b..f49e8ce 100644
--- a/room/room-ktx/src/test/java/androidx/room/MigrationTest.kt
+++ b/room/room-ktx/src/test/java/androidx/room/MigrationTest.kt
@@ -21,11 +21,11 @@
import android.database.sqlite.SQLiteTransactionListener
import android.os.CancellationSignal
import android.util.Pair
+import androidx.kruth.assertThat
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteQuery
import androidx.sqlite.db.SupportSQLiteStatement
-import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
diff --git a/slidingpanelayout/slidingpanelayout/build.gradle b/slidingpanelayout/slidingpanelayout/build.gradle
index c1e7fc2..6a29dca 100644
--- a/slidingpanelayout/slidingpanelayout/build.gradle
+++ b/slidingpanelayout/slidingpanelayout/build.gradle
@@ -19,7 +19,7 @@
androidTestImplementation(libs.kotlinStdlib)
androidTestImplementation(libs.truth)
androidTestImplementation(project(':internal-testutils-runtime'))
- androidTestImplementation(project(":window:window-testing"))
+ androidTestImplementation("androidx.window:window-testing:1.0.0")
}
androidx {
diff --git a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/FeaturedCarousel.kt b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/FeaturedCarousel.kt
index deeafa0..6a5007f 100644
--- a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/FeaturedCarousel.kt
+++ b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/FeaturedCarousel.kt
@@ -112,22 +112,22 @@
val carouselState = remember { CarouselState() }
Carousel(
- slideCount = backgrounds.size,
+ itemCount = backgrounds.size,
carouselState = carouselState,
modifier = modifier
.height(300.dp)
.fillMaxWidth(),
carouselIndicator = {
CarouselDefaults.IndicatorRow(
- slideCount = backgrounds.size,
- activeSlideIndex = carouselState.activeSlideIndex,
+ itemCount = backgrounds.size,
+ activeItemIndex = carouselState.activeItemIndex,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp),
)
}
) { itemIndex ->
- CarouselSlide(
+ CarouselItem(
background = {
Box(
modifier = Modifier
diff --git a/tv/samples/src/main/java/androidx/tv/samples/CarouselSamples.kt b/tv/samples/src/main/java/androidx/tv/samples/CarouselSamples.kt
index ab964bd..aba7109 100644
--- a/tv/samples/src/main/java/androidx/tv/samples/CarouselSamples.kt
+++ b/tv/samples/src/main/java/androidx/tv/samples/CarouselSamples.kt
@@ -17,7 +17,6 @@
package androidx.tv.samples
import androidx.annotation.Sampled
-import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
@@ -45,7 +44,7 @@
import androidx.tv.material3.CarouselState
import androidx.tv.material3.ExperimentalTvMaterial3Api
-@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class)
+@OptIn(ExperimentalTvMaterial3Api::class)
@Sampled
@Composable
fun SimpleCarousel() {
@@ -56,12 +55,12 @@
)
Carousel(
- slideCount = backgrounds.size,
+ itemCount = backgrounds.size,
modifier = Modifier
.height(300.dp)
.fillMaxWidth(),
) { itemIndex ->
- CarouselSlide(
+ CarouselItem(
background = {
Box(
modifier = Modifier
@@ -91,7 +90,7 @@
}
}
-@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class)
+@OptIn(ExperimentalTvMaterial3Api::class)
@Sampled
@Composable
fun CarouselIndicatorWithRectangleShape() {
@@ -103,15 +102,15 @@
val carouselState = remember { CarouselState() }
Carousel(
- slideCount = backgrounds.size,
+ itemCount = backgrounds.size,
modifier = Modifier
.height(300.dp)
.fillMaxWidth(),
carouselState = carouselState,
carouselIndicator = {
CarouselDefaults.IndicatorRow(
- slideCount = backgrounds.size,
- activeSlideIndex = carouselState.activeSlideIndex,
+ itemCount = backgrounds.size,
+ activeItemIndex = carouselState.activeItemIndex,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp),
@@ -130,7 +129,7 @@
)
}
) { itemIndex ->
- CarouselSlide(
+ CarouselItem(
background = {
Box(
modifier = Modifier
diff --git a/tv/tv-material/api/public_plus_experimental_current.txt b/tv/tv-material/api/public_plus_experimental_current.txt
index 38f62bb..b0fe368 100644
--- a/tv/tv-material/api/public_plus_experimental_current.txt
+++ b/tv/tv-material/api/public_plus_experimental_current.txt
@@ -25,38 +25,38 @@
}
@androidx.tv.material3.ExperimentalTvMaterial3Api public final class CarouselDefaults {
- method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public void IndicatorRow(int slideCount, int activeSlideIndex, optional androidx.compose.ui.Modifier modifier, optional float spacing, optional kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> indicator);
+ method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public void IndicatorRow(int itemCount, int activeItemIndex, optional androidx.compose.ui.Modifier modifier, optional float spacing, optional kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> indicator);
method @androidx.compose.runtime.Composable public androidx.compose.animation.ContentTransform getContentTransform();
property @androidx.compose.runtime.Composable public final androidx.compose.animation.ContentTransform contentTransform;
field public static final androidx.tv.material3.CarouselDefaults INSTANCE;
- field public static final long TimeToDisplaySlideMillis = 5000L; // 0x1388L
+ field public static final long TimeToDisplayItemMillis = 5000L; // 0x1388L
+ }
+
+ @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CarouselItemDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.animation.ContentTransform getContentTransformEndToStart();
+ method @androidx.compose.runtime.Composable public androidx.compose.animation.ContentTransform getContentTransformLeftToRight();
+ method @androidx.compose.runtime.Composable public androidx.compose.animation.ContentTransform getContentTransformRightToLeft();
+ method @androidx.compose.runtime.Composable public androidx.compose.animation.ContentTransform getContentTransformStartToEnd();
+ property @androidx.compose.runtime.Composable public final androidx.compose.animation.ContentTransform contentTransformEndToStart;
+ property @androidx.compose.runtime.Composable public final androidx.compose.animation.ContentTransform contentTransformLeftToRight;
+ property @androidx.compose.runtime.Composable public final androidx.compose.animation.ContentTransform contentTransformRightToLeft;
+ property @androidx.compose.runtime.Composable public final androidx.compose.animation.ContentTransform contentTransformStartToEnd;
+ field public static final androidx.tv.material3.CarouselItemDefaults INSTANCE;
}
public final class CarouselKt {
- method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Carousel(int slideCount, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.CarouselState carouselState, optional long autoScrollDurationMillis, optional androidx.compose.animation.ContentTransform contentTransformForward, optional androidx.compose.animation.ContentTransform contentTransformBackward, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> carouselIndicator, kotlin.jvm.functions.Function2<? super androidx.tv.material3.CarouselScope,? super java.lang.Integer,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Carousel(int itemCount, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.CarouselState carouselState, optional long autoScrollDurationMillis, optional androidx.compose.animation.ContentTransform contentTransformStartToEnd, optional androidx.compose.animation.ContentTransform contentTransformEndToStart, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> carouselIndicator, kotlin.jvm.functions.Function2<? super androidx.tv.material3.CarouselScope,? super java.lang.Integer,kotlin.Unit> content);
}
@androidx.tv.material3.ExperimentalTvMaterial3Api public final class CarouselScope {
- method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public void CarouselSlide(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> background, optional androidx.compose.animation.ContentTransform contentTransformForward, optional androidx.compose.animation.ContentTransform contentTransformBackward, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- }
-
- @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CarouselSlideDefaults {
- method @androidx.compose.runtime.Composable public androidx.compose.animation.ContentTransform getContentTransformBackward();
- method @androidx.compose.runtime.Composable public androidx.compose.animation.ContentTransform getContentTransformForward();
- method @androidx.compose.runtime.Composable public androidx.compose.animation.ContentTransform getContentTransformLeftToRight();
- method @androidx.compose.runtime.Composable public androidx.compose.animation.ContentTransform getContentTransformRightToLeft();
- property @androidx.compose.runtime.Composable public final androidx.compose.animation.ContentTransform contentTransformBackward;
- property @androidx.compose.runtime.Composable public final androidx.compose.animation.ContentTransform contentTransformForward;
- property @androidx.compose.runtime.Composable public final androidx.compose.animation.ContentTransform contentTransformLeftToRight;
- property @androidx.compose.runtime.Composable public final androidx.compose.animation.ContentTransform contentTransformRightToLeft;
- field public static final androidx.tv.material3.CarouselSlideDefaults INSTANCE;
+ method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public void CarouselItem(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> background, optional androidx.compose.animation.ContentTransform contentTransformStartToEnd, optional androidx.compose.animation.ContentTransform contentTransformEndToStart, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
@androidx.compose.runtime.Stable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CarouselState {
- ctor public CarouselState(optional int initialActiveSlideIndex);
- method public int getActiveSlideIndex();
- method public androidx.tv.material3.ScrollPauseHandle pauseAutoScroll(int slideIndex);
- property public final int activeSlideIndex;
+ ctor public CarouselState(optional int initialActiveItemIndex);
+ method public int getActiveItemIndex();
+ method public androidx.tv.material3.ScrollPauseHandle pauseAutoScroll(int itemIndex);
+ property public final int activeItemIndex;
}
@androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ClickableSurfaceBorder {
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselScopeTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselScopeTest.kt
index 6fdeaa9..10080e0 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselScopeTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselScopeTest.kt
@@ -52,9 +52,9 @@
@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class)
@Test
- fun carouselSlide_parentContainerGainsFocused_onBackPress() {
+ fun carouselItem_parentContainerGainsFocused_onBackPress() {
val containerBoxTag = "container-box"
- val carouselSlideTag = "carousel-slide"
+ val carouselItemTag = "carousel-item"
rule.setContent {
val carouselState = remember { CarouselState() }
@@ -68,8 +68,8 @@
.focusable()
) {
CarouselScope(carouselState = carouselState)
- .CarouselSlide(
- modifier = Modifier.testTag(carouselSlideTag),
+ .CarouselItem(
+ modifier = Modifier.testTag(carouselItemTag),
background = {
Box(
modifier = Modifier
@@ -82,7 +82,7 @@
}
// Request focus for Carousel Item on start
- rule.onNodeWithTag(carouselSlideTag)
+ rule.onNodeWithTag(carouselItemTag)
.performSemanticsAction(SemanticsActions.RequestFocus)
rule.waitForIdle()
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
index d9c5a2f..7582b2c 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
@@ -71,7 +71,7 @@
import org.junit.Rule
import org.junit.Test
-private const val delayBetweenSlides = 2500L
+private const val delayBetweenItems = 2500L
private const val animationTime = 900L
@OptIn(ExperimentalTvMaterial3Api::class)
@@ -89,10 +89,10 @@
rule.onNodeWithText("Text 1").assertIsDisplayed()
- rule.mainClock.advanceTimeBy(delayBetweenSlides)
+ rule.mainClock.advanceTimeBy(delayBetweenItems)
rule.onNodeWithText("Text 2").assertIsDisplayed()
- rule.mainClock.advanceTimeBy(delayBetweenSlides)
+ rule.mainClock.advanceTimeBy(delayBetweenItems)
rule.onNodeWithText("Text 3").assertIsDisplayed()
}
@@ -111,7 +111,7 @@
.onParent()
.performSemanticsAction(SemanticsActions.RequestFocus)
- rule.mainClock.advanceTimeBy(delayBetweenSlides)
+ rule.mainClock.advanceTimeBy(delayBetweenItems)
rule.onNodeWithText("Text 2").assertDoesNotExist()
rule.onNodeWithText("Text 1").onParent().assertIsFocused()
@@ -130,7 +130,7 @@
rule.onNodeWithText("Text 1").assertIsDisplayed()
rule.onNodeWithText("Text 1").onParent().assertIsNotFocused()
- rule.mainClock.advanceTimeBy(delayBetweenSlides)
+ rule.mainClock.advanceTimeBy(delayBetweenItems)
rule.onNodeWithText("Text 2").assertDoesNotExist()
rule.onNodeWithText("Text 1").assertIsDisplayed()
@@ -154,17 +154,17 @@
rule.onNodeWithText("Text 1").assertIsDisplayed()
rule.onNodeWithText("Text 1").onParent().assertIsNotFocused()
- rule.mainClock.advanceTimeBy(delayBetweenSlides)
+ rule.mainClock.advanceTimeBy(delayBetweenItems)
// pause handle has not been resumed, so Text 1 should still be on the screen.
rule.onNodeWithText("Text 2").assertDoesNotExist()
rule.onNodeWithText("Text 1").assertIsDisplayed()
rule.runOnIdle { pauseHandle?.resumeAutoScroll() }
- rule.mainClock.advanceTimeBy(delayBetweenSlides)
+ rule.mainClock.advanceTimeBy(delayBetweenItems)
// pause handle has been resumed, so Text 2 should be on the screen after
- // delayBetweenSlides + animationTime
+ // delayBetweenItems + animationTime
rule.onNodeWithText("Text 1").assertDoesNotExist()
rule.onNodeWithText("Text 2").assertIsDisplayed()
}
@@ -192,23 +192,23 @@
rule.onNodeWithText("Text 1").assertIsDisplayed()
rule.onNodeWithText("Text 1").onParent().assertIsNotFocused()
- rule.mainClock.advanceTimeBy(delayBetweenSlides)
+ rule.mainClock.advanceTimeBy(delayBetweenItems)
// pause handles have not been resumed, so Text 1 should still be on the screen.
rule.onNodeWithText("Text 2").assertDoesNotExist()
rule.onNodeWithText("Text 1").assertIsDisplayed()
rule.runOnIdle { pauseHandle1?.resumeAutoScroll() }
- rule.mainClock.advanceTimeBy(delayBetweenSlides)
+ rule.mainClock.advanceTimeBy(delayBetweenItems)
// Second pause handle has not been resumed, so Text 1 should still be on the screen.
rule.onNodeWithText("Text 2").assertDoesNotExist()
rule.onNodeWithText("Text 1").assertIsDisplayed()
rule.runOnIdle { pauseHandle2?.resumeAutoScroll() }
- rule.mainClock.advanceTimeBy(delayBetweenSlides)
+ rule.mainClock.advanceTimeBy(delayBetweenItems)
// All pause handles have been resumed, so Text 2 should be on the screen after
- // delayBetweenSlides + animationTime
+ // delayBetweenItems + animationTime
rule.onNodeWithText("Text 1").assertDoesNotExist()
rule.onNodeWithText("Text 2").assertIsDisplayed()
}
@@ -236,7 +236,7 @@
rule.onNodeWithText("Text 1").assertIsDisplayed()
rule.onNodeWithText("Text 1").onParent().assertIsNotFocused()
- rule.mainClock.advanceTimeBy(delayBetweenSlides)
+ rule.mainClock.advanceTimeBy(delayBetweenItems)
// pause handles have not been resumed, so Text 1 should still be on the screen.
rule.onNodeWithText("Text 2").assertDoesNotExist()
@@ -245,7 +245,7 @@
rule.runOnIdle { pauseHandle1?.resumeAutoScroll() }
// subsequent call to resume should be ignored
rule.runOnIdle { pauseHandle1?.resumeAutoScroll() }
- rule.mainClock.advanceTimeBy(delayBetweenSlides)
+ rule.mainClock.advanceTimeBy(delayBetweenItems)
// Second pause handle has not been resumed, so Text 1 should still be on the screen.
rule.onNodeWithText("Text 2").assertDoesNotExist()
@@ -270,7 +270,7 @@
rule.onNodeWithText("Card").performSemanticsAction(SemanticsActions.RequestFocus)
rule.onNodeWithText("Card").assertIsFocused()
- rule.mainClock.advanceTimeBy(delayBetweenSlides)
+ rule.mainClock.advanceTimeBy(delayBetweenItems)
rule.onNodeWithText("Text 1").assertDoesNotExist()
rule.onNodeWithText("Text 2").assertIsDisplayed()
}
@@ -280,7 +280,7 @@
fun carousel_pagerIndicatorDisplayed() {
rule.setContent {
SampleCarousel {
- SampleCarouselSlide(index = it)
+ SampleCarouselItem(index = it)
}
}
@@ -292,7 +292,7 @@
fun carousel_withAnimatedContent_successfulTransition() {
rule.setContent {
SampleCarousel {
- SampleCarouselSlide(index = it) {
+ SampleCarouselItem(index = it) {
Column {
BasicText(text = "Text ${it + 1}")
BasicText(text = "PLAY")
@@ -313,7 +313,7 @@
fun carousel_withAnimatedContent_successfulFocusIn() {
rule.setContent {
SampleCarousel {
- SampleCarouselSlide(index = it)
+ SampleCarouselItem(index = it)
}
}
@@ -321,7 +321,7 @@
rule.onNodeWithTag("pager")
.performSemanticsAction(SemanticsActions.RequestFocus)
- // current slide overlay render delay
+ // current item overlay render delay
rule.mainClock.advanceTimeBy(animationTime, false)
rule.mainClock.advanceTimeBy(animationTime, false)
rule.mainClock.advanceTimeByFrame()
@@ -367,14 +367,14 @@
@OptIn(ExperimentalAnimationApi::class)
@Test
- fun carousel_withCarouselSlide_parentContainerGainsFocus_onBackPress() {
+ fun carousel_withCarouselItem_parentContainerGainsFocus_onBackPress() {
rule.setContent {
Box(modifier = Modifier
.testTag("box-container")
.fillMaxSize()
.focusable()) {
SampleCarousel {
- SampleCarouselSlide(index = it)
+ SampleCarouselItem(index = it)
}
}
}
@@ -432,10 +432,10 @@
.testTag("featured-carousel")
.border(2.dp, Color.Black),
carouselState = remember { CarouselState() },
- slideCount = 3,
- autoScrollDurationMillis = delayBetweenSlides
+ itemCount = 3,
+ autoScrollDurationMillis = delayBetweenItems
) {
- SampleCarouselSlide(index = it) {
+ SampleCarouselItem(index = it) {
Box {
Column(modifier = Modifier.align(Alignment.BottomStart)) {
BasicText(text = "carousel-frame")
@@ -493,10 +493,10 @@
@OptIn(ExperimentalAnimationApi::class)
@Test
- fun carousel_zeroSlideCount_shouldNotCrash() {
+ fun carousel_zeroItemCount_shouldNotCrash() {
val testTag = "emptyCarousel"
rule.setContent {
- Carousel(slideCount = 0, modifier = Modifier.testTag(testTag)) {}
+ Carousel(itemCount = 0, modifier = Modifier.testTag(testTag)) {}
}
rule.onNodeWithTag(testTag).assertExists()
@@ -504,10 +504,10 @@
@OptIn(ExperimentalAnimationApi::class)
@Test
- fun carousel_oneSlideCount_shouldNotCrash() {
+ fun carousel_oneItemCount_shouldNotCrash() {
val testTag = "emptyCarousel"
rule.setContent {
- Carousel(slideCount = 1, modifier = Modifier.testTag(testTag)) {}
+ Carousel(itemCount = 1, modifier = Modifier.testTag(testTag)) {}
}
rule.onNodeWithTag(testTag).assertExists()
@@ -536,20 +536,20 @@
rule.mainClock.advanceTimeByFrame()
rule.waitForIdle()
- // Check that slide 1 is in view and button 1 has focus
+ // Check that item 1 is in view and button 1 has focus
rule.onNodeWithText("Button-1").assertIsDisplayed()
rule.onNodeWithText("Button-1").assertIsFocused()
- // press dpad right to scroll to next slide
+ // press dpad right to scroll to next item
performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
- // Wait for slide to load
+ // Wait for item to load
rule.mainClock.advanceTimeByFrame()
rule.waitForIdle()
rule.mainClock.advanceTimeBy(animationTime, false)
rule.waitForIdle()
- // Check that slide 2 is in view and button 2 has focus
+ // Check that item 2 is in view and button 2 has focus
rule.onNodeWithText("Button-2").assertIsDisplayed()
// TODO: Fix button 2 isn't gaining focus
// rule.onNodeWithText("Button-2").assertIsFocused()
@@ -557,16 +557,16 @@
// Check if the first focusable element in parent has focus
rule.onNodeWithText("Row-button-1").assertIsNotFocused()
- // press dpad left to scroll to previous slide
+ // press dpad left to scroll to previous item
performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
- // Wait for slide to load
+ // Wait for item to load
rule.mainClock.advanceTimeByFrame()
rule.waitForIdle()
rule.mainClock.advanceTimeBy(animationTime, false)
rule.waitForIdle()
- // Check that slide 1 is in view and button 1 has focus
+ // Check that item 1 is in view and button 1 has focus
rule.onNodeWithText("Button-1").assertIsDisplayed()
rule.onNodeWithText("Button-1").assertIsFocused()
}
@@ -592,8 +592,8 @@
}
}
- SampleCarousel(carouselState = carouselState, slideCount = 20) {
- SampleCarouselSlide(modifier = Modifier.testTag("slide-$it"), index = it)
+ SampleCarousel(carouselState = carouselState, itemCount = 20) {
+ SampleCarouselItem(modifier = Modifier.testTag("item-$it"), index = it)
}
}
}
@@ -602,9 +602,9 @@
rule.onNodeWithTag("pager").performSemanticsAction(SemanticsActions.RequestFocus)
rule.waitForIdle()
- val slideProgression = listOf(6, 3, -4, 3, -6, 5, 3)
+ val itemProgression = listOf(6, 3, -4, 3, -6, 5, 3)
- slideProgression.forEach {
+ itemProgression.forEach {
if (it < 0) {
performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT, it * -1)
} else {
@@ -614,20 +614,20 @@
rule.mainClock.advanceTimeBy(animationTime)
- val finalSlide = slideProgression.sum()
- rule.onNodeWithText("Play $finalSlide").assertIsFocused()
+ val finalItem = itemProgression.sum()
+ rule.onNodeWithText("Play $finalItem").assertIsFocused()
performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT, 3)
rule.mainClock.advanceTimeBy((animationTime) * 3)
- rule.onNodeWithText("Play ${finalSlide + 3}").assertIsFocused()
+ rule.onNodeWithText("Play ${finalItem + 3}").assertIsFocused()
}
@Test
fun carousel_manualScrolling_onDpadLongPress() {
rule.setContent {
- SampleCarousel(slideCount = 6) { index ->
+ SampleCarousel(itemCount = 6) { index ->
SampleButton("Button ${index + 1}")
}
}
@@ -641,31 +641,31 @@
rule.mainClock.advanceTimeByFrame()
rule.waitForIdle()
- // Assert that Button 1 from first slide is focused
+ // Assert that Button 1 from first item is focused
rule.onNodeWithText("Button 1").assertIsFocused()
// Trigger dpad right key long press
performLongKeyPress(rule, NativeKeyEvent.KEYCODE_DPAD_RIGHT)
- // Advance time and trigger recomposition to switch to next slide
+ // Advance time and trigger recomposition to switch to next item
rule.mainClock.advanceTimeByFrame()
rule.waitForIdle()
- rule.mainClock.advanceTimeBy(delayBetweenSlides, false)
+ rule.mainClock.advanceTimeBy(delayBetweenItems, false)
rule.waitForIdle()
- // Assert that Button 2 from second slide is focused
+ // Assert that Button 2 from second item is focused
rule.onNodeWithText("Button 2").assertIsFocused()
// Trigger dpad left key long press
performLongKeyPress(rule, NativeKeyEvent.KEYCODE_DPAD_LEFT)
- // Advance time and trigger recomposition to switch to previous slide
- rule.mainClock.advanceTimeBy(delayBetweenSlides, false)
+ // Advance time and trigger recomposition to switch to previous item
+ rule.mainClock.advanceTimeBy(delayBetweenItems, false)
rule.waitForIdle()
rule.mainClock.advanceTimeByFrame()
rule.waitForIdle()
- // Assert that Button 1 from first slide is focused
+ // Assert that Button 1 from first item is focused
rule.onNodeWithText("Button 1").assertIsFocused()
}
@@ -681,37 +681,37 @@
rule.onNodeWithTag("pager")
.performSemanticsAction(SemanticsActions.RequestFocus)
- // current slide overlay render delay
+ // current item overlay render delay
rule.mainClock.advanceTimeBy(animationTime, false)
rule.mainClock.advanceTimeBy(animationTime, false)
rule.mainClock.advanceTimeByFrame()
- // Assert that slide 1 is in view
+ // Assert that item 1 is in view
rule.onNodeWithText("Button 1").assertIsDisplayed()
// advance time
- rule.mainClock.advanceTimeBy(delayBetweenSlides + animationTime, false)
+ rule.mainClock.advanceTimeBy(delayBetweenItems + animationTime, false)
rule.mainClock.advanceTimeByFrame()
// go right once
performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
- // Wait for slide to load
+ // Wait for item to load
rule.mainClock.advanceTimeBy(animationTime)
rule.mainClock.advanceTimeByFrame()
- // Assert that slide 2 is in view
+ // Assert that item 2 is in view
rule.onNodeWithText("Button 2").assertIsDisplayed()
// go left once
performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
- // Wait for slide to load
- rule.mainClock.advanceTimeBy(delayBetweenSlides)
+ // Wait for item to load
+ rule.mainClock.advanceTimeBy(delayBetweenItems)
rule.mainClock.advanceTimeBy(animationTime)
rule.mainClock.advanceTimeByFrame()
- // Assert that slide 1 is in view
+ // Assert that item 1 is in view
rule.onNodeWithText("Button 1").assertIsDisplayed()
}
@@ -731,70 +731,70 @@
rule.onNodeWithTag("pager")
.performSemanticsAction(SemanticsActions.RequestFocus)
- // current slide overlay render delay
+ // current item overlay render delay
rule.mainClock.advanceTimeBy(animationTime, false)
rule.mainClock.advanceTimeBy(animationTime, false)
rule.mainClock.advanceTimeByFrame()
- // Assert that slide 1 is in view
+ // Assert that item 1 is in view
rule.onNodeWithText("Button 1").assertIsDisplayed()
// advance time
- rule.mainClock.advanceTimeBy(delayBetweenSlides + animationTime, false)
+ rule.mainClock.advanceTimeBy(delayBetweenItems + animationTime, false)
rule.mainClock.advanceTimeByFrame()
// go right once
performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
- // Wait for slide to load
+ // Wait for item to load
rule.mainClock.advanceTimeBy(animationTime)
rule.mainClock.advanceTimeByFrame()
- // Assert that slide 2 is in view
+ // Assert that item 2 is in view
rule.onNodeWithText("Button 2").assertIsDisplayed()
// go left once
performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
- // Wait for slide to load
- rule.mainClock.advanceTimeBy(delayBetweenSlides + animationTime, false)
+ // Wait for item to load
+ rule.mainClock.advanceTimeBy(delayBetweenItems + animationTime, false)
rule.mainClock.advanceTimeByFrame()
- // Assert that slide 1 is in view
+ // Assert that item 1 is in view
rule.onNodeWithText("Button 1").assertIsDisplayed()
}
@Test
- fun carousel_slideCountChangesDuringAnimation_shouldNotCrash() {
- val slideDisplayDurationMs: Long = 100
- var slideChanges = 0
- // number of slides will fall from 4 to 2, but 4 slide transitions should happen without a
+ fun carousel_itemCountChangesDuringAnimation_shouldNotCrash() {
+ val itemDisplayDurationMs: Long = 100
+ var itemChanges = 0
+ // number of items will fall from 4 to 2, but 4 item transitions should happen without a
// crash
- val minSuccessfulSlideChanges = 4
+ val minSuccessfulItemChanges = 4
rule.setContent {
- var slideCount by remember { mutableStateOf(4) }
+ var itemCount by remember { mutableStateOf(4) }
LaunchedEffect(Unit) {
- while (slideCount >= 2) {
- delay(slideDisplayDurationMs)
- slideCount--
+ while (itemCount >= 2) {
+ delay(itemDisplayDurationMs)
+ itemCount--
}
}
SampleCarousel(
- slideCount = slideCount,
- timeToDisplaySlideMillis = slideDisplayDurationMs
+ itemCount = itemCount,
+ timeToDisplayItemMillis = itemDisplayDurationMs
) { index ->
- if (index >= slideCount) {
- // slideIndex requested should not be greater than slideCount. User could be
+ if (index >= itemCount) {
+ // itemIndex requested should not be greater than itemCount. User could be
// using a data-structure that could throw an IndexOutOfBoundsException.
- // This can happen when the slideCount changes during the transition between
- // slides.
- throw Exception("Index is larger, index=$index, slideCount=$slideCount")
+ // This can happen when the itemCount changes during the transition between
+ // items.
+ throw Exception("Index is larger, index=$index, itemCount=$itemCount")
}
- slideChanges++
+ itemChanges++
}
}
- rule.waitUntil(timeoutMillis = 5000) { slideChanges > minSuccessfulSlideChanges }
+ rule.waitUntil(timeoutMillis = 5000) { itemChanges > minSuccessfulItemChanges }
}
}
@@ -802,8 +802,8 @@
@Composable
private fun SampleCarousel(
carouselState: CarouselState = remember { CarouselState() },
- slideCount: Int = 3,
- timeToDisplaySlideMillis: Long = delayBetweenSlides,
+ itemCount: Int = 3,
+ timeToDisplayItemMillis: Long = delayBetweenItems,
content: @Composable CarouselScope.(index: Int) -> Unit
) {
Carousel(
@@ -813,16 +813,16 @@
.height(200.dp)
.testTag("pager"),
carouselState = carouselState,
- slideCount = slideCount,
- autoScrollDurationMillis = timeToDisplaySlideMillis,
+ itemCount = itemCount,
+ autoScrollDurationMillis = timeToDisplayItemMillis,
carouselIndicator = {
CarouselDefaults.IndicatorRow(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp)
.testTag("indicator"),
- activeSlideIndex = carouselState.activeSlideIndex,
- slideCount = slideCount
+ activeItemIndex = carouselState.activeItemIndex,
+ itemCount = itemCount
)
},
content = { content(it) },
@@ -831,16 +831,16 @@
@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class)
@Composable
-private fun CarouselScope.SampleCarouselSlide(
+private fun CarouselScope.SampleCarouselItem(
index: Int,
modifier: Modifier = Modifier,
- contentTransformForward: ContentTransform =
- CarouselSlideDefaults.contentTransformForward,
+ contentTransformStartToEnd: ContentTransform =
+ CarouselItemDefaults.contentTransformStartToEnd,
content: (@Composable () -> Unit) = { SampleButton("Play $index") },
) {
- CarouselSlide(
+ CarouselItem(
modifier = modifier,
- contentTransformForward = contentTransformForward,
+ contentTransformStartToEnd = contentTransformStartToEnd,
background = {
Box(
modifier = Modifier
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
index 442c810..8f5661b 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
@@ -78,34 +78,33 @@
* @sample androidx.tv.samples.CarouselIndicatorWithRectangleShape
*
* @param modifier Modifier applied to the Carousel.
- * @param slideCount total number of slides present in the carousel.
+ * @param itemCount total number of items present in the carousel.
* @param carouselState state associated with this carousel.
- * @param autoScrollDurationMillis duration for which slide should be visible before moving to
- * the next slide.
- * @param contentTransformForward animation transform applied when we are moving forward in the
- * carousel while scrolling
- * @param contentTransformBackward animation transform applied when we are moving backward in the
- * carousel while scrolling
- * in the next slide
- * @param carouselIndicator indicator showing the position of the current slide among all slides.
- * @param content defines the slides for a given index.
+ * @param autoScrollDurationMillis duration for which item should be visible before moving to
+ * the next item.
+ * @param contentTransformStartToEnd animation transform applied when we are moving from start to
+ * end in the carousel while scrolling to the next item
+ * @param contentTransformEndToStart animation transform applied when we are moving from end to
+ * start in the carousel while scrolling to the next item
+ * @param carouselIndicator indicator showing the position of the current item among all items.
+ * @param content defines the items for a given index.
*/
@Suppress("IllegalExperimentalApiUsage")
-@OptIn(ExperimentalComposeUiApi::class, ExperimentalAnimationApi::class)
+@OptIn(ExperimentalComposeUiApi::class)
@ExperimentalTvMaterial3Api
@Composable
fun Carousel(
- slideCount: Int,
+ itemCount: Int,
modifier: Modifier = Modifier,
carouselState: CarouselState = remember { CarouselState() },
- autoScrollDurationMillis: Long = CarouselDefaults.TimeToDisplaySlideMillis,
- contentTransformForward: ContentTransform = CarouselDefaults.contentTransform,
- contentTransformBackward: ContentTransform = CarouselDefaults.contentTransform,
+ autoScrollDurationMillis: Long = CarouselDefaults.TimeToDisplayItemMillis,
+ contentTransformStartToEnd: ContentTransform = CarouselDefaults.contentTransform,
+ contentTransformEndToStart: ContentTransform = CarouselDefaults.contentTransform,
carouselIndicator:
@Composable BoxScope.() -> Unit = {
CarouselDefaults.IndicatorRow(
- slideCount = slideCount,
- activeSlideIndex = carouselState.activeSlideIndex,
+ itemCount = itemCount,
+ activeItemIndex = carouselState.activeItemIndex,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp),
@@ -113,7 +112,7 @@
},
content: @Composable CarouselScope.(index: Int) -> Unit
) {
- CarouselStateUpdater(carouselState, slideCount)
+ CarouselStateUpdater(carouselState, itemCount)
var focusState: FocusState? by remember { mutableStateOf(null) }
val focusManager = LocalFocusManager.current
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
@@ -122,7 +121,7 @@
AutoScrollSideEffect(
autoScrollDurationMillis = autoScrollDurationMillis,
- slideCount = slideCount,
+ itemCount = itemCount,
carouselState = carouselState,
doAutoScroll = shouldPerformAutoScroll(focusState),
onAutoScrollChange = { isAutoScrollActive = it })
@@ -142,21 +141,21 @@
carouselState = carouselState,
outerBoxFocusRequester = carouselOuterBoxFocusRequester,
focusManager = focusManager,
- slideCount = slideCount,
+ itemCount = itemCount,
isLtr = isLtr,
)
.focusable()
) {
AnimatedContent(
- targetState = carouselState.activeSlideIndex,
+ targetState = carouselState.activeItemIndex,
transitionSpec = {
if (carouselState.isMovingBackward) {
- contentTransformBackward
+ contentTransformEndToStart
} else {
- contentTransformForward
+ contentTransformStartToEnd
}
}
- ) { activeSlideIndex ->
+ ) { activeItemIndex ->
LaunchedEffect(Unit) {
this@AnimatedContent.onAnimationCompletion {
// Outer box is focused
@@ -166,13 +165,13 @@
}
}
}
- // it is possible for the slideCount to have changed during the transition.
- // This can cause the slideIndex to be greater than or equal to slideCount and cause
- // IndexOutOfBoundsException. Guarding against this by checking against slideCount
+ // it is possible for the itemCount to have changed during the transition.
+ // This can cause the itemIndex to be greater than or equal to itemCount and cause
+ // IndexOutOfBoundsException. Guarding against this by checking against itemCount
// before invoking.
- if (slideCount > 0) {
+ if (itemCount > 0) {
CarouselScope(carouselState = carouselState)
- .content(if (activeSlideIndex < slideCount) activeSlideIndex else 0)
+ .content(if (activeItemIndex < itemCount) activeItemIndex else 0)
}
}
this.carouselIndicator()
@@ -197,13 +196,13 @@
@Composable
private fun AutoScrollSideEffect(
autoScrollDurationMillis: Long,
- slideCount: Int,
+ itemCount: Int,
carouselState: CarouselState,
doAutoScroll: Boolean,
onAutoScrollChange: (isAutoScrollActive: Boolean) -> Unit = {},
) {
- // Needed to ensure that the code within LaunchedEffect receives updates to the slideCount.
- val updatedSlideCount by rememberUpdatedState(newValue = slideCount)
+ // Needed to ensure that the code within LaunchedEffect receives updates to the itemCount.
+ val updatedItemCount by rememberUpdatedState(newValue = itemCount)
if (doAutoScroll) {
LaunchedEffect(carouselState) {
while (true) {
@@ -213,7 +212,7 @@
snapshotFlow { carouselState.activePauseHandlesCount }
.first { pauseHandleCount -> pauseHandleCount == 0 }
}
- carouselState.moveToNextSlide(updatedSlideCount)
+ carouselState.moveToNextItem(updatedItemCount)
}
}
}
@@ -226,7 +225,7 @@
carouselState: CarouselState,
outerBoxFocusRequester: FocusRequester,
focusManager: FocusManager,
- slideCount: Int,
+ itemCount: Int,
isLtr: Boolean
): Modifier = onKeyEvent {
// Ignore KeyUp action type
@@ -234,20 +233,20 @@
return@onKeyEvent KeyEventPropagation.ContinuePropagation
}
- val showPreviousSlideAndGetKeyEventPropagation = {
- if (carouselState.isFirstSlide()) {
+ val showPreviousItemAndGetKeyEventPropagation = {
+ if (carouselState.isFirstItem()) {
KeyEventPropagation.ContinuePropagation
} else {
- carouselState.moveToPreviousSlide(slideCount)
+ carouselState.moveToPreviousItem(itemCount)
outerBoxFocusRequester.requestFocus()
KeyEventPropagation.StopPropagation
}
}
- val showNextSlideAndGetKeyEventPropagation = {
- if (carouselState.isLastSlide(slideCount)) {
+ val showNextItemAndGetKeyEventPropagation = {
+ if (carouselState.isLastItem(itemCount)) {
KeyEventPropagation.ContinuePropagation
} else {
- carouselState.moveToNextSlide(slideCount)
+ carouselState.moveToNextItem(itemCount)
outerBoxFocusRequester.requestFocus()
KeyEventPropagation.StopPropagation
}
@@ -266,9 +265,9 @@
}
if (isLtr) {
- showPreviousSlideAndGetKeyEventPropagation()
+ showPreviousItemAndGetKeyEventPropagation()
} else {
- showNextSlideAndGetKeyEventPropagation()
+ showNextItemAndGetKeyEventPropagation()
}
}
@@ -279,9 +278,9 @@
}
if (isLtr) {
- showNextSlideAndGetKeyEventPropagation()
+ showNextItemAndGetKeyEventPropagation()
} else {
- showPreviousSlideAndGetKeyEventPropagation()
+ showPreviousItemAndGetKeyEventPropagation()
}
}
@@ -291,31 +290,31 @@
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
-private fun CarouselStateUpdater(carouselState: CarouselState, slideCount: Int) {
- LaunchedEffect(carouselState, slideCount) {
- if (slideCount != 0) {
- carouselState.activeSlideIndex = floorMod(carouselState.activeSlideIndex, slideCount)
+private fun CarouselStateUpdater(carouselState: CarouselState, itemCount: Int) {
+ LaunchedEffect(carouselState, itemCount) {
+ if (itemCount != 0) {
+ carouselState.activeItemIndex = floorMod(carouselState.activeItemIndex, itemCount)
}
}
}
/**
- * State of the Carousel which allows the user to specify the first slide that is shown when the
+ * State of the Carousel which allows the user to specify the first item that is shown when the
* Carousel is instantiated in the constructor.
*
* It also provides the user with support to pause and resume the auto-scroll behaviour of the
* Carousel.
- * @param initialActiveSlideIndex the index of the first active slide
+ * @param initialActiveItemIndex the index of the first active item
*/
@Stable
@ExperimentalTvMaterial3Api
-class CarouselState(initialActiveSlideIndex: Int = 0) {
+class CarouselState(initialActiveItemIndex: Int = 0) {
internal var activePauseHandlesCount by mutableStateOf(0)
/**
- * The index of the slide that is currently displayed by the carousel
+ * The index of the item that is currently displayed by the carousel
*/
- var activeSlideIndex by mutableStateOf(initialActiveSlideIndex)
+ var activeItemIndex by mutableStateOf(initialActiveItemIndex)
internal set
/**
@@ -327,38 +326,38 @@
/**
* Pauses the auto-scrolling behaviour of Carousel.
- * The pause request is ignored if [slideIndex] is not the current slide that is visible.
+ * The pause request is ignored if [itemIndex] is not the current item that is visible.
* Returns a [ScrollPauseHandle] that can be used to resume
*/
- fun pauseAutoScroll(slideIndex: Int): ScrollPauseHandle {
- if (this.activeSlideIndex != slideIndex) {
+ fun pauseAutoScroll(itemIndex: Int): ScrollPauseHandle {
+ if (this.activeItemIndex != itemIndex) {
return NoOpScrollPauseHandle
}
return ScrollPauseHandleImpl(this)
}
- internal fun isFirstSlide() = activeSlideIndex == 0
+ internal fun isFirstItem() = activeItemIndex == 0
- internal fun isLastSlide(slideCount: Int) = activeSlideIndex == slideCount - 1
+ internal fun isLastItem(itemCount: Int) = activeItemIndex == itemCount - 1
- internal fun moveToPreviousSlide(slideCount: Int) {
- // No slides available for carousel
- if (slideCount == 0) return
+ internal fun moveToPreviousItem(itemCount: Int) {
+ // No items available for carousel
+ if (itemCount == 0) return
isMovingBackward = true
- // Go to previous slide
- activeSlideIndex = floorMod(activeSlideIndex - 1, slideCount)
+ // Go to previous item
+ activeItemIndex = floorMod(activeItemIndex - 1, itemCount)
}
- internal fun moveToNextSlide(slideCount: Int) {
- // No slides available for carousel
- if (slideCount == 0) return
+ internal fun moveToNextItem(itemCount: Int) {
+ // No items available for carousel
+ if (itemCount == 0) return
isMovingBackward = false
- // Go to next slide
- activeSlideIndex = floorMod(activeSlideIndex + 1, slideCount)
+ // Go to next item
+ activeItemIndex = floorMod(activeItemIndex + 1, itemCount)
}
}
@@ -403,34 +402,33 @@
@ExperimentalTvMaterial3Api
object CarouselDefaults {
/**
- * Default time for which the slide is visible to the user.
+ * Default time for which the item is visible to the user.
*/
- const val TimeToDisplaySlideMillis: Long = 5000
+ const val TimeToDisplayItemMillis: Long = 5000
/**
* Transition applied when bringing it into view and removing it from the view
*/
- @OptIn(ExperimentalAnimationApi::class)
val contentTransform: ContentTransform
@Composable get() =
fadeIn(animationSpec = tween(100))
.with(fadeOut(animationSpec = tween(100)))
/**
- * An indicator showing the position of the current active slide among the slides of the
+ * An indicator showing the position of the current active item among the items of the
* carousel.
*
- * @param slideCount total number of slides in the carousel
- * @param activeSlideIndex the current active slide index
+ * @param itemCount total number of items in the carousel
+ * @param activeItemIndex the current active item index
* @param modifier Modifier applied to the indicators' container
* @param spacing spacing between the indicator dots
- * @param indicator indicator dot representing each slide in the carousel
+ * @param indicator indicator dot representing each item in the carousel
*/
@ExperimentalTvMaterial3Api
@Composable
fun IndicatorRow(
- slideCount: Int,
- activeSlideIndex: Int,
+ itemCount: Int,
+ activeItemIndex: Int,
modifier: Modifier = Modifier,
spacing: Dp = 8.dp,
indicator: @Composable (isActive: Boolean) -> Unit = { isActive ->
@@ -451,8 +449,8 @@
verticalAlignment = Alignment.CenterVertically,
modifier = modifier,
) {
- repeat(slideCount) {
- val isActive = it == activeSlideIndex
+ repeat(itemCount) {
+ val isActive = it == activeItemIndex
indicator(isActive = isActive)
}
}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/CarouselSlide.kt b/tv/tv-material/src/main/java/androidx/tv/material3/CarouselItem.kt
similarity index 86%
rename from tv/tv-material/src/main/java/androidx/tv/material3/CarouselSlide.kt
rename to tv/tv-material/src/main/java/androidx/tv/material3/CarouselItem.kt
index 162f167..14650f7 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/CarouselSlide.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/CarouselItem.kt
@@ -48,10 +48,10 @@
* - a [background] layer that is rendered as soon as the composable is visible.
* - a [content] layer that is rendered on top of the [background]
*
- * @param background composable defining the background of the slide
- * @param slideIndex current active slide index of the carousel
- * @param modifier modifier applied to the CarouselSlide
- * @param contentTransform content transform to be applied to the content of the slide when
+ * @param background composable defining the background of the item
+ * @param itemIndex current active item index of the carousel
+ * @param modifier modifier applied to the CarouselItem
+ * @param contentTransform content transform to be applied to the content of the item when
* scrolling
* @param content composable defining the content displayed on top of the background
*/
@@ -59,12 +59,12 @@
@OptIn(ExperimentalAnimationApi::class, ExperimentalComposeUiApi::class)
@ExperimentalTvMaterial3Api
@Composable
-internal fun CarouselSlide(
- slideIndex: Int,
+internal fun CarouselItem(
+ itemIndex: Int,
modifier: Modifier = Modifier,
background: @Composable () -> Unit = {},
contentTransform: ContentTransform =
- CarouselSlideDefaults.contentTransformForward,
+ CarouselItemDefaults.contentTransformStartToEnd,
content: @Composable () -> Unit,
) {
var containerBoxFocusState: FocusState? by remember { mutableStateOf(null) }
@@ -73,7 +73,7 @@
var isVisible by remember { mutableStateOf(false) }
- DisposableEffect(slideIndex) {
+ DisposableEffect(itemIndex) {
isVisible = true
onDispose { isVisible = false }
}
@@ -112,13 +112,11 @@
}
@ExperimentalTvMaterial3Api
-object CarouselSlideDefaults {
+object CarouselItemDefaults {
/**
* Transform the content from right to left
*/
// Keeping this as public so that users can access it directly without the isLTR helper
- @Suppress("IllegalExperimentalApiUsage")
- @OptIn(ExperimentalAnimationApi::class)
val contentTransformRightToLeft: ContentTransform
@Composable get() =
slideInHorizontally { it * 4 }
@@ -128,8 +126,6 @@
* Transform the content from left to right
*/
// Keeping this as public so that users can access it directly without the isLTR helper
- @Suppress("IllegalExperimentalApiUsage")
- @OptIn(ExperimentalAnimationApi::class)
val contentTransformLeftToRight: ContentTransform
@Composable get() =
slideInHorizontally()
@@ -138,9 +134,7 @@
/**
* Content transform applied when moving forward taking isLTR into account
*/
- @Suppress("IllegalExperimentalApiUsage")
- @OptIn(ExperimentalAnimationApi::class)
- val contentTransformForward
+ val contentTransformStartToEnd
@Composable get() =
if (isLtr())
contentTransformRightToLeft
@@ -150,9 +144,7 @@
/**
* Content transform applied when moving backward taking isLTR into account
*/
- @Suppress("IllegalExperimentalApiUsage")
- @OptIn(ExperimentalAnimationApi::class)
- val contentTransformBackward
+ val contentTransformEndToStart
@Composable get() =
if (isLtr())
contentTransformLeftToRight
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/CarouselScope.kt b/tv/tv-material/src/main/java/androidx/tv/material3/CarouselScope.kt
index 838e1e6..665ceb9 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/CarouselScope.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/CarouselScope.kt
@@ -17,51 +17,48 @@
package androidx.tv.material3
import androidx.compose.animation.ContentTransform
-import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
/**
- * CarouselScope provides a [CarouselScope.CarouselSlide] function which you can use to
- * provide the slide's animation, background and the inner content.
+ * CarouselScope provides a [CarouselScope.CarouselItem] function which you can use to
+ * provide the carousel item's animation, background and the inner content.
*/
@ExperimentalTvMaterial3Api
class CarouselScope @OptIn(ExperimentalTvMaterial3Api::class)
internal constructor(private val carouselState: CarouselState) {
/**
- * [CarouselScope.CarouselSlide] can be used to define a slide's animation, background, and
- * content. Using this is optional and you can choose to define your own CarouselSlide from
+ * [CarouselScope.CarouselItem] can be used to define a item's animation, background, and
+ * content. Using this is optional and you can choose to define your own CarouselItem from
* scratch
*
- * @param modifier modifier applied to the CarouselSlide
- * @param background composable defining the background of the slide
- * @param contentTransformForward content transform to be applied to the content of the slide
+ * @param modifier modifier applied to the CarouselItem
+ * @param background composable defining the background of the item
+ * @param contentTransformStartToEnd content transform to be applied to the content of the item
* when scrolling forward in the carousel
- * @param contentTransformBackward content transform to be applied to the content of the slide
+ * @param contentTransformEndToStart content transform to be applied to the content of the item
* when scrolling backward in the carousel
* @param content composable defining the content displayed on top of the background
*/
@Composable
- @Suppress("IllegalExperimentalApiUsage")
- @OptIn(ExperimentalAnimationApi::class)
@ExperimentalTvMaterial3Api
- fun CarouselSlide(
+ fun CarouselItem(
modifier: Modifier = Modifier,
background: @Composable () -> Unit = {},
- contentTransformForward: ContentTransform =
- CarouselSlideDefaults.contentTransformForward,
- contentTransformBackward: ContentTransform =
- CarouselSlideDefaults.contentTransformBackward,
+ contentTransformStartToEnd: ContentTransform =
+ CarouselItemDefaults.contentTransformStartToEnd,
+ contentTransformEndToStart: ContentTransform =
+ CarouselItemDefaults.contentTransformEndToStart,
content: @Composable () -> Unit
) {
- CarouselSlide(
+ CarouselItem(
background = background,
- slideIndex = carouselState.activeSlideIndex,
+ itemIndex = carouselState.activeItemIndex,
contentTransform =
if (carouselState.isMovingBackward)
- contentTransformBackward
+ contentTransformEndToStart
else
- contentTransformForward,
+ contentTransformStartToEnd,
modifier = modifier,
content = content,
)
diff --git a/wear/protolayout/protolayout-material/build.gradle b/wear/protolayout/protolayout-material/build.gradle
index fdd52a9..3421707 100644
--- a/wear/protolayout/protolayout-material/build.gradle
+++ b/wear/protolayout/protolayout-material/build.gradle
@@ -18,6 +18,7 @@
plugins {
id("AndroidXPlugin")
+ id("kotlin-android")
id("com.android.library")
}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ButtonColorsTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ButtonColorsTest.java
new file mode 100644
index 0000000..fcbf8b2
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ButtonColorsTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.wear.protolayout.material;
+
+import static androidx.wear.protolayout.ColorBuilders.argb;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.ColorBuilders.ColorProp;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+public class ButtonColorsTest {
+ private static final int ARGB_BACKGROUND_COLOR = 0x12345678;
+ private static final int ARGB_CONTENT_COLOR = 0x11223344;
+ private static final ColorProp BACKGROUND_COLOR = argb(ARGB_BACKGROUND_COLOR);
+ private static final ColorProp CONTENT_COLOR = argb(ARGB_CONTENT_COLOR);
+ private static final Colors COLORS = new Colors(0x123, 0x234, 0x345, 0x456);
+
+ @Test
+ public void testCreateButtonColorsFromArgb() {
+ ButtonColors buttonColors = new ButtonColors(ARGB_BACKGROUND_COLOR, ARGB_CONTENT_COLOR);
+
+ assertThat(buttonColors.getBackgroundColor().getArgb())
+ .isEqualTo(BACKGROUND_COLOR.getArgb());
+ assertThat(buttonColors.getContentColor().getArgb()).isEqualTo(CONTENT_COLOR.getArgb());
+ }
+
+ @Test
+ public void testCreateButtonColorsFromColorProp() {
+ ButtonColors buttonColors = new ButtonColors(BACKGROUND_COLOR, CONTENT_COLOR);
+
+ assertThat(buttonColors.getBackgroundColor().getArgb())
+ .isEqualTo(BACKGROUND_COLOR.getArgb());
+ assertThat(buttonColors.getContentColor().getArgb()).isEqualTo(CONTENT_COLOR.getArgb());
+ }
+
+ @Test
+ public void testCreateButtonColorsFromHelperPrimary() {
+ ButtonColors buttonColors = ButtonColors.primaryButtonColors(COLORS);
+
+ assertThat(buttonColors.getBackgroundColor().getArgb()).isEqualTo(COLORS.getPrimary());
+ assertThat(buttonColors.getContentColor().getArgb()).isEqualTo(COLORS.getOnPrimary());
+ }
+
+ @Test
+ public void testCreateButtonColorsFromHelperSurface() {
+ ButtonColors buttonColors = ButtonColors.secondaryButtonColors(COLORS);
+
+ assertThat(buttonColors.getBackgroundColor().getArgb()).isEqualTo(COLORS.getSurface());
+ assertThat(buttonColors.getContentColor().getArgb()).isEqualTo(COLORS.getOnSurface());
+ }
+}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ButtonTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ButtonTest.java
new file mode 100644
index 0000000..1cdede2
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ButtonTest.java
@@ -0,0 +1,355 @@
+/*
+ * 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.wear.protolayout.material;
+
+import static androidx.wear.protolayout.ColorBuilders.argb;
+import static androidx.wear.protolayout.DimensionBuilders.dp;
+import static androidx.wear.protolayout.material.ButtonDefaults.DEFAULT_SIZE;
+import static androidx.wear.protolayout.material.ButtonDefaults.EXTRA_LARGE_SIZE;
+import static androidx.wear.protolayout.material.ButtonDefaults.LARGE_SIZE;
+import static androidx.wear.protolayout.material.ButtonDefaults.PRIMARY_COLORS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.ActionBuilders.LaunchAction;
+import androidx.wear.protolayout.DimensionBuilders.DpProp;
+import androidx.wear.protolayout.LayoutElementBuilders.Box;
+import androidx.wear.protolayout.LayoutElementBuilders.Column;
+import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
+import androidx.wear.protolayout.ModifiersBuilders.Clickable;
+import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata;
+import androidx.wear.protolayout.ModifiersBuilders.Modifiers;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+public class ButtonTest {
+ private static final String RESOURCE_ID = "icon";
+ private static final String TEXT = "ABC";
+ private static final String CONTENT_DESCRIPTION = "clickable button";
+ private static final Clickable CLICKABLE =
+ new Clickable.Builder()
+ .setOnClick(new LaunchAction.Builder().build())
+ .setId("action_id")
+ .build();
+ private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
+ private static final LayoutElement CONTENT =
+ new Text.Builder(CONTEXT, "ABC").setColor(argb(0)).build();
+
+ @Test
+ public void testButtonCustomAddedContentNoContentDesc() {
+ Button button = new Button.Builder(CONTEXT, CLICKABLE).setCustomContent(CONTENT).build();
+
+ assertButton(
+ button,
+ DEFAULT_SIZE,
+ new ButtonColors(Colors.PRIMARY, 0),
+ null,
+ Button.METADATA_TAG_CUSTOM_CONTENT,
+ null,
+ null,
+ null,
+ CONTENT);
+ }
+
+ @Test
+ public void testButtonCustom() {
+ DpProp mSize = LARGE_SIZE;
+ ButtonColors mButtonColors = new ButtonColors(0x11223344, 0);
+
+ Button button =
+ new Button.Builder(CONTEXT, CLICKABLE)
+ .setCustomContent(CONTENT)
+ .setSize(mSize)
+ .setButtonColors(mButtonColors)
+ .setContentDescription(CONTENT_DESCRIPTION)
+ .build();
+
+ assertButton(
+ button,
+ mSize,
+ mButtonColors,
+ CONTENT_DESCRIPTION,
+ Button.METADATA_TAG_CUSTOM_CONTENT,
+ null,
+ null,
+ null,
+ CONTENT);
+ }
+
+ @Test
+ public void testButtonSetIcon() {
+
+ Button button =
+ new Button.Builder(CONTEXT, CLICKABLE)
+ .setIconContent(RESOURCE_ID)
+ .setContentDescription(CONTENT_DESCRIPTION)
+ .build();
+
+ assertButton(
+ button,
+ DEFAULT_SIZE,
+ PRIMARY_COLORS,
+ CONTENT_DESCRIPTION,
+ Button.METADATA_TAG_ICON,
+ null,
+ RESOURCE_ID,
+ null,
+ null);
+ }
+
+ @Test
+ public void testButtonSetIconSetSize() {
+ Button button =
+ new Button.Builder(CONTEXT, CLICKABLE)
+ .setIconContent(RESOURCE_ID)
+ .setSize(LARGE_SIZE)
+ .setContentDescription(CONTENT_DESCRIPTION)
+ .build();
+
+ assertButton(
+ button,
+ LARGE_SIZE,
+ PRIMARY_COLORS,
+ CONTENT_DESCRIPTION,
+ Button.METADATA_TAG_ICON,
+ null,
+ RESOURCE_ID,
+ null,
+ null);
+ }
+
+ @Test
+ public void testButtonSetIconCustomSize() {
+ DpProp mSize = dp(36);
+
+ Button button =
+ new Button.Builder(CONTEXT, CLICKABLE)
+ .setIconContent(RESOURCE_ID, mSize)
+ .setContentDescription(CONTENT_DESCRIPTION)
+ .build();
+
+ assertButton(
+ button,
+ DEFAULT_SIZE,
+ PRIMARY_COLORS,
+ CONTENT_DESCRIPTION,
+ Button.METADATA_TAG_ICON,
+ null,
+ RESOURCE_ID,
+ null,
+ null);
+ }
+
+ @Test
+ public void testButtonSetText() {
+ Button button =
+ new Button.Builder(CONTEXT, CLICKABLE)
+ .setTextContent(TEXT)
+ .setContentDescription(CONTENT_DESCRIPTION)
+ .build();
+
+ assertButton(
+ button,
+ DEFAULT_SIZE,
+ PRIMARY_COLORS,
+ CONTENT_DESCRIPTION,
+ Button.METADATA_TAG_TEXT,
+ TEXT,
+ null,
+ null,
+ null);
+ }
+
+ @Test
+ public void testButtonSetTextSetSize() {
+ Button button =
+ new Button.Builder(CONTEXT, CLICKABLE)
+ .setTextContent(TEXT)
+ .setContentDescription(CONTENT_DESCRIPTION)
+ .setSize(EXTRA_LARGE_SIZE)
+ .build();
+
+ assertButton(
+ button,
+ EXTRA_LARGE_SIZE,
+ PRIMARY_COLORS,
+ CONTENT_DESCRIPTION,
+ Button.METADATA_TAG_TEXT,
+ TEXT,
+ null,
+ null,
+ null);
+ }
+
+ @Test
+ public void testWrongElementForButton() {
+ Column box = new Column.Builder().build();
+
+ assertThat(Button.fromLayoutElement(box)).isNull();
+ }
+
+ @Test
+ public void testWrongBoxForButton() {
+ Box box = new Box.Builder().build();
+
+ assertThat(Button.fromLayoutElement(box)).isNull();
+ }
+
+ @Test
+ public void testWrongTagForButton() {
+ Box box =
+ new Box.Builder()
+ .setModifiers(
+ new Modifiers.Builder()
+ .setMetadata(
+ new ElementMetadata.Builder()
+ .setTagData("test".getBytes(UTF_8))
+ .build())
+ .build())
+ .build();
+
+ assertThat(Button.fromLayoutElement(box)).isNull();
+ }
+
+ private void assertButton(
+ @NonNull Button actualButton,
+ @NonNull DpProp expectedSize,
+ @NonNull ButtonColors expectedButtonColors,
+ @Nullable String expectedContentDescription,
+ @NonNull String expectedMetadataTag,
+ @Nullable String expectedTextContent,
+ @Nullable String expectedIconContent,
+ @Nullable String expectedImageContent,
+ @Nullable LayoutElement expectedCustomContent) {
+ assertButtonIsEqual(
+ actualButton,
+ expectedSize,
+ expectedButtonColors,
+ expectedContentDescription,
+ expectedMetadataTag,
+ expectedTextContent,
+ expectedIconContent,
+ expectedImageContent,
+ expectedCustomContent);
+
+ assertFromLayoutElementButtonIsEqual(
+ actualButton,
+ expectedSize,
+ expectedButtonColors,
+ expectedContentDescription,
+ expectedMetadataTag,
+ expectedTextContent,
+ expectedIconContent,
+ expectedImageContent,
+ expectedCustomContent);
+
+ assertThat(Button.fromLayoutElement(actualButton)).isEqualTo(actualButton);
+ }
+
+ private void assertButtonIsEqual(
+ @NonNull Button actualButton,
+ @NonNull DpProp expectedSize,
+ @NonNull ButtonColors expectedButtonColors,
+ @Nullable String expectedContentDescription,
+ @NonNull String expectedMetadataTag,
+ @Nullable String expectedTextContent,
+ @Nullable String expectedIconContent,
+ @Nullable String expectedImageContent,
+ @Nullable LayoutElement expectedCustomContent) {
+ // Mandatory
+ assertThat(actualButton.getMetadataTag()).isEqualTo(expectedMetadataTag);
+ assertThat(actualButton.getClickable().toProto()).isEqualTo(CLICKABLE.toProto());
+ assertThat(actualButton.getSize().toContainerDimensionProto())
+ .isEqualTo(expectedSize.toContainerDimensionProto());
+ assertThat(actualButton.getButtonColors().getBackgroundColor().getArgb())
+ .isEqualTo(expectedButtonColors.getBackgroundColor().getArgb());
+ assertThat(actualButton.getButtonColors().getContentColor().getArgb())
+ .isEqualTo(expectedButtonColors.getContentColor().getArgb());
+
+ // Nullable
+ if (expectedContentDescription == null) {
+ assertThat(actualButton.getContentDescription()).isNull();
+ } else {
+ assertThat(actualButton.getContentDescription().toString())
+ .isEqualTo(expectedContentDescription);
+ }
+
+ if (expectedTextContent == null) {
+ assertThat(actualButton.getTextContent()).isNull();
+ } else {
+ assertThat(actualButton.getTextContent()).isEqualTo(expectedTextContent);
+ }
+
+ if (expectedIconContent == null) {
+ assertThat(actualButton.getIconContent()).isNull();
+ } else {
+ assertThat(actualButton.getIconContent()).isEqualTo(expectedIconContent);
+ }
+
+ if (expectedImageContent == null) {
+ assertThat(actualButton.getImageContent()).isNull();
+ } else {
+ assertThat(actualButton.getImageContent()).isEqualTo(expectedImageContent);
+ }
+
+ if (expectedCustomContent == null) {
+ assertThat(actualButton.getCustomContent()).isNull();
+ } else {
+ assertThat(actualButton.getCustomContent().toLayoutElementProto())
+ .isEqualTo(expectedCustomContent.toLayoutElementProto());
+ }
+ }
+
+ private void assertFromLayoutElementButtonIsEqual(
+ @NonNull Button button,
+ @NonNull DpProp expectedSize,
+ @NonNull ButtonColors expectedButtonColors,
+ @Nullable String expectedContentDescription,
+ @NonNull String expectedMetadataTag,
+ @Nullable String expectedTextContent,
+ @Nullable String expectedIconContent,
+ @Nullable String expectedImageContent,
+ @Nullable LayoutElement expectedCustomContent) {
+ Box box = new Box.Builder().addContent(button).build();
+
+ Button newButton = Button.fromLayoutElement(box.getContents().get(0));
+
+ assertThat(newButton).isNotNull();
+ assertButtonIsEqual(
+ newButton,
+ expectedSize,
+ expectedButtonColors,
+ expectedContentDescription,
+ expectedMetadataTag,
+ expectedTextContent,
+ expectedIconContent,
+ expectedImageContent,
+ expectedCustomContent);
+ }
+}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ChipColorsTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ChipColorsTest.java
new file mode 100644
index 0000000..5bf4319
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ChipColorsTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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.wear.protolayout.material;
+
+import static androidx.wear.protolayout.ColorBuilders.argb;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.ColorBuilders.ColorProp;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+public class ChipColorsTest {
+ private static final int ARGB_BACKGROUND_COLOR = 0x12345678;
+ private static final int ARGB_CONTENT_COLOR = 0x11223344;
+ private static final int ARGB_SECONDARY_CONTENT_COLOR = 0x11223355;
+ private static final int ARGB_ICON_COLOR = 0x11223366;
+ private static final ColorProp BACKGROUND_COLOR = argb(ARGB_BACKGROUND_COLOR);
+ private static final ColorProp CONTENT_COLOR = argb(ARGB_CONTENT_COLOR);
+ private static final ColorProp ICON_COLOR = argb(ARGB_ICON_COLOR);
+ private static final ColorProp SECONDARY_CONTENT_COLOR = argb(ARGB_SECONDARY_CONTENT_COLOR);
+ private static final Colors COLORS = new Colors(0x123, 0x234, 0x345, 0x456);
+
+ @Test
+ public void testCreateChipColorsFromArgb() {
+ ChipColors chipColors = new ChipColors(ARGB_BACKGROUND_COLOR, ARGB_CONTENT_COLOR);
+
+ assertThat(chipColors.getBackgroundColor().getArgb()).isEqualTo(BACKGROUND_COLOR.getArgb());
+ assertThat(chipColors.getIconColor().getArgb()).isEqualTo(CONTENT_COLOR.getArgb());
+ assertThat(chipColors.getContentColor().getArgb()).isEqualTo(CONTENT_COLOR.getArgb());
+ assertThat(chipColors.getSecondaryContentColor().getArgb())
+ .isEqualTo(CONTENT_COLOR.getArgb());
+ }
+
+ @Test
+ public void testCreateChipColorsFromColorProp() {
+ ChipColors chipColors = new ChipColors(BACKGROUND_COLOR, CONTENT_COLOR);
+
+ assertThat(chipColors.getBackgroundColor().getArgb()).isEqualTo(BACKGROUND_COLOR.getArgb());
+ assertThat(chipColors.getIconColor().getArgb()).isEqualTo(CONTENT_COLOR.getArgb());
+ assertThat(chipColors.getContentColor().getArgb()).isEqualTo(CONTENT_COLOR.getArgb());
+ assertThat(chipColors.getSecondaryContentColor().getArgb())
+ .isEqualTo(CONTENT_COLOR.getArgb());
+ }
+
+ @Test
+ public void testCreateChipColorsFullFromArgb() {
+ ChipColors chipColors =
+ new ChipColors(
+ ARGB_BACKGROUND_COLOR,
+ ARGB_ICON_COLOR,
+ ARGB_CONTENT_COLOR,
+ ARGB_SECONDARY_CONTENT_COLOR);
+
+ assertThat(chipColors.getBackgroundColor().getArgb()).isEqualTo(BACKGROUND_COLOR.getArgb());
+ assertThat(chipColors.getIconColor().getArgb()).isEqualTo(ICON_COLOR.getArgb());
+ assertThat(chipColors.getContentColor().getArgb()).isEqualTo(CONTENT_COLOR.getArgb());
+ assertThat(chipColors.getSecondaryContentColor().getArgb())
+ .isEqualTo(SECONDARY_CONTENT_COLOR.getArgb());
+ }
+
+ @Test
+ public void testCreateChipColorsFullFromColorProp() {
+ ChipColors chipColors =
+ new ChipColors(
+ BACKGROUND_COLOR, ICON_COLOR, CONTENT_COLOR, SECONDARY_CONTENT_COLOR);
+
+ assertThat(chipColors.getBackgroundColor().getArgb()).isEqualTo(BACKGROUND_COLOR.getArgb());
+ assertThat(chipColors.getIconColor().getArgb()).isEqualTo(ICON_COLOR.getArgb());
+ assertThat(chipColors.getContentColor().getArgb()).isEqualTo(CONTENT_COLOR.getArgb());
+ assertThat(chipColors.getSecondaryContentColor().getArgb())
+ .isEqualTo(SECONDARY_CONTENT_COLOR.getArgb());
+ }
+
+ @Test
+ public void testCreateChipColorsFromHelperPrimary() {
+ ChipColors chipColors = ChipColors.primaryChipColors(COLORS);
+
+ assertThat(chipColors.getBackgroundColor().getArgb()).isEqualTo(COLORS.getPrimary());
+ assertThat(chipColors.getIconColor().getArgb()).isEqualTo(COLORS.getOnPrimary());
+ assertThat(chipColors.getContentColor().getArgb()).isEqualTo(COLORS.getOnPrimary());
+ assertThat(chipColors.getSecondaryContentColor().getArgb())
+ .isEqualTo(COLORS.getOnPrimary());
+ }
+
+ @Test
+ public void testCreateChipColorsFromHelperSurface() {
+ ChipColors chipColors = ChipColors.secondaryChipColors(COLORS);
+
+ assertThat(chipColors.getBackgroundColor().getArgb()).isEqualTo(COLORS.getSurface());
+ assertThat(chipColors.getIconColor().getArgb()).isEqualTo(COLORS.getOnSurface());
+ assertThat(chipColors.getContentColor().getArgb()).isEqualTo(COLORS.getOnSurface());
+ assertThat(chipColors.getSecondaryContentColor().getArgb())
+ .isEqualTo(COLORS.getOnSurface());
+ }
+}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ChipTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ChipTest.java
new file mode 100644
index 0000000..316b904
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ChipTest.java
@@ -0,0 +1,319 @@
+/*
+ * 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.wear.protolayout.material;
+
+import static androidx.wear.protolayout.ColorBuilders.argb;
+import static androidx.wear.protolayout.DimensionBuilders.dp;
+import static androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER;
+import static androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_START;
+import static androidx.wear.protolayout.material.Utils.areChipColorsEqual;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.content.Context;
+import android.graphics.Color;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.ActionBuilders.LaunchAction;
+import androidx.wear.protolayout.ColorBuilders.ColorProp;
+import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.protolayout.DimensionBuilders.DpProp;
+import androidx.wear.protolayout.LayoutElementBuilders.Box;
+import androidx.wear.protolayout.LayoutElementBuilders.Column;
+import androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignment;
+import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
+import androidx.wear.protolayout.LayoutElementBuilders.Row;
+import androidx.wear.protolayout.ModifiersBuilders.Clickable;
+import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata;
+import androidx.wear.protolayout.ModifiersBuilders.Modifiers;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+public class ChipTest {
+ private static final String MAIN_TEXT = "Primary text";
+ private static final Clickable CLICKABLE =
+ new Clickable.Builder()
+ .setOnClick(new LaunchAction.Builder().build())
+ .setId("action_id")
+ .build();
+ private static final DeviceParameters DEVICE_PARAMETERS =
+ new DeviceParameters.Builder().setScreenWidthDp(192).setScreenHeightDp(192).build();
+ private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
+ private static final DpProp EXPECTED_WIDTH =
+ dp(
+ DEVICE_PARAMETERS.getScreenWidthDp()
+ * (100 - 2 * ChipDefaults.DEFAULT_MARGIN_PERCENT)
+ / 100);
+
+ @Test
+ public void testChip() {
+ String contentDescription = "Chip";
+ Chip chip =
+ new Chip.Builder(CONTEXT, CLICKABLE, DEVICE_PARAMETERS)
+ .setPrimaryLabelContent(MAIN_TEXT)
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
+ .setContentDescription(contentDescription)
+ .build();
+ assertChip(
+ chip,
+ HORIZONTAL_ALIGN_CENTER,
+ ChipDefaults.PRIMARY_COLORS,
+ contentDescription,
+ Chip.METADATA_TAG_TEXT,
+ MAIN_TEXT,
+ null,
+ null,
+ null);
+ }
+
+ @Test
+ public void testFullChipColors() {
+ ChipColors colors = new ChipColors(Color.YELLOW, Color.WHITE, Color.BLUE, Color.MAGENTA);
+ String secondaryLabel = "Label";
+ Chip chip =
+ new Chip.Builder(CONTEXT, CLICKABLE, DEVICE_PARAMETERS)
+ .setChipColors(colors)
+ .setPrimaryLabelContent(MAIN_TEXT)
+ .setSecondaryLabelContent(secondaryLabel)
+ .setIconContent("ICON_ID")
+ .build();
+ assertChip(
+ chip,
+ HORIZONTAL_ALIGN_START,
+ colors,
+ MAIN_TEXT + "\n" + secondaryLabel,
+ Chip.METADATA_TAG_ICON,
+ MAIN_TEXT,
+ secondaryLabel,
+ "ICON_ID",
+ null);
+ }
+
+ @Test
+ public void testChipLeftAligned() {
+ Chip chip =
+ new Chip.Builder(CONTEXT, CLICKABLE, DEVICE_PARAMETERS)
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_START)
+ .setPrimaryLabelContent(MAIN_TEXT)
+ .build();
+ assertChip(
+ chip,
+ HORIZONTAL_ALIGN_START,
+ ChipDefaults.PRIMARY_COLORS,
+ MAIN_TEXT,
+ Chip.METADATA_TAG_TEXT,
+ MAIN_TEXT,
+ null,
+ null,
+ null);
+ }
+
+ @Test
+ public void testChipCustomContent() {
+ ColorProp yellow = argb(Color.YELLOW);
+ ColorProp blue = argb(Color.BLUE);
+ LayoutElement content =
+ new Row.Builder()
+ .addContent(
+ new Text.Builder(CONTEXT, "text1")
+ .setTypography(Typography.TYPOGRAPHY_TITLE3)
+ .setColor(yellow)
+ .setItalic(true)
+ .build())
+ .addContent(
+ new Text.Builder(CONTEXT, "text2")
+ .setTypography(Typography.TYPOGRAPHY_TITLE2)
+ .setColor(blue)
+ .build())
+ .build();
+
+ String contentDescription = "Custom chip";
+ Chip chip =
+ new Chip.Builder(CONTEXT, CLICKABLE, DEVICE_PARAMETERS)
+ .setCustomContent(content)
+ .setHorizontalAlignment(HORIZONTAL_ALIGN_START)
+ .setContentDescription(contentDescription)
+ .build();
+
+ assertChip(
+ chip,
+ HORIZONTAL_ALIGN_START,
+ new ChipColors(
+ ChipDefaults.PRIMARY_COLORS.getBackgroundColor(),
+ new ColorProp.Builder(0).build()),
+ contentDescription,
+ Chip.METADATA_TAG_CUSTOM_CONTENT,
+ null,
+ null,
+ null,
+ content);
+ assertThat(chip.getCustomContent().toLayoutElementProto())
+ .isEqualTo(content.toLayoutElementProto());
+ }
+
+ private void assertChip(
+ @NonNull Chip actualChip,
+ @HorizontalAlignment int hAlign,
+ @NonNull ChipColors colors,
+ @Nullable String expectedContDesc,
+ @NonNull String expectedMetadata,
+ @Nullable String expectedPrimaryText,
+ @Nullable String expectedLabel,
+ @Nullable String expectedIcon,
+ @Nullable LayoutElement expectedCustomContent) {
+ assertChipIsEqual(
+ actualChip,
+ hAlign,
+ colors,
+ expectedContDesc,
+ expectedMetadata,
+ expectedPrimaryText,
+ expectedLabel,
+ expectedIcon,
+ expectedCustomContent);
+
+ assertFromLayoutElementChipIsEqual(
+ actualChip,
+ hAlign,
+ colors,
+ expectedContDesc,
+ expectedMetadata,
+ expectedPrimaryText,
+ expectedLabel,
+ expectedIcon,
+ expectedCustomContent);
+
+ assertThat(Chip.fromLayoutElement(actualChip)).isEqualTo(actualChip);
+ }
+
+ @Test
+ public void testWrongElement() {
+ Column box = new Column.Builder().build();
+
+ assertThat(Chip.fromLayoutElement(box)).isNull();
+ }
+
+ @Test
+ public void testWrongBox() {
+ Box box = new Box.Builder().build();
+
+ assertThat(Chip.fromLayoutElement(box)).isNull();
+ }
+
+ @Test
+ public void testWrongTag() {
+ Box box =
+ new Box.Builder()
+ .setModifiers(
+ new Modifiers.Builder()
+ .setMetadata(
+ new ElementMetadata.Builder()
+ .setTagData("test".getBytes(UTF_8))
+ .build())
+ .build())
+ .build();
+
+ assertThat(Chip.fromLayoutElement(box)).isNull();
+ }
+
+ private void assertFromLayoutElementChipIsEqual(
+ @NonNull Chip chip,
+ @HorizontalAlignment int hAlign,
+ @NonNull ChipColors colors,
+ @Nullable String expectedContDesc,
+ @NonNull String expectedMetadata,
+ @Nullable String expectedPrimaryText,
+ @Nullable String expectedLabel,
+ @Nullable String expectedIcon,
+ @Nullable LayoutElement expectedCustomContent) {
+ Box box = new Box.Builder().addContent(chip).build();
+
+ Chip newChip = Chip.fromLayoutElement(box.getContents().get(0));
+
+ assertThat(newChip).isNotNull();
+ assertChipIsEqual(
+ newChip,
+ hAlign,
+ colors,
+ expectedContDesc,
+ expectedMetadata,
+ expectedPrimaryText,
+ expectedLabel,
+ expectedIcon,
+ expectedCustomContent);
+ }
+
+ private void assertChipIsEqual(
+ @NonNull Chip actualChip,
+ @HorizontalAlignment int hAlign,
+ @NonNull ChipColors colors,
+ @Nullable String expectedContDesc,
+ @NonNull String expectedMetadata,
+ @Nullable String expectedPrimaryText,
+ @Nullable String expectedLabel,
+ @Nullable String expectedIcon,
+ @Nullable LayoutElement expectedCustomContent) {
+ assertThat(actualChip.getMetadataTag()).isEqualTo(expectedMetadata);
+ assertThat(actualChip.getClickable().toProto()).isEqualTo(CLICKABLE.toProto());
+ assertThat(actualChip.getWidth().toContainerDimensionProto())
+ .isEqualTo(EXPECTED_WIDTH.toContainerDimensionProto());
+ assertThat(actualChip.getHeight().toContainerDimensionProto())
+ .isEqualTo(ChipDefaults.DEFAULT_HEIGHT.toContainerDimensionProto());
+ assertThat(areChipColorsEqual(actualChip.getChipColors(), colors)).isTrue();
+ assertThat(actualChip.getHorizontalAlignment()).isEqualTo(hAlign);
+
+ if (expectedContDesc == null) {
+ assertThat(actualChip.getContentDescription()).isNull();
+ } else {
+ assertThat(actualChip.getContentDescription().toString()).isEqualTo(expectedContDesc);
+ }
+
+ if (expectedPrimaryText == null) {
+ assertThat(actualChip.getPrimaryLabelContent()).isNull();
+ } else {
+ assertThat(actualChip.getPrimaryLabelContent()).isEqualTo(expectedPrimaryText);
+ }
+
+ if (expectedLabel == null) {
+ assertThat(actualChip.getSecondaryLabelContent()).isNull();
+ } else {
+ assertThat(actualChip.getSecondaryLabelContent()).isEqualTo(expectedLabel);
+ }
+
+ if (expectedIcon == null) {
+ assertThat(actualChip.getIconContent()).isNull();
+ } else {
+ assertThat(actualChip.getIconContent()).isEqualTo(expectedIcon);
+ }
+
+ if (expectedCustomContent == null) {
+ assertThat(actualChip.getCustomContent()).isNull();
+ } else {
+ assertThat(actualChip.getCustomContent().toLayoutElementProto())
+ .isEqualTo(expectedCustomContent.toLayoutElementProto());
+ }
+ }
+}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/CircularProgressIndicatorTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/CircularProgressIndicatorTest.java
new file mode 100644
index 0000000..f956e06
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/CircularProgressIndicatorTest.java
@@ -0,0 +1,200 @@
+/*
+ * 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.wear.protolayout.material;
+
+import static androidx.wear.protolayout.material.ProgressIndicatorDefaults.DEFAULT_COLORS;
+import static androidx.wear.protolayout.material.ProgressIndicatorDefaults.DEFAULT_END_ANGLE;
+import static androidx.wear.protolayout.material.ProgressIndicatorDefaults.DEFAULT_START_ANGLE;
+import static androidx.wear.protolayout.material.ProgressIndicatorDefaults.DEFAULT_STROKE_WIDTH;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.graphics.Color;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.LayoutElementBuilders.Box;
+import androidx.wear.protolayout.LayoutElementBuilders.Column;
+import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata;
+import androidx.wear.protolayout.ModifiersBuilders.Modifiers;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+public class CircularProgressIndicatorTest {
+ @Test
+ public void testOpenRingIndicatorDefault() {
+ CircularProgressIndicator circularProgressIndicator =
+ new CircularProgressIndicator.Builder().build();
+
+ assertProgressIndicator(
+ circularProgressIndicator,
+ 0,
+ DEFAULT_START_ANGLE,
+ DEFAULT_END_ANGLE,
+ DEFAULT_COLORS,
+ DEFAULT_STROKE_WIDTH.getValue(),
+ null);
+ }
+
+ @Test
+ public void testProgressIndicatorCustom() {
+ float progress = 0.25f;
+ String contentDescription = "60 degrees progress";
+ ProgressIndicatorColors colors = new ProgressIndicatorColors(Color.YELLOW, Color.BLACK);
+ int thickness = 16;
+ float startAngle = -24;
+ float endAngle = 24;
+
+ CircularProgressIndicator circularProgressIndicator =
+ new CircularProgressIndicator.Builder()
+ .setProgress(progress)
+ .setStartAngle(startAngle)
+ .setEndAngle(endAngle)
+ .setCircularProgressIndicatorColors(colors)
+ .setStrokeWidth(thickness)
+ .setContentDescription(contentDescription)
+ .build();
+
+ assertProgressIndicator(
+ circularProgressIndicator,
+ progress,
+ startAngle,
+ endAngle,
+ colors,
+ thickness,
+ contentDescription);
+ }
+
+ @Test
+ public void testWrongElement() {
+ Column box = new Column.Builder().build();
+
+ assertThat(CircularProgressIndicator.fromLayoutElement(box)).isNull();
+ }
+
+ @Test
+ public void testWrongBox() {
+ Box box = new Box.Builder().build();
+
+ assertThat(CircularProgressIndicator.fromLayoutElement(box)).isNull();
+ }
+
+ @Test
+ public void testWrongTag() {
+ Box box =
+ new Box.Builder()
+ .setModifiers(
+ new Modifiers.Builder()
+ .setMetadata(
+ new ElementMetadata.Builder()
+ .setTagData("test".getBytes(UTF_8))
+ .build())
+ .build())
+ .build();
+
+ assertThat(CircularProgressIndicator.fromLayoutElement(box)).isNull();
+ }
+
+ private void assertProgressIndicator(
+ @NonNull CircularProgressIndicator actualCircularProgressIndicator,
+ float expectedProgress,
+ float expectedStartAngle,
+ float expectedEndAngle,
+ @NonNull ProgressIndicatorColors expectedColors,
+ float expectedThickness,
+ @Nullable String expectedContentDescription) {
+ assertProgressIndicatorIsEqual(
+ actualCircularProgressIndicator,
+ expectedProgress,
+ expectedStartAngle,
+ expectedEndAngle,
+ expectedColors,
+ expectedThickness,
+ expectedContentDescription);
+
+ Box box = new Box.Builder().addContent(actualCircularProgressIndicator).build();
+
+ CircularProgressIndicator newCpi =
+ CircularProgressIndicator.fromLayoutElement(box.getContents().get(0));
+
+ assertThat(newCpi).isNotNull();
+ assertProgressIndicatorIsEqual(
+ actualCircularProgressIndicator,
+ expectedProgress,
+ expectedStartAngle,
+ expectedEndAngle,
+ expectedColors,
+ expectedThickness,
+ expectedContentDescription);
+
+ assertThat(CircularProgressIndicator.fromLayoutElement(actualCircularProgressIndicator))
+ .isEqualTo(actualCircularProgressIndicator);
+ }
+
+ private void assertProgressIndicatorIsEqual(
+ @NonNull CircularProgressIndicator actualCircularProgressIndicator,
+ float expectedProgress,
+ float expectedStartAngle,
+ float expectedEndAngle,
+ @NonNull ProgressIndicatorColors expectedColors,
+ float expectedThickness,
+ @Nullable String expectedContentDescription) {
+ float total =
+ expectedEndAngle
+ + (expectedEndAngle <= expectedStartAngle ? 360 : 0)
+ - expectedStartAngle;
+ assertThat(actualCircularProgressIndicator.getMetadataTag())
+ .isEqualTo(CircularProgressIndicator.METADATA_TAG);
+ assertThat(actualCircularProgressIndicator.getProgress().getValue())
+ .isWithin(0.01f)
+ .of(expectedProgress * total);
+ assertThat(actualCircularProgressIndicator.getStartAngle().getValue())
+ .isWithin(0.01f)
+ .of(expectedStartAngle);
+ assertThat(actualCircularProgressIndicator.getEndAngle().getValue())
+ .isWithin(0.01f)
+ .of(expectedEndAngle);
+ assertThat(
+ actualCircularProgressIndicator
+ .getCircularProgressIndicatorColors()
+ .getIndicatorColor()
+ .getArgb())
+ .isEqualTo(expectedColors.getIndicatorColor().getArgb());
+ assertThat(
+ actualCircularProgressIndicator
+ .getCircularProgressIndicatorColors()
+ .getTrackColor()
+ .getArgb())
+ .isEqualTo(expectedColors.getTrackColor().getArgb());
+ assertThat(actualCircularProgressIndicator.getStrokeWidth().getValue())
+ .isEqualTo(expectedThickness);
+
+ if (expectedContentDescription == null) {
+ assertThat(actualCircularProgressIndicator.getContentDescription()).isNull();
+ } else {
+ assertThat(actualCircularProgressIndicator.getContentDescription().toString())
+ .isEqualTo(expectedContentDescription);
+ }
+ }
+}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/CompactChipTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/CompactChipTest.java
new file mode 100644
index 0000000..f3834d6
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/CompactChipTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.wear.protolayout.material;
+
+import static androidx.wear.protolayout.material.Utils.areChipColorsEqual;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.content.Context;
+import android.graphics.Color;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.ActionBuilders.LaunchAction;
+import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.protolayout.LayoutElementBuilders.Box;
+import androidx.wear.protolayout.LayoutElementBuilders.Column;
+import androidx.wear.protolayout.ModifiersBuilders.Clickable;
+import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata;
+import androidx.wear.protolayout.ModifiersBuilders.Modifiers;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+public class CompactChipTest {
+ private static final String MAIN_TEXT = "Action";
+ private static final Clickable CLICKABLE =
+ new Clickable.Builder()
+ .setOnClick(new LaunchAction.Builder().build())
+ .setId("action_id")
+ .build();
+ private static final DeviceParameters DEVICE_PARAMETERS =
+ new DeviceParameters.Builder().setScreenWidthDp(192).setScreenHeightDp(192).build();
+ private static final ChipColors COLORS = new ChipColors(Color.YELLOW, Color.BLUE);
+ private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
+
+ @Test
+ public void testCompactChipDefault() {
+ CompactChip compactChip =
+ new CompactChip.Builder(CONTEXT, MAIN_TEXT, CLICKABLE, DEVICE_PARAMETERS).build();
+
+ assertChip(compactChip, ChipDefaults.COMPACT_PRIMARY_COLORS);
+ }
+
+ @Test
+ public void testCompactChipCustomColor() {
+ CompactChip compactChip =
+ new CompactChip.Builder(CONTEXT, MAIN_TEXT, CLICKABLE, DEVICE_PARAMETERS)
+ .setChipColors(COLORS)
+ .build();
+
+ assertChip(compactChip, COLORS);
+ }
+
+ @Test
+ public void testWrongElement() {
+ Column box = new Column.Builder().build();
+
+ assertThat(CompactChip.fromLayoutElement(box)).isNull();
+ }
+
+ @Test
+ public void testWrongBox() {
+ Box box = new Box.Builder().build();
+
+ assertThat(CompactChip.fromLayoutElement(box)).isNull();
+ }
+
+ @Test
+ public void testWrongTag() {
+ Box box =
+ new Box.Builder()
+ .setModifiers(
+ new Modifiers.Builder()
+ .setMetadata(
+ new ElementMetadata.Builder()
+ .setTagData("test".getBytes(UTF_8))
+ .build())
+ .build())
+ .build();
+
+ assertThat(CompactChip.fromLayoutElement(box)).isNull();
+ }
+
+ private void assertChip(CompactChip actualCompactChip, ChipColors colors) {
+ assertChipIsEqual(actualCompactChip, colors);
+ assertFromLayoutElementChipIsEqual(actualCompactChip, colors);
+ assertThat(CompactChip.fromLayoutElement(actualCompactChip)).isEqualTo(actualCompactChip);
+ }
+
+ private void assertChipIsEqual(CompactChip actualCompactChip, ChipColors colors) {
+ assertThat(actualCompactChip.getMetadataTag()).isEqualTo(CompactChip.METADATA_TAG);
+ assertThat(actualCompactChip.getClickable().toProto()).isEqualTo(CLICKABLE.toProto());
+ assertThat(areChipColorsEqual(actualCompactChip.getChipColors(), colors)).isTrue();
+ assertThat(actualCompactChip.getText()).isEqualTo(MAIN_TEXT);
+ }
+
+ private void assertFromLayoutElementChipIsEqual(CompactChip chip, ChipColors colors) {
+ Box box = new Box.Builder().addContent(chip).build();
+
+ CompactChip newChip = CompactChip.fromLayoutElement(box.getContents().get(0));
+
+ assertThat(newChip).isNotNull();
+ assertChipIsEqual(newChip, colors);
+ }
+}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ProgressIndicatorColorsTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ProgressIndicatorColorsTest.java
new file mode 100644
index 0000000..485a4f1
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/ProgressIndicatorColorsTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.wear.protolayout.material;
+
+import static androidx.wear.protolayout.ColorBuilders.argb;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.ColorBuilders.ColorProp;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+public class ProgressIndicatorColorsTest {
+ private static final int ARGB_BACKGROUND_COLOR = 0x12345678;
+ private static final int ARGB_CONTENT_COLOR = 0x11223344;
+ private static final ColorProp TRACK_COLOR = argb(ARGB_BACKGROUND_COLOR);
+ private static final ColorProp INDICATOR_COLOR = argb(ARGB_CONTENT_COLOR);
+ private static final Colors COLORS = new Colors(0x123, 0x234, 0x345, 0x456);
+
+ @Test
+ public void testCreateProgressIndicatorColorsFromArgb() {
+ ProgressIndicatorColors progressIndicatorColors =
+ new ProgressIndicatorColors(ARGB_CONTENT_COLOR, ARGB_BACKGROUND_COLOR);
+
+ assertThat(progressIndicatorColors.getTrackColor().getArgb())
+ .isEqualTo(TRACK_COLOR.getArgb());
+ assertThat(progressIndicatorColors.getIndicatorColor().getArgb())
+ .isEqualTo(INDICATOR_COLOR.getArgb());
+ }
+
+ @Test
+ public void testCreateProgressIndicatorColorsFromColorProp() {
+ ProgressIndicatorColors progressIndicatorColors =
+ new ProgressIndicatorColors(INDICATOR_COLOR, TRACK_COLOR);
+
+ assertThat(progressIndicatorColors.getTrackColor().getArgb())
+ .isEqualTo(TRACK_COLOR.getArgb());
+ assertThat(progressIndicatorColors.getIndicatorColor().getArgb())
+ .isEqualTo(INDICATOR_COLOR.getArgb());
+ }
+
+ @Test
+ public void testCreateProgressIndicatorColorsFromHelper() {
+ ProgressIndicatorColors progressIndicatorColors =
+ ProgressIndicatorColors.progressIndicatorColors(COLORS);
+
+ assertThat(progressIndicatorColors.getTrackColor().getArgb())
+ .isEqualTo(COLORS.getSurface());
+ assertThat(progressIndicatorColors.getIndicatorColor().getArgb())
+ .isEqualTo(COLORS.getPrimary());
+ }
+}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/TextTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/TextTest.java
new file mode 100644
index 0000000..3f424d0
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/TextTest.java
@@ -0,0 +1,197 @@
+/*
+ * 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.wear.protolayout.material;
+
+import static androidx.wear.protolayout.ColorBuilders.argb;
+import static androidx.wear.protolayout.LayoutElementBuilders.FONT_WEIGHT_BOLD;
+import static androidx.wear.protolayout.LayoutElementBuilders.FONT_WEIGHT_MEDIUM;
+import static androidx.wear.protolayout.LayoutElementBuilders.FONT_WEIGHT_NORMAL;
+import static androidx.wear.protolayout.LayoutElementBuilders.TEXT_ALIGN_END;
+import static androidx.wear.protolayout.LayoutElementBuilders.TEXT_OVERFLOW_ELLIPSIZE_END;
+import static androidx.wear.protolayout.material.Typography.TYPOGRAPHY_BODY1;
+import static androidx.wear.protolayout.material.Typography.TYPOGRAPHY_CAPTION2;
+import static androidx.wear.protolayout.material.Typography.TYPOGRAPHY_TITLE1;
+import static androidx.wear.protolayout.material.Typography.getFontStyleBuilder;
+import static androidx.wear.protolayout.material.Typography.getLineHeightForTypography;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.content.Context;
+import android.graphics.Color;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.LayoutElementBuilders.Box;
+import androidx.wear.protolayout.LayoutElementBuilders.Column;
+import androidx.wear.protolayout.LayoutElementBuilders.FontStyle;
+import androidx.wear.protolayout.ModifiersBuilders.Background;
+import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata;
+import androidx.wear.protolayout.ModifiersBuilders.Modifiers;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+public class TextTest {
+
+ public static final int NUM_OF_FONT_STYLE_CONST = 12;
+ private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
+
+ @Test
+ public void testTypography_incorrectTypography_negativeValue() {
+ assertThrows(IllegalArgumentException.class, () -> getFontStyleBuilder(-1, CONTEXT));
+ }
+
+ @Test
+ public void testTypography_incorrectTypography_positiveValue() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> getFontStyleBuilder(NUM_OF_FONT_STYLE_CONST + 1, CONTEXT));
+ }
+
+ @Test
+ public void testLineHeight_incorrectTypography_negativeValue() {
+ assertThrows(IllegalArgumentException.class, () -> getLineHeightForTypography(-1));
+ }
+
+ @Test
+ public void testLineHeight_incorrectTypography_positiveValue() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> getLineHeightForTypography(NUM_OF_FONT_STYLE_CONST + 1));
+ }
+
+ @Test
+ public void testTypography_body1() {
+ FontStyle fontStyle = getFontStyleBuilder(TYPOGRAPHY_BODY1, CONTEXT).build();
+ assertFontStyle(fontStyle, 16, FONT_WEIGHT_NORMAL, 0.01f, 20, TYPOGRAPHY_BODY1);
+ }
+
+ @Test
+ public void testTypography_caption2() {
+ FontStyle fontStyle = getFontStyleBuilder(TYPOGRAPHY_CAPTION2, CONTEXT).build();
+ assertFontStyle(fontStyle, 12, FONT_WEIGHT_MEDIUM, 0.01f, 16, TYPOGRAPHY_CAPTION2);
+ }
+
+ @Test
+ public void testWrongElement() {
+ Column box = new Column.Builder().build();
+
+ assertThat(Text.fromLayoutElement(box)).isNull();
+ }
+
+ @Test
+ public void testWrongBox() {
+ Box box = new Box.Builder().build();
+
+ assertThat(Text.fromLayoutElement(box)).isNull();
+ }
+
+ @Test
+ public void testWrongTag() {
+ Box box =
+ new Box.Builder()
+ .setModifiers(
+ new Modifiers.Builder()
+ .setMetadata(
+ new ElementMetadata.Builder()
+ .setTagData("test".getBytes(UTF_8))
+ .build())
+ .build())
+ .build();
+
+ assertThat(Text.fromLayoutElement(box)).isNull();
+ }
+
+ @Test
+ public void testText() {
+ String textContent = "Testing text.";
+ Modifiers modifiers =
+ new Modifiers.Builder()
+ .setBackground(new Background.Builder().setColor(argb(Color.BLUE)).build())
+ .build();
+ int color = Color.YELLOW;
+ Text text =
+ new Text.Builder(CONTEXT, textContent)
+ .setItalic(true)
+ .setColor(argb(color))
+ .setTypography(TYPOGRAPHY_TITLE1)
+ .setUnderline(true)
+ .setMaxLines(2)
+ .setModifiers(modifiers)
+ .setOverflow(TEXT_OVERFLOW_ELLIPSIZE_END)
+ .setMultilineAlignment(TEXT_ALIGN_END)
+ .setWeight(FONT_WEIGHT_BOLD)
+ .build();
+
+ FontStyle expectedFontStyle =
+ getFontStyleBuilder(TYPOGRAPHY_TITLE1, CONTEXT)
+ .setItalic(true)
+ .setUnderline(true)
+ .setColor(argb(color))
+ .setWeight(FONT_WEIGHT_BOLD)
+ .build();
+
+ assertTextIsEqual(text, textContent, modifiers, color, expectedFontStyle);
+
+ Box box = new Box.Builder().addContent(text).build();
+ Text newText = Text.fromLayoutElement(box.getContents().get(0));
+ assertThat(newText).isNotNull();
+ assertTextIsEqual(newText, textContent, modifiers, color, expectedFontStyle);
+
+ assertThat(Text.fromLayoutElement(text)).isEqualTo(text);
+ }
+
+ private void assertTextIsEqual(
+ Text actualText,
+ String expectedTextContent,
+ Modifiers expectedModifiers,
+ int expectedColor,
+ FontStyle expectedFontStyle) {
+ assertThat(actualText.getFontStyle().toProto()).isEqualTo(expectedFontStyle.toProto());
+ assertThat(actualText.getText()).isEqualTo(expectedTextContent);
+ assertThat(actualText.getColor().getArgb()).isEqualTo(expectedColor);
+ assertThat(actualText.getOverflow()).isEqualTo(TEXT_OVERFLOW_ELLIPSIZE_END);
+ assertThat(actualText.getMultilineAlignment()).isEqualTo(TEXT_ALIGN_END);
+ assertThat(actualText.getMaxLines()).isEqualTo(2);
+ assertThat(actualText.getLineHeight())
+ .isEqualTo(getLineHeightForTypography(TYPOGRAPHY_TITLE1).getValue());
+ }
+
+ private void assertFontStyle(
+ FontStyle actualFontStyle,
+ int expectedSize,
+ int expectedWeight,
+ float expectedLetterSpacing,
+ float expectedLineHeight,
+ int typographyCode) {
+ assertThat(actualFontStyle.getSize()).isNotNull();
+ assertThat(actualFontStyle.getWeight()).isNotNull();
+ assertThat(actualFontStyle.getLetterSpacing()).isNotNull();
+ assertThat(actualFontStyle.getSize().getValue()).isEqualTo(expectedSize);
+ assertThat(actualFontStyle.getWeight().getValue()).isEqualTo(expectedWeight);
+ assertThat(actualFontStyle.getLetterSpacing().getValue()).isEqualTo(expectedLetterSpacing);
+ assertThat(getLineHeightForTypography(typographyCode).getValue())
+ .isEqualTo(expectedLineHeight);
+ }
+}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/TitleChipTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/TitleChipTest.java
new file mode 100644
index 0000000..408484f
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/TitleChipTest.java
@@ -0,0 +1,137 @@
+/*
+ * 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.wear.protolayout.material;
+
+import static androidx.wear.protolayout.DimensionBuilders.dp;
+import static androidx.wear.protolayout.material.Utils.areChipColorsEqual;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.content.Context;
+import android.graphics.Color;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.ActionBuilders.LaunchAction;
+import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.protolayout.DimensionBuilders.DpProp;
+import androidx.wear.protolayout.LayoutElementBuilders.Box;
+import androidx.wear.protolayout.LayoutElementBuilders.Column;
+import androidx.wear.protolayout.ModifiersBuilders.Clickable;
+import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata;
+import androidx.wear.protolayout.ModifiersBuilders.Modifiers;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+public class TitleChipTest {
+ private static final String MAIN_TEXT = "Action";
+ private static final Clickable CLICKABLE =
+ new Clickable.Builder()
+ .setOnClick(new LaunchAction.Builder().build())
+ .setId("action_id")
+ .build();
+ private static final DeviceParameters DEVICE_PARAMETERS =
+ new DeviceParameters.Builder().setScreenWidthDp(192).setScreenHeightDp(192).build();
+ private static final ChipColors COLORS = new ChipColors(Color.YELLOW, Color.BLUE);
+ private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
+ private static final DpProp EXPECTED_WIDTH =
+ dp(
+ DEVICE_PARAMETERS.getScreenWidthDp()
+ * (100 - 2 * ChipDefaults.DEFAULT_MARGIN_PERCENT)
+ / 100);
+
+ @Test
+ public void testTitleChipDefault() {
+ TitleChip titleChip =
+ new TitleChip.Builder(CONTEXT, MAIN_TEXT, CLICKABLE, DEVICE_PARAMETERS).build();
+
+ assertChip(titleChip, ChipDefaults.TITLE_PRIMARY_COLORS, EXPECTED_WIDTH);
+ }
+
+ @Test
+ public void testTitleChipCustom() {
+ DpProp width = dp(150);
+ TitleChip titleChip =
+ new TitleChip.Builder(CONTEXT, MAIN_TEXT, CLICKABLE, DEVICE_PARAMETERS)
+ .setChipColors(COLORS)
+ .setWidth(width)
+ .build();
+
+ assertChip(titleChip, COLORS, width);
+ }
+
+ @Test
+ public void testWrongElement() {
+ Column box = new Column.Builder().build();
+
+ assertThat(TitleChip.fromLayoutElement(box)).isNull();
+ }
+
+ @Test
+ public void testWrongBox() {
+ Box box = new Box.Builder().build();
+
+ assertThat(TitleChip.fromLayoutElement(box)).isNull();
+ }
+
+ @Test
+ public void testWrongTag() {
+ Box box =
+ new Box.Builder()
+ .setModifiers(
+ new Modifiers.Builder()
+ .setMetadata(
+ new ElementMetadata.Builder()
+ .setTagData("test".getBytes(UTF_8))
+ .build())
+ .build())
+ .build();
+
+ assertThat(TitleChip.fromLayoutElement(box)).isNull();
+ }
+
+ private void assertChip(TitleChip actualTitleChip, ChipColors colors, DpProp width) {
+ assertChipIsEqual(actualTitleChip, colors, width);
+ assertFromLayoutElementChipIsEqual(actualTitleChip, colors, width);
+ assertThat(TitleChip.fromLayoutElement(actualTitleChip)).isEqualTo(actualTitleChip);
+ }
+
+ private void assertChipIsEqual(TitleChip actualTitleChip, ChipColors colors, DpProp width) {
+ assertThat(actualTitleChip.getMetadataTag()).isEqualTo(TitleChip.METADATA_TAG);
+ assertThat(actualTitleChip.getClickable().toProto()).isEqualTo(CLICKABLE.toProto());
+ assertThat(actualTitleChip.getWidth().toContainerDimensionProto())
+ .isEqualTo(width.toContainerDimensionProto());
+ assertThat(areChipColorsEqual(actualTitleChip.getChipColors(), colors)).isTrue();
+ assertThat(actualTitleChip.getText()).isEqualTo(MAIN_TEXT);
+ }
+
+ private void assertFromLayoutElementChipIsEqual(
+ TitleChip chip, ChipColors colors, DpProp width) {
+ Box box = new Box.Builder().addContent(chip).build();
+
+ TitleChip newChip = TitleChip.fromLayoutElement(box.getContents().get(0));
+
+ assertThat(newChip).isNotNull();
+ assertChipIsEqual(newChip, colors, width);
+ }
+}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/Utils.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/Utils.java
new file mode 100644
index 0000000..363a204
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/Utils.java
@@ -0,0 +1,37 @@
+/*
+ * 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.wear.protolayout.material;
+
+import androidx.annotation.Dimension;
+
+public final class Utils {
+ /** Returns true if the given ChipColors have the same colored content. */
+ static boolean areChipColorsEqual(ChipColors colors1, ChipColors colors2) {
+ return colors1.getBackgroundColor().getArgb() == colors2.getBackgroundColor().getArgb()
+ && colors1.getContentColor().getArgb() == colors2.getContentColor().getArgb()
+ && colors1.getSecondaryContentColor().getArgb()
+ == colors2.getSecondaryContentColor().getArgb()
+ && colors1.getIconColor().getArgb() == colors2.getIconColor().getArgb();
+ }
+
+ @Dimension(unit = Dimension.DP)
+ public static int pxToDp(int px, float scale) {
+ return (int) ((px - 0.5f) / scale);
+ }
+
+ private Utils() {}
+}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/EdgeContentLayoutTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/EdgeContentLayoutTest.java
new file mode 100644
index 0000000..c673b23
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/EdgeContentLayoutTest.java
@@ -0,0 +1,227 @@
+/*
+ * 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.wear.protolayout.material.layouts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.protolayout.LayoutElementBuilders.Box;
+import androidx.wear.protolayout.LayoutElementBuilders.Column;
+import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
+import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata;
+import androidx.wear.protolayout.ModifiersBuilders.Modifiers;
+import androidx.wear.protolayout.material.CircularProgressIndicator;
+import androidx.wear.protolayout.material.Text;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+public class EdgeContentLayoutTest {
+ private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
+ private static final DeviceParameters DEVICE_PARAMETERS =
+ new DeviceParameters.Builder().setScreenWidthDp(192).setScreenHeightDp(192).build();
+ private static final Text PRIMARY_LABEL = new Text.Builder(CONTEXT, "Primary label").build();
+ private static final Text SECONDARY_LABEL =
+ new Text.Builder(CONTEXT, "Secondary label").build();
+
+ @Test
+ public void testAll() {
+ LayoutElement content = new Box.Builder().build();
+ CircularProgressIndicator progressIndicator =
+ new CircularProgressIndicator.Builder().build();
+ EdgeContentLayout layout =
+ new EdgeContentLayout.Builder(DEVICE_PARAMETERS)
+ .setContent(content)
+ .setEdgeContent(progressIndicator)
+ .setPrimaryLabelTextContent(PRIMARY_LABEL)
+ .setSecondaryLabelTextContent(SECONDARY_LABEL)
+ .build();
+
+ assertLayout(layout, progressIndicator, content, PRIMARY_LABEL, SECONDARY_LABEL);
+ }
+
+ @Test
+ public void testContentOnly() {
+ LayoutElement content = new Box.Builder().build();
+ EdgeContentLayout layout =
+ new EdgeContentLayout.Builder(DEVICE_PARAMETERS).setContent(content).build();
+
+ assertLayout(layout, null, content, null, null);
+ }
+
+ @Test
+ public void testIndicatorOnly() {
+ CircularProgressIndicator progressIndicator =
+ new CircularProgressIndicator.Builder().build();
+ EdgeContentLayout layout =
+ new EdgeContentLayout.Builder(DEVICE_PARAMETERS)
+ .setEdgeContent(progressIndicator)
+ .build();
+
+ assertLayout(layout, progressIndicator, null, null, null);
+ }
+
+ @Test
+ public void testEmpty() {
+ EdgeContentLayout layout = new EdgeContentLayout.Builder(DEVICE_PARAMETERS).build();
+
+ assertLayout(layout, null, null, null, null);
+ }
+
+ @Test
+ public void testWrongElement() {
+ Column box = new Column.Builder().build();
+
+ assertThat(EdgeContentLayout.fromLayoutElement(box)).isNull();
+ }
+
+ @Test
+ public void testWrongBox() {
+ Box box = new Box.Builder().build();
+
+ assertThat(EdgeContentLayout.fromLayoutElement(box)).isNull();
+ }
+
+ @Test
+ public void testWrongTag() {
+ Box box =
+ new Box.Builder()
+ .setModifiers(
+ new Modifiers.Builder()
+ .setMetadata(
+ new ElementMetadata.Builder()
+ .setTagData("test".getBytes(UTF_8))
+ .build())
+ .build())
+ .build();
+
+ assertThat(EdgeContentLayout.fromLayoutElement(box)).isNull();
+ }
+
+ @Test
+ public void testWrongLengthTag() {
+ Box box =
+ new Box.Builder()
+ .setModifiers(
+ new Modifiers.Builder()
+ .setMetadata(
+ new ElementMetadata.Builder()
+ .setTagData(
+ EdgeContentLayout
+ .METADATA_TAG_PREFIX
+ .getBytes(UTF_8))
+ .build())
+ .build())
+ .build();
+
+ assertThat(EdgeContentLayout.fromLayoutElement(box)).isNull();
+ }
+
+ private void assertLayout(
+ @NonNull EdgeContentLayout actualLayout,
+ @Nullable LayoutElement expectedProgressIndicator,
+ @Nullable LayoutElement expectedContent,
+ @Nullable LayoutElement expectedPrimaryLabel,
+ @Nullable LayoutElement expectedSecondaryLabel) {
+ assertLayoutIsEqual(
+ actualLayout,
+ expectedProgressIndicator,
+ expectedContent,
+ expectedPrimaryLabel,
+ expectedSecondaryLabel);
+
+ Box box = new Box.Builder().addContent(actualLayout).build();
+
+ EdgeContentLayout newLayout = EdgeContentLayout.fromLayoutElement(box.getContents().get(0));
+
+ assertThat(newLayout).isNotNull();
+ assertLayoutIsEqual(
+ newLayout,
+ expectedProgressIndicator,
+ expectedContent,
+ expectedPrimaryLabel,
+ expectedSecondaryLabel);
+
+ assertThat(EdgeContentLayout.fromLayoutElement(actualLayout)).isEqualTo(actualLayout);
+ }
+
+ private void assertLayoutIsEqual(
+ @NonNull EdgeContentLayout actualLayout,
+ @Nullable LayoutElement expectedProgressIndicator,
+ @Nullable LayoutElement expectedContent,
+ @Nullable LayoutElement expectedPrimaryLabel,
+ @Nullable LayoutElement expectedSecondaryLabel) {
+ byte[] expectedMetadata = EdgeContentLayout.METADATA_TAG_BASE.clone();
+
+ if (expectedProgressIndicator == null) {
+ assertThat(actualLayout.getEdgeContent()).isNull();
+ } else {
+ assertThat(actualLayout.getEdgeContent().toLayoutElementProto())
+ .isEqualTo(expectedProgressIndicator.toLayoutElementProto());
+ expectedMetadata[EdgeContentLayout.FLAG_INDEX] =
+ (byte)
+ (expectedMetadata[EdgeContentLayout.FLAG_INDEX]
+ | EdgeContentLayout.EDGE_CONTENT_PRESENT);
+ }
+
+ if (expectedContent == null) {
+ assertThat(actualLayout.getContent()).isNull();
+ } else {
+ assertThat(actualLayout.getContent().toLayoutElementProto())
+ .isEqualTo(expectedContent.toLayoutElementProto());
+ expectedMetadata[EdgeContentLayout.FLAG_INDEX] =
+ (byte)
+ (expectedMetadata[EdgeContentLayout.FLAG_INDEX]
+ | EdgeContentLayout.CONTENT_PRESENT);
+ }
+
+ if (expectedPrimaryLabel == null) {
+ assertThat(actualLayout.getPrimaryLabelTextContent()).isNull();
+ } else {
+ assertThat(actualLayout.getPrimaryLabelTextContent().toLayoutElementProto())
+ .isEqualTo(expectedPrimaryLabel.toLayoutElementProto());
+ expectedMetadata[EdgeContentLayout.FLAG_INDEX] =
+ (byte)
+ (expectedMetadata[EdgeContentLayout.FLAG_INDEX]
+ | EdgeContentLayout.PRIMARY_LABEL_PRESENT);
+ }
+
+ if (expectedSecondaryLabel == null) {
+ assertThat(actualLayout.getSecondaryLabelTextContent()).isNull();
+ } else {
+ assertThat(actualLayout.getSecondaryLabelTextContent().toLayoutElementProto())
+ .isEqualTo(expectedSecondaryLabel.toLayoutElementProto());
+ expectedMetadata[EdgeContentLayout.FLAG_INDEX] =
+ (byte)
+ (expectedMetadata[EdgeContentLayout.FLAG_INDEX]
+ | EdgeContentLayout.SECONDARY_LABEL_PRESENT);
+ }
+
+ assertThat(actualLayout.getMetadataTag()).isEqualTo(expectedMetadata);
+ }
+}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/MultiButtonLayoutTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/MultiButtonLayoutTest.java
new file mode 100644
index 0000000..085b61d
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/MultiButtonLayoutTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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.wear.protolayout.material.layouts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.ActionBuilders.LaunchAction;
+import androidx.wear.protolayout.LayoutElementBuilders.Box;
+import androidx.wear.protolayout.LayoutElementBuilders.Column;
+import androidx.wear.protolayout.ModifiersBuilders.Clickable;
+import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata;
+import androidx.wear.protolayout.ModifiersBuilders.Modifiers;
+import androidx.wear.protolayout.material.Button;
+import androidx.wear.protolayout.material.ButtonDefaults;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+public class MultiButtonLayoutTest {
+ private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
+ private static final Clickable CLICKABLE =
+ new Clickable.Builder()
+ .setOnClick(new LaunchAction.Builder().build())
+ .setId("action_id")
+ .build();
+
+ @Test
+ public void test_1button() {
+ Button button1 =
+ new Button.Builder(CONTEXT, CLICKABLE)
+ .setTextContent("1")
+ .setSize(ButtonDefaults.EXTRA_LARGE_SIZE)
+ .build();
+
+ MultiButtonLayout layout =
+ new MultiButtonLayout.Builder().addButtonContent(button1).build();
+
+ assertLayout(layout, Arrays.asList(button1));
+ }
+
+ @Test
+ public void test_2buttons() {
+ Button button1 = new Button.Builder(CONTEXT, CLICKABLE).setTextContent("1").build();
+ Button button2 = new Button.Builder(CONTEXT, CLICKABLE).setTextContent("2").build();
+
+ MultiButtonLayout layout =
+ new MultiButtonLayout.Builder()
+ .addButtonContent(button1)
+ .addButtonContent(button2)
+ .build();
+
+ assertLayout(layout, Arrays.asList(button1, button2));
+ }
+
+ @Test
+ public void test_5buttons() {
+ List<Button> buttons = new ArrayList<>();
+ int size = 5;
+ for (int i = 0; i < size; i++) {
+ buttons.add(new Button.Builder(CONTEXT, CLICKABLE).setTextContent("" + i).build());
+ }
+
+ MultiButtonLayout.Builder layoutBuilder = new MultiButtonLayout.Builder();
+ for (Button b : buttons) {
+ layoutBuilder.addButtonContent(b);
+ }
+ layoutBuilder.setFiveButtonDistribution(
+ MultiButtonLayout.FIVE_BUTTON_DISTRIBUTION_TOP_HEAVY);
+ MultiButtonLayout layout = layoutBuilder.build();
+
+ assertLayout(layout, buttons);
+ assertThat(layout.getFiveButtonDistribution())
+ .isEqualTo(MultiButtonLayout.FIVE_BUTTON_DISTRIBUTION_TOP_HEAVY);
+ }
+
+ @Test
+ public void test_too_many_button() {
+ Button button = new Button.Builder(CONTEXT, CLICKABLE).setTextContent("1").build();
+ MultiButtonLayout.Builder layoutBuilder = new MultiButtonLayout.Builder();
+ for (int i = 0; i < LayoutDefaults.MULTI_BUTTON_MAX_NUMBER + 1; i++) {
+ layoutBuilder.addButtonContent(button);
+ }
+
+ assertThrows(IllegalArgumentException.class, layoutBuilder::build);
+ }
+
+ @Test
+ public void testWrongElement() {
+ Column box = new Column.Builder().build();
+
+ assertThat(MultiButtonLayout.fromLayoutElement(box)).isNull();
+ }
+
+ @Test
+ public void testWrongBox() {
+ Box box = new Box.Builder().build();
+
+ assertThat(MultiButtonLayout.fromLayoutElement(box)).isNull();
+ }
+
+ @Test
+ public void testWrongTag() {
+ Box box =
+ new Box.Builder()
+ .setModifiers(
+ new Modifiers.Builder()
+ .setMetadata(
+ new ElementMetadata.Builder()
+ .setTagData("test".getBytes(UTF_8))
+ .build())
+ .build())
+ .build();
+
+ assertThat(MultiButtonLayout.fromLayoutElement(box)).isNull();
+ }
+
+ private void assertLayout(MultiButtonLayout actualLayout, List<Button> expectedButtons) {
+ assertLayoutIsEqual(actualLayout, expectedButtons);
+
+ Box box = new Box.Builder().addContent(actualLayout).build();
+
+ MultiButtonLayout newLayout = MultiButtonLayout.fromLayoutElement(box.getContents().get(0));
+
+ assertThat(newLayout).isNotNull();
+ assertLayoutIsEqual(newLayout, expectedButtons);
+
+ assertThat(MultiButtonLayout.fromLayoutElement(actualLayout)).isEqualTo(actualLayout);
+ }
+
+ private void assertLayoutIsEqual(MultiButtonLayout actualLayout, List<Button> expectedButtons) {
+ int size = expectedButtons.size();
+ assertThat(actualLayout.getMetadataTag()).isEqualTo(MultiButtonLayout.METADATA_TAG);
+ assertThat(actualLayout.getButtonContents()).hasSize(size);
+ for (int i = 0; i < size; i++) {
+ assertThat(actualLayout.getButtonContents().get(i).toLayoutElementProto())
+ .isEqualTo(expectedButtons.get(i).toLayoutElementProto());
+ }
+ }
+}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/MultiSlotLayoutTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/MultiSlotLayoutTest.java
new file mode 100644
index 0000000..db1e4c7
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/MultiSlotLayoutTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.wear.protolayout.material.layouts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.LayoutElementBuilders.Box;
+import androidx.wear.protolayout.LayoutElementBuilders.Column;
+import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
+import androidx.wear.protolayout.LayoutElementBuilders.Row;
+import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata;
+import androidx.wear.protolayout.ModifiersBuilders.Modifiers;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+public class MultiSlotLayoutTest {
+
+ @Test
+ public void test() {
+ LayoutElement content1 = new Box.Builder().build();
+ LayoutElement content2 = new Row.Builder().build();
+ float spacerWidth = 10f;
+
+ MultiSlotLayout layout =
+ new MultiSlotLayout.Builder()
+ .addSlotContent(content1)
+ .addSlotContent(content2)
+ .setHorizontalSpacerWidth(spacerWidth)
+ .build();
+
+ assertLayoutIsEqual(content1, content2, spacerWidth, layout);
+
+ Box box = new Box.Builder().addContent(layout).build();
+
+ MultiSlotLayout newLayout = MultiSlotLayout.fromLayoutElement(box.getContents().get(0));
+
+ assertThat(newLayout).isNotNull();
+ assertLayoutIsEqual(content1, content2, spacerWidth, newLayout);
+
+ assertThat(MultiSlotLayout.fromLayoutElement(layout)).isEqualTo(layout);
+ }
+
+ @Test
+ public void testWrongElement() {
+ Column box = new Column.Builder().build();
+
+ assertThat(MultiSlotLayout.fromLayoutElement(box)).isNull();
+ }
+
+ @Test
+ public void testWrongBox() {
+ Box box = new Box.Builder().build();
+
+ assertThat(MultiSlotLayout.fromLayoutElement(box)).isNull();
+ }
+
+ @Test
+ public void testWrongTag() {
+ Box box =
+ new Box.Builder()
+ .setModifiers(
+ new Modifiers.Builder()
+ .setMetadata(
+ new ElementMetadata.Builder()
+ .setTagData("test".getBytes(UTF_8))
+ .build())
+ .build())
+ .build();
+
+ assertThat(MultiSlotLayout.fromLayoutElement(box)).isNull();
+ }
+
+ private void assertLayoutIsEqual(
+ LayoutElement content1,
+ LayoutElement content2,
+ float spacerWidth,
+ MultiSlotLayout layout) {
+ assertThat(layout.getSlotContents()).hasSize(2);
+ assertThat(layout.getMetadataTag()).isEqualTo(MultiSlotLayout.METADATA_TAG);
+ assertThat(layout.getSlotContents().get(0).toLayoutElementProto())
+ .isEqualTo(content1.toLayoutElementProto());
+ assertThat(layout.getSlotContents().get(1).toLayoutElementProto())
+ .isEqualTo(content2.toLayoutElementProto());
+ assertThat(layout.getHorizontalSpacerWidth()).isEqualTo(spacerWidth);
+ }
+}
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/PrimaryLayoutTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/PrimaryLayoutTest.java
new file mode 100644
index 0000000..6669cda
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/PrimaryLayoutTest.java
@@ -0,0 +1,274 @@
+/*
+ * 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.wear.protolayout.material.layouts;
+
+import static androidx.wear.protolayout.material.layouts.LayoutDefaults.DEFAULT_VERTICAL_SPACER_HEIGHT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.ActionBuilders.LaunchAction;
+import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.protolayout.LayoutElementBuilders.Box;
+import androidx.wear.protolayout.LayoutElementBuilders.Column;
+import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
+import androidx.wear.protolayout.ModifiersBuilders.Clickable;
+import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata;
+import androidx.wear.protolayout.ModifiersBuilders.Modifiers;
+import androidx.wear.protolayout.material.CompactChip;
+import androidx.wear.protolayout.material.Text;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+public class PrimaryLayoutTest {
+ private static final Clickable CLICKABLE =
+ new Clickable.Builder()
+ .setOnClick(new LaunchAction.Builder().build())
+ .setId("action_id")
+ .build();
+ private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
+ private static final DeviceParameters DEVICE_PARAMETERS =
+ new DeviceParameters.Builder().setScreenWidthDp(192).setScreenHeightDp(192).build();
+ private static final LayoutElement CONTENT = new Box.Builder().build();
+ private static final CompactChip PRIMARY_CHIP =
+ new CompactChip.Builder(CONTEXT, "Compact", CLICKABLE, DEVICE_PARAMETERS).build();
+ private static final Text PRIMARY_LABEL = new Text.Builder(CONTEXT, "Primary label").build();
+ private static final Text SECONDARY_LABEL =
+ new Text.Builder(CONTEXT, "Secondary label").build();
+
+ @Test
+ public void testOnlyContent() {
+ PrimaryLayout layout =
+ new PrimaryLayout.Builder(DEVICE_PARAMETERS).setContent(CONTENT).build();
+
+ assertLayout(DEFAULT_VERTICAL_SPACER_HEIGHT.getValue(), layout, CONTENT, null, null, null);
+ }
+
+ @Test
+ public void testContentChip() {
+ PrimaryLayout layout =
+ new PrimaryLayout.Builder(DEVICE_PARAMETERS)
+ .setContent(CONTENT)
+ .setPrimaryChipContent(PRIMARY_CHIP)
+ .build();
+
+ assertLayout(
+ DEFAULT_VERTICAL_SPACER_HEIGHT.getValue(),
+ layout,
+ CONTENT,
+ PRIMARY_CHIP,
+ null,
+ null);
+ }
+
+ @Test
+ public void testContentPrimaryLabel() {
+ PrimaryLayout layout =
+ new PrimaryLayout.Builder(DEVICE_PARAMETERS)
+ .setContent(CONTENT)
+ .setPrimaryLabelTextContent(PRIMARY_LABEL)
+ .build();
+
+ assertLayout(
+ DEFAULT_VERTICAL_SPACER_HEIGHT.getValue(),
+ layout,
+ CONTENT,
+ null,
+ PRIMARY_LABEL,
+ null);
+ }
+
+ @Test
+ public void testContentSecondaryLabel() {
+ PrimaryLayout layout =
+ new PrimaryLayout.Builder(DEVICE_PARAMETERS)
+ .setContent(CONTENT)
+ .setSecondaryLabelTextContent(SECONDARY_LABEL)
+ .build();
+
+ assertLayout(
+ DEFAULT_VERTICAL_SPACER_HEIGHT.getValue(),
+ layout,
+ CONTENT,
+ null,
+ null,
+ SECONDARY_LABEL);
+ }
+
+ @Test
+ public void testAll() {
+ float height = 12;
+ PrimaryLayout layout =
+ new PrimaryLayout.Builder(DEVICE_PARAMETERS)
+ .setContent(CONTENT)
+ .setPrimaryChipContent(PRIMARY_CHIP)
+ .setPrimaryLabelTextContent(PRIMARY_LABEL)
+ .setSecondaryLabelTextContent(SECONDARY_LABEL)
+ .setVerticalSpacerHeight(height)
+ .build();
+
+ assertLayout(height, layout, CONTENT, PRIMARY_CHIP, PRIMARY_LABEL, SECONDARY_LABEL);
+ }
+
+ @Test
+ public void testWrongElement() {
+ Column box = new Column.Builder().build();
+
+ assertThat(PrimaryLayout.fromLayoutElement(box)).isNull();
+ }
+
+ @Test
+ public void testWrongBox() {
+ Box box = new Box.Builder().build();
+
+ assertThat(PrimaryLayout.fromLayoutElement(box)).isNull();
+ }
+
+ @Test
+ public void testWrongTag() {
+ Box box =
+ new Box.Builder()
+ .setModifiers(
+ new Modifiers.Builder()
+ .setMetadata(
+ new ElementMetadata.Builder()
+ .setTagData("test".getBytes(UTF_8))
+ .build())
+ .build())
+ .build();
+
+ assertThat(PrimaryLayout.fromLayoutElement(box)).isNull();
+ }
+
+ @Test
+ public void testWrongLengthTag() {
+ Box box =
+ new Box.Builder()
+ .setModifiers(
+ new Modifiers.Builder()
+ .setMetadata(
+ new ElementMetadata.Builder()
+ .setTagData(
+ PrimaryLayout.METADATA_TAG_PREFIX
+ .getBytes(UTF_8))
+ .build())
+ .build())
+ .build();
+
+ assertThat(PrimaryLayout.fromLayoutElement(box)).isNull();
+ }
+
+ private void assertLayout(
+ float height,
+ @NonNull PrimaryLayout actualLayout,
+ @Nullable LayoutElement expectedContent,
+ @Nullable LayoutElement expectedPrimaryChip,
+ @Nullable LayoutElement expectedPrimaryLabel,
+ @Nullable LayoutElement expectedSecondaryLabel) {
+ assertLayoutIsEqual(
+ height,
+ actualLayout,
+ expectedContent,
+ expectedPrimaryChip,
+ expectedPrimaryLabel,
+ expectedSecondaryLabel);
+
+ Box box = new Box.Builder().addContent(actualLayout).build();
+
+ PrimaryLayout newLayout = PrimaryLayout.fromLayoutElement(box.getContents().get(0));
+
+ assertThat(newLayout).isNotNull();
+ assertLayoutIsEqual(
+ height,
+ newLayout,
+ expectedContent,
+ expectedPrimaryChip,
+ expectedPrimaryLabel,
+ expectedSecondaryLabel);
+
+ assertThat(PrimaryLayout.fromLayoutElement(actualLayout)).isEqualTo(actualLayout);
+ }
+
+ private void assertLayoutIsEqual(
+ float height,
+ @NonNull PrimaryLayout actualLayout,
+ @Nullable LayoutElement expectedContent,
+ @Nullable LayoutElement expectedPrimaryChip,
+ @Nullable LayoutElement expectedPrimaryLabel,
+ @Nullable LayoutElement expectedSecondaryLabel) {
+ byte[] expectedMetadata = PrimaryLayout.METADATA_TAG_BASE.clone();
+
+ if (expectedContent == null) {
+ assertThat(actualLayout.getContent()).isNull();
+ } else {
+ assertThat(actualLayout.getContent().toLayoutElementProto())
+ .isEqualTo(expectedContent.toLayoutElementProto());
+ expectedMetadata[PrimaryLayout.FLAG_INDEX] =
+ (byte)
+ (expectedMetadata[PrimaryLayout.FLAG_INDEX]
+ | PrimaryLayout.CONTENT_PRESENT);
+ }
+
+ if (expectedPrimaryChip == null) {
+ assertThat(actualLayout.getPrimaryChipContent()).isNull();
+ } else {
+ assertThat(actualLayout.getPrimaryChipContent().toLayoutElementProto())
+ .isEqualTo(expectedPrimaryChip.toLayoutElementProto());
+ expectedMetadata[PrimaryLayout.FLAG_INDEX] =
+ (byte)
+ (expectedMetadata[PrimaryLayout.FLAG_INDEX]
+ | PrimaryLayout.CHIP_PRESENT);
+ }
+
+ assertThat(actualLayout.getVerticalSpacerHeight()).isEqualTo(height);
+
+ if (expectedPrimaryLabel == null) {
+ assertThat(actualLayout.getPrimaryLabelTextContent()).isNull();
+ } else {
+ assertThat(actualLayout.getPrimaryLabelTextContent().toLayoutElementProto())
+ .isEqualTo(expectedPrimaryLabel.toLayoutElementProto());
+ expectedMetadata[PrimaryLayout.FLAG_INDEX] =
+ (byte)
+ (expectedMetadata[PrimaryLayout.FLAG_INDEX]
+ | PrimaryLayout.PRIMARY_LABEL_PRESENT);
+ }
+
+ if (expectedSecondaryLabel == null) {
+ assertThat(actualLayout.getSecondaryLabelTextContent()).isNull();
+ } else {
+ assertThat(actualLayout.getSecondaryLabelTextContent().toLayoutElementProto())
+ .isEqualTo(expectedSecondaryLabel.toLayoutElementProto());
+ expectedMetadata[PrimaryLayout.FLAG_INDEX] =
+ (byte)
+ (expectedMetadata[PrimaryLayout.FLAG_INDEX]
+ | PrimaryLayout.SECONDARY_LABEL_PRESENT);
+ }
+
+ assertThat(actualLayout.getMetadataTag()).isEqualTo(expectedMetadata);
+ }
+}
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
index a4cb503..f496e85 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
@@ -29,12 +29,10 @@
import androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway
import java.util.concurrent.Executor
import kotlin.coroutines.ContinuationInterceptor
-import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asCoroutineDispatcher
-import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -96,9 +94,10 @@
/**
* @see ComplicationDataExpressionEvaluator.init, [executor] is used in place of
- * `coroutineScope`.
+ * `coroutineScope`, and defaults to an immediate main-thread [Executor].
*/
- fun init(executor: Executor) {
+ @JvmOverloads
+ fun init(executor: Executor = CoroutineScope(Dispatchers.Main.immediate).asExecutor()) {
evaluator.init(CoroutineScope(executor.asCoroutineDispatcher()))
evaluator.data
.filterNotNull()
@@ -129,14 +128,12 @@
*
* This needs to be called exactly once.
*
- * @param coroutineScope used for background evaluation
+ * @param coroutineScope used for background evaluation, must be single threaded
*/
- fun init(coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Main)) {
+ fun init(coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Main.immediate)) {
// Add all the receivers before we start binding them because binding can synchronously
// trigger the receiver, which would update the data before all the fields are evaluated.
- initStateReceivers(coroutineScope)
- initEvaluator()
- monitorState(coroutineScope)
+ Initializer(coroutineScope).init()
}
/**
@@ -149,115 +146,115 @@
if (this::evaluator.isInitialized) evaluator.close()
}
- /** Adds [ComplicationEvaluationResultReceiver]s to [state]. */
- private fun initStateReceivers(coroutineScope: CoroutineScope) {
- val receivers = mutableSetOf<ComplicationEvaluationResultReceiver<out Any>>()
-
- if (unevaluatedData.hasRangedValueExpression()) {
- unevaluatedData.rangedValueExpression
- ?.buildReceiver(
- coroutineScope,
- expressionTrimmer = { setRangedValueExpression(null) },
- setter = { setRangedValue(it) },
- )
- ?.let { receivers += it }
- }
- if (unevaluatedData.hasLongText()) {
- unevaluatedData.longText
- ?.buildReceiver(coroutineScope) { setLongText(it) }
- ?.let { receivers += it }
- }
- if (unevaluatedData.hasLongTitle()) {
- unevaluatedData.longTitle
- ?.buildReceiver(coroutineScope) { setLongTitle(it) }
- ?.let { receivers += it }
- }
- if (unevaluatedData.hasShortText()) {
- unevaluatedData.shortText
- ?.buildReceiver(coroutineScope) { setShortText(it) }
- ?.let { receivers += it }
- }
- if (unevaluatedData.hasShortTitle()) {
- unevaluatedData.shortTitle
- ?.buildReceiver(coroutineScope) { setShortTitle(it) }
- ?.let { receivers += it }
- }
- if (unevaluatedData.hasContentDescription()) {
- unevaluatedData.contentDescription
- ?.buildReceiver(coroutineScope) { setContentDescription(it) }
- ?.let { receivers += it }
+ private inner class Initializer(val coroutineScope: CoroutineScope) {
+ fun init() {
+ initStateReceivers()
+ initEvaluator()
+ monitorState()
}
- state.value = State(unevaluatedData, receivers)
- }
+ /** Adds [ComplicationEvaluationResultReceiver]s to [state]. */
+ private fun initStateReceivers() {
+ val receivers = mutableSetOf<ComplicationEvaluationResultReceiver<out Any>>()
- private fun DynamicFloat.buildReceiver(
- coroutineScope: CoroutineScope,
- expressionTrimmer: WireComplicationData.Builder.() -> WireComplicationData.Builder,
- setter: WireComplicationData.Builder.(Float) -> WireComplicationData.Builder,
- ) =
- ComplicationEvaluationResultReceiver(
- setter = {
- if (!keepExpression) expressionTrimmer(this)
- setter(this, it)
- },
- binder = { receiver ->
- evaluator.bind(
- this@buildReceiver,
- coroutineScope.coroutineContext.asExecutor(),
- receiver
- )
- },
- )
-
- private fun WireComplicationText.buildReceiver(
- coroutineScope: CoroutineScope,
- setter: WireComplicationData.Builder.(WireComplicationText) -> WireComplicationData.Builder,
- ) =
- expression?.let { expression ->
- ComplicationEvaluationResultReceiver<String>(
- setter = {
- setter(
- if (keepExpression) {
- WireComplicationText(it, expression)
- } else {
- WireComplicationText(it)
- }
+ if (unevaluatedData.hasRangedValueExpression()) {
+ unevaluatedData.rangedValueExpression
+ ?.buildReceiver(
+ expressionTrimmer = { setRangedValueExpression(null) },
+ setter = { setRangedValue(it) },
)
+ ?.let { receivers += it }
+ }
+ if (unevaluatedData.hasLongText()) {
+ unevaluatedData.longText?.buildReceiver { setLongText(it) }?.let { receivers += it }
+ }
+ if (unevaluatedData.hasLongTitle()) {
+ unevaluatedData.longTitle
+ ?.buildReceiver { setLongTitle(it) }
+ ?.let { receivers += it }
+ }
+ if (unevaluatedData.hasShortText()) {
+ unevaluatedData.shortText
+ ?.buildReceiver { setShortText(it) }
+ ?.let { receivers += it }
+ }
+ if (unevaluatedData.hasShortTitle()) {
+ unevaluatedData.shortTitle
+ ?.buildReceiver { setShortTitle(it) }
+ ?.let { receivers += it }
+ }
+ if (unevaluatedData.hasContentDescription()) {
+ unevaluatedData.contentDescription
+ ?.buildReceiver { setContentDescription(it) }
+ ?.let { receivers += it }
+ }
+
+ state.value = State(unevaluatedData, receivers)
+ }
+
+ private fun DynamicFloat.buildReceiver(
+ expressionTrimmer: WireComplicationData.Builder.() -> WireComplicationData.Builder,
+ setter: WireComplicationData.Builder.(Float) -> WireComplicationData.Builder,
+ ) =
+ ComplicationEvaluationResultReceiver(
+ setter = {
+ if (!keepExpression) expressionTrimmer(this)
+ setter(this, it)
},
binder = { receiver ->
- evaluator.bind(
- expression,
- ULocale.getDefault(),
- coroutineScope.coroutineContext.asExecutor(),
- receiver
- )
+ evaluator.bind(this@buildReceiver, coroutineScope.asExecutor(), receiver)
},
)
+
+ private fun WireComplicationText.buildReceiver(
+ setter:
+ WireComplicationData.Builder.(WireComplicationText) -> WireComplicationData.Builder,
+ ) =
+ expression?.let { expression ->
+ ComplicationEvaluationResultReceiver<String>(
+ setter = {
+ setter(
+ if (keepExpression) {
+ WireComplicationText(it, expression)
+ } else {
+ WireComplicationText(it)
+ }
+ )
+ },
+ binder = { receiver ->
+ evaluator.bind(
+ expression,
+ ULocale.getDefault(),
+ coroutineScope.asExecutor(),
+ receiver
+ )
+ },
+ )
+ }
+
+ /** Initializes the internal [DynamicTypeEvaluator] if there are pending receivers. */
+ private fun initEvaluator() {
+ if (state.value.pending.isEmpty()) return
+ evaluator =
+ DynamicTypeEvaluator(
+ /* platformDataSourcesInitiallyEnabled = */ true,
+ sensorGateway,
+ stateStore,
+ )
+ for (receiver in state.value.pending) receiver.bind()
+ for (receiver in state.value.pending) receiver.startEvaluation()
+ evaluator.enablePlatformDataSources()
}
- /** Initializes the internal [DynamicTypeEvaluator] if there are pending receivers. */
- private fun initEvaluator() {
- if (state.value.pending.isEmpty()) return
- evaluator =
- DynamicTypeEvaluator(
- /* platformDataSourcesInitiallyEnabled = */ true,
- sensorGateway,
- stateStore,
- )
- for (receiver in state.value.pending) receiver.bind()
- for (receiver in state.value.pending) receiver.startEvaluation()
- evaluator.enablePlatformDataSources()
- }
-
- /** Monitors [state] changes and updates [data]. */
- private fun monitorState(coroutineScope: CoroutineScope) {
- state
- .onEach {
- if (it.invalid.isNotEmpty()) _data.value = INVALID_DATA
- else if (it.pending.isEmpty() && it.preUpdateCount == 0) _data.value = it.data
- }
- .launchIn(coroutineScope)
+ /** Monitors [state] changes and updates [data]. */
+ private fun monitorState() {
+ state
+ .onEach {
+ if (it.invalid.isNotEmpty()) _data.value = INVALID_DATA
+ else if (it.pending.isEmpty() && it.preUpdateCount == 0) _data.value = it.data
+ }
+ .launchIn(coroutineScope)
+ }
}
/**
@@ -359,5 +356,15 @@
}
}
-internal fun CoroutineContext.asExecutor(): Executor =
- (get(ContinuationInterceptor) as CoroutineDispatcher).asExecutor()
+/**
+ * Replacement for CoroutineDispatcher.asExecutor extension due to
+ * https://github.com/Kotlin/kotlinx.coroutines/pull/3683.
+ */
+internal fun CoroutineScope.asExecutor() = Executor { runnable ->
+ val dispatcher = coroutineContext[ContinuationInterceptor] as CoroutineDispatcher
+ if (dispatcher.isDispatchNeeded(coroutineContext)) {
+ dispatcher.dispatch(coroutineContext, runnable)
+ } else {
+ runnable.run()
+ }
+}
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
index 279a8b0..1ee050f 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
@@ -16,12 +16,12 @@
package androidx.wear.watchface.complications.data
+import android.os.Handler
+import android.os.Looper
import android.support.wearable.complications.ComplicationData as WireComplicationData
import android.support.wearable.complications.ComplicationText as WireComplicationText
import android.util.Log
-import androidx.core.content.ContextCompat
import androidx.core.util.Consumer
-import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString
import androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue
@@ -30,6 +30,7 @@
import androidx.wear.watchface.complications.data.ComplicationDataExpressionEvaluator.Companion.hasExpression
import com.google.common.truth.Expect
import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
@@ -45,7 +46,6 @@
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.robolectric.shadows.ShadowLog
-import org.robolectric.shadows.ShadowLooper.runUiThreadTasks
@RunWith(SharedRobolectricTestRunner::class)
class ComplicationDataExpressionEvaluatorTest {
@@ -69,7 +69,6 @@
fun data_noExpression_setToUnevaluated() {
ComplicationDataExpressionEvaluator(DATA_WITH_NO_EXPRESSION).use { evaluator ->
evaluator.init()
- runUiThreadTasks()
assertThat(evaluator.data.value).isEqualTo(DATA_WITH_NO_EXPRESSION)
}
@@ -219,16 +218,14 @@
evaluator.data
.filterNotNull()
.shareIn(
- CoroutineScope(Dispatchers.Main),
+ CoroutineScope(Dispatchers.Main.immediate),
SharingStarted.Eagerly,
replay = 10,
)
evaluator.init()
- runUiThreadTasks() // Ensures data sharing started.
for (state in scenario.states) {
stateStore.setStateEntryValues(state)
- runUiThreadTasks() // Ensures data sharing ended.
}
expect
@@ -241,6 +238,17 @@
@Test
fun data_keepExpression_doesNotTrimUnevaluatedExpression() {
+ class MainExecutor : Executor {
+ private val handler = Handler(Looper.getMainLooper())
+
+ override fun execute(runnable: Runnable) {
+ if (handler.looper == Looper.myLooper()) {
+ runnable.run()
+ } else {
+ handler.post(runnable)
+ }
+ }
+ }
val expressed =
WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setRangedValueExpression(DynamicFloat.constant(1f))
@@ -252,7 +260,6 @@
.build()
ComplicationDataExpressionEvaluator(expressed, keepExpression = true).use { evaluator ->
evaluator.init()
- runUiThreadTasks()
assertThat(evaluator.data.value)
.isEqualTo(
@@ -334,11 +341,7 @@
fun compat_notInitialized_listenerNotInvoked() {
ComplicationDataExpressionEvaluator.Compat.Builder(DATA_WITH_NO_EXPRESSION, listener)
.build()
- .use {
- runUiThreadTasks()
-
- verify(listener, never()).accept(any())
- }
+ .use { verify(listener, never()).accept(any()) }
}
@Test
@@ -346,13 +349,31 @@
ComplicationDataExpressionEvaluator.Compat.Builder(DATA_WITH_NO_EXPRESSION, listener)
.build()
.use { evaluator ->
- evaluator.init(ContextCompat.getMainExecutor(getApplicationContext()))
- runUiThreadTasks()
+ evaluator.init()
verify(listener, times(1)).accept(DATA_WITH_NO_EXPRESSION)
}
}
+ @Test
+ fun compat_expression_listenerInvokedWithEvaluatedData() {
+ val data =
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ .setRangedValueExpression(DynamicFloat.constant(1f))
+ .build()
+ ComplicationDataExpressionEvaluator.Compat.Builder(data, listener).build().use { evaluator
+ ->
+ evaluator.init()
+
+ verify(listener, times(1))
+ .accept(
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ .setRangedValue(1f)
+ .build()
+ )
+ }
+ }
+
private companion object {
val DATA_WITH_NO_EXPRESSION =
WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
diff --git a/window/extensions/extensions/build.gradle b/window/extensions/extensions/build.gradle
index 6d72848..bccb7ae 100644
--- a/window/extensions/extensions/build.gradle
+++ b/window/extensions/extensions/build.gradle
@@ -26,7 +26,7 @@
api(libs.kotlinStdlib)
implementation("androidx.annotation:annotation:1.3.0")
implementation("androidx.annotation:annotation-experimental:1.1.0")
- implementation(project(":window:extensions:core:core"))
+ implementation("androidx.window.extensions.core:core:1.0.0-beta01")
testImplementation(libs.robolectric)
testImplementation(libs.testExtJunit)
diff --git a/window/window/build.gradle b/window/window/build.gradle
index 23e165d..32d38d8 100644
--- a/window/window/build.gradle
+++ b/window/window/build.gradle
@@ -48,10 +48,10 @@
implementation("androidx.annotation:annotation:1.3.0")
implementation("androidx.collection:collection:1.1.0")
implementation("androidx.core:core:1.8.0")
- implementation(project(":window:extensions:core:core"))
+ implementation("androidx.window.extensions.core:core:1.0.0-beta01")
compileOnly(project(":window:sidecar:sidecar"))
- compileOnly(project(":window:extensions:extensions"))
+ compileOnly("androidx.window.extensions:extensions:1.1.0-beta01")
testImplementation(libs.testCore)
testImplementation(libs.testRunner)
@@ -61,9 +61,9 @@
testImplementation(libs.mockitoCore4)
testImplementation(libs.mockitoKotlin4)
testImplementation(libs.kotlinCoroutinesTest)
+ testImplementation("androidx.window.extensions.core:core:1.0.0-beta01")
testImplementation(compileOnly(project(":window:sidecar:sidecar")))
- testImplementation(compileOnly(project(":window:extensions:extensions")))
- testImplementation(implementation(project(":window:extensions:core:core")))
+ testImplementation(compileOnly("androidx.window.extensions:extensions:1.1.0-beta01"))
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.kotlinTestJunit)
@@ -77,9 +77,9 @@
androidTestImplementation(libs.multidex)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.junit) // Needed for Assert.assertThrows
- androidTestImplementation(compileOnly(project(":window:extensions:extensions")))
+ androidTestImplementation("androidx.window.extensions.core:core:1.0.0-beta01")
androidTestImplementation(compileOnly(project(":window:sidecar:sidecar")))
- androidTestImplementation(implementation(project(":window:extensions:core:core")))
+ androidTestImplementation(compileOnly("androidx.window.extensions:extensions:1.1.0-beta01"))
}
androidx {