Merge "Move XClassName into a separate file" into androidx-main
diff --git a/.gitignore b/.gitignore
index 9b7bb83..ee07b90 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@
!.idea/copyright/AndroidCopyright.xml
!.idea/copyright/profiles_settings.xml
!.idea/inspectionProfiles/Project_Default.xml
+!.idea/ktfmt.xml
.project
.settings/
project.properties
diff --git a/.idea/ktfmt.xml b/.idea/ktfmt.xml
new file mode 100644
index 0000000..a2d5a3a
--- /dev/null
+++ b/.idea/ktfmt.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="KtfmtSettings">
+ <option name="enableKtfmt" value="Enabled" />
+ <option name="enabled" value="true" />
+ <option name="uiFormatterStyle" value="Kotlinlang" />
+ </component>
+</project>
\ No newline at end of file
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioTask.kt
index ede03af..fe26e2c 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioTask.kt
@@ -26,10 +26,13 @@
import java.io.File
import java.nio.file.Files
import java.nio.file.Paths
+import java.security.MessageDigest
import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.Project
+import org.gradle.api.file.ArchiveOperations
+import org.gradle.api.file.FileSystemOperations
import org.gradle.api.internal.tasks.userinput.UserInputHandler
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
@@ -59,6 +62,7 @@
fun studiow() {
validateEnvironment()
install()
+ installKtfmtPlugin()
launch()
}
@@ -66,8 +70,12 @@
StudioPlatformUtilities.get(projectRoot, studioInstallationDir)
}
+ @get:Inject abstract val archiveOperations: ArchiveOperations
+
@get:Inject abstract val execOperations: ExecOperations
+ @get:Inject abstract val fileSystemOperations: FileSystemOperations
+
/**
* If `true`, checks for `ANDROIDX_PROJECTS` environment variable to decide which projects need
* to be loaded.
@@ -106,6 +114,29 @@
File(studioInstallationDir.parentFile, studioArchiveName).absolutePath
}
+ /** Directory where Studio downloads plugins to */
+ private val studioPluginDir =
+ File(System.getenv("HOME"), ".AndroidStudioAndroidX/config/plugins").also { it.mkdirs() }
+
+ private val studioKtfmtPluginVersion by lazy { project.getVersionByName("ktfmtIdeaPlugin") }
+
+ /**
+ * This ID changes for each ktfmt plugin version; see
+ * https://plugins.jetbrains.com/plugin/14912-ktfmt/versions/stable and you'll see the number in
+ * the redirection URL when hovering over the [studioKtfmtPluginVersion] you want downloaded
+ */
+ private val studioKtfmtPluginId = "553364"
+
+ private val studioKtfmtPluginDownloadUrl =
+ "https://downloads.marketplace.jetbrains.com/files/14912/$studioKtfmtPluginId/ktfmt_idea_plugin-$studioKtfmtPluginVersion.zip"
+
+ /** Storage location for the ktfmt plugin zip file */
+ private val studioKtfmtPluginZip = File(studioPluginDir, "ktfmt-$studioKtfmtPluginVersion.zip")
+
+ /** Download ktfmt plugin zip file and run `shasum -a 256 ./path/to/zip` to get checksum */
+ private val studioKtfmtPluginChecksum =
+ "79602c7fa94a23df7ca5c06effd50b180bc6518396488e20662f8d5d52b323db"
+
/** The idea.properties file that we want to tell Studio to use */
@get:Internal protected abstract val ideaProperties: File
@@ -159,6 +190,40 @@
}
}
+ private fun installKtfmtPlugin() {
+ // TODO: When upgrading to ktfmt_idea_plugin 1.2.x.x, remove the `instrumented-` prefix from
+ // the plugin jar name
+ if (
+ File(
+ studioPluginDir,
+ "ktfmt_idea_plugin/lib/instrumented-ktfmt_idea_plugin-$studioKtfmtPluginVersion.jar"
+ )
+ .exists()
+ ) {
+ return
+ } else {
+ File(studioPluginDir, "ktfmt_idea_plugin").deleteRecursively()
+ }
+
+ println("Downloading ktfmt plugin from $studioKtfmtPluginDownloadUrl")
+ execOperations.exec { execSpec ->
+ with(execSpec) {
+ executable("curl")
+ args(studioKtfmtPluginDownloadUrl, "--output", studioKtfmtPluginZip.absolutePath)
+ }
+ }
+
+ studioKtfmtPluginZip.verifyChecksum()
+
+ println("Installing ktfmt plugin into ${studioPluginDir.absolutePath}")
+ fileSystemOperations.copy {
+ it.from(archiveOperations.zipTree(studioKtfmtPluginZip))
+ it.into(studioPluginDir)
+ }
+ studioKtfmtPluginZip.delete()
+ println("ktfmt plugin installed successfully.")
+ }
+
/** Attempts to symlink the system-images and emulator SDK directories to a canonical SDK. */
private fun setupSymlinksIfNeeded() {
val paths = listOf("system-images", "emulator")
@@ -177,7 +242,7 @@
}
}
- val canonicalSdkPath = File(File(System.getProperty("user.home")).parent, relativeSdkPath)
+ val canonicalSdkPath = File(System.getenv("HOME"), relativeSdkPath)
if (!canonicalSdkPath.exists()) {
// In the future, we might want to try a little harder to locate a canonical SDK path.
println("Failed to locate canonical SDK, not found at: $canonicalSdkPath")
@@ -333,6 +398,26 @@
File(studioArchivePath).delete()
}
+ private fun File.verifyChecksum() {
+ val actualChecksum =
+ MessageDigest.getInstance("SHA-256")
+ .also { it.update(this.readBytes()) }
+ .digest()
+ .joinToString(separator = "") { "%02x".format(it) }
+
+ if (actualChecksum != studioKtfmtPluginChecksum) {
+ this.delete()
+ throw GradleException(
+ """
+ Checksum mismatch for file: ${this.absolutePath}
+ Expected: $studioKtfmtPluginChecksum
+ Actual: $actualChecksum
+ """
+ .trimIndent()
+ )
+ }
+ }
+
companion object {
private const val STUDIO_TASK = "studio"
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/GuaranteedConfigurationsUtil.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/GuaranteedConfigurationsUtil.kt
index 6b6d2ef..759e3cf 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/GuaranteedConfigurationsUtil.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/GuaranteedConfigurationsUtil.kt
@@ -19,11 +19,15 @@
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraMetadata
import android.os.Build
+import android.util.Size
import androidx.annotation.RequiresApi
+import androidx.camera.core.impl.CameraMode
+import androidx.camera.core.impl.ImageFormatConstants
import androidx.camera.core.impl.SurfaceCombination
import androidx.camera.core.impl.SurfaceConfig
import androidx.camera.core.impl.SurfaceConfig.ConfigSize
import androidx.camera.core.impl.SurfaceConfig.ConfigType
+import androidx.camera.core.impl.SurfaceSizeDefinition
public object GuaranteedConfigurationsUtil {
@JvmStatic
@@ -902,4 +906,44 @@
.also { combinationList.add(it) }
return combinationList
}
+
+ /** Returns the supported stream combinations for high-speed sessions. */
+ @JvmStatic
+ public fun generateHighSpeedSupportedCombinationList(
+ maxSupportedSize: Size,
+ surfaceSizeDefinition: SurfaceSizeDefinition
+ ): List<SurfaceCombination> {
+ val surfaceCombinations = mutableListOf<SurfaceCombination>()
+
+ // Find the closest SurfaceConfig that can contain the max supported size. Ultimately,
+ // the target resolution still needs to be verified by the StreamConfigurationMap API for
+ // high-speed.
+ val surfaceConfig =
+ SurfaceConfig.transformSurfaceConfig(
+ CameraMode.DEFAULT,
+ ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
+ maxSupportedSize,
+ surfaceSizeDefinition
+ )
+
+ // Create high-speed supported combinations based on the constraints:
+ // - Only support preview and/or video surface.
+ // - Maximum 2 surfaces.
+ // - All surfaces must have the same size.
+
+ // PRIV
+ SurfaceCombination()
+ .apply { addSurfaceConfig(surfaceConfig) }
+ .also { surfaceCombinations.add(it) }
+
+ // PRIV + PRIV
+ SurfaceCombination()
+ .apply {
+ addSurfaceConfig(surfaceConfig)
+ addSurfaceConfig(surfaceConfig)
+ }
+ .also { surfaceCombinations.add(it) }
+
+ return surfaceCombinations
+ }
}
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 0c7d723..185aaed 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
@@ -39,6 +39,7 @@
import androidx.camera.camera2.pipe.integration.compat.workaround.TargetAspectRatio
import androidx.camera.camera2.pipe.integration.impl.DisplayInfoManager
import androidx.camera.camera2.pipe.integration.internal.DynamicRangeResolver
+import androidx.camera.camera2.pipe.integration.internal.HighSpeedResolver
import androidx.camera.camera2.pipe.integration.internal.StreamUseCaseUtil
import androidx.camera.core.DynamicRange
import androidx.camera.core.impl.AttachedSurfaceInfo
@@ -93,6 +94,7 @@
private val ultraHighSurfaceCombinations: MutableList<SurfaceCombination> = mutableListOf()
private val previewStabilizationSurfaceCombinations: MutableList<SurfaceCombination> =
mutableListOf()
+ private val highSpeedSurfaceCombinations = mutableListOf<SurfaceCombination>()
private val featureSettingsToSupportedCombinationsMap:
MutableMap<FeatureSettings, List<SurfaceCombination>> =
mutableMapOf()
@@ -113,6 +115,7 @@
private val resolutionCorrector = ResolutionCorrector()
private val targetAspectRatio: TargetAspectRatio = TargetAspectRatio()
private val dynamicRangeResolver: DynamicRangeResolver = DynamicRangeResolver(cameraMetadata)
+ private val highSpeedResolver: HighSpeedResolver = HighSpeedResolver(cameraMetadata)
init {
checkCapabilities()
@@ -192,6 +195,11 @@
if (featureSettings.cameraMode == CameraMode.DEFAULT) {
supportedSurfaceCombinations.addAll(surfaceCombinationsUltraHdr)
}
+ } else if (featureSettings.isHighSpeedOn) {
+ if (highSpeedSurfaceCombinations.isEmpty()) {
+ generateHighSpeedSupportedCombinationList()
+ }
+ supportedSurfaceCombinations.addAll(highSpeedSurfaceCombinations)
} else if (featureSettings.requiredMaxBitDepth == DynamicRange.BIT_DEPTH_8_BIT) {
when (featureSettings.cameraMode) {
CameraMode.CONCURRENT_CAMERA ->
@@ -258,12 +266,23 @@
attachedSurfaces: List<AttachedSurfaceInfo>,
newUseCaseConfigsSupportedSizeMap: Map<UseCaseConfig<*>, List<Size>>,
isPreviewStabilizationOn: Boolean = false,
- hasVideoCapture: Boolean = false
+ hasVideoCapture: Boolean = false,
+ targetHighSpeedFpsRange: Range<Int>? = null
): Pair<Map<UseCaseConfig<*>, StreamSpec>, Map<AttachedSurfaceInfo, StreamSpec>> {
// Refresh Preview Size based on current display configurations.
refreshPreviewSize()
- val newUseCaseConfigs = newUseCaseConfigsSupportedSizeMap.keys.toList()
+ val isHighSpeedOn = targetHighSpeedFpsRange != null
+ // Filter out unsupported sizes for high-speed at the beginning to ensure correct
+ // resolution selection later. High-speed session requires all surface sizes to be the same.
+ val filteredNewUseCaseConfigsSupportedSizeMap =
+ if (isHighSpeedOn) {
+ highSpeedResolver.filterCommonSupportedSizes(newUseCaseConfigsSupportedSizeMap)
+ } else {
+ newUseCaseConfigsSupportedSizeMap
+ }
+
+ val newUseCaseConfigs = filteredNewUseCaseConfigsSupportedSizeMap.keys.toList()
// Get the index order list by the use case priority for finding stream configuration
val useCasesPriorityOrder = getUseCasesPriorityOrder(newUseCaseConfigs)
@@ -273,19 +292,20 @@
newUseCaseConfigs,
useCasesPriorityOrder
)
- val isUltraHdrOn = isUltraHdrOn(attachedSurfaces, newUseCaseConfigsSupportedSizeMap)
+ val isUltraHdrOn = isUltraHdrOn(attachedSurfaces, filteredNewUseCaseConfigsSupportedSizeMap)
val featureSettings =
createFeatureSettings(
cameraMode,
resolvedDynamicRanges,
isPreviewStabilizationOn,
- isUltraHdrOn
+ isUltraHdrOn,
+ isHighSpeedOn
)
val isSurfaceCombinationSupported =
isUseCasesCombinationSupported(
featureSettings,
attachedSurfaces,
- newUseCaseConfigsSupportedSizeMap
+ filteredNewUseCaseConfigsSupportedSizeMap
)
require(isSurfaceCombinationSupported) {
"No supported surface combination is found for camera device - Id : $cameraId. " +
@@ -295,11 +315,22 @@
// Calculates the target FPS range
val targetFpsRange =
- getTargetFpsRange(attachedSurfaces, newUseCaseConfigs, useCasesPriorityOrder)
+ if (isHighSpeedOn) targetHighSpeedFpsRange
+ else getTargetFpsRange(attachedSurfaces, newUseCaseConfigs, useCasesPriorityOrder)
// Filters the unnecessary output sizes for performance improvement. This will
// significantly reduce the number of all possible size arrangements below.
val useCaseConfigToFilteredSupportedSizesMap =
- filterSupportedSizes(newUseCaseConfigsSupportedSizeMap, featureSettings, targetFpsRange)
+ filterSupportedSizes(
+ filteredNewUseCaseConfigsSupportedSizeMap,
+ featureSettings,
+ targetFpsRange
+ )
+ val supportedOutputSizesList =
+ getSupportedOutputSizesList(
+ useCaseConfigToFilteredSupportedSizesMap,
+ newUseCaseConfigs,
+ useCasesPriorityOrder
+ )
// The two maps are used to keep track of the attachedSurfaceInfo or useCaseConfigs the
// surfaceConfigs are made from. They are populated in getSurfaceConfigListAndFpsCeiling().
// The keys are the position of their corresponding surfaceConfigs in the list. We can
@@ -310,14 +341,8 @@
mutableMapOf()
val surfaceConfigIndexUseCaseConfigMap: MutableMap<Int, UseCaseConfig<*>> = mutableMapOf()
val allPossibleSizeArrangements =
- getAllPossibleSizeArrangements(
- getSupportedOutputSizesList(
- useCaseConfigToFilteredSupportedSizesMap,
- newUseCaseConfigs,
- useCasesPriorityOrder
- )
- )
-
+ if (isHighSpeedOn) highSpeedResolver.getSizeArrangements(supportedOutputSizesList)
+ else getAllPossibleSizeArrangements(supportedOutputSizesList)
val containsZsl: Boolean =
StreamUseCaseUtil.containsZslUseCase(attachedSurfaces, newUseCaseConfigs)
var orderedSurfaceConfigListForStreamUseCase: List<SurfaceConfig>? = null
@@ -336,7 +361,8 @@
)
}
- val maxSupportedFps = getMaxSupportedFpsFromAttachedSurfaces(attachedSurfaces)
+ val maxSupportedFps =
+ getMaxSupportedFpsFromAttachedSurfaces(attachedSurfaces, isHighSpeedOn)
val bestSizesAndFps =
findBestSizesAndFps(
allPossibleSizeArrangements,
@@ -356,7 +382,8 @@
newUseCaseConfigs,
useCasesPriorityOrder,
resolvedDynamicRanges,
- hasVideoCapture
+ hasVideoCapture,
+ isHighSpeedOn
)
val attachedSurfaceStreamSpecMap = mutableMapOf<AttachedSurfaceInfo, StreamSpec>()
@@ -385,7 +412,8 @@
@CameraMode.Mode cameraMode: Int,
resolvedDynamicRanges: Map<UseCaseConfig<*>, DynamicRange>,
isPreviewStabilizationOn: Boolean,
- isUltraHdrOn: Boolean
+ isUltraHdrOn: Boolean,
+ isHighSpeedOn: Boolean
): FeatureSettings {
require(!(cameraMode != CameraMode.DEFAULT && isUltraHdrOn)) {
"Camera device Id is $cameraId. Ultra HDR is not " +
@@ -400,11 +428,17 @@
"Camera device Id is $cameraId. 10 bit dynamic range is not " +
"currently supported in ${CameraMode.toLabelString(cameraMode)} camera mode."
}
+
+ require(!(isHighSpeedOn && !highSpeedResolver.isHighSpeedSupported)) {
+ "High-speed session is not supported on this device."
+ }
+
return FeatureSettings(
cameraMode,
requiredMaxBitDepth,
isPreviewStabilizationOn,
- isUltraHdrOn
+ isUltraHdrOn,
+ isHighSpeedOn
)
}
@@ -468,7 +502,7 @@
* surfaceConfigIndexAttachedSurfaceInfoMap and surfaceConfigIndexUseCaseConfigMap.
*/
private fun getOrderedSurfaceConfigListForStreamUseCase(
- allPossibleSizeArrangements: List<MutableList<Size>>,
+ allPossibleSizeArrangements: List<List<Size>>,
attachedSurfaces: List<AttachedSurfaceInfo>,
newUseCaseConfigs: List<UseCaseConfig<*>>,
useCasesPriorityOrder: List<Int>,
@@ -625,6 +659,7 @@
private fun getMaxSupportedFpsFromAttachedSurfaces(
attachedSurfaces: List<AttachedSurfaceInfo>,
+ isHighSpeedOn: Boolean
): Int {
var existingSurfaceFrameRateCeiling = Int.MAX_VALUE
for (attachedSurfaceInfo in attachedSurfaces) {
@@ -633,7 +668,8 @@
getUpdatedMaximumFps(
existingSurfaceFrameRateCeiling,
attachedSurfaceInfo.imageFormat,
- attachedSurfaceInfo.size
+ attachedSurfaceInfo.size,
+ isHighSpeedOn
)
}
return existingSurfaceFrameRateCeiling
@@ -668,7 +704,7 @@
// Filters the sizes with frame rate only if there is target FPS setting
val maxFrameRate =
if (targetFpsRange != null) {
- getMaxFrameRate(imageFormat, size)
+ getMaxFrameRate(imageFormat, size, featureSettings.isHighSpeedOn)
} else {
Int.MAX_VALUE
}
@@ -716,7 +752,7 @@
}
private fun findBestSizesAndFps(
- allPossibleSizeArrangements: List<MutableList<Size>>,
+ allPossibleSizeArrangements: List<List<Size>>,
attachedSurfaces: List<AttachedSurfaceInfo>,
newUseCaseConfigs: List<UseCaseConfig<*>>,
existingSurfaceFrameRateCeiling: Int,
@@ -750,7 +786,8 @@
possibleSizeList,
newUseCaseConfigs,
useCasesPriorityOrder,
- existingSurfaceFrameRateCeiling
+ existingSurfaceFrameRateCeiling,
+ featureSettings.isHighSpeedOn
)
var isConfigFrameRateAcceptable = true
if (targetFrameRateForConfig != null) {
@@ -841,13 +878,25 @@
newUseCaseConfigs: List<UseCaseConfig<*>>,
useCasesPriorityOrder: List<Int>,
resolvedDynamicRanges: Map<UseCaseConfig<*>, DynamicRange>,
- hasVideoCapture: Boolean
+ hasVideoCapture: Boolean,
+ isHighSpeedOn: Boolean
): MutableMap<UseCaseConfig<*>, StreamSpec> {
val suggestedStreamSpecMap = mutableMapOf<UseCaseConfig<*>, StreamSpec>()
var targetFrameRateForDevice: Range<Int>? = null
if (targetFpsRange != null) {
+ // get all fps ranges supported by device
+ val availableFpsRanges =
+ if (isHighSpeedOn) {
+ highSpeedResolver.getFrameRateRangesFor(bestSizesAndMaxFps.bestSizes)
+ } else {
+ cameraMetadata[CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES]
+ }
targetFrameRateForDevice =
- getClosestSupportedDeviceFrameRate(targetFpsRange, bestSizesAndMaxFps.maxFps)
+ getClosestSupportedDeviceFrameRate(
+ targetFpsRange,
+ bestSizesAndMaxFps.maxFps,
+ availableFpsRanges
+ )
}
for ((index, useCaseConfig) in newUseCaseConfigs.withIndex()) {
val resolutionForUseCase =
@@ -922,6 +971,7 @@
newUseCaseConfigs: List<UseCaseConfig<*>>,
useCasesPriorityOrder: List<Int>,
currentConfigFrameRateCeiling: Int,
+ isHighSpeedOn: Boolean
): Int {
var newConfigFrameRateCeiling: Int = currentConfigFrameRateCeiling
// Attach SurfaceConfig of new use cases
@@ -930,11 +980,24 @@
// get the maximum fps of the new surface and update the maximum fps of the
// proposed configuration
newConfigFrameRateCeiling =
- getUpdatedMaximumFps(newConfigFrameRateCeiling, newUseCase.inputFormat, size)
+ getUpdatedMaximumFps(
+ newConfigFrameRateCeiling,
+ newUseCase.inputFormat,
+ size,
+ isHighSpeedOn
+ )
}
return newConfigFrameRateCeiling
}
+ private fun getMaxFrameRate(imageFormat: Int, size: Size, isHighSpeedOn: Boolean): Int {
+ return if (isHighSpeedOn) {
+ highSpeedResolver.getMaxFrameRate(imageFormat, size)
+ } else {
+ getMaxFrameRate(imageFormat, size)
+ }
+ }
+
private fun getMaxFrameRate(imageFormat: Int, size: Size?): Int {
var maxFrameRate = 0
try {
@@ -1019,23 +1082,23 @@
*
* @param targetFrameRate The Target Frame Rate resolved from all current existing surfaces and
* incoming new use cases.
+ * @param availableFpsRanges the device available frame rate ranges.
* @return A frame rate range supported by the device that is closest to targetFrameRate when it
* is specified. [StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED] is returned if targetFrameRate is
* [StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED].
*/
private fun getClosestSupportedDeviceFrameRate(
targetFrameRate: Range<Int>,
- maxFps: Int
+ maxFps: Int,
+ availableFpsRanges: Array<out Range<Int>>?
): Range<Int> {
if (targetFrameRate == StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED) {
return StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED
}
+ availableFpsRanges ?: return StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED
+
var newTargetFrameRate = targetFrameRate
- // get all fps ranges supported by device
- val availableFpsRanges =
- cameraMetadata[CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES]
- ?: return StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED
// if whole target frame rate range > maxFps of configuration, the target for this
// calculation will be [max,max].
@@ -1135,9 +1198,15 @@
* @param currentMaxFps the previously stored Max FPS
* @param imageFormat the image format of the incoming surface
* @param size the size of the incoming surface
+ * @param isHighSpeedOn whether high-speed session is enabled
*/
- private fun getUpdatedMaximumFps(currentMaxFps: Int, imageFormat: Int, size: Size): Int {
- return min(currentMaxFps, getMaxFrameRate(imageFormat, size))
+ private fun getUpdatedMaximumFps(
+ currentMaxFps: Int,
+ imageFormat: Int,
+ size: Size,
+ isHighSpeedOn: Boolean
+ ): Int {
+ return min(currentMaxFps, getMaxFrameRate(imageFormat, size, isHighSpeedOn))
}
/**
@@ -1276,6 +1345,24 @@
)
}
+ private fun generateHighSpeedSupportedCombinationList() {
+ if (!highSpeedResolver.isHighSpeedSupported) {
+ return
+ }
+ highSpeedSurfaceCombinations.clear()
+ // Find maximum supported size.
+ highSpeedResolver.maxSize?.let { maxSize ->
+ highSpeedSurfaceCombinations.addAll(
+ GuaranteedConfigurationsUtil.generateHighSpeedSupportedCombinationList(
+ maxSize,
+ getUpdatedSurfaceSizeDefinitionByFormat(
+ ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
+ )
+ )
+ )
+ }
+ }
+
private fun generate10BitSupportedCombinationList() {
surfaceCombinations10Bit.addAll(
GuaranteedConfigurationsUtil.get10BitSupportedCombinationList()
@@ -1616,7 +1703,8 @@
@CameraMode.Mode val cameraMode: Int,
val requiredMaxBitDepth: Int,
val isPreviewStabilizationOn: Boolean = false,
- val isUltraHdrOn: Boolean = false
+ val isUltraHdrOn: Boolean = false,
+ val isHighSpeedOn: Boolean = false
)
public data class BestSizesAndMaxFpsForConfigs(
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CapturePipeline.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CapturePipeline.kt
index cca39a8..96b5f9c 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CapturePipeline.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CapturePipeline.kt
@@ -412,7 +412,7 @@
it.lock3AForCapture(
timeLimitNs = timeLimitNs,
triggerAf = captureMode == CAPTURE_MODE_MAXIMIZE_QUALITY,
- waitForAwb = true,
+ waitForAwb = captureMode == CAPTURE_MODE_MAXIMIZE_QUALITY,
)
.await()
}
@@ -489,7 +489,8 @@
debug { "CapturePipeline#aePreCaptureApplyCapture: Locking 3A for capture" }
it.lock3AForCapture(
timeLimitNs = timeLimitNs,
- triggerAf = captureMode == CAPTURE_MODE_MAXIMIZE_QUALITY
+ triggerAf = captureMode == CAPTURE_MODE_MAXIMIZE_QUALITY,
+ waitForAwb = captureMode == CAPTURE_MODE_MAXIMIZE_QUALITY,
)
.join()
debug {
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/HighSpeedResolver.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/HighSpeedResolver.kt
new file mode 100644
index 0000000..8317a94
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/HighSpeedResolver.kt
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.internal
+
+import android.graphics.ImageFormat.PRIVATE
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES
+import android.hardware.camera2.CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO
+import android.os.Build
+import android.util.Range
+import android.util.Size
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
+import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
+import androidx.camera.core.Logger
+import androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
+import androidx.camera.core.internal.utils.SizeUtil.getArea
+
+/** A class responsible for resolving parameters for high-speed session scenario. */
+public class HighSpeedResolver(private val cameraMetadata: CameraMetadata) {
+
+ /** Indicates whether the camera supports high-speed session. */
+ public val isHighSpeedSupported: Boolean by lazy {
+ Build.VERSION.SDK_INT >= 23 &&
+ cameraMetadata[REQUEST_AVAILABLE_CAPABILITIES]?.any {
+ it == REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO
+ } == true
+ }
+
+ /** The maximum supported size based on area, or `null` if there are no supported sizes. */
+ public val maxSize: Size? by lazy {
+ supportedSizes.takeIf { it.isNotEmpty() }?.maxBy { getArea(it) }
+ }
+
+ private val streamConfigurationMapCompat: StreamConfigurationMapCompat by lazy {
+ val map =
+ cameraMetadata[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]
+ ?: throw IllegalArgumentException("Cannot retrieve SCALER_STREAM_CONFIGURATION_MAP")
+ StreamConfigurationMapCompat(map, OutputSizesCorrector(cameraMetadata, map))
+ }
+
+ private val supportedSizes: List<Size> by lazy {
+ streamConfigurationMapCompat.getHighSpeedVideoSizes()?.toList() ?: emptyList()
+ }
+
+ /**
+ * Filters supported sizes for each use case, retaining only the sizes common to all use cases
+ * and present in the overall supported sizes.
+ *
+ * This function analyzes a map of use case configurations and their corresponding lists of
+ * supported sizes. It identifies the sizes common to all use cases and filters each use case's
+ * supported sizes, retaining only those that are both common across all use cases and present
+ * in the `supportedSizes` list. The original order of the supported sizes for each use case is
+ * preserved.
+ *
+ * @param sizesMap A map where keys represent use case configurations and values are lists of
+ * `Size` objects representing the supported sizes for each use case.
+ * @return A new map with the same keys as the input `sizesMap`, but with the values (lists of
+ * sizes) filtered to contain only the common supported sizes that are also present in the
+ * `supportedSizes` list, while maintaining the original order.
+ */
+ public fun <T> filterCommonSupportedSizes(sizesMap: Map<T, List<Size>>): Map<T, List<Size>> {
+ val commonSupportedSizes =
+ sizesMap.values.toList().findCommonElements().filter { it in supportedSizes }
+ return sizesMap.mapValues { (_, sizes) -> sizes.filter { it in commonSupportedSizes } }
+ }
+
+ /**
+ * Returns the maximum frame rate supported for a given size in a high-speed session.
+ *
+ * This method retrieves the supported high-speed FPS ranges for the given size from the camera
+ * characteristics. It then returns the maximum frame rate (upper bound) among those ranges.
+ *
+ * @param imageFormat The image format. Only [PRIVATE] is supported for high-speed session.
+ * @param size The size for which to find the maximum supported high-speed frame rate.
+ * @return The maximum high-speed frame rate supported for the given size, or 0 if no high-speed
+ * FPS ranges are supported for that size or the image format is not supported.
+ */
+ public fun getMaxFrameRate(imageFormat: Int, size: Size): Int {
+ if (imageFormat != SUPPORTED_FORMAT) {
+ return 0
+ }
+
+ val supportedFpsRangesForSize =
+ getHighSpeedVideoFpsRangesFor(size).takeIf { it.isNotEmpty() }
+ ?: run {
+ Logger.w(TAG, "No supported high speed fps for $size")
+ return 0
+ }
+
+ return supportedFpsRangesForSize.maxOf { it.upper }
+ }
+
+ /**
+ * Returns size arrangements where all inner lists have the same size, maintaining order.
+ *
+ * This method takes a list of lists of sizes, where each inner list represents the supported
+ * sizes for a specific use case. It finds the common sizes across all use cases and creates
+ * arrangements where each use case has the same size. The order in the first list of the input
+ * determines the order of the common sizes in the output.
+ *
+ * This method is necessary due to a limitation in high-speed session configuration, where all
+ * streams (use cases) in a high-speed session must have the same size.
+ *
+ * @param sizesList A list of lists of sizes. Each inner list represents the supported sizes for
+ * a use case. The first dimension represents the use case, and the second dimension is the
+ * supported sizes.
+ * @return A list of size arrangements where each inner list contains the same size. Returns an
+ * empty list if the input is empty or null.
+ */
+ public fun getSizeArrangements(sizesList: List<List<Size>>): List<List<Size>> {
+ if (sizesList.isEmpty()) {
+ return emptyList()
+ }
+
+ val commonSizes = sizesList.findCommonElements()
+
+ // Generate arrangements with common sizes.
+ return commonSizes.map { commonSize -> List(sizesList.size) { commonSize } }
+ }
+
+ /**
+ * Returns the supported frame rate ranges for high-speed capture sessions with the given
+ * surface sizes.
+ *
+ * High-speed sessions have restrictions:
+ * 1. Maximum 2 surfaces.
+ * 2. All surfaces must have the same size. When the restrictions are not met, this method will
+ * return null.
+ *
+ * @param surfaceSizes The list of surface sizes.
+ * @return An array of supported frame rate ranges, or null if the input is invalid or no
+ * supported ranges are found.
+ */
+ public fun getFrameRateRangesFor(surfaceSizes: List<Size>): Array<Range<Int>>? {
+ // High-speed capture sessions have restrictions:
+ // 1. Maximum 2 surfaces.
+ // 2. All surfaces must have the same size.
+ if (surfaceSizes.size !in 1..2 || surfaceSizes.distinct().size != 1) {
+ return null
+ }
+
+ val supportedFpsRanges =
+ getHighSpeedVideoFpsRangesFor(surfaceSizes[0]).takeIf { it.isNotEmpty() } ?: return null
+
+ // For 2 surfaces case, the FPS range must be fixed (lower == upper). See
+ // CameraConstrainedHighSpeedCaptureSession#createHighSpeedRequestList.
+ return if (surfaceSizes.size == 2) {
+ supportedFpsRanges.filter { it.lower == it.upper }
+ } else {
+ supportedFpsRanges
+ }
+ .toTypedArray()
+ }
+
+ /**
+ * Finds the common elements present in all given lists, preserving the order from the first
+ * list.
+ *
+ * This function takes a list of lists and returns a new list containing only the elements that
+ * appear in every input list. The order of elements in the output list matches their order in
+ * the first list.
+ *
+ * @return A list containing only the elements found in all input lists, ordered according to
+ * their presence in the first list.
+ */
+ private fun <T> List<List<T>>.findCommonElements(): List<T> {
+ if (isEmpty()) return emptyList()
+
+ val commonElements = this.first().toMutableList()
+ this.drop(1).forEach { commonElements.retainAll(it) }
+ return commonElements
+ }
+
+ private fun getHighSpeedVideoFpsRangesFor(size: Size): List<Range<Int>> {
+ return runCatching { streamConfigurationMapCompat.getHighSpeedVideoFpsRangesFor(size) }
+ .getOrNull()
+ ?.filterNotNull()
+ ?.toList() ?: emptyList()
+ }
+
+ private companion object {
+ private const val TAG = "HighSpeedResolver"
+ private const val SUPPORTED_FORMAT = INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
+ }
+}
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 d25894f..2bac7fd 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
@@ -26,6 +26,7 @@
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CameraMetadata
import android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+import android.hardware.camera2.CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO
import android.hardware.camera2.CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT
import android.hardware.camera2.params.DynamicRangeProfiles
import android.hardware.camera2.params.StreamConfigurationMap
@@ -81,11 +82,16 @@
import androidx.camera.core.impl.ImageFormatConstants
import androidx.camera.core.impl.ImageInputConfig
import androidx.camera.core.impl.StreamSpec
+import androidx.camera.core.impl.SurfaceCombination
import androidx.camera.core.impl.SurfaceConfig
+import androidx.camera.core.impl.SurfaceConfig.ConfigSize
+import androidx.camera.core.impl.SurfaceConfig.ConfigType
import androidx.camera.core.impl.UseCaseConfig
import androidx.camera.core.impl.UseCaseConfigFactory
import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType
+import androidx.camera.core.impl.utils.CompareSizesByArea
import androidx.camera.core.internal.utils.SizeUtil
+import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_1080P
import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_1440P
import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_720P
import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_VGA
@@ -174,6 +180,25 @@
Size(7200, 4050), // 16:9
)
private val maximumResolutionHighResolutionSupportedSizes = arrayOf(Size(8000, 6000))
+ private val commonHighSpeedSupportedSizeFpsMap =
+ mapOf(
+ RESOLUTION_1080P to
+ listOf(
+ Range.create(30, 120),
+ Range.create(120, 120),
+ Range.create(30, 240),
+ Range.create(240, 240),
+ ),
+ RESOLUTION_720P to
+ listOf(
+ Range.create(30, 120),
+ Range.create(120, 120),
+ Range.create(30, 240),
+ Range.create(240, 240),
+ Range.create(30, 480),
+ Range.create(480, 480)
+ )
+ )
private val streamUseCaseOverrideValue = 3L
private val context = InstrumentationRegistry.getInstrumentation().context
@@ -543,6 +568,69 @@
}
}
+ @Config(minSdk = Build.VERSION_CODES.M)
+ @Test
+ fun checkSurfaceCombinationSupportForHighSpeed() {
+ setupCamera(
+ capabilities = intArrayOf(REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO),
+ supportedHighSpeedSizeAndFpsMap = commonHighSpeedSupportedSizeFpsMap
+ )
+ val supportedSurfaceCombination =
+ SupportedSurfaceCombination(context, fakeCameraMetadata, mockEncoderProfilesAdapter)
+ val featureSettings =
+ SupportedSurfaceCombination.FeatureSettings(
+ CameraMode.DEFAULT,
+ DynamicRange.BIT_DEPTH_8_BIT,
+ isHighSpeedOn = true
+ )
+
+ // The expected SurfaceConfig is PRIV + RECORD because the max high speed size 1920x1080 is
+ // between PREVIEW and RECORD size.
+ val shouldSupportCombinations =
+ listOf(
+ SurfaceCombination().apply {
+ addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.RECORD))
+ },
+ SurfaceCombination().apply {
+ addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.PREVIEW))
+ addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.PREVIEW))
+ }
+ )
+ shouldSupportCombinations.forEach {
+ assertThat(
+ supportedSurfaceCombination.checkSupported(
+ featureSettings,
+ it.surfaceConfigList
+ )
+ )
+ .isTrue()
+ }
+
+ val shouldNotSupportCombinations =
+ listOf(
+ SurfaceCombination().apply {
+ addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.MAXIMUM))
+ },
+ SurfaceCombination().apply {
+ addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.PREVIEW))
+ addSurfaceConfig(SurfaceConfig.create(ConfigType.JPEG, ConfigSize.MAXIMUM))
+ },
+ SurfaceCombination().apply {
+ addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.PREVIEW))
+ addSurfaceConfig(SurfaceConfig.create(ConfigType.YUV, ConfigSize.PREVIEW))
+ }
+ )
+ shouldNotSupportCombinations.forEach {
+ assertThat(
+ supportedSurfaceCombination.checkSupported(
+ featureSettings,
+ it.surfaceConfigList
+ )
+ )
+ .isFalse()
+ }
+ }
+
// //////////////////////////////////////////////////////////////////////////////////////////
//
// Surface config transformation tests
@@ -1506,6 +1594,7 @@
private fun getSuggestedSpecsAndVerify(
useCasesExpectedResultMap: Map<UseCase, Size>,
+ useCasesOutputSizesMap: Map<UseCase, List<Size>>? = null,
attachedSurfaceInfoList: List<AttachedSurfaceInfo> = emptyList(),
hardwareLevel: Int = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
capabilities: IntArray? = null,
@@ -1514,31 +1603,37 @@
cameraMode: Int = CameraMode.DEFAULT,
useCasesExpectedDynamicRangeMap: Map<UseCase, DynamicRange> = emptyMap(),
supportedOutputFormats: IntArray? = null,
+ supportedHighSpeedSizeAndFpsMap: Map<Size, List<Range<Int>>>? = null,
dynamicRangeProfiles: DynamicRangeProfiles? = null,
default10BitProfile: Long? = null,
isPreviewStabilizationOn: Boolean = false,
- hasVideoCapture: Boolean = false
+ hasVideoCapture: Boolean = false,
+ targetHighSpeedFpsRange: Range<Int>? = null
): Pair<Map<UseCaseConfig<*>, StreamSpec>, Map<AttachedSurfaceInfo, StreamSpec>> {
setupCamera(
hardwareLevel = hardwareLevel,
capabilities = capabilities,
dynamicRangeProfiles = dynamicRangeProfiles,
default10BitProfile = default10BitProfile,
- supportedFormats = supportedOutputFormats
+ supportedFormats = supportedOutputFormats,
+ supportedHighSpeedSizeAndFpsMap = supportedHighSpeedSizeAndFpsMap,
)
val supportedSurfaceCombination =
SupportedSurfaceCombination(context, fakeCameraMetadata, mockEncoderProfilesAdapter)
val useCaseConfigMap = getUseCaseToConfigMap(useCasesExpectedResultMap.keys.toList())
val useCaseConfigToOutputSizesMap =
- getUseCaseConfigToOutputSizesMap(useCaseConfigMap.values.toList())
+ useCaseConfigMap.entries.associate { (useCase, config) ->
+ config to (useCasesOutputSizesMap?.get(useCase) ?: supportedSizes.toList())
+ }
val resultPair =
supportedSurfaceCombination.getSuggestedStreamSpecifications(
cameraMode,
attachedSurfaceInfoList,
useCaseConfigToOutputSizesMap,
isPreviewStabilizationOn,
- hasVideoCapture
+ hasVideoCapture,
+ targetHighSpeedFpsRange
)
val suggestedStreamSpecsForNewUseCases = resultPair.first
val suggestedStreamSpecsForOldSurfaces = resultPair.second
@@ -1612,17 +1707,6 @@
return useCaseConfigMap
}
- private fun getUseCaseConfigToOutputSizesMap(
- useCaseConfigs: List<UseCaseConfig<*>>
- ): Map<UseCaseConfig<*>, List<Size>> {
- val resultMap =
- mutableMapOf<UseCaseConfig<*>, List<Size>>().apply {
- useCaseConfigs.forEach { put(it, supportedSizes.toList()) }
- }
-
- return resultMap
- }
-
/** Helper function that returns whether size is <= maxSize */
private fun sizeIsAtMost(size: Size, maxSize: Size): Boolean {
return (size.height * size.width) <= (maxSize.height * maxSize.width)
@@ -3242,6 +3326,137 @@
.isFalse()
}
+ @Config(minSdk = Build.VERSION_CODES.M)
+ @Test
+ fun getSuggestedStreamSpec_highSpeed_returnsCorrectSizeAndFpsRange() {
+ val previewUseCase = createUseCase(CaptureType.PREVIEW, surfaceOccupancyPriority = 2)
+ val videoUseCase = createUseCase(CaptureType.VIDEO_CAPTURE, surfaceOccupancyPriority = 5)
+ val useCasesOutputSizesMap =
+ mapOf(
+ previewUseCase to listOf(RESOLUTION_VGA, RESOLUTION_1080P, RESOLUTION_720P),
+ videoUseCase to listOf(RESOLUTION_1440P, RESOLUTION_720P, RESOLUTION_1080P)
+ )
+ // videoUseCase has higher surface priority so the expected size should be the first
+ // common size of videoUseCases. i.e. RESOLUTION_720P.
+ val useCaseExpectedResultMap =
+ mapOf(previewUseCase to RESOLUTION_720P, videoUseCase to RESOLUTION_720P)
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ capabilities = intArrayOf(REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO),
+ useCasesOutputSizesMap = useCasesOutputSizesMap,
+ supportedHighSpeedSizeAndFpsMap = commonHighSpeedSupportedSizeFpsMap,
+ targetHighSpeedFpsRange = Range.create(240, 240),
+ compareExpectedFps = Range.create(240, 240)
+ )
+ }
+
+ @Config(minSdk = Build.VERSION_CODES.M)
+ @Test
+ fun getSuggestedStreamSpec_highSpeed_singleSurface_returnsCorrectSizeAndClosestFps() {
+ val previewUseCase = createUseCase(CaptureType.PREVIEW)
+ val useCasesOutputSizesMap = mapOf(previewUseCase to listOf(RESOLUTION_1080P))
+ val useCaseExpectedResultMap = mapOf(previewUseCase to RESOLUTION_1080P)
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ capabilities = intArrayOf(REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO),
+ useCasesOutputSizesMap = useCasesOutputSizesMap,
+ supportedHighSpeedSizeAndFpsMap = commonHighSpeedSupportedSizeFpsMap,
+ targetHighSpeedFpsRange = Range.create(30, 480),
+ compareExpectedFps = Range.create(30, 240) // Find the closest supported fps.
+ )
+ }
+
+ @Config(minSdk = Build.VERSION_CODES.M)
+ @Test
+ fun getSuggestedStreamSpec_highSpeed_multipleSurfaces_returnsCorrectSizeAndClosetMaxFps() {
+ val previewUseCase = createUseCase(CaptureType.PREVIEW)
+ val videoUseCase = createUseCase(CaptureType.VIDEO_CAPTURE)
+ val useCasesOutputSizesMap =
+ mapOf(
+ previewUseCase to listOf(RESOLUTION_1080P),
+ videoUseCase to listOf(RESOLUTION_1080P)
+ )
+ val useCaseExpectedResultMap =
+ mapOf(previewUseCase to RESOLUTION_1080P, videoUseCase to RESOLUTION_1080P)
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ capabilities = intArrayOf(REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO),
+ useCasesOutputSizesMap = useCasesOutputSizesMap,
+ supportedHighSpeedSizeAndFpsMap = commonHighSpeedSupportedSizeFpsMap,
+ targetHighSpeedFpsRange = Range.create(30, 480),
+ compareExpectedFps = Range.create(240, 240) // Find the closest max supported fps.
+ )
+ }
+
+ @Config(minSdk = 21, maxSdk = 22)
+ @Test
+ fun getSuggestedStreamSpec_highSpeed_unsupportedSdkVersion_throwException() {
+ val useCase = createUseCase(CaptureType.PREVIEW)
+ val useCaseExpectedResultMap = mapOf(useCase to RESOLUTION_1080P)
+ assertThrows(IllegalArgumentException::class.java) {
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ capabilities =
+ intArrayOf(REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO),
+ targetHighSpeedFpsRange = Range.create(240, 240),
+ )
+ }
+ }
+
+ @Config(minSdk = Build.VERSION_CODES.M)
+ @Test
+ fun getSuggestedStreamSpec_highSpeed_noCommonSize_throwException() {
+ val previewUseCase = createUseCase(CaptureType.PREVIEW)
+ val videoUseCase = createUseCase(CaptureType.VIDEO_CAPTURE)
+ val useCasesOutputSizesMap =
+ mapOf(
+ previewUseCase to listOf(RESOLUTION_VGA, RESOLUTION_720P),
+ videoUseCase to listOf(RESOLUTION_1440P, RESOLUTION_1080P)
+ )
+ val useCaseExpectedResultMap =
+ mapOf(previewUseCase to RESOLUTION_VGA, videoUseCase to RESOLUTION_1440P)
+ assertThrows(IllegalArgumentException::class.java) {
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ capabilities =
+ intArrayOf(REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO),
+ useCasesOutputSizesMap = useCasesOutputSizesMap,
+ supportedHighSpeedSizeAndFpsMap = commonHighSpeedSupportedSizeFpsMap,
+ targetHighSpeedFpsRange = Range.create(240, 240),
+ )
+ }
+ }
+
+ @Config(minSdk = Build.VERSION_CODES.M)
+ @Test
+ fun getSuggestedStreamSpec_highSpeed_tooManyUseCases_throwException() {
+ val previewUseCase1 = createUseCase(CaptureType.PREVIEW)
+ val previewUseCase2 = createUseCase(CaptureType.PREVIEW)
+ val videoUseCase = createUseCase(CaptureType.VIDEO_CAPTURE)
+ val useCasesOutputSizesMap =
+ mapOf(
+ previewUseCase1 to listOf(RESOLUTION_1080P),
+ previewUseCase2 to listOf(RESOLUTION_1080P),
+ videoUseCase to listOf(RESOLUTION_1080P)
+ )
+ val useCaseExpectedResultMap =
+ mapOf(
+ previewUseCase1 to RESOLUTION_1080P,
+ previewUseCase2 to RESOLUTION_1080P,
+ videoUseCase to RESOLUTION_1080P
+ )
+ assertThrows(IllegalArgumentException::class.java) {
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ capabilities =
+ intArrayOf(REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO),
+ useCasesOutputSizesMap = useCasesOutputSizesMap,
+ supportedHighSpeedSizeAndFpsMap = commonHighSpeedSupportedSizeFpsMap,
+ targetHighSpeedFpsRange = Range.create(240, 240),
+ )
+ }
+ }
+
private fun setupCamera(
hardwareLevel: Int = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
sensorOrientation: Int = sensorOrientation90,
@@ -3249,6 +3464,7 @@
supportedSizes: Array<Size> = this.supportedSizes,
supportedFormats: IntArray? = null,
highResolutionSupportedSizes: Array<Size>? = null,
+ supportedHighSpeedSizeAndFpsMap: Map<Size, List<Range<Int>>>? = null,
maximumResolutionSupportedSizes: Array<Size>? = null,
maximumResolutionHighResolutionSupportedSizes: Array<Size>? = null,
dynamicRangeProfiles: DynamicRangeProfiles? = null,
@@ -3374,6 +3590,36 @@
}
}
+ if (
+ supportedHighSpeedSizeAndFpsMap != null &&
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
+ ) {
+ // Mock highSpeedVideoSizes
+ whenever(mockMap.highSpeedVideoSizes)
+ .thenReturn(supportedHighSpeedSizeAndFpsMap.keys.toTypedArray())
+
+ // Mock highSpeedVideoFpsRanges
+ val allFpsRanges = supportedHighSpeedSizeAndFpsMap.values.flatten().distinct()
+ whenever(mockMap.highSpeedVideoFpsRanges).thenReturn(allFpsRanges.toTypedArray())
+
+ // Mock getHighSpeedVideoSizesFor
+ allFpsRanges.forEach { fpsRange ->
+ val sizesForRange =
+ supportedHighSpeedSizeAndFpsMap.entries
+ .filter { (_, fpsRanges) -> fpsRanges.contains(fpsRange) }
+ .map { it.key }
+ .sortedWith(CompareSizesByArea(false)) // Descending order
+ .toTypedArray()
+ whenever(mockMap.getHighSpeedVideoSizesFor(fpsRange)).thenReturn(sizesForRange)
+ }
+
+ // Mock getHighSpeedVideoFpsRangesFor
+ supportedHighSpeedSizeAndFpsMap.forEach { (size, fpsRanges) ->
+ whenever(mockMap.getHighSpeedVideoFpsRangesFor(size))
+ .thenReturn(fpsRanges.toTypedArray())
+ }
+ }
+
// setup to return different minimum frame durations depending on resolution
// minimum frame durations were designated only for the purpose of testing
Mockito.`when`(
@@ -3528,9 +3774,16 @@
private fun createUseCase(
captureType: UseCaseConfigFactory.CaptureType,
targetFrameRate: Range<Int>? = null,
- dynamicRange: DynamicRange = DynamicRange.UNSPECIFIED
+ dynamicRange: DynamicRange = DynamicRange.UNSPECIFIED,
+ surfaceOccupancyPriority: Int? = null
): UseCase {
- return createUseCase(captureType, targetFrameRate, dynamicRange, false)
+ return createUseCase(
+ captureType,
+ targetFrameRate,
+ dynamicRange,
+ streamUseCaseOverride = false,
+ surfaceOccupancyPriority = surfaceOccupancyPriority
+ )
}
private fun createUseCase(
@@ -3538,7 +3791,8 @@
targetFrameRate: Range<Int>? = null,
dynamicRange: DynamicRange? = DynamicRange.UNSPECIFIED,
streamUseCaseOverride: Boolean = false,
- imageFormat: Int? = null
+ imageFormat: Int? = null,
+ surfaceOccupancyPriority: Int? = null,
): UseCase {
val builder =
FakeUseCaseConfig.Builder(
@@ -3560,6 +3814,9 @@
if (streamUseCaseOverride) {
builder.mutableConfig.insertOption(streamUseCaseOption, streamUseCaseOverrideValue)
}
+
+ surfaceOccupancyPriority?.let { builder.setSurfaceOccupancyPriority(it) }
+
return builder.build()
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/HighSpeedResolverTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/HighSpeedResolverTest.kt
new file mode 100644
index 0000000..1a1aa3d
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/HighSpeedResolverTest.kt
@@ -0,0 +1,308 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.internal
+
+import android.graphics.ImageFormat
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
+import android.hardware.camera2.CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
+import android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
+import android.hardware.camera2.params.StreamConfigurationMap
+import android.os.Build
+import android.util.Range
+import android.util.Size
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.core.impl.ImageFormatConstants
+import androidx.camera.core.impl.UseCaseConfig
+import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType
+import androidx.camera.core.impl.utils.CompareSizesByArea
+import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_1080P
+import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_480P
+import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_720P
+import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_VGA
+import androidx.camera.testing.impl.EncoderProfilesUtil.RESOLUTION_2160P
+import androidx.camera.testing.impl.fakes.FakeUseCaseConfig
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class HighSpeedResolverTest {
+
+ private companion object {
+ private const val CAMERA_ID = "0"
+
+ private const val FPS_30 = 30
+ private const val FPS_120 = 120
+ private const val FPS_240 = 240
+ private const val FPS_480 = 480
+
+ private val RANGE_30_120 = Range.create(FPS_30, FPS_120)
+ private val RANGE_120_120 = Range.create(FPS_120, FPS_120)
+ private val RANGE_30_240 = Range.create(FPS_30, FPS_240)
+ private val RANGE_240_240 = Range.create(FPS_240, FPS_240)
+ private val RANGE_30_480 = Range.create(FPS_30, FPS_480)
+ private val RANGE_480_480 = Range.create(FPS_480, FPS_480)
+
+ private const val FORMAT_PRIVATE =
+ ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
+
+ private val COMMON_HIGH_SPEED_SUPPORTED_SIZE_FPS_MAP =
+ mapOf(
+ RESOLUTION_1080P to
+ listOf(
+ RANGE_30_120,
+ RANGE_120_120,
+ RANGE_30_240,
+ RANGE_240_240,
+ ),
+ RESOLUTION_720P to
+ listOf(
+ RANGE_30_120,
+ RANGE_120_120,
+ RANGE_30_240,
+ RANGE_240_240,
+ RANGE_30_480,
+ RANGE_480_480
+ )
+ )
+ }
+
+ private val defaultHighSpeedResolver = createHighSpeedResolver()
+ private val emptyHighSpeedResolver =
+ createHighSpeedResolver(
+ characteristics = createCharacteristicsMap(supportedHighSpeedSizeAndFpsMap = emptyMap())
+ )
+
+ @Test
+ fun filterCommonSupportedSizes_returnsCorrectMap() {
+ val useCaseSupportedSizeMap =
+ listOf(
+ listOf(RESOLUTION_480P, RESOLUTION_720P, RESOLUTION_1080P),
+ listOf(RESOLUTION_1080P, RESOLUTION_720P, RESOLUTION_2160P),
+ listOf(RESOLUTION_480P, RESOLUTION_720P, RESOLUTION_1080P, RESOLUTION_VGA)
+ )
+ .toUseCaseSupportedSizeMap()
+
+ val result = defaultHighSpeedResolver.filterCommonSupportedSizes(useCaseSupportedSizeMap)
+
+ // Assert: return common sizes and preserve the original order.
+ assertThat(result.values)
+ .containsExactly(
+ listOf(RESOLUTION_720P, RESOLUTION_1080P),
+ listOf(RESOLUTION_1080P, RESOLUTION_720P),
+ listOf(RESOLUTION_720P, RESOLUTION_1080P)
+ )
+ .inOrder()
+ }
+
+ @Test
+ fun getMaxSize_noSupportedSizes_returnsNull() {
+ val result = emptyHighSpeedResolver.maxSize
+
+ assertThat(result).isNull()
+ }
+
+ @Test
+ fun getMaxSize_supportedSizesExist_returnsLargestSize() {
+ val result = defaultHighSpeedResolver.maxSize
+
+ assertThat(result).isEqualTo(RESOLUTION_1080P)
+ }
+
+ @Test
+ fun getMaxFrameRate_unsupportedImageFormat_returnsZero() {
+ val result = defaultHighSpeedResolver.getMaxFrameRate(ImageFormat.JPEG, RESOLUTION_1080P)
+
+ assertThat(result).isEqualTo(0)
+ }
+
+ @Test
+ fun getMaxFrameRate_noSupportedFpsRanges_returnsZero() {
+ val result = emptyHighSpeedResolver.getMaxFrameRate(FORMAT_PRIVATE, RESOLUTION_1080P)
+
+ assertThat(result).isEqualTo(0)
+ }
+
+ @Test
+ fun getMaxFrameRate_supportedFpsRangesExist_returnMaxFps() {
+ val result = defaultHighSpeedResolver.getMaxFrameRate(FORMAT_PRIVATE, RESOLUTION_1080P)
+
+ assertThat(result).isEqualTo(FPS_240)
+ }
+
+ @Test
+ fun getSizeArrangements_emptyInput_returnEmptyList() {
+ val sizeArrangements = defaultHighSpeedResolver.getSizeArrangements(emptyList())
+
+ assertThat(sizeArrangements).isEmpty()
+ }
+
+ @Test
+ fun getSizeArrangements_hasCommonSizes_returnCorrectArrangements() {
+ val common1080p = RESOLUTION_1080P
+ val common720p = RESOLUTION_720P
+ val supportedOutputSizesList =
+ listOf(
+ listOf(RESOLUTION_480P, common720p, common1080p),
+ listOf(common1080p, common720p, RESOLUTION_2160P),
+ listOf(RESOLUTION_480P, common720p, common1080p, RESOLUTION_VGA)
+ )
+ val sizeArrangements =
+ defaultHighSpeedResolver.getSizeArrangements(supportedOutputSizesList)
+
+ assertThat(sizeArrangements)
+ .containsExactly(
+ listOf(common720p, common720p, common720p),
+ listOf(common1080p, common1080p, common1080p)
+ )
+ .inOrder()
+ }
+
+ @Test
+ fun getSizeArrangements_noCommonSizes_returnEmptyList() {
+ val supportedOutputSizesList =
+ listOf(
+ listOf(RESOLUTION_480P, RESOLUTION_720P),
+ listOf(RESOLUTION_1080P, RESOLUTION_2160P),
+ listOf(RESOLUTION_1080P, RESOLUTION_720P)
+ )
+
+ val result = defaultHighSpeedResolver.getSizeArrangements(supportedOutputSizesList)
+
+ assertThat(result).isEmpty()
+ }
+
+ @Test
+ fun getFrameRateRangesFor_invalidInput_returnsNull() {
+ // More than 2 surfaces.
+ assertThat(
+ defaultHighSpeedResolver.getFrameRateRangesFor(
+ listOf(RESOLUTION_720P, RESOLUTION_720P, RESOLUTION_720P)
+ )
+ )
+ .isNull()
+
+ // Different sizes.
+ assertThat(
+ defaultHighSpeedResolver.getFrameRateRangesFor(
+ listOf(RESOLUTION_720P, RESOLUTION_1080P)
+ )
+ )
+ .isNull()
+
+ // Empty list.
+ assertThat(defaultHighSpeedResolver.getFrameRateRangesFor(emptyList())).isNull()
+ }
+
+ @Test
+ fun getFrameRateRangesFor_noSupportedSizes_returnsNull() {
+ assertThat(emptyHighSpeedResolver.getFrameRateRangesFor(listOf(RESOLUTION_720P))).isNull()
+ }
+
+ @Test
+ fun getFrameRateRangesFor_oneSurface_returnsAllSupportedRanges() {
+ val result = defaultHighSpeedResolver.getFrameRateRangesFor(listOf(RESOLUTION_720P))
+
+ assertThat(result!!.toList())
+ .containsExactly(
+ RANGE_30_120,
+ RANGE_120_120,
+ RANGE_30_240,
+ RANGE_240_240,
+ RANGE_30_480,
+ RANGE_480_480
+ )
+ }
+
+ @Test
+ fun getFrameRateRangesFor_twoSurfaces_returnsFixedFpsRanges() {
+ val result =
+ defaultHighSpeedResolver.getFrameRateRangesFor(listOf(RESOLUTION_720P, RESOLUTION_720P))
+
+ assertThat(result!!.toList())
+ .containsExactly(RANGE_120_120, RANGE_240_240, RANGE_480_480)
+ .inOrder()
+ }
+
+ private fun createHighSpeedResolver(
+ cameraId: CameraId = CameraId(CAMERA_ID),
+ characteristics: Map<CameraCharacteristics.Key<*>, Any?> = createCharacteristicsMap(),
+ ): HighSpeedResolver {
+ return HighSpeedResolver(
+ cameraMetadata =
+ FakeCameraMetadata(cameraId = cameraId, characteristics = characteristics)
+ )
+ }
+
+ private fun createCharacteristicsMap(
+ hardwareLevel: Int = INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
+ supportedHighSpeedSizeAndFpsMap: Map<Size, List<Range<Int>>>? =
+ COMMON_HIGH_SPEED_SUPPORTED_SIZE_FPS_MAP,
+ ): Map<CameraCharacteristics.Key<*>, Any?> {
+ val mockMap =
+ Mockito.mock(StreamConfigurationMap::class.java).also { map ->
+ if (supportedHighSpeedSizeAndFpsMap != null) {
+ // Mock highSpeedVideoSizes
+ Mockito.`when`(map.highSpeedVideoSizes)
+ .thenReturn(supportedHighSpeedSizeAndFpsMap.keys.toTypedArray())
+
+ // Mock highSpeedVideoFpsRanges
+ val allFpsRanges = supportedHighSpeedSizeAndFpsMap.values.flatten().distinct()
+ Mockito.`when`(map.highSpeedVideoFpsRanges)
+ .thenReturn(allFpsRanges.toTypedArray())
+
+ // Mock getHighSpeedVideoSizesFor
+ allFpsRanges.forEach { fpsRange ->
+ val sizesForRange =
+ supportedHighSpeedSizeAndFpsMap.entries
+ .filter { (_, fpsRanges) -> fpsRanges.contains(fpsRange) }
+ .map { it.key }
+ .sortedWith(CompareSizesByArea(false)) // Descending order
+ .toTypedArray()
+ Mockito.`when`(map.getHighSpeedVideoSizesFor(fpsRange))
+ .thenReturn(sizesForRange)
+ }
+
+ // Mock getHighSpeedVideoFpsRangesFor
+ supportedHighSpeedSizeAndFpsMap.forEach { (size, fpsRanges) ->
+ Mockito.`when`(map.getHighSpeedVideoFpsRangesFor(size))
+ .thenReturn(fpsRanges.toTypedArray())
+ }
+ }
+ }
+
+ return mutableMapOf<CameraCharacteristics.Key<*>, Any?>(
+ INFO_SUPPORTED_HARDWARE_LEVEL to hardwareLevel,
+ SCALER_STREAM_CONFIGURATION_MAP to mockMap
+ )
+ }
+
+ private fun List<List<Size>>.toUseCaseSupportedSizeMap(): Map<UseCaseConfig<*>, List<Size>> {
+ return associate { sizes ->
+ FakeUseCaseConfig.Builder(CaptureType.PREVIEW, FORMAT_PRIVATE).build().currentConfig to
+ sizes
+ }
+ }
+}
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/HighSpeedCaptureSessionTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/HighSpeedCaptureSessionTest.kt
new file mode 100644
index 0000000..38e90b1
--- /dev/null
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/HighSpeedCaptureSessionTest.kt
@@ -0,0 +1,412 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.camera2.internal
+
+import android.content.Context
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraDevice.TEMPLATE_RECORD
+import android.hardware.camera2.CameraMetadata
+import android.hardware.camera2.CaptureResult.CONTROL_AE_TARGET_FPS_RANGE
+import android.hardware.camera2.params.SessionConfiguration.SESSION_HIGH_SPEED
+import android.media.CamcorderProfile
+import android.media.CamcorderProfile.QUALITY_HIGH_SPEED_1080P
+import android.media.CamcorderProfile.QUALITY_HIGH_SPEED_2160P
+import android.media.CamcorderProfile.QUALITY_HIGH_SPEED_480P
+import android.media.CamcorderProfile.QUALITY_HIGH_SPEED_720P
+import android.media.MediaCodec
+import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
+import android.media.MediaFormat
+import android.media.MediaFormat.KEY_BIT_RATE
+import android.media.MediaFormat.KEY_COLOR_FORMAT
+import android.media.MediaFormat.KEY_FRAME_RATE
+import android.media.MediaFormat.KEY_I_FRAME_INTERVAL
+import android.media.MediaFormat.MIMETYPE_VIDEO_AVC
+import android.media.MediaFormat.MIMETYPE_VIDEO_H263
+import android.media.MediaFormat.MIMETYPE_VIDEO_HEVC
+import android.media.MediaFormat.MIMETYPE_VIDEO_MPEG4
+import android.media.MediaFormat.MIMETYPE_VIDEO_VP8
+import android.media.MediaFormat.MIMETYPE_VIDEO_VP9
+import android.media.MediaRecorder.VideoEncoder.H263
+import android.media.MediaRecorder.VideoEncoder.H264
+import android.media.MediaRecorder.VideoEncoder.HEVC
+import android.media.MediaRecorder.VideoEncoder.MPEG_4_SP
+import android.media.MediaRecorder.VideoEncoder.VP8
+import android.media.MediaRecorder.VideoEncoder.VP9
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.util.Log
+import android.util.Range
+import android.util.Size
+import android.view.Surface
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.internal.SynchronizedCaptureSession.OpenerBuilder
+import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat
+import androidx.camera.camera2.internal.compat.CameraManagerCompat
+import androidx.camera.camera2.internal.compat.params.DynamicRangesCompat
+import androidx.camera.camera2.internal.compat.quirk.CameraQuirks
+import androidx.camera.camera2.internal.compat.quirk.DeviceQuirks
+import androidx.camera.core.impl.CameraCaptureCallback
+import androidx.camera.core.impl.CameraCaptureResult
+import androidx.camera.core.impl.CaptureConfig
+import androidx.camera.core.impl.DeferrableSurface
+import androidx.camera.core.impl.ImmediateSurface
+import androidx.camera.core.impl.Quirks
+import androidx.camera.core.impl.SessionConfig
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.core.impl.utils.futures.Futures
+import androidx.camera.testing.impl.CameraUtil
+import androidx.camera.testing.impl.CameraUtil.CameraDeviceHolder
+import androidx.camera.testing.impl.CameraUtil.PreTestCameraIdList
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertWithMessage
+import com.google.common.util.concurrent.ListenableFuture
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import org.junit.After
+import org.junit.AfterClass
+import org.junit.Assert.fail
+import org.junit.Assume.assumeTrue
+import org.junit.AssumptionViolatedException
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.M)
+class HighSpeedCaptureSessionTest {
+
+ @get:Rule
+ val cameraRule =
+ CameraUtil.grantCameraPermissionAndPreTestAndPostTest(
+ PreTestCameraIdList(Camera2Config.defaultConfig())
+ )
+
+ private lateinit var mCameraDeviceHolder: CameraDeviceHolder
+ private lateinit var mCameraCharacteristics: CameraCharacteristicsCompat
+ private lateinit var mDynamicRangesCompat: DynamicRangesCompat
+ private lateinit var cameraQuirks: Quirks
+ private lateinit var captureSessionOpenerBuilder: OpenerBuilder
+ private lateinit var cameraId: String
+
+ private val mCaptureSessions = mutableListOf<CaptureSession>()
+ private val mDeferrableSurfaces = mutableListOf<DeferrableSurface>()
+
+ private val isHighSpeedSupported: Boolean
+ get() {
+ val capabilities =
+ mCameraCharacteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)
+ return capabilities?.any {
+ it == CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO
+ } == true
+ }
+
+ @Before
+ fun setup() {
+ val handler = Handler(handlerThread.getLooper())
+ val executor = CameraXExecutors.newHandlerExecutor(handler)
+ val scheduledExecutor = CameraXExecutors.newHandlerExecutor(handler)
+
+ cameraId = CameraUtil.getBackwardCompatibleCameraIdListOrThrow()[0]
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ val cameraManager = CameraManagerCompat.from(context, handler)
+ try {
+ mCameraCharacteristics = cameraManager.getCameraCharacteristicsCompat(cameraId)
+ } catch (e: CameraAccessExceptionCompat) {
+ throw AssumptionViolatedException("Could not retrieve camera characteristics", e)
+ }
+
+ cameraQuirks = CameraQuirks.get(cameraId, mCameraCharacteristics)
+ mDynamicRangesCompat = DynamicRangesCompat.fromCameraCharacteristics(mCameraCharacteristics)
+
+ val captureSessionRepository = CaptureSessionRepository(executor)
+ captureSessionOpenerBuilder =
+ OpenerBuilder(
+ executor,
+ scheduledExecutor,
+ handler,
+ captureSessionRepository,
+ CameraQuirks.get(cameraId, mCameraCharacteristics),
+ DeviceQuirks.getAll()
+ )
+
+ mCameraDeviceHolder =
+ CameraUtil.getCameraDevice(cameraId, captureSessionRepository.cameraStateCallback)
+ }
+
+ @After
+ fun tearDown() {
+ // Ensure all capture sessions are fully closed
+ val releaseFutures = mutableListOf<ListenableFuture<Void?>?>()
+ for (captureSession in mCaptureSessions) {
+ releaseFutures.add(captureSession.release(/* abortInFlightCaptures= */ false))
+ }
+ mCaptureSessions.clear()
+ Futures.allAsList<Void?>(releaseFutures).get(10L, TimeUnit.SECONDS)
+
+ if (this::mCameraDeviceHolder.isInitialized) {
+ CameraUtil.releaseCameraDevice(mCameraDeviceHolder)
+ }
+
+ for (deferrableSurface in mDeferrableSurfaces) {
+ deferrableSurface.close()
+ }
+ }
+
+ @Test
+ fun issueCaptureRequest_forRecording_canIssueRepeatingAndSingleRequests() {
+ // Arrange: check capability and get supported high speed size and fps range.
+ assumeTrue(isHighSpeedSupported)
+
+ val profile =
+ getHighSpeedCamcorderProfile()
+ ?: throw AssumptionViolatedException("No CamcorderProfile")
+ val profileInfo =
+ "video codec:${profile.videoCodec}, " +
+ "size:${profile.videoSize}, " +
+ "frame rate:${profile.videoFrameRate}, " +
+ "bit rate:${profile.videoBitRate}"
+ Log.d(TAG, "Selected profile $profileInfo")
+
+ // Create SessionConfig for high-speed capture
+ val repeatingLatch = CountDownLatch(2)
+ val templateType = TEMPLATE_RECORD
+ val previewSurface = createSurfaceTextureDeferrableSurface(profile.videoSize)
+ val videoSurface = createMediaCodecDeferrableSurface(profile)
+ val fpsRange = Range.create(profile.videoFrameRate, profile.videoFrameRate)
+ val sessionConfig =
+ SessionConfig.Builder()
+ .apply {
+ setSessionType(SESSION_HIGH_SPEED)
+ setTemplateType(templateType)
+ addSurface(previewSurface)
+ addSurface(videoSurface)
+ setExpectedFrameRateRange(fpsRange)
+ addCameraCaptureCallback(
+ object : CameraCaptureCallback() {
+ override fun onCaptureCompleted(
+ captureConfigId: Int,
+ cameraCaptureResult: CameraCaptureResult
+ ) {
+ val fps =
+ cameraCaptureResult.captureResult?.get(
+ CONTROL_AE_TARGET_FPS_RANGE
+ )
+ if (repeatingLatch.count > 0L) {
+ Log.d(TAG, "Repeating onCaptureCompleted: fps = $fps")
+ }
+ if (fps == fpsRange) {
+ repeatingLatch.countDown()
+ }
+ }
+ }
+ )
+ }
+ .build()
+
+ // Act: open capture session and issue repeating request via SessionConfig.
+ val captureSession =
+ createCaptureSession().apply {
+ open(
+ sessionConfig,
+ mCameraDeviceHolder.get()!!,
+ captureSessionOpenerBuilder.build()
+ )
+ this.sessionConfig = sessionConfig
+ }
+
+ // Assert.
+ assertWithMessage("Failed to issue repeating request by $profileInfo")
+ .that(repeatingLatch.await(10, TimeUnit.SECONDS))
+ .isTrue()
+
+ // Arrange: create CaptureConfig.
+ val captureId = 100
+ val captureLatch = CountDownLatch(1)
+ val captureConfig =
+ CaptureConfig.Builder()
+ .apply {
+ this.templateType = templateType
+ addSurface(previewSurface)
+ addSurface(videoSurface)
+ setId(captureId)
+ addCameraCaptureCallback(
+ object : CameraCaptureCallback() {
+ override fun onCaptureCompleted(
+ captureConfigId: Int,
+ cameraCaptureResult: CameraCaptureResult
+ ) {
+ // Count down when the request is proceeded and fps is applied.
+ val fps =
+ cameraCaptureResult.captureResult?.get(
+ CONTROL_AE_TARGET_FPS_RANGE
+ )
+ Log.d(
+ TAG,
+ "Single capture onCaptureCompleted: " +
+ "captureConfigId = $captureConfigId, fps = $fps"
+ )
+ if (captureId == captureConfigId && fps == fpsRange) {
+ captureLatch.countDown()
+ }
+ }
+ }
+ )
+ }
+ .build()
+
+ // Act. issue single request.
+ captureSession.issueCaptureRequests(listOf(captureConfig))
+
+ // Assert.
+ assertWithMessage("Failed to issue single capture request by $profileInfo")
+ .that(captureLatch.await(5, TimeUnit.SECONDS))
+ .isTrue()
+ }
+
+ private fun createCaptureSession() =
+ CaptureSession(mDynamicRangesCompat, cameraQuirks).also { mCaptureSessions.add(it) }
+
+ private fun getHighSpeedCamcorderProfile(): CamcorderProfile? {
+ return listOf(
+ QUALITY_HIGH_SPEED_480P,
+ QUALITY_HIGH_SPEED_720P,
+ QUALITY_HIGH_SPEED_1080P,
+ QUALITY_HIGH_SPEED_2160P
+ )
+ .filter { CamcorderProfile.hasProfile(it) }
+ .firstNotNullOfOrNull { quality ->
+ @Suppress("DEPRECATION") CamcorderProfile.get(cameraId.toInt(), quality)
+ }
+ }
+
+ private fun createSurfaceTextureDeferrableSurface(size: Size): DeferrableSurface {
+ val surfaceTexture =
+ SurfaceTexture(0).apply {
+ setDefaultBufferSize(size.width, size.height)
+ detachFromGLContext()
+ }
+ val surface = Surface(surfaceTexture)
+ return ImmediateSurface(surface).apply {
+ terminationFuture.addListener(
+ Runnable {
+ surface.release()
+ surfaceTexture.release()
+ },
+ CameraXExecutors.directExecutor()
+ )
+ mDeferrableSurfaces.add(this)
+ }
+ }
+
+ private fun createMediaCodecDeferrableSurface(profile: CamcorderProfile): DeferrableSurface {
+ val surface = MediaCodec.createPersistentInputSurface()
+ val mimeType = profile.videoMime
+ val codec = MediaCodec.createEncoderByType(mimeType)
+ codec.setCallback(emptyCodecCallback)
+ val format =
+ MediaFormat.createVideoFormat(
+ mimeType,
+ profile.videoFrameWidth,
+ profile.videoFrameHeight
+ )
+ .apply {
+ setInteger(KEY_COLOR_FORMAT, COLOR_FormatSurface)
+ setInteger(KEY_FRAME_RATE, profile.videoFrameRate)
+ setInteger(KEY_BIT_RATE, profile.videoBitRate) // 4 Mbps
+ setInteger(KEY_I_FRAME_INTERVAL, 1) // 1 second
+ }
+ codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
+ codec.setInputSurface(surface)
+ codec.start()
+
+ return ImmediateSurface(surface).apply {
+ terminationFuture.addListener(
+ Runnable {
+ codec.stop()
+ codec.release()
+ surface.release()
+ },
+ CameraXExecutors.directExecutor()
+ )
+ mDeferrableSurfaces.add(this)
+ }
+ }
+
+ private val CamcorderProfile.videoMime
+ get() =
+ when (videoCodec) {
+ H263 -> MIMETYPE_VIDEO_H263
+ H264 -> MIMETYPE_VIDEO_AVC
+ MPEG_4_SP -> MIMETYPE_VIDEO_MPEG4
+ HEVC -> MIMETYPE_VIDEO_HEVC
+ VP8 -> MIMETYPE_VIDEO_VP8
+ VP9 -> MIMETYPE_VIDEO_VP9
+ else -> throw IllegalArgumentException("Unsupported video codec: $videoCodec")
+ }
+
+ private val CamcorderProfile.videoSize
+ get() = Size(videoFrameWidth, videoFrameHeight)
+
+ private val emptyCodecCallback by lazy {
+ object : MediaCodec.Callback() {
+ override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {}
+
+ override fun onOutputBufferAvailable(
+ codec: MediaCodec,
+ index: Int,
+ info: MediaCodec.BufferInfo
+ ) {
+ codec.getOutputBuffer(index)
+ codec.releaseOutputBuffer(index, false)
+ }
+
+ override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
+ fail(e.message)
+ }
+
+ override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {}
+ }
+ }
+
+ private companion object {
+ private const val TAG = "HighSpeedCaptureSessionTest"
+ private lateinit var handlerThread: HandlerThread
+
+ @BeforeClass
+ @JvmStatic
+ fun setUpClass() {
+ handlerThread = HandlerThread("HighSpeedCaptureSessionTest")
+ handlerThread.start()
+ }
+
+ @AfterClass
+ @JvmStatic
+ fun tearDownClass() {
+ if (this::handlerThread.isInitialized) {
+ handlerThread.quitSafely()
+ }
+ }
+ }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
index 7aeb76c..7589dc8 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
@@ -1246,7 +1246,7 @@
try {
mSupportedSurfaceCombination.getSuggestedStreamSpecifications(cameraMode,
- attachedSurfaces, useCaseConfigToSizeMap, false, false);
+ attachedSurfaces, useCaseConfigToSizeMap, false, false, null);
} catch (IllegalArgumentException e) {
debugLog("Surface combination with metering repeating not supported!", e);
return false;
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
index f858f4e..e687de9 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
@@ -189,6 +189,7 @@
existingSurfaces,
newUseCaseConfigsSupportedSizeMap,
isPreviewStabilizationOn,
- hasVideoCapture);
+ hasVideoCapture,
+ null);
}
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
index 865587b..95d9e1c 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
@@ -27,6 +27,7 @@
import android.hardware.camera2.params.DynamicRangeProfiles;
import android.hardware.camera2.params.MultiResolutionStreamInfo;
import android.hardware.camera2.params.OutputConfiguration;
+import android.hardware.camera2.params.SessionConfiguration;
import android.os.Build;
import android.view.Surface;
@@ -227,7 +228,6 @@
}
}
-
/**
* {@inheritDoc}
*/
@@ -705,8 +705,15 @@
mRequestMonitor.createMonitorListener(createCamera2CaptureCallback(
captureConfig.getCameraCaptureCallbacks()));
- return mSynchronizedCaptureSession.setSingleRepeatingRequest(captureRequest,
- comboCaptureCallback);
+ if (sessionConfig.getSessionType() == SessionConfiguration.SESSION_HIGH_SPEED) {
+ List<CaptureRequest> requests =
+ mSynchronizedCaptureSession.createHighSpeedRequestList(captureRequest);
+ return mSynchronizedCaptureSession.setRepeatingBurstRequests(requests,
+ comboCaptureCallback);
+ } else { // SessionConfiguration.SESSION_REGULAR
+ return mSynchronizedCaptureSession.setSingleRepeatingRequest(captureRequest,
+ comboCaptureCallback);
+ }
} catch (CameraAccessException e) {
Logger.e(TAG, "Unable to access camera: " + e.getMessage());
Thread.dumpStack();
@@ -857,8 +864,13 @@
}
}));
}
- return mSynchronizedCaptureSession.captureBurstRequests(captureRequests,
- callbackAggregator);
+ if (mSessionConfig != null && mSessionConfig.getSessionType()
+ == SessionConfiguration.SESSION_HIGH_SPEED) {
+ return captureHighSpeedBurst(captureRequests, callbackAggregator);
+ } else { // SessionConfiguration.SESSION_REGULAR
+ return mSynchronizedCaptureSession.captureBurstRequests(captureRequests,
+ callbackAggregator);
+ }
} else {
Logger.d(TAG,
"Skipping issuing burst request due to no valid request elements");
@@ -872,6 +884,40 @@
}
}
+ @GuardedBy("mSessionLock")
+ private int captureHighSpeedBurst(@NonNull List<CaptureRequest> captureRequests,
+ @NonNull CameraBurstCaptureCallback callbackAggregator)
+ throws CameraAccessException {
+ // Create a new CameraBurstCaptureCallback to handle callbacks from high-speed requests.
+ // This is necessary because high-speed capture sessions generate multiple requests for
+ // each original request, and we need to map the callbacks back to the original requests.
+ CameraBurstCaptureCallback highSpeedCallbackAggregator = new CameraBurstCaptureCallback();
+
+ int sequenceId = -1;
+
+ for (CaptureRequest captureRequest : captureRequests) {
+ List<CaptureRequest> highSpeedRequests =
+ Objects.requireNonNull(mSynchronizedCaptureSession)
+ .createHighSpeedRequestList(captureRequest);
+
+ // For each high-speed request, create a forwarding callback that maps the high-speed
+ // request back to the original request and forwards the callback to the original
+ // callback aggregator.
+ for (CaptureRequest highSpeedRequest : highSpeedRequests) {
+ CaptureCallback forwardingCallback = new RequestForwardingCaptureCallback(
+ captureRequest, callbackAggregator);
+ highSpeedCallbackAggregator.addCamera2Callbacks(highSpeedRequest,
+ Collections.singletonList(forwardingCallback));
+ }
+
+ sequenceId = mSynchronizedCaptureSession.captureBurstRequests(
+ highSpeedRequests, highSpeedCallbackAggregator);
+ }
+
+ // Return the sequence ID of the last burst capture as a representative ID.
+ return sequenceId;
+ }
+
/**
* Discards all captures currently pending and in-progress as fast as possible.
*/
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/GuaranteedConfigurationsUtil.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/GuaranteedConfigurationsUtil.java
index 215ddd2..16a544d 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/GuaranteedConfigurationsUtil.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/GuaranteedConfigurationsUtil.java
@@ -19,12 +19,16 @@
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraMetadata;
import android.os.Build;
+import android.util.Size;
import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.CameraMode;
+import androidx.camera.core.impl.ImageFormatConstants;
import androidx.camera.core.impl.SurfaceCombination;
import androidx.camera.core.impl.SurfaceConfig;
import androidx.camera.core.impl.SurfaceConfig.ConfigSize;
import androidx.camera.core.impl.SurfaceConfig.ConfigType;
+import androidx.camera.core.impl.SurfaceSizeDefinition;
import org.jspecify.annotations.NonNull;
@@ -972,4 +976,38 @@
}
return surfaceCombinations;
}
+
+ /**
+ * Returns the supported stream combinations for high-speed sessions.
+ */
+ public static @NonNull List<SurfaceCombination> generateHighSpeedSupportedCombinationList(
+ @NonNull Size maxSupportedSize,
+ @NonNull SurfaceSizeDefinition surfaceSizeDefinition) {
+ List<SurfaceCombination> surfaceCombinations = new ArrayList<>();
+
+ // Find the closest SurfaceConfig that can contain the max supported size. Ultimately,
+ // the target resolution still needs to be verified by the StreamConfigurationMap API for
+ // high-speed.
+ SurfaceConfig surfaceConfig = SurfaceConfig.transformSurfaceConfig(CameraMode.DEFAULT,
+ ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE, maxSupportedSize,
+ surfaceSizeDefinition);
+
+ // Create high-speed supported combinations based on the constraints:
+ // - Only support preview and/or video surface.
+ // - Maximum 2 surfaces.
+ // - All surfaces must have the same size.
+
+ // PRIV
+ SurfaceCombination surfaceCombination = new SurfaceCombination();
+ surfaceCombination.addSurfaceConfig(surfaceConfig);
+ surfaceCombinations.add(surfaceCombination);
+
+ // PRIV + PRIV
+ surfaceCombination = new SurfaceCombination();
+ surfaceCombination.addSurfaceConfig(surfaceConfig);
+ surfaceCombination.addSurfaceConfig(surfaceConfig);
+ surfaceCombinations.add(surfaceCombination);
+
+ return surfaceCombinations;
+ }
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/HighSpeedResolver.kt b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/HighSpeedResolver.kt
new file mode 100644
index 0000000..15311a0
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/HighSpeedResolver.kt
@@ -0,0 +1,193 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.internal
+
+import android.graphics.ImageFormat.PRIVATE
+import android.hardware.camera2.CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES
+import android.hardware.camera2.CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO
+import android.os.Build
+import android.util.Range
+import android.util.Size
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat
+import androidx.camera.core.Logger
+import androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
+import androidx.camera.core.internal.utils.SizeUtil.getArea
+
+/** A class responsible for resolving parameters for high-speed session scenario. */
+public class HighSpeedResolver(private val characteristics: CameraCharacteristicsCompat) {
+
+ /** Indicates whether the camera supports high-speed session. */
+ public val isHighSpeedSupported: Boolean by lazy {
+ Build.VERSION.SDK_INT >= 23 &&
+ characteristics.get(REQUEST_AVAILABLE_CAPABILITIES)?.any {
+ it == REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO
+ } == true
+ }
+
+ /** The maximum supported size based on area, or `null` if there are no supported sizes. */
+ public val maxSize: Size? by lazy {
+ supportedSizes.takeIf { it.isNotEmpty() }?.maxBy { getArea(it) }
+ }
+
+ private val supportedSizes: List<Size> by lazy {
+ characteristics.streamConfigurationMapCompat.highSpeedVideoSizes?.filterNotNull()
+ ?: emptyList()
+ }
+
+ /**
+ * Filters supported sizes for each use case, retaining only the sizes common to all use cases
+ * and present in the overall supported sizes.
+ *
+ * This function analyzes a map of use case configurations and their corresponding lists of
+ * supported sizes. It identifies the sizes common to all use cases and filters each use case's
+ * supported sizes, retaining only those that are both common across all use cases and present
+ * in the `supportedSizes` list. The original order of the supported sizes for each use case is
+ * preserved.
+ *
+ * @param sizesMap A map where keys represent use case configurations and values are lists of
+ * `Size` objects representing the supported sizes for each use case.
+ * @return A new map with the same keys as the input `sizesMap`, but with the values (lists of
+ * sizes) filtered to contain only the common supported sizes that are also present in the
+ * `supportedSizes` list, while maintaining the original order.
+ */
+ public fun <T> filterCommonSupportedSizes(sizesMap: Map<T, List<Size>>): Map<T, List<Size>> {
+ val commonSupportedSizes =
+ sizesMap.values.toList().findCommonElements().filter { it in supportedSizes }
+ return sizesMap.mapValues { (_, sizes) -> sizes.filter { it in commonSupportedSizes } }
+ }
+
+ /**
+ * Returns the maximum frame rate supported for a given size in a high-speed session.
+ *
+ * This method retrieves the supported high-speed FPS ranges for the given size from the camera
+ * characteristics. It then returns the maximum frame rate (upper bound) among those ranges.
+ *
+ * @param imageFormat The image format. Only [PRIVATE] is supported for high-speed session.
+ * @param size The size for which to find the maximum supported high-speed frame rate.
+ * @return The maximum high-speed frame rate supported for the given size, or 0 if no high-speed
+ * FPS ranges are supported for that size or the image format is not supported.
+ */
+ public fun getMaxFrameRate(imageFormat: Int, size: Size): Int {
+ if (imageFormat != SUPPORTED_FORMAT) {
+ return 0
+ }
+
+ val supportedFpsRangesForSize =
+ getHighSpeedVideoFpsRangesFor(size).takeIf { it.isNotEmpty() }
+ ?: run {
+ Logger.w(TAG, "No supported high speed fps for $size")
+ return 0
+ }
+
+ return supportedFpsRangesForSize.maxOf { it.upper }
+ }
+
+ /**
+ * Returns size arrangements where all inner lists have the same size, maintaining order.
+ *
+ * This method takes a list of lists of sizes, where each inner list represents the supported
+ * sizes for a specific use case. It finds the common sizes across all use cases and creates
+ * arrangements where each use case has the same size. The order in the first list of the input
+ * determines the order of the common sizes in the output.
+ *
+ * This method is necessary due to a limitation in high-speed session configuration, where all
+ * streams (use cases) in a high-speed session must have the same size.
+ *
+ * @param sizesList A list of lists of sizes. Each inner list represents the supported sizes for
+ * a use case. The first dimension represents the use case, and the second dimension is the
+ * supported sizes.
+ * @return A list of size arrangements where each inner list contains the same size. Returns an
+ * empty list if the input is empty or null.
+ */
+ public fun getSizeArrangements(sizesList: List<List<Size>>): List<List<Size>> {
+ if (sizesList.isEmpty()) {
+ return emptyList()
+ }
+
+ val commonSizes = sizesList.findCommonElements()
+
+ // Generate arrangements with common sizes.
+ return commonSizes.map { commonSize -> List(sizesList.size) { commonSize } }
+ }
+
+ /**
+ * Returns the supported frame rate ranges for high-speed capture sessions with the given
+ * surface sizes.
+ *
+ * High-speed sessions have restrictions:
+ * 1. Maximum 2 surfaces.
+ * 2. All surfaces must have the same size. When the restrictions are not met, this method will
+ * return null.
+ *
+ * @param surfaceSizes The list of surface sizes.
+ * @return An array of supported frame rate ranges, or null if the input is invalid or no
+ * supported ranges are found.
+ */
+ public fun getFrameRateRangesFor(surfaceSizes: List<Size>): Array<Range<Int>>? {
+ // High-speed capture sessions have restrictions:
+ // 1. Maximum 2 surfaces.
+ // 2. All surfaces must have the same size.
+ if (surfaceSizes.size !in 1..2 || surfaceSizes.distinct().size != 1) {
+ return null
+ }
+
+ val supportedFpsRanges =
+ getHighSpeedVideoFpsRangesFor(surfaceSizes[0]).takeIf { it.isNotEmpty() } ?: return null
+
+ // For 2 surfaces case, the FPS range must be fixed (lower == upper). See
+ // CameraConstrainedHighSpeedCaptureSession#createHighSpeedRequestList.
+ return if (surfaceSizes.size == 2) {
+ supportedFpsRanges.filter { it.lower == it.upper }
+ } else {
+ supportedFpsRanges
+ }
+ .toTypedArray()
+ }
+
+ /**
+ * Finds the common elements present in all given lists, preserving the order from the first
+ * list.
+ *
+ * This function takes a list of lists and returns a new list containing only the elements that
+ * appear in every input list. The order of elements in the output list matches their order in
+ * the first list.
+ *
+ * @return A list containing only the elements found in all input lists, ordered according to
+ * their presence in the first list.
+ */
+ private fun <T> List<List<T>>.findCommonElements(): List<T> {
+ if (isEmpty()) return emptyList()
+
+ val commonElements = this.first().toMutableList()
+ this.drop(1).forEach { commonElements.retainAll(it) }
+ return commonElements
+ }
+
+ private fun getHighSpeedVideoFpsRangesFor(size: Size): List<Range<Int>> {
+ return runCatching {
+ characteristics.streamConfigurationMapCompat.getHighSpeedVideoFpsRangesFor(size)
+ }
+ .getOrNull()
+ ?.filterNotNull()
+ ?.toList() ?: emptyList()
+ }
+
+ private companion object {
+ private const val TAG = "HighSpeedResolver"
+ private const val SUPPORTED_FORMAT = INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
+ }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/RequestForwardingCaptureCallback.kt b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/RequestForwardingCaptureCallback.kt
new file mode 100644
index 0000000..c304344
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/RequestForwardingCaptureCallback.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2025 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.internal
+
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CaptureFailure
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureResult
+import android.hardware.camera2.TotalCaptureResult
+import android.os.Build
+import android.view.Surface
+import androidx.annotation.RequiresApi
+
+/**
+ * A `CaptureCallback` that forwards all callbacks to another `CaptureCallback` with a specific
+ * `CaptureRequest`.
+ */
+public class RequestForwardingCaptureCallback(
+ private val forwardedRequest: CaptureRequest,
+ private val delegate: CameraCaptureSession.CaptureCallback
+) : CameraCaptureSession.CaptureCallback() {
+
+ override fun onCaptureStarted(
+ session: CameraCaptureSession,
+ request: CaptureRequest,
+ timestamp: Long,
+ frameNumber: Long
+ ) {
+ delegate.onCaptureStarted(session, forwardedRequest, timestamp, frameNumber)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ override fun onReadoutStarted(
+ session: CameraCaptureSession,
+ request: CaptureRequest,
+ timestamp: Long,
+ frameNumber: Long
+ ) {
+ delegate.onReadoutStarted(session, forwardedRequest, timestamp, frameNumber)
+ }
+
+ override fun onCaptureProgressed(
+ session: CameraCaptureSession,
+ request: CaptureRequest,
+ partialResult: CaptureResult
+ ) {
+ delegate.onCaptureProgressed(session, forwardedRequest, partialResult)
+ }
+
+ override fun onCaptureCompleted(
+ session: CameraCaptureSession,
+ request: CaptureRequest,
+ result: TotalCaptureResult
+ ) {
+ delegate.onCaptureCompleted(session, forwardedRequest, result)
+ }
+
+ override fun onCaptureFailed(
+ session: CameraCaptureSession,
+ request: CaptureRequest,
+ failure: CaptureFailure
+ ) {
+ delegate.onCaptureFailed(session, forwardedRequest, failure)
+ }
+
+ override fun onCaptureSequenceCompleted(
+ session: CameraCaptureSession,
+ sequenceId: Int,
+ frameNumber: Long
+ ) {
+ delegate.onCaptureSequenceCompleted(session, sequenceId, frameNumber)
+ }
+
+ override fun onCaptureSequenceAborted(session: CameraCaptureSession, sequenceId: Int) {
+ delegate.onCaptureSequenceAborted(session, sequenceId)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ override fun onCaptureBufferLost(
+ session: CameraCaptureSession,
+ request: CaptureRequest,
+ target: Surface,
+ frameNumber: Long
+ ) {
+ delegate.onCaptureBufferLost(session, forwardedRequest, target, frameNumber)
+ }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
index e77663e..4dc8635 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
@@ -99,6 +99,7 @@
private final List<SurfaceCombination> mConcurrentSurfaceCombinations = new ArrayList<>();
private final List<SurfaceCombination> mPreviewStabilizationSurfaceCombinations =
new ArrayList<>();
+ private final List<SurfaceCombination> mHighSpeedSurfaceCombinations = new ArrayList<>();
private final Map<FeatureSettings, List<SurfaceCombination>>
mFeatureSettingsToSupportedCombinationsMap = new HashMap<>();
private final List<SurfaceCombination> mSurfaceCombinations10Bit = new ArrayList<>();
@@ -124,6 +125,7 @@
private final TargetAspectRatio mTargetAspectRatio = new TargetAspectRatio();
private final ResolutionCorrector mResolutionCorrector = new ResolutionCorrector();
private final DynamicRangeResolver mDynamicRangeResolver;
+ private final HighSpeedResolver mHighSpeedResolver;
@IntDef({DynamicRange.BIT_DEPTH_8_BIT, DynamicRange.BIT_DEPTH_10_BIT})
@Retention(RetentionPolicy.SOURCE)
@@ -168,6 +170,7 @@
}
mDynamicRangeResolver = new DynamicRangeResolver(mCharacteristics);
+ mHighSpeedResolver = new HighSpeedResolver(mCharacteristics);
generateSupportedCombinationList();
if (mIsUltraHighResolutionSensorSupported) {
@@ -276,6 +279,11 @@
if (featureSettings.getCameraMode() == CameraMode.DEFAULT) {
supportedSurfaceCombinations.addAll(mSurfaceCombinationsUltraHdr);
}
+ } else if (featureSettings.isHighSpeedOn()) {
+ if (mHighSpeedSurfaceCombinations.isEmpty()) {
+ generateHighSpeedSupportedCombinationList();
+ }
+ supportedSurfaceCombinations.addAll(mHighSpeedSurfaceCombinations);
} else if (featureSettings.getRequiredMaxBitDepth() == DynamicRange.BIT_DEPTH_8_BIT) {
switch (featureSettings.getCameraMode()) {
case CameraMode.CONCURRENT_CAMERA:
@@ -322,6 +330,11 @@
getUpdatedSurfaceSizeDefinitionByFormat(imageFormat));
}
+ private int getMaxFrameRate(int imageFormat, @NonNull Size size, boolean isHighSpeedOn) {
+ return isHighSpeedOn ? mHighSpeedResolver.getMaxFrameRate(imageFormat, size)
+ : getMaxFrameRate(mCharacteristics, imageFormat, size);
+ }
+
static int getMaxFrameRate(CameraCharacteristicsCompat characteristics, int imageFormat,
Size size) {
int maxFramerate = 0;
@@ -413,18 +426,16 @@
*
* @param targetFrameRate the Target Frame Rate resolved from all current existing surfaces
* and incoming new use cases
+ * @param availableFpsRanges the device available frame rate ranges
* @return a frame rate range supported by the device that is closest to targetFrameRate
*/
private @NonNull Range<Integer> getClosestSupportedDeviceFrameRate(
- @Nullable Range<Integer> targetFrameRate, int maxFps) {
+ @Nullable Range<Integer> targetFrameRate, int maxFps,
+ @Nullable Range<Integer>[] availableFpsRanges) {
if (targetFrameRate == null || targetFrameRate.equals(FRAME_RATE_RANGE_UNSPECIFIED)) {
return FRAME_RATE_RANGE_UNSPECIFIED;
}
- // get all fps ranges supported by device
- Range<Integer>[] availableFpsRanges =
- mCharacteristics.get(CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
-
if (availableFpsRanges == null) {
return FRAME_RATE_RANGE_UNSPECIFIED;
}
@@ -527,9 +538,11 @@
* @param currentMaxFps the previously stored Max FPS
* @param imageFormat the image format of the incoming surface
* @param size the size of the incoming surface
+ * @param isHighSpeedOn whether high-speed session is enabled
*/
- private int getUpdatedMaximumFps(int currentMaxFps, int imageFormat, Size size) {
- return Math.min(currentMaxFps, getMaxFrameRate(mCharacteristics, imageFormat, size));
+ private int getUpdatedMaximumFps(int currentMaxFps, int imageFormat, Size size,
+ boolean isHighSpeedOn) {
+ return Math.min(currentMaxFps, getMaxFrameRate(imageFormat, size, isHighSpeedOn));
}
/**
@@ -557,10 +570,19 @@
@NonNull List<AttachedSurfaceInfo> attachedSurfaces,
@NonNull Map<UseCaseConfig<?>, List<Size>> newUseCaseConfigsSupportedSizeMap,
boolean isPreviewStabilizationOn,
- boolean hasVideoCapture) {
+ boolean hasVideoCapture,
+ @Nullable Range<Integer> targetHighSpeedFpsRange) {
// Refresh Preview Size based on current display configurations.
refreshPreviewSize();
+ boolean isHighSpeedOn = targetHighSpeedFpsRange != null;
+ // Filter out unsupported sizes for high-speed at the beginning to ensure correct
+ // resolution selection later. High-speed session requires all surface sizes to be the same.
+ if (isHighSpeedOn) {
+ newUseCaseConfigsSupportedSizeMap = mHighSpeedResolver.filterCommonSupportedSizes(
+ newUseCaseConfigsSupportedSizeMap);
+ }
+
List<UseCaseConfig<?>> newUseCaseConfigs = new ArrayList<>(
newUseCaseConfigsSupportedSizeMap.keySet());
@@ -572,7 +594,7 @@
boolean isUltraHdrOn = isUltraHdrOn(attachedSurfaces, newUseCaseConfigsSupportedSizeMap);
FeatureSettings featureSettings = createFeatureSettings(cameraMode, resolvedDynamicRanges,
- isPreviewStabilizationOn, isUltraHdrOn);
+ isPreviewStabilizationOn, isUltraHdrOn, isHighSpeedOn);
boolean isSurfaceCombinationSupported = isUseCasesCombinationSupported(featureSettings,
attachedSurfaces, newUseCaseConfigsSupportedSizeMap);
@@ -586,8 +608,8 @@
}
// Calculates the target FPS range
- Range<Integer> targetFpsRange = getTargetFpsRange(attachedSurfaces,
- newUseCaseConfigs, useCasesPriorityOrder);
+ Range<Integer> targetFpsRange = isHighSpeedOn ? targetHighSpeedFpsRange
+ : getTargetFpsRange(attachedSurfaces, newUseCaseConfigs, useCasesPriorityOrder);
// Filters the unnecessary output sizes for performance improvement. This will
// significantly reduce the number of all possible size arrangements below.
Map<UseCaseConfig<?>, List<Size>> useCaseConfigToFilteredSupportedSizesMap =
@@ -608,8 +630,8 @@
// Get all possible size arrangements
List<List<Size>> allPossibleSizeArrangements =
- getAllPossibleSizeArrangements(
- supportedOutputSizesList);
+ isHighSpeedOn ? mHighSpeedResolver.getSizeArrangements(supportedOutputSizesList)
+ : getAllPossibleSizeArrangements(supportedOutputSizesList);
Map<AttachedSurfaceInfo, StreamSpec> attachedSurfaceStreamSpecMap = new HashMap<>();
Map<UseCaseConfig<?>, StreamSpec> suggestedStreamSpecMap = new HashMap<>();
@@ -632,13 +654,15 @@
boolean containsZsl = StreamUseCaseUtil.containsZslUseCase(attachedSurfaces,
newUseCaseConfigs);
List<SurfaceConfig> orderedSurfaceConfigListForStreamUseCase = null;
- int maxSupportedFps = getMaxSupportedFpsFromAttachedSurfaces(attachedSurfaces);
+ int maxSupportedFps = getMaxSupportedFpsFromAttachedSurfaces(attachedSurfaces,
+ isHighSpeedOn);
// Only checks the stream use case combination support when ZSL is not required.
if (mIsStreamUseCaseSupported && !containsZsl) {
// Check if any possible size arrangement is supported for stream use case.
for (List<Size> possibleSizeList : allPossibleSizeArrangements) {
List<SurfaceConfig> surfaceConfigs = getSurfaceConfigListAndFpsCeiling(
cameraMode,
+ isHighSpeedOn,
attachedSurfaces, possibleSizeList, newUseCaseConfigs,
useCasesPriorityOrder, maxSupportedFps,
surfaceConfigIndexAttachedSurfaceInfoMap,
@@ -684,7 +708,7 @@
for (List<Size> possibleSizeList : allPossibleSizeArrangements) {
// Attach SurfaceConfig of original use cases since it will impact the new use cases
Pair<List<SurfaceConfig>, Integer> resultPair =
- getSurfaceConfigListAndFpsCeiling(cameraMode,
+ getSurfaceConfigListAndFpsCeiling(cameraMode, isHighSpeedOn,
attachedSurfaces, possibleSizeList, newUseCaseConfigs,
useCasesPriorityOrder, maxSupportedFps, null, null);
List<SurfaceConfig> surfaceConfigList = resultPair.first;
@@ -758,9 +782,11 @@
if (savedSizes != null) {
Range<Integer> targetFramerateForDevice = null;
if (targetFpsRange != null) {
- targetFramerateForDevice =
- getClosestSupportedDeviceFrameRate(targetFpsRange,
- savedConfigMaxFps);
+ Range<Integer>[] availableFpsRanges = isHighSpeedOn
+ ? mHighSpeedResolver.getFrameRateRangesFor(savedSizes)
+ : mCharacteristics.get(CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
+ targetFramerateForDevice = getClosestSupportedDeviceFrameRate(targetFpsRange,
+ savedConfigMaxFps, availableFpsRanges);
}
for (UseCaseConfig<?> useCaseConfig : newUseCaseConfigs) {
Size resolutionForUseCase = savedSizes.get(
@@ -843,7 +869,7 @@
private @NonNull FeatureSettings createFeatureSettings(
@CameraMode.Mode int cameraMode,
@NonNull Map<UseCaseConfig<?>, DynamicRange> resolvedDynamicRanges,
- boolean isPreviewStabilizationOn, boolean isUltraHdrOn) {
+ boolean isPreviewStabilizationOn, boolean isUltraHdrOn, boolean isHighSpeedOn) {
int requiredMaxBitDepth = getRequiredMaxBitDepth(resolvedDynamicRanges);
if (cameraMode != CameraMode.DEFAULT && isUltraHdrOn) {
@@ -861,8 +887,13 @@
CameraMode.toLabelString(cameraMode)));
}
+ if (isHighSpeedOn && !mHighSpeedResolver.isHighSpeedSupported()) {
+ throw new IllegalArgumentException(
+ "High-speed session is not supported on this device.");
+ }
+
return FeatureSettings.of(cameraMode, requiredMaxBitDepth, isPreviewStabilizationOn,
- isUltraHdrOn);
+ isUltraHdrOn, isHighSpeedOn);
}
/**
@@ -943,14 +974,15 @@
}
private int getMaxSupportedFpsFromAttachedSurfaces(
- @NonNull List<AttachedSurfaceInfo> attachedSurfaces) {
+ @NonNull List<AttachedSurfaceInfo> attachedSurfaces, boolean isHighSpeedOn) {
int existingSurfaceFrameRateCeiling = Integer.MAX_VALUE;
for (AttachedSurfaceInfo attachedSurfaceInfo : attachedSurfaces) {
//get the fps ceiling for existing surfaces
existingSurfaceFrameRateCeiling = getUpdatedMaximumFps(
existingSurfaceFrameRateCeiling,
- attachedSurfaceInfo.getImageFormat(), attachedSurfaceInfo.getSize());
+ attachedSurfaceInfo.getImageFormat(), attachedSurfaceInfo.getSize(),
+ isHighSpeedOn);
}
return existingSurfaceFrameRateCeiling;
@@ -981,7 +1013,8 @@
int maxFrameRate = Integer.MAX_VALUE;
// Filters the sizes with frame rate only if there is target FPS setting
if (targetFpsRange != null) {
- maxFrameRate = getMaxFrameRate(mCharacteristics, imageFormat, size);
+ maxFrameRate = getMaxFrameRate(imageFormat, size,
+ featureSettings.isHighSpeedOn());
}
Set<Integer> uniqueMaxFrameRates = configSizeUniqueMaxFpsMap.get(configSize);
// Creates an empty FPS list for the config size when it doesn't exist.
@@ -1027,6 +1060,7 @@
private Pair<List<SurfaceConfig>, Integer> getSurfaceConfigListAndFpsCeiling(
@CameraMode.Mode int cameraMode,
+ boolean isHighSpeedOn,
List<AttachedSurfaceInfo> attachedSurfaces,
List<Size> possibleSizeList, List<UseCaseConfig<?>> newUseCaseConfigs,
List<Integer> useCasesPriorityOrder,
@@ -1063,7 +1097,7 @@
currentConfigFramerateCeiling = getUpdatedMaximumFps(
currentConfigFramerateCeiling,
newUseCase.getInputFormat(),
- size);
+ size, isHighSpeedOn);
}
return new Pair<>(surfaceConfigList, currentConfigFramerateCeiling);
}
@@ -1319,6 +1353,21 @@
}
}
+ private void generateHighSpeedSupportedCombinationList() {
+ if (!mHighSpeedResolver.isHighSpeedSupported()) {
+ return;
+ }
+ mHighSpeedSurfaceCombinations.clear();
+ // Find maximum supported size.
+ Size maxSize = mHighSpeedResolver.getMaxSize();
+ if (maxSize != null) {
+ mHighSpeedSurfaceCombinations.addAll(
+ GuaranteedConfigurationsUtil.generateHighSpeedSupportedCombinationList(maxSize,
+ getUpdatedSurfaceSizeDefinitionByFormat(
+ ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE)));
+ }
+ }
+
private void checkCustomization() {
// TODO(b/119466260): Integrate found feasible stream combinations into supported list
}
@@ -1540,9 +1589,9 @@
abstract static class FeatureSettings {
static @NonNull FeatureSettings of(@CameraMode.Mode int cameraMode,
@RequiredMaxBitDepth int requiredMaxBitDepth, boolean isPreviewStabilizationOn,
- boolean isUltraHdrOn) {
+ boolean isUltraHdrOn, boolean isHighSpeedOn) {
return new AutoValue_SupportedSurfaceCombination_FeatureSettings(cameraMode,
- requiredMaxBitDepth, isPreviewStabilizationOn, isUltraHdrOn);
+ requiredMaxBitDepth, isPreviewStabilizationOn, isUltraHdrOn, isHighSpeedOn);
}
/**
@@ -1580,5 +1629,10 @@
* Whether the Ultra HDR image capture is enabled.
*/
abstract boolean isUltraHdrOn();
+
+ /**
+ * Whether the high-speed capture is enabled.
+ */
+ abstract boolean isHighSpeedOn();
}
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSession.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSession.java
index faab52d..9993a25 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSession.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSession.java
@@ -18,6 +18,7 @@
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.params.SessionConfiguration;
@@ -182,6 +183,16 @@
throws CameraAccessException;
/**
+ * Create a unmodifiable list of requests that is suitable for constrained high speed capture
+ * session streaming.
+ *
+ * @see CameraConstrainedHighSpeedCaptureSession#createHighSpeedRequestList(CaptureRequest)
+ */
+ @NonNull
+ List<CaptureRequest> createHighSpeedRequestList(@NonNull CaptureRequest request)
+ throws CameraAccessException;
+
+ /**
* Submit a request for an image to be captured by the camera device.
*
* <p>The behavior of this method matches that of
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionBaseImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionBaseImpl.java
index df7283c..1e43fc9 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionBaseImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionBaseImpl.java
@@ -16,8 +16,11 @@
package androidx.camera.camera2.internal;
+import static java.util.Collections.emptyList;
+
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CaptureRequest;
import android.os.Build;
@@ -398,6 +401,21 @@
}
@Override
+ @NonNull
+ public List<CaptureRequest> createHighSpeedRequestList(@NonNull CaptureRequest request)
+ throws CameraAccessException {
+ CameraCaptureSession cameraCaptureSession =
+ Preconditions.checkNotNull(mCameraCaptureSessionCompat).toCameraCaptureSession();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+ && cameraCaptureSession instanceof CameraConstrainedHighSpeedCaptureSession) {
+ return Api23Impl.createHighSpeedRequestList(
+ (CameraConstrainedHighSpeedCaptureSession) cameraCaptureSession, request);
+ } else {
+ return emptyList();
+ }
+ }
+
+ @Override
public int captureSingleRequest(@NonNull CaptureRequest request, @NonNull Executor executor,
CameraCaptureSession.@NonNull CaptureCallback listener) throws CameraAccessException {
Preconditions.checkNotNull(mCameraCaptureSessionCompat,
@@ -622,5 +640,13 @@
static Surface getInputSurface(CameraCaptureSession cameraCaptureSession) {
return cameraCaptureSession.getInputSurface();
}
+
+ @NonNull
+ static List<CaptureRequest> createHighSpeedRequestList(
+ @NonNull CameraConstrainedHighSpeedCaptureSession constrainedHighSpeedSession,
+ @NonNull CaptureRequest captureRequest)
+ throws CameraAccessException {
+ return constrainedHighSpeedSession.createHighSpeedRequestList(captureRequest);
+ }
}
}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/HighSpeedResolverTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/HighSpeedResolverTest.kt
new file mode 100644
index 0000000..5163753
--- /dev/null
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/HighSpeedResolverTest.kt
@@ -0,0 +1,304 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.internal
+
+import android.graphics.ImageFormat
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.params.StreamConfigurationMap
+import android.os.Build
+import android.util.Range
+import android.util.Size
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat
+import androidx.camera.core.impl.ImageFormatConstants
+import androidx.camera.core.impl.UseCaseConfig
+import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType
+import androidx.camera.core.impl.utils.CompareSizesByArea
+import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_1080P
+import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_480P
+import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_720P
+import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_VGA
+import androidx.camera.testing.impl.EncoderProfilesUtil.RESOLUTION_2160P
+import androidx.camera.testing.impl.fakes.FakeUseCaseConfig
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadow.api.Shadow
+import org.robolectric.shadows.ShadowCameraCharacteristics
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class HighSpeedResolverTest {
+
+ private companion object {
+ private const val CAMERA_ID = "0"
+
+ private const val FPS_30 = 30
+ private const val FPS_120 = 120
+ private const val FPS_240 = 240
+ private const val FPS_480 = 480
+
+ private val RANGE_30_120 = Range.create(FPS_30, FPS_120)
+ private val RANGE_120_120 = Range.create(FPS_120, FPS_120)
+ private val RANGE_30_240 = Range.create(FPS_30, FPS_240)
+ private val RANGE_240_240 = Range.create(FPS_240, FPS_240)
+ private val RANGE_30_480 = Range.create(FPS_30, FPS_480)
+ private val RANGE_480_480 = Range.create(FPS_480, FPS_480)
+
+ private const val FORMAT_PRIVATE =
+ ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
+
+ private val COMMON_HIGH_SPEED_SUPPORTED_SIZE_FPS_MAP =
+ mapOf(
+ RESOLUTION_1080P to
+ listOf(
+ RANGE_30_120,
+ RANGE_120_120,
+ RANGE_30_240,
+ RANGE_240_240,
+ ),
+ RESOLUTION_720P to
+ listOf(
+ RANGE_30_120,
+ RANGE_120_120,
+ RANGE_30_240,
+ RANGE_240_240,
+ RANGE_30_480,
+ RANGE_480_480
+ )
+ )
+ }
+
+ private val defaultHighSpeedResolver = createHighSpeedResolver()
+ private val emptyHighSpeedResolver =
+ createHighSpeedResolver(createCharacteristics(supportedHighSpeedSizeAndFpsMap = emptyMap()))
+
+ @Test
+ fun filterCommonSupportedSizes_returnsCorrectMap() {
+ val useCaseSupportedSizeMap =
+ listOf(
+ listOf(RESOLUTION_480P, RESOLUTION_720P, RESOLUTION_1080P),
+ listOf(RESOLUTION_1080P, RESOLUTION_720P, RESOLUTION_2160P),
+ listOf(RESOLUTION_480P, RESOLUTION_720P, RESOLUTION_1080P, RESOLUTION_VGA)
+ )
+ .toUseCaseSupportedSizeMap()
+
+ val result = defaultHighSpeedResolver.filterCommonSupportedSizes(useCaseSupportedSizeMap)
+
+ // Assert: return common sizes and preserve the original order.
+ assertThat(result.values)
+ .containsExactly(
+ listOf(RESOLUTION_720P, RESOLUTION_1080P),
+ listOf(RESOLUTION_1080P, RESOLUTION_720P),
+ listOf(RESOLUTION_720P, RESOLUTION_1080P)
+ )
+ .inOrder()
+ }
+
+ @Test
+ fun getMaxSize_noSupportedSizes_returnsNull() {
+ val result = emptyHighSpeedResolver.maxSize
+
+ assertThat(result).isNull()
+ }
+
+ @Test
+ fun getMaxSize_supportedSizesExist_returnsLargestSize() {
+ val result = defaultHighSpeedResolver.maxSize
+
+ assertThat(result).isEqualTo(RESOLUTION_1080P)
+ }
+
+ @Test
+ fun getMaxFrameRate_unsupportedImageFormat_returnsZero() {
+ val result = defaultHighSpeedResolver.getMaxFrameRate(ImageFormat.JPEG, RESOLUTION_1080P)
+
+ assertThat(result).isEqualTo(0)
+ }
+
+ @Test
+ fun getMaxFrameRate_noSupportedFpsRanges_returnsZero() {
+ val result = emptyHighSpeedResolver.getMaxFrameRate(FORMAT_PRIVATE, RESOLUTION_1080P)
+
+ assertThat(result).isEqualTo(0)
+ }
+
+ @Test
+ fun getMaxFrameRate_supportedFpsRangesExist_returnMaxFps() {
+ val result = defaultHighSpeedResolver.getMaxFrameRate(FORMAT_PRIVATE, RESOLUTION_1080P)
+
+ assertThat(result).isEqualTo(FPS_240)
+ }
+
+ @Test
+ fun getSizeArrangements_emptyInput_returnEmptyList() {
+ val sizeArrangements = defaultHighSpeedResolver.getSizeArrangements(emptyList())
+
+ assertThat(sizeArrangements).isEmpty()
+ }
+
+ @Test
+ fun getSizeArrangements_hasCommonSizes_returnCorrectArrangements() {
+ val common1080p = RESOLUTION_1080P
+ val common720p = RESOLUTION_720P
+ val supportedOutputSizesList =
+ listOf(
+ listOf(RESOLUTION_480P, common720p, common1080p),
+ listOf(common1080p, common720p, RESOLUTION_2160P),
+ listOf(RESOLUTION_480P, common720p, common1080p, RESOLUTION_VGA)
+ )
+ val sizeArrangements =
+ defaultHighSpeedResolver.getSizeArrangements(supportedOutputSizesList)
+
+ assertThat(sizeArrangements)
+ .containsExactly(
+ listOf(common720p, common720p, common720p),
+ listOf(common1080p, common1080p, common1080p)
+ )
+ .inOrder()
+ }
+
+ @Test
+ fun getSizeArrangements_noCommonSizes_returnEmptyList() {
+ val supportedOutputSizesList =
+ listOf(
+ listOf(RESOLUTION_480P, RESOLUTION_720P),
+ listOf(RESOLUTION_1080P, RESOLUTION_2160P),
+ listOf(RESOLUTION_1080P, RESOLUTION_720P)
+ )
+
+ val result = defaultHighSpeedResolver.getSizeArrangements(supportedOutputSizesList)
+
+ assertThat(result).isEmpty()
+ }
+
+ @Test
+ fun getFrameRateRangesFor_invalidInput_returnsNull() {
+ // More than 2 surfaces.
+ assertThat(
+ defaultHighSpeedResolver.getFrameRateRangesFor(
+ listOf(RESOLUTION_720P, RESOLUTION_720P, RESOLUTION_720P)
+ )
+ )
+ .isNull()
+
+ // Different sizes.
+ assertThat(
+ defaultHighSpeedResolver.getFrameRateRangesFor(
+ listOf(RESOLUTION_720P, RESOLUTION_1080P)
+ )
+ )
+ .isNull()
+
+ // Empty list.
+ assertThat(defaultHighSpeedResolver.getFrameRateRangesFor(emptyList())).isNull()
+ }
+
+ @Test
+ fun getFrameRateRangesFor_noSupportedSizes_returnsNull() {
+ assertThat(emptyHighSpeedResolver.getFrameRateRangesFor(listOf(RESOLUTION_720P))).isNull()
+ }
+
+ @Test
+ fun getFrameRateRangesFor_oneSurface_returnsAllSupportedRanges() {
+ val result = defaultHighSpeedResolver.getFrameRateRangesFor(listOf(RESOLUTION_720P))
+
+ assertThat(result!!.toList())
+ .containsExactly(
+ RANGE_30_120,
+ RANGE_120_120,
+ RANGE_30_240,
+ RANGE_240_240,
+ RANGE_30_480,
+ RANGE_480_480
+ )
+ }
+
+ @Test
+ fun getFrameRateRangesFor_twoSurfaces_returnsFixedFpsRanges() {
+ val result =
+ defaultHighSpeedResolver.getFrameRateRangesFor(listOf(RESOLUTION_720P, RESOLUTION_720P))
+
+ assertThat(result!!.toList())
+ .containsExactly(RANGE_120_120, RANGE_240_240, RANGE_480_480)
+ .inOrder()
+ }
+
+ private fun createHighSpeedResolver(
+ characteristics: CameraCharacteristicsCompat = createCharacteristics(),
+ ): HighSpeedResolver {
+ return HighSpeedResolver(characteristics = characteristics)
+ }
+
+ private fun createCharacteristics(
+ cameraId: String = CAMERA_ID,
+ hardwareLevel: Int = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
+ supportedHighSpeedSizeAndFpsMap: Map<Size, List<Range<Int>>>? =
+ COMMON_HIGH_SPEED_SUPPORTED_SIZE_FPS_MAP,
+ ): CameraCharacteristicsCompat {
+ val mockMap =
+ Mockito.mock(StreamConfigurationMap::class.java).also { map ->
+ if (supportedHighSpeedSizeAndFpsMap != null) {
+ // Mock highSpeedVideoSizes
+ Mockito.`when`(map.highSpeedVideoSizes)
+ .thenReturn(supportedHighSpeedSizeAndFpsMap.keys.toTypedArray())
+
+ // Mock highSpeedVideoFpsRanges
+ val allFpsRanges = supportedHighSpeedSizeAndFpsMap.values.flatten().distinct()
+ Mockito.`when`(map.highSpeedVideoFpsRanges)
+ .thenReturn(allFpsRanges.toTypedArray())
+
+ // Mock getHighSpeedVideoSizesFor
+ allFpsRanges.forEach { fpsRange ->
+ val sizesForRange =
+ supportedHighSpeedSizeAndFpsMap.entries
+ .filter { (_, fpsRanges) -> fpsRanges.contains(fpsRange) }
+ .map { it.key }
+ .sortedWith(CompareSizesByArea(false)) // Descending order
+ .toTypedArray()
+ Mockito.`when`(map.getHighSpeedVideoSizesFor(fpsRange))
+ .thenReturn(sizesForRange)
+ }
+
+ // Mock getHighSpeedVideoFpsRangesFor
+ supportedHighSpeedSizeAndFpsMap.forEach { (size, fpsRanges) ->
+ Mockito.`when`(map.getHighSpeedVideoFpsRangesFor(size))
+ .thenReturn(fpsRanges.toTypedArray())
+ }
+ }
+ }
+
+ val characteristics = ShadowCameraCharacteristics.newCameraCharacteristics()
+ Shadow.extract<ShadowCameraCharacteristics>(characteristics).apply {
+ set(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, hardwareLevel)
+ set(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP, mockMap)
+ }
+
+ return CameraCharacteristicsCompat.toCameraCharacteristicsCompat(characteristics, cameraId)
+ }
+
+ private fun List<List<Size>>.toUseCaseSupportedSizeMap(): Map<UseCaseConfig<*>, List<Size>> {
+ return associate { sizes ->
+ FakeUseCaseConfig.Builder(CaptureType.PREVIEW, FORMAT_PRIVATE).build().currentConfig to
+ sizes
+ }
+ }
+}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/StreamUseCaseTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/StreamUseCaseTest.java
index d09a141..f9d9cba 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/StreamUseCaseTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/StreamUseCaseTest.java
@@ -240,7 +240,8 @@
CameraMode.CONCURRENT_CAMERA,
BIT_DEPTH_8_BIT,
/*isPreviewStabilizationOn=*/false,
- /*isUltraHdrOn=*/ false
+ /*isUltraHdrOn=*/ false,
+ /*isHighSpeedOn=*/ false
);
assertFalse(shouldUseStreamUseCase(featureSettings));
}
@@ -251,7 +252,8 @@
CameraMode.DEFAULT,
BIT_DEPTH_10_BIT,
/*isPreviewStabilizationOn=*/false,
- /*isUltraHdrOn=*/ false
+ /*isUltraHdrOn=*/ false,
+ /*isHighSpeedOn=*/ false
);
assertFalse(shouldUseStreamUseCase(featureSettings));
}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
index aa25062..cf9f36b 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
@@ -25,6 +25,7 @@
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CameraMetadata
import android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+import android.hardware.camera2.CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO
import android.hardware.camera2.CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT
import android.hardware.camera2.params.DynamicRangeProfiles
import android.hardware.camera2.params.DynamicRangeProfiles.DOLBY_VISION_10B_HDR_OEM
@@ -69,12 +70,14 @@
import androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
import androidx.camera.core.impl.ImageInputConfig
import androidx.camera.core.impl.StreamSpec
+import androidx.camera.core.impl.SurfaceCombination
import androidx.camera.core.impl.SurfaceConfig
import androidx.camera.core.impl.SurfaceConfig.ConfigSize
import androidx.camera.core.impl.SurfaceConfig.ConfigType
import androidx.camera.core.impl.UseCaseConfig
import androidx.camera.core.impl.UseCaseConfigFactory
import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType
+import androidx.camera.core.impl.utils.CompareSizesByArea
import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_1080P
import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_1440P
import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_720P
@@ -149,6 +152,25 @@
arrayOf(
Size(8000, 6000), // 4:3
)
+private val COMMON_HIGH_SPEED_SUPPORTED_SIZE_FPS_MAP =
+ mapOf(
+ RESOLUTION_1080P to
+ listOf(
+ Range.create(30, 120),
+ Range.create(120, 120),
+ Range.create(30, 240),
+ Range.create(240, 240),
+ ),
+ RESOLUTION_720P to
+ listOf(
+ Range.create(30, 120),
+ Range.create(120, 120),
+ Range.create(30, 240),
+ Range.create(240, 240),
+ Range.create(30, 480),
+ Range.create(480, 480)
+ )
+ )
/** Robolectric test for [SupportedSurfaceCombination] class */
@RunWith(RobolectricTestRunner::class)
@@ -598,6 +620,68 @@
}
}
+ @Config(minSdk = Build.VERSION_CODES.M)
+ @Test
+ fun checkSurfaceCombinationSupportForHighSpeed() {
+ setupCameraAndInitCameraX(
+ capabilities = intArrayOf(REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO),
+ supportedHighSpeedSizeAndFpsMap = COMMON_HIGH_SPEED_SUPPORTED_SIZE_FPS_MAP
+ )
+ val supportedSurfaceCombination =
+ SupportedSurfaceCombination(
+ context,
+ DEFAULT_CAMERA_ID,
+ cameraManagerCompat!!,
+ mockCamcorderProfileHelper
+ )
+
+ // The expected SurfaceConfig is PRIV + RECORD because the max high speed size 1920x1080 is
+ // between PREVIEW and RECORD size.
+ val shouldSupportCombinations =
+ listOf(
+ SurfaceCombination().apply {
+ addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.RECORD))
+ },
+ SurfaceCombination().apply {
+ addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.PREVIEW))
+ addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.PREVIEW))
+ }
+ )
+ shouldSupportCombinations.forEach {
+ assertThat(
+ supportedSurfaceCombination.checkSupported(
+ createFeatureSettings(isHighSpeedOn = true),
+ it.surfaceConfigList
+ )
+ )
+ .isTrue()
+ }
+
+ val shouldNotSupportCombinations =
+ listOf(
+ SurfaceCombination().apply {
+ addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.MAXIMUM))
+ },
+ SurfaceCombination().apply {
+ addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.PREVIEW))
+ addSurfaceConfig(SurfaceConfig.create(ConfigType.JPEG, ConfigSize.MAXIMUM))
+ },
+ SurfaceCombination().apply {
+ addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.PREVIEW))
+ addSurfaceConfig(SurfaceConfig.create(ConfigType.YUV, ConfigSize.PREVIEW))
+ }
+ )
+ shouldNotSupportCombinations.forEach {
+ assertThat(
+ supportedSurfaceCombination.checkSupported(
+ createFeatureSettings(isHighSpeedOn = true),
+ it.surfaceConfigList
+ )
+ )
+ .isFalse()
+ }
+ }
+
// //////////////////////////////////////////////////////////////////////////////////////////
//
// Surface config transformation tests
@@ -1601,6 +1685,7 @@
private fun getSuggestedSpecsAndVerify(
useCasesExpectedSizeMap: Map<UseCase, Size>,
+ useCasesOutputSizesMap: Map<UseCase, List<Size>>? = null,
attachedSurfaceInfoList: List<AttachedSurfaceInfo> = emptyList(),
hardwareLevel: Int = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
capabilities: IntArray? = null,
@@ -1611,8 +1696,10 @@
default10BitProfile: Long? = null,
useCasesExpectedDynamicRangeMap: Map<UseCase, DynamicRange> = emptyMap(),
supportedOutputFormats: IntArray? = null,
+ supportedHighSpeedSizeAndFpsMap: Map<Size, List<Range<Int>>>? = null,
isPreviewStabilizationOn: Boolean = false,
- hasVideoCapture: Boolean = false
+ hasVideoCapture: Boolean = false,
+ targetHighSpeedFpsRange: Range<Int>? = null,
): Pair<Map<UseCaseConfig<*>, StreamSpec>, Map<AttachedSurfaceInfo, StreamSpec>> {
setupCameraAndInitCameraX(
hardwareLevel = hardwareLevel,
@@ -1620,6 +1707,7 @@
dynamicRangeProfiles = dynamicRangeProfiles,
default10BitProfile = default10BitProfile,
supportedFormats = supportedOutputFormats,
+ supportedHighSpeedSizeAndFpsMap = supportedHighSpeedSizeAndFpsMap,
)
val supportedSurfaceCombination =
SupportedSurfaceCombination(
@@ -1628,17 +1716,19 @@
cameraManagerCompat!!,
mockCamcorderProfileHelper
)
-
val useCaseConfigMap = getUseCaseToConfigMap(useCasesExpectedSizeMap.keys.toList())
val useCaseConfigToOutputSizesMap =
- getUseCaseConfigToOutputSizesMap(useCaseConfigMap.values.toList())
+ useCaseConfigMap.entries.associate { (useCase, config) ->
+ config to (useCasesOutputSizesMap?.get(useCase) ?: DEFAULT_SUPPORTED_SIZES.toList())
+ }
val resultPair =
supportedSurfaceCombination.getSuggestedStreamSpecifications(
cameraMode,
attachedSurfaceInfoList,
useCaseConfigToOutputSizesMap,
isPreviewStabilizationOn,
- hasVideoCapture
+ hasVideoCapture,
+ targetHighSpeedFpsRange
)
val suggestedStreamSpecsForNewUseCases = resultPair.first
val suggestedStreamSpecsForOldSurfaces = resultPair.second
@@ -1723,17 +1813,6 @@
return useCaseConfigMap
}
- private fun getUseCaseConfigToOutputSizesMap(
- useCaseConfigs: List<UseCaseConfig<*>>
- ): Map<UseCaseConfig<*>, List<Size>> {
- val resultMap =
- mutableMapOf<UseCaseConfig<*>, List<Size>>().apply {
- useCaseConfigs.forEach { put(it, DEFAULT_SUPPORTED_SIZES.toList()) }
- }
-
- return resultMap
- }
-
// //////////////////////////////////////////////////////////////////////////////////////////
//
// Resolution selection tests for Ultra HDR
@@ -3308,6 +3387,137 @@
.isFalse()
}
+ @Config(minSdk = Build.VERSION_CODES.M)
+ @Test
+ fun getSuggestedStreamSpec_highSpeed_returnsCorrectSizeAndFpsRange() {
+ val previewUseCase = createUseCase(CaptureType.PREVIEW, surfaceOccupancyPriority = 2)
+ val videoUseCase = createUseCase(CaptureType.VIDEO_CAPTURE, surfaceOccupancyPriority = 5)
+ val useCasesOutputSizesMap =
+ mapOf(
+ previewUseCase to listOf(RESOLUTION_VGA, RESOLUTION_1080P, RESOLUTION_720P),
+ videoUseCase to listOf(RESOLUTION_1440P, RESOLUTION_720P, RESOLUTION_1080P)
+ )
+ // videoUseCase has higher surface priority so the expected size should be the first
+ // common size of videoUseCases. i.e. RESOLUTION_720P.
+ val useCaseExpectedResultMap =
+ mapOf(previewUseCase to RESOLUTION_720P, videoUseCase to RESOLUTION_720P)
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ capabilities = intArrayOf(REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO),
+ useCasesOutputSizesMap = useCasesOutputSizesMap,
+ supportedHighSpeedSizeAndFpsMap = COMMON_HIGH_SPEED_SUPPORTED_SIZE_FPS_MAP,
+ targetHighSpeedFpsRange = Range.create(240, 240),
+ compareExpectedFps = Range.create(240, 240)
+ )
+ }
+
+ @Config(minSdk = Build.VERSION_CODES.M)
+ @Test
+ fun getSuggestedStreamSpec_highSpeed_singleSurface_returnsCorrectSizeAndClosestFps() {
+ val previewUseCase = createUseCase(CaptureType.PREVIEW)
+ val useCasesOutputSizesMap = mapOf(previewUseCase to listOf(RESOLUTION_1080P))
+ val useCaseExpectedResultMap = mapOf(previewUseCase to RESOLUTION_1080P)
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ capabilities = intArrayOf(REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO),
+ useCasesOutputSizesMap = useCasesOutputSizesMap,
+ supportedHighSpeedSizeAndFpsMap = COMMON_HIGH_SPEED_SUPPORTED_SIZE_FPS_MAP,
+ targetHighSpeedFpsRange = Range.create(30, 480),
+ compareExpectedFps = Range.create(30, 240) // Find the closest supported fps.
+ )
+ }
+
+ @Config(minSdk = Build.VERSION_CODES.M)
+ @Test
+ fun getSuggestedStreamSpec_highSpeed_multipleSurfaces_returnsCorrectSizeAndClosetMaxFps() {
+ val previewUseCase = createUseCase(CaptureType.PREVIEW)
+ val videoUseCase = createUseCase(CaptureType.VIDEO_CAPTURE)
+ val useCasesOutputSizesMap =
+ mapOf(
+ previewUseCase to listOf(RESOLUTION_1080P),
+ videoUseCase to listOf(RESOLUTION_1080P)
+ )
+ val useCaseExpectedResultMap =
+ mapOf(previewUseCase to RESOLUTION_1080P, videoUseCase to RESOLUTION_1080P)
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ capabilities = intArrayOf(REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO),
+ useCasesOutputSizesMap = useCasesOutputSizesMap,
+ supportedHighSpeedSizeAndFpsMap = COMMON_HIGH_SPEED_SUPPORTED_SIZE_FPS_MAP,
+ targetHighSpeedFpsRange = Range.create(30, 480),
+ compareExpectedFps = Range.create(240, 240) // Find the closest max supported fps.
+ )
+ }
+
+ @Config(minSdk = 21, maxSdk = 22)
+ @Test
+ fun getSuggestedStreamSpec_highSpeed_unsupportedSdkVersion_throwException() {
+ val useCase = createUseCase(CaptureType.PREVIEW)
+ val useCaseExpectedResultMap = mapOf(useCase to RESOLUTION_1080P)
+ assertThrows(IllegalArgumentException::class.java) {
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ capabilities =
+ intArrayOf(REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO),
+ targetHighSpeedFpsRange = Range.create(240, 240),
+ )
+ }
+ }
+
+ @Config(minSdk = Build.VERSION_CODES.M)
+ @Test
+ fun getSuggestedStreamSpec_highSpeed_noCommonSize_throwException() {
+ val previewUseCase = createUseCase(CaptureType.PREVIEW)
+ val videoUseCase = createUseCase(CaptureType.VIDEO_CAPTURE)
+ val useCasesOutputSizesMap =
+ mapOf(
+ previewUseCase to listOf(RESOLUTION_VGA, RESOLUTION_720P),
+ videoUseCase to listOf(RESOLUTION_1440P, RESOLUTION_1080P)
+ )
+ val useCaseExpectedResultMap =
+ mapOf(previewUseCase to RESOLUTION_VGA, videoUseCase to RESOLUTION_1440P)
+ assertThrows(IllegalArgumentException::class.java) {
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ capabilities =
+ intArrayOf(REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO),
+ useCasesOutputSizesMap = useCasesOutputSizesMap,
+ supportedHighSpeedSizeAndFpsMap = COMMON_HIGH_SPEED_SUPPORTED_SIZE_FPS_MAP,
+ targetHighSpeedFpsRange = Range.create(240, 240),
+ )
+ }
+ }
+
+ @Config(minSdk = Build.VERSION_CODES.M)
+ @Test
+ fun getSuggestedStreamSpec_highSpeed_tooManyUseCases_throwException() {
+ val previewUseCase1 = createUseCase(CaptureType.PREVIEW)
+ val previewUseCase2 = createUseCase(CaptureType.PREVIEW)
+ val videoUseCase = createUseCase(CaptureType.VIDEO_CAPTURE)
+ val useCasesOutputSizesMap =
+ mapOf(
+ previewUseCase1 to listOf(RESOLUTION_1080P),
+ previewUseCase2 to listOf(RESOLUTION_1080P),
+ videoUseCase to listOf(RESOLUTION_1080P)
+ )
+ val useCaseExpectedResultMap =
+ mapOf(
+ previewUseCase1 to RESOLUTION_1080P,
+ previewUseCase2 to RESOLUTION_1080P,
+ videoUseCase to RESOLUTION_1080P
+ )
+ assertThrows(IllegalArgumentException::class.java) {
+ getSuggestedSpecsAndVerify(
+ useCaseExpectedResultMap,
+ capabilities =
+ intArrayOf(REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO),
+ useCasesOutputSizesMap = useCasesOutputSizesMap,
+ supportedHighSpeedSizeAndFpsMap = COMMON_HIGH_SPEED_SUPPORTED_SIZE_FPS_MAP,
+ targetHighSpeedFpsRange = Range.create(240, 240),
+ )
+ }
+ }
+
/**
* Sets up camera according to the specified settings and initialize [CameraX].
*
@@ -3322,6 +3532,8 @@
* [DEFAULT_SUPPORTED_SIZES].
* @param supportedHighResolutionSizes the high resolution supported sizes of the camera.
* Default value is null.
+ * @param supportedHighSpeedSizeAndFpsMap a map of supported high speed video sizes to their
+ * corresponding lists of supported FPS ranges. Default value is null.
* @param maximumResolutionSupportedSizes the maximum resolution mode supported sizes of the
* camera. Default value is null.
* @param maximumResolutionHighResolutionSupportedSizes the maximum resolution mode high
@@ -3336,6 +3548,7 @@
supportedSizes: Array<Size>? = DEFAULT_SUPPORTED_SIZES,
supportedFormats: IntArray? = null,
supportedHighResolutionSizes: Array<Size>? = null,
+ supportedHighSpeedSizeAndFpsMap: Map<Size, List<Range<Int>>>? = null,
maximumResolutionSupportedSizes: Array<Size>? = null,
maximumResolutionHighResolutionSupportedSizes: Array<Size>? = null,
dynamicRangeProfiles: DynamicRangeProfiles? = null,
@@ -3350,6 +3563,7 @@
supportedSizes,
supportedFormats,
supportedHighResolutionSizes,
+ supportedHighSpeedSizeAndFpsMap,
maximumResolutionSupportedSizes,
maximumResolutionHighResolutionSupportedSizes,
dynamicRangeProfiles,
@@ -3400,6 +3614,8 @@
* @param supportedFormats the supported output formats of the camera. Default value is null.
* @param supportedHighResolutionSizes the high resolution supported sizes of the camera.
* Default value is null.
+ * @param supportedHighSpeedSizeAndFpsMap a map of supported high speed video sizes to their
+ * corresponding lists of supported FPS ranges. Default value is null.
* @param maximumResolutionSupportedSizes the maximum resolution mode supported sizes of the
* camera. Default value is null.
* @param maximumResolutionHighResolutionSupportedSizes the maximum resolution mode high
@@ -3414,6 +3630,7 @@
supportedSizes: Array<Size>? = DEFAULT_SUPPORTED_SIZES,
supportedFormats: IntArray? = null,
supportedHighResolutionSizes: Array<Size>? = null,
+ supportedHighSpeedSizeAndFpsMap: Map<Size, List<Range<Int>>>? = null,
maximumResolutionSupportedSizes: Array<Size>? = null,
maximumResolutionHighResolutionSupportedSizes: Array<Size>? = null,
dynamicRangeProfiles: DynamicRangeProfiles? = null,
@@ -3515,6 +3732,38 @@
Mockito.`when`(map.getHighResolutionOutputSizes(ArgumentMatchers.anyInt()))
.thenReturn(supportedHighResolutionSizes)
}
+
+ if (
+ supportedHighSpeedSizeAndFpsMap != null &&
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
+ ) {
+ // Mock highSpeedVideoSizes
+ Mockito.`when`(map.highSpeedVideoSizes)
+ .thenReturn(supportedHighSpeedSizeAndFpsMap.keys.toTypedArray())
+
+ // Mock highSpeedVideoFpsRanges
+ val allFpsRanges = supportedHighSpeedSizeAndFpsMap.values.flatten().distinct()
+ Mockito.`when`(map.highSpeedVideoFpsRanges)
+ .thenReturn(allFpsRanges.toTypedArray())
+
+ // Mock getHighSpeedVideoSizesFor
+ allFpsRanges.forEach { fpsRange ->
+ val sizesForRange =
+ supportedHighSpeedSizeAndFpsMap.entries
+ .filter { (_, fpsRanges) -> fpsRanges.contains(fpsRange) }
+ .map { it.key }
+ .sortedWith(CompareSizesByArea(false)) // Descending order
+ .toTypedArray()
+ Mockito.`when`(map.getHighSpeedVideoSizesFor(fpsRange))
+ .thenReturn(sizesForRange)
+ }
+
+ // Mock getHighSpeedVideoFpsRangesFor
+ supportedHighSpeedSizeAndFpsMap.forEach { (size, fpsRanges) ->
+ Mockito.`when`(map.getHighSpeedVideoFpsRangesFor(size))
+ .thenReturn(fpsRanges.toTypedArray())
+ }
+ }
}
val maximumResolutionMap =
@@ -3651,9 +3900,16 @@
private fun createUseCase(
captureType: CaptureType,
targetFrameRate: Range<Int>? = null,
- dynamicRange: DynamicRange = DynamicRange.UNSPECIFIED
+ dynamicRange: DynamicRange = DynamicRange.UNSPECIFIED,
+ surfaceOccupancyPriority: Int? = null
): UseCase {
- return createUseCase(captureType, targetFrameRate, dynamicRange, false)
+ return createUseCase(
+ captureType,
+ targetFrameRate,
+ dynamicRange,
+ streamUseCaseOverride = false,
+ surfaceOccupancyPriority = surfaceOccupancyPriority
+ )
}
private fun createUseCase(
@@ -3661,7 +3917,8 @@
targetFrameRate: Range<Int>? = null,
dynamicRange: DynamicRange = DynamicRange.UNSPECIFIED,
streamUseCaseOverride: Boolean = false,
- imageFormat: Int? = null
+ imageFormat: Int? = null,
+ surfaceOccupancyPriority: Int? = null,
): UseCase {
val builder =
FakeUseCaseConfig.Builder(
@@ -3691,6 +3948,8 @@
)
}
+ surfaceOccupancyPriority?.let { builder.setSurfaceOccupancyPriority(it) }
+
return builder.build()
}
@@ -3708,12 +3967,14 @@
@RequiredMaxBitDepth requiredMaxBitDepth: Int = BIT_DEPTH_8_BIT,
isPreviewStabilizationOn: Boolean = false,
isUltraHdrOn: Boolean = false,
+ isHighSpeedOn: Boolean = false,
): FeatureSettings {
return FeatureSettings.of(
cameraMode,
requiredMaxBitDepth,
isPreviewStabilizationOn,
- isUltraHdrOn
+ isUltraHdrOn,
+ isHighSpeedOn
)
}
}
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index 4f331a5..fe9cc3c 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -48,6 +48,7 @@
}
public final class BasicTooltipKt {
+ method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void BasicTooltipBox(androidx.compose.ui.window.PopupPositionProvider positionProvider, kotlin.jvm.functions.Function0<kotlin.Unit> tooltip, androidx.compose.foundation.BasicTooltipState state, optional androidx.compose.ui.Modifier modifier, optional boolean focusable, optional boolean enableUserInput, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public static androidx.compose.foundation.BasicTooltipState BasicTooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static androidx.compose.foundation.BasicTooltipState rememberBasicTooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
}
@@ -63,7 +64,7 @@
}
public final class BasicTooltip_androidKt {
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void BasicTooltipBox(androidx.compose.ui.window.PopupPositionProvider positionProvider, kotlin.jvm.functions.Function0<kotlin.Unit> tooltip, androidx.compose.foundation.BasicTooltipState state, optional androidx.compose.ui.Modifier modifier, optional boolean focusable, optional boolean enableUserInput, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @Deprecated @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void BasicTooltipBox(androidx.compose.ui.window.PopupPositionProvider positionProvider, kotlin.jvm.functions.Function0<kotlin.Unit> tooltip, androidx.compose.foundation.BasicTooltipState state, optional androidx.compose.ui.Modifier modifier, optional boolean focusable, optional boolean enableUserInput, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
public final class BorderKt {
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 1f847d1..db4b26e 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -48,6 +48,7 @@
}
public final class BasicTooltipKt {
+ method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void BasicTooltipBox(androidx.compose.ui.window.PopupPositionProvider positionProvider, kotlin.jvm.functions.Function0<kotlin.Unit> tooltip, androidx.compose.foundation.BasicTooltipState state, optional androidx.compose.ui.Modifier modifier, optional boolean focusable, optional boolean enableUserInput, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public static androidx.compose.foundation.BasicTooltipState BasicTooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static androidx.compose.foundation.BasicTooltipState rememberBasicTooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
}
@@ -63,7 +64,7 @@
}
public final class BasicTooltip_androidKt {
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void BasicTooltipBox(androidx.compose.ui.window.PopupPositionProvider positionProvider, kotlin.jvm.functions.Function0<kotlin.Unit> tooltip, androidx.compose.foundation.BasicTooltipState state, optional androidx.compose.ui.Modifier modifier, optional boolean focusable, optional boolean enableUserInput, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @Deprecated @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void BasicTooltipBox(androidx.compose.ui.window.PopupPositionProvider positionProvider, kotlin.jvm.functions.Function0<kotlin.Unit> tooltip, androidx.compose.foundation.BasicTooltipState state, optional androidx.compose.ui.Modifier modifier, optional boolean focusable, optional boolean enableUserInput, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
public final class BorderKt {
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/ScrollerBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/ScrollerBenchmark.kt
index e183e97..7828cd4 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/ScrollerBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/ScrollerBenchmark.kt
@@ -16,6 +16,8 @@
package androidx.compose.foundation.benchmark
+import androidx.compose.foundation.benchmark.lazy.doFramesUntilIdle
+import androidx.compose.testutils.ComposeExecutionControl
import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
import androidx.compose.testutils.benchmark.benchmarkDrawPerf
import androidx.compose.testutils.benchmark.benchmarkFirstCompose
@@ -27,6 +29,7 @@
import androidx.compose.testutils.benchmark.toggleStateBenchmarkDraw
import androidx.compose.testutils.benchmark.toggleStateBenchmarkLayout
import androidx.compose.testutils.benchmark.toggleStateBenchmarkMeasure
+import androidx.compose.ui.platform.ViewRootForTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.junit.Rule
@@ -84,6 +87,45 @@
}
@Test
+ fun mouseWheelScroll_initialScroll() {
+ with(benchmarkRule) {
+ runBenchmarkFor({ MouseWheelScrollerTestCase() }) {
+ measureRepeatedOnUiThread {
+ runWithTimingDisabled {
+ doFrame()
+ assertNoPendingRecompositionMeasureOrLayout()
+ }
+
+ performToggle(getTestCase())
+
+ runWithTimingDisabled {
+ // This benchmark only cares about initial cost of adding the scroll node
+ disposeContent()
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun mouseWheelScroll_successiveScrolls() {
+ with(benchmarkRule) {
+ runBenchmarkFor({ MouseWheelScrollerTestCase() }) {
+ runOnUiThread {
+ doFrame()
+ performToggle(getTestCase())
+ doFrame()
+ }
+
+ measureRepeatedOnUiThread {
+ performToggle(getTestCase())
+ runWithTimingDisabled { doFramesUntilIdle() }
+ }
+ }
+ }
+ }
+
+ @Test
fun layout() {
benchmarkRule.benchmarkLayoutPerf(scrollerCaseFactory)
}
@@ -93,3 +135,23 @@
benchmarkRule.benchmarkDrawPerf(scrollerCaseFactory)
}
}
+
+// Below are forked from LazyBenchmarkCommon
+private fun ComposeExecutionControl.performToggle(testCase: MouseWheelScrollerTestCase) {
+ testCase.toggleState()
+ if (hasPendingChanges()) {
+ recompose()
+ }
+ if (hasPendingMeasureOrLayout()) {
+ getViewRoot().measureAndLayoutForTest()
+ }
+}
+
+private fun ComposeExecutionControl.assertNoPendingRecompositionMeasureOrLayout() {
+ if (hasPendingChanges() || hasPendingMeasureOrLayout()) {
+ throw AssertionError("Expected no pending changes but there were some.")
+ }
+}
+
+private fun ComposeExecutionControl.getViewRoot(): ViewRootForTest =
+ getHostView() as ViewRootForTest
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/ScrollerTestCase.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/ScrollerTestCase.kt
index 4c202c1..275e985 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/ScrollerTestCase.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/ScrollerTestCase.kt
@@ -16,6 +16,9 @@
package androidx.compose.foundation.benchmark
+import android.view.InputDevice
+import android.view.MotionEvent
+import android.view.View
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.Column
@@ -28,12 +31,15 @@
import androidx.compose.testutils.ToggleableTestCase
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.runBlocking
/**
* Test case that puts a large number of boxes in a column in a vertical scroller to force
* scrolling.
+ *
+ * [toggleState] calls [ScrollState.scrollTo] between oscillating values
*/
class ScrollerTestCase : LayeredComposeTestCase(), ToggleableTestCase {
private lateinit var scrollState: ScrollState
@@ -42,37 +48,117 @@
override fun MeasuredContent() {
scrollState = rememberScrollState()
Column(Modifier.verticalScroll(scrollState)) {
- Column(Modifier.fillMaxHeight()) {
- for (green in 0..0xFF) {
- ColorStripe(0xFF, green, 0)
- }
- for (red in 0xFF downTo 0) {
- ColorStripe(red, 0xFF, 0)
- }
- for (blue in 0..0xFF) {
- ColorStripe(0, 0xFF, blue)
- }
- for (green in 0xFF downTo 0) {
- ColorStripe(0, green, 0xFF)
- }
- for (red in 0..0xFF) {
- ColorStripe(red, 0, 0xFF)
- }
- for (blue in 0xFF downTo 0) {
- ColorStripe(0xFF, 0, blue)
- }
- }
+ ColorStripes(step = 1, Modifier.fillMaxHeight())
}
}
override fun toggleState() {
runBlocking { scrollState.scrollTo(if (scrollState.value == 0) 10 else 0) }
}
+}
+
+/**
+ * Test case that puts a large number of boxes in a column in a vertical scroller to force
+ * scrolling.
+ *
+ * [toggleState] injects mouse wheel events to scroll forward and backwards repeatedly.
+ */
+class MouseWheelScrollerTestCase() : LayeredComposeTestCase(), ToggleableTestCase {
+ private lateinit var scrollState: ScrollState
+ private var view: View? = null
+ private var currentEventTime: Long = 0
+ private var lastScrollUp: Boolean = true
@Composable
- fun ColorStripe(red: Int, green: Int, blue: Int) {
- Canvas(Modifier.size(45.dp, 5.dp)) {
- drawRect(Color(red = red, green = green, blue = blue))
+ override fun MeasuredContent() {
+ view = LocalView.current
+ scrollState = rememberScrollState()
+ Column(Modifier.verticalScroll(scrollState)) {
+ // A lower step causes benchmark issues due to the resulting size / number of nodes
+ ColorStripes(step = 5, Modifier.fillMaxHeight())
}
}
+
+ override fun toggleState() {
+ // For mouse wheel scroll, negative values scroll down
+ // Note: these aren't the actual values that will be used to scroll, depending on
+ // Android version these values will get converted into a different (larger) value.
+ // This also unfortunately includes an animation that cannot be disabled, so this
+ // is a best effort at some repeated scrolling
+ val scrollAmount =
+ if (lastScrollUp) {
+ lastScrollUp = false
+ -10
+ } else {
+ lastScrollUp = true
+ 10
+ }
+ dispatchMouseWheelScroll(scrollAmount = scrollAmount, eventTime = currentEventTime, view!!)
+ currentEventTime += 10
+ }
+}
+
+@Composable
+private fun ColorStripes(step: Int, modifier: Modifier) {
+ Column(modifier) {
+ for (green in 0..0xFF step step) {
+ ColorStripe(0xFF, green, 0)
+ }
+ for (red in 0xFF downTo 0 step step) {
+ ColorStripe(red, 0xFF, 0)
+ }
+ for (blue in 0..0xFF step step) {
+ ColorStripe(0, 0xFF, blue)
+ }
+ for (green in 0xFF downTo 0 step step) {
+ ColorStripe(0, green, 0xFF)
+ }
+ for (red in 0..0xFF step step) {
+ ColorStripe(red, 0, 0xFF)
+ }
+ for (blue in 0xFF downTo 0 step step) {
+ ColorStripe(0xFF, 0, blue)
+ }
+ }
+}
+
+@Composable
+private fun ColorStripe(red: Int, green: Int, blue: Int) {
+ Canvas(Modifier.size(45.dp, 5.dp)) { drawRect(Color(red = red, green = green, blue = blue)) }
+}
+
+private fun dispatchMouseWheelScroll(
+ scrollAmount: Int,
+ eventTime: Long,
+ view: View,
+) {
+ val properties =
+ MotionEvent.PointerProperties().apply { toolType = MotionEvent.TOOL_TYPE_MOUSE }
+
+ val coords =
+ MotionEvent.PointerCoords().apply {
+ x = view.measuredWidth / 2f
+ y = view.measuredHeight / 2f
+ setAxisValue(MotionEvent.AXIS_VSCROLL, scrollAmount.toFloat())
+ }
+
+ val event =
+ MotionEvent.obtain(
+ 0, /* downTime */
+ eventTime, /* eventTime */
+ MotionEvent.ACTION_SCROLL, /* action */
+ 1, /* pointerCount */
+ arrayOf(properties),
+ arrayOf(coords),
+ 0, /* metaState */
+ 0, /* buttonState */
+ 0f, /* xPrecision */
+ 0f, /* yPrecision */
+ 0, /* deviceId */
+ 0, /* edgeFlags */
+ InputDevice.SOURCE_MOUSE, /* source */
+ 0 /* flags */
+ )
+
+ view.dispatchGenericMotionEvent(event)
}
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/BasicTooltip.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/BasicTooltip.android.kt
index 1dc2f3d..95c91b5b 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/BasicTooltip.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/BasicTooltip.android.kt
@@ -16,31 +16,16 @@
package androidx.compose.foundation
-import androidx.compose.foundation.gestures.LongPressResult
-import androidx.compose.foundation.gestures.awaitEachGesture
-import androidx.compose.foundation.gestures.awaitFirstDown
-import androidx.compose.foundation.gestures.waitForLongPress
-import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerEventType
-import androidx.compose.ui.input.pointer.PointerType
-import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.semantics.LiveRegionMode
-import androidx.compose.ui.semantics.liveRegion
-import androidx.compose.ui.semantics.onLongClick
-import androidx.compose.ui.semantics.paneTitle
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupPositionProvider
-import androidx.compose.ui.window.PopupProperties
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.launch
+
+internal actual object BasicTooltipStrings {
+ @Composable actual fun label(): String = stringResource(R.string.tooltip_label)
+
+ @Composable actual fun description(): String = stringResource(R.string.tooltip_description)
+}
/**
* BasicTooltipBox that wraps a composable with a tooltip.
@@ -62,157 +47,26 @@
* and mouse hover to trigger the tooltip through the state provided.
* @param content the composable that the tooltip will anchor to.
*/
+@Deprecated(level = DeprecationLevel.HIDDEN, message = "Maintained for binary compatibility.")
@Composable
@ExperimentalFoundationApi
-actual fun BasicTooltipBox(
+@JvmName("BasicTooltipBox")
+fun BasicTooltipBoxAndroid(
positionProvider: PopupPositionProvider,
tooltip: @Composable () -> Unit,
state: BasicTooltipState,
- modifier: Modifier,
- focusable: Boolean,
- enableUserInput: Boolean,
- content: @Composable () -> Unit
-) {
- val scope = rememberCoroutineScope()
- Box {
- if (state.isVisible) {
- TooltipPopup(
- positionProvider = positionProvider,
- state = state,
- scope = scope,
- focusable = focusable,
- content = tooltip
- )
- }
-
- WrappedAnchor(
- enableUserInput = enableUserInput,
- state = state,
- modifier = modifier,
- content = content
- )
- }
-
- DisposableEffect(state) { onDispose { state.onDispose() } }
-}
-
-@Composable
-@OptIn(ExperimentalFoundationApi::class)
-private fun WrappedAnchor(
- enableUserInput: Boolean,
- state: BasicTooltipState,
modifier: Modifier = Modifier,
+ focusable: Boolean = true,
+ enableUserInput: Boolean = true,
content: @Composable () -> Unit
) {
- val scope = rememberCoroutineScope()
- val longPressLabel = stringResource(R.string.tooltip_label)
- Box(
- modifier =
- modifier
- .handleGestures(enableUserInput, state)
- .anchorSemantics(longPressLabel, enableUserInput, state, scope)
- ) {
- content()
- }
+ BasicTooltipBox(
+ positionProvider = positionProvider,
+ tooltip = tooltip,
+ state = state,
+ modifier = modifier,
+ focusable = focusable,
+ enableUserInput = enableUserInput,
+ content = content,
+ )
}
-
-@Composable
-@OptIn(ExperimentalFoundationApi::class)
-private fun TooltipPopup(
- positionProvider: PopupPositionProvider,
- state: BasicTooltipState,
- scope: CoroutineScope,
- focusable: Boolean,
- content: @Composable () -> Unit
-) {
- val tooltipDescription = stringResource(R.string.tooltip_description)
- Popup(
- popupPositionProvider = positionProvider,
- onDismissRequest = {
- if (state.isVisible) {
- scope.launch { state.dismiss() }
- }
- },
- properties = PopupProperties(focusable = focusable)
- ) {
- Box(
- modifier =
- Modifier.semantics {
- liveRegion = LiveRegionMode.Assertive
- paneTitle = tooltipDescription
- }
- ) {
- content()
- }
- }
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-private fun Modifier.handleGestures(enabled: Boolean, state: BasicTooltipState): Modifier =
- if (enabled) {
- this.pointerInput(state) {
- coroutineScope {
- awaitEachGesture {
- val pass = PointerEventPass.Initial
-
- // wait for the first down press
- val inputType = awaitFirstDown(pass = pass).type
-
- if (inputType == PointerType.Touch || inputType == PointerType.Stylus) {
- val longPress = waitForLongPress(pass = pass)
- if (longPress is LongPressResult.Success) {
- // handle long press - Show the tooltip
- launch { state.show(MutatePriority.UserInput) }
-
- // consume the children's click handling
- val changes = awaitPointerEvent(pass = pass).changes
- for (i in 0 until changes.size) {
- changes[i].consume()
- }
- }
- }
- }
- }
- }
- .pointerInput(state) {
- coroutineScope {
- awaitPointerEventScope {
- val pass = PointerEventPass.Main
-
- while (true) {
- val event = awaitPointerEvent(pass)
- val inputType = event.changes[0].type
- if (inputType == PointerType.Mouse) {
- when (event.type) {
- PointerEventType.Enter -> {
- launch { state.show(MutatePriority.UserInput) }
- }
- PointerEventType.Exit -> {
- state.dismiss()
- }
- }
- }
- }
- }
- }
- }
- } else this
-
-@OptIn(ExperimentalFoundationApi::class)
-private fun Modifier.anchorSemantics(
- label: String,
- enabled: Boolean,
- state: BasicTooltipState,
- scope: CoroutineScope
-): Modifier =
- if (enabled) {
- this.semantics(mergeDescendants = true) {
- onLongClick(
- label = label,
- action = {
- scope.launch { state.show() }
- true
- }
- )
- }
- } else this
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/gestures/AndroidScrollable.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/gestures/AndroidScrollable.android.kt
index 295e75e..51b27f3 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/gestures/AndroidScrollable.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/gestures/AndroidScrollable.android.kt
@@ -22,7 +22,6 @@
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
import androidx.compose.ui.node.requireView
-import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicTooltip.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicTooltip.kt
index 63da5c3..911ae1f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicTooltip.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicTooltip.kt
@@ -16,15 +16,36 @@
package androidx.compose.foundation
+import androidx.compose.foundation.gestures.LongPressResult
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.waitForLongPress
+import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerEventType
+import androidx.compose.ui.input.pointer.PointerType
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.semantics.LiveRegionMode
+import androidx.compose.ui.semantics.liveRegion
+import androidx.compose.ui.semantics.onLongClick
+import androidx.compose.ui.semantics.paneTitle
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupPositionProvider
+import androidx.compose.ui.window.PopupProperties
import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeout
@@ -50,7 +71,7 @@
*/
@Composable
@ExperimentalFoundationApi
-expect fun BasicTooltipBox(
+fun BasicTooltipBox(
positionProvider: PopupPositionProvider,
tooltip: @Composable () -> Unit,
state: BasicTooltipState,
@@ -58,7 +79,150 @@
focusable: Boolean = true,
enableUserInput: Boolean = true,
content: @Composable () -> Unit
-)
+) {
+ val scope = rememberCoroutineScope()
+ Box {
+ if (state.isVisible) {
+ TooltipPopup(
+ positionProvider = positionProvider,
+ state = state,
+ scope = scope,
+ focusable = focusable,
+ content = tooltip
+ )
+ }
+
+ WrappedAnchor(
+ enableUserInput = enableUserInput,
+ state = state,
+ modifier = modifier,
+ content = content
+ )
+ }
+
+ DisposableEffect(state) { onDispose { state.onDispose() } }
+}
+
+@Composable
+@OptIn(ExperimentalFoundationApi::class)
+private fun WrappedAnchor(
+ enableUserInput: Boolean,
+ state: BasicTooltipState,
+ modifier: Modifier = Modifier,
+ content: @Composable () -> Unit
+) {
+ val scope = rememberCoroutineScope()
+ val longPressLabel = BasicTooltipStrings.label()
+ Box(
+ modifier =
+ modifier
+ .handleGestures(enableUserInput, state)
+ .anchorSemantics(longPressLabel, enableUserInput, state, scope)
+ ) {
+ content()
+ }
+}
+
+@Composable
+@OptIn(ExperimentalFoundationApi::class)
+private fun TooltipPopup(
+ positionProvider: PopupPositionProvider,
+ state: BasicTooltipState,
+ scope: CoroutineScope,
+ focusable: Boolean,
+ content: @Composable () -> Unit
+) {
+ val tooltipDescription = BasicTooltipStrings.description()
+ Popup(
+ popupPositionProvider = positionProvider,
+ onDismissRequest = {
+ if (state.isVisible) {
+ scope.launch { state.dismiss() }
+ }
+ },
+ properties = PopupProperties(focusable = focusable)
+ ) {
+ Box(
+ modifier =
+ Modifier.semantics {
+ liveRegion = LiveRegionMode.Assertive
+ paneTitle = tooltipDescription
+ }
+ ) {
+ content()
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun Modifier.handleGestures(enabled: Boolean, state: BasicTooltipState): Modifier =
+ if (enabled) {
+ this.pointerInput(state) {
+ coroutineScope {
+ awaitEachGesture {
+ val pass = PointerEventPass.Initial
+
+ // wait for the first down press
+ val inputType = awaitFirstDown(pass = pass).type
+
+ if (inputType == PointerType.Touch || inputType == PointerType.Stylus) {
+ val longPress = waitForLongPress(pass = pass)
+ if (longPress is LongPressResult.Success) {
+ // handle long press - Show the tooltip
+ launch { state.show(MutatePriority.UserInput) }
+
+ // consume the children's click handling
+ val changes = awaitPointerEvent(pass = pass).changes
+ for (i in 0 until changes.size) {
+ changes[i].consume()
+ }
+ }
+ }
+ }
+ }
+ }
+ .pointerInput(state) {
+ coroutineScope {
+ awaitPointerEventScope {
+ val pass = PointerEventPass.Main
+
+ while (true) {
+ val event = awaitPointerEvent(pass)
+ val inputType = event.changes[0].type
+ if (inputType == PointerType.Mouse) {
+ when (event.type) {
+ PointerEventType.Enter -> {
+ launch { state.show(MutatePriority.UserInput) }
+ }
+ PointerEventType.Exit -> {
+ state.dismiss()
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ } else this
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun Modifier.anchorSemantics(
+ label: String,
+ enabled: Boolean,
+ state: BasicTooltipState,
+ scope: CoroutineScope
+): Modifier =
+ if (enabled) {
+ this.semantics(mergeDescendants = true) {
+ onLongClick(
+ label = label,
+ action = {
+ scope.launch { state.show() }
+ true
+ }
+ )
+ }
+ } else this
/**
* Create and remember the default [BasicTooltipState].
@@ -216,3 +380,10 @@
*/
const val TooltipDuration = 1500L
}
+
+@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
+internal expect object BasicTooltipStrings {
+ @Composable fun label(): String
+
+ @Composable fun description(): String
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/MouseWheelScrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/MouseWheelScrollable.kt
index d9a21a4..94c22fb 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/MouseWheelScrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/MouseWheelScrollable.kt
@@ -26,24 +26,21 @@
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
-import androidx.compose.ui.input.pointer.AwaitPointerEventScope
import androidx.compose.ui.input.pointer.PointerEvent
+import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerEventType
-import androidx.compose.ui.input.pointer.PointerInputScope
-import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
import androidx.compose.ui.input.pointer.util.VelocityTracker1D
-import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
-import androidx.compose.ui.node.DelegatingNode
-import androidx.compose.ui.node.currentValueOf
-import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastForEach
-import kotlin.coroutines.coroutineContext
import kotlin.math.abs
import kotlin.math.roundToInt
import kotlin.math.sign
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.isActive
@@ -51,71 +48,27 @@
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.withTimeoutOrNull
-internal class MouseWheelScrollNode(
+internal class MouseWheelScrollingLogic(
private val scrollingLogic: ScrollingLogic,
+ private val mouseWheelScrollConfig: ScrollConfig,
private val onScrollStopped: suspend (velocity: Velocity) -> Unit,
- private var enabled: Boolean,
-) : DelegatingNode(), CompositionLocalConsumerModifierNode {
-
- // Need to wait until onAttach to read the scroll config. Currently this is static, so we
- // don't need to worry about observation / updating this over time.
- private lateinit var mouseWheelScrollConfig: ScrollConfig
-
- override fun onAttach() {
- mouseWheelScrollConfig = platformScrollConfig()
- coroutineScope.launch { receiveMouseWheelEvents() }
+ private var density: Density,
+) {
+ fun updateDensity(density: Density) {
+ this.density = density
}
- // Note that when `MouseWheelScrollNode` is used as a delegate of `ScrollableNode`,
- // pointerInputNode does not get dispatched pointer events to in the standard manner because
- // Modifier.Node.dispatchForKind does not dispatch to child/delegate nodes of the matching type,
- // and `ScrollableNode` is already an instance of `PointerInputModifierNode`.
- // This is worked around by having `MouseWheelScrollNode` simply forward the corresponding calls
- // to pointerInputNode (hence its need to be `internal`).
- internal val pointerInputNode =
- delegate(
- SuspendingPointerInputModifierNode {
- if (enabled) {
- mouseWheelInput()
- }
- }
- )
-
- fun update(
- enabled: Boolean,
- ) {
- var resetPointerInputHandling = false
- if (this.enabled != enabled) {
- this.enabled = enabled
- resetPointerInputHandling = true
- }
- if (resetPointerInputHandling) {
- pointerInputNode.resetPointerInputHandler()
- }
- }
-
- private suspend fun PointerInputScope.mouseWheelInput() {
- awaitPointerEventScope {
- while (coroutineScope.isActive) {
- val event = awaitScrollEvent()
- if (!event.isConsumed) {
- val consumed = onMouseWheel(event)
- if (consumed) {
- event.consume()
- }
+ fun onPointerEvent(pointerEvent: PointerEvent, pass: PointerEventPass, bounds: IntSize) {
+ if (pass == PointerEventPass.Main && pointerEvent.type == PointerEventType.Scroll) {
+ if (!pointerEvent.isConsumed) {
+ val consumed = onMouseWheel(pointerEvent, bounds)
+ if (consumed) {
+ pointerEvent.consume()
}
}
}
}
- private suspend fun AwaitPointerEventScope.awaitScrollEvent(): PointerEvent {
- var event: PointerEvent
- do {
- event = awaitPointerEvent()
- } while (event.type != PointerEventType.Scroll)
- return event
- }
-
private inline val PointerEvent.isConsumed: Boolean
get() = changes.fastAny { it.isConsumed }
@@ -143,13 +96,23 @@
private val channel = Channel<MouseWheelScrollDelta>(capacity = Channel.UNLIMITED)
private var isScrolling = false
- private suspend fun receiveMouseWheelEvents() {
- while (coroutineContext.isActive) {
- val scrollDelta = channel.receive()
- val density = currentValueOf(LocalDensity)
- val threshold = with(density) { AnimationThreshold.toPx() }
- val speed = with(density) { AnimationSpeed.toPx() }
- scrollingLogic.dispatchMouseWheelScroll(scrollDelta, threshold, speed)
+ private var receivingMouseWheelEventsJob: Job? = null
+
+ fun startReceivingMouseWheelEvents(coroutineScope: CoroutineScope) {
+ if (receivingMouseWheelEventsJob == null) {
+ receivingMouseWheelEventsJob =
+ coroutineScope.launch {
+ try {
+ while (coroutineContext.isActive) {
+ val scrollDelta = channel.receive()
+ val threshold = with(density) { AnimationThreshold.toPx() }
+ val speed = with(density) { AnimationSpeed.toPx() }
+ scrollingLogic.dispatchMouseWheelScroll(scrollDelta, threshold, speed)
+ }
+ } finally {
+ receivingMouseWheelEventsJob = null
+ }
+ }
}
}
@@ -160,9 +123,11 @@
isScrolling = false
}
- private fun PointerInputScope.onMouseWheel(pointerEvent: PointerEvent): Boolean {
+ private fun onMouseWheel(pointerEvent: PointerEvent, bounds: IntSize): Boolean {
val scrollDelta =
- with(mouseWheelScrollConfig) { calculateMouseWheelScroll(pointerEvent, size) }
+ with(mouseWheelScrollConfig) {
+ with(density) { calculateMouseWheelScroll(pointerEvent, bounds) }
+ }
return if (scrollingLogic.canConsumeDelta(scrollDelta)) {
channel
.trySend(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index 93725b7..a7792c3 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -275,7 +275,8 @@
orientationLock = orientation
),
KeyInputModifierNode,
- SemanticsModifierNode {
+ SemanticsModifierNode,
+ CompositionLocalConsumerModifierNode {
override val shouldAutoInvalidate: Boolean = false
@@ -309,7 +310,7 @@
private var scrollByAction: ((x: Float, y: Float) -> Boolean)? = null
private var scrollByOffsetAction: (suspend (Offset) -> Offset)? = null
- private var mouseWheelScrollNode: MouseWheelScrollNode? = null
+ private var mouseWheelScrollingLogic: MouseWheelScrollingLogic? = null
init {
/** Nested scrolling */
@@ -352,15 +353,17 @@
}
private fun ensureMouseWheelScrollNodeInitialized() {
- if (mouseWheelScrollNode != null) return
- mouseWheelScrollNode =
- delegate(
- MouseWheelScrollNode(
+ if (mouseWheelScrollingLogic == null) {
+ mouseWheelScrollingLogic =
+ MouseWheelScrollingLogic(
scrollingLogic = scrollingLogic,
+ mouseWheelScrollConfig = platformScrollConfig(),
onScrollStopped = ::onWheelScrollStopped,
- enabled = enabled,
+ density = requireDensity()
)
- )
+ }
+
+ mouseWheelScrollingLogic?.startReceivingMouseWheelEvents(coroutineScope)
}
fun update(
@@ -392,7 +395,6 @@
nestedScrollDispatcher = nestedScrollDispatcher
)
contentInViewNode.update(orientation, reverseDirection, bringIntoViewSpec)
- mouseWheelScrollNode?.update(enabled = enabled)
this.overscrollEffect = overscrollEffect
this.flingBehavior = flingBehavior
@@ -414,6 +416,7 @@
override fun onAttach() {
updateDefaultFlingBehavior()
+ mouseWheelScrollingLogic?.updateDensity(requireDensity())
}
private fun updateDefaultFlingBehavior() {
@@ -425,7 +428,7 @@
override fun onDensityChange() {
onCancelPointerInput()
updateDefaultFlingBehavior()
- mouseWheelScrollNode?.pointerInputNode?.onDensityChange()
+ mouseWheelScrollingLogic?.updateDensity(requireDensity())
}
// Key handler for Page up/down scrolling behavior.
@@ -492,10 +495,12 @@
if (pointerEvent.changes.fastAny { canDrag.invoke(it) }) {
super.onPointerEvent(pointerEvent, pass, bounds)
}
- if (pass == PointerEventPass.Initial && pointerEvent.type == PointerEventType.Scroll) {
- ensureMouseWheelScrollNodeInitialized()
+ if (enabled) {
+ if (pass == PointerEventPass.Initial && pointerEvent.type == PointerEventType.Scroll) {
+ ensureMouseWheelScrollNodeInitialized()
+ }
+ mouseWheelScrollingLogic?.onPointerEvent(pointerEvent, pass, bounds)
}
- mouseWheelScrollNode?.pointerInputNode?.onPointerEvent(pointerEvent, pass, bounds)
}
override fun SemanticsPropertyReceiver.applySemantics() {
@@ -521,16 +526,6 @@
scrollByAction = null
scrollByOffsetAction = null
}
-
- override fun onCancelPointerInput() {
- super.onCancelPointerInput()
- mouseWheelScrollNode?.pointerInputNode?.onCancelPointerInput()
- }
-
- override fun onViewConfigurationChange() {
- super.onViewConfigurationChange()
- mouseWheelScrollNode?.pointerInputNode?.onViewConfigurationChange()
- }
}
/** Contains the default values used by [scrollable] */
diff --git a/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/BasicTooltip.commonStubs.kt b/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/BasicTooltip.commonStubs.kt
index 22ff0c8..892f259 100644
--- a/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/BasicTooltip.commonStubs.kt
+++ b/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/BasicTooltip.commonStubs.kt
@@ -17,17 +17,10 @@
package androidx.compose.foundation
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.window.PopupPositionProvider
-@Composable
-@ExperimentalFoundationApi
-actual fun BasicTooltipBox(
- positionProvider: PopupPositionProvider,
- tooltip: @Composable () -> Unit,
- state: BasicTooltipState,
- modifier: Modifier,
- focusable: Boolean,
- enableUserInput: Boolean,
- content: @Composable () -> Unit
-): Unit = implementedInJetBrainsFork()
+@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
+internal actual object BasicTooltipStrings {
+ @Composable actual fun label(): String = implementedInJetBrainsFork()
+
+ @Composable actual fun description(): String = implementedInJetBrainsFork()
+}
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index 2f679dbf..98fd41b 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -2868,6 +2868,8 @@
}
public final class TooltipKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void PlainTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional long caretSize, optional float maxWidth, optional androidx.compose.ui.graphics.Shape shape, optional long contentColor, optional long containerColor, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RichTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional long caretSize, optional float maxWidth, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.RichTooltipColors colors, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> text);
method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TooltipBox(androidx.compose.ui.window.PopupPositionProvider positionProvider, kotlin.jvm.functions.Function1<? super androidx.compose.material3.TooltipScope,kotlin.Unit> tooltip, androidx.compose.material3.TooltipState state, optional androidx.compose.ui.Modifier modifier, optional boolean focusable, optional boolean enableUserInput, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TooltipBox(androidx.compose.ui.window.PopupPositionProvider positionProvider, kotlin.jvm.functions.Function1<? super androidx.compose.material3.TooltipScope,kotlin.Unit> tooltip, androidx.compose.material3.TooltipState state, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onDismissRequest, optional boolean focusable, optional boolean enableUserInput, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.material3.TooltipState TooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
@@ -2892,9 +2894,9 @@
public final class Tooltip_androidKt {
method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void PlainTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional long caretSize, optional androidx.compose.ui.graphics.Shape shape, optional long contentColor, optional long containerColor, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void PlainTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional long caretSize, optional float maxWidth, optional androidx.compose.ui.graphics.Shape shape, optional long contentColor, optional long containerColor, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void PlainTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional long caretSize, optional float maxWidth, optional androidx.compose.ui.graphics.Shape shape, optional long contentColor, optional long containerColor, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RichTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional long caretSize, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.RichTooltipColors colors, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> text);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RichTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional long caretSize, optional float maxWidth, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.RichTooltipColors colors, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> text);
+ method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RichTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional long caretSize, optional float maxWidth, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.RichTooltipColors colors, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> text);
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class TopAppBarColors {
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index 2f679dbf..98fd41b 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -2868,6 +2868,8 @@
}
public final class TooltipKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void PlainTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional long caretSize, optional float maxWidth, optional androidx.compose.ui.graphics.Shape shape, optional long contentColor, optional long containerColor, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RichTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional long caretSize, optional float maxWidth, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.RichTooltipColors colors, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> text);
method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TooltipBox(androidx.compose.ui.window.PopupPositionProvider positionProvider, kotlin.jvm.functions.Function1<? super androidx.compose.material3.TooltipScope,kotlin.Unit> tooltip, androidx.compose.material3.TooltipState state, optional androidx.compose.ui.Modifier modifier, optional boolean focusable, optional boolean enableUserInput, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TooltipBox(androidx.compose.ui.window.PopupPositionProvider positionProvider, kotlin.jvm.functions.Function1<? super androidx.compose.material3.TooltipScope,kotlin.Unit> tooltip, androidx.compose.material3.TooltipState state, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onDismissRequest, optional boolean focusable, optional boolean enableUserInput, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.material3.TooltipState TooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
@@ -2892,9 +2894,9 @@
public final class Tooltip_androidKt {
method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void PlainTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional long caretSize, optional androidx.compose.ui.graphics.Shape shape, optional long contentColor, optional long containerColor, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void PlainTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional long caretSize, optional float maxWidth, optional androidx.compose.ui.graphics.Shape shape, optional long contentColor, optional long containerColor, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void PlainTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional long caretSize, optional float maxWidth, optional androidx.compose.ui.graphics.Shape shape, optional long contentColor, optional long containerColor, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RichTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional long caretSize, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.RichTooltipColors colors, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> text);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RichTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional long caretSize, optional float maxWidth, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.RichTooltipColors colors, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> text);
+ method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void RichTooltip(androidx.compose.material3.TooltipScope, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? action, optional long caretSize, optional float maxWidth, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.RichTooltipColors colors, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function0<kotlin.Unit> text);
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class TopAppBarColors {
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/NavigationDrawer.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/NavigationDrawer.android.kt
deleted file mode 100644
index d214bbd..0000000
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/NavigationDrawer.android.kt
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.material3
-
-import androidx.activity.BackEventCompat
-import androidx.activity.compose.PredictiveBackHandler
-import androidx.compose.animation.core.animate
-import androidx.compose.material3.internal.PredictiveBack
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import kotlin.coroutines.cancellation.CancellationException
-import kotlinx.coroutines.launch
-
-/**
- * Registers a [PredictiveBackHandler] and provides animation values in [DrawerPredictiveBackState]
- * based on back progress.
- *
- * @param drawerState state of the drawer
- * @param content content of the rest of the UI
- */
-@Composable
-internal actual fun DrawerPredictiveBackHandler(
- drawerState: DrawerState,
- content: @Composable (DrawerPredictiveBackState) -> Unit
-) {
- val drawerPredictiveBackState = remember { DrawerPredictiveBackState() }
- val scope = rememberCoroutineScope()
- val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
- val maxScaleXDistanceGrow: Float
- val maxScaleXDistanceShrink: Float
- val maxScaleYDistance: Float
- with(LocalDensity.current) {
- maxScaleXDistanceGrow = PredictiveBackDrawerMaxScaleXDistanceGrow.toPx()
- maxScaleXDistanceShrink = PredictiveBackDrawerMaxScaleXDistanceShrink.toPx()
- maxScaleYDistance = PredictiveBackDrawerMaxScaleYDistance.toPx()
- }
-
- PredictiveBackHandler(enabled = drawerState.isOpen) { progress ->
- try {
- progress.collect { backEvent ->
- drawerPredictiveBackState.update(
- PredictiveBack.transform(backEvent.progress),
- backEvent.swipeEdge == BackEventCompat.EDGE_LEFT,
- isRtl,
- maxScaleXDistanceGrow,
- maxScaleXDistanceShrink,
- maxScaleYDistance
- )
- }
- } catch (e: CancellationException) {
- drawerPredictiveBackState.clear()
- } finally {
- if (drawerPredictiveBackState.swipeEdgeMatchesDrawer) {
- // If swipe edge matches drawer gravity and we've stretched the drawer horizontally,
- // un-stretch it smoothly so that it hides completely during the drawer close.
- scope.launch {
- animate(
- initialValue = drawerPredictiveBackState.scaleXDistance,
- targetValue = 0f
- ) { value, _ ->
- drawerPredictiveBackState.scaleXDistance = value
- }
- drawerPredictiveBackState.clear()
- }
- }
- drawerState.close()
- }
- }
-
- LaunchedEffect(drawerState.isClosed) {
- if (drawerState.isClosed) {
- drawerPredictiveBackState.clear()
- }
- }
-
- content(drawerPredictiveBackState)
-}
-
-internal val PredictiveBackDrawerMaxScaleXDistanceGrow = 12.dp
-internal val PredictiveBackDrawerMaxScaleXDistanceShrink = 24.dp
-internal val PredictiveBackDrawerMaxScaleYDistance = 48.dp
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Tooltip.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Tooltip.android.kt
index ffa265c..d1c12d9 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Tooltip.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Tooltip.android.kt
@@ -16,33 +16,22 @@
package androidx.compose.material3
-import android.content.res.Configuration
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.paddingFromBaseline
-import androidx.compose.foundation.layout.requiredHeightIn
-import androidx.compose.foundation.layout.sizeIn
-import androidx.compose.material3.tokens.PlainTooltipTokens
+import androidx.compose.material3.tokens.ElevationTokens
import androidx.compose.material3.tokens.RichTooltipTokens
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.CacheDrawScope
-import androidx.compose.ui.draw.DrawResult
-import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.isSpecified
+
+@Composable
+internal actual fun windowContainerWidthInPx(): Int {
+ return with(LocalDensity.current) { LocalConfiguration.current.screenWidthDp.dp.roundToPx() }
+}
/**
* Plain tooltip that provides a descriptive message.
@@ -60,21 +49,21 @@
* @param shadowElevation the shadow elevation of the tooltip.
* @param content the composable that will be used to populate the tooltip's content.
*/
-@Suppress("DEPRECATION")
@Deprecated(
level = DeprecationLevel.HIDDEN,
message = "Maintained for binary compatibility. " + "Use overload with maxWidth parameter."
)
@Composable
@ExperimentalMaterial3Api
-actual fun TooltipScope.PlainTooltip(
- modifier: Modifier,
- caretSize: DpSize,
- shape: Shape,
- contentColor: Color,
- containerColor: Color,
- tonalElevation: Dp,
- shadowElevation: Dp,
+@JvmName("PlainTooltip")
+fun TooltipScope.PlainTooltipAndroid(
+ modifier: Modifier = Modifier,
+ caretSize: DpSize = DpSize.Unspecified,
+ shape: Shape = TooltipDefaults.plainTooltipContainerShape,
+ contentColor: Color = TooltipDefaults.plainTooltipContentColor,
+ containerColor: Color = TooltipDefaults.plainTooltipContainerColor,
+ tonalElevation: Dp = 0.dp,
+ shadowElevation: Dp = 0.dp,
content: @Composable () -> Unit
) =
PlainTooltip(
@@ -106,59 +95,32 @@
* @param shadowElevation the shadow elevation of the tooltip.
* @param content the composable that will be used to populate the tooltip's content.
*/
+@Deprecated(level = DeprecationLevel.HIDDEN, message = "Maintained for binary compatibility.")
@Composable
@ExperimentalMaterial3Api
-actual fun TooltipScope.PlainTooltip(
- modifier: Modifier,
- caretSize: DpSize,
- maxWidth: Dp,
- shape: Shape,
- contentColor: Color,
- containerColor: Color,
- tonalElevation: Dp,
- shadowElevation: Dp,
+@JvmName("PlainTooltip")
+fun TooltipScope.PlainTooltipAndroid(
+ modifier: Modifier = Modifier,
+ caretSize: DpSize = DpSize.Unspecified,
+ maxWidth: Dp = TooltipDefaults.plainTooltipMaxWidth,
+ shape: Shape = TooltipDefaults.plainTooltipContainerShape,
+ contentColor: Color = TooltipDefaults.plainTooltipContentColor,
+ containerColor: Color = TooltipDefaults.plainTooltipContainerColor,
+ tonalElevation: Dp = 0.dp,
+ shadowElevation: Dp = 0.dp,
content: @Composable () -> Unit
) {
- val drawCaretModifier =
- if (caretSize.isSpecified) {
- val density = LocalDensity.current
- val configuration = LocalConfiguration.current
- Modifier.drawCaret { anchorLayoutCoordinates ->
- drawCaretWithPath(
- density,
- configuration,
- containerColor,
- caretSize,
- anchorLayoutCoordinates
- )
- }
- .then(modifier)
- } else modifier
- Surface(
- modifier = drawCaretModifier,
+ PlainTooltip(
+ modifier = modifier,
+ caretSize = caretSize,
+ maxWidth = maxWidth,
shape = shape,
- color = containerColor,
+ contentColor = contentColor,
+ containerColor = containerColor,
tonalElevation = tonalElevation,
- shadowElevation = shadowElevation
- ) {
- Box(
- modifier =
- Modifier.sizeIn(
- minWidth = TooltipMinWidth,
- maxWidth = maxWidth,
- minHeight = TooltipMinHeight
- )
- .padding(PlainTooltipContentPadding)
- ) {
- val textStyle = PlainTooltipTokens.SupportingTextFont.value
-
- CompositionLocalProvider(
- LocalContentColor provides contentColor,
- LocalTextStyle provides textStyle,
- content = content
- )
- }
- }
+ shadowElevation = shadowElevation,
+ content = content,
+ )
}
/**
@@ -179,22 +141,22 @@
* @param shadowElevation the shadow elevation of the tooltip.
* @param text the composable that will be used to populate the rich tooltip's text.
*/
-@Suppress("DEPRECATION")
@Deprecated(
level = DeprecationLevel.HIDDEN,
message = "Maintained for binary compatibility. " + "Use overload with maxWidth parameter."
)
@Composable
@ExperimentalMaterial3Api
-actual fun TooltipScope.RichTooltip(
- modifier: Modifier,
- title: (@Composable () -> Unit)?,
- action: (@Composable () -> Unit)?,
- caretSize: DpSize,
- shape: Shape,
- colors: RichTooltipColors,
- tonalElevation: Dp,
- shadowElevation: Dp,
+@JvmName("RichTooltip")
+fun TooltipScope.RichTooltipAndroid(
+ modifier: Modifier = Modifier,
+ title: (@Composable () -> Unit)? = null,
+ action: (@Composable () -> Unit)? = null,
+ caretSize: DpSize = DpSize.Unspecified,
+ shape: Shape = TooltipDefaults.richTooltipContainerShape,
+ colors: RichTooltipColors = TooltipDefaults.richTooltipColors(),
+ tonalElevation: Dp = ElevationTokens.Level0,
+ shadowElevation: Dp = RichTooltipTokens.ContainerElevation,
text: @Composable () -> Unit
) =
RichTooltip(
@@ -229,171 +191,32 @@
* @param shadowElevation the shadow elevation of the tooltip.
* @param text the composable that will be used to populate the rich tooltip's text.
*/
+@Deprecated(level = DeprecationLevel.HIDDEN, message = "Maintained for binary compatibility.")
@Composable
@ExperimentalMaterial3Api
-actual fun TooltipScope.RichTooltip(
- modifier: Modifier,
- title: (@Composable () -> Unit)?,
- action: (@Composable () -> Unit)?,
- caretSize: DpSize,
- maxWidth: Dp,
- shape: Shape,
- colors: RichTooltipColors,
- tonalElevation: Dp,
- shadowElevation: Dp,
+@JvmName("RichTooltip")
+fun TooltipScope.RichTooltipAndroid(
+ modifier: Modifier = Modifier,
+ title: (@Composable () -> Unit)? = null,
+ action: (@Composable () -> Unit)? = null,
+ caretSize: DpSize = DpSize.Unspecified,
+ maxWidth: Dp = TooltipDefaults.richTooltipMaxWidth,
+ shape: Shape = TooltipDefaults.richTooltipContainerShape,
+ colors: RichTooltipColors = TooltipDefaults.richTooltipColors(),
+ tonalElevation: Dp = ElevationTokens.Level0,
+ shadowElevation: Dp = RichTooltipTokens.ContainerElevation,
text: @Composable () -> Unit
) {
- val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation
- val elevatedColor =
- MaterialTheme.colorScheme.applyTonalElevation(colors.containerColor, absoluteElevation)
- val drawCaretModifier =
- if (caretSize.isSpecified) {
- val density = LocalDensity.current
- val configuration = LocalConfiguration.current
- Modifier.drawCaret { anchorLayoutCoordinates ->
- drawCaretWithPath(
- density,
- configuration,
- elevatedColor,
- caretSize,
- anchorLayoutCoordinates
- )
- }
- .then(modifier)
- } else modifier
- Surface(
- modifier =
- drawCaretModifier.sizeIn(
- minWidth = TooltipMinWidth,
- maxWidth = maxWidth,
- minHeight = TooltipMinHeight
- ),
+ RichTooltip(
+ modifier = modifier,
+ title = title,
+ action = action,
+ caretSize = caretSize,
+ maxWidth = maxWidth,
shape = shape,
- color = colors.containerColor,
+ colors = colors,
tonalElevation = tonalElevation,
- shadowElevation = shadowElevation
- ) {
- val actionLabelTextStyle = RichTooltipTokens.ActionLabelTextFont.value
- val subheadTextStyle = RichTooltipTokens.SubheadFont.value
- val supportingTextStyle = RichTooltipTokens.SupportingTextFont.value
-
- Column(modifier = Modifier.padding(horizontal = RichTooltipHorizontalPadding)) {
- title?.let {
- Box(modifier = Modifier.paddingFromBaseline(top = HeightToSubheadFirstLine)) {
- CompositionLocalProvider(
- LocalContentColor provides colors.titleContentColor,
- LocalTextStyle provides subheadTextStyle,
- content = it
- )
- }
- }
- Box(modifier = Modifier.textVerticalPadding(title != null, action != null)) {
- CompositionLocalProvider(
- LocalContentColor provides colors.contentColor,
- LocalTextStyle provides supportingTextStyle,
- content = text
- )
- }
- action?.let {
- Box(
- modifier =
- Modifier.requiredHeightIn(min = ActionLabelMinHeight)
- .padding(bottom = ActionLabelBottomPadding)
- ) {
- CompositionLocalProvider(
- LocalContentColor provides colors.actionContentColor,
- LocalTextStyle provides actionLabelTextStyle,
- content = it
- )
- }
- }
- }
- }
-}
-
-@ExperimentalMaterial3Api
-private fun CacheDrawScope.drawCaretWithPath(
- density: Density,
- configuration: Configuration,
- containerColor: Color,
- caretSize: DpSize,
- anchorLayoutCoordinates: LayoutCoordinates?
-): DrawResult {
- val path = Path()
-
- if (anchorLayoutCoordinates != null) {
- val caretHeightPx: Int
- val caretWidthPx: Int
- val screenWidthPx: Int
- val tooltipAnchorSpacing: Int
- with(density) {
- caretHeightPx = caretSize.height.roundToPx()
- caretWidthPx = caretSize.width.roundToPx()
- screenWidthPx = configuration.screenWidthDp.dp.roundToPx()
- tooltipAnchorSpacing = SpacingBetweenTooltipAndAnchor.roundToPx()
- }
- val anchorBounds = anchorLayoutCoordinates.boundsInWindow()
- val anchorLeft = anchorBounds.left
- val anchorRight = anchorBounds.right
- val anchorTop = anchorBounds.top
- val anchorMid = (anchorRight + anchorLeft) / 2
- val anchorWidth = anchorRight - anchorLeft
- val tooltipWidth = this.size.width
- val tooltipHeight = this.size.height
- val isCaretTop = anchorTop - tooltipHeight - tooltipAnchorSpacing < 0
- val caretY =
- if (isCaretTop) {
- 0f
- } else {
- tooltipHeight
- }
-
- // Default the caret to be in the middle
- // caret might need to be offset depending on where
- // the tooltip is placed relative to the anchor
- var position: Offset =
- if (anchorLeft - tooltipWidth / 2 + anchorWidth / 2 <= 0) {
- Offset(anchorMid, caretY)
- } else if (anchorRight + tooltipWidth / 2 - anchorWidth / 2 >= screenWidthPx) {
- val anchorMidFromRightScreenEdge = screenWidthPx - anchorMid
- val caretX = tooltipWidth - anchorMidFromRightScreenEdge
- Offset(caretX, caretY)
- } else {
- Offset(tooltipWidth / 2, caretY)
- }
- if (anchorMid - tooltipWidth / 2 < 0) {
- // The tooltip needs to be start aligned if it would collide with the left side of
- // screen.
- position = Offset(anchorMid - anchorLeft, caretY)
- } else if (anchorMid + tooltipWidth / 2 > screenWidthPx) {
- // The tooltip needs to be end aligned if it would collide with the right side of the
- // screen.
- position = Offset(anchorMid - (anchorRight - tooltipWidth), caretY)
- }
-
- if (isCaretTop) {
- path.apply {
- moveTo(x = position.x, y = position.y)
- lineTo(x = position.x + caretWidthPx / 2, y = position.y)
- lineTo(x = position.x, y = position.y - caretHeightPx)
- lineTo(x = position.x - caretWidthPx / 2, y = position.y)
- close()
- }
- } else {
- path.apply {
- moveTo(x = position.x, y = position.y)
- lineTo(x = position.x + caretWidthPx / 2, y = position.y)
- lineTo(x = position.x, y = position.y + caretHeightPx.toFloat())
- lineTo(x = position.x - caretWidthPx / 2, y = position.y)
- close()
- }
- }
- }
-
- return onDrawWithContent {
- if (anchorLayoutCoordinates != null) {
- drawContent()
- drawPath(path = path, color = containerColor)
- }
- }
+ shadowElevation = shadowElevation,
+ text = text,
+ )
}
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/BasicTooltip.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/BasicTooltip.android.kt
index e7d8212..db71747 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/BasicTooltip.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/BasicTooltip.android.kt
@@ -14,241 +14,14 @@
* limitations under the License.
*/
-@file:OptIn(ExperimentalMaterial3Api::class)
-
package androidx.compose.material3.internal
-import androidx.compose.foundation.MutatePriority
import androidx.compose.foundation.R
-import androidx.compose.foundation.gestures.awaitEachGesture
-import androidx.compose.foundation.gestures.awaitFirstDown
-import androidx.compose.foundation.gestures.waitForUpOrCancellation
-import androidx.compose.foundation.layout.Box
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.TooltipState
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerEventTimeoutCancellationException
-import androidx.compose.ui.input.pointer.PointerEventType
-import androidx.compose.ui.input.pointer.PointerType
-import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.semantics.LiveRegionMode
-import androidx.compose.ui.semantics.liveRegion
-import androidx.compose.ui.semantics.onLongClick
-import androidx.compose.ui.semantics.paneTitle
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.window.Popup
-import androidx.compose.ui.window.PopupPositionProvider
-import androidx.compose.ui.window.PopupProperties
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.launch
-/**
- * NOTICE: Fork from androidx.compose.foundation.BasicTooltip box since those are experimental
- *
- * BasicTooltipBox that wraps a composable with a tooltip.
- *
- * Tooltip that provides a descriptive message for an anchor. It can be used to call the users
- * attention to the anchor.
- *
- * @param positionProvider [PopupPositionProvider] that will be used to place the tooltip relative
- * to the anchor content.
- * @param tooltip the composable that will be used to populate the tooltip's content.
- * @param state handles the state of the tooltip's visibility.
- * @param modifier the [Modifier] to be applied to this BasicTooltipBox.
- * @param focusable [Boolean] that determines if the tooltip is focusable. When true, the tooltip
- * will consume touch events while it's shown and will have accessibility focus move to the first
- * element of the component. When false, the tooltip won't consume touch events while it's shown
- * but assistive-tech users will need to swipe or drag to get to the first element of the
- * component.
- * @param enableUserInput [Boolean] which determines if this BasicTooltipBox will handle long press
- * and mouse hover to trigger the tooltip through the state provided.
- * @param content the composable that the tooltip will anchor to.
- */
-@Composable
-@ExperimentalMaterial3Api
-internal actual fun BasicTooltipBox(
- positionProvider: PopupPositionProvider,
- tooltip: @Composable () -> Unit,
- state: TooltipState,
- modifier: Modifier,
- onDismissRequest: (() -> Unit)?,
- focusable: Boolean,
- enableUserInput: Boolean,
- content: @Composable () -> Unit
-) {
- val scope = rememberCoroutineScope()
- Box {
- if (state.isVisible) {
- TooltipPopup(
- positionProvider = positionProvider,
- state = state,
- onDismissRequest = onDismissRequest,
- scope = scope,
- focusable = focusable,
- content = tooltip
- )
- }
+internal actual object BasicTooltipStrings {
+ @Composable actual fun label(): String = stringResource(R.string.tooltip_label)
- WrappedAnchor(
- enableUserInput = enableUserInput,
- state = state,
- modifier = modifier,
- content = content
- )
- }
-
- DisposableEffect(state) { onDispose { state.onDispose() } }
+ @Composable actual fun description(): String = stringResource(R.string.tooltip_description)
}
-
-@Composable
-private fun WrappedAnchor(
- enableUserInput: Boolean,
- state: TooltipState,
- modifier: Modifier = Modifier,
- content: @Composable () -> Unit
-) {
- val scope = rememberCoroutineScope()
- val longPressLabel = stringResource(R.string.tooltip_label)
- Box(
- modifier =
- modifier
- .handleGestures(enableUserInput, state)
- .anchorSemantics(longPressLabel, enableUserInput, state, scope)
- ) {
- content()
- }
-}
-
-@Composable
-private fun TooltipPopup(
- positionProvider: PopupPositionProvider,
- state: TooltipState,
- onDismissRequest: (() -> Unit)?,
- scope: CoroutineScope,
- focusable: Boolean,
- content: @Composable () -> Unit
-) {
- val tooltipDescription = stringResource(R.string.tooltip_description)
- Popup(
- popupPositionProvider = positionProvider,
- onDismissRequest = {
- if (onDismissRequest == null) {
- if (state.isVisible) {
- scope.launch { state.dismiss() }
- }
- } else {
- onDismissRequest()
- }
- },
- properties = PopupProperties(focusable = focusable)
- ) {
- Box(
- modifier =
- Modifier.semantics {
- liveRegion = LiveRegionMode.Assertive
- paneTitle = tooltipDescription
- }
- ) {
- content()
- }
- }
-}
-
-private fun Modifier.handleGestures(enabled: Boolean, state: TooltipState): Modifier =
- if (enabled) {
- this.pointerInput(state) {
- coroutineScope {
- awaitEachGesture {
- // Long press will finish before or after show so keep track of it, in a
- // flow to handle both cases
- val isLongPressedFlow: MutableStateFlow<Boolean> = MutableStateFlow(false)
- val longPressTimeout = viewConfiguration.longPressTimeoutMillis
- val pass = PointerEventPass.Initial
- // wait for the first down press
- val inputType = awaitFirstDown(pass = pass).type
-
- if (inputType == PointerType.Touch || inputType == PointerType.Stylus) {
- try {
- // listen to if there is up gesture
- // within the longPressTimeout limit
- withTimeout(longPressTimeout) {
- waitForUpOrCancellation(pass = pass)
- }
- } catch (_: PointerEventTimeoutCancellationException) {
- // handle long press - Show the tooltip
- launch(start = CoroutineStart.UNDISPATCHED) {
- try {
- isLongPressedFlow.tryEmit(true)
- state.show(MutatePriority.PreventUserInput)
- } finally {
- if (state.isVisible) {
- isLongPressedFlow.collectLatest { isLongPressed ->
- if (!isLongPressed) {
- state.dismiss()
- }
- }
- }
- }
- }
-
- // consume the children's click handling
- // Long press may still be in progress
- val upEvent = waitForUpOrCancellation(pass = pass)
- upEvent?.consume()
- } finally {
- isLongPressedFlow.tryEmit(false)
- }
- }
- }
- }
- }
- .pointerInput(state) {
- coroutineScope {
- awaitPointerEventScope {
- val pass = PointerEventPass.Main
-
- while (true) {
- val event = awaitPointerEvent(pass)
- val inputType = event.changes[0].type
- if (inputType == PointerType.Mouse) {
- when (event.type) {
- PointerEventType.Enter -> {
- launch { state.show(MutatePriority.UserInput) }
- }
- PointerEventType.Exit -> {
- state.dismiss()
- }
- }
- }
- }
- }
- }
- }
- } else this
-
-private fun Modifier.anchorSemantics(
- label: String,
- enabled: Boolean,
- state: TooltipState,
- scope: CoroutineScope
-): Modifier =
- if (enabled) {
- this.parentSemantics {
- onLongClick(
- label = label,
- action = {
- scope.launch { state.show() }
- true
- }
- )
- }
- } else this
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
index 09ba3d4..391938b 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
@@ -45,8 +45,11 @@
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.internal.AnchoredDraggableState
+import androidx.compose.material3.internal.BackEventCompat
import androidx.compose.material3.internal.DraggableAnchors
import androidx.compose.material3.internal.FloatProducer
+import androidx.compose.material3.internal.PredictiveBack
+import androidx.compose.material3.internal.PredictiveBackHandler
import androidx.compose.material3.internal.Strings
import androidx.compose.material3.internal.anchoredDraggable
import androidx.compose.material3.internal.getString
@@ -58,6 +61,7 @@
import androidx.compose.material3.tokens.ScrimTokens
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
@@ -934,11 +938,70 @@
}
}
+/**
+ * Registers a [PredictiveBackHandler] and provides animation values in [DrawerPredictiveBackState]
+ * based on back progress.
+ *
+ * @param drawerState state of the drawer
+ * @param content content of the rest of the UI
+ */
@Composable
-internal expect fun DrawerPredictiveBackHandler(
+internal fun DrawerPredictiveBackHandler(
drawerState: DrawerState,
content: @Composable (DrawerPredictiveBackState) -> Unit
-)
+) {
+ val drawerPredictiveBackState = remember { DrawerPredictiveBackState() }
+ val scope = rememberCoroutineScope()
+ val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
+ val maxScaleXDistanceGrow: Float
+ val maxScaleXDistanceShrink: Float
+ val maxScaleYDistance: Float
+ with(LocalDensity.current) {
+ maxScaleXDistanceGrow = PredictiveBackDrawerMaxScaleXDistanceGrow.toPx()
+ maxScaleXDistanceShrink = PredictiveBackDrawerMaxScaleXDistanceShrink.toPx()
+ maxScaleYDistance = PredictiveBackDrawerMaxScaleYDistance.toPx()
+ }
+
+ PredictiveBackHandler(enabled = drawerState.isOpen) { progress ->
+ try {
+ progress.collect { backEvent ->
+ drawerPredictiveBackState.update(
+ PredictiveBack.transform(backEvent.progress),
+ backEvent.swipeEdge == BackEventCompat.EDGE_LEFT,
+ isRtl,
+ maxScaleXDistanceGrow,
+ maxScaleXDistanceShrink,
+ maxScaleYDistance
+ )
+ }
+ } catch (e: kotlin.coroutines.cancellation.CancellationException) {
+ drawerPredictiveBackState.clear()
+ } finally {
+ if (drawerPredictiveBackState.swipeEdgeMatchesDrawer) {
+ // If swipe edge matches drawer gravity and we've stretched the drawer horizontally,
+ // un-stretch it smoothly so that it hides completely during the drawer close.
+ scope.launch {
+ animate(
+ initialValue = drawerPredictiveBackState.scaleXDistance,
+ targetValue = 0f
+ ) { value, _ ->
+ drawerPredictiveBackState.scaleXDistance = value
+ }
+ drawerPredictiveBackState.clear()
+ }
+ }
+ drawerState.close()
+ }
+ }
+
+ LaunchedEffect(drawerState.isClosed) {
+ if (drawerState.isClosed) {
+ drawerPredictiveBackState.clear()
+ }
+ }
+
+ content(drawerPredictiveBackState)
+}
/** Object to hold default values for [ModalNavigationDrawer] */
object DrawerDefaults {
@@ -1252,6 +1315,10 @@
private val DrawerVelocityThreshold = 400.dp
private val MinimumDrawerWidth = 240.dp
+internal val PredictiveBackDrawerMaxScaleXDistanceGrow = 12.dp
+internal val PredictiveBackDrawerMaxScaleXDistanceShrink = 24.dp
+internal val PredictiveBackDrawerMaxScaleYDistance = 48.dp
+
// TODO: b/177571613 this should be a proper decay settling
// this is taken from the DrawerLayout's DragViewHelper as a min duration.
private val AnchoredDraggableDefaultAnimationSpec = TweenSpec<Float>(durationMillis = 256)
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt
index 50dffb1..11e2e8f 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt
@@ -23,9 +23,12 @@
import androidx.compose.foundation.MutatePriority
import androidx.compose.foundation.MutatorMutex
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.paddingFromBaseline
+import androidx.compose.foundation.layout.requiredHeightIn
+import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material3.internal.BasicTooltipBox
import androidx.compose.material3.internal.BasicTooltipDefaults
import androidx.compose.material3.tokens.ElevationTokens
@@ -33,6 +36,7 @@
import androidx.compose.material3.tokens.PlainTooltipTokens
import androidx.compose.material3.tokens.RichTooltipTokens
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
@@ -44,14 +48,18 @@
import androidx.compose.ui.draw.CacheDrawScope
import androidx.compose.ui.draw.DrawResult
import androidx.compose.ui.draw.drawWithCache
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntOffset
@@ -59,6 +67,7 @@
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.isSpecified
import androidx.compose.ui.window.PopupPositionProvider
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.suspendCancellableCoroutine
@@ -258,40 +267,6 @@
* @param caretSize [DpSize] for the caret of the tooltip, if a default caret is desired with a
* specific dimension. Please see [TooltipDefaults.caretSize] to see the default dimensions. Pass
* in Dp.Unspecified for this parameter if no caret is desired.
- * @param shape the [Shape] that should be applied to the tooltip container.
- * @param contentColor [Color] that will be applied to the tooltip's content.
- * @param containerColor [Color] that will be applied to the tooltip's container.
- * @param tonalElevation the tonal elevation of the tooltip.
- * @param shadowElevation the shadow elevation of the tooltip.
- * @param content the composable that will be used to populate the tooltip's content.
- */
-@Suppress("DEPRECATION")
-@Deprecated(
- level = DeprecationLevel.HIDDEN,
- message = "Maintained for binary compatibility. " + "Use overload with maxWidth parameter."
-)
-@Composable
-@ExperimentalMaterial3Api
-expect fun TooltipScope.PlainTooltip(
- modifier: Modifier = Modifier,
- caretSize: DpSize = DpSize.Unspecified,
- shape: Shape = TooltipDefaults.plainTooltipContainerShape,
- contentColor: Color = TooltipDefaults.plainTooltipContentColor,
- containerColor: Color = TooltipDefaults.plainTooltipContainerColor,
- tonalElevation: Dp = 0.dp,
- shadowElevation: Dp = 0.dp,
- content: @Composable () -> Unit
-)
-
-/**
- * Plain tooltip that provides a descriptive message.
- *
- * Usually used with [TooltipBox].
- *
- * @param modifier the [Modifier] to be applied to the tooltip.
- * @param caretSize [DpSize] for the caret of the tooltip, if a default caret is desired with a
- * specific dimension. Please see [TooltipDefaults.caretSize] to see the default dimensions. Pass
- * in Dp.Unspecified for this parameter if no caret is desired.
* @param maxWidth the maximum width for the plain tooltip
* @param shape the [Shape] that should be applied to the tooltip container.
* @param contentColor [Color] that will be applied to the tooltip's content.
@@ -302,7 +277,7 @@
*/
@Composable
@ExperimentalMaterial3Api
-expect fun TooltipScope.PlainTooltip(
+fun TooltipScope.PlainTooltip(
modifier: Modifier = Modifier,
caretSize: DpSize = DpSize.Unspecified,
maxWidth: Dp = TooltipDefaults.plainTooltipMaxWidth,
@@ -312,44 +287,48 @@
tonalElevation: Dp = 0.dp,
shadowElevation: Dp = 0.dp,
content: @Composable () -> Unit
-)
+) {
+ val drawCaretModifier =
+ if (caretSize.isSpecified) {
+ val density = LocalDensity.current
+ val windowContainerWidthInPx = windowContainerWidthInPx()
+ Modifier.drawCaret { anchorLayoutCoordinates ->
+ drawCaretWithPath(
+ density,
+ windowContainerWidthInPx,
+ containerColor,
+ caretSize,
+ anchorLayoutCoordinates
+ )
+ }
+ .then(modifier)
+ } else modifier
+ Surface(
+ modifier = drawCaretModifier,
+ shape = shape,
+ color = containerColor,
+ tonalElevation = tonalElevation,
+ shadowElevation = shadowElevation
+ ) {
+ Box(
+ modifier =
+ Modifier.sizeIn(
+ minWidth = TooltipMinWidth,
+ maxWidth = maxWidth,
+ minHeight = TooltipMinHeight
+ )
+ .padding(PlainTooltipContentPadding)
+ ) {
+ val textStyle = PlainTooltipTokens.SupportingTextFont.value
-/**
- * Rich text tooltip that allows the user to pass in a title, text, and action. Tooltips are used to
- * provide a descriptive message.
- *
- * Usually used with [TooltipBox]
- *
- * @param modifier the [Modifier] to be applied to the tooltip.
- * @param title An optional title for the tooltip.
- * @param action An optional action for the tooltip.
- * @param caretSize [DpSize] for the caret of the tooltip, if a default caret is desired with a
- * specific dimension. Please see [TooltipDefaults.caretSize] to see the default dimensions. Pass
- * in Dp.Unspecified for this parameter if no caret is desired.
- * @param shape the [Shape] that should be applied to the tooltip container.
- * @param colors [RichTooltipColors] that will be applied to the tooltip's container and content.
- * @param tonalElevation the tonal elevation of the tooltip.
- * @param shadowElevation the shadow elevation of the tooltip.
- * @param text the composable that will be used to populate the rich tooltip's text.
- */
-@Suppress("DEPRECATION")
-@Deprecated(
- level = DeprecationLevel.HIDDEN,
- message = "Maintained for binary compatibility. " + "Use overload with maxWidth parameter."
-)
-@Composable
-@ExperimentalMaterial3Api
-expect fun TooltipScope.RichTooltip(
- modifier: Modifier = Modifier,
- title: (@Composable () -> Unit)? = null,
- action: (@Composable () -> Unit)? = null,
- caretSize: DpSize = DpSize.Unspecified,
- shape: Shape = TooltipDefaults.richTooltipContainerShape,
- colors: RichTooltipColors = TooltipDefaults.richTooltipColors(),
- tonalElevation: Dp = ElevationTokens.Level0,
- shadowElevation: Dp = RichTooltipTokens.ContainerElevation,
- text: @Composable () -> Unit
-)
+ CompositionLocalProvider(
+ LocalContentColor provides contentColor,
+ LocalTextStyle provides textStyle,
+ content = content
+ )
+ }
+ }
+}
/**
* Rich text tooltip that allows the user to pass in a title, text, and action. Tooltips are used to
@@ -372,7 +351,7 @@
*/
@Composable
@ExperimentalMaterial3Api
-expect fun TooltipScope.RichTooltip(
+fun TooltipScope.RichTooltip(
modifier: Modifier = Modifier,
title: (@Composable () -> Unit)? = null,
action: (@Composable () -> Unit)? = null,
@@ -383,7 +362,74 @@
tonalElevation: Dp = ElevationTokens.Level0,
shadowElevation: Dp = RichTooltipTokens.ContainerElevation,
text: @Composable () -> Unit
-)
+) {
+ val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation
+ val elevatedColor =
+ MaterialTheme.colorScheme.applyTonalElevation(colors.containerColor, absoluteElevation)
+ val drawCaretModifier =
+ if (caretSize.isSpecified) {
+ val density = LocalDensity.current
+ val windowContainerWidthInPx = windowContainerWidthInPx()
+ Modifier.drawCaret { anchorLayoutCoordinates ->
+ drawCaretWithPath(
+ density,
+ windowContainerWidthInPx,
+ elevatedColor,
+ caretSize,
+ anchorLayoutCoordinates
+ )
+ }
+ .then(modifier)
+ } else modifier
+ Surface(
+ modifier =
+ drawCaretModifier.sizeIn(
+ minWidth = TooltipMinWidth,
+ maxWidth = maxWidth,
+ minHeight = TooltipMinHeight
+ ),
+ shape = shape,
+ color = colors.containerColor,
+ tonalElevation = tonalElevation,
+ shadowElevation = shadowElevation
+ ) {
+ val actionLabelTextStyle = RichTooltipTokens.ActionLabelTextFont.value
+ val subheadTextStyle = RichTooltipTokens.SubheadFont.value
+ val supportingTextStyle = RichTooltipTokens.SupportingTextFont.value
+
+ Column(modifier = Modifier.padding(horizontal = RichTooltipHorizontalPadding)) {
+ title?.let {
+ Box(modifier = Modifier.paddingFromBaseline(top = HeightToSubheadFirstLine)) {
+ CompositionLocalProvider(
+ LocalContentColor provides colors.titleContentColor,
+ LocalTextStyle provides subheadTextStyle,
+ content = it
+ )
+ }
+ }
+ Box(modifier = Modifier.textVerticalPadding(title != null, action != null)) {
+ CompositionLocalProvider(
+ LocalContentColor provides colors.contentColor,
+ LocalTextStyle provides supportingTextStyle,
+ content = text
+ )
+ }
+ action?.let {
+ Box(
+ modifier =
+ Modifier.requiredHeightIn(min = ActionLabelMinHeight)
+ .padding(bottom = ActionLabelBottomPadding)
+ ) {
+ CompositionLocalProvider(
+ LocalContentColor provides colors.actionContentColor,
+ LocalTextStyle provides actionLabelTextStyle,
+ content = it
+ )
+ }
+ }
+ }
+ }
+}
/** Tooltip defaults that contain default values for both [PlainTooltip] and [RichTooltip] */
@ExperimentalMaterial3Api
@@ -821,6 +867,95 @@
this.graphicsLayer(scaleX = scale, scaleY = scale, alpha = alpha)
}
+@ExperimentalMaterial3Api
+private fun CacheDrawScope.drawCaretWithPath(
+ density: Density,
+ windowContainerWidthInPx: Int,
+ containerColor: Color,
+ caretSize: DpSize,
+ anchorLayoutCoordinates: LayoutCoordinates?
+): DrawResult {
+ val path = Path()
+
+ if (anchorLayoutCoordinates != null) {
+ val caretHeightPx: Int
+ val caretWidthPx: Int
+ val screenWidthPx: Int
+ val tooltipAnchorSpacing: Int
+ with(density) {
+ caretHeightPx = caretSize.height.roundToPx()
+ caretWidthPx = caretSize.width.roundToPx()
+ screenWidthPx = windowContainerWidthInPx
+ tooltipAnchorSpacing = SpacingBetweenTooltipAndAnchor.roundToPx()
+ }
+ val anchorBounds = anchorLayoutCoordinates.boundsInWindow()
+ val anchorLeft = anchorBounds.left
+ val anchorRight = anchorBounds.right
+ val anchorTop = anchorBounds.top
+ val anchorMid = (anchorRight + anchorLeft) / 2
+ val anchorWidth = anchorRight - anchorLeft
+ val tooltipWidth = this.size.width
+ val tooltipHeight = this.size.height
+ val isCaretTop = anchorTop - tooltipHeight - tooltipAnchorSpacing < 0
+ val caretY =
+ if (isCaretTop) {
+ 0f
+ } else {
+ tooltipHeight
+ }
+
+ // Default the caret to be in the middle
+ // caret might need to be offset depending on where
+ // the tooltip is placed relative to the anchor
+ var position: Offset =
+ if (anchorLeft - tooltipWidth / 2 + anchorWidth / 2 <= 0) {
+ Offset(anchorMid, caretY)
+ } else if (anchorRight + tooltipWidth / 2 - anchorWidth / 2 >= screenWidthPx) {
+ val anchorMidFromRightScreenEdge = screenWidthPx - anchorMid
+ val caretX = tooltipWidth - anchorMidFromRightScreenEdge
+ Offset(caretX, caretY)
+ } else {
+ Offset(tooltipWidth / 2, caretY)
+ }
+ if (anchorMid - tooltipWidth / 2 < 0) {
+ // The tooltip needs to be start aligned if it would collide with the left side of
+ // screen.
+ position = Offset(anchorMid - anchorLeft, caretY)
+ } else if (anchorMid + tooltipWidth / 2 > screenWidthPx) {
+ // The tooltip needs to be end aligned if it would collide with the right side of the
+ // screen.
+ position = Offset(anchorMid - (anchorRight - tooltipWidth), caretY)
+ }
+
+ if (isCaretTop) {
+ path.apply {
+ moveTo(x = position.x, y = position.y)
+ lineTo(x = position.x + caretWidthPx / 2, y = position.y)
+ lineTo(x = position.x, y = position.y - caretHeightPx)
+ lineTo(x = position.x - caretWidthPx / 2, y = position.y)
+ close()
+ }
+ } else {
+ path.apply {
+ moveTo(x = position.x, y = position.y)
+ lineTo(x = position.x + caretWidthPx / 2, y = position.y)
+ lineTo(x = position.x, y = position.y + caretHeightPx.toFloat())
+ lineTo(x = position.x - caretWidthPx / 2, y = position.y)
+ close()
+ }
+ }
+ }
+
+ return onDrawWithContent {
+ if (anchorLayoutCoordinates != null) {
+ drawContent()
+ drawPath(path = path, color = containerColor)
+ }
+ }
+}
+
+@Composable internal expect fun windowContainerWidthInPx(): Int
+
internal val SpacingBetweenTooltipAndAnchor = 4.dp
internal val TooltipMinHeight = 24.dp
internal val TooltipMinWidth = 40.dp
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/BasicTooltip.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/BasicTooltip.kt
index a9114ca..19d6004 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/BasicTooltip.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/BasicTooltip.kt
@@ -21,17 +21,41 @@
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.foundation.MutatePriority
import androidx.compose.foundation.MutatorMutex
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.waitForUpOrCancellation
+import androidx.compose.foundation.layout.Box
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TooltipState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerEventTimeoutCancellationException
+import androidx.compose.ui.input.pointer.PointerEventType
+import androidx.compose.ui.input.pointer.PointerType
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.semantics.LiveRegionMode
+import androidx.compose.ui.semantics.liveRegion
+import androidx.compose.ui.semantics.onLongClick
+import androidx.compose.ui.semantics.paneTitle
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupPositionProvider
+import androidx.compose.ui.window.PopupProperties
import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeout
@@ -58,7 +82,7 @@
* @param content the composable that the tooltip will anchor to.
*/
@Composable
-internal expect fun BasicTooltipBox(
+internal fun BasicTooltipBox(
positionProvider: PopupPositionProvider,
tooltip: @Composable () -> Unit,
state: TooltipState,
@@ -67,7 +91,174 @@
focusable: Boolean = true,
enableUserInput: Boolean = true,
content: @Composable () -> Unit
-)
+) {
+ val scope = rememberCoroutineScope()
+ Box {
+ if (state.isVisible) {
+ TooltipPopup(
+ positionProvider = positionProvider,
+ state = state,
+ onDismissRequest = onDismissRequest,
+ scope = scope,
+ focusable = focusable,
+ content = tooltip
+ )
+ }
+
+ WrappedAnchor(
+ enableUserInput = enableUserInput,
+ state = state,
+ modifier = modifier,
+ content = content
+ )
+ }
+
+ DisposableEffect(state) { onDispose { state.onDispose() } }
+}
+
+@Composable
+private fun WrappedAnchor(
+ enableUserInput: Boolean,
+ state: TooltipState,
+ modifier: Modifier = Modifier,
+ content: @Composable () -> Unit
+) {
+ val scope = rememberCoroutineScope()
+ val longPressLabel = BasicTooltipStrings.label()
+ Box(
+ modifier =
+ modifier
+ .handleGestures(enableUserInput, state)
+ .anchorSemantics(longPressLabel, enableUserInput, state, scope)
+ ) {
+ content()
+ }
+}
+
+@Composable
+private fun TooltipPopup(
+ positionProvider: PopupPositionProvider,
+ state: TooltipState,
+ onDismissRequest: (() -> Unit)?,
+ scope: CoroutineScope,
+ focusable: Boolean,
+ content: @Composable () -> Unit
+) {
+ val tooltipDescription = BasicTooltipStrings.description()
+ Popup(
+ popupPositionProvider = positionProvider,
+ onDismissRequest = {
+ if (onDismissRequest == null) {
+ if (state.isVisible) {
+ scope.launch { state.dismiss() }
+ }
+ } else {
+ onDismissRequest()
+ }
+ },
+ properties = PopupProperties(focusable = focusable)
+ ) {
+ Box(
+ modifier =
+ Modifier.semantics {
+ liveRegion = LiveRegionMode.Assertive
+ paneTitle = tooltipDescription
+ }
+ ) {
+ content()
+ }
+ }
+}
+
+private fun Modifier.handleGestures(enabled: Boolean, state: TooltipState): Modifier =
+ if (enabled) {
+ this.pointerInput(state) {
+ coroutineScope {
+ awaitEachGesture {
+ // Long press will finish before or after show so keep track of it, in a
+ // flow to handle both cases
+ val isLongPressedFlow: MutableStateFlow<Boolean> = MutableStateFlow(false)
+ val longPressTimeout = viewConfiguration.longPressTimeoutMillis
+ val pass = PointerEventPass.Initial
+ // wait for the first down press
+ val inputType = awaitFirstDown(pass = pass).type
+
+ if (inputType == PointerType.Touch || inputType == PointerType.Stylus) {
+ try {
+ // listen to if there is up gesture
+ // within the longPressTimeout limit
+ withTimeout(longPressTimeout) {
+ waitForUpOrCancellation(pass = pass)
+ }
+ } catch (_: PointerEventTimeoutCancellationException) {
+ // handle long press - Show the tooltip
+ launch(start = CoroutineStart.UNDISPATCHED) {
+ try {
+ isLongPressedFlow.tryEmit(true)
+ state.show(MutatePriority.PreventUserInput)
+ } finally {
+ if (state.isVisible) {
+ isLongPressedFlow.collectLatest { isLongPressed ->
+ if (!isLongPressed) {
+ state.dismiss()
+ }
+ }
+ }
+ }
+ }
+
+ // consume the children's click handling
+ // Long press may still be in progress
+ val upEvent = waitForUpOrCancellation(pass = pass)
+ upEvent?.consume()
+ } finally {
+ isLongPressedFlow.tryEmit(false)
+ }
+ }
+ }
+ }
+ }
+ .pointerInput(state) {
+ coroutineScope {
+ awaitPointerEventScope {
+ val pass = PointerEventPass.Main
+
+ while (true) {
+ val event = awaitPointerEvent(pass)
+ val inputType = event.changes[0].type
+ if (inputType == PointerType.Mouse) {
+ when (event.type) {
+ PointerEventType.Enter -> {
+ launch { state.show(MutatePriority.UserInput) }
+ }
+ PointerEventType.Exit -> {
+ state.dismiss()
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ } else this
+
+private fun Modifier.anchorSemantics(
+ label: String,
+ enabled: Boolean,
+ state: TooltipState,
+ scope: CoroutineScope
+): Modifier =
+ if (enabled) {
+ this.parentSemantics {
+ onLongClick(
+ label = label,
+ action = {
+ scope.launch { state.show() }
+ true
+ }
+ )
+ }
+ } else this
/**
* Create and remember the default [BasicTooltipState].
@@ -186,3 +377,10 @@
*/
const val TooltipDuration = 1500L
}
+
+@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
+internal expect object BasicTooltipStrings {
+ @Composable fun label(): String
+
+ @Composable fun description(): String
+}
diff --git a/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/NavigationDrawer.commonStubs.kt b/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/NavigationDrawer.commonStubs.kt
deleted file mode 100644
index 39fe5a5..0000000
--- a/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/NavigationDrawer.commonStubs.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.material3
-
-import androidx.compose.runtime.Composable
-
-@Composable
-internal actual fun DrawerPredictiveBackHandler(
- drawerState: DrawerState,
- content: @Composable (DrawerPredictiveBackState) -> Unit
-): Unit = implementedInJetBrainsFork()
diff --git a/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/Tooltip.commonStubs.kt b/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/Tooltip.commonStubs.kt
index 6c8027d..72f9b6f 100644
--- a/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/Tooltip.commonStubs.kt
+++ b/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/Tooltip.commonStubs.kt
@@ -17,64 +17,5 @@
package androidx.compose.material3
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.DpSize
-@Composable
-@ExperimentalMaterial3Api
-actual fun TooltipScope.PlainTooltip(
- modifier: Modifier,
- caretSize: DpSize,
- shape: Shape,
- contentColor: Color,
- containerColor: Color,
- tonalElevation: Dp,
- shadowElevation: Dp,
- content: @Composable () -> Unit
-): Unit = implementedInJetBrainsFork()
-
-@Composable
-@ExperimentalMaterial3Api
-actual fun TooltipScope.PlainTooltip(
- modifier: Modifier,
- caretSize: DpSize,
- maxWidth: Dp,
- shape: Shape,
- contentColor: Color,
- containerColor: Color,
- tonalElevation: Dp,
- shadowElevation: Dp,
- content: @Composable () -> Unit
-): Unit = implementedInJetBrainsFork()
-
-@Composable
-@ExperimentalMaterial3Api
-actual fun TooltipScope.RichTooltip(
- modifier: Modifier,
- title: (@Composable () -> Unit)?,
- action: (@Composable () -> Unit)?,
- caretSize: DpSize,
- shape: Shape,
- colors: RichTooltipColors,
- tonalElevation: Dp,
- shadowElevation: Dp,
- text: @Composable () -> Unit
-): Unit = implementedInJetBrainsFork()
-
-@Composable
-@ExperimentalMaterial3Api
-actual fun TooltipScope.RichTooltip(
- modifier: Modifier,
- title: (@Composable () -> Unit)?,
- action: (@Composable () -> Unit)?,
- caretSize: DpSize,
- maxWidth: Dp,
- shape: Shape,
- colors: RichTooltipColors,
- tonalElevation: Dp,
- shadowElevation: Dp,
- text: @Composable () -> Unit
-): Unit = implementedInJetBrainsFork()
+@Composable internal actual fun windowContainerWidthInPx(): Int = implementedInJetBrainsFork()
diff --git a/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/internal/BasicTooltip.commonStubs.kt b/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/internal/BasicTooltip.commonStubs.kt
index 89c7499..cd96c5f 100644
--- a/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/internal/BasicTooltip.commonStubs.kt
+++ b/compose/material3/material3/src/commonStubsMain/kotlin/androidx/compose/material3/internal/BasicTooltip.commonStubs.kt
@@ -16,22 +16,12 @@
package androidx.compose.material3.internal
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.TooltipState
import androidx.compose.material3.implementedInJetBrainsFork
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.window.PopupPositionProvider
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-internal actual fun BasicTooltipBox(
- positionProvider: PopupPositionProvider,
- tooltip: @Composable () -> Unit,
- state: TooltipState,
- modifier: Modifier,
- onDismissRequest: (() -> Unit)?,
- focusable: Boolean,
- enableUserInput: Boolean,
- content: @Composable () -> Unit
-): Unit = implementedInJetBrainsFork()
+@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
+internal actual object BasicTooltipStrings {
+ @Composable actual fun label(): String = implementedInJetBrainsFork()
+
+ @Composable actual fun description(): String = implementedInJetBrainsFork()
+}
diff --git a/development/plot-benchmarks/.nvmrc b/development/plot-benchmarks/.nvmrc
index 238155b..2f68077 100644
--- a/development/plot-benchmarks/.nvmrc
+++ b/development/plot-benchmarks/.nvmrc
@@ -1 +1 @@
-v20.12.2
\ No newline at end of file
+v22.12.0
\ No newline at end of file
diff --git a/development/plot-benchmarks/index.html b/development/plot-benchmarks/index.html
index 0e19900..ca577dc 100644
--- a/development/plot-benchmarks/index.html
+++ b/development/plot-benchmarks/index.html
@@ -5,7 +5,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Plot Benchmarks</title>
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
</head>
<body>
diff --git a/development/plot-benchmarks/package-lock.json b/development/plot-benchmarks/package-lock.json
index c6f8262..2f5a92f 100644
--- a/development/plot-benchmarks/package-lock.json
+++ b/development/plot-benchmarks/package-lock.json
@@ -8,16 +8,16 @@
"name": "plot-benchmarks",
"version": "0.2.0",
"dependencies": {
- "chart.js": "^4.4.6",
+ "chart.js": "^4.4.7",
"comlink": "4.4.2"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^4.0.0-next.6",
"@tsconfig/svelte": "^5.0.4",
- "svelte": "^5.1.12",
- "svelte-check": "^4.0.5",
+ "svelte": "^5.16.1",
+ "svelte-check": "^4.1.1",
"tslib": "^2.8.1",
- "typescript": "^5.6.3",
+ "typescript": "^5.7.2",
"vite": "^5.4.10"
}
},
@@ -758,7 +758,7 @@
"vite": "^5.0.0"
}
},
- "node_modules/@sveltejs/vite-plugin-svelte-inspector": {
+ "node_modules/@sveltejs/vite-plugin-svelte/node_modules/@sveltejs/vite-plugin-svelte-inspector": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.1.tgz",
"integrity": "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==",
@@ -834,9 +834,9 @@
}
},
"node_modules/chart.js": {
- "version": "4.4.6",
- "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.6.tgz",
- "integrity": "sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA==",
+ "version": "4.4.7",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz",
+ "integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==",
"license": "MIT",
"dependencies": {
"@kurkle/color": "^0.3.0"
@@ -861,6 +861,16 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/comlink": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.2.tgz",
@@ -935,21 +945,20 @@
}
},
"node_modules/esm-env": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.1.4.tgz",
- "integrity": "sha512-oO82nKPHKkzIj/hbtuDYy/JHqBHFlMIW36SDiPCVsj87ntDLcWN+sJ1erdVryd4NxODacFTsdrIE3b7IamqbOg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.1.tgz",
+ "integrity": "sha512-U9JedYYjCnadUlXk7e1Kr+aENQhtUaoaV9+gZm1T8LC/YBAPJx3NSPIAurFOC0U5vrdSevnUJS2/wUVxGwPhng==",
"dev": true,
"license": "MIT"
},
"node_modules/esrap": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.2.2.tgz",
- "integrity": "sha512-F2pSJklxx1BlQIQgooczXCPHmcWpn6EsP5oo73LQfonG9fIlIENQ8vMmfGXeojP9MrkzUNAfyU5vdFlR9shHAw==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.3.2.tgz",
+ "integrity": "sha512-C4PXusxYhFT98GjLSmb20k9PREuUdporer50dhzGuJu9IJXktbMddVCMLAERl5dAHyAi73GWWCE4FVHGP1794g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jridgewell/sourcemap-codec": "^1.4.15",
- "@types/estree": "^1.0.1"
+ "@jridgewell/sourcemap-codec": "^1.4.15"
}
},
"node_modules/fdir": {
@@ -983,13 +992,13 @@
}
},
"node_modules/is-reference": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
- "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==",
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
+ "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@types/estree": "*"
+ "@types/estree": "^1.0.6"
}
},
"node_modules/kleur": {
@@ -1035,9 +1044,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
- "version": "3.3.7",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
- "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+ "version": "3.3.8",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+ "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"dev": true,
"funding": [
{
@@ -1061,9 +1070,9 @@
"license": "ISC"
},
"node_modules/postcss": {
- "version": "8.4.47",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
- "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
+ "version": "8.4.49",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
+ "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
"dev": true,
"funding": [
{
@@ -1082,7 +1091,7 @@
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
- "picocolors": "^1.1.0",
+ "picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
@@ -1164,9 +1173,9 @@
}
},
"node_modules/svelte": {
- "version": "5.1.12",
- "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.1.12.tgz",
- "integrity": "sha512-U9BwbSybb9QAKAHg4hl61hVBk97U2QjUKmZa5++QEGoi6Nml6x6cC9KmNT1XObGawToN3DdLpdCs/Z5Yl5IXjQ==",
+ "version": "5.16.1",
+ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.16.1.tgz",
+ "integrity": "sha512-FsA1OjAKMAFSDob6j/Tv2ZV9rY4SeqPd1WXQlQkFkePAozSHLp6tbkU9qa1xJ+uTRzMSM2Vx3USdsYZBXd3H3g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1177,9 +1186,10 @@
"acorn-typescript": "^1.4.13",
"aria-query": "^5.3.1",
"axobject-query": "^4.1.0",
- "esm-env": "^1.0.0",
- "esrap": "^1.2.2",
- "is-reference": "^3.0.2",
+ "clsx": "^2.1.1",
+ "esm-env": "^1.2.1",
+ "esrap": "^1.3.2",
+ "is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",
"zimmerframe": "^1.1.2"
@@ -1189,9 +1199,9 @@
}
},
"node_modules/svelte-check": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.0.5.tgz",
- "integrity": "sha512-icBTBZ3ibBaywbXUat3cK6hB5Du+Kq9Z8CRuyLmm64XIe2/r+lQcbuBx/IQgsbrC+kT2jQ0weVpZSSRIPwB6jQ==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.1.1.tgz",
+ "integrity": "sha512-NfaX+6Qtc8W/CyVGS/F7/XdiSSyXz+WGYA9ZWV3z8tso14V2vzjfXviKaTFEzB7g8TqfgO2FOzP6XT4ApSTUTw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1220,9 +1230,9 @@
"license": "0BSD"
},
"node_modules/typescript": {
- "version": "5.6.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
- "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
+ "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -1234,9 +1244,9 @@
}
},
"node_modules/vite": {
- "version": "5.4.10",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz",
- "integrity": "sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==",
+ "version": "5.4.11",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
+ "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1674,15 +1684,17 @@
"kleur": "^4.1.5",
"magic-string": "^0.30.12",
"vitefu": "^1.0.3"
- }
- },
- "@sveltejs/vite-plugin-svelte-inspector": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.1.tgz",
- "integrity": "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==",
- "dev": true,
- "requires": {
- "debug": "^4.3.7"
+ },
+ "dependencies": {
+ "@sveltejs/vite-plugin-svelte-inspector": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.1.tgz",
+ "integrity": "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==",
+ "dev": true,
+ "requires": {
+ "debug": "^4.3.7"
+ }
+ }
}
},
"@tsconfig/svelte": {
@@ -1723,9 +1735,9 @@
"dev": true
},
"chart.js": {
- "version": "4.4.6",
- "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.6.tgz",
- "integrity": "sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA==",
+ "version": "4.4.7",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz",
+ "integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==",
"requires": {
"@kurkle/color": "^0.3.0"
}
@@ -1739,6 +1751,12 @@
"readdirp": "^4.0.1"
}
},
+ "clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "dev": true
+ },
"comlink": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.2.tgz",
@@ -1791,19 +1809,18 @@
}
},
"esm-env": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.1.4.tgz",
- "integrity": "sha512-oO82nKPHKkzIj/hbtuDYy/JHqBHFlMIW36SDiPCVsj87ntDLcWN+sJ1erdVryd4NxODacFTsdrIE3b7IamqbOg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.1.tgz",
+ "integrity": "sha512-U9JedYYjCnadUlXk7e1Kr+aENQhtUaoaV9+gZm1T8LC/YBAPJx3NSPIAurFOC0U5vrdSevnUJS2/wUVxGwPhng==",
"dev": true
},
"esrap": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.2.2.tgz",
- "integrity": "sha512-F2pSJklxx1BlQIQgooczXCPHmcWpn6EsP5oo73LQfonG9fIlIENQ8vMmfGXeojP9MrkzUNAfyU5vdFlR9shHAw==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.3.2.tgz",
+ "integrity": "sha512-C4PXusxYhFT98GjLSmb20k9PREuUdporer50dhzGuJu9IJXktbMddVCMLAERl5dAHyAi73GWWCE4FVHGP1794g==",
"dev": true,
"requires": {
- "@jridgewell/sourcemap-codec": "^1.4.15",
- "@types/estree": "^1.0.1"
+ "@jridgewell/sourcemap-codec": "^1.4.15"
}
},
"fdir": {
@@ -1821,12 +1838,12 @@
"optional": true
},
"is-reference": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
- "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==",
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
+ "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
"dev": true,
"requires": {
- "@types/estree": "*"
+ "@types/estree": "^1.0.6"
}
},
"kleur": {
@@ -1863,9 +1880,9 @@
"dev": true
},
"nanoid": {
- "version": "3.3.7",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
- "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+ "version": "3.3.8",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+ "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"dev": true
},
"picocolors": {
@@ -1875,13 +1892,13 @@
"dev": true
},
"postcss": {
- "version": "8.4.47",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
- "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
+ "version": "8.4.49",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
+ "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
"dev": true,
"requires": {
"nanoid": "^3.3.7",
- "picocolors": "^1.1.0",
+ "picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
}
},
@@ -1935,9 +1952,9 @@
"dev": true
},
"svelte": {
- "version": "5.1.12",
- "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.1.12.tgz",
- "integrity": "sha512-U9BwbSybb9QAKAHg4hl61hVBk97U2QjUKmZa5++QEGoi6Nml6x6cC9KmNT1XObGawToN3DdLpdCs/Z5Yl5IXjQ==",
+ "version": "5.16.1",
+ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.16.1.tgz",
+ "integrity": "sha512-FsA1OjAKMAFSDob6j/Tv2ZV9rY4SeqPd1WXQlQkFkePAozSHLp6tbkU9qa1xJ+uTRzMSM2Vx3USdsYZBXd3H3g==",
"dev": true,
"requires": {
"@ampproject/remapping": "^2.3.0",
@@ -1947,18 +1964,19 @@
"acorn-typescript": "^1.4.13",
"aria-query": "^5.3.1",
"axobject-query": "^4.1.0",
- "esm-env": "^1.0.0",
- "esrap": "^1.2.2",
- "is-reference": "^3.0.2",
+ "clsx": "^2.1.1",
+ "esm-env": "^1.2.1",
+ "esrap": "^1.3.2",
+ "is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",
"zimmerframe": "^1.1.2"
}
},
"svelte-check": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.0.5.tgz",
- "integrity": "sha512-icBTBZ3ibBaywbXUat3cK6hB5Du+Kq9Z8CRuyLmm64XIe2/r+lQcbuBx/IQgsbrC+kT2jQ0weVpZSSRIPwB6jQ==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.1.1.tgz",
+ "integrity": "sha512-NfaX+6Qtc8W/CyVGS/F7/XdiSSyXz+WGYA9ZWV3z8tso14V2vzjfXviKaTFEzB7g8TqfgO2FOzP6XT4ApSTUTw==",
"dev": true,
"requires": {
"@jridgewell/trace-mapping": "^0.3.25",
@@ -1975,15 +1993,15 @@
"dev": true
},
"typescript": {
- "version": "5.6.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
- "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
+ "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
"dev": true
},
"vite": {
- "version": "5.4.10",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz",
- "integrity": "sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==",
+ "version": "5.4.11",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
+ "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
"dev": true,
"requires": {
"esbuild": "^0.21.3",
diff --git a/development/plot-benchmarks/package.json b/development/plot-benchmarks/package.json
index 0a30e86..73bf40c 100644
--- a/development/plot-benchmarks/package.json
+++ b/development/plot-benchmarks/package.json
@@ -12,14 +12,14 @@
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^4.0.0-next.6",
"@tsconfig/svelte": "^5.0.4",
- "svelte": "^5.1.12",
- "svelte-check": "^4.0.5",
+ "svelte": "^5.16.1",
+ "svelte-check": "^4.1.1",
"tslib": "^2.8.1",
- "typescript": "^5.6.3",
+ "typescript": "^5.7.2",
"vite": "^5.4.10"
},
"dependencies": {
- "chart.js": "^4.4.6",
+ "chart.js": "^4.4.7",
"comlink": "4.4.2"
}
}
\ No newline at end of file
diff --git a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
index 5cb1563..ceaad52 100644
--- a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
+++ b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
@@ -563,6 +563,64 @@
}
/**
+ * {@code webp_without_exif_trailing_data.webp} contains the same data as {@code
+ * webp_without_exif.webp} with {@code 0xDEADBEEFDEADBEEF} appended on the end (but excluded
+ * from the RIFF length).
+ *
+ * <p>This test ensures the resulting file is valid (i.e. the trailing data is still excluded
+ * from RIFF length).
+ */
+ // https://issuetracker.google.com/385766064
+ @Test
+ @LargeTest
+ public void testWebpWithoutExifAndTrailingData() throws Throwable {
+ File imageFile =
+ copyFromResourceToFile(
+ R.raw.webp_without_exif_trailing_data,
+ "webp_without_exif_trailing_data.webp");
+ testWritingExif(imageFile, /* expectedAttributes= */ null);
+ }
+
+ /**
+ * {@code webp_without_exif_trailing_data.webp} contains the same data as {@code
+ * webp_without_exif.webp} with {@code 0xDEADBEEFDEADBEEF} appended on the end (but excluded
+ * from the RIFF length).
+ *
+ * <p>This test ensures the trailing data is preserved.
+ */
+ // https://issuetracker.google.com/385766064
+ @Test
+ @LargeTest
+ public void testWebpWithoutExifAndTrailingData_trailingDataPreserved() throws Throwable {
+ File imageFile =
+ copyFromResourceToFile(
+ R.raw.webp_without_exif_trailing_data,
+ "webp_without_exif_trailing_data.webp");
+
+ ExifInterface exifInterface = new ExifInterface(imageFile.getAbsolutePath());
+ exifInterface.saveAttributes();
+
+ byte[] imageData = Files.toByteArray(imageFile);
+ byte[] expectedTrailingData =
+ new byte[] {
+ (byte) 0xDE,
+ (byte) 0xAD,
+ (byte) 0xBE,
+ (byte) 0xEF,
+ (byte) 0xDE,
+ (byte) 0xAD,
+ (byte) 0xBE,
+ (byte) 0xEF
+ };
+ byte[] actualTrailingData =
+ Arrays.copyOfRange(
+ imageData,
+ imageData.length - expectedTrailingData.length,
+ imageData.length);
+ assertThat(actualTrailingData).isEqualTo(expectedTrailingData);
+ }
+
+ /**
* Support for retrieving EXIF from HEIC was added in SDK 28.
*/
@Test
diff --git a/exifinterface/exifinterface/src/androidTest/res/raw/webp_without_exif_trailing_data.webp b/exifinterface/exifinterface/src/androidTest/res/raw/webp_without_exif_trailing_data.webp
new file mode 100644
index 0000000..1b4d584
--- /dev/null
+++ b/exifinterface/exifinterface/src/androidTest/res/raw/webp_without_exif_trailing_data.webp
Binary files differ
diff --git a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
index 857e673..17da6076 100644
--- a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
+++ b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
@@ -6749,8 +6749,8 @@
// WebP signature
copy(totalInputStream, totalOutputStream, WEBP_SIGNATURE_1.length);
- // File length will be written after all the chunks have been written
- totalInputStream.skipFully(WEBP_FILE_SIZE_BYTE_LENGTH + WEBP_SIGNATURE_2.length);
+ int riffLength = totalInputStream.readInt();
+ totalInputStream.skipFully(WEBP_SIGNATURE_2.length);
// Create a separate byte array to calculate file length
ByteArrayOutputStream nonHeaderByteArrayOutputStream = null;
@@ -6925,8 +6925,9 @@
}
}
- // Copy the rest of the file
- copy(totalInputStream, nonHeaderOutputStream);
+ // Copy the rest of the RIFF part of the file
+ int remainingRiffBytes = riffLength + 8 - totalInputStream.position();
+ copy(totalInputStream, nonHeaderOutputStream, remainingRiffBytes);
// Write file length + second signature
totalOutputStream.writeInt(nonHeaderByteArrayOutputStream.size()
@@ -6936,6 +6937,8 @@
mOffsetToExifData = totalOutputStream.mOutputStream.size() + exifOffset;
}
nonHeaderByteArrayOutputStream.writeTo(totalOutputStream);
+ // Copy any non-RIFF trailing data
+ copy(totalInputStream, totalOutputStream);
} catch (Exception e) {
throw new IOException("Failed to save WebP file", e);
} finally {
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ff3331b..510a96e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -61,6 +61,9 @@
kotlinToolingCore = "1.9.24"
ksp = "2.1.0-1.0.29"
ktfmt = "0.50"
+# Version format is: 1.KOTLIN_MAJOR_VERSION.0.KTFMT_VERSION
+# When updated, the id and checksum in StudioTask needs to be updated too
+ktfmtIdeaPlugin = "1.1.0.50"
leakcanary = "2.13"
media3 = "1.4.1"
metalava = "1.0.0-alpha12"
diff --git a/libraryversions.toml b/libraryversions.toml
index bee8c21..0b26c3d 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -13,11 +13,11 @@
BLUETOOTH = "1.0.0-alpha02"
BROWSER = "1.9.0-alpha01"
BUILDSRC_TESTS = "1.0.0-alpha01"
-CAMERA = "1.5.0-alpha04"
-CAMERA_MEDIA3 = "1.0.0-alpha01"
+CAMERA = "1.5.0-alpha05"
+CAMERA_MEDIA3 = "1.0.0-alpha02"
CAMERA_PIPE = "1.0.0-alpha01"
CAMERA_TESTING = "1.0.0-alpha01"
-CAMERA_VIEWFINDER = "1.4.0-alpha11"
+CAMERA_VIEWFINDER = "1.4.0-alpha12"
CARDVIEW = "1.1.0-alpha01"
CAR_APP = "1.7.0-beta03"
COLLECTION = "1.5.0-beta01"
@@ -161,18 +161,18 @@
VIEWPAGER = "1.1.0-rc01"
VIEWPAGER2 = "1.2.0-alpha01"
WEAR = "1.4.0-alpha01"
-WEAR_COMPOSE = "1.5.0-alpha07"
-WEAR_COMPOSE_MATERIAL3 = "1.0.0-alpha30"
+WEAR_COMPOSE = "1.5.0-alpha08"
+WEAR_COMPOSE_MATERIAL3 = "1.0.0-alpha31"
WEAR_CORE = "1.0.0-alpha01"
WEAR_INPUT = "1.2.0-alpha03"
WEAR_INPUT_TESTING = "1.2.0-alpha03"
WEAR_ONGOING = "1.1.0-alpha02"
WEAR_PHONE_INTERACTIONS = "1.1.0-alpha05"
-WEAR_PROTOLAYOUT = "1.3.0-alpha05"
+WEAR_PROTOLAYOUT = "1.3.0-alpha06"
WEAR_REMOTE_INTERACTIONS = "1.1.0-rc01"
-WEAR_TILES = "1.5.0-alpha05"
+WEAR_TILES = "1.5.0-alpha06"
WEAR_TOOLING_PREVIEW = "1.0.0-rc01"
-WEAR_WATCHFACE = "1.3.0-alpha04"
+WEAR_WATCHFACE = "1.3.0-alpha05"
WEBKIT = "1.13.0-alpha03"
# Adding a comment to prevent merge conflicts for Window artifact
WINDOW = "1.4.0-beta01"
diff --git a/room/benchmark/build.gradle b/room/benchmark/build.gradle
index 19362ae..d027d93 100644
--- a/room/benchmark/build.gradle
+++ b/room/benchmark/build.gradle
@@ -21,6 +21,8 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
+
+import androidx.build.KotlinTarget
import androidx.build.LibraryType
plugins {
@@ -54,4 +56,9 @@
androidx {
type = LibraryType.BENCHMARK
+ kotlinTarget = KotlinTarget.KOTLIN_2_0
+}
+
+ksp {
+ useKsp2 = true
}
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 1ba2f01..929ee4d 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
@@ -75,8 +75,7 @@
import kotlin.math.abs
import kotlinx.coroutines.delay
import org.junit.Rule
-
-// import org.junit.Test
+import org.junit.Test
private const val delayBetweenItems = 2500L
private const val animationTime = 900L
@@ -85,7 +84,7 @@
class CarouselTest {
@get:Rule val rule = createComposeRule()
- // @Test
+ @Test
fun carousel_autoScrolls() {
rule.setContent { SampleCarousel { BasicText(text = "Text ${it + 1}") } }
@@ -98,7 +97,7 @@
rule.onNodeWithText("Text 3").assertIsDisplayed()
}
- // @Test
+ @Test
fun carousel_onFocus_stopsScroll() {
rule.setContent { SampleCarousel { BasicText(text = "Text ${it + 1}") } }
@@ -113,7 +112,7 @@
rule.onNodeWithText("Text 1").onParent().assertIsFocused()
}
- // @Test
+ @Test
fun carousel_onUserTriggeredPause_stopsScroll() {
rule.setContent {
val carouselState = rememberCarouselState()
@@ -132,7 +131,7 @@
rule.onNodeWithText("Text 1").assertIsDisplayed()
}
- // @Test
+ @Test
fun carousel_onUserTriggeredPauseAndResume_resumeScroll() {
var pauseHandle: ScrollPauseHandle? = null
rule.setContent {
@@ -163,7 +162,7 @@
rule.onNodeWithText("Text 2").assertIsDisplayed()
}
- // @Test
+ @Test
fun carousel_onMultipleUserTriggeredPauseAndResume_resumesScroll() {
var pauseHandle1: ScrollPauseHandle? = null
var pauseHandle2: ScrollPauseHandle? = null
@@ -207,7 +206,7 @@
rule.onNodeWithText("Text 2").assertIsDisplayed()
}
- // @Test
+ @Test
fun carousel_onRepeatedResumesOnSamePauseHandle_ignoresSubsequentResumeCalls() {
var pauseHandle1: ScrollPauseHandle? = null
rule.setContent {
@@ -246,7 +245,7 @@
rule.onNodeWithText("Text 1").assertIsDisplayed()
}
- // @Test
+ @Test
fun carousel_outOfFocus_resumesScroll() {
rule.setContent {
Column {
@@ -265,14 +264,14 @@
rule.onNodeWithText("Text 2").assertIsDisplayed()
}
- // @Test
+ @Test
fun carousel_pagerIndicatorDisplayed() {
rule.setContent { SampleCarousel { SampleCarouselItem(index = it) } }
rule.onNodeWithTag("indicator").assertIsDisplayed()
}
- // @Test
+ @Test
fun carousel_withAnimatedContent_successfulTransition() {
rule.setContent {
SampleCarousel {
@@ -292,7 +291,7 @@
rule.onNodeWithText("PLAY").assertIsDisplayed()
}
- // @Test
+ @Test
fun carousel_withAnimatedContent_successfulFocusIn() {
rule.setContent { SampleCarousel { SampleCarouselItem(index = it) } }
@@ -307,7 +306,7 @@
rule.onNodeWithText("Play 0", useUnmergedTree = true).assertIsDisplayed().assertIsFocused()
}
- // @Test
+ @Test
fun carousel_parentContainerGainsFocus_onBackPress() {
rule.setContent {
Box(modifier = Modifier.testTag("box-container").fillMaxSize().focusable()) {
@@ -336,7 +335,7 @@
rule.onNodeWithTag("box-container").assertIsFocused()
}
- // @Test
+ @Test
fun carousel_withCarouselItem_parentContainerGainsFocusOnBackPress() {
rule.setContent {
Box(modifier = Modifier.testTag("box-container").fillMaxSize().focusable()) {
@@ -367,7 +366,7 @@
rule.onNodeWithTag("box-container").assertIsFocused()
}
- // @Test
+ @Test
fun carousel_scrollToRegainFocus_checkBringIntoView() {
val focusRequester = FocusRequester()
rule.setContent {
@@ -453,7 +452,7 @@
assertThat(checkNodeCompletelyVisible(rule, "featured-carousel")).isTrue()
}
- // @Test
+ @Test
fun carousel_zeroItemCount_shouldNotCrash() {
val testTag = "emptyCarousel"
rule.setContent { Carousel(itemCount = 0, modifier = Modifier.testTag(testTag)) {} }
@@ -461,7 +460,7 @@
rule.onNodeWithTag(testTag).assertExists()
}
- // @Test
+ @Test
fun carousel_oneItemCount_shouldNotCrash() {
val testTag = "emptyCarousel"
rule.setContent { Carousel(itemCount = 1, modifier = Modifier.testTag(testTag)) {} }
@@ -469,7 +468,7 @@
rule.onNodeWithTag(testTag).assertExists()
}
- // @Test
+ @Test
fun carousel_manualScrollingWithFocusableItemsOnTop_focusStaysWithinCarousel() {
rule.setContent {
Column {
@@ -521,7 +520,7 @@
rule.onNodeWithText("Button-1").assertIsFocused()
}
- // @Test
+ @Test
fun carousel_manualScrollingFastMultipleKeyPresses_focusStaysWithinCarousel() {
val carouselState = CarouselState()
val tabs = listOf("Tab 1", "Tab 2", "Tab 3")
@@ -585,7 +584,7 @@
rule.onNodeWithText("Play ${finalItem + 3}", useUnmergedTree = true).assertIsFocused()
}
- // @Test
+ @Test
fun carousel_manualScrollingDpadLongPress_moveOnlyOneSlide() {
rule.setContent {
SampleCarousel(itemCount = 6) { index -> SampleButton("Button ${index + 1}") }
@@ -627,7 +626,7 @@
rule.onNodeWithText("Button 1").assertIsFocused()
}
- // @Test
+ @Test
fun carousel_manualScrollingLtr_RightMovesToNextSlideLeftMovesToPrevSlide() {
rule.setContent { SampleCarousel { index -> SampleButton("Button ${index + 1}") } }
@@ -668,7 +667,7 @@
rule.onNodeWithText("Button 1").assertIsDisplayed()
}
- // @Test
+ @Test
fun carousel_manualScrollingRtl_LeftMovesToNextSlideRightMovesToPrevSlide() {
rule.setContent {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
@@ -712,7 +711,7 @@
rule.onNodeWithText("Button 1").assertIsDisplayed()
}
- // @Test
+ @Test
fun carousel_itemCountChangesDuringAnimation_shouldNotCrash() {
val itemDisplayDurationMs: Long = 100
var itemChanges = 0
@@ -745,7 +744,7 @@
rule.waitUntil(timeoutMillis = 5000) { itemChanges > minSuccessfulItemChanges }
}
- // @Test
+ @Test
fun carousel_slideWithTwoButtonsInARow_focusMovesWithinSlideAndChangesSlideOnlyOnFocusExit() {
rule.setContent {
// No AutoScrolling
@@ -766,7 +765,7 @@
rule.onNodeWithText("Left Button 2").assertIsFocused()
}
- // @Test
+ @Test
fun carousel_manualScrollingLtr_loopsAroundWhenNoAdjacentFocusableItemsArePresent() {
rule.setContent {
// No AutoScrolling
@@ -788,7 +787,7 @@
rule.onNodeWithText("Button-0").assertIsFocused()
}
- // @Test
+ @Test
fun carousel_manualScrollingLtr_focusMovesToAdjacentItemsOutsideCarousel() {
rule.setContent {
val focusRequester = remember { FocusRequester() }
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/BringIntoViewIfChildrenAreFocused.kt b/tv/tv-material/src/main/java/androidx/tv/material3/BringIntoViewIfChildrenAreFocused.kt
index 572fbfe..3dfbd42 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/BringIntoViewIfChildrenAreFocused.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/BringIntoViewIfChildrenAreFocused.kt
@@ -16,28 +16,42 @@
package androidx.tv.material3
-// @OptIn(ExperimentalFoundationApi::class)
-// internal fun Modifier.bringIntoViewIfChildrenAreFocused(): Modifier = composed(
-// inspectorInfo = debugInspectorInfo { name = "bringIntoViewIfChildrenAreFocused" },
-// factory = {
-// var myRect: Rect = Rect.Zero
-// this
-// .onSizeChanged {
-// myRect = Rect(Offset.Zero, Offset(it.width.toFloat(), it.height.toFloat()))
-// }
-// .bringIntoViewResponder(
-// remember {
-// object : BringIntoViewResponder {
-// // return the current rectangle and ignoring the child rectangle received.
-// @ExperimentalFoundationApi
-// override fun calculateRectForParent(localRect: Rect): Rect = myRect
-//
-// // The container is not expected to be scrollable. Hence the child is
-// // already in view with respect to the container.
-// @ExperimentalFoundationApi
-// override suspend fun bringChildIntoView(localRect: () -> Rect?) {}
-// }
-// }
-// )
-// }
-// )
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.relocation.bringIntoViewResponder
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.debugInspectorInfo
+
+// Suppressed the deprecation because BringIntoViewModifierNode is not
+// available in compose.ui 1.7.x
+@Suppress("DEPRECATION")
+@OptIn(ExperimentalFoundationApi::class)
+internal fun Modifier.bringIntoViewIfChildrenAreFocused(): Modifier =
+ composed(
+ inspectorInfo = debugInspectorInfo { name = "bringIntoViewIfChildrenAreFocused" },
+ factory = {
+ var myRect: Rect = Rect.Zero
+ this.onSizeChanged {
+ myRect = Rect(Offset.Zero, Offset(it.width.toFloat(), it.height.toFloat()))
+ }
+ .bringIntoViewResponder(
+ remember {
+ object : androidx.compose.foundation.relocation.BringIntoViewResponder {
+ // return the current rectangle and ignoring the child rectangle
+ // received.
+ @ExperimentalFoundationApi
+ override fun calculateRectForParent(localRect: Rect): Rect = myRect
+
+ // The container is not expected to be scrollable. Hence the child is
+ // already in view with respect to the container.
+ @ExperimentalFoundationApi
+ override suspend fun bringChildIntoView(localRect: () -> Rect?) {}
+ }
+ }
+ )
+ }
+ )
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 e5ff9a8..da786e0 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
@@ -48,6 +48,7 @@
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusDirection.Companion.Left
@@ -84,10 +85,6 @@
/**
* Composes a hero card rotator to highlight a piece of content.
*
- * Note: The animations and focus management features have been dropped temporarily due to some
- * technical challenges. If you need them, consider using the previous version of the library
- * (1.0.0-alpha10) or kindly wait until the next alpha version (1.1.0-alpha01).
- *
* Examples:
*
* @sample androidx.tv.material3.samples.SimpleCarousel
@@ -104,6 +101,7 @@
* @param carouselIndicator indicator showing the position of the current item among all items.
* @param content defines the items for a given index.
*/
+@OptIn(ExperimentalComposeUiApi::class)
@ExperimentalTvMaterial3Api
@Composable
fun Carousel(
@@ -145,14 +143,14 @@
modifier =
modifier
.carouselSemantics(itemCount = itemCount, state = carouselState)
- // .bringIntoViewIfChildrenAreFocused()
+ .bringIntoViewIfChildrenAreFocused()
.focusRequester(carouselOuterBoxFocusRequester)
.onFocusChanged {
focusState = it
// When the carousel gains focus for the first time
- // if (it.isFocused && isAutoScrollActive) {
- // focusManager.moveFocus(FocusDirection.Enter)
- // }
+ if (it.isFocused && isAutoScrollActive) {
+ focusManager.moveFocus(FocusDirection.Enter)
+ }
}
.handleKeyEvents(
carouselState = carouselState,
@@ -184,7 +182,7 @@
// Outer box is focused
if (!isAutoScrollActive && focusState?.isFocused == true) {
carouselOuterBoxFocusRequester.requestFocus()
- // focusManager.moveFocus(FocusDirection.Enter)
+ focusManager.moveFocus(FocusDirection.Enter)
}
}
}
@@ -212,9 +210,8 @@
return !accessibilityManager.isEnabled && !(carouselIsFocused || carouselHasFocus)
}
-// @OptIn(ExperimentalAnimationApi::class)
private suspend fun AnimatedVisibilityScope.onAnimationCompletion(action: suspend () -> Unit) {
- // snapshotFlow { transition.currentState == transition.targetState }.first { it }
+ snapshotFlow { transition.currentState == transition.targetState }.first { it }
action.invoke()
}
@@ -249,9 +246,7 @@
onAutoScrollChange(doAutoScroll)
}
-@OptIn(
- ExperimentalTvMaterial3Api::class,
-)
+@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalComposeUiApi::class)
private fun Modifier.handleKeyEvents(
carouselState: CarouselState,
outerBoxFocusRequester: FocusRequester,
@@ -306,7 +301,7 @@
// Ignore KeyUp action type
it.type == KeyUp -> KeyEventPropagation.ContinuePropagation
it.key == Key.Back -> {
- // focusManager.moveFocus(FocusDirection.Exit)
+ focusManager.moveFocus(FocusDirection.Exit)
KeyEventPropagation.ContinuePropagation
}
it.key == Key.DirectionLeft -> handledHorizontalFocusMove(Left)
@@ -316,14 +311,15 @@
}
.focusProperties {
// allow exit along horizontal axis only for first and last slide.
- // exit = {
- // when {
- // shouldFocusExitCarousel(it, carouselState, itemCount, isLtr) ->
- // FocusRequester.Default
- //
- // else -> FocusRequester.Cancel
- // }
- // }
+ // Suppressed the deprecation because onExit is not available in compose.ui 1.7.x
+ @Suppress("DEPRECATION")
+ exit = {
+ when {
+ shouldFocusExitCarousel(it, carouselState, itemCount, isLtr) ->
+ FocusRequester.Default
+ else -> FocusRequester.Cancel
+ }
+ }
}
@OptIn(ExperimentalTvMaterial3Api::class)
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index 9594347..20bb8e8 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -26,8 +26,8 @@
defaultConfig {
applicationId = "androidx.wear.compose.integration.demos"
minSdk = 25
- versionCode = 60
- versionName = "1.60"
+ versionCode = 61
+ versionName = "1.61"
}
buildTypes {
diff --git a/wear/protolayout/protolayout-material3/api/current.txt b/wear/protolayout/protolayout-material3/api/current.txt
index 3ffc8fd..07fd4fb 100644
--- a/wear/protolayout/protolayout-material3/api/current.txt
+++ b/wear/protolayout/protolayout-material3/api/current.txt
@@ -46,9 +46,9 @@
}
public final class ButtonKt {
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement button(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.types.LayoutColor? backgroundColor, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement iconButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> iconContent, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.ButtonColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.IconButtonStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement textButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> labelContent, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.ButtonColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.TextButtonStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement button(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? backgroundContent, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement iconButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> iconContent, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.ButtonColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? backgroundContent, optional androidx.wear.protolayout.material3.IconButtonStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement textButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> labelContent, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.ButtonColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? backgroundContent, optional androidx.wear.protolayout.material3.TextButtonStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
}
public final class CardColors {
@@ -78,12 +78,12 @@
}
public final class CardKt {
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement appCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? avatar, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? label, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? time, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.AppCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement card(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.types.LayoutColor? backgroundColor, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> content);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement appCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? avatar, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? label, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? time, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? backgroundContent, optional androidx.wear.protolayout.material3.AppCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement card(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? backgroundContent, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> content);
method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement graphicDataCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> graphic, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional androidx.wear.protolayout.material3.GraphicDataCardStyle style, optional int horizontalAlignment, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement iconDataCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? secondaryIcon, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.DataCardStyle style, optional int titleContentPlacement, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement textDataCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? secondaryText, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.DataCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement titleCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? time, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.TitleCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding, optional int horizontalAlignment);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement iconDataCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? secondaryIcon, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? backgroundContent, optional androidx.wear.protolayout.material3.DataCardStyle style, optional int titleContentPlacement, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement textDataCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? secondaryText, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? backgroundContent, optional androidx.wear.protolayout.material3.DataCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement titleCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? time, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? backgroundContent, optional androidx.wear.protolayout.material3.TitleCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding, optional int horizontalAlignment);
}
public final class ColorScheme {
@@ -209,8 +209,8 @@
}
public final class ImageKt {
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement avatarImage(androidx.wear.protolayout.material3.MaterialScope, String protoLayoutResourceId, optional androidx.wear.protolayout.DimensionBuilders.ImageDimension width, optional androidx.wear.protolayout.DimensionBuilders.ImageDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional int contentScaleMode);
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement backgroundImage(androidx.wear.protolayout.material3.MaterialScope, String protoLayoutResourceId, optional androidx.wear.protolayout.DimensionBuilders.ImageDimension width, optional androidx.wear.protolayout.DimensionBuilders.ImageDimension height, optional androidx.wear.protolayout.types.LayoutColor overlayColor, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension overlayWidth, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension overlayHeight, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional int contentScaleMode);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement avatarImage(androidx.wear.protolayout.material3.MaterialScope, String protoLayoutResourceId, optional androidx.wear.protolayout.DimensionBuilders.ImageDimension width, optional androidx.wear.protolayout.DimensionBuilders.ImageDimension height, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional int contentScaleMode);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement backgroundImage(androidx.wear.protolayout.material3.MaterialScope, String protoLayoutResourceId, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.DimensionBuilders.ImageDimension width, optional androidx.wear.protolayout.DimensionBuilders.ImageDimension height, optional androidx.wear.protolayout.types.LayoutColor overlayColor, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension overlayWidth, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension overlayHeight, optional int contentScaleMode);
}
@androidx.wear.protolayout.material3.MaterialScopeMarker public class MaterialScope {
diff --git a/wear/protolayout/protolayout-material3/api/restricted_current.txt b/wear/protolayout/protolayout-material3/api/restricted_current.txt
index 3ffc8fd..07fd4fb 100644
--- a/wear/protolayout/protolayout-material3/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-material3/api/restricted_current.txt
@@ -46,9 +46,9 @@
}
public final class ButtonKt {
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement button(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.types.LayoutColor? backgroundColor, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement iconButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> iconContent, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.ButtonColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.IconButtonStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement textButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> labelContent, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.ButtonColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.TextButtonStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement button(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? backgroundContent, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement iconButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> iconContent, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.ButtonColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? backgroundContent, optional androidx.wear.protolayout.material3.IconButtonStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement textButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> labelContent, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.ButtonColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? backgroundContent, optional androidx.wear.protolayout.material3.TextButtonStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
}
public final class CardColors {
@@ -78,12 +78,12 @@
}
public final class CardKt {
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement appCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? avatar, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? label, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? time, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.AppCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement card(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.types.LayoutColor? backgroundColor, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> content);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement appCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? avatar, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? label, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? time, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? backgroundContent, optional androidx.wear.protolayout.material3.AppCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement card(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? backgroundContent, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> content);
method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement graphicDataCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> graphic, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional androidx.wear.protolayout.material3.GraphicDataCardStyle style, optional int horizontalAlignment, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement iconDataCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? secondaryIcon, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.DataCardStyle style, optional int titleContentPlacement, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement textDataCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? secondaryText, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.DataCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement titleCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? time, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.TitleCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding, optional int horizontalAlignment);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement iconDataCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? secondaryIcon, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? backgroundContent, optional androidx.wear.protolayout.material3.DataCardStyle style, optional int titleContentPlacement, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement textDataCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? secondaryText, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? backgroundContent, optional androidx.wear.protolayout.material3.DataCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement titleCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? time, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? backgroundContent, optional androidx.wear.protolayout.material3.TitleCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding, optional int horizontalAlignment);
}
public final class ColorScheme {
@@ -209,8 +209,8 @@
}
public final class ImageKt {
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement avatarImage(androidx.wear.protolayout.material3.MaterialScope, String protoLayoutResourceId, optional androidx.wear.protolayout.DimensionBuilders.ImageDimension width, optional androidx.wear.protolayout.DimensionBuilders.ImageDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional int contentScaleMode);
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement backgroundImage(androidx.wear.protolayout.material3.MaterialScope, String protoLayoutResourceId, optional androidx.wear.protolayout.DimensionBuilders.ImageDimension width, optional androidx.wear.protolayout.DimensionBuilders.ImageDimension height, optional androidx.wear.protolayout.types.LayoutColor overlayColor, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension overlayWidth, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension overlayHeight, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional int contentScaleMode);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement avatarImage(androidx.wear.protolayout.material3.MaterialScope, String protoLayoutResourceId, optional androidx.wear.protolayout.DimensionBuilders.ImageDimension width, optional androidx.wear.protolayout.DimensionBuilders.ImageDimension height, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional int contentScaleMode);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement backgroundImage(androidx.wear.protolayout.material3.MaterialScope, String protoLayoutResourceId, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.DimensionBuilders.ImageDimension width, optional androidx.wear.protolayout.DimensionBuilders.ImageDimension height, optional androidx.wear.protolayout.types.LayoutColor overlayColor, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension overlayWidth, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension overlayHeight, optional int contentScaleMode);
}
@androidx.wear.protolayout.material3.MaterialScopeMarker public class MaterialScope {
diff --git a/wear/protolayout/protolayout-material3/samples/src/main/java/androidx/wear/protolayout/material3/samples/Material3ComponentsSample.kt b/wear/protolayout/protolayout-material3/samples/src/main/java/androidx/wear/protolayout/material3/samples/Material3ComponentsSample.kt
index 36283be..618b67c 100644
--- a/wear/protolayout/protolayout-material3/samples/src/main/java/androidx/wear/protolayout/material3/samples/Material3ComponentsSample.kt
+++ b/wear/protolayout/protolayout-material3/samples/src/main/java/androidx/wear/protolayout/material3/samples/Material3ComponentsSample.kt
@@ -52,6 +52,8 @@
import androidx.wear.protolayout.material3.textEdgeButton
import androidx.wear.protolayout.material3.titleCard
import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.backgroundColor
+import androidx.wear.protolayout.modifiers.clickable
import androidx.wear.protolayout.modifiers.contentDescription
import androidx.wear.protolayout.types.LayoutString
import androidx.wear.protolayout.types.asLayoutConstraint
@@ -116,7 +118,7 @@
}
@Sampled
-fun topLeveLayout(
+fun topLevelLayout(
context: Context,
deviceConfiguration: DeviceParameters,
clickable: Clickable
@@ -144,7 +146,7 @@
},
bottomSlot = {
iconEdgeButton(
- clickable,
+ onClick = clickable,
modifier = LayoutModifier.contentDescription("Description")
) {
icon("id")
@@ -164,10 +166,12 @@
mainSlot = {
card(
onClick = clickable,
- modifier = LayoutModifier.contentDescription("Card with image background"),
+ modifier =
+ LayoutModifier.contentDescription("Card with image background")
+ .clickable(id = "card"),
width = expand(),
height = expand(),
- background = { backgroundImage(protoLayoutResourceId = "id") }
+ backgroundContent = { backgroundImage(protoLayoutResourceId = "id") }
) {
text("Content of the Card!".layoutString)
}
@@ -314,10 +318,10 @@
button(
onClick = clickable,
modifier =
- LayoutModifier.contentDescription("Big button with image background"),
+ LayoutModifier.contentDescription("Big button with image background")
+ .backgroundColor(colorScheme.primary),
width = expand(),
height = expand(),
- backgroundColor = colorScheme.primary,
content = { text("Button!".layoutString) }
)
}
@@ -392,7 +396,7 @@
LayoutModifier.contentDescription("Big button with image background"),
width = expand(),
height = expand(),
- background = { backgroundImage(protoLayoutResourceId = "id") }
+ backgroundContent = { backgroundImage(protoLayoutResourceId = "id") }
)
}
)
diff --git a/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt b/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt
index b7c09ff..fd7ae36 100644
--- a/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt
+++ b/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt
@@ -18,12 +18,10 @@
import androidx.test.core.app.ApplicationProvider
import androidx.test.platform.app.InstrumentationRegistry
-import androidx.wear.protolayout.ActionBuilders
import androidx.wear.protolayout.DeviceParametersBuilders
import androidx.wear.protolayout.DimensionBuilders.expand
import androidx.wear.protolayout.LayoutElementBuilders
import androidx.wear.protolayout.LayoutElementBuilders.Box
-import androidx.wear.protolayout.ModifiersBuilders
import androidx.wear.protolayout.ModifiersBuilders.Background
import androidx.wear.protolayout.ModifiersBuilders.Corner
import androidx.wear.protolayout.ModifiersBuilders.Modifiers
@@ -36,6 +34,7 @@
import androidx.wear.protolayout.material3.MaterialGoldenTest.Companion.pxToDp
import androidx.wear.protolayout.material3.TitleContentPlacementInDataCard.Companion.Bottom
import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.clickable
import androidx.wear.protolayout.modifiers.contentDescription
import androidx.wear.protolayout.types.LayoutColor
import androidx.wear.protolayout.types.layoutString
@@ -69,11 +68,7 @@
.setFontScale(1f)
.setScreenShape(DeviceParametersBuilders.SCREEN_SHAPE_RECT)
.build()
- val clickable: ModifiersBuilders.Clickable =
- ModifiersBuilders.Clickable.Builder()
- .setOnClick(ActionBuilders.LaunchAction.Builder().build())
- .setId("action_id")
- .build()
+ val clickable = clickable(id = "action_id")
val testCases: HashMap<String, LayoutElementBuilders.LayoutElement> = HashMap()
testCases["primarylayout_edgebuttonfilled_buttongroup_iconoverride_golden$goldenSuffix"] =
@@ -217,7 +212,9 @@
modifier = LayoutModifier.contentDescription("Card"),
width = expand(),
height = expand(),
- background = { backgroundImage(protoLayoutResourceId = IMAGE_ID) }
+ backgroundContent = {
+ backgroundImage(protoLayoutResourceId = IMAGE_ID)
+ }
) {
text(
"Card with image background".layoutString,
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Button.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Button.kt
index ac3e2cf..8fd28cf 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Button.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Button.kt
@@ -23,15 +23,16 @@
import androidx.wear.protolayout.ModifiersBuilders.Clickable
import androidx.wear.protolayout.ModifiersBuilders.Corner
import androidx.wear.protolayout.ModifiersBuilders.Padding
-import androidx.wear.protolayout.material3.ButtonDefaults.DEFAULT_CONTENT_PADDING_DP
+import androidx.wear.protolayout.material3.ButtonDefaults.DEFAULT_CONTENT_PADDING
import androidx.wear.protolayout.material3.ButtonDefaults.IMAGE_BUTTON_DEFAULT_SIZE_DP
import androidx.wear.protolayout.material3.ButtonDefaults.METADATA_TAG_BUTTON
import androidx.wear.protolayout.material3.ButtonDefaults.filledButtonColors
import androidx.wear.protolayout.material3.IconButtonStyle.Companion.defaultIconButtonStyle
import androidx.wear.protolayout.material3.TextButtonStyle.Companion.defaultTextButtonStyle
import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.background
+import androidx.wear.protolayout.modifiers.clip
import androidx.wear.protolayout.modifiers.contentDescription
-import androidx.wear.protolayout.types.LayoutColor
/**
* Opinionated ProtoLayout Material3 icon button that offers a single slot to take content
@@ -55,7 +56,7 @@
* [ButtonDefaults.filledTonalButtonColors] and [ButtonDefaults.filledVariantButtonColors]. If
* using custom colors, it is important to choose a color pair from same role to ensure
* accessibility with sufficient color contrast.
- * @param background The background object to be used behind the content in the button. It is
+ * @param backgroundContent The background object to be used behind the content in the button. It is
* recommended to use the default styling that is automatically provided by only calling
* [backgroundImage] with the content. It can be combined with the specified
* [ButtonColors.container] behind it.
@@ -78,18 +79,16 @@
height: ContainerDimension = wrapWithMinTapTargetDimension(),
shape: Corner = shapes.full,
colors: ButtonColors = filledButtonColors(),
- background: (MaterialScope.() -> LayoutElement)? = null,
+ backgroundContent: (MaterialScope.() -> LayoutElement)? = null,
style: IconButtonStyle = defaultIconButtonStyle(),
- contentPadding: Padding = Padding.Builder().setAll(DEFAULT_CONTENT_PADDING_DP.toDp()).build()
+ contentPadding: Padding = DEFAULT_CONTENT_PADDING
): LayoutElement =
button(
onClick = onClick,
- modifier = modifier,
+ modifier = modifier.background(color = colors.container, corner = shape),
width = width,
height = height,
- shape = shape,
- backgroundColor = colors.container,
- background = background,
+ backgroundContent = backgroundContent,
contentPadding = contentPadding,
content = {
withStyle(
@@ -122,7 +121,7 @@
* [ButtonDefaults.filledTonalButtonColors] and [ButtonDefaults.filledVariantButtonColors]. If
* using custom colors, it is important to choose a color pair from same role to ensure
* accessibility with sufficient color contrast.
- * @param background The background object to be used behind the content in the button. It is
+ * @param backgroundContent The background object to be used behind the content in the button. It is
* recommended to use the default styling that is automatically provided by only calling
* [backgroundImage] with the content. It can be combined with the specified
* [ButtonColors.container] behind it.
@@ -146,18 +145,16 @@
height: ContainerDimension = wrapWithMinTapTargetDimension(),
shape: Corner = shapes.full,
colors: ButtonColors = filledButtonColors(),
- background: (MaterialScope.() -> LayoutElement)? = null,
+ backgroundContent: (MaterialScope.() -> LayoutElement)? = null,
style: TextButtonStyle = defaultTextButtonStyle(),
- contentPadding: Padding = Padding.Builder().setAll(DEFAULT_CONTENT_PADDING_DP.toDp()).build()
+ contentPadding: Padding = DEFAULT_CONTENT_PADDING
): LayoutElement =
button(
onClick = onClick,
- modifier = modifier,
+ modifier = modifier.background(color = colors.container, corner = shape),
width = width,
height = height,
- shape = shape,
- backgroundColor = colors.container,
- background = background,
+ backgroundContent = backgroundContent,
contentPadding = contentPadding,
content = {
withStyle(
@@ -184,15 +181,13 @@
* @param onClick Associated [Clickable] for click events. When the button is clicked it will fire
* the associated action.
* @param modifier Modifiers to set to this element. It's highly recommended to set a content
- * description using [contentDescription].
- * @param shape Defines the button's shape, in other words the corner radius for this button.
- * @param backgroundColor The color to be used as a background of this button. If the background
- * image is also specified, the image will be laid out on top of this color. In case of the fully
- * opaque background image, then this background color will not be shown.
- * @param background The background object to be used behind the content in the button. It is
+ * description using [contentDescription]. If [LayoutModifier.background] modifier is used and the
+ * the background image is also specified, the image will be laid out on top of this color. In
+ * case of the fully opaque background image, then the background color will not be shown.
+ * @param backgroundContent The background object to be used behind the content in the button. It is
* recommended to use the default styling that is automatically provided by only calling
- * [backgroundImage] with the content. It can be combined with the specified [backgroundColor]
- * behind it.
+ * [backgroundImage] with the content. It can be combined with the specified
+ * [LayoutModifier.background] behind it.
* @param width The width of this button. It's highly recommended to set this to [expand] or
* [weight]
* @param height The height of this button. It's highly recommended to set this to [expand] or
@@ -215,19 +210,15 @@
height: ContainerDimension =
if (content == null) IMAGE_BUTTON_DEFAULT_SIZE_DP.toDp()
else wrapWithMinTapTargetDimension(),
- shape: Corner = shapes.full,
- backgroundColor: LayoutColor? = null,
- background: (MaterialScope.() -> LayoutElement)? = null,
- contentPadding: Padding = Padding.Builder().setAll(DEFAULT_CONTENT_PADDING_DP.toDp()).build()
+ backgroundContent: (MaterialScope.() -> LayoutElement)? = null,
+ contentPadding: Padding = DEFAULT_CONTENT_PADDING
): LayoutElement =
componentContainer(
onClick = onClick,
- modifier = modifier,
+ modifier = LayoutModifier.clip(shapes.full) then modifier,
width = width,
height = height,
- shape = shape,
- backgroundColor = backgroundColor,
- background = background,
+ backgroundContent = backgroundContent,
contentPadding = contentPadding,
metadataTag = METADATA_TAG_BUTTON,
content = content
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/ButtonDefaults.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/ButtonDefaults.kt
index eff0392..f105a4b 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/ButtonDefaults.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/ButtonDefaults.kt
@@ -19,10 +19,10 @@
import android.graphics.Color
import androidx.annotation.Dimension
import androidx.annotation.Dimension.Companion.DP
-import androidx.wear.protolayout.ColorBuilders.argb
import androidx.wear.protolayout.ModifiersBuilders.Padding
-import androidx.wear.protolayout.material3.ButtonDefaults.DEFAULT_CONTENT_PADDING_DP
+import androidx.wear.protolayout.material3.ButtonDefaults.DEFAULT_CONTENT_PADDING
import androidx.wear.protolayout.material3.Typography.TypographyToken
+import androidx.wear.protolayout.modifiers.padding
import androidx.wear.protolayout.types.LayoutColor
import androidx.wear.protolayout.types.argb
@@ -81,7 +81,7 @@
)
internal const val METADATA_TAG_BUTTON: String = "BTN"
- @Dimension(DP) internal const val DEFAULT_CONTENT_PADDING_DP: Int = 8
+ internal val DEFAULT_CONTENT_PADDING = padding(8f)
@Dimension(DP) internal const val IMAGE_BUTTON_DEFAULT_SIZE_DP = 52
}
@@ -89,7 +89,7 @@
public class IconButtonStyle
internal constructor(
@Dimension(unit = DP) internal val iconSize: Int,
- internal val innerPadding: Padding = DEFAULT_CONTENT_PADDING_DP.toPadding()
+ internal val innerPadding: Padding = DEFAULT_CONTENT_PADDING
) {
public companion object {
/**
@@ -110,7 +110,7 @@
public class TextButtonStyle
internal constructor(
@TypographyToken internal val labelTypography: Int,
- internal val innerPadding: Padding = DEFAULT_CONTENT_PADDING_DP.toPadding()
+ internal val innerPadding: Padding = DEFAULT_CONTENT_PADDING
) {
public companion object {
/**
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Card.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Card.kt
index 318cc46..8a2d807 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Card.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Card.kt
@@ -41,8 +41,10 @@
import androidx.wear.protolayout.material3.TitleCardDefaults.buildContentForTitleCard
import androidx.wear.protolayout.material3.TitleCardStyle.Companion.defaultTitleCardStyle
import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.background
+import androidx.wear.protolayout.modifiers.clip
import androidx.wear.protolayout.modifiers.contentDescription
-import androidx.wear.protolayout.types.LayoutColor
+import androidx.wear.protolayout.modifiers.padding
/**
* Opinionated ProtoLayout Material3 title card that offers 1 to 3 slots, usually text based.
@@ -69,7 +71,7 @@
* [CardDefaults.filledTonalCardColors] for low/medium emphasis card,
* [CardDefaults.imageBackgroundCardColors] for card with image as a background or custom built
* [CardColors].
- * @param background The background object to be used behind the content in the card. It is
+ * @param backgroundContent The background object to be used behind the content in the card. It is
* recommended to use the default styling that is automatically provided by only calling
* [backgroundImage] with the content. It can be combined with the specified [colors]'s background
* color behind it.
@@ -95,7 +97,7 @@
shape: Corner =
if (deviceConfiguration.screenWidthDp.isBreakpoint()) shapes.extraLarge else shapes.large,
colors: CardColors = filledCardColors(),
- background: (MaterialScope.() -> LayoutElement)? = null,
+ backgroundContent: (MaterialScope.() -> LayoutElement)? = null,
style: TitleCardStyle = defaultTitleCardStyle(),
contentPadding: Padding = style.innerPadding,
@HorizontalAlignment
@@ -103,12 +105,10 @@
): LayoutElement =
card(
onClick = onClick,
- modifier = modifier,
+ modifier = modifier.background(colors.background).clip(shape),
width = expand(),
height = height,
- shape = shape,
- backgroundColor = colors.background,
- background = background,
+ backgroundContent = backgroundContent,
contentPadding = contentPadding
) {
buildContentForTitleCard(
@@ -193,7 +193,7 @@
* [CardDefaults.filledTonalCardColors] for low/medium emphasis card,
* [CardDefaults.imageBackgroundCardColors] for card with image as a background or custom built
* [CardColors].
- * @param background The background object to be used behind the content in the card. It is
+ * @param backgroundContent The background object to be used behind the content in the card. It is
* recommended to use the default styling that is automatically provided by only calling
* [backgroundImage] with the content. It can be combined with the specified [colors]'s background
* color behind it.
@@ -218,18 +218,16 @@
shape: Corner =
if (deviceConfiguration.screenWidthDp.isBreakpoint()) shapes.extraLarge else shapes.large,
colors: CardColors = filledCardColors(),
- background: (MaterialScope.() -> LayoutElement)? = null,
+ backgroundContent: (MaterialScope.() -> LayoutElement)? = null,
style: AppCardStyle = defaultAppCardStyle(),
contentPadding: Padding = style.innerPadding,
): LayoutElement =
card(
onClick = onClick,
- modifier = modifier,
+ modifier = modifier.background(colors.background).clip(shape),
width = expand(),
height = height,
- shape = shape,
- backgroundColor = colors.background,
- background = background,
+ backgroundContent = backgroundContent,
contentPadding = contentPadding
) {
buildContentForAppCard(
@@ -321,7 +319,7 @@
* [CardDefaults.filledTonalCardColors] for low/medium emphasis card,
* [CardDefaults.imageBackgroundCardColors] for card with image as a background or custom built
* [CardColors].
- * @param background The background object to be used behind the content in the card. It is
+ * @param backgroundContent The background object to be used behind the content in the card. It is
* recommended to use the default styling that is automatically provided by only calling
* [backgroundImage] with the content. It can be combined with the specified [colors]'s background
* color behind it.
@@ -349,19 +347,17 @@
height: ContainerDimension = wrapWithMinTapTargetDimension(),
shape: Corner = shapes.large,
colors: CardColors = filledCardColors(),
- background: (MaterialScope.() -> LayoutElement)? = null,
+ backgroundContent: (MaterialScope.() -> LayoutElement)? = null,
style: DataCardStyle =
if (secondaryText == null) defaultCompactDataCardStyle() else defaultDataCardStyle(),
contentPadding: Padding = style.innerPadding,
): LayoutElement =
card(
onClick = onClick,
- modifier = modifier,
+ modifier = modifier.background(colors.background).clip(shape),
width = width,
height = height,
- shape = shape,
- backgroundColor = colors.background,
- background = background,
+ backgroundContent = backgroundContent,
contentPadding = contentPadding
) {
buildContentForDataCard(
@@ -435,7 +431,7 @@
* [CardDefaults.filledTonalCardColors] for low/medium emphasis card,
* [CardDefaults.imageBackgroundCardColors] for card with image as a background or custom built
* [CardColors].
- * @param background The background object to be used behind the content in the card. It is
+ * @param backgroundContent The background object to be used behind the content in the card. It is
* recommended to use the default styling that is automatically provided by only calling
* [backgroundImage] with the content. It can be combined with the specified [colors]'s background
* color behind it.
@@ -465,7 +461,7 @@
height: ContainerDimension = wrapWithMinTapTargetDimension(),
shape: Corner = shapes.large,
colors: CardColors = filledCardColors(),
- background: (MaterialScope.() -> LayoutElement)? = null,
+ backgroundContent: (MaterialScope.() -> LayoutElement)? = null,
style: DataCardStyle =
if (secondaryIcon == null) defaultCompactDataCardStyle() else defaultDataCardStyle(),
titleContentPlacement: TitleContentPlacementInDataCard = TitleContentPlacementInDataCard.Bottom,
@@ -473,12 +469,10 @@
): LayoutElement =
card(
onClick = onClick,
- modifier = modifier,
+ modifier = modifier.background(colors.background).clip(shape),
width = width,
height = height,
- shape = shape,
- backgroundColor = colors.background,
- background = background,
+ backgroundContent = backgroundContent,
contentPadding = contentPadding
) {
buildContentForDataCard(
@@ -570,11 +564,9 @@
): LayoutElement =
card(
onClick = onClick,
- modifier = modifier,
+ modifier = modifier.background(colors.background).clip(shape),
width = expand(),
height = height,
- shape = shape,
- backgroundColor = colors.background,
contentPadding = contentPadding
) {
buildContentForGraphicDataCard(
@@ -628,15 +620,13 @@
* @param onClick Associated [Clickable] for click events. When the card is clicked it will fire the
* associated action.
* @param modifier Modifiers to set to this element. It's highly recommended to set a content
- * description using [contentDescription].
- * @param shape Defines the card's shape, in other words the corner radius for this card.
- * @param backgroundColor The color to be used as a background of this card. If the background image
- * is also specified, the image will be laid out on top of this color. In case of the fully opaque
- * background image, then this background color will not be shown.
- * @param background The background object to be used behind the content in the card. It is
+ * description using [contentDescription]. If [LayoutModifier.background] modifier is used and the
+ * the background image is also specified, the image will be laid out on top of this color. In
+ * case of the fully opaque background image, then the background color will not be shown.
+ * @param backgroundContent The background object to be used behind the content in the card. It is
* recommended to use the default styling that is automatically provided by only calling
- * [backgroundImage] with the content. It can be combined with the specified [backgroundColor]
- * behind it.
+ * [backgroundImage] with the content. It can be combined with the specified
+ * [LayoutModifier.background] behind it.
* @param width The width of this card. It's highly recommended to set this to [expand] or [weight]
* @param height The height of this card. It's highly recommended to set this to [expand] or
* [weight]
@@ -652,20 +642,16 @@
modifier: LayoutModifier = LayoutModifier,
width: ContainerDimension = wrapWithMinTapTargetDimension(),
height: ContainerDimension = wrapWithMinTapTargetDimension(),
- shape: Corner = shapes.large,
- backgroundColor: LayoutColor? = null,
- background: (MaterialScope.() -> LayoutElement)? = null,
- contentPadding: Padding = Padding.Builder().setAll(DEFAULT_CONTENT_PADDING.toDp()).build(),
+ backgroundContent: (MaterialScope.() -> LayoutElement)? = null,
+ contentPadding: Padding = padding(DEFAULT_CONTENT_PADDING),
content: (MaterialScope.() -> LayoutElement)
): LayoutElement =
componentContainer(
onClick = onClick,
- modifier = modifier,
+ modifier = LayoutModifier.clip(shapes.large) then modifier,
width = width,
height = height,
- shape = shape,
- backgroundColor = backgroundColor,
- background = background,
+ backgroundContent = backgroundContent,
contentPadding = contentPadding,
metadataTag = METADATA_TAG,
content = content
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/CardDefaults.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/CardDefaults.kt
index 77bdda2..ad91cc6 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/CardDefaults.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/CardDefaults.kt
@@ -94,5 +94,5 @@
)
internal const val METADATA_TAG: String = "CR"
- internal const val DEFAULT_CONTENT_PADDING: Int = 4
+ internal const val DEFAULT_CONTENT_PADDING = 4f
}
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/DataCardDefaults.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/DataCardDefaults.kt
index f6c083b..d6552a3 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/DataCardDefaults.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/DataCardDefaults.kt
@@ -24,6 +24,7 @@
import androidx.wear.protolayout.material3.TitleContentPlacementInDataCard.Companion.Bottom
import androidx.wear.protolayout.material3.TitleContentPlacementInDataCard.Companion.Top
import androidx.wear.protolayout.material3.Typography.TypographyToken
+import androidx.wear.protolayout.modifiers.padding
internal object DataCardDefaults {
/**
@@ -127,13 +128,13 @@
@Dimension(unit = DP) private const val ICON_SIZE_LARGE_DP: Int = 32
- @Dimension(unit = DP) private const val PADDING_SMALL_DP: Int = 8
+ @Dimension(unit = DP) private const val PADDING_SMALL_DP = 8f
- @Dimension(unit = DP) private const val PADDING_DEFAULT_DP: Int = 10
+ @Dimension(unit = DP) private const val PADDING_DEFAULT_DP = 10f
- @Dimension(unit = DP) private const val PADDING_LARGE_DP: Int = 14
+ @Dimension(unit = DP) private const val PADDING_LARGE_DP = 14f
- @Dimension(unit = DP) private const val PADDING_EXTRA_LARGE_DP: Int = 16
+ @Dimension(unit = DP) private const val PADDING_EXTRA_LARGE_DP = 16f
/**
* Default style variation for the [iconDataCard] or [textDataCard] where all opinionated
@@ -141,7 +142,7 @@
*/
public fun smallDataCardStyle(): DataCardStyle =
DataCardStyle(
- innerPadding = PADDING_SMALL_DP.toPadding(),
+ innerPadding = padding(PADDING_SMALL_DP),
titleToContentSpaceDp = SMALL_SPACE_DP,
titleTypography = Typography.LABEL_MEDIUM,
contentTypography = Typography.BODY_SMALL,
@@ -155,7 +156,7 @@
*/
public fun defaultDataCardStyle(): DataCardStyle =
DataCardStyle(
- innerPadding = PADDING_DEFAULT_DP.toPadding(),
+ innerPadding = padding(PADDING_DEFAULT_DP),
titleToContentSpaceDp = SMALL_SPACE_DP,
titleTypography = Typography.LABEL_LARGE,
contentTypography = Typography.BODY_SMALL,
@@ -169,7 +170,7 @@
*/
public fun largeDataCardStyle(): DataCardStyle =
DataCardStyle(
- innerPadding = PADDING_DEFAULT_DP.toPadding(),
+ innerPadding = padding(PADDING_DEFAULT_DP),
titleToContentSpaceDp = EMPTY_SPACE_DP,
titleTypography = Typography.DISPLAY_SMALL,
contentTypography = Typography.BODY_SMALL,
@@ -184,12 +185,7 @@
public fun extraLargeDataCardStyle(): DataCardStyle =
DataCardStyle(
innerPadding =
- Padding.Builder()
- .setStart(PADDING_DEFAULT_DP.toDp())
- .setEnd(PADDING_DEFAULT_DP.toDp())
- .setTop(PADDING_EXTRA_LARGE_DP.toDp())
- .setBottom(PADDING_EXTRA_LARGE_DP.toDp())
- .build(),
+ padding(horizontal = PADDING_DEFAULT_DP, vertical = PADDING_EXTRA_LARGE_DP),
titleToContentSpaceDp = EMPTY_SPACE_DP,
titleTypography = Typography.DISPLAY_MEDIUM,
contentTypography = Typography.BODY_SMALL,
@@ -204,13 +200,7 @@
*/
public fun smallCompactDataCardStyle(): DataCardStyle =
DataCardStyle(
- innerPadding =
- Padding.Builder()
- .setTop(PADDING_SMALL_DP.toDp())
- .setBottom(PADDING_SMALL_DP.toDp())
- .setStart(PADDING_LARGE_DP.toDp())
- .setEnd(PADDING_LARGE_DP.toDp())
- .build(),
+ innerPadding = padding(horizontal = PADDING_LARGE_DP, vertical = PADDING_SMALL_DP),
titleToContentSpaceDp = DEFAULT_SPACE_DP,
titleTypography = Typography.NUMERAL_MEDIUM,
contentTypography = Typography.LABEL_MEDIUM,
@@ -225,13 +215,7 @@
*/
public fun defaultCompactDataCardStyle(): DataCardStyle =
DataCardStyle(
- innerPadding =
- Padding.Builder()
- .setTop(PADDING_SMALL_DP.toDp())
- .setBottom(PADDING_SMALL_DP.toDp())
- .setStart(PADDING_LARGE_DP.toDp())
- .setEnd(PADDING_LARGE_DP.toDp())
- .build(),
+ innerPadding = padding(horizontal = PADDING_LARGE_DP, vertical = PADDING_SMALL_DP),
titleToContentSpaceDp = EMPTY_SPACE_DP,
titleTypography = Typography.NUMERAL_LARGE,
contentTypography = Typography.LABEL_LARGE,
@@ -246,13 +230,7 @@
*/
public fun largeCompactDataCardStyle(): DataCardStyle =
DataCardStyle(
- innerPadding =
- Padding.Builder()
- .setTop(PADDING_SMALL_DP.toDp())
- .setBottom(PADDING_SMALL_DP.toDp())
- .setStart(PADDING_LARGE_DP.toDp())
- .setEnd(PADDING_LARGE_DP.toDp())
- .build(),
+ innerPadding = padding(horizontal = PADDING_LARGE_DP, vertical = PADDING_SMALL_DP),
titleToContentSpaceDp = EMPTY_SPACE_DP,
titleTypography = Typography.NUMERAL_EXTRA_LARGE,
contentTypography = Typography.LABEL_LARGE,
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/EdgeButton.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/EdgeButton.kt
index 227f92c..cf093bc 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/EdgeButton.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/EdgeButton.kt
@@ -16,17 +16,16 @@
package androidx.wear.protolayout.material3
-import androidx.wear.protolayout.DimensionBuilders.DpProp
+import android.R.attr.clickable
+import androidx.annotation.Dimension
+import androidx.annotation.Dimension.Companion.DP
import androidx.wear.protolayout.DimensionBuilders.dp
import androidx.wear.protolayout.LayoutElementBuilders
import androidx.wear.protolayout.LayoutElementBuilders.Box
import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement
import androidx.wear.protolayout.LayoutElementBuilders.VERTICAL_ALIGN_CENTER
import androidx.wear.protolayout.LayoutElementBuilders.VerticalAlignment
-import androidx.wear.protolayout.ModifiersBuilders.Background
import androidx.wear.protolayout.ModifiersBuilders.Clickable
-import androidx.wear.protolayout.ModifiersBuilders.Corner
-import androidx.wear.protolayout.ModifiersBuilders.Modifiers
import androidx.wear.protolayout.ModifiersBuilders.Padding
import androidx.wear.protolayout.ModifiersBuilders.SEMANTICS_ROLE_BUTTON
import androidx.wear.protolayout.material3.ButtonDefaults.filledButtonColors
@@ -42,9 +41,16 @@
import androidx.wear.protolayout.material3.EdgeButtonStyle.Companion.DEFAULT
import androidx.wear.protolayout.material3.EdgeButtonStyle.Companion.TOP_ALIGN
import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.background
+import androidx.wear.protolayout.modifiers.clickable
+import androidx.wear.protolayout.modifiers.clip
+import androidx.wear.protolayout.modifiers.clipBottomLeft
+import androidx.wear.protolayout.modifiers.clipBottomRight
import androidx.wear.protolayout.modifiers.contentDescription
+import androidx.wear.protolayout.modifiers.padding
import androidx.wear.protolayout.modifiers.semanticsRole
-import androidx.wear.protolayout.modifiers.toProtoLayoutModifiersBuilder
+import androidx.wear.protolayout.modifiers.tag
+import androidx.wear.protolayout.modifiers.toProtoLayoutModifiers
/**
* ProtoLayout Material3 component edge button that offers a single slot to take an icon or similar
@@ -167,26 +173,18 @@
else HORIZONTAL_MARGIN_PERCENT_SMALL
val edgeButtonWidth: Float =
(100f - 2f * horizontalMarginPercent) * deviceConfiguration.screenWidthDp / 100f
- val bottomCornerRadiusX = dp(edgeButtonWidth / 2f)
- val bottomCornerRadiusY = dp(EDGE_BUTTON_HEIGHT_DP - TOP_CORNER_RADIUS.value)
+ val bottomCornerRadiusX = edgeButtonWidth / 2f
+ val bottomCornerRadiusY = EDGE_BUTTON_HEIGHT_DP - TOP_CORNER_RADIUS
- val defaultModifier = LayoutModifier.semanticsRole(SEMANTICS_ROLE_BUTTON) then modifier
- val modifiers =
- defaultModifier
- .toProtoLayoutModifiersBuilder()
- .setClickable(onClick)
- .setBackground(
- Background.Builder()
- .setColor(colors.container.prop)
- .setCorner(
- Corner.Builder()
- .setRadius(TOP_CORNER_RADIUS)
- .setBottomLeftRadius(bottomCornerRadiusX, bottomCornerRadiusY)
- .setBottomRightRadius(bottomCornerRadiusX, bottomCornerRadiusY)
- .build()
- )
- .build()
- )
+ var mod =
+ (LayoutModifier.semanticsRole(SEMANTICS_ROLE_BUTTON) then modifier)
+ .clickable(onClick)
+ .background(colors.container)
+ .clip(TOP_CORNER_RADIUS)
+ .clipBottomLeft(bottomCornerRadiusX, bottomCornerRadiusY)
+ .clipBottomRight(bottomCornerRadiusX, bottomCornerRadiusY)
+
+ style.padding?.let { mod = mod.padding(it) }
val button = Box.Builder().setHeight(EDGE_BUTTON_HEIGHT_DP.toDp()).setWidth(dp(edgeButtonWidth))
button
@@ -194,15 +192,13 @@
.setHorizontalAlignment(LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER)
.addContent(content())
- style.padding?.let { modifiers.setPadding(it) }
-
return Box.Builder()
.setHeight((EDGE_BUTTON_HEIGHT_DP + BOTTOM_MARGIN_DP).toDp())
.setWidth(containerWidth)
.setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_TOP)
.setHorizontalAlignment(LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER)
- .addContent(button.setModifiers(modifiers.build()).build())
- .setModifiers(Modifiers.Builder().setMetadata(METADATA_TAG.toElementMetadata()).build())
+ .addContent(button.setModifiers(mod.toProtoLayoutModifiers()).build())
+ .setModifiers(LayoutModifier.tag(METADATA_TAG).toProtoLayoutModifiers())
.build()
}
@@ -224,11 +220,11 @@
EdgeButtonStyle(
verticalAlignment = LayoutElementBuilders.VERTICAL_ALIGN_TOP,
padding =
- Padding.Builder()
- .setTop(TEXT_TOP_PADDING_DP.toDp())
- .setStart(TEXT_SIDE_PADDING_DP.toDp())
- .setEnd(TEXT_SIDE_PADDING_DP.toDp())
- .build()
+ padding(
+ start = TEXT_SIDE_PADDING_DP,
+ top = TEXT_TOP_PADDING_DP,
+ end = TEXT_SIDE_PADDING_DP
+ )
)
/**
@@ -242,7 +238,7 @@
}
internal object EdgeButtonDefaults {
- @JvmField internal val TOP_CORNER_RADIUS: DpProp = dp(17f)
+ @Dimension(DP) internal const val TOP_CORNER_RADIUS: Float = 17f
/** The horizontal margin used for width of the EdgeButton, below the 225dp breakpoint. */
internal const val HORIZONTAL_MARGIN_PERCENT_SMALL: Float = 24f
/** The horizontal margin used for width of the EdgeButton, above the 225dp breakpoint. */
@@ -251,8 +247,8 @@
internal const val EDGE_BUTTON_HEIGHT_DP: Int = 46
internal const val METADATA_TAG: String = "EB"
internal const val ICON_SIZE_DP = 24
- internal const val TEXT_TOP_PADDING_DP = 12
- internal const val TEXT_SIDE_PADDING_DP = 8
+ internal const val TEXT_TOP_PADDING_DP = 12f
+ internal const val TEXT_SIDE_PADDING_DP = 8f
}
internal fun LayoutElement.isSlotEdgeButton(): Boolean =
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/GraphicDataCardDefaults.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/GraphicDataCardDefaults.kt
index dc256d7..c931cb6 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/GraphicDataCardDefaults.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/GraphicDataCardDefaults.kt
@@ -31,6 +31,7 @@
import androidx.wear.protolayout.LayoutElementBuilders.Row
import androidx.wear.protolayout.ModifiersBuilders.Padding
import androidx.wear.protolayout.material3.Typography.TypographyToken
+import androidx.wear.protolayout.modifiers.padding
internal object GraphicDataCardDefaults {
@FloatRange(from = 0.0, to = 100.0) private const val GRAPHIC_SPACE_PERCENTAGE: Float = 40f
@@ -135,7 +136,7 @@
/** The default smaller spacer width or height that should be between different elements. */
@Dimension(unit = DP) private const val SMALL_SPACE_DP: Int = 2
- private const val DEFAULT_VERTICAL_PADDING_DP = 8
+ private const val DEFAULT_VERTICAL_PADDING_DP = 8f
/**
* Default style variation for the [graphicDataCard] where all opinionated inner content is
@@ -144,10 +145,10 @@
public fun defaultGraphicDataCardStyle(): GraphicDataCardStyle =
GraphicDataCardStyle(
innerPadding =
- Padding.Builder()
- .setTop(DEFAULT_VERTICAL_PADDING_DP.toDp())
- .setBottom(DEFAULT_VERTICAL_PADDING_DP.toDp())
- .build(),
+ padding(
+ top = DEFAULT_VERTICAL_PADDING_DP,
+ bottom = DEFAULT_VERTICAL_PADDING_DP
+ ),
titleToContentSpaceDp = SMALL_SPACE_DP,
titleTypography = Typography.DISPLAY_SMALL,
contentTypography = Typography.LABEL_SMALL,
@@ -162,10 +163,10 @@
public fun largeGraphicDataCardStyle(): GraphicDataCardStyle =
GraphicDataCardStyle(
innerPadding =
- Padding.Builder()
- .setTop(DEFAULT_VERTICAL_PADDING_DP.toDp())
- .setBottom(DEFAULT_VERTICAL_PADDING_DP.toDp())
- .build(),
+ padding(
+ top = DEFAULT_VERTICAL_PADDING_DP,
+ bottom = DEFAULT_VERTICAL_PADDING_DP
+ ),
titleToContentSpaceDp = 0,
titleTypography = Typography.DISPLAY_MEDIUM,
contentTypography = Typography.LABEL_MEDIUM,
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt
index 09b8979..ed51c87 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt
@@ -40,17 +40,17 @@
import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement
import androidx.wear.protolayout.LayoutElementBuilders.Spacer
import androidx.wear.protolayout.LayoutElementBuilders.TextAlignment
-import androidx.wear.protolayout.ModifiersBuilders.Background
import androidx.wear.protolayout.ModifiersBuilders.Clickable
-import androidx.wear.protolayout.ModifiersBuilders.Corner
import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata
-import androidx.wear.protolayout.ModifiersBuilders.Modifiers
import androidx.wear.protolayout.ModifiersBuilders.Padding
import androidx.wear.protolayout.ModifiersBuilders.SEMANTICS_ROLE_BUTTON
import androidx.wear.protolayout.materialcore.fontscaling.FontScaleConverterFactory
import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.clickable
+import androidx.wear.protolayout.modifiers.padding
import androidx.wear.protolayout.modifiers.semanticsRole
-import androidx.wear.protolayout.modifiers.toProtoLayoutModifiersBuilder
+import androidx.wear.protolayout.modifiers.tag
+import androidx.wear.protolayout.modifiers.toProtoLayoutModifiers
import androidx.wear.protolayout.types.LayoutColor
import androidx.wear.protolayout.types.argb
import java.nio.charset.StandardCharsets
@@ -91,8 +91,6 @@
internal fun Int.toDp() = dp(this.toFloat())
-internal fun String.toElementMetadata() = ElementMetadata.Builder().setTagData(toTagBytes()).build()
-
/** Builds a horizontal Spacer, with width set to expand and height set to the given value. */
internal fun horizontalSpacer(@Dimension(unit = DP) heightDp: Int): Spacer =
Spacer.Builder().setWidth(expand()).setHeight(heightDp.toDp()).build()
@@ -112,15 +110,6 @@
internal fun wrapWithMinTapTargetDimension(): WrappedDimensionProp =
WrappedDimensionProp.Builder().setMinimumSize(MINIMUM_TAP_TARGET_SIZE).build()
-/** Returns the [Modifiers] object containing this padding and nothing else. */
-internal fun Padding.toModifiers(): Modifiers = Modifiers.Builder().setPadding(this).build()
-
-/** Returns the [Background] object containing this color and nothing else. */
-internal fun LayoutColor.toBackground(): Background = Background.Builder().setColor(prop).build()
-
-/** Returns the [Background] object containing this corner and nothing else. */
-internal fun Corner.toBackground(): Background = Background.Builder().setCorner(this).build()
-
/**
* Changes the opacity/transparency of the given color.
*
@@ -156,8 +145,6 @@
*/
internal fun Int.isBreakpoint() = this >= SCREEN_SIZE_BREAKPOINT_DP
-internal fun Int.toPadding(): Padding = Padding.Builder().setAll(this.toDp()).build()
-
/**
* Builds [Box] that represents a clickable container with the given [content] inside, and
* [SEMANTICS_ROLE_BUTTON], that can be used to create container or more opinionated card or button
@@ -168,37 +155,29 @@
modifier: LayoutModifier,
width: ContainerDimension,
height: ContainerDimension,
- shape: Corner,
- backgroundColor: LayoutColor?,
- background: (MaterialScope.() -> LayoutElement)?,
+ backgroundContent: (MaterialScope.() -> LayoutElement)?,
contentPadding: Padding,
metadataTag: String,
content: (MaterialScope.() -> LayoutElement)?
): LayoutElement {
- val backgroundBuilder = Background.Builder().setCorner(shape)
- backgroundColor?.let { backgroundBuilder.setColor(it.prop) }
- val defaultModifier = LayoutModifier.semanticsRole(SEMANTICS_ROLE_BUTTON) then modifier
- val modifiers =
- defaultModifier
- .toProtoLayoutModifiersBuilder()
- .setClickable(onClick)
- .setMetadata(metadataTag.toElementMetadata())
- .setBackground(backgroundBuilder.build())
+ val mod =
+ LayoutModifier.semanticsRole(SEMANTICS_ROLE_BUTTON) then
+ modifier.clickable(onClick).tag(metadataTag)
val container =
Box.Builder().setHeight(height).setWidth(width).apply {
content?.let { addContent(content()) }
}
- if (background == null) {
- modifiers.setPadding(contentPadding)
- container.setModifiers(modifiers.build())
+ if (backgroundContent == null) {
+ container.setModifiers(mod.padding(contentPadding).toProtoLayoutModifiers())
return container.build()
}
+ val protoLayoutModifiers = mod.toProtoLayoutModifiers()
return Box.Builder()
- .setModifiers(modifiers.build())
+ .setModifiers(protoLayoutModifiers)
.addContent(
withStyle(
defaultBackgroundImageStyle =
@@ -208,18 +187,18 @@
overlayColor = colorScheme.primary.withOpacity(0.6f),
overlayWidth = width,
overlayHeight = height,
- shape = shape,
+ shape = protoLayoutModifiers.background?.corner ?: shapes.large,
contentScaleMode = LayoutElementBuilders.CONTENT_SCALE_MODE_FILL_BOUNDS
)
)
- .background()
+ .backgroundContent()
)
.setWidth(width)
.setHeight(height)
.addContent(
container
// Padding in this case is needed on the inner content, not the whole card.
- .setModifiers(contentPadding.toModifiers())
+ .setModifiers(LayoutModifier.padding(contentPadding).toProtoLayoutModifiers())
.build()
)
.build()
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Image.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Image.kt
index ab3a3aa..b14da86 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Image.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Image.kt
@@ -22,8 +22,10 @@
import androidx.wear.protolayout.LayoutElementBuilders.ContentScaleMode
import androidx.wear.protolayout.LayoutElementBuilders.Image
import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement
-import androidx.wear.protolayout.ModifiersBuilders.Corner
-import androidx.wear.protolayout.ModifiersBuilders.Modifiers
+import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.background
+import androidx.wear.protolayout.modifiers.clip
+import androidx.wear.protolayout.modifiers.toProtoLayoutModifiers
import androidx.wear.protolayout.types.LayoutColor
/**
@@ -34,6 +36,7 @@
*
* @param protoLayoutResourceId The protolayout resource id of the image. Node that, this is not an
* Android resource id.
+ * @param modifier Modifiers to set to this element.
* @param width The width of an image. Usually, this matches the width of the parent component this
* is used in.
* @param height The height of an image. Usually, this matches the height of the parent component
@@ -42,18 +45,17 @@
* It's recommended to use [ColorScheme.background] color with 60% opacity.
* @param overlayWidth The width of the overlay on top of the image background
* @param overlayHeight The height of the overlay on top of the image background
- * @param shape The shape of the corners for the image
* @param contentScaleMode The content scale mode for the image to define how image will adapt to
* the given size
*/
public fun MaterialScope.backgroundImage(
protoLayoutResourceId: String,
+ modifier: LayoutModifier = LayoutModifier,
width: ImageDimension = defaultBackgroundImageStyle.width,
height: ImageDimension = defaultBackgroundImageStyle.height,
overlayColor: LayoutColor = defaultBackgroundImageStyle.overlayColor,
overlayWidth: ContainerDimension = defaultBackgroundImageStyle.overlayWidth,
overlayHeight: ContainerDimension = defaultBackgroundImageStyle.overlayHeight,
- shape: Corner = defaultBackgroundImageStyle.shape,
@ContentScaleMode contentScaleMode: Int = defaultBackgroundImageStyle.contentScaleMode,
): LayoutElement =
Box.Builder()
@@ -64,7 +66,10 @@
Image.Builder()
.setWidth(width)
.setHeight(height)
- .setModifiers(Modifiers.Builder().setBackground(shape.toBackground()).build())
+ .setModifiers(
+ (LayoutModifier.clip(defaultBackgroundImageStyle.shape) then modifier)
+ .toProtoLayoutModifiers()
+ )
.setResourceId(protoLayoutResourceId)
.setContentScaleMode(contentScaleMode)
.build()
@@ -74,9 +79,7 @@
Box.Builder()
.setWidth(overlayWidth)
.setHeight(overlayHeight)
- .setModifiers(
- Modifiers.Builder().setBackground(overlayColor.toBackground()).build()
- )
+ .setModifiers(LayoutModifier.background(overlayColor).toProtoLayoutModifiers())
.build()
)
.build()
@@ -90,9 +93,9 @@
*
* @param protoLayoutResourceId The protolayout resource id of the image. Node that, this is not an
* Android resource id.
+ * @param modifier Modifiers to set to this element.
* @param width The width of an image. Usually, a small image that fit into the component's slot.
* @param height The height of an image. Usually, a small image that fit into the component's slot.
- * @param shape The shape of the corners for the image. Usually it's circular image.
* @param contentScaleMode The content scale mode for the image to define how image will adapt to
* the given size
*/
@@ -100,13 +103,16 @@
protoLayoutResourceId: String,
width: ImageDimension = defaultAvatarImageStyle.width,
height: ImageDimension = defaultAvatarImageStyle.height,
- shape: Corner = defaultAvatarImageStyle.shape,
+ modifier: LayoutModifier = LayoutModifier,
@ContentScaleMode contentScaleMode: Int = defaultAvatarImageStyle.contentScaleMode,
): LayoutElement =
Image.Builder()
.setWidth(width)
.setHeight(height)
- .setModifiers(Modifiers.Builder().setBackground(shape.toBackground()).build())
+ .setModifiers(
+ (LayoutModifier.clip(defaultAvatarImageStyle.shape) then modifier)
+ .toProtoLayoutModifiers()
+ )
.setResourceId(protoLayoutResourceId)
.setContentScaleMode(contentScaleMode)
.build()
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/PrimaryLayout.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/PrimaryLayout.kt
index eb361c5..0bd6491 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/PrimaryLayout.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/PrimaryLayout.kt
@@ -99,7 +99,7 @@
* it an edge button, the given label will be ignored.
* @param onClick The clickable action for whole layout. If any area (outside of other added
* tappable components) is clicked, it will fire the associated action.
- * @sample androidx.wear.protolayout.material3.samples.topLeveLayout
+ * @sample androidx.wear.protolayout.material3.samples.topLevelLayout
*/
// TODO: b/356568440 - Add sample above and put it in a proper samples file and link with @sample
// TODO: b/346958146 - Link visuals once they are available.
diff --git a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/ButtonTest.kt b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/ButtonTest.kt
index ba6ab99..e56e836 100644
--- a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/ButtonTest.kt
+++ b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/ButtonTest.kt
@@ -23,6 +23,8 @@
import androidx.wear.protolayout.DeviceParametersBuilders
import androidx.wear.protolayout.DimensionBuilders.expand
import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.backgroundColor
+import androidx.wear.protolayout.modifiers.clickable
import androidx.wear.protolayout.modifiers.contentDescription
import androidx.wear.protolayout.testing.LayoutElementAssertionsProvider
import androidx.wear.protolayout.testing.hasClickable
@@ -96,7 +98,7 @@
fun containerButton_hasClickable() {
LayoutElementAssertionsProvider(DEFAULT_CONTAINER_BUTTON_WITH_TEXT)
.onRoot()
- .assert(hasClickable(CLICKABLE))
+ .assert(hasClickable(id = CLICKABLE.id))
.assert(hasTag(ButtonDefaults.METADATA_TAG_BUTTON))
}
@@ -126,7 +128,7 @@
button(
onClick = CLICKABLE,
modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION),
- background = { backgroundImage(IMAGE_ID) }
+ backgroundContent = { backgroundImage(IMAGE_ID) }
)
}
@@ -143,8 +145,9 @@
materialScope(CONTEXT, DEVICE_CONFIGURATION) {
button(
onClick = CLICKABLE,
- modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION),
- backgroundColor = color.argb,
+ modifier =
+ LayoutModifier.contentDescription(CONTENT_DESCRIPTION)
+ .backgroundColor(color.argb),
content = { text(TEXT.layoutString) }
)
}
@@ -187,7 +190,7 @@
.setScreenHeightDp(192)
.build()
- private val CLICKABLE = clickable("id")
+ private val CLICKABLE = clickable(id = "id")
private const val CONTENT_DESCRIPTION = "This is a button"
diff --git a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/CardTest.kt b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/CardTest.kt
index 01c2a3c..d2d2a81 100644
--- a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/CardTest.kt
+++ b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/CardTest.kt
@@ -23,6 +23,8 @@
import androidx.wear.protolayout.DeviceParametersBuilders
import androidx.wear.protolayout.DimensionBuilders.expand
import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.background
+import androidx.wear.protolayout.modifiers.clickable
import androidx.wear.protolayout.modifiers.contentDescription
import androidx.wear.protolayout.testing.LayoutElementAssertionsProvider
import androidx.wear.protolayout.testing.hasClickable
@@ -99,7 +101,7 @@
fun containerCard_hasClickable() {
LayoutElementAssertionsProvider(DEFAULT_CONTAINER_CARD_WITH_TEXT)
.onRoot()
- .assert(hasClickable(CLICKABLE))
+ .assert(hasClickable(id = CLICKABLE.id))
.assert(hasTag(CardDefaults.METADATA_TAG))
}
@@ -222,7 +224,7 @@
card(
onClick = CLICKABLE,
modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION),
- background = { backgroundImage(IMAGE_ID) }
+ backgroundContent = { backgroundImage(IMAGE_ID) }
) {
text(TEXT.layoutString)
}
@@ -239,8 +241,10 @@
materialScope(CONTEXT, DEVICE_CONFIGURATION) {
card(
onClick = CLICKABLE,
- modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION),
- backgroundColor = color.argb
+ modifier =
+ LayoutModifier.contentDescription(CONTENT_DESCRIPTION)
+ .background(color.argb)
+ .clickable(id = "id")
) {
text(TEXT.layoutString)
}
@@ -458,7 +462,7 @@
.setScreenHeightDp(192)
.build()
- private val CLICKABLE = clickable("id")
+ private val CLICKABLE = clickable(id = "id")
private const val CONTENT_DESCRIPTION = "This is a card"
diff --git a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/EdgeButtonTest.kt b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/EdgeButtonTest.kt
index bea992cf..ec85a48 100644
--- a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/EdgeButtonTest.kt
+++ b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/EdgeButtonTest.kt
@@ -16,18 +16,19 @@
package androidx.wear.protolayout.material3
+import android.content.ComponentName
import android.content.Context
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.wear.protolayout.ActionBuilders.LaunchAction
+import androidx.wear.protolayout.ActionBuilders.launchAction
import androidx.wear.protolayout.DeviceParametersBuilders
import androidx.wear.protolayout.LayoutElementBuilders.Image
-import androidx.wear.protolayout.ModifiersBuilders.Clickable
import androidx.wear.protolayout.expression.AppDataKey
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32
import androidx.wear.protolayout.material3.EdgeButtonDefaults.BOTTOM_MARGIN_DP
import androidx.wear.protolayout.material3.EdgeButtonDefaults.EDGE_BUTTON_HEIGHT_DP
import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.clickable
import androidx.wear.protolayout.modifiers.contentDescription
import androidx.wear.protolayout.testing.LayoutElementAssertionsProvider
import androidx.wear.protolayout.testing.LayoutElementMatcher
@@ -173,10 +174,7 @@
.build()
private val CLICKABLE =
- Clickable.Builder()
- .setOnClick(LaunchAction.Builder().build())
- .setId("action_id")
- .build()
+ clickable(action = launchAction(ComponentName("pkg", "cls")), id = "action_id")
private const val CONTENT_DESCRIPTION = "it is an edge button"
diff --git a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/Utils.kt b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/Utils.kt
index b671a70..7d765cc 100644
--- a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/Utils.kt
+++ b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/Utils.kt
@@ -19,8 +19,6 @@
import android.content.Context
import android.provider.Settings
import androidx.test.core.app.ApplicationProvider
-import androidx.wear.protolayout.ActionBuilders.LaunchAction
-import androidx.wear.protolayout.ModifiersBuilders.Clickable
// TODO: b/373336064 - Move this to protolayout-material3-testing
internal fun enableDynamicTheme() {
@@ -30,6 +28,3 @@
/* dynamic theming is enabled */ 1
)
}
-
-internal fun clickable(id: String) =
- Clickable.Builder().setOnClick(LaunchAction.Builder().build()).setId(id).build()
diff --git a/wear/protolayout/protolayout-testing/api/current.txt b/wear/protolayout/protolayout-testing/api/current.txt
index 1503d9d1..544da65 100644
--- a/wear/protolayout/protolayout-testing/api/current.txt
+++ b/wear/protolayout/protolayout-testing/api/current.txt
@@ -1,10 +1,14 @@
// Signature format: 4.0
package androidx.wear.protolayout.testing {
- public final class FiltersKt {
+ public final class Filters {
method public static androidx.wear.protolayout.testing.LayoutElementMatcher containsTag(String value);
method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasChild(androidx.wear.protolayout.testing.LayoutElementMatcher matcher);
- method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasClickable(androidx.wear.protolayout.ModifiersBuilders.Clickable clickable);
+ method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasClickable();
+ method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasClickable(optional androidx.wear.protolayout.ActionBuilders.Action action);
+ method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasClickable(optional androidx.wear.protolayout.ActionBuilders.Action action, optional String? id);
+ method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasClickable(optional androidx.wear.protolayout.ActionBuilders.Action action, optional String? id, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float minClickableWidth);
+ method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasClickable(optional androidx.wear.protolayout.ActionBuilders.Action action, optional String? id, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float minClickableWidth, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float minClickableHeight);
method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasColor(@ColorInt int argb);
method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasContentDescription(String value);
method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasContentDescription(kotlin.text.Regex pattern);
diff --git a/wear/protolayout/protolayout-testing/api/restricted_current.txt b/wear/protolayout/protolayout-testing/api/restricted_current.txt
index 1503d9d1..544da65 100644
--- a/wear/protolayout/protolayout-testing/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-testing/api/restricted_current.txt
@@ -1,10 +1,14 @@
// Signature format: 4.0
package androidx.wear.protolayout.testing {
- public final class FiltersKt {
+ public final class Filters {
method public static androidx.wear.protolayout.testing.LayoutElementMatcher containsTag(String value);
method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasChild(androidx.wear.protolayout.testing.LayoutElementMatcher matcher);
- method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasClickable(androidx.wear.protolayout.ModifiersBuilders.Clickable clickable);
+ method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasClickable();
+ method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasClickable(optional androidx.wear.protolayout.ActionBuilders.Action action);
+ method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasClickable(optional androidx.wear.protolayout.ActionBuilders.Action action, optional String? id);
+ method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasClickable(optional androidx.wear.protolayout.ActionBuilders.Action action, optional String? id, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float minClickableWidth);
+ method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasClickable(optional androidx.wear.protolayout.ActionBuilders.Action action, optional String? id, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float minClickableWidth, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float minClickableHeight);
method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasColor(@ColorInt int argb);
method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasContentDescription(String value);
method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasContentDescription(kotlin.text.Regex pattern);
diff --git a/wear/protolayout/protolayout-testing/src/main/java/androidx/wear/protolayout/testing/filters.kt b/wear/protolayout/protolayout-testing/src/main/java/androidx/wear/protolayout/testing/filters.kt
index 8a8a141..13ded25 100644
--- a/wear/protolayout/protolayout-testing/src/main/java/androidx/wear/protolayout/testing/filters.kt
+++ b/wear/protolayout/protolayout-testing/src/main/java/androidx/wear/protolayout/testing/filters.kt
@@ -13,11 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+@file:JvmName("Filters")
package androidx.wear.protolayout.testing
import androidx.annotation.ColorInt
+import androidx.annotation.Dimension
+import androidx.annotation.Dimension.Companion.DP
import androidx.annotation.RestrictTo
+import androidx.wear.protolayout.ActionBuilders.Action
import androidx.wear.protolayout.DimensionBuilders.ContainerDimension
import androidx.wear.protolayout.DimensionBuilders.DpProp
import androidx.wear.protolayout.DimensionBuilders.ExpandedDimensionProp
@@ -33,6 +37,7 @@
import androidx.wear.protolayout.LayoutElementBuilders.Spannable
import androidx.wear.protolayout.LayoutElementBuilders.Text
import androidx.wear.protolayout.ModifiersBuilders.Clickable
+import androidx.wear.protolayout.modifiers.loadAction
import androidx.wear.protolayout.proto.DimensionProto
import androidx.wear.protolayout.types.LayoutString
@@ -44,9 +49,22 @@
* Returns a [LayoutElementMatcher] which checks whether the element has the specific [Clickable]
* attached.
*/
-public fun hasClickable(clickable: Clickable): LayoutElementMatcher =
- LayoutElementMatcher("has $clickable") {
- it.modifiers?.clickable?.toProto() == clickable.toProto()
+@JvmOverloads
+public fun hasClickable(
+ action: Action = loadAction(),
+ id: String? = null,
+ @Dimension(DP) minClickableWidth: Float = Float.NaN,
+ @Dimension(DP) minClickableHeight: Float = Float.NaN
+): LayoutElementMatcher =
+ LayoutElementMatcher("has clickable($action, $id, $minClickableWidth, $minClickableHeight)") {
+ val clk = it.modifiers?.clickable ?: return@LayoutElementMatcher false
+ if (!minClickableWidth.isNaN() && clk.minimumClickableWidth.value != minClickableWidth) {
+ return@LayoutElementMatcher false
+ }
+ if (!minClickableHeight.isNaN() && clk.minimumClickableHeight.value != minClickableHeight) {
+ return@LayoutElementMatcher false
+ }
+ clk.onClick?.toActionProto() == action.toActionProto() && id?.let { clk.id == it } != false
}
/**
diff --git a/wear/protolayout/protolayout-testing/src/test/java/androidx/wear/protolayout/testing/FiltersTest.kt b/wear/protolayout/protolayout-testing/src/test/java/androidx/wear/protolayout/testing/FiltersTest.kt
index ba732a8..84b7810 100644
--- a/wear/protolayout/protolayout-testing/src/test/java/androidx/wear/protolayout/testing/FiltersTest.kt
+++ b/wear/protolayout/protolayout-testing/src/test/java/androidx/wear/protolayout/testing/FiltersTest.kt
@@ -18,7 +18,6 @@
import android.graphics.Color
import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.wear.protolayout.ActionBuilders.LoadAction
import androidx.wear.protolayout.ColorBuilders.ColorProp
import androidx.wear.protolayout.DimensionBuilders.ProportionalDimensionProp
import androidx.wear.protolayout.DimensionBuilders.dp
@@ -36,10 +35,10 @@
import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata
import androidx.wear.protolayout.ModifiersBuilders.Modifiers
import androidx.wear.protolayout.ModifiersBuilders.Semantics
-import androidx.wear.protolayout.StateBuilders
import androidx.wear.protolayout.TypeBuilders.StringProp
import androidx.wear.protolayout.expression.DynamicBuilders
import androidx.wear.protolayout.layout.basicText
+import androidx.wear.protolayout.modifiers.loadAction
import androidx.wear.protolayout.types.LayoutString
import androidx.wear.protolayout.types.asLayoutConstraint
import androidx.wear.protolayout.types.layoutString
@@ -70,32 +69,25 @@
@Test
fun hasClickable_matches() {
- val clickable = Clickable.Builder().setOnClick(LoadAction.Builder().build()).build()
+ val clickable = Clickable.Builder().setOnClick(loadAction()).build()
val testElement =
Column.Builder()
.setModifiers(Modifiers.Builder().setClickable(clickable).build())
.build()
- assertThat(hasClickable(clickable).matches(testElement)).isTrue()
+ assertThat(hasClickable().matches(testElement)).isTrue()
}
@Test
fun hasClickable_doesNotMatch() {
- val clickable = Clickable.Builder().setOnClick(LoadAction.Builder().build()).build()
- val otherClickable =
- Clickable.Builder()
- .setOnClick(
- LoadAction.Builder()
- .setRequestState(StateBuilders.State.Builder().build())
- .build()
- )
- .build()
+ val clickable = Clickable.Builder().setOnClick(loadAction()).build()
+ val action = loadAction {}
val testElement =
Column.Builder()
.setModifiers(Modifiers.Builder().setClickable(clickable).build())
.build()
- assertThat(hasClickable(otherClickable).matches(testElement)).isFalse()
+ assertThat(hasClickable(action = action).matches(testElement)).isFalse()
}
@Test
diff --git a/wear/protolayout/protolayout/api/current.txt b/wear/protolayout/protolayout/api/current.txt
index 73620fa..b58a040 100644
--- a/wear/protolayout/protolayout/api/current.txt
+++ b/wear/protolayout/protolayout/api/current.txt
@@ -1492,6 +1492,30 @@
package androidx.wear.protolayout.modifiers {
+ public final class BackgroundKt {
+ method public static androidx.wear.protolayout.modifiers.LayoutModifier background(androidx.wear.protolayout.modifiers.LayoutModifier, androidx.wear.protolayout.types.LayoutColor color, optional androidx.wear.protolayout.ModifiersBuilders.Corner? corner);
+ method public static androidx.wear.protolayout.modifiers.LayoutModifier backgroundColor(androidx.wear.protolayout.modifiers.LayoutModifier, androidx.wear.protolayout.types.LayoutColor color);
+ method public static androidx.wear.protolayout.modifiers.LayoutModifier clip(androidx.wear.protolayout.modifiers.LayoutModifier, androidx.wear.protolayout.ModifiersBuilders.Corner corner);
+ method public static androidx.wear.protolayout.modifiers.LayoutModifier clip(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float cornerRadius);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static androidx.wear.protolayout.modifiers.LayoutModifier clip(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float x, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float y);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static androidx.wear.protolayout.modifiers.LayoutModifier clipBottomLeft(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float x, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float y);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static androidx.wear.protolayout.modifiers.LayoutModifier clipBottomRight(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float x, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float y);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static androidx.wear.protolayout.modifiers.LayoutModifier clipTopLeft(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float x, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float y);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static androidx.wear.protolayout.modifiers.LayoutModifier clipTopRight(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float x, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float y);
+ }
+
+ public final class ClickableKt {
+ method public static androidx.wear.protolayout.ModifiersBuilders.Clickable clickable();
+ method public static androidx.wear.protolayout.ModifiersBuilders.Clickable clickable(optional androidx.wear.protolayout.ActionBuilders.Action action);
+ method public static androidx.wear.protolayout.ModifiersBuilders.Clickable clickable(optional androidx.wear.protolayout.ActionBuilders.Action action, optional String? id);
+ method public static androidx.wear.protolayout.ModifiersBuilders.Clickable clickable(optional androidx.wear.protolayout.ActionBuilders.Action action, optional String? id, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) float minClickableWidth);
+ method public static androidx.wear.protolayout.ModifiersBuilders.Clickable clickable(optional androidx.wear.protolayout.ActionBuilders.Action action, optional String? id, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) float minClickableWidth, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) float minClickableHeight);
+ method public static androidx.wear.protolayout.modifiers.LayoutModifier clickable(androidx.wear.protolayout.modifiers.LayoutModifier, optional androidx.wear.protolayout.ActionBuilders.Action action, optional String? id);
+ method public static androidx.wear.protolayout.modifiers.LayoutModifier clickable(androidx.wear.protolayout.modifiers.LayoutModifier, androidx.wear.protolayout.ModifiersBuilders.Clickable clickable);
+ method public static androidx.wear.protolayout.ActionBuilders.LoadAction loadAction(optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.StateBuilders.State.Builder,kotlin.Unit>? requestedState);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) public static androidx.wear.protolayout.modifiers.LayoutModifier minimumTouchTargetSize(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float minWidth, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float minHeight);
+ }
+
public interface LayoutModifier {
method public <R> R foldIn(R initial, kotlin.jvm.functions.Function2<? super R,? super androidx.wear.protolayout.modifiers.LayoutModifier.Element,? extends R> operation);
method public default infix androidx.wear.protolayout.modifiers.LayoutModifier then(androidx.wear.protolayout.modifiers.LayoutModifier other);
@@ -1510,6 +1534,16 @@
method public static androidx.wear.protolayout.ModifiersBuilders.Modifiers toProtoLayoutModifiers(androidx.wear.protolayout.modifiers.LayoutModifier);
}
+ public final class PaddingKt {
+ method public static androidx.wear.protolayout.modifiers.LayoutModifier padding(androidx.wear.protolayout.modifiers.LayoutModifier, androidx.wear.protolayout.ModifiersBuilders.Padding padding);
+ method public static androidx.wear.protolayout.modifiers.LayoutModifier padding(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float all);
+ method public static androidx.wear.protolayout.modifiers.LayoutModifier padding(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float horizontal, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float vertical);
+ method public static androidx.wear.protolayout.modifiers.LayoutModifier padding(androidx.wear.protolayout.modifiers.LayoutModifier, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float start, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float top, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float end, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float bottom, optional boolean rtlAware);
+ method public static androidx.wear.protolayout.ModifiersBuilders.Padding padding(@Dimension(unit=androidx.annotation.Dimension.Companion.DP) float all);
+ method public static androidx.wear.protolayout.ModifiersBuilders.Padding padding(@Dimension(unit=androidx.annotation.Dimension.Companion.DP) float horizontal, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float vertical);
+ method public static androidx.wear.protolayout.ModifiersBuilders.Padding padding(optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float start, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float top, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float end, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float bottom, optional boolean rtlAware);
+ }
+
public final class SemanticsKt {
method public static androidx.wear.protolayout.modifiers.LayoutModifier contentDescription(androidx.wear.protolayout.modifiers.LayoutModifier, String staticValue, optional @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) androidx.wear.protolayout.expression.DynamicBuilders.DynamicString? dynamicValue);
method public static androidx.wear.protolayout.modifiers.LayoutModifier semanticsRole(androidx.wear.protolayout.modifiers.LayoutModifier, int semanticsRole);
diff --git a/wear/protolayout/protolayout/api/restricted_current.txt b/wear/protolayout/protolayout/api/restricted_current.txt
index 73620fa..b58a040 100644
--- a/wear/protolayout/protolayout/api/restricted_current.txt
+++ b/wear/protolayout/protolayout/api/restricted_current.txt
@@ -1492,6 +1492,30 @@
package androidx.wear.protolayout.modifiers {
+ public final class BackgroundKt {
+ method public static androidx.wear.protolayout.modifiers.LayoutModifier background(androidx.wear.protolayout.modifiers.LayoutModifier, androidx.wear.protolayout.types.LayoutColor color, optional androidx.wear.protolayout.ModifiersBuilders.Corner? corner);
+ method public static androidx.wear.protolayout.modifiers.LayoutModifier backgroundColor(androidx.wear.protolayout.modifiers.LayoutModifier, androidx.wear.protolayout.types.LayoutColor color);
+ method public static androidx.wear.protolayout.modifiers.LayoutModifier clip(androidx.wear.protolayout.modifiers.LayoutModifier, androidx.wear.protolayout.ModifiersBuilders.Corner corner);
+ method public static androidx.wear.protolayout.modifiers.LayoutModifier clip(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float cornerRadius);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static androidx.wear.protolayout.modifiers.LayoutModifier clip(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float x, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float y);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static androidx.wear.protolayout.modifiers.LayoutModifier clipBottomLeft(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float x, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float y);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static androidx.wear.protolayout.modifiers.LayoutModifier clipBottomRight(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float x, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float y);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static androidx.wear.protolayout.modifiers.LayoutModifier clipTopLeft(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float x, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float y);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static androidx.wear.protolayout.modifiers.LayoutModifier clipTopRight(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float x, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float y);
+ }
+
+ public final class ClickableKt {
+ method public static androidx.wear.protolayout.ModifiersBuilders.Clickable clickable();
+ method public static androidx.wear.protolayout.ModifiersBuilders.Clickable clickable(optional androidx.wear.protolayout.ActionBuilders.Action action);
+ method public static androidx.wear.protolayout.ModifiersBuilders.Clickable clickable(optional androidx.wear.protolayout.ActionBuilders.Action action, optional String? id);
+ method public static androidx.wear.protolayout.ModifiersBuilders.Clickable clickable(optional androidx.wear.protolayout.ActionBuilders.Action action, optional String? id, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) float minClickableWidth);
+ method public static androidx.wear.protolayout.ModifiersBuilders.Clickable clickable(optional androidx.wear.protolayout.ActionBuilders.Action action, optional String? id, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) float minClickableWidth, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) float minClickableHeight);
+ method public static androidx.wear.protolayout.modifiers.LayoutModifier clickable(androidx.wear.protolayout.modifiers.LayoutModifier, optional androidx.wear.protolayout.ActionBuilders.Action action, optional String? id);
+ method public static androidx.wear.protolayout.modifiers.LayoutModifier clickable(androidx.wear.protolayout.modifiers.LayoutModifier, androidx.wear.protolayout.ModifiersBuilders.Clickable clickable);
+ method public static androidx.wear.protolayout.ActionBuilders.LoadAction loadAction(optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.StateBuilders.State.Builder,kotlin.Unit>? requestedState);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) public static androidx.wear.protolayout.modifiers.LayoutModifier minimumTouchTargetSize(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float minWidth, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float minHeight);
+ }
+
public interface LayoutModifier {
method public <R> R foldIn(R initial, kotlin.jvm.functions.Function2<? super R,? super androidx.wear.protolayout.modifiers.LayoutModifier.Element,? extends R> operation);
method public default infix androidx.wear.protolayout.modifiers.LayoutModifier then(androidx.wear.protolayout.modifiers.LayoutModifier other);
@@ -1510,6 +1534,16 @@
method public static androidx.wear.protolayout.ModifiersBuilders.Modifiers toProtoLayoutModifiers(androidx.wear.protolayout.modifiers.LayoutModifier);
}
+ public final class PaddingKt {
+ method public static androidx.wear.protolayout.modifiers.LayoutModifier padding(androidx.wear.protolayout.modifiers.LayoutModifier, androidx.wear.protolayout.ModifiersBuilders.Padding padding);
+ method public static androidx.wear.protolayout.modifiers.LayoutModifier padding(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float all);
+ method public static androidx.wear.protolayout.modifiers.LayoutModifier padding(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float horizontal, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float vertical);
+ method public static androidx.wear.protolayout.modifiers.LayoutModifier padding(androidx.wear.protolayout.modifiers.LayoutModifier, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float start, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float top, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float end, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float bottom, optional boolean rtlAware);
+ method public static androidx.wear.protolayout.ModifiersBuilders.Padding padding(@Dimension(unit=androidx.annotation.Dimension.Companion.DP) float all);
+ method public static androidx.wear.protolayout.ModifiersBuilders.Padding padding(@Dimension(unit=androidx.annotation.Dimension.Companion.DP) float horizontal, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float vertical);
+ method public static androidx.wear.protolayout.ModifiersBuilders.Padding padding(optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float start, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float top, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float end, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float bottom, optional boolean rtlAware);
+ }
+
public final class SemanticsKt {
method public static androidx.wear.protolayout.modifiers.LayoutModifier contentDescription(androidx.wear.protolayout.modifiers.LayoutModifier, String staticValue, optional @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) androidx.wear.protolayout.expression.DynamicBuilders.DynamicString? dynamicValue);
method public static androidx.wear.protolayout.modifiers.LayoutModifier semanticsRole(androidx.wear.protolayout.modifiers.LayoutModifier, int semanticsRole);
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Background.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Background.kt
new file mode 100644
index 0000000..045a971
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Background.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.wear.protolayout.modifiers
+
+import android.annotation.SuppressLint
+import androidx.annotation.Dimension
+import androidx.annotation.Dimension.Companion.DP
+import androidx.wear.protolayout.ModifiersBuilders.Background
+import androidx.wear.protolayout.ModifiersBuilders.Corner
+import androidx.wear.protolayout.ModifiersBuilders.CornerRadius
+import androidx.wear.protolayout.expression.RequiresSchemaVersion
+import androidx.wear.protolayout.types.LayoutColor
+import androidx.wear.protolayout.types.cornerRadius
+import androidx.wear.protolayout.types.dp
+
+/** Sets the background color to [color]. */
+fun LayoutModifier.backgroundColor(color: LayoutColor): LayoutModifier =
+ this then BaseBackgroundElement(color)
+
+/**
+ * Sets the background color and clipping.
+ *
+ * @param color for the background
+ * @param corner to use for clipping the background
+ */
+fun LayoutModifier.background(color: LayoutColor, corner: Corner? = null): LayoutModifier =
+ this then BaseBackgroundElement(color).apply { corner?.let { clip(it) } }
+
+/** Clips the element to a rounded rectangle with four corners with [cornerRadius] radius. */
+fun LayoutModifier.clip(@Dimension(DP) cornerRadius: Float): LayoutModifier =
+ this then BaseCornerElement(cornerRadius)
+
+/**
+ * Clips the element to a rounded shape with [x] as the radius on the horizontal axis and [y] as the
+ * radius on the vertical axis for the four corners.
+ */
+@RequiresSchemaVersion(major = 1, minor = 400)
+fun LayoutModifier.clip(@Dimension(DP) x: Float, @Dimension(DP) y: Float): LayoutModifier {
+ val r = cornerRadius(x, y)
+ return this then
+ BaseCornerElement(
+ topLeftRadius = r,
+ topRightRadius = r,
+ bottomLeftRadius = r,
+ bottomRightRadius = r
+ )
+}
+
+/** Clips the element to a rounded rectangle with corners specified in [corner]. */
+fun LayoutModifier.clip(corner: Corner): LayoutModifier =
+ this then
+ BaseCornerElement(
+ cornerRadiusDp = corner.radius?.value,
+ topLeftRadius = corner.topLeftRadius,
+ topRightRadius = corner.topRightRadius,
+ bottomLeftRadius = corner.bottomLeftRadius,
+ bottomRightRadius = corner.bottomRightRadius
+ )
+
+/**
+ * Clips the top left corner of the element with [x] as the radius on the horizontal axis and [y] as
+ * the radius on the vertical axis.
+ */
+@RequiresSchemaVersion(major = 1, minor = 400)
+fun LayoutModifier.clipTopLeft(
+ @Dimension(DP) x: Float,
+ @Dimension(DP) y: Float = x
+): LayoutModifier = this then BaseCornerElement(topLeftRadius = cornerRadius(x, y))
+
+/**
+ * Clips the top right corner of the element with [x] as the radius on the horizontal axis and [y]
+ * as the radius on the vertical axis.
+ */
+@RequiresSchemaVersion(major = 1, minor = 400)
+fun LayoutModifier.clipTopRight(
+ @Dimension(DP) x: Float,
+ @Dimension(DP) y: Float = x
+): LayoutModifier = this then BaseCornerElement(topRightRadius = cornerRadius(x, y))
+
+/**
+ * Clips the bottom left corner of the element with [x] as the radius on the horizontal axis and [y]
+ * as the radius on the vertical axis.
+ */
+@RequiresSchemaVersion(major = 1, minor = 400)
+fun LayoutModifier.clipBottomLeft(
+ @Dimension(DP) x: Float,
+ @Dimension(DP) y: Float = x
+): LayoutModifier = this then BaseCornerElement(bottomLeftRadius = cornerRadius(x, y))
+
+/**
+ * Clips the bottom right corner of the element with [x] as the radius on the horizontal axis and
+ * [y] as the radius on the vertical axis.
+ */
+@RequiresSchemaVersion(major = 1, minor = 400)
+fun LayoutModifier.clipBottomRight(
+ @Dimension(DP) x: Float,
+ @Dimension(DP) y: Float = x
+): LayoutModifier = this then BaseCornerElement(bottomRightRadius = cornerRadius(x, y))
+
+internal class BaseBackgroundElement(val color: LayoutColor) : LayoutModifier.Element {
+ fun foldIn(initial: Background.Builder?): Background.Builder =
+ (initial ?: Background.Builder()).setColor(color.prop)
+}
+
+internal class BaseCornerElement(
+ val cornerRadiusDp: Float? = null,
+ @RequiresSchemaVersion(major = 1, minor = 400) val topLeftRadius: CornerRadius? = null,
+ @RequiresSchemaVersion(major = 1, minor = 400) val topRightRadius: CornerRadius? = null,
+ @RequiresSchemaVersion(major = 1, minor = 400) val bottomLeftRadius: CornerRadius? = null,
+ @RequiresSchemaVersion(major = 1, minor = 400) val bottomRightRadius: CornerRadius? = null
+) : LayoutModifier.Element {
+ @SuppressLint("ProtoLayoutMinSchema")
+ fun foldIn(initial: Corner.Builder?): Corner.Builder =
+ (initial ?: Corner.Builder()).apply {
+ cornerRadiusDp?.let { setRadius(cornerRadiusDp.dp) }
+ topLeftRadius?.let { setTopLeftRadius(cornerRadius(it.x.value, it.y.value)) }
+ topRightRadius?.let { setTopRightRadius(cornerRadius(it.x.value, it.y.value)) }
+ bottomLeftRadius?.let { setBottomLeftRadius(cornerRadius(it.x.value, it.y.value)) }
+ bottomRightRadius?.let { setBottomRightRadius(cornerRadius(it.x.value, it.y.value)) }
+ }
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Clickable.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Clickable.kt
new file mode 100644
index 0000000..da4d74f
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Clickable.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.modifiers
+
+import android.annotation.SuppressLint
+import androidx.annotation.Dimension
+import androidx.annotation.Dimension.Companion.DP
+import androidx.wear.protolayout.ActionBuilders.Action
+import androidx.wear.protolayout.ActionBuilders.LoadAction
+import androidx.wear.protolayout.ActionBuilders.actionFromProto
+import androidx.wear.protolayout.ModifiersBuilders.Clickable
+import androidx.wear.protolayout.StateBuilders.State
+import androidx.wear.protolayout.expression.RequiresSchemaVersion
+import androidx.wear.protolayout.types.dp
+
+/**
+ * Adds the clickable property of the modified element. It allows the modified element to have
+ * actions associated with it, which will be executed when the element is tapped.
+ *
+ * @param action is triggered whenever the element is tapped. By default adds an empty [LoadAction].
+ * @param id is the associated identifier for this clickable. This will be passed to the action
+ * handler.
+ */
+fun LayoutModifier.clickable(action: Action = loadAction(), id: String? = null): LayoutModifier =
+ this then BaseClickableElement(action = action, id = id)
+
+/**
+ * Creates a [Clickable] that allows the modified element to have actions associated with it, which
+ * will be executed when the element is tapped.
+ *
+ * @param action is triggered whenever the element is tapped. By default adds an empty [LoadAction].
+ * @param id is the associated identifier for this clickable. This will be passed to the action
+ * handler.
+ * @param minClickableWidth of the clickable area. The default value is 48dp, following the Material
+ * design accessibility guideline. Note that this value does not affect the layout, so the minimum
+ * clickable width is not guaranteed unless there is enough space around the element within its
+ * parent bounds.
+ * @param minClickableHeight of the clickable area. The default value is 48dp, following the
+ * Material design accessibility guideline. Note that this value does not affect the layout, so
+ * the minimum clickable height is not guaranteed unless there is enough space around the element
+ * within its parent bounds.
+ */
+@SuppressLint("ProtoLayoutMinSchema")
+@JvmOverloads
+fun clickable(
+ action: Action = loadAction(),
+ id: String? = null,
+ @RequiresSchemaVersion(major = 1, minor = 300)
+ @Dimension(DP)
+ minClickableWidth: Float = Float.NaN,
+ @RequiresSchemaVersion(major = 1, minor = 300)
+ @Dimension(DP)
+ minClickableHeight: Float = Float.NaN
+): Clickable =
+ Clickable.Builder()
+ .setOnClick(action)
+ .apply {
+ id?.let { setId(it) }
+ if (!minClickableWidth.isNaN()) setMinimumClickableWidth(minClickableWidth.dp)
+ if (!minClickableHeight.isNaN()) setMinimumClickableHeight(minClickableHeight.dp)
+ }
+ .build()
+
+/**
+ * Adds the clickable property of the modified element. It allows the modified element to have
+ * actions associated with it, which will be executed when the element is tapped.
+ */
+fun LayoutModifier.clickable(clickable: Clickable): LayoutModifier =
+ this then
+ BaseClickableElement(
+ action =
+ clickable.onClick?.let {
+ actionFromProto(it.toActionProto(), checkNotNull(clickable.fingerprint))
+ },
+ id = clickable.id,
+ minClickableWidth = clickable.minimumClickableWidth.value,
+ minClickableHeight = clickable.minimumClickableHeight.value
+ )
+
+/**
+ * Creates an action used to load (or reload) the layout contents.
+ *
+ * @param requestedState is the [State] associated with this action. This state will be passed to
+ * the action handler.
+ */
+fun loadAction(requestedState: (State.Builder.() -> Unit)? = null): LoadAction =
+ LoadAction.Builder()
+ .apply { requestedState?.let { this.setRequestState(State.Builder().apply(it).build()) } }
+ .build()
+
+/**
+ * Sets the minimum width and height of the clickable area. The default value is 48dp, following the
+ * Material design accessibility guideline. Note that this value does not affect the layout, so the
+ * minimum clickable width/height is not guaranteed unless there is enough space around the element
+ * within its parent bounds.
+ */
+@RequiresSchemaVersion(major = 1, minor = 300)
+fun LayoutModifier.minimumTouchTargetSize(
+ @Dimension(DP) minWidth: Float,
+ @Dimension(DP) minHeight: Float
+): LayoutModifier =
+ this then BaseClickableElement(minClickableWidth = minWidth, minClickableHeight = minHeight)
+
+internal class BaseClickableElement(
+ val action: Action? = null,
+ val id: String? = null,
+ @Dimension(DP) val minClickableWidth: Float = Float.NaN,
+ @Dimension(DP) val minClickableHeight: Float = Float.NaN,
+) : LayoutModifier.Element {
+ @SuppressLint("ProtoLayoutMinSchema")
+ fun foldIn(initial: Clickable.Builder?): Clickable.Builder =
+ (initial ?: Clickable.Builder()).apply {
+ if (!id.isNullOrEmpty()) setId(id)
+ action?.let { setOnClick(it) }
+ if (!minClickableWidth.isNaN()) setMinimumClickableWidth(minClickableWidth.dp)
+ if (!minClickableHeight.isNaN()) setMinimumClickableHeight(minClickableHeight.dp)
+ }
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Metadata.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Metadata.kt
new file mode 100644
index 0000000..9c0f8ca
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Metadata.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:RestrictTo(Scope.LIBRARY_GROUP)
+
+package androidx.wear.protolayout.modifiers
+
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope
+import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata
+
+/**
+ * Applies additional metadata about an element. This is meant to be used by libraries building
+ * higher-level components. This can be used to track component metadata.
+ */
+fun LayoutModifier.tag(tagData: ByteArray): LayoutModifier =
+ this then BaseMetadataElement(tagData = tagData)
+
+/**
+ * Applies additional metadata about an element. This is meant to be used by libraries building
+ * higher-level components. This can be used to track component metadata.
+ */
+fun LayoutModifier.tag(tag: String): LayoutModifier = tag(tag.toByteArray())
+
+internal class BaseMetadataElement(val tagData: ByteArray) : LayoutModifier.Element {
+ fun foldIn(initial: ElementMetadata.Builder?): ElementMetadata.Builder =
+ (initial ?: ElementMetadata.Builder()).setTagData(tagData)
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/ModifierAppliers.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/ModifierAppliers.kt
index 194809c..0b5531a 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/ModifierAppliers.kt
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/ModifierAppliers.kt
@@ -16,29 +16,43 @@
package androidx.wear.protolayout.modifiers
-import androidx.annotation.RestrictTo
import androidx.wear.protolayout.ModifiersBuilders
+import androidx.wear.protolayout.ModifiersBuilders.Background
+import androidx.wear.protolayout.ModifiersBuilders.Clickable
+import androidx.wear.protolayout.ModifiersBuilders.Corner
+import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata
+import androidx.wear.protolayout.ModifiersBuilders.Padding
import androidx.wear.protolayout.ModifiersBuilders.Semantics
/** Creates a [ModifiersBuilders.Modifiers] from a [LayoutModifier]. */
-fun LayoutModifier.toProtoLayoutModifiers(): ModifiersBuilders.Modifiers =
- toProtoLayoutModifiersBuilder().build()
+fun LayoutModifier.toProtoLayoutModifiers(): ModifiersBuilders.Modifiers {
+ var semantics: Semantics.Builder? = null
+ var background: Background.Builder? = null
+ var corners: Corner.Builder? = null
+ var clickable: Clickable.Builder? = null
+ var padding: Padding.Builder? = null
+ var metadata: ElementMetadata.Builder? = null
-// TODO: b/384921198 - Remove when M3 elements can use LayoutModifier chain for everything.
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-/** Creates a [ModifiersBuilders.Modifiers.Builder] from a [LayoutModifier]. */
-fun LayoutModifier.toProtoLayoutModifiersBuilder(): ModifiersBuilders.Modifiers.Builder {
- data class AccumulatingModifier(val semantics: Semantics.Builder? = null)
-
- val accumulatingModifier =
- this.foldIn(AccumulatingModifier()) { acc, e ->
- when (e) {
- is BaseSemanticElement -> AccumulatingModifier(semantics = e.foldIn(acc.semantics))
- else -> acc
- }
+ this.foldIn(Unit) { _, e ->
+ when (e) {
+ is BaseSemanticElement -> semantics = e.foldIn(semantics)
+ is BaseBackgroundElement -> background = e.foldIn(background)
+ is BaseCornerElement -> corners = e.foldIn(corners)
+ is BaseClickableElement -> clickable = e.foldIn(clickable)
+ is BasePaddingElement -> padding = e.foldIn(padding)
+ is BaseMetadataElement -> metadata = e.foldIn(metadata)
}
-
- return ModifiersBuilders.Modifiers.Builder().apply {
- accumulatingModifier.semantics?.let { setSemantics(it.build()) }
}
+
+ corners?.let { background = (background ?: Background.Builder()).setCorner(it.build()) }
+
+ return ModifiersBuilders.Modifiers.Builder()
+ .apply {
+ semantics?.let { setSemantics(it.build()) }
+ background?.let { setBackground(it.build()) }
+ clickable?.let { setClickable(it.build()) }
+ padding?.let { setPadding(it.build()) }
+ metadata?.let { setMetadata(it.build()) }
+ }
+ .build()
}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Padding.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Padding.kt
new file mode 100644
index 0000000..050b8ce
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Padding.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.modifiers
+
+import androidx.annotation.Dimension
+import androidx.annotation.Dimension.Companion.DP
+import androidx.wear.protolayout.ModifiersBuilders.Padding
+import androidx.wear.protolayout.types.dp
+
+/**
+ * Applies [all] dp of additional space along each edge of the content, left, top, right and bottom.
+ */
+fun LayoutModifier.padding(@Dimension(DP) all: Float): LayoutModifier =
+ this then BasePaddingElement(start = all, top = all, end = all, bottom = all, rtlAware = false)
+
+/**
+ * Creates a [Padding] that applies [all] dp of additional space along each edge of the content,
+ * left, top, right and bottom.
+ */
+fun padding(@Dimension(DP) all: Float): Padding = Padding.Builder().setAll(all.dp).build()
+
+/**
+ * Applies [horizontal] dp of additional space along the left and right edges of the content and
+ * [vertical] dp of additional space along the top and bottom edges of the content.
+ */
+fun LayoutModifier.padding(
+ @Dimension(DP) horizontal: Float,
+ @Dimension(DP) vertical: Float
+): LayoutModifier = padding(horizontal, vertical, horizontal, vertical, rtlAware = false)
+
+/**
+ * Creates a [Padding] that applies [horizontal] dp of additional space along the left and right
+ * edges of the content and [vertical] dp of additional space along the top and bottom edges of the
+ * content.
+ */
+fun padding(@Dimension(DP) horizontal: Float, @Dimension(DP) vertical: Float): Padding =
+ padding(horizontal, vertical, horizontal, vertical)
+
+/**
+ * Applies additional space along each edge of the content in [DP]: [start], [top], [end] and
+ * [bottom]
+ *
+ * @param start The padding on the start of the content, depending on the layout direction, in [DP]
+ * and the value of [rtlAware].
+ * @param top The padding at the top, in [DP].
+ * @param end The padding on the end of the content, depending on the layout direction, in [DP] and
+ * the value of [rtlAware].
+ * @param bottom The padding at the bottom, in [DP].
+ * @param rtlAware specifies whether the [start]/[end] padding is aware of RTL support. If `true`,
+ * the values for [start]/[end] will follow the layout direction (i.e. [start] will refer to the
+ * right hand side of the container if the device is using an RTL locale). If `false`,
+ * [start]/[end] will always map to left/right, accordingly.
+ */
+fun LayoutModifier.padding(
+ @Dimension(DP) start: Float = Float.NaN,
+ @Dimension(DP) top: Float = Float.NaN,
+ @Dimension(DP) end: Float = Float.NaN,
+ @Dimension(DP) bottom: Float = Float.NaN,
+ rtlAware: Boolean = true
+): LayoutModifier =
+ this then
+ BasePaddingElement(
+ start = start,
+ top = top,
+ end = end,
+ bottom = bottom,
+ rtlAware = rtlAware
+ )
+
+/** Applies additional space along each edge of the content. */
+fun LayoutModifier.padding(padding: Padding): LayoutModifier =
+ padding(
+ start = padding.start?.value ?: Float.NaN,
+ top = padding.top?.value ?: Float.NaN,
+ end = padding.end?.value ?: Float.NaN,
+ bottom = padding.bottom?.value ?: Float.NaN
+ )
+
+/**
+ * Creates a [Padding] that applies additional space along each edge of the content in [DP]:
+ * [start], [top], [end] and [bottom]
+ *
+ * @param start The padding on the start of the content, depending on the layout direction, in [DP]
+ * and the value of [rtlAware].
+ * @param top The padding at the top, in [DP].
+ * @param end The padding on the end of the content, depending on the layout direction, in [DP] and
+ * the value of [rtlAware].
+ * @param bottom The padding at the bottom, in [DP].
+ * @param rtlAware specifies whether the [start]/[end] padding is aware of RTL support. If `true`,
+ * the values for [start]/[end] will follow the layout direction (i.e. [start] will refer to the
+ * right hand side of the container if the device is using an RTL locale). If `false`,
+ * [start]/[end] will always map to left/right, accordingly.
+ */
+@Suppress("MissingJvmstatic") // Conflicts with the other overloads
+fun padding(
+ @Dimension(DP) start: Float = Float.NaN,
+ @Dimension(DP) top: Float = Float.NaN,
+ @Dimension(DP) end: Float = Float.NaN,
+ @Dimension(DP) bottom: Float = Float.NaN,
+ rtlAware: Boolean = true
+): Padding =
+ Padding.Builder()
+ .apply {
+ if (!start.isNaN()) {
+ setStart(start.dp)
+ }
+ if (!top.isNaN()) {
+ setTop(top.dp)
+ }
+ if (!end.isNaN()) {
+ setEnd(end.dp)
+ }
+ if (!bottom.isNaN()) {
+ setBottom(bottom.dp)
+ }
+ }
+ .setRtlAware(rtlAware)
+ .build()
+
+internal class BasePaddingElement(
+ val start: Float = Float.NaN,
+ val top: Float = Float.NaN,
+ val end: Float = Float.NaN,
+ val bottom: Float = Float.NaN,
+ val rtlAware: Boolean = true
+) : LayoutModifier.Element {
+
+ fun foldIn(initial: Padding.Builder?): Padding.Builder =
+ (initial ?: Padding.Builder()).apply {
+ if (!start.isNaN()) {
+ setStart(start.dp)
+ }
+ if (!top.isNaN()) {
+ setTop(top.dp)
+ }
+ if (!end.isNaN()) {
+ setEnd(end.dp)
+ }
+ if (!bottom.isNaN()) {
+ setBottom(bottom.dp)
+ }
+ setRtlAware(rtlAware)
+ }
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Semantics.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Semantics.kt
index 3bf0caf..d35b071 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Semantics.kt
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Semantics.kt
@@ -25,30 +25,6 @@
import androidx.wear.protolayout.expression.RequiresSchemaVersion
import java.util.Objects
-internal class BaseSemanticElement(
- val contentDescription: StringProp? = null,
- @SemanticsRole val semanticsRole: Int = SEMANTICS_ROLE_NONE
-) : LayoutModifier.Element {
- @SuppressLint("ProtoLayoutMinSchema")
- fun foldIn(initial: Semantics.Builder?): Semantics.Builder =
- (initial ?: Semantics.Builder()).apply {
- contentDescription?.let { setContentDescription(it) }
- if (semanticsRole != SEMANTICS_ROLE_NONE) {
- setRole(semanticsRole)
- }
- }
-
- override fun equals(other: Any?): Boolean =
- other is BaseSemanticElement &&
- contentDescription == other.contentDescription &&
- semanticsRole == other.semanticsRole
-
- override fun hashCode(): Int = Objects.hash(contentDescription, semanticsRole)
-
- override fun toString(): String =
- "BaseSemanticElement[contentDescription=$contentDescription, semanticRole=$semanticsRole"
-}
-
/**
* Adds content description to be read by Talkback.
*
@@ -76,3 +52,27 @@
*/
fun LayoutModifier.semanticsRole(@SemanticsRole semanticsRole: Int): LayoutModifier =
this then BaseSemanticElement(semanticsRole = semanticsRole)
+
+internal class BaseSemanticElement(
+ val contentDescription: StringProp? = null,
+ @SemanticsRole val semanticsRole: Int = SEMANTICS_ROLE_NONE
+) : LayoutModifier.Element {
+ @SuppressLint("ProtoLayoutMinSchema")
+ fun foldIn(initial: Semantics.Builder?): Semantics.Builder =
+ (initial ?: Semantics.Builder()).apply {
+ contentDescription?.let { setContentDescription(it) }
+ if (semanticsRole != SEMANTICS_ROLE_NONE) {
+ setRole(semanticsRole)
+ }
+ }
+
+ override fun equals(other: Any?): Boolean =
+ other is BaseSemanticElement &&
+ contentDescription == other.contentDescription &&
+ semanticsRole == other.semanticsRole
+
+ override fun hashCode(): Int = Objects.hash(contentDescription, semanticsRole)
+
+ override fun toString(): String =
+ "BaseSemanticElement[contentDescription=$contentDescription, semanticRole=$semanticsRole"
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/types/Helpers.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/types/Helpers.kt
index 2f4295e..ef30cd7 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/types/Helpers.kt
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/types/Helpers.kt
@@ -16,9 +16,12 @@
package androidx.wear.protolayout.types
+import androidx.wear.protolayout.DimensionBuilders.DpProp
import androidx.wear.protolayout.DimensionBuilders.EmProp
import androidx.wear.protolayout.DimensionBuilders.SpProp
+import androidx.wear.protolayout.ModifiersBuilders
import androidx.wear.protolayout.TypeBuilders.BoolProp
+import androidx.wear.protolayout.expression.RequiresSchemaVersion
internal val Float.sp: SpProp
get() = SpProp.Builder().setValue(this).build()
@@ -28,3 +31,10 @@
internal val Boolean.prop: BoolProp
get() = BoolProp.Builder(this).build()
+
+internal val Float.dp: DpProp
+ get() = DpProp.Builder(this).build()
+
+@RequiresSchemaVersion(major = 1, minor = 400)
+internal fun cornerRadius(x: Float, y: Float) =
+ ModifiersBuilders.CornerRadius.Builder(x.dp, y.dp).build()
diff --git a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/modifiers/ModifiersTest.kt b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/modifiers/ModifiersTest.kt
index f91621c..fdc7f7f 100644
--- a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/modifiers/ModifiersTest.kt
+++ b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/modifiers/ModifiersTest.kt
@@ -16,10 +16,16 @@
package androidx.wear.protolayout.modifiers
+import android.graphics.Color
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.wear.protolayout.ActionBuilders.LoadAction
import androidx.wear.protolayout.ModifiersBuilders.SEMANTICS_ROLE_BUTTON
import androidx.wear.protolayout.ModifiersBuilders.SEMANTICS_ROLE_NONE
-import androidx.wear.protolayout.expression.DynamicBuilders
+import androidx.wear.protolayout.expression.AppDataKey
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString
+import androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue
+import androidx.wear.protolayout.types.LayoutColor
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@@ -68,8 +74,174 @@
assertThat(modifiers.semantics?.role).isEqualTo(SEMANTICS_ROLE_BUTTON)
}
+ @Test
+ fun background_clip_toModifier() {
+ val modifiers =
+ LayoutModifier.background(COLOR)
+ .clip(CORNER_RADIUS)
+ .clip(CORNER_RADIUS_X, CORNER_RADIUS_Y)
+ .clipTopRight(0f, 0f)
+ .toProtoLayoutModifiers()
+
+ assertThat(modifiers.background?.color?.argb).isEqualTo(COLOR.prop.argb)
+ assertThat(modifiers.background?.corner?.radius?.value).isEqualTo(CORNER_RADIUS)
+ assertThat(modifiers.background?.corner?.topLeftRadius?.x?.value).isEqualTo(CORNER_RADIUS_X)
+ assertThat(modifiers.background?.corner?.topLeftRadius?.y?.value).isEqualTo(CORNER_RADIUS_Y)
+ assertThat(modifiers.background?.corner?.topRightRadius?.x?.value).isEqualTo(0f)
+ assertThat(modifiers.background?.corner?.topRightRadius?.y?.value).isEqualTo(0f)
+ assertThat(modifiers.background?.corner?.bottomLeftRadius?.x?.value)
+ .isEqualTo(CORNER_RADIUS_X)
+ assertThat(modifiers.background?.corner?.bottomLeftRadius?.y?.value)
+ .isEqualTo(CORNER_RADIUS_Y)
+ assertThat(modifiers.background?.corner?.bottomRightRadius?.x?.value)
+ .isEqualTo(CORNER_RADIUS_X)
+ assertThat(modifiers.background?.corner?.bottomRightRadius?.y?.value)
+ .isEqualTo(CORNER_RADIUS_Y)
+ }
+
+ @Test
+ fun perCornerClip_clip_overwritesAllCorners() {
+ val modifiers =
+ LayoutModifier.clipTopLeft(0f, 1f)
+ .clipTopRight(2f, 3f)
+ .clipBottomLeft(4f, 5f)
+ .clipBottomRight(6f, 7f)
+ .clip(CORNER_RADIUS_X, CORNER_RADIUS_Y)
+ .toProtoLayoutModifiers()
+
+ assertThat(modifiers.background?.color).isNull()
+ assertThat(modifiers.background?.corner?.radius?.value).isEqualTo(null)
+ assertThat(modifiers.background?.corner?.topLeftRadius?.x?.value).isEqualTo(CORNER_RADIUS_X)
+ assertThat(modifiers.background?.corner?.topLeftRadius?.y?.value).isEqualTo(CORNER_RADIUS_Y)
+ assertThat(modifiers.background?.corner?.topRightRadius?.x?.value)
+ .isEqualTo(CORNER_RADIUS_X)
+ assertThat(modifiers.background?.corner?.topRightRadius?.y?.value)
+ .isEqualTo(CORNER_RADIUS_Y)
+ assertThat(modifiers.background?.corner?.bottomLeftRadius?.x?.value)
+ .isEqualTo(CORNER_RADIUS_X)
+ assertThat(modifiers.background?.corner?.bottomLeftRadius?.y?.value)
+ .isEqualTo(CORNER_RADIUS_Y)
+ assertThat(modifiers.background?.corner?.bottomRightRadius?.x?.value)
+ .isEqualTo(CORNER_RADIUS_X)
+ assertThat(modifiers.background?.corner?.bottomRightRadius?.y?.value)
+ .isEqualTo(CORNER_RADIUS_Y)
+ }
+
+ @Test
+ fun clickable_toModifier() {
+ val id = "ID"
+ val minTouchWidth = 51f
+ val minTouchHeight = 52f
+ val statePair1 = Pair(AppDataKey<DynamicInt32>("Int"), DynamicDataValue.fromInt(42))
+ val statePair2 =
+ Pair(AppDataKey<DynamicString>("String"), DynamicDataValue.fromString("42"))
+
+ val modifiers =
+ LayoutModifier.clickable(
+ loadAction {
+ addKeyToValueMapping(statePair1.first, statePair1.second)
+ addKeyToValueMapping(statePair2.first, statePair2.second)
+ },
+ id
+ )
+ .minimumTouchTargetSize(minTouchWidth, minTouchHeight)
+ .toProtoLayoutModifiers()
+
+ assertThat(modifiers.clickable?.id).isEqualTo(id)
+ assertThat(modifiers.clickable?.minimumClickableWidth?.value).isEqualTo(minTouchWidth)
+ assertThat(modifiers.clickable?.minimumClickableHeight?.value).isEqualTo(minTouchHeight)
+ assertThat(modifiers.clickable?.onClick).isInstanceOf(LoadAction::class.java)
+ val action = modifiers.clickable?.onClick as LoadAction
+ assertThat(action.requestState?.keyToValueMapping)
+ .containsExactlyEntriesIn(mapOf(statePair1, statePair2))
+ }
+
+ @Test
+ fun clickable_fromProto_toModifier() {
+ val id = "ID"
+ val minTouchWidth = 51f
+ val minTouchHeight = 52f
+ val statePair1 = Pair(AppDataKey<DynamicInt32>("Int"), DynamicDataValue.fromInt(42))
+ val statePair2 =
+ Pair(AppDataKey<DynamicString>("String"), DynamicDataValue.fromString("42"))
+
+ val modifiers =
+ LayoutModifier.clickable(
+ clickable(
+ loadAction {
+ addKeyToValueMapping(statePair1.first, statePair1.second)
+ addKeyToValueMapping(statePair2.first, statePair2.second)
+ },
+ id = id,
+ minClickableWidth = minTouchWidth,
+ minClickableHeight = minTouchHeight
+ )
+ )
+ .toProtoLayoutModifiers()
+
+ assertThat(modifiers.clickable?.id).isEqualTo(id)
+ assertThat(modifiers.clickable?.minimumClickableWidth?.value).isEqualTo(minTouchWidth)
+ assertThat(modifiers.clickable?.minimumClickableHeight?.value).isEqualTo(minTouchHeight)
+ assertThat(modifiers.clickable?.onClick).isInstanceOf(LoadAction::class.java)
+ val action = modifiers.clickable?.onClick as LoadAction
+ assertThat(action.requestState?.keyToValueMapping)
+ .containsExactlyEntriesIn(mapOf(statePair1, statePair2))
+ }
+
+ @Test
+ fun padding_toModifier() {
+ val modifiers =
+ LayoutModifier.padding(PADDING_ALL)
+ .padding(bottom = BOTTOM_PADDING, rtlAware = false)
+ .toProtoLayoutModifiers()
+
+ assertThat(modifiers.padding?.start?.value).isEqualTo(PADDING_ALL)
+ assertThat(modifiers.padding?.top?.value).isEqualTo(PADDING_ALL)
+ assertThat(modifiers.padding?.end?.value).isEqualTo(PADDING_ALL)
+ assertThat(modifiers.padding?.bottom?.value).isEqualTo(BOTTOM_PADDING)
+ assertThat(modifiers.padding?.rtlAware?.value).isFalse()
+ }
+
+ @Test
+ fun perSidePadding_padding_overwritesAllSides() {
+ val modifiers =
+ LayoutModifier.padding(
+ start = START_PADDING,
+ top = TOP_PADDING,
+ end = END_PADDING,
+ bottom = BOTTOM_PADDING,
+ rtlAware = true
+ )
+ .padding(PADDING_ALL)
+ .toProtoLayoutModifiers()
+
+ assertThat(modifiers.padding?.start?.value).isEqualTo(PADDING_ALL)
+ assertThat(modifiers.padding?.top?.value).isEqualTo(PADDING_ALL)
+ assertThat(modifiers.padding?.end?.value).isEqualTo(PADDING_ALL)
+ assertThat(modifiers.padding?.bottom?.value).isEqualTo(PADDING_ALL)
+ assertThat(modifiers.padding?.rtlAware?.value).isFalse()
+ }
+
+ @Test
+ fun metadata_toModifier() {
+ val modifiers = LayoutModifier.tag(METADATA).toProtoLayoutModifiers()
+
+ assertThat(modifiers.metadata?.tagData).isEqualTo(METADATA_BYTE_ARRAY)
+ }
+
companion object {
const val STATIC_CONTENT_DESCRIPTION = "content desc"
- val DYNAMIC_CONTENT_DESCRIPTION = DynamicBuilders.DynamicString.constant("dynamic content")
+ val DYNAMIC_CONTENT_DESCRIPTION = DynamicString.constant("dynamic content")
+ val COLOR = LayoutColor(Color.RED)
+ const val CORNER_RADIUS_X = 1.2f
+ const val CORNER_RADIUS_Y = 3.4f
+ const val CORNER_RADIUS = 5.6f
+ const val START_PADDING = 1f
+ const val TOP_PADDING = 2f
+ const val END_PADDING = 3f
+ const val BOTTOM_PADDING = 4f
+ const val PADDING_ALL = 5f
+ const val METADATA = "metadata"
+ val METADATA_BYTE_ARRAY = METADATA.toByteArray()
}
}
diff --git a/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/Helpers.kt b/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/Helpers.kt
deleted file mode 100644
index bc76b6c..0000000
--- a/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/Helpers.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.wear.tiles.samples.tile
-
-import androidx.wear.protolayout.ActionBuilders.LoadAction
-import androidx.wear.protolayout.ModifiersBuilders.Clickable
-import androidx.wear.protolayout.TypeBuilders.StringProp
-
-internal val EMPTY_LOAD_CLICKABLE =
- Clickable.Builder().setOnClick(LoadAction.Builder().build()).build()
-
-internal fun String.prop(): StringProp = StringProp.Builder(this).build()
diff --git a/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/PlaygroundTileService.kt b/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/PlaygroundTileService.kt
index 1b5e62c..f7704fa 100644
--- a/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/PlaygroundTileService.kt
+++ b/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/PlaygroundTileService.kt
@@ -48,6 +48,7 @@
import androidx.wear.protolayout.material3.textDataCard
import androidx.wear.protolayout.material3.textEdgeButton
import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.clickable
import androidx.wear.protolayout.modifiers.contentDescription
import androidx.wear.protolayout.types.layoutString
import androidx.wear.tiles.RequestBuilders
@@ -122,7 +123,7 @@
mainSlot = { oneSlotButtons() },
bottomSlot = {
textEdgeButton(
- onClick = EMPTY_LOAD_CLICKABLE,
+ onClick = clickable(),
modifier = LayoutModifier.contentDescription("EdgeButton"),
) {
text("Edge".layoutString)
@@ -134,7 +135,7 @@
private fun MaterialScope.oneSlotButtons() = buttonGroup {
buttonGroupItem {
iconButton(
- onClick = EMPTY_LOAD_CLICKABLE,
+ onClick = clickable(),
modifier = LayoutModifier.contentDescription("Icon button"),
width = expand(),
iconContent = { icon(ICON_ID) }
@@ -142,7 +143,7 @@
}
buttonGroupItem {
iconButton(
- onClick = EMPTY_LOAD_CLICKABLE,
+ onClick = clickable(),
modifier = LayoutModifier.contentDescription("Icon button"),
width = expand(),
shape = shapes.large,
@@ -151,7 +152,7 @@
}
buttonGroupItem {
textButton(
- onClick = EMPTY_LOAD_CLICKABLE,
+ onClick = clickable(),
modifier = LayoutModifier.contentDescription("Text button"),
width = expand(),
style = smallTextButtonStyle(),
@@ -163,7 +164,7 @@
private fun MaterialScope.appCardSample() =
appCard(
- onClick = EMPTY_LOAD_CLICKABLE,
+ onClick = clickable(),
modifier = LayoutModifier.contentDescription("Sample Card"),
colors =
CardColors(
@@ -199,7 +200,7 @@
private fun MaterialScope.graphicDataCardSample() =
graphicDataCard(
- onClick = EMPTY_LOAD_CLICKABLE,
+ onClick = clickable(),
modifier = LayoutModifier.contentDescription("Graphic Data Card"),
height = expand(),
horizontalAlignment = LayoutElementBuilders.HORIZONTAL_ALIGN_END,
@@ -234,7 +235,7 @@
private fun MaterialScope.dataCards() = buttonGroup {
buttonGroupItem {
textDataCard(
- onClick = EMPTY_LOAD_CLICKABLE,
+ onClick = clickable(),
modifier = LayoutModifier.contentDescription("Data Card with icon"),
width = weight(1f),
height = expand(),
@@ -247,7 +248,7 @@
}
buttonGroupItem {
iconDataCard(
- onClick = EMPTY_LOAD_CLICKABLE,
+ onClick = clickable(),
modifier =
LayoutModifier.contentDescription(
"Compact Data Card without icon or secondary label"
diff --git a/webkit/webkit/api/current.txt b/webkit/webkit/api/current.txt
index 3fe8986..9e58425 100644
--- a/webkit/webkit/api/current.txt
+++ b/webkit/webkit/api/current.txt
@@ -59,17 +59,18 @@
method public static void apply(androidx.webkit.ProcessGlobalConfig);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX, enforcement="androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)") public androidx.webkit.ProcessGlobalConfig setDataDirectorySuffix(android.content.Context, String);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS, enforcement="androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)") public androidx.webkit.ProcessGlobalConfig setDirectoryBasePaths(android.content.Context, java.io.File, java.io.File);
+ method @RequiresFeature(name=androidx.webkit.WebViewFeature.STARTUP_FEATURE_CONFIGURE_PARTITIONED_COOKIES, enforcement="androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)") public androidx.webkit.ProcessGlobalConfig setPartitionedCookiesEnabled(android.content.Context, boolean);
}
public interface Profile {
- method @SuppressCompatibility @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.PROFILE_URL_PREFETCH, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @androidx.webkit.Profile.ExperimentalUrlPrefetch public void clearPrefetchAsync(String, androidx.webkit.OutcomeReceiverCompat<java.lang.Void!,androidx.webkit.PrefetchException!>);
+ method @SuppressCompatibility @RequiresFeature(name=androidx.webkit.WebViewFeature.PROFILE_URL_PREFETCH, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread @androidx.webkit.Profile.ExperimentalUrlPrefetch public void clearPrefetchAsync(String, java.util.concurrent.Executor, androidx.webkit.OutcomeReceiverCompat<java.lang.Void!,androidx.webkit.PrefetchException!>);
method @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.MULTI_PROFILE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public android.webkit.CookieManager getCookieManager();
method @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.MULTI_PROFILE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public android.webkit.GeolocationPermissions getGeolocationPermissions();
method @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.MULTI_PROFILE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public String getName();
method @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.MULTI_PROFILE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public android.webkit.ServiceWorkerController getServiceWorkerController();
method @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.MULTI_PROFILE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public android.webkit.WebStorage getWebStorage();
- method @SuppressCompatibility @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.PROFILE_URL_PREFETCH, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @androidx.webkit.Profile.ExperimentalUrlPrefetch public void prefetchUrlAsync(String, androidx.core.os.CancellationSignal?, androidx.webkit.OutcomeReceiverCompat<java.lang.Void!,androidx.webkit.PrefetchException!>);
- method @SuppressCompatibility @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.PROFILE_URL_PREFETCH, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @androidx.webkit.Profile.ExperimentalUrlPrefetch public void prefetchUrlAsync(String, androidx.core.os.CancellationSignal?, androidx.webkit.SpeculativeLoadingParameters, androidx.webkit.OutcomeReceiverCompat<java.lang.Void!,androidx.webkit.PrefetchException!>);
+ method @SuppressCompatibility @RequiresFeature(name=androidx.webkit.WebViewFeature.PROFILE_URL_PREFETCH, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread @androidx.webkit.Profile.ExperimentalUrlPrefetch public void prefetchUrlAsync(String, android.os.CancellationSignal?, java.util.concurrent.Executor, androidx.webkit.OutcomeReceiverCompat<java.lang.Void!,androidx.webkit.PrefetchException!>);
+ method @SuppressCompatibility @RequiresFeature(name=androidx.webkit.WebViewFeature.PROFILE_URL_PREFETCH, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread @androidx.webkit.Profile.ExperimentalUrlPrefetch public void prefetchUrlAsync(String, android.os.CancellationSignal?, java.util.concurrent.Executor, androidx.webkit.SpeculativeLoadingParameters, androidx.webkit.OutcomeReceiverCompat<java.lang.Void!,androidx.webkit.PrefetchException!>);
field public static final String DEFAULT_PROFILE_NAME = "Default";
}
@@ -333,6 +334,13 @@
@SuppressCompatibility @RequiresOptIn(level=androidx.annotation.RequiresOptIn.Level.ERROR) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.TYPE}) public static @interface WebSettingsCompat.ExperimentalSpeculativeLoading {
}
+ public final class WebStorageCompat {
+ method @RequiresFeature(name=androidx.webkit.WebViewFeature.DELETE_BROWSING_DATA, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void deleteBrowsingData(android.webkit.WebStorage, Runnable);
+ method @RequiresFeature(name=androidx.webkit.WebViewFeature.DELETE_BROWSING_DATA, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void deleteBrowsingData(android.webkit.WebStorage, java.util.concurrent.Executor, Runnable);
+ method @RequiresFeature(name=androidx.webkit.WebViewFeature.DELETE_BROWSING_DATA, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static String deleteBrowsingDataForSite(android.webkit.WebStorage, String, Runnable);
+ method @RequiresFeature(name=androidx.webkit.WebViewFeature.DELETE_BROWSING_DATA, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static String deleteBrowsingDataForSite(android.webkit.WebStorage, String, java.util.concurrent.Executor, Runnable);
+ }
+
public final class WebViewAssetLoader {
method @WorkerThread public android.webkit.WebResourceResponse? shouldInterceptRequest(android.net.Uri);
field public static final String DEFAULT_DOMAIN = "appassets.androidplatform.net";
@@ -424,6 +432,7 @@
field public static final String BACK_FORWARD_CACHE = "BACK_FORWARD_CACHE";
field public static final String CREATE_WEB_MESSAGE_CHANNEL = "CREATE_WEB_MESSAGE_CHANNEL";
field public static final String DEFAULT_TRAFFICSTATS_TAGGING = "DEFAULT_TRAFFICSTATS_TAGGING";
+ field public static final String DELETE_BROWSING_DATA = "DELETE_BROWSING_DATA";
field public static final String DISABLED_ACTION_MODE_MENU_ITEMS = "DISABLED_ACTION_MODE_MENU_ITEMS";
field public static final String DOCUMENT_START_SCRIPT = "DOCUMENT_START_SCRIPT";
field public static final String ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY = "ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY";
@@ -439,7 +448,7 @@
field public static final String MUTE_AUDIO = "MUTE_AUDIO";
field public static final String OFF_SCREEN_PRERASTER = "OFF_SCREEN_PRERASTER";
field public static final String POST_WEB_MESSAGE = "POST_WEB_MESSAGE";
- field @SuppressCompatibility @androidx.webkit.Profile.ExperimentalUrlPrefetch public static final String PROFILE_URL_PREFETCH = "PREFETCH_URL_V2";
+ field @SuppressCompatibility @androidx.webkit.Profile.ExperimentalUrlPrefetch public static final String PROFILE_URL_PREFETCH = "PREFETCH_URL_V3";
field public static final String PROXY_OVERRIDE = "PROXY_OVERRIDE";
field public static final String PROXY_OVERRIDE_REVERSE_BYPASS = "PROXY_OVERRIDE_REVERSE_BYPASS";
field public static final String RECEIVE_HTTP_ERROR = "RECEIVE_HTTP_ERROR";
@@ -460,6 +469,7 @@
field public static final String SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST = "SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST";
field public static final String SHOULD_OVERRIDE_WITH_REDIRECTS = "SHOULD_OVERRIDE_WITH_REDIRECTS";
field public static final String SPECULATIVE_LOADING = "SPECULATIVE_LOADING_STATUS";
+ field public static final String STARTUP_FEATURE_CONFIGURE_PARTITIONED_COOKIES = "STARTUP_FEATURE_CONFIGURE_PARTITIONED_COOKIES";
field public static final String STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX = "STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX";
field public static final String STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS = "STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS";
field public static final String START_SAFE_BROWSING = "START_SAFE_BROWSING";
diff --git a/webkit/webkit/api/restricted_current.txt b/webkit/webkit/api/restricted_current.txt
index 3fe8986..9e58425 100644
--- a/webkit/webkit/api/restricted_current.txt
+++ b/webkit/webkit/api/restricted_current.txt
@@ -59,17 +59,18 @@
method public static void apply(androidx.webkit.ProcessGlobalConfig);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX, enforcement="androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)") public androidx.webkit.ProcessGlobalConfig setDataDirectorySuffix(android.content.Context, String);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS, enforcement="androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)") public androidx.webkit.ProcessGlobalConfig setDirectoryBasePaths(android.content.Context, java.io.File, java.io.File);
+ method @RequiresFeature(name=androidx.webkit.WebViewFeature.STARTUP_FEATURE_CONFIGURE_PARTITIONED_COOKIES, enforcement="androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)") public androidx.webkit.ProcessGlobalConfig setPartitionedCookiesEnabled(android.content.Context, boolean);
}
public interface Profile {
- method @SuppressCompatibility @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.PROFILE_URL_PREFETCH, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @androidx.webkit.Profile.ExperimentalUrlPrefetch public void clearPrefetchAsync(String, androidx.webkit.OutcomeReceiverCompat<java.lang.Void!,androidx.webkit.PrefetchException!>);
+ method @SuppressCompatibility @RequiresFeature(name=androidx.webkit.WebViewFeature.PROFILE_URL_PREFETCH, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread @androidx.webkit.Profile.ExperimentalUrlPrefetch public void clearPrefetchAsync(String, java.util.concurrent.Executor, androidx.webkit.OutcomeReceiverCompat<java.lang.Void!,androidx.webkit.PrefetchException!>);
method @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.MULTI_PROFILE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public android.webkit.CookieManager getCookieManager();
method @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.MULTI_PROFILE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public android.webkit.GeolocationPermissions getGeolocationPermissions();
method @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.MULTI_PROFILE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public String getName();
method @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.MULTI_PROFILE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public android.webkit.ServiceWorkerController getServiceWorkerController();
method @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.MULTI_PROFILE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public android.webkit.WebStorage getWebStorage();
- method @SuppressCompatibility @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.PROFILE_URL_PREFETCH, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @androidx.webkit.Profile.ExperimentalUrlPrefetch public void prefetchUrlAsync(String, androidx.core.os.CancellationSignal?, androidx.webkit.OutcomeReceiverCompat<java.lang.Void!,androidx.webkit.PrefetchException!>);
- method @SuppressCompatibility @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.PROFILE_URL_PREFETCH, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @androidx.webkit.Profile.ExperimentalUrlPrefetch public void prefetchUrlAsync(String, androidx.core.os.CancellationSignal?, androidx.webkit.SpeculativeLoadingParameters, androidx.webkit.OutcomeReceiverCompat<java.lang.Void!,androidx.webkit.PrefetchException!>);
+ method @SuppressCompatibility @RequiresFeature(name=androidx.webkit.WebViewFeature.PROFILE_URL_PREFETCH, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread @androidx.webkit.Profile.ExperimentalUrlPrefetch public void prefetchUrlAsync(String, android.os.CancellationSignal?, java.util.concurrent.Executor, androidx.webkit.OutcomeReceiverCompat<java.lang.Void!,androidx.webkit.PrefetchException!>);
+ method @SuppressCompatibility @RequiresFeature(name=androidx.webkit.WebViewFeature.PROFILE_URL_PREFETCH, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread @androidx.webkit.Profile.ExperimentalUrlPrefetch public void prefetchUrlAsync(String, android.os.CancellationSignal?, java.util.concurrent.Executor, androidx.webkit.SpeculativeLoadingParameters, androidx.webkit.OutcomeReceiverCompat<java.lang.Void!,androidx.webkit.PrefetchException!>);
field public static final String DEFAULT_PROFILE_NAME = "Default";
}
@@ -333,6 +334,13 @@
@SuppressCompatibility @RequiresOptIn(level=androidx.annotation.RequiresOptIn.Level.ERROR) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.TYPE}) public static @interface WebSettingsCompat.ExperimentalSpeculativeLoading {
}
+ public final class WebStorageCompat {
+ method @RequiresFeature(name=androidx.webkit.WebViewFeature.DELETE_BROWSING_DATA, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void deleteBrowsingData(android.webkit.WebStorage, Runnable);
+ method @RequiresFeature(name=androidx.webkit.WebViewFeature.DELETE_BROWSING_DATA, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void deleteBrowsingData(android.webkit.WebStorage, java.util.concurrent.Executor, Runnable);
+ method @RequiresFeature(name=androidx.webkit.WebViewFeature.DELETE_BROWSING_DATA, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static String deleteBrowsingDataForSite(android.webkit.WebStorage, String, Runnable);
+ method @RequiresFeature(name=androidx.webkit.WebViewFeature.DELETE_BROWSING_DATA, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static String deleteBrowsingDataForSite(android.webkit.WebStorage, String, java.util.concurrent.Executor, Runnable);
+ }
+
public final class WebViewAssetLoader {
method @WorkerThread public android.webkit.WebResourceResponse? shouldInterceptRequest(android.net.Uri);
field public static final String DEFAULT_DOMAIN = "appassets.androidplatform.net";
@@ -424,6 +432,7 @@
field public static final String BACK_FORWARD_CACHE = "BACK_FORWARD_CACHE";
field public static final String CREATE_WEB_MESSAGE_CHANNEL = "CREATE_WEB_MESSAGE_CHANNEL";
field public static final String DEFAULT_TRAFFICSTATS_TAGGING = "DEFAULT_TRAFFICSTATS_TAGGING";
+ field public static final String DELETE_BROWSING_DATA = "DELETE_BROWSING_DATA";
field public static final String DISABLED_ACTION_MODE_MENU_ITEMS = "DISABLED_ACTION_MODE_MENU_ITEMS";
field public static final String DOCUMENT_START_SCRIPT = "DOCUMENT_START_SCRIPT";
field public static final String ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY = "ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY";
@@ -439,7 +448,7 @@
field public static final String MUTE_AUDIO = "MUTE_AUDIO";
field public static final String OFF_SCREEN_PRERASTER = "OFF_SCREEN_PRERASTER";
field public static final String POST_WEB_MESSAGE = "POST_WEB_MESSAGE";
- field @SuppressCompatibility @androidx.webkit.Profile.ExperimentalUrlPrefetch public static final String PROFILE_URL_PREFETCH = "PREFETCH_URL_V2";
+ field @SuppressCompatibility @androidx.webkit.Profile.ExperimentalUrlPrefetch public static final String PROFILE_URL_PREFETCH = "PREFETCH_URL_V3";
field public static final String PROXY_OVERRIDE = "PROXY_OVERRIDE";
field public static final String PROXY_OVERRIDE_REVERSE_BYPASS = "PROXY_OVERRIDE_REVERSE_BYPASS";
field public static final String RECEIVE_HTTP_ERROR = "RECEIVE_HTTP_ERROR";
@@ -460,6 +469,7 @@
field public static final String SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST = "SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST";
field public static final String SHOULD_OVERRIDE_WITH_REDIRECTS = "SHOULD_OVERRIDE_WITH_REDIRECTS";
field public static final String SPECULATIVE_LOADING = "SPECULATIVE_LOADING_STATUS";
+ field public static final String STARTUP_FEATURE_CONFIGURE_PARTITIONED_COOKIES = "STARTUP_FEATURE_CONFIGURE_PARTITIONED_COOKIES";
field public static final String STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX = "STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX";
field public static final String STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS = "STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS";
field public static final String START_SAFE_BROWSING = "START_SAFE_BROWSING";
diff --git a/webkit/webkit/src/main/java/androidx/webkit/ProcessGlobalConfig.java b/webkit/webkit/src/main/java/androidx/webkit/ProcessGlobalConfig.java
index 6bb25fa..9bcbc95 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/ProcessGlobalConfig.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/ProcessGlobalConfig.java
@@ -20,7 +20,6 @@
import androidx.annotation.GuardedBy;
import androidx.annotation.RequiresFeature;
-import androidx.annotation.RestrictTo;
import androidx.webkit.internal.ApiHelperForP;
import androidx.webkit.internal.StartupApiFeature;
import androidx.webkit.internal.WebViewFeatureInternal;
@@ -197,7 +196,6 @@
* Partitioned cookies will be enabled by default for apps that target Android B and above.
* For apps that target below Android B, this is disabled.
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@RequiresFeature(name = WebViewFeature.STARTUP_FEATURE_CONFIGURE_PARTITIONED_COOKIES,
enforcement =
"androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)")
diff --git a/webkit/webkit/src/main/java/androidx/webkit/Profile.java b/webkit/webkit/src/main/java/androidx/webkit/Profile.java
index fd0e2e3..ec6fcb1 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/Profile.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/Profile.java
@@ -16,6 +16,7 @@
package androidx.webkit;
+import android.os.CancellationSignal;
import android.webkit.CookieManager;
import android.webkit.GeolocationPermissions;
import android.webkit.ServiceWorkerController;
@@ -24,7 +25,7 @@
import androidx.annotation.AnyThread;
import androidx.annotation.RequiresFeature;
import androidx.annotation.RequiresOptIn;
-import androidx.core.os.CancellationSignal;
+import androidx.annotation.UiThread;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
@@ -33,6 +34,8 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+import java.util.Map;
+import java.util.concurrent.Executor;
/**
* A Profile represents one browsing session for WebView.
@@ -123,7 +126,7 @@
}
/**
- * Starts a URL prefetch request. Can be called from any thread.
+ * Starts a URL prefetch request. Must be called from the UI thread.
* <p>
* All WebViews associated with this Profile will use a URL request
* matching algorithm during execution of all variants of
@@ -137,32 +140,36 @@
* {@link android.webkit.WebView#loadUrl(String)} to display web contents
* in a WebView.
* <p>
+ * NOTE: Additional headers passed to
+ * {@link android.webkit.WebView#loadUrl(String, Map)} are not considered
+ * in the matching algorithm for determining whether or not to serve a
+ * prefetched response to a navigation.
+ * <p>
* For max latency saving benefits, it is recommended to call this method
* as early as possible (i.e. before any WebView associated with this
* profile is created).
* <p>
* Only supports HTTPS scheme.
- * <p>
- * All result callbacks will be resolved on the calling thread.
- * <p>
*
* @param url the url associated with the prefetch request.
* @param cancellationSignal will make the best effort to cancel an
* in-flight prefetch request, However cancellation is not
* guaranteed.
+ * @param callbackExecutor the executor to resolve the callback with.
* @param operationCallback callbacks for reporting result back to application.
* @throws IllegalArgumentException if the url or callback is null.
*/
@RequiresFeature(name = WebViewFeature.PROFILE_URL_PREFETCH,
enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
- @AnyThread
+ @UiThread
@ExperimentalUrlPrefetch
void prefetchUrlAsync(@NonNull String url,
@Nullable CancellationSignal cancellationSignal,
+ @NonNull Executor callbackExecutor,
@NonNull OutcomeReceiverCompat<Void, PrefetchException> operationCallback);
/**
- * Starts a URL prefetch request. Can be called from any thread.
+ * Starts a URL prefetch request. Must be called from the UI thread.
* <p>
* All WebViews associated with this Profile will use a URL request
* matching algorithm during execution of all variants of
@@ -176,29 +183,33 @@
* {@link android.webkit.WebView#loadUrl(String)} to display web contents
* in a WebView.
* <p>
+ * NOTE: Additional headers passed to
+ * {@link android.webkit.WebView#loadUrl(String, Map)} are not considered
+ * in the matching algorithm for determining whether or not to serve a
+ * prefetched response to a navigation.
+ * <p>
* For max latency saving benefits, it is recommended to call this method
* as early as possible (i.e. before any WebView associated with this
* profile is created).
* <p>
* Only supports HTTPS scheme.
- * <p>
- * All result callbacks will be resolved on the calling thread.
- * <p>
*
* @param url the url associated with the prefetch request.
* @param cancellationSignal will make the best effort to cancel an
* in-flight prefetch request, However cancellation is not
* guaranteed.
+ * @param callbackExecutor the executor to resolve the callback with.
* @param speculativeLoadingParameters parameters to customize the prefetch request.
* @param operationCallback callbacks for reporting result back to application.
* @throws IllegalArgumentException if the url or callback is null.
*/
@RequiresFeature(name = WebViewFeature.PROFILE_URL_PREFETCH,
enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
- @AnyThread
+ @UiThread
@ExperimentalUrlPrefetch
void prefetchUrlAsync(@NonNull String url,
@Nullable CancellationSignal cancellationSignal,
+ @NonNull Executor callbackExecutor,
@NonNull SpeculativeLoadingParameters speculativeLoadingParameters,
@NonNull OutcomeReceiverCompat<Void, PrefetchException> operationCallback);
@@ -210,16 +221,19 @@
* not be served to a WebView before it is cleared.
* <p>
*
- * @param url the url associated with the prefetch request.
+ * @param url the url associated with the prefetch request. Should be
+ * an exact match with the URL passed to {@link #prefetchUrlAsync}.
+ * @param callbackExecutor the executor to resolve the callback with.
* @param operationCallback runs when the clear operation is complete Or and error occurred
* during it.
* @throws IllegalArgumentException if the url or callback is null.
*/
@RequiresFeature(name = WebViewFeature.PROFILE_URL_PREFETCH,
enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
- @AnyThread
+ @UiThread
@ExperimentalUrlPrefetch
void clearPrefetchAsync(@NonNull String url,
+ @NonNull Executor callbackExecutor,
@NonNull OutcomeReceiverCompat<Void, PrefetchException> operationCallback);
}
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebStorageCompat.java b/webkit/webkit/src/main/java/androidx/webkit/WebStorageCompat.java
index d461610..2013bc0 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebStorageCompat.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebStorageCompat.java
@@ -21,7 +21,6 @@
import android.webkit.WebStorage;
import androidx.annotation.RequiresFeature;
-import androidx.annotation.RestrictTo;
import androidx.annotation.UiThread;
import androidx.webkit.internal.ApiFeature;
import androidx.webkit.internal.WebStorageAdapter;
@@ -44,10 +43,14 @@
* {@link androidx.webkit.Profile#getWebStorage()} if your app is using multiple
* WebView profiles.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public final class WebStorageCompat {
/**
+ * Class is not intended to be instantiated.
+ */
+ private WebStorageCompat() {}
+
+ /**
* Delete all data stored by websites in the given WebStorage instance.
* This includes network cache, cookies, and any JavaScript-readable storage.
* <p>
@@ -62,7 +65,6 @@
@RequiresFeature(name = WebViewFeature.DELETE_BROWSING_DATA,
enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
@UiThread
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static void deleteBrowsingData(
@NonNull WebStorage instance, @NonNull Executor executor,
@NonNull Runnable doneCallback) {
@@ -86,7 +88,6 @@
@RequiresFeature(name = WebViewFeature.DELETE_BROWSING_DATA,
enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
@UiThread
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static void deleteBrowsingData(
@NonNull WebStorage instance, @NonNull Runnable doneCallback) {
deleteBrowsingData(instance, r -> new Handler(Looper.getMainLooper()).post(r),
@@ -122,9 +123,7 @@
*/
@RequiresFeature(name = WebViewFeature.DELETE_BROWSING_DATA,
enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
-
@UiThread
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static @NonNull String deleteBrowsingDataForSite(
@NonNull WebStorage instance, @NonNull String site, @NonNull Executor executor,
@NonNull Runnable doneCallback) {
@@ -149,7 +148,6 @@
@RequiresFeature(name = WebViewFeature.DELETE_BROWSING_DATA,
enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
@UiThread
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static @NonNull String deleteBrowsingDataForSite(
@NonNull WebStorage instance, @NonNull String site, @NonNull Runnable doneCallback) {
return deleteBrowsingDataForSite(instance, site,
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java b/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
index e6d322f..24e8e30 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
@@ -19,17 +19,18 @@
import android.annotation.SuppressLint;
import android.content.Context;
import android.net.Uri;
+import android.os.CancellationSignal;
import android.os.Handler;
import android.webkit.CookieManager;
import android.webkit.ValueCallback;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebSettings;
+import android.webkit.WebStorage;
import android.webkit.WebView;
import androidx.annotation.RestrictTo;
import androidx.annotation.StringDef;
-import androidx.core.os.CancellationSignal;
import androidx.webkit.internal.WebViewFeatureInternal;
import org.jspecify.annotations.NonNull;
@@ -49,9 +50,11 @@
*/
public class WebViewFeature {
- private WebViewFeature() {}
+ private WebViewFeature() {
+ }
/**
+ *
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@StringDef(value = {
@@ -118,9 +121,11 @@
})
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.PARAMETER, ElementType.METHOD})
- public @interface WebViewSupportFeature {}
+ public @interface WebViewSupportFeature {
+ }
/**
+ *
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@StringDef(value = {
@@ -130,7 +135,8 @@
})
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.PARAMETER, ElementType.METHOD})
- public @interface WebViewStartupFeature {}
+ public @interface WebViewStartupFeature {
+ }
/**
* Feature for {@link #isFeatureSupported(String)}.
@@ -138,7 +144,7 @@
* {@link androidx.webkit.WebViewCompat#postVisualStateCallback(android.webkit.WebView, long,
* WebViewCompat.VisualStateCallback)}, and {@link
* WebViewClientCompat#onPageCommitVisible(
- * android.webkit.WebView, String)}.
+ *android.webkit.WebView, String)}.
*/
public static final String VISUAL_STATE_CALLBACK = "VISUAL_STATE_CALLBACK";
@@ -373,7 +379,7 @@
* Feature for {@link #isFeatureSupported(String)}.
* This feature covers
* {@link androidx.webkit.WebMessagePortCompat#setWebMessageCallback(
- * WebMessagePortCompat.WebMessageCallbackCompat)}, and
+ *WebMessagePortCompat.WebMessageCallbackCompat)}, and
* {@link androidx.webkit.WebMessagePortCompat#setWebMessageCallback(Handler,
* WebMessagePortCompat.WebMessageCallbackCompat)}.
*/
@@ -428,7 +434,7 @@
public static final String WEB_VIEW_RENDERER_TERMINATE = "WEB_VIEW_RENDERER_TERMINATE";
/**
- i* Feature for {@link #isFeatureSupported(String)}.
+ * Feature for {@link #isFeatureSupported(String)}.
* This feature covers
* {@link androidx.webkit.WebViewCompat#getWebViewRenderProcessClient(WebView)},
* {@link androidx.webkit.WebViewCompat#setWebViewRenderProcessClient(WebView, WebViewRenderProcessClient)},
@@ -507,7 +513,6 @@
* This feature covers
* {@link WebSettingsCompat#setEnterpriseAuthenticationAppLinkPolicyEnabled(WebSettings, boolean)}and
* {@link WebSettingsCompat#getEnterpriseAuthenticationAppLinkPolicyEnabled(WebSettings)}.
- *
*/
public static final String ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY =
"ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY";
@@ -540,17 +545,16 @@
* This feature covers
* {@link androidx.webkit.ProcessGlobalConfig#setDirectoryBasePaths(Context, File, File)}
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static final String STARTUP_FEATURE_CONFIGURE_PARTITIONED_COOKIES =
"STARTUP_FEATURE_CONFIGURE_PARTITIONED_COOKIES";
/**
* Feature for {@link #isFeatureSupported(String)}.
* This feature covers
- * {@link androidx.webkit.WebSettingsCompat#getRequestedWithHeaderOriginAllowList(WebSettings)],
- * {@link androidx.webkit.WebSettingsCompat#setRequestedWithHeaderAllowList(WebSettings, Set)},
- * {@link androidx.webkit.ServiceWorkerWebSettingsCompat#getRequestedWithHeaderAllowList(WebSettings)}, and
- * {@link androidx.webkit.ServiceWorkerWebSettingsCompat#setRequestedWithHeaderAllowList(WebSettings, Set)}.
+ * {@link androidx.webkit.WebSettingsCompat#getRequestedWithHeaderOriginAllowList(WebSettings)},
+ * {@link androidx.webkit.WebSettingsCompat#setRequestedWithHeaderOriginAllowList(WebSettings, Set)},
+ * {@link androidx.webkit.ServiceWorkerWebSettingsCompat#getRequestedWithHeaderOriginAllowList()},
+ * {@link androidx.webkit.ServiceWorkerWebSettingsCompat#setRequestedWithHeaderOriginAllowList(Set)}
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static final String REQUESTED_WITH_HEADER_ALLOW_LIST =
@@ -634,12 +638,12 @@
/**
* Feature for {@link #isFeatureSupported(String)}.
* This feature covers
- * {@link androidx.webkit.Profile#prefetchUrlAsync(String, CancellationSignal, SpeculativeLoadingParameters, OutcomeReceiverCompat)}
- * {@link androidx.webkit.Profile#prefetchUrlAsync(String, CancellationSignal, OutcomeReceiverCompat)}
- * {@link androidx.webkit.Profile#clearPrefetchAsync(String, OutcomeReceiverCompat)}
+ * {@link androidx.webkit.Profile#prefetchUrlAsync(String, android.os.CancellationSignal, Executor, SpeculativeLoadingParameters, OutcomeReceiverCompat)}
+ * {@link androidx.webkit.Profile#prefetchUrlAsync(String, CancellationSignal, Executor, OutcomeReceiverCompat)}
+ * {@link androidx.webkit.Profile#clearPrefetchAsync(String, Executor, OutcomeReceiverCompat)}
*/
@Profile.ExperimentalUrlPrefetch
- public static final String PROFILE_URL_PREFETCH = "PREFETCH_URL_V2";
+ public static final String PROFILE_URL_PREFETCH = "PREFETCH_URL_V3";
/**
* Feature for {@link #isFeatureSupported(String)}.
@@ -648,7 +652,14 @@
*/
public static final String DEFAULT_TRAFFICSTATS_TAGGING = "DEFAULT_TRAFFICSTATS_TAGGING";
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ /**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link androidx.webkit.WebStorageCompat#deleteBrowsingData(WebStorage, Executor, Runnable)},
+ * {@link androidx.webkit.WebStorageCompat#deleteBrowsingData(WebStorage, Runnable)},
+ * {@link androidx.webkit.WebStorageCompat#deleteBrowsingDataForSite(WebStorage, String, Executor, Runnable)},
+ * {@link androidx.webkit.WebStorageCompat#deleteBrowsingDataForSite(WebStorage, String, Runnable)}
+ */
public static final String DELETE_BROWSING_DATA = "DELETE_BROWSING_DATA";
/**
@@ -657,9 +668,9 @@
* device, and the WebView APK on the device.
*
* <p class="note"><b>Note:</b> This method is different from
- * {@link WebViewFeature#isStartupFeatureSupported(Context, String)} and this method only accepts
- * certain features. Please verify that the correct feature checking method is used for a
- * particular feature.
+ * {@link WebViewFeature#isStartupFeatureSupported(Context, String)} and this method only
+ * accepts certain features. Please verify that the correct feature checking method is used for
+ * a particular feature.
*
* <p class="note"><b>Note:</b> If this method returns {@code false}, it is not safe to invoke
* the methods requiring the desired feature. Furthermore, if this method returns {@code false}
@@ -686,7 +697,7 @@
* the methods requiring the desired feature. Furthermore, if this method returns {@code false}
* for a particular feature, any callback guarded by that feature will not be invoked.
*
- * @param context a Context to access application assets This value cannot be null.
+ * @param context a Context to access application assets This value cannot be null.
* @param startupFeature the startup feature to be checked
* @return whether the feature is supported given the current platform SDK and WebView version
*/
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/ProfileImpl.java b/webkit/webkit/src/main/java/androidx/webkit/internal/ProfileImpl.java
index 6b84532..dd61055 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/ProfileImpl.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/ProfileImpl.java
@@ -16,21 +16,25 @@
package androidx.webkit.internal;
+import android.os.CancellationSignal;
import android.webkit.CookieManager;
import android.webkit.GeolocationPermissions;
import android.webkit.ServiceWorkerController;
import android.webkit.WebStorage;
-import androidx.core.os.CancellationSignal;
import androidx.webkit.OutcomeReceiverCompat;
import androidx.webkit.PrefetchException;
import androidx.webkit.Profile;
import androidx.webkit.SpeculativeLoadingParameters;
import org.chromium.support_lib_boundary.ProfileBoundaryInterface;
+import org.chromium.support_lib_boundary.util.BoundaryInterfaceReflectionUtil;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
+import java.lang.reflect.InvocationHandler;
+import java.util.concurrent.Executor;
+
/**
* Internal implementation of Profile.
@@ -103,19 +107,49 @@
@Override
public void prefetchUrlAsync(@NonNull String url,
@Nullable CancellationSignal cancellationSignal,
+ @NonNull Executor callbackExecutor,
@NonNull SpeculativeLoadingParameters params,
@NonNull OutcomeReceiverCompat<Void, PrefetchException> callback) {
+ ApiFeature.NoFramework feature = WebViewFeatureInternal.PROFILE_URL_PREFETCH;
+ if (feature.isSupportedByWebView()) {
+ InvocationHandler paramsBoundaryInterface =
+ BoundaryInterfaceReflectionUtil.createInvocationHandlerFor(
+ new SpeculativeLoadingParametersAdapter(params));
+
+ mProfileImpl.prefetchUrl(url, cancellationSignal, callbackExecutor,
+ paramsBoundaryInterface,
+ PrefetchOperationCallbackAdapter.buildInvocationHandler(callback));
+
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
}
@Override
public void prefetchUrlAsync(@NonNull String url,
@Nullable CancellationSignal cancellationSignal,
+ @NonNull Executor callbackExecutor,
@NonNull OutcomeReceiverCompat<Void, PrefetchException> callback) {
+ ApiFeature.NoFramework feature = WebViewFeatureInternal.PROFILE_URL_PREFETCH;
+ if (feature.isSupportedByWebView()) {
+ mProfileImpl.prefetchUrl(url, cancellationSignal, callbackExecutor,
+ PrefetchOperationCallbackAdapter.buildInvocationHandler(callback));
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
}
@Override
public void clearPrefetchAsync(@NonNull String url,
+ @NonNull Executor callbackExecutor,
@NonNull OutcomeReceiverCompat<Void, PrefetchException> callback) {
+ ApiFeature.NoFramework feature = WebViewFeatureInternal.PROFILE_URL_PREFETCH;
+ if (feature.isSupportedByWebView()) {
+ mProfileImpl.clearPrefetch(url, callbackExecutor,
+ PrefetchOperationCallbackAdapter.buildInvocationHandler(callback));
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
}
}
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/StartupFeatures.java b/webkit/webkit/src/main/java/androidx/webkit/internal/StartupFeatures.java
index a847af2..54d505c 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/StartupFeatures.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/StartupFeatures.java
@@ -16,8 +16,6 @@
package androidx.webkit.internal;
-import androidx.annotation.RestrictTo;
-
/**
* Class containing all the startup features the AndroidX library can support.
* The constants defined in this file should not be modified as their value is used in the
@@ -37,7 +35,6 @@
"STARTUP_FEATURE_SET_DIRECTORY_BASE_PATH";
// ProcessGlobalConfig#setPartitionedCookiesEnabled(boolean)
- @RestrictTo(RestrictTo.Scope.LIBRARY)
public static final String STARTUP_FEATURE_CONFIGURE_PARTITIONED_COOKIES =
"STARTUP_FEATURE_CONFIGURE_PARTITIONED_COOKIES";
}
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
index e61807c..efde9bc 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
@@ -20,6 +20,7 @@
import android.content.pm.PackageInfo;
import android.net.Uri;
import android.os.Build;
+import android.os.CancellationSignal;
import android.os.Handler;
import android.webkit.ValueCallback;
import android.webkit.WebResourceRequest;
@@ -29,7 +30,6 @@
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
-import androidx.core.os.CancellationSignal;
import androidx.webkit.OutcomeReceiverCompat;
import androidx.webkit.Profile;
import androidx.webkit.ProfileStore;
@@ -650,8 +650,9 @@
/**
* Feature for {@link WebViewFeature#isFeatureSupported(String)}.
* This feature covers
- * {@link androidx.webkit.Profile#prefetchUrlAsync(String, CancellationSignal, SpeculativeLoadingParameters, OutcomeReceiverCompat)}
- * {@link androidx.webkit.Profile#clearPrefetchAsync(String, OutcomeReceiverCompat)}
+ * {@link androidx.webkit.Profile#prefetchUrlAsync(String, CancellationSignal, Executor, SpeculativeLoadingParameters, OutcomeReceiverCompat)}
+ * {@link androidx.webkit.Profile#prefetchUrlAsync(String, CancellationSignal, Executor, OutcomeReceiverCompat)}
+ * {@link androidx.webkit.Profile#clearPrefetchAsync(String, Executor, OutcomeReceiverCompat)}
*/
public static final ApiFeature.NoFramework PROFILE_URL_PREFETCH =
new ApiFeature.NoFramework(WebViewFeature.PROFILE_URL_PREFETCH,