Merge "Revert "Create a FakeSurfaceEffect for testing."" into androidx-main
diff --git a/activity/activity-compose/build.gradle b/activity/activity-compose/build.gradle
index 43b6a5a..d51c200 100644
--- a/activity/activity-compose/build.gradle
+++ b/activity/activity-compose/build.gradle
@@ -36,7 +36,7 @@
// Outside of androidx this is resolved via constraint added to lifecycle-common,
// but it doesn't work in androidx.
// See aosp/1804059
- implementation("androidx.lifecycle:lifecycle-common-java8:2.5.0")
+ implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
androidTestImplementation projectOrArtifact(":compose:ui:ui-test-junit4")
androidTestImplementation projectOrArtifact(":compose:material:material")
diff --git a/activity/activity-ktx/build.gradle b/activity/activity-ktx/build.gradle
index 71da6fa..b28eac7 100644
--- a/activity/activity-ktx/build.gradle
+++ b/activity/activity-ktx/build.gradle
@@ -33,10 +33,10 @@
api("androidx.core:core-ktx:1.1.0") {
because "Mirror activity dependency graph for -ktx artifacts"
}
- api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.0") {
+ api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1") {
because 'Mirror activity dependency graph for -ktx artifacts'
}
- api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0")
+ api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
api("androidx.savedstate:savedstate-ktx:1.2.0") {
because 'Mirror activity dependency graph for -ktx artifacts'
}
diff --git a/activity/activity/build.gradle b/activity/activity/build.gradle
index b2b7705..d086cea 100644
--- a/activity/activity/build.gradle
+++ b/activity/activity/build.gradle
@@ -23,10 +23,10 @@
api("androidx.annotation:annotation:1.1.0")
implementation("androidx.collection:collection:1.0.0")
api("androidx.core:core:1.8.0")
- api("androidx.lifecycle:lifecycle-runtime:2.5.0")
- api("androidx.lifecycle:lifecycle-viewmodel:2.5.0")
+ api("androidx.lifecycle:lifecycle-runtime:2.5.1")
+ api("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
api("androidx.savedstate:savedstate:1.2.0")
- api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.0")
+ api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1")
implementation("androidx.tracing:tracing:1.0.0")
api(libs.kotlinStdlib)
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt
index 3e7fefd..0cf6486 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt
@@ -90,7 +90,7 @@
}
@Test
- fun noActivityAvailableTest() {
+ fun noActivityAvailableLifecycleTest() {
ActivityScenario.launch(RegisterInInitActivity::class.java).use { scenario ->
var exceptionThrown = false
scenario.withActivity {
@@ -107,6 +107,25 @@
}
}
}
+
+ @Test
+ fun noActivityAvailableNoLifecycleTest() {
+ ActivityScenario.launch(RegisterInInitActivity::class.java).use { scenario ->
+ var exceptionThrown = false
+ scenario.withActivity {
+ try {
+ launcherNoLifecycle.launch(Intent("no action"))
+ } catch (e: ActivityNotFoundException) {
+ exceptionThrown = true
+ }
+ }
+
+ scenario.withActivity {
+ assertThat(exceptionThrown).isTrue()
+ assertThat(launchCount).isEqualTo(0)
+ }
+ }
+ }
}
class PassThroughActivity : ComponentActivity() {
@@ -175,12 +194,16 @@
class RegisterInInitActivity : ComponentActivity() {
var launcher: ActivityResultLauncher<Intent>
+ val launcherNoLifecycle: ActivityResultLauncher<Intent>
var launchCount = 0
init {
launcher = registerForActivityResult(StartActivityForResult()) {
launchCount++
}
+ launcherNoLifecycle = activityResultRegistry.register("test", StartActivityForResult()) {
+ launchCount++
+ }
}
}
diff --git a/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.java b/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.java
index 1a15c7e..7333e3e 100644
--- a/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.java
+++ b/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.java
@@ -242,7 +242,12 @@
+ "before calling launch().");
}
mLaunchedKeys.add(key);
- onLaunch(innerCode, contract, input, options);
+ try {
+ onLaunch(innerCode, contract, input, options);
+ } catch (Exception e) {
+ mLaunchedKeys.remove(key);
+ throw e;
+ }
}
@Override
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
index 9dc5995..c3e83b6 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
@@ -33,9 +33,12 @@
import androidx.tracing.perfetto.PerfettoHandshake.ResponseExitCodes.RESULT_CODE_ALREADY_ENABLED
import androidx.tracing.perfetto.PerfettoHandshake.ResponseExitCodes.RESULT_CODE_ERROR_BINARY_MISSING
import androidx.tracing.perfetto.PerfettoHandshake.ResponseExitCodes.RESULT_CODE_SUCCESS
+import androidx.tracing.perfetto.PerfettoHandshake.ResponseExitCodes.RESULT_CODE_CANCELLED
+import androidx.tracing.perfetto.PerfettoHandshake.ResponseExitCodes.RESULT_CODE_ERROR_OTHER
import com.google.common.truth.Truth.assertThat
import java.io.File
import java.io.StringReader
+import java.util.regex.Pattern
import org.junit.After
import org.junit.Assume.assumeTrue
import org.junit.Before
@@ -251,6 +254,70 @@
}
}
+ @Test
+ fun test_handshake_package_does_not_exist() {
+ assumeTrue(isAbiSupported())
+ assumeTrue(Build.VERSION.SDK_INT >= minSupportedSdk)
+
+ val response = perfettoCapture.enableAndroidxTracingPerfetto(
+ "package.does.not.exist.89e51176_bc28_41f1_ac73_ca717454b517",
+ shouldProvideBinaries(testConfig.sdkDelivery)
+ )
+
+ assertThat(response).ignoringCase()
+ .contains("The broadcast to enable tracing was not received")
+ }
+
+ /**
+ * Unlike [test_handshake_package_does_not_exist], which uses [PerfettoCapture], this test
+ * uses a lower-level component [PerfettoHandshake].
+ */
+ @Test
+ fun test_handshake_framework_package_does_not_exist() {
+ assumeTrue(isAbiSupported())
+ assumeTrue(Build.VERSION.SDK_INT >= minSupportedSdk)
+
+ val handshake = PerfettoHandshake(
+ "package.does.not.exist.89e51176_bc28_41f1_ac73_ca717454b517",
+ parseJsonMap = { emptyMap() },
+ Shell::executeCommand
+ )
+
+ // try
+ handshake.enableTracing(null).also { response ->
+ assertThat(response.exitCode).isEqualTo(RESULT_CODE_CANCELLED)
+ assertThat(response.requiredVersion).isNull()
+ }
+
+ // try again
+ handshake.enableTracing(null).also { response ->
+ assertThat(response.exitCode).isEqualTo(RESULT_CODE_CANCELLED)
+ assertThat(response.requiredVersion).isNull()
+ }
+ }
+
+ @Test
+ fun test_handshake_framework_parsing_error() {
+ assumeTrue(isAbiSupported())
+ assumeTrue(Build.VERSION.SDK_INT >= minSupportedSdk)
+
+ val parsingException = "I don't know how to JSON"
+ val handshake = PerfettoHandshake(
+ targetPackage,
+ parseJsonMap = { throw IllegalArgumentException(parsingException) },
+ Shell::executeCommand
+ )
+
+ handshake.enableTracing(null).also { response ->
+ assertThat(response.exitCode).isEqualTo(RESULT_CODE_ERROR_OTHER)
+ assertThat(response.requiredVersion).isNull()
+ assertThat(response.message).containsMatch(
+ "Exception occurred while trying to parse a response.*Error.*$parsingException"
+ .toPattern(Pattern.CASE_INSENSITIVE)
+ )
+ }
+ }
+
private fun enablePackage() {
scope.pressHome()
scope.startActivityAndWait()
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index 89a23c4..cd7042e 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -301,6 +301,7 @@
project.tasks.withType(KotlinCompile::class.java).configureEach { task ->
// Workaround for https://youtrack.jetbrains.com/issue/KT-37652
if (task.name.endsWith("TestKotlin")) return@configureEach
+ if (task.name.endsWith("TestKotlinJvm")) return@configureEach
task.kotlinOptions.freeCompilerArgs += listOf("-Xexplicit-api=strict")
}
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXPlaygroundRootImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXPlaygroundRootImplPlugin.kt
index f4934f1..39af336 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXPlaygroundRootImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXPlaygroundRootImplPlugin.kt
@@ -16,10 +16,7 @@
package androidx.build
-import androidx.build.dependencyTracker.DependencyTracker
-import androidx.build.dependencyTracker.ProjectGraph
import androidx.build.gradle.isRoot
-import androidx.build.playground.FindAffectedModulesTask
import groovy.xml.DOMBuilder
import org.gradle.api.GradleException
import org.gradle.api.Plugin
@@ -63,14 +60,6 @@
rootProject.subprojects {
configureSubProject(it)
}
-
- rootProject.tasks.register(
- "findAffectedModules",
- FindAffectedModulesTask::class.java
- ) { task ->
- task.projectGraph = ProjectGraph(rootProject)
- task.dependencyTracker = DependencyTracker(rootProject, task.logger)
- }
}
private fun configureSubProject(project: Project) {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
index 8a43bf6..bfc7189 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
@@ -39,7 +39,7 @@
const val ERROR_PRONE_TASK = "runErrorProne"
private const val ERROR_PRONE_CONFIGURATION = "errorprone"
-private const val ERROR_PRONE_VERSION = "com.google.errorprone:error_prone_core:2.4.0"
+private const val ERROR_PRONE_VERSION = "com.google.errorprone:error_prone_core:2.14.0"
private val log = Logging.getLogger("ErrorProneConfiguration")
fun Project.configureErrorProneForJava() {
@@ -128,6 +128,32 @@
"-XepExcludedPaths:.*/(build/generated|build/errorProne|external)/.*",
+ // Consider re-enabling the following checks. Disabled as part of
+ // error-prone upgrade
+ "-Xep:InlineMeSuggester:OFF",
+ "-Xep:UnusedVariable:OFF",
+ "-Xep:UnusedMethod:OFF",
+ "-Xep:NarrowCalculation:OFF",
+ "-Xep:LongDoubleConversion:OFF",
+ "-Xep:UnicodeEscape:OFF",
+ "-Xep:JavaUtilDate:OFF",
+ "-Xep:UnrecognisedJavadocTag:OFF",
+ "-Xep:ObjectEqualsForPrimitives:OFF",
+ "-Xep:UnnecessaryParentheses:OFF",
+ "-Xep:DoNotCallSuggester:OFF",
+ "-Xep:EqualsNull:OFF",
+ "-Xep:MalformedInlineTag:OFF",
+ "-Xep:MissingSuperCall:OFF",
+ "-Xep:ToStringReturnsNull:OFF",
+ "-Xep:ReturnValueIgnored:OFF",
+ "-Xep:MissingImplementsComparable:OFF",
+ "-Xep:EmptyTopLevelDeclaration:OFF",
+ "-Xep:InvalidThrowsLink:OFF",
+ "-Xep:StaticAssignmentOfThrowable:OFF",
+ "-Xep:DoNotClaimAnnotations:OFF",
+ "-Xep:AlreadyChecked:OFF",
+ "-Xep:StringSplitter:OFF",
+
// We allow inter library RestrictTo usage.
"-Xep:RestrictTo:OFF",
@@ -173,13 +199,13 @@
"-Xep:MissingFail:ERROR",
"-Xep:JavaLangClash:ERROR",
"-Xep:TypeParameterUnusedInFormals:ERROR",
- "-Xep:StringSplitter:ERROR",
+ // "-Xep:StringSplitter:ERROR", // disabled with upgrade to 2.14.0
"-Xep:ReferenceEquality:ERROR",
"-Xep:AssertionFailureIgnored:ERROR",
- "-Xep:UnnecessaryParentheses:ERROR",
+ // "-Xep:UnnecessaryParentheses:ERROR", // disabled with upgrade to 2.14.0
"-Xep:EqualsGetClass:ERROR",
- "-Xep:UnusedVariable:ERROR",
- "-Xep:UnusedMethod:ERROR",
+ // "-Xep:UnusedVariable:ERROR", // disabled with upgrade to 2.14.0
+ // "-Xep:UnusedMethod:ERROR", // disabled with upgrade to 2.14.0
"-Xep:UndefinedEquals:ERROR",
"-Xep:ThreadLocalUsage:ERROR",
"-Xep:FutureReturnValueIgnored:ERROR",
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
index e51bc8b..6ae1586 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
@@ -101,9 +101,9 @@
private fun computeArguments(): File {
// path comes with colons but dokka json expects an ArrayList
- val classPath = dependenciesClasspath.asPath.split(':').toMutableList<String>()
+ val classPath = dependenciesClasspath.asPath.split(':').toMutableList()
- var linksConfiguration = ""
+ val linksConfiguration = ""
val linksMap = mapOf(
"coroutinesCore"
to "https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core",
@@ -138,8 +138,8 @@
)
@Suppress("UNCHECKED_CAST")
if (includes.isNotEmpty())
- ((jsonMap["sourceSets"]as List<*>).single() as MutableMap<String, Any>)
- .put("includes", includes)
+ ((jsonMap["sourceSets"]as List<*>).single() as MutableMap<String, Any>)["includes"] =
+ includes
val json = JSONObject(jsonMap)
val outputFile = File.createTempFile("dackkaArgs", ".json")
@@ -163,7 +163,6 @@
}
}
-@Suppress("UnstableApiUsage")
interface DackkaParams : WorkParameters {
val args: ListProperty<String>
val classpath: SetProperty<File>
@@ -174,7 +173,6 @@
var showLibraryMetadata: Boolean
}
-@Suppress("UnstableApiUsage")
fun runDackkaWithArgs(
classpath: FileCollection,
argsFile: File,
@@ -187,7 +185,7 @@
) {
val workQueue = workerExecutor.noIsolation()
workQueue.submit(DackkaWorkAction::class.java) { parameters ->
- parameters.args.set(listOf(argsFile.getPath(), "-loggingLevel", "WARN"))
+ parameters.args.set(listOf(argsFile.path, "-loggingLevel", "WARN"))
parameters.classpath.set(classpath)
parameters.excludedPackages.set(excludedPackages)
parameters.excludedPackagesForJava.set(excludedPackagesForJava)
@@ -197,7 +195,6 @@
}
}
-@Suppress("UnstableApiUsage")
abstract class DackkaWorkAction @Inject constructor(
private val execOperations: ExecOperations
) : WorkAction<DackkaParams> {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
index d6224bc..b548a83 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
@@ -180,8 +180,8 @@
val localVar = archiveOperations
task.from(
sources.elements.map { jars ->
- jars.map {
- localVar.zipTree(it).matching {
+ jars.map { jar ->
+ localVar.zipTree(jar).matching {
// Filter out files that documentation tools cannot process.
it.exclude("**/*.MF")
it.exclude("**/*.aidl")
@@ -427,9 +427,9 @@
task.dependsOn(unzipSamplesTask)
val androidJar = project.getAndroidJar()
- val dokkaClasspath = project.provider({
+ val dokkaClasspath = project.provider {
project.files(androidJar).plus(dependencyClasspath)
- })
+ }
// DokkaTask tries to resolve DokkaTask#classpath right away for jars that might not
// be there yet. Delay the setting of this property to before we run the task.
task.inputs.files(androidJar, dependencyClasspath)
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/playground/FindAffectedModulesTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/playground/FindAffectedModulesTask.kt
deleted file mode 100644
index 77ce6aa..0000000
--- a/buildSrc/private/src/main/kotlin/androidx/build/playground/FindAffectedModulesTask.kt
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.build.playground
-
-import androidx.build.dependencyTracker.AffectedModuleDetectorImpl
-import androidx.build.dependencyTracker.DependencyTracker
-import androidx.build.dependencyTracker.ProjectGraph
-import org.gradle.api.DefaultTask
-import org.gradle.api.tasks.Input
-import org.gradle.api.tasks.OutputFile
-import org.gradle.api.tasks.TaskAction
-import org.gradle.api.tasks.options.Option
-import org.gradle.work.DisableCachingByDefault
-import java.io.File
-
-/**
- * A task to print the list of affected modules based on given parameters.
- *
- * The list of changed files can be passed via [changedFiles] property and the list of module
- * paths will be written to the given [outputFilePath].
- *
- * This task is specialized for Playground projects where any change in .github or
- * playground-common will be considered as an `INFRA` change and will be listed in the outputs.
- */
-@DisableCachingByDefault(because = "Fast to run, and declaring all inputs is difficult")
-abstract class FindAffectedModulesTask : DefaultTask() {
- @get:Input
- @set:Option(
- option = "changedFilePath",
- description = "Changed file in the build (including removed files). Can be passed " +
- "multiple times, e.g.: --changedFilePath=a.kt --changedFilePath=b.kt " +
- "File paths must be relative to the root directory of the main checkout"
- )
- abstract var changedFiles: List<String>
-
- @get:Input
- @set:Option(
- option = "outputFilePath",
- description = """
- The output file path which will contain the list of project paths (line separated) that
- are affected by the given list of changed files. It might also include "$INFRA_CHANGE"
- if the change affects any of the common playground build files outside the project.
- """
- )
- abstract var outputFilePath: String
-
- @get:Input
- abstract var projectGraph: ProjectGraph
-
- @get:Input
- abstract var dependencyTracker: DependencyTracker
-
- @get:OutputFile
- val outputFile by lazy {
- File(outputFilePath)
- }
-
- init {
- group = "Tooling"
- description = """
- Outputs the list of projects in the playground project that are affected by the
- given list of files.
- ./gradlew findAffectedModules --changedFilePath=file1 --changedFilePath=file2 \
- --outputFilePath=`pwd`/changes.txt
- """.trimIndent()
- }
-
- @TaskAction
- fun checkAffectedModules() {
- val hasChangedGithubInfraFiles = changedFiles.any {
- it.contains(".github") ||
- it.contains("playground-common") ||
- it.contains("buildSrc")
- }
- val detector = AffectedModuleDetectorImpl(
- projectGraph = projectGraph,
- dependencyTracker = dependencyTracker,
- logger = logger,
- cobuiltTestPaths = setOf<Set<String>>(),
- changedFilesProvider = {
- changedFiles
- }
- )
- val changedProjectPaths = detector.affectedProjects.map {
- it
- } + if (hasChangedGithubInfraFiles) {
- listOf(INFRA_CHANGE)
- } else {
- emptyList()
- }
- check(outputFile.parentFile?.exists() == true) {
- "invalid output file argument: $outputFile. Make sure to pass an absolute path"
- }
- val changedProjects = changedProjectPaths.joinToString(System.lineSeparator())
- outputFile.writeText(changedProjects, charset = Charsets.UTF_8)
- logger.info("putting result $changedProjects into ${outputFile.absolutePath}")
- }
-
- companion object {
- /**
- * Denotes that the changes affect common playground build files / configuration.
- */
- const val INFRA_CHANGE = "INFRA"
- }
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index 77ea567..21dc7a8 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -1915,7 +1915,7 @@
* A rotation of 90 degrees would mean rotating the image 90 degrees clockwise produces an
* image that will match the display orientation.
*
- * <p>See also {@link Builder#setTargetRotation(int)} and
+ * <p>See also {@link ImageCapture.Builder#setTargetRotation(int)} and
* {@link #setTargetRotation(int)}.
*
* <p>Timestamps are in nanoseconds and monotonic and can be compared to timestamps from
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/TagBundle.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/TagBundle.java
index ae4ae00..44e2abd 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/TagBundle.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/TagBundle.java
@@ -16,6 +16,7 @@
package androidx.camera.core.impl;
+import android.hardware.camera2.CaptureRequest;
import android.util.ArrayMap;
import android.util.Pair;
@@ -40,6 +41,9 @@
private static final TagBundle EMPTY_TAGBUNDLE = new TagBundle(new ArrayMap<>());
+ private static final String USER_TAG_PREFIX = "android.hardware.camera2.CaptureRequest.setTag.";
+
+ private static final String CAMERAX_USER_TAG_PREFIX = USER_TAG_PREFIX + "CX";
/**
* Creates an empty TagBundle.
*
@@ -101,4 +105,24 @@
public Set<String> listKeys() {
return mTagMap.keySet();
}
+
+ /**
+ * Produces a string that can be used to identify CameraX usage in a Camera2
+ * {@link CaptureRequest}.
+ *
+ * <p>In Android 13 or later, Camera2 will log the string representation of any
+ * tag set on {@link CaptureRequest.Builder#setTag(Object)}. Since
+ * tag bundles are always set internally by CameraX as the tag in a capture
+ * request, the constant string value returned here can be used to identify
+ * usage of CameraX versus application usage of Camera2.
+ *
+ * <p>Note: Doesn't return an actual string representation of the tag bundle.
+ *
+ * @return Returns a constant string value used to identify usage of CameraX.
+ */
+ @NonNull
+ @Override
+ public final String toString() {
+ return CAMERAX_USER_TAG_PREFIX;
+ }
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/ShaderProvider.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/ShaderProvider.java
index e204317..3edf244 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/ShaderProvider.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/ShaderProvider.java
@@ -18,9 +18,15 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
-/** A provider that supplies OpenGL shader code. */
-interface ShaderProvider {
+/**
+ * A provider that supplies OpenGL shader code.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface ShaderProvider {
/**
* Creates the fragment shader code with the given variable names.
@@ -34,14 +40,15 @@
* varying vec2 {$fragCoordsVarName};
* void main() {
* vec4 sampleColor = texture2D({$samplerVarName}, {$fragCoordsVarName});
- * gl_FragColor = vec4(sampleColor.r * 0.493 + sampleColor. g * 0.769 +
- * sampleColor.b * 0.289, sampleColor.r * 0.449 + sampleColor.g * 0.686 +
- * sampleColor.b * 0.268, sampleColor.r * 0.272 + sampleColor.g * 0.534 +
- * sampleColor.b * 0.131, 1.0);
+ * gl_FragColor = vec4(
+ * sampleColor.r * 0.5 + sampleColor.g * 0.8 + sampleColor.b * 0.3,
+ * sampleColor.r * 0.4 + sampleColor.g * 0.7 + sampleColor.b * 0.2,
+ * sampleColor.r * 0.3 + sampleColor.g * 0.5 + sampleColor.b * 0.1,
+ * 1.0);
* }
* }</pre>
*
- * @param samplerVarName the variable name of the samplerExternalOES.
+ * @param samplerVarName the variable name of the samplerExternalOES.
* @param fragCoordsVarName the variable name of the fragment coordinates.
* @return the shader code. Return null to use the default shader.
*/
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/TagBundleTest.java b/camera/camera-core/src/test/java/androidx/camera/core/impl/TagBundleTest.java
index 37ea1bf..1896e6c 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/impl/TagBundleTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/TagBundleTest.java
@@ -26,8 +26,6 @@
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.internal.DoNotInstrument;
-import java.util.ArrayList;
-import java.util.List;
import java.util.Set;
@RunWith(RobolectricTestRunner.class)
@@ -44,13 +42,6 @@
private static final Integer TAG_VALUE_2 = 2;
TagBundle mTagBundle;
- private static final List<String> KEY_LIST = new ArrayList<>();
-
- static {
- KEY_LIST.add(TAG_0);
- KEY_LIST.add(TAG_1);
- KEY_LIST.add(TAG_2);
- }
@Before
public void setUp() {
@@ -84,4 +75,10 @@
assertThat(keyList).containsExactly(TAG_0, TAG_1, TAG_2);
}
+
+ @Test
+ public void verifyTagBundleToString() {
+ assertThat(mTagBundle.toString()).startsWith("android.hardware.camera2.CaptureRequest"
+ + ".setTag.CX");
+ }
}
diff --git a/camera/camera-extensions/lint-baseline.xml b/camera/camera-extensions/lint-baseline.xml
index 382b9d2..eb45276 100644
--- a/camera/camera-extensions/lint-baseline.xml
+++ b/camera/camera-extensions/lint-baseline.xml
@@ -4,51 +4,6 @@
<issue
id="UnsafeOptInUsageError"
message="This declaration is opt-in and its usage should be marked with
'@androidx.camera.camera2.interop.ExperimentalCamera2Interop' or '@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)'"
- errorLine1=" Camera2ImplConfig.Builder camera2ConfigurationBuilder = new Camera2ImplConfig.Builder();"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java"/>
- </issue>
-
- <issue
- id="UnsafeOptInUsageError"
- message="This declaration is opt-in and its usage should be marked with
'@androidx.camera.camera2.interop.ExperimentalCamera2Interop' or '@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)'"
- errorLine1=" camera2ConfigurationBuilder.setCaptureRequestOption(captureParameter.first,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java"/>
- </issue>
-
- <issue
- id="UnsafeOptInUsageError"
- message="This declaration is opt-in and its usage should be marked with
'@androidx.camera.camera2.interop.ExperimentalCamera2Interop' or '@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)'"
- errorLine1=" camera2ConfigurationBuilder.setCaptureRequestOption(captureParameter.first,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java"/>
- </issue>
-
- <issue
- id="UnsafeOptInUsageError"
- message="This declaration is opt-in and its usage should be marked with
'@androidx.camera.camera2.interop.ExperimentalCamera2Interop' or '@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)'"
- errorLine1=" captureParameter.second);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java"/>
- </issue>
-
- <issue
- id="UnsafeOptInUsageError"
- message="This declaration is opt-in and its usage should be marked with
'@androidx.camera.camera2.interop.ExperimentalCamera2Interop' or '@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)'"
- errorLine1=" captureConfigBuilder.addImplementationOptions(camera2ConfigurationBuilder.build());"
- errorLine2=" ~~~~~">
- <location
- file="src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java"/>
- </issue>
-
- <issue
- id="UnsafeOptInUsageError"
- message="This declaration is opt-in and its usage should be marked with
'@androidx.camera.camera2.interop.ExperimentalCamera2Interop' or '@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)'"
errorLine1=" CaptureRequestOptions.Builder.from(parameters).build();"
errorLine2=" ~~~~">
<location
@@ -124,42 +79,6 @@
errorLine1=" new Camera2ImplConfig.Extender<>(builder).setCameraEventCallback("
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
- file="src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java"/>
- </issue>
-
- <issue
- id="UnsafeOptInUsageError"
- message="This declaration is opt-in and its usage should be marked with
'@androidx.camera.camera2.interop.ExperimentalCamera2Interop' or '@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)'"
- errorLine1=" new Camera2ImplConfig.Extender<>(builder).setCameraEventCallback("
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java"/>
- </issue>
-
- <issue
- id="UnsafeOptInUsageError"
- message="This declaration is opt-in and its usage should be marked with
'@androidx.camera.camera2.interop.ExperimentalCamera2Interop' or '@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)'"
- errorLine1=" new Camera2ImplConfig.Extender<>(builder).setCameraEventCallback("
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java"/>
- </issue>
-
- <issue
- id="UnsafeOptInUsageError"
- message="This declaration is opt-in and its usage should be marked with
'@androidx.camera.camera2.interop.ExperimentalCamera2Interop' or '@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)'"
- errorLine1=" new CameraEventCallbacks(imageCaptureEventAdapter));"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java"/>
- </issue>
-
- <issue
- id="UnsafeOptInUsageError"
- message="This declaration is opt-in and its usage should be marked with
'@androidx.camera.camera2.interop.ExperimentalCamera2Interop' or '@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)'"
- errorLine1=" new Camera2ImplConfig.Extender<>(builder).setCameraEventCallback("
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
file="src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java"/>
</issue>
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java
index 225da9a..f9bc984 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java
@@ -20,8 +20,10 @@
import android.util.Pair;
import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.camera.camera2.impl.Camera2ImplConfig;
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
import androidx.camera.core.impl.CaptureConfig;
import androidx.camera.core.impl.CaptureStage;
import androidx.camera.extensions.impl.CaptureStageImpl;
@@ -34,9 +36,9 @@
private final int mId;
@SuppressWarnings("unchecked")
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
public AdaptingCaptureStage(@NonNull CaptureStageImpl impl) {
mId = impl.getId();
-
Camera2ImplConfig.Builder camera2ConfigurationBuilder = new Camera2ImplConfig.Builder();
for (Pair<CaptureRequest.Key, Object> captureParameter : impl.getParameters()) {
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
index c280363..c752065 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
@@ -88,6 +88,7 @@
/**
* Update extension related configs to the builder.
*/
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
void updateBuilderConfig(@NonNull ImageCapture.Builder builder,
@ExtensionMode.Mode int effectMode, @NonNull VendorExtender vendorExtender,
@NonNull Context context) {
diff --git a/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/model/AudioGeneratorDeviceTest.kt b/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/model/AudioGeneratorDeviceTest.kt
index 74a5c35..6f91eca 100644
--- a/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/model/AudioGeneratorDeviceTest.kt
+++ b/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/model/AudioGeneratorDeviceTest.kt
@@ -16,7 +16,9 @@
package androidx.camera.integration.avsync.model
+import android.content.Context
import android.media.AudioTrack
+import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.LargeTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
@@ -33,6 +35,7 @@
@RunWith(AndroidJUnit4::class)
class AudioGeneratorDeviceTest {
+ private val context: Context = ApplicationProvider.getApplicationContext()
private lateinit var audioGenerator: AudioGenerator
@Before
@@ -42,12 +45,12 @@
@Test(expected = IllegalArgumentException::class)
fun initAudioTrack_throwExceptionWhenFrequencyNegative() = runTest {
- audioGenerator.initAudioTrack(-5300, 11.0)
+ audioGenerator.initAudioTrack(context, -5300, 11.0)
}
@Test(expected = IllegalArgumentException::class)
fun initAudioTrack_throwExceptionWhenLengthNegative() = runTest {
- audioGenerator.initAudioTrack(5300, -11.0)
+ audioGenerator.initAudioTrack(context, 5300, -11.0)
}
@Test
@@ -71,7 +74,7 @@
}
private suspend fun initialAudioTrack(frequency: Int, beepLengthInSec: Double) {
- val isInitialized = audioGenerator.initAudioTrack(frequency, beepLengthInSec)
+ val isInitialized = audioGenerator.initAudioTrack(context, frequency, beepLengthInSec)
assertThat(isInitialized).isTrue()
assertThat(audioGenerator.audioTrack!!.state).isEqualTo(AudioTrack.STATE_INITIALIZED)
assertThat(audioGenerator.audioTrack!!.playbackHeadPosition).isEqualTo(0)
diff --git a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorViewModel.kt b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorViewModel.kt
index 2997a17..2762f2d 100644
--- a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorViewModel.kt
+++ b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorViewModel.kt
@@ -79,6 +79,7 @@
withContext(Dispatchers.Default) {
audioGenerator.initAudioTrack(
+ context = context,
frequency = beepFrequency,
beepLengthInSec = ACTIVE_LENGTH_SEC,
)
diff --git a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/AudioGenerator.kt b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/AudioGenerator.kt
index 80c3936..8e12eb2 100644
--- a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/AudioGenerator.kt
+++ b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/AudioGenerator.kt
@@ -16,6 +16,7 @@
package androidx.camera.integration.avsync.model
+import android.content.Context
import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioManager
@@ -30,8 +31,8 @@
import kotlin.math.sin
private const val TAG = "AudioGenerator"
+private const val DEFAULT_SAMPLE_RATE: Int = 44100
private const val SAMPLE_WIDTH: Int = 2
-private const val SAMPLE_RATE: Int = 44100
private const val MAGNITUDE = 0.5
private const val ENCODING: Int = AudioFormat.ENCODING_PCM_16BIT
private const val CHANNEL = AudioFormat.CHANNEL_OUT_MONO
@@ -46,26 +47,33 @@
}
fun stop() {
+ Logger.i(TAG, "playState before stopped: ${audioTrack!!.playState}")
+ Logger.i(TAG, "playbackHeadPosition before stopped: ${audioTrack!!.playbackHeadPosition}")
audioTrack!!.stop()
}
suspend fun initAudioTrack(
+ context: Context,
frequency: Int,
beepLengthInSec: Double,
): Boolean {
checkArgumentNonnegative(frequency, "The input frequency should not be negative.")
checkArgument(beepLengthInSec >= 0, "The beep length should not be negative.")
- Logger.i(TAG, "initAudioTrack with beep frequency: $frequency")
-
- val samples = generateSineSamples(frequency, beepLengthInSec, SAMPLE_WIDTH, SAMPLE_RATE)
+ val sampleRate = getOutputSampleRate(context)
+ val samples = generateSineSamples(frequency, beepLengthInSec, SAMPLE_WIDTH, sampleRate)
val bufferSize = samples.size
+
+ Logger.i(TAG, "initAudioTrack with sample rate: $sampleRate")
+ Logger.i(TAG, "initAudioTrack with beep frequency: $frequency")
+ Logger.i(TAG, "initAudioTrack with buffer size: $bufferSize")
+
val audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
val audioFormat = AudioFormat.Builder()
- .setSampleRate(SAMPLE_RATE)
+ .setSampleRate(sampleRate)
.setEncoding(ENCODING)
.setChannelMask(CHANNEL)
.build()
@@ -83,6 +91,13 @@
return true
}
+ private fun getOutputSampleRate(context: Context): Int {
+ val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+ val sampleRate: String? = audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE)
+
+ return sampleRate?.toInt() ?: DEFAULT_SAMPLE_RATE
+ }
+
@VisibleForTesting
suspend fun generateSineSamples(
frequency: Int,
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BasicUITest.java b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BasicUITest.java
index aefea06..5d146b13 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BasicUITest.java
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BasicUITest.java
@@ -28,6 +28,8 @@
import android.content.Intent;
import androidx.camera.camera2.Camera2Config;
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig;
+import androidx.camera.core.CameraXConfig;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.Preview;
import androidx.camera.testing.CameraUtil;
@@ -35,7 +37,6 @@
import androidx.camera.testing.CoreAppTestUtil.ForegroundOccupiedError;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.espresso.IdlingRegistry;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
@@ -48,11 +49,16 @@
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
import leakcanary.FailTestOnLeak;
// Tests basic UI operation when using CoreTest app.
-@RunWith(AndroidJUnit4.class)
+@RunWith(Parameterized.class)
@LargeTest
public final class BasicUITest {
private static final String BASIC_SAMPLE_PACKAGE = "androidx.camera.integration.core";
@@ -63,13 +69,24 @@
private final Intent mIntent = mContext.getPackageManager()
.getLaunchIntentForPackage(BASIC_SAMPLE_PACKAGE);
+ @Parameterized.Parameter(0)
+ public CameraXConfig mCameraConfig;
+
+ @Parameterized.Parameters
+ public static Collection<Object[]> getParameters() {
+ List<Object[]> result = new ArrayList<>();
+ result.add(new Object[]{Camera2Config.defaultConfig()});
+ result.add(new Object[]{CameraPipeConfig.INSTANCE.defaultConfig()});
+ return result;
+ }
+
@Rule
public ActivityTestRule<CameraXActivity> mActivityRule =
new ActivityTestRule<>(CameraXActivity.class, true, false);
@Rule
public TestRule mUseCamera = CameraUtil.grantCameraPermissionAndPreTest(
- new CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
+ new CameraUtil.PreTestCameraIdList(mCameraConfig)
);
@Rule
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
index 4bab0a9..cb47a1d 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
@@ -31,8 +31,6 @@
*/
internal fun ActivityScenario<CameraXActivity>.waitForViewfinderIdle() {
val idlingResource = withActivity {
- // Make sure that the test target use case is not null
- assertThat(preview).isNotNull()
resetViewIdlingResource()
viewIdlingResource
}
@@ -50,8 +48,6 @@
*/
internal fun ActivityScenario<CameraXActivity>.switchCameraAndWaitForViewfinderIdle() {
val idlingResource = withActivity {
- // Make sure that the test target use case is not null
- assertThat(preview).isNotNull()
resetViewIdlingResource()
viewIdlingResource
}
@@ -68,8 +64,6 @@
*/
internal fun ActivityScenario<CameraXActivity>.takePictureAndWaitForImageSavedIdle() {
val idlingResource = withActivity {
- // Make sure that the test target use case is not null
- assertThat(imageCapture).isNotNull()
imageSavedIdlingResource
}
try {
@@ -88,8 +82,6 @@
*/
internal fun ActivityScenario<CameraXActivity>.waitForImageAnalysisIdle() {
val idlingResource = withActivity {
- // Make sure that the test target use case is not null
- assertThat(imageAnalysis).isNotNull()
resetAnalysisIdlingResource()
analysisIdlingResource
}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt
index 0636c9b..981b5d2 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt
@@ -18,10 +18,13 @@
import android.Manifest
import android.app.Instrumentation
import android.content.Context
+import android.content.Intent
import android.os.Build
import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
import androidx.camera.core.CameraSelector
import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.CameraPipeConfigTestRule
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CameraUtil.PreTestCameraIdList
import androidx.camera.testing.CoreAppTestUtil
@@ -29,10 +32,9 @@
import androidx.lifecycle.Lifecycle.State.RESUMED
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
-import androidx.test.espresso.Espresso
+import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
-import androidx.test.espresso.matcher.ViewMatchers
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
@@ -42,20 +44,23 @@
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.runBlocking
import org.junit.After
-import org.junit.AfterClass
import org.junit.Assume
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
private const val HOME_TIMEOUT_MS = 3000L
private const val ROTATE_TIMEOUT_MS = 2000L
// Test application lifecycle when using CameraX.
-@RunWith(AndroidJUnit4::class)
+@RunWith(Parameterized::class)
@LargeTest
-class ExistingActivityLifecycleTest {
+class ExistingActivityLifecycleTest(
+ private val implName: String,
+ private val cameraConfig: String
+) {
private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@get:Rule
@@ -73,14 +78,17 @@
@get:Rule
val repeatRule = RepeatRule()
- companion object {
- @AfterClass
- @JvmStatic
- fun shutdownCameraX() {
- val context = ApplicationProvider.getApplicationContext<Context>()
- val cameraProvider = ProcessCameraProvider.getInstance(context)[10, TimeUnit.SECONDS]
- cameraProvider.shutdown()[10, TimeUnit.SECONDS]
- }
+ @get:Rule
+ val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
+ active = implName == CameraPipeConfig::class.simpleName,
+ forAllTests = true,
+ )
+
+ private val launchIntent = Intent(
+ ApplicationProvider.getApplicationContext(),
+ CameraXActivity::class.java
+ ).apply {
+ putExtra(CameraXActivity.INTENT_EXTRA_CAMERA_IMPLEMENTATION, cameraConfig)
}
@Before
@@ -108,13 +116,17 @@
device.unfreezeRotation()
device.pressHome()
device.waitForIdle(HOME_TIMEOUT_MS)
+
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ val cameraProvider = ProcessCameraProvider.getInstance(context)[10, TimeUnit.SECONDS]
+ cameraProvider.shutdown()[10, TimeUnit.SECONDS]
}
// Check if Preview screen is updated or not, after Destroy-Create lifecycle.
@Test
@RepeatRule.Repeat(times = 5)
fun checkPreviewUpdatedAfterDestroyRecreate() {
- with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+ with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
use { // Ensure ActivityScenario is cleaned up properly
// Wait for viewfinder to receive enough frames for its IdlingResource to idle.
waitForViewfinderIdle()
@@ -129,7 +141,7 @@
@Test
@RepeatRule.Repeat(times = 5)
fun checkImageCaptureAfterDestroyRecreate() {
- with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+ with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
use {
// Arrange.
// Ensure ActivityScenario is cleaned up properly
@@ -150,7 +162,7 @@
@Test
@RepeatRule.Repeat(times = 5)
fun checkPreviewUpdatedAfterStopResume() {
- with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+ with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
use { // Ensure ActivityScenario is cleaned up properly
// Wait for viewfinder to receive enough frames for its IdlingResource to idle.
waitForViewfinderIdle()
@@ -179,7 +191,7 @@
@Test
@RepeatRule.Repeat(times = 5)
fun checkImageCaptureAfterStopResume() {
- with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+ with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
use {
// Arrange.
// Ensure ActivityScenario is cleaned up properly
@@ -210,13 +222,13 @@
)
)
- with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+ with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
use { // Ensure ActivityScenario is cleaned up properly
// Wait for viewfinder to receive enough frames for its IdlingResource to idle.
waitForViewfinderIdle()
// Switch camera.
- Espresso.onView(ViewMatchers.withId(R.id.direction_toggle))
+ onView(withId(R.id.direction_toggle))
.perform(ViewActions.click())
// Check front camera is now idle
@@ -244,7 +256,7 @@
)
)
- with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+ with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
use {
// Arrange.
// Ensure ActivityScenario is cleaned up properly
@@ -252,7 +264,7 @@
waitForViewfinderIdle()
// Act. Switch camera.
- Espresso.onView(ViewMatchers.withId(R.id.direction_toggle))
+ onView(withId(R.id.direction_toggle))
.perform(ViewActions.click())
// Assert.
@@ -272,7 +284,7 @@
@Test
@RepeatRule.Repeat(times = 5)
fun checkPreviewUpdatedAfterRotateDeviceAndStopResume() {
- with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+ with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
use { // Ensure ActivityScenario is cleaned up properly
// Wait for viewfinder to receive enough frames for its IdlingResource to idle.
waitForViewfinderIdle()
@@ -298,7 +310,7 @@
@Test
@RepeatRule.Repeat(times = 5)
fun checkImageCaptureAfterRotateDeviceAndStopResume() {
- with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+ with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
use {
// Arrange.
// Ensure ActivityScenario is cleaned up properly
@@ -337,4 +349,20 @@
)
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
}
+
+ companion object {
+
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun data() = listOf(
+ arrayOf(
+ Camera2Config::class.simpleName,
+ CameraXViewModel.CAMERA2_IMPLEMENTATION_OPTION
+ ),
+ arrayOf(
+ CameraPipeConfig::class.simpleName,
+ CameraXViewModel.CAMERA_PIPE_IMPLEMENTATION_OPTION
+ )
+ )
+ }
}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java
deleted file mode 100644
index 7951333..0000000
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java
+++ /dev/null
@@ -1,219 +0,0 @@
-/*
- * 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.integration.core;
-
-import static androidx.test.espresso.Espresso.onView;
-import static androidx.test.espresso.action.ViewActions.click;
-import static androidx.test.espresso.assertion.ViewAssertions.matches;
-import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
-import static androidx.test.espresso.matcher.ViewMatchers.isEnabled;
-import static androidx.test.espresso.matcher.ViewMatchers.withId;
-
-import static junit.framework.TestCase.assertNotNull;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assume.assumeNotNull;
-import static org.junit.Assume.assumeTrue;
-
-import android.content.Intent;
-
-import androidx.camera.camera2.Camera2Config;
-import androidx.camera.core.CameraInfo;
-import androidx.camera.core.CameraSelector;
-import androidx.camera.core.ImageCapture;
-import androidx.camera.core.TorchState;
-import androidx.camera.integration.core.idlingresource.ElapsedTimeIdlingResource;
-import androidx.camera.integration.core.idlingresource.WaitForViewToShow;
-import androidx.camera.lifecycle.ProcessCameraProvider;
-import androidx.camera.testing.CameraUtil;
-import androidx.camera.testing.CoreAppTestUtil;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.espresso.Espresso;
-import androidx.test.espresso.IdlingRegistry;
-import androidx.test.espresso.IdlingResource;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.LargeTest;
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.rule.ActivityTestRule;
-import androidx.test.rule.GrantPermissionRule;
-import androidx.test.uiautomator.UiDevice;
-
-import junit.framework.AssertionFailedError;
-
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestRule;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-/** Test toggle buttons in CoreTestApp. */
-@RunWith(AndroidJUnit4.class)
-@LargeTest
-public final class ToggleButtonUITest {
-
- private static final int IDLE_TIMEOUT_MS = 1000;
- private static final String BASIC_SAMPLE_PACKAGE = "androidx.camera.integration.core";
-
- private final UiDevice mDevice =
- UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
- private final Intent mIntent = ApplicationProvider.getApplicationContext().getPackageManager()
- .getLaunchIntentForPackage(BASIC_SAMPLE_PACKAGE);
-
- @Rule
- public ActivityTestRule<CameraXActivity> mActivityRule =
- new ActivityTestRule<>(CameraXActivity.class, true,
- false);
-
- @Rule
- public TestRule mUseCamera = CameraUtil.grantCameraPermissionAndPreTest(
- new CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
- );
- @Rule
- public GrantPermissionRule mStoragePermissionRule =
- GrantPermissionRule.grant(android.Manifest.permission.WRITE_EXTERNAL_STORAGE);
- @Rule
- public GrantPermissionRule mAudioPermissionRule =
- GrantPermissionRule.grant(android.Manifest.permission.RECORD_AUDIO);
-
- public static void waitFor(IdlingResource idlingResource) {
- IdlingRegistry.getInstance().register(idlingResource);
- Espresso.onIdle();
- IdlingRegistry.getInstance().unregister(idlingResource);
- }
-
- @Before
- public void setUp() throws CoreAppTestUtil.ForegroundOccupiedError {
- assumeTrue(CameraUtil.deviceHasCamera());
- CoreAppTestUtil.assumeCompatibleDevice();
-
- // Clear the device UI and check if there is no dialog or lock screen on the top of the
- // window before start the test.
- CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation());
-
- // Launch Activity
- mActivityRule.launchActivity(mIntent);
- }
-
- @After
- public void tearDown() {
- // Idles Espresso thread and make activity complete each action.
- waitFor(new ElapsedTimeIdlingResource(IDLE_TIMEOUT_MS));
-
- mActivityRule.finishActivity();
-
- // Returns to Home to restart next test.
- mDevice.pressHome();
- mDevice.waitForIdle(IDLE_TIMEOUT_MS);
- }
-
- @AfterClass
- public static void shutdownCameraX()
- throws InterruptedException, ExecutionException, TimeoutException {
- ProcessCameraProvider cameraProvider = ProcessCameraProvider.getInstance(
- ApplicationProvider.getApplicationContext()).get(10, TimeUnit.SECONDS);
- cameraProvider.shutdown().get(10, TimeUnit.SECONDS);
- }
-
-
- @Test
- public void testFlashToggleButton() {
- waitFor(new WaitForViewToShow(R.id.constraintLayout));
- assumeTrue(isButtonEnabled(R.id.flash_toggle));
-
- ImageCapture useCase = mActivityRule.getActivity().getImageCapture();
- assertNotNull(useCase);
-
- // There are 3 different states of flash mode: ON, OFF and AUTO.
- // By pressing flash mode toggle button, the flash mode would switch to the next state.
- // The flash mode would loop in following sequence: OFF -> AUTO -> ON -> OFF.
- @ImageCapture.FlashMode int mode1 = useCase.getFlashMode();
-
- onView(withId(R.id.flash_toggle)).perform(click());
- @ImageCapture.FlashMode int mode2 = useCase.getFlashMode();
- // After the switch, the mode2 should be different from mode1.
- assertNotEquals(mode2, mode1);
-
- onView(withId(R.id.flash_toggle)).perform(click());
- @ImageCapture.FlashMode int mode3 = useCase.getFlashMode();
- // The mode3 should be different from first and second time.
- assertNotEquals(mode3, mode2);
- assertNotEquals(mode3, mode1);
- }
-
- @Test
- public void testTorchToggleButton() {
- waitFor(new WaitForViewToShow(R.id.constraintLayout));
- assumeTrue(isButtonEnabled(R.id.torch_toggle));
-
- CameraInfo cameraInfo = mActivityRule.getActivity().getCameraInfo();
- assertNotNull(cameraInfo);
- boolean isTorchOn = isTorchOn(cameraInfo);
-
- onView(withId(R.id.torch_toggle)).perform(click());
- assertNotEquals(isTorchOn(cameraInfo), isTorchOn);
-
- // By pressing the torch toggle button two times, it should switch back to original state.
- onView(withId(R.id.torch_toggle)).perform(click());
- assertEquals(isTorchOn(cameraInfo), isTorchOn);
- }
-
- @Test
- public void testSwitchCameraToggleButton() {
- assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_FRONT));
- waitFor(new WaitForViewToShow(R.id.direction_toggle));
-
- assumeNotNull(mActivityRule.getActivity().getPreview());
-
- for (int i = 0; i < 5; i++) {
-
- // Wait for preview update.
- mActivityRule.getActivity().resetViewIdlingResource();
- IdlingRegistry.getInstance().register(
- mActivityRule.getActivity().getViewIdlingResource());
- onView(withId(R.id.viewFinder)).check(matches(isDisplayed()));
- IdlingRegistry.getInstance().unregister(
- mActivityRule.getActivity().getViewIdlingResource());
-
- onView(withId(R.id.direction_toggle)).perform(click());
- }
- }
-
- private boolean isTorchOn(CameraInfo cameraInfo) {
- return cameraInfo.getTorchState().getValue() == TorchState.ON;
- }
-
- private boolean isButtonEnabled(int resource) {
- try {
- onView(withId(resource)).check(matches(isEnabled()));
- // View is in hierarchy
- return true;
- } catch (AssertionFailedError e) {
- // View is not in hierarchy
- return false;
- } catch (Exception e) {
- // View is not in hierarchy
- return false;
- }
- }
-}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.kt
new file mode 100644
index 0000000..d7fc670
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.kt
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.integration.core
+
+import android.Manifest
+import android.content.Context
+import android.content.Intent
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
+import androidx.camera.core.CameraInfo
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.TorchState
+import androidx.camera.integration.core.idlingresource.WaitForViewToShow
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.CameraPipeConfigTestRule
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.CoreAppTestUtil
+import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.Espresso.onIdle
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.IdlingRegistry
+import androidx.test.espresso.IdlingResource
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.assertion.ViewAssertions
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
+import androidx.test.uiautomator.UiDevice
+import androidx.testutils.withActivity
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.TimeUnit
+import junit.framework.AssertionFailedError
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/** Test toggle buttons in CoreTestApp. */
+@LargeTest
+@RunWith(Parameterized::class)
+class ToggleButtonUITest(
+ private val implName: String,
+ private val cameraConfig: String
+) {
+ private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+
+ @get:Rule
+ val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
+ CameraUtil.PreTestCameraIdList(
+ if (implName == Camera2Config::class.simpleName) {
+ Camera2Config.defaultConfig()
+ } else {
+ CameraPipeConfig.defaultConfig()
+ }
+ )
+ )
+
+ @get:Rule
+ val permissionRule: GrantPermissionRule =
+ GrantPermissionRule.grant(
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ Manifest.permission.RECORD_AUDIO
+ )
+
+ @get:Rule
+ val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
+ active = implName == CameraPipeConfig::class.simpleName,
+ forAllTests = true,
+ )
+
+ private val launchIntent = Intent(
+ ApplicationProvider.getApplicationContext(),
+ CameraXActivity::class.java
+ ).apply {
+ putExtra(CameraXActivity.INTENT_EXTRA_CAMERA_IMPLEMENTATION, cameraConfig)
+ }
+
+ @Before
+ fun setUp() {
+ assumeTrue(CameraUtil.deviceHasCamera())
+ CoreAppTestUtil.assumeCompatibleDevice()
+ // Use the natural orientation throughout these tests to ensure the activity isn't
+ // recreated unexpectedly. This will also freeze the sensors until
+ // mDevice.unfreezeRotation() in the tearDown() method. Any simulated rotations will be
+ // explicitly initiated from within the test.
+ device.setOrientationNatural()
+ // Clear the device UI and check if there is no dialog or lock screen on the top of the
+ // window before start the test.
+ CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation())
+ }
+
+ @After
+ fun tearDown(): Unit = runBlocking(Dispatchers.Main) {
+ // Returns to Home to restart next test.
+ device.pressHome()
+ device.waitForIdle(IDLE_TIMEOUT_MS)
+ // Unfreeze rotation so the device can choose the orientation via its own policy. Be nice
+ // to other tests :)
+ device.unfreezeRotation()
+
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ val cameraProvider = ProcessCameraProvider.getInstance(context)[10, TimeUnit.SECONDS]
+ cameraProvider.shutdown()[10, TimeUnit.SECONDS]
+ }
+
+ @Test
+ fun testFlashToggleButton() {
+ ActivityScenario.launch<CameraXActivity>(launchIntent).use { scenario ->
+ // Arrange.
+ WaitForViewToShow(R.id.constraintLayout).wait()
+ assumeTrue(isButtonEnabled(R.id.flash_toggle))
+ val useCase = scenario.withActivity { imageCapture }
+ // There are 3 different states of flash mode: ON, OFF and AUTO.
+ // By pressing flash mode toggle button, the flash mode would switch to the next state.
+ // The flash mode would loop in following sequence: OFF -> AUTO -> ON -> OFF.
+ // Act.
+ @ImageCapture.FlashMode val mode1 = useCase.flashMode
+ onView(withId(R.id.flash_toggle)).perform(click())
+ @ImageCapture.FlashMode val mode2 = useCase.flashMode
+ onView(withId(R.id.flash_toggle)).perform(click())
+ @ImageCapture.FlashMode val mode3 = useCase.flashMode
+
+ // Assert.
+ // After the switch, the mode2 should be different from mode1.
+ assertThat(mode2).isNotEqualTo(mode1)
+ // The mode3 should be different from first and second time.
+ assertThat(mode3).isNoneOf(mode2, mode1)
+ }
+ }
+
+ @Test
+ fun testTorchToggleButton() {
+ ActivityScenario.launch<CameraXActivity>(launchIntent).use { scenario ->
+ WaitForViewToShow(R.id.constraintLayout).wait()
+ assumeTrue(isButtonEnabled(R.id.torch_toggle))
+ val cameraInfo = scenario.withActivity { cameraInfo!! }
+ val isTorchOn = cameraInfo.isTorchOn()
+ onView(withId(R.id.torch_toggle)).perform(click())
+ assertThat(cameraInfo.isTorchOn()).isNotEqualTo(isTorchOn)
+ // By pressing the torch toggle button two times, it should switch back to original
+ // state.
+ onView(withId(R.id.torch_toggle)).perform(click())
+ assertThat(cameraInfo.isTorchOn()).isEqualTo(isTorchOn)
+ }
+ }
+
+ @Test
+ fun testSwitchCameraToggleButton() {
+ assumeTrue(
+ "Ignore the camera switch test since there's no front camera.",
+ CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_FRONT)
+ )
+ ActivityScenario.launch<CameraXActivity>(launchIntent).use { scenario ->
+ WaitForViewToShow(R.id.direction_toggle).wait()
+ assertThat(scenario.withActivity { preview }).isNotNull()
+ for (i in 0..4) {
+ scenario.waitForViewfinderIdle()
+ // Click switch camera button.
+ onView(withId(R.id.direction_toggle)).perform(click())
+ }
+ }
+ }
+
+ private fun CameraInfo.isTorchOn(): Boolean = torchState.value == TorchState.ON
+
+ private fun isButtonEnabled(resource: Int): Boolean {
+ return try {
+ onView(withId(resource))
+ .check(ViewAssertions.matches(ViewMatchers.isEnabled()))
+ // View is in hierarchy
+ true
+ } catch (e: AssertionFailedError) {
+ // View is not in hierarchy
+ false
+ } catch (e: Exception) {
+ // View is not in hierarchy
+ false
+ }
+ }
+
+ private fun IdlingResource.wait() {
+ IdlingRegistry.getInstance().register(this)
+ onIdle()
+ IdlingRegistry.getInstance().unregister(this)
+ }
+
+ companion object {
+ private const val IDLE_TIMEOUT_MS = 1_000L
+
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun data() = listOf(
+ arrayOf(
+ Camera2Config::class.simpleName,
+ CameraXViewModel.CAMERA2_IMPLEMENTATION_OPTION
+ ),
+ arrayOf(
+ CameraPipeConfig::class.simpleName,
+ CameraXViewModel.CAMERA_PIPE_IMPLEMENTATION_OPTION
+ )
+ )
+ }
+}
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/PreviewTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
similarity index 93%
rename from camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/PreviewTest.kt
rename to camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
index a351980..f61bdc6 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/PreviewTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 The Android Open Source Project
+ * Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,18 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.camera.camera2
+package androidx.camera.integration.core.camera2
import android.content.Context
import android.graphics.SurfaceTexture
import android.util.Size
import android.view.Surface
+import androidx.camera.camera2.Camera2Config
import androidx.camera.camera2.internal.DisplayInfoManager
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
import androidx.camera.core.AspectRatio
import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraXConfig
import androidx.camera.core.ImageCapture
import androidx.camera.core.Preview
-import androidx.camera.core.SurfaceRequest
import androidx.camera.core.UseCase
import androidx.camera.core.impl.ImageOutputConfig
import androidx.camera.core.impl.utils.executor.CameraXExecutors
@@ -37,7 +39,6 @@
import androidx.camera.testing.SurfaceTextureProvider.SurfaceTextureCallback
import androidx.core.util.Consumer
import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
@@ -50,24 +51,40 @@
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import java.util.concurrent.atomic.AtomicReference
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
import org.junit.After
import org.junit.Assume
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers
-import org.mockito.Mockito
-import org.mockito.invocation.InvocationOnMock
+import org.junit.runners.Parameterized
@LargeTest
-@RunWith(AndroidJUnit4::class)
+@RunWith(Parameterized::class)
@SdkSuppress(minSdkVersion = 21)
-class PreviewTest {
+class PreviewTest(
+ private val implName: String,
+ private val cameraConfig: CameraXConfig
+) {
@get:Rule
val cameraRule = CameraUtil.grantCameraPermissionAndPreTest(
- PreTestCameraIdList(Camera2Config.defaultConfig())
+ PreTestCameraIdList(cameraConfig)
)
+
+ companion object {
+ private const val ANY_THREAD_NAME = "any-thread-name"
+ private val DEFAULT_RESOLUTION: Size by lazy { Size(640, 480) }
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun data() = listOf(
+ arrayOf(Camera2Config::class.simpleName, Camera2Config.defaultConfig()),
+ arrayOf(CameraPipeConfig::class.simpleName, CameraPipeConfig.defaultConfig())
+ )
+ }
+
private val instrumentation = InstrumentationRegistry.getInstrumentation()
private val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
private var defaultBuilder: Preview.Builder? = null
@@ -81,8 +98,7 @@
@Throws(ExecutionException::class, InterruptedException::class)
fun setUp() {
context = ApplicationProvider.getApplicationContext()
- val cameraXConfig = Camera2Config.defaultConfig()
- CameraXUtil.initialize(context!!, cameraXConfig).get()
+ CameraXUtil.initialize(context!!, cameraConfig).get()
// init CameraX before creating Preview to get preview size with CameraX's context
defaultBuilder = Preview.Builder.fromConfig(Preview.DEFAULT_CONFIG.config)
@@ -106,38 +122,30 @@
}
@Test
- fun surfaceProvider_isUsedAfterSetting() {
- val surfaceProvider = Mockito.mock(
- Preview.SurfaceProvider::class.java
- )
- Mockito.doAnswer { args: InvocationOnMock ->
- val surfaceTexture = SurfaceTexture(0)
- surfaceTexture.setDefaultBufferSize(640, 480)
- val surface = Surface(surfaceTexture)
- (args.getArgument<Any>(0) as SurfaceRequest).provideSurface(
- surface,
- CameraXExecutors.directExecutor()
- ) {
- surfaceTexture.release()
- surface.release()
- }
- null
- }.`when`(surfaceProvider).onSurfaceRequested(
- ArgumentMatchers.any(
- SurfaceRequest::class.java
- )
- )
+ fun surfaceProvider_isUsedAfterSetting() = runBlocking {
val preview = defaultBuilder!!.build()
+ val completableDeferred = CompletableDeferred<Unit>()
// TODO(b/160261462) move off of main thread when setSurfaceProvider does not need to be
// done on the main thread
- instrumentation.runOnMainSync { preview.setSurfaceProvider(surfaceProvider) }
- camera = CameraUtil.createCameraAndAttachUseCase(context!!, cameraSelector, preview)
- Mockito.verify(surfaceProvider, Mockito.timeout(3000)).onSurfaceRequested(
- ArgumentMatchers.any(
- SurfaceRequest::class.java
+ instrumentation.runOnMainSync { preview.setSurfaceProvider { request ->
+ val surfaceTexture = SurfaceTexture(0)
+ surfaceTexture.setDefaultBufferSize(
+ request.resolution.width,
+ request.resolution.height
)
- )
+ surfaceTexture.detachFromGLContext()
+ val surface = Surface(surfaceTexture)
+ request.provideSurface(surface, CameraXExecutors.directExecutor()) {
+ surface.release()
+ surfaceTexture.release()
+ }
+ completableDeferred.complete(Unit)
+ } }
+ camera = CameraUtil.createCameraAndAttachUseCase(context!!, cameraSelector, preview)
+ withTimeout(3_000) {
+ completableDeferred.await()
+ }
}
@Test
@@ -564,9 +572,4 @@
} while (totalCheckTime < timeoutMs)
return false
}
-
- companion object {
- private const val ANY_THREAD_NAME = "any-thread-name"
- private val DEFAULT_RESOLUTION: Size by lazy { Size(640, 480) }
- }
}
\ No newline at end of file
diff --git a/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/Diagnosis.kt b/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/Diagnosis.kt
index 38b9612..0eb3827 100644
--- a/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/Diagnosis.kt
+++ b/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/Diagnosis.kt
@@ -19,7 +19,6 @@
import android.content.Context
import android.os.Build
import android.util.Log
-import android.widget.Toast
import java.io.File
import java.io.FileOutputStream
import java.util.zip.ZipEntry
@@ -30,8 +29,8 @@
*/
class Diagnosis {
- // TODO: convert to async function
- fun collectDeviceInfo(context: Context) {
+ // TODO: convert to a suspend function for running different tasks within this function
+ fun collectDeviceInfo(context: Context): File {
Log.d(TAG, "calling collectDeviceInfo()")
// TODO: verify if external storage is available
@@ -57,12 +56,7 @@
zout.close()
fout.close()
- Log.d(TAG, "file at ${tempFile.path}")
- if (tempFile.exists()) {
- val msg = "Successfully collected information"
- Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
- Log.d(TAG, msg)
- }
+ return tempFile
}
private fun createTemp(context: Context, filename: String): File {
diff --git a/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt b/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt
index 519dad6..f195557 100644
--- a/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt
+++ b/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt
@@ -32,7 +32,6 @@
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.view.CameraController
import androidx.camera.view.CameraController.IMAGE_CAPTURE
import androidx.camera.view.CameraController.VIDEO_CAPTURE
@@ -46,13 +45,21 @@
import androidx.core.content.ContextCompat
import java.text.SimpleDateFormat
import java.util.Locale
-import java.util.concurrent.Executor
+import java.util.concurrent.ExecutorService
import androidx.camera.mlkit.vision.MlKitAnalyzer
import androidx.camera.view.CameraController.IMAGE_ANALYSIS
+import androidx.core.util.Preconditions
+import androidx.lifecycle.lifecycleScope
import com.google.android.material.tabs.TabLayout
import com.google.mlkit.vision.barcode.BarcodeScanner
import com.google.mlkit.vision.barcode.BarcodeScanning
import java.io.IOException
+import java.util.concurrent.Executor
+import java.util.concurrent.Executors
+import kotlinx.coroutines.ExecutorCoroutineDispatcher
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
class MainActivity : AppCompatActivity() {
@@ -65,6 +72,9 @@
private lateinit var barcodeScanner: BarcodeScanner
private lateinit var analyzer: MlKitAnalyzer
private lateinit var diagnoseBtn: Button
+ private lateinit var calibrationExecutor: ExecutorService
+ private var calibrationThreadId: Long = -1
+ private lateinit var diagnosisDispatcher: ExecutorCoroutineDispatcher
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -79,6 +89,13 @@
diagnosis = Diagnosis()
barcodeScanner = BarcodeScanning.getClient()
diagnoseBtn = findViewById(R.id.diagnose_btn)
+ calibrationExecutor = Executors.newSingleThreadExecutor() { runnable ->
+ val thread = Executors.defaultThreadFactory().newThread(runnable)
+ thread.name = "CalibrationThread"
+ calibrationThreadId = thread.id
+ return@newSingleThreadExecutor thread
+ }
+ diagnosisDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
// Request CAMERA permission and fail gracefully if not granted.
if (allPermissionsGranted()) {
@@ -125,12 +142,20 @@
})
diagnoseBtn.setOnClickListener {
- try {
- diagnosis.collectDeviceInfo(baseContext)
- } catch (e: IOException) {
- val msg = "Failed to collect information"
- Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
- Log.e(TAG, "IOException caught: ${e.message}")
+ lifecycleScope.launch {
+ try {
+ val tempFile = withContext(diagnosisDispatcher) {
+ Log.i(TAG, "dispatcher: ${Thread.currentThread().name}")
+ diagnosis.collectDeviceInfo(baseContext)
+ }
+ Log.d(TAG, "file at ${tempFile.path}")
+ val msg = "Successfully collected device info"
+ Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
+ } catch (e: IOException) {
+ val msg = "Failed to collect information"
+ Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
+ Log.e(TAG, "IOException caught: ${e.message}")
+ }
}
}
}
@@ -287,20 +312,25 @@
analyzer = MlKitAnalyzer(
listOf(barcodeScanner),
CameraController.COORDINATE_SYSTEM_VIEW_REFERENCED,
- CameraXExecutors.mainThreadExecutor()
+ calibrationExecutor
) { result ->
+ // validating thread
+ checkCalibrationThread()
val barcodes = result.getValue(barcodeScanner)
if (barcodes != null && barcodes.size > 0) {
calibrate.analyze(barcodes)
- // gives overlayView access to Calibration
- overlayView.setCalibrationResult(calibrate)
- // enable diagnose button when alignment is successful
- diagnoseBtn.isEnabled = calibrate.isAligned
- overlayView.invalidate()
+ // run UI on main thread
+ lifecycleScope.launch {
+ // gives overlayView access to Calibration
+ overlayView.setCalibrationResult(calibrate)
+ // enable diagnose button when alignment is successful
+ diagnoseBtn.isEnabled = calibrate.isAligned
+ overlayView.invalidate()
+ }
}
}
cameraController.setImageAnalysisAnalyzer(
- CameraXExecutors.mainThreadExecutor(), analyzer)
+ calibrationExecutor, analyzer)
}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
@@ -309,6 +339,17 @@
) == PackageManager.PERMISSION_GRANTED
}
+ private fun checkCalibrationThread() {
+ Preconditions.checkState(calibrationThreadId == Thread.currentThread().id,
+ "Not working on Calibration Thread")
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ calibrationExecutor.shutdown()
+ diagnosisDispatcher.close()
+ }
+
companion object {
private const val TAG = "DiagnoseApp"
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
diff --git a/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml b/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml
index 5aa38fc..48ca50a 100644
--- a/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml
+++ b/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml
@@ -27,7 +27,7 @@
<activity
android:name=".CameraExtensionsActivity"
android:exported="true"
- android:label="Camera Extensions">
+ android:label="CameraX Extensions">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -35,6 +35,16 @@
</activity>
<activity
+ android:name=".Camera2ExtensionsActivity"
+ android:exported="false"
+ android:label="Camera2 Extensions">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <activity
android:name=".validation.CameraValidationResultActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:exported="false">
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/Camera2ExtensionsActivity.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/Camera2ExtensionsActivity.kt
new file mode 100644
index 0000000..d682e82
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/Camera2ExtensionsActivity.kt
@@ -0,0 +1,661 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.extensions
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraAccessException
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CameraExtensionCharacteristics
+import android.hardware.camera2.CameraExtensionSession
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.params.ExtensionSessionConfiguration
+import android.hardware.camera2.params.OutputConfiguration
+import android.media.ImageReader
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import android.view.Menu
+import android.view.MenuItem
+import android.view.Surface
+import android.view.TextureView
+import android.view.ViewStub
+import android.widget.Button
+import android.widget.Toast
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import androidx.camera.core.impl.utils.futures.Futures
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.calculateRelativeImageRotationDegrees
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.createExtensionCaptureCallback
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.getDisplayRotationDegrees
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.getExtensionModeStringFromId
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.getLensFacingCameraId
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.pickPreviewResolution
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.pickStillImageResolution
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.transformPreview
+import androidx.camera.integration.extensions.utils.FileUtil
+import androidx.camera.integration.extensions.validation.CameraValidationResultActivity
+import androidx.concurrent.futures.CallbackToFutureAdapter
+import androidx.concurrent.futures.CallbackToFutureAdapter.Completer
+import androidx.core.util.Preconditions
+import androidx.lifecycle.lifecycleScope
+import com.google.common.util.concurrent.ListenableFuture
+import java.text.Format
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Locale
+import java.util.concurrent.Executors
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+private const val TAG = "Camera2ExtensionsAct~"
+private const val EXTENSION_MODE_INVALID = -1
+
+@RequiresApi(31)
+class Camera2ExtensionsActivity : AppCompatActivity() {
+
+ private lateinit var cameraManager: CameraManager
+
+ /**
+ * A reference to the opened [CameraDevice].
+ */
+ private var cameraDevice: CameraDevice? = null
+
+ /**
+ * The current camera extension session.
+ */
+ private var cameraExtensionSession: CameraExtensionSession? = null
+
+ private var currentCameraId = "0"
+
+ private lateinit var backCameraId: String
+ private lateinit var frontCameraId: String
+
+ private var cameraSensorRotationDegrees = 0
+
+ /**
+ * Still capture image reader
+ */
+ private var stillImageReader: ImageReader? = null
+
+ /**
+ * Camera extension characteristics for the current camera device.
+ */
+ private lateinit var extensionCharacteristics: CameraExtensionCharacteristics
+
+ /**
+ * Flag whether we should restart preview after an extension switch.
+ */
+ private var restartPreview = false
+
+ /**
+ * Flag whether we should restart after an camera switch.
+ */
+ private var restartCamera = false
+
+ /**
+ * Track current extension type and index.
+ */
+ private var currentExtensionMode = EXTENSION_MODE_INVALID
+ private var currentExtensionIdx = -1
+ private val supportedExtensionModes = mutableListOf<Int>()
+
+ private lateinit var textureView: TextureView
+
+ private lateinit var previewSurface: Surface
+
+ private val surfaceTextureListener = object : TextureView.SurfaceTextureListener {
+
+ override fun onSurfaceTextureAvailable(
+ surfaceTexture: SurfaceTexture,
+ with: Int,
+ height: Int
+ ) {
+ previewSurface = Surface(surfaceTexture)
+ openCameraWithExtensionMode(currentCameraId)
+ }
+
+ override fun onSurfaceTextureSizeChanged(
+ surfaceTexture: SurfaceTexture,
+ with: Int,
+ height: Int
+ ) {
+ }
+
+ override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean {
+ return true
+ }
+
+ override fun onSurfaceTextureUpdated(surfaceTexture: SurfaceTexture) {
+ }
+ }
+
+ private val captureCallbacks = createExtensionCaptureCallback()
+
+ private var restartOnStart = false
+
+ private var activityStopped = false
+
+ private val cameraTaskDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
+
+ private var imageSaveTerminationFuture: ListenableFuture<Any?> = Futures.immediateFuture(null)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ Log.d(TAG, "onCreate()")
+ setContentView(R.layout.activity_camera_extensions)
+
+ cameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
+ backCameraId = getLensFacingCameraId(cameraManager, CameraCharacteristics.LENS_FACING_BACK)
+ frontCameraId =
+ getLensFacingCameraId(cameraManager, CameraCharacteristics.LENS_FACING_FRONT)
+
+ currentCameraId = if (isCameraSupportExtensions(backCameraId)) {
+ backCameraId
+ } else if (isCameraSupportExtensions(frontCameraId)) {
+ frontCameraId
+ } else {
+ Toast.makeText(
+ this,
+ "Can't find camera supporting Camera2 extensions.",
+ Toast.LENGTH_SHORT
+ ).show()
+ closeCameraAndStartActivity(CameraExtensionsActivity::class.java.name)
+ return
+ }
+
+ updateExtensionInfo()
+
+ setupTextureView()
+ enableUiControl(false)
+ setupUiControl()
+ }
+
+ private fun isCameraSupportExtensions(cameraId: String): Boolean {
+ val characteristics = cameraManager.getCameraExtensionCharacteristics(cameraId)
+ return characteristics.supportedExtensions.isNotEmpty()
+ }
+
+ private fun updateExtensionInfo() {
+ Log.d(
+ TAG,
+ "updateExtensionInfo() - camera Id: $currentCameraId, ${
+ getExtensionModeStringFromId(currentExtensionMode)
+ }"
+ )
+ extensionCharacteristics = cameraManager.getCameraExtensionCharacteristics(currentCameraId)
+ supportedExtensionModes.clear()
+ supportedExtensionModes.addAll(extensionCharacteristics.supportedExtensions)
+
+ cameraSensorRotationDegrees = cameraManager.getCameraCharacteristics(
+ currentCameraId)[CameraCharacteristics.SENSOR_ORIENTATION] ?: 0
+
+ currentExtensionIdx = -1
+
+ // Checks whether the original selected extension mode is supported by the new target camera
+ if (currentExtensionMode != EXTENSION_MODE_INVALID) {
+ for (i in 0..supportedExtensionModes.size) {
+ if (supportedExtensionModes[i] == currentExtensionMode) {
+ currentExtensionIdx = i
+ break
+ }
+ }
+ }
+
+ // Switches to the first supported extension mode if the original selected mode is not
+ // supported
+ if (currentExtensionIdx == -1) {
+ currentExtensionIdx = 0
+ currentExtensionMode = supportedExtensionModes[0]
+ }
+ }
+
+ private fun setupTextureView() {
+ val viewFinderStub = findViewById<ViewStub>(R.id.viewFinderStub)
+ viewFinderStub.layoutResource = R.layout.full_textureview
+ textureView = viewFinderStub.inflate() as TextureView
+ textureView.surfaceTextureListener = surfaceTextureListener
+ }
+
+ private fun enableUiControl(enabled: Boolean) {
+ findViewById<Button>(R.id.PhotoToggle).isEnabled = enabled
+ findViewById<Button>(R.id.Switch).isEnabled = enabled
+ findViewById<Button>(R.id.Picture).isEnabled = enabled
+ }
+
+ private fun setupUiControl() {
+ val extensionModeToggleButton = findViewById<Button>(R.id.PhotoToggle)
+ extensionModeToggleButton.text = getExtensionModeStringFromId(currentExtensionMode)
+ extensionModeToggleButton.setOnClickListener {
+ enableUiControl(false)
+ currentExtensionIdx = (currentExtensionIdx + 1) % supportedExtensionModes.size
+ currentExtensionMode = supportedExtensionModes[currentExtensionIdx]
+ restartPreview = true
+ extensionModeToggleButton.text = getExtensionModeStringFromId(currentExtensionMode)
+
+ closeCaptureSession()
+ }
+
+ val cameraSwitchButton = findViewById<Button>(R.id.Switch)
+ cameraSwitchButton.setOnClickListener {
+ val newCameraId = if (currentCameraId == backCameraId) frontCameraId else backCameraId
+
+ if (!isCameraSupportExtensions(newCameraId)) {
+ Toast.makeText(
+ this,
+ "Camera of the other lens facing doesn't support Camera2 extensions.",
+ Toast.LENGTH_SHORT
+ ).show()
+ return@setOnClickListener
+ }
+
+ enableUiControl(false)
+ currentCameraId = newCameraId
+ restartCamera = true
+
+ closeCamera()
+ }
+
+ val captureButton = findViewById<Button>(R.id.Picture)
+ captureButton.setOnClickListener {
+ enableUiControl(false)
+ takePicture()
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ Log.d(TAG, "onStart()")
+ activityStopped = false
+ if (restartOnStart) {
+ restartOnStart = false
+ openCameraWithExtensionMode(currentCameraId)
+ }
+ }
+
+ override fun onStop() {
+ Log.d(TAG, "onStop()++")
+ super.onStop()
+ // Needs to close the camera first. Otherwise, the next activity might be failed to open
+ // the camera and configure the capture session.
+ runBlocking {
+ closeCaptureSession().await()
+ closeCamera().await()
+ }
+ restartOnStart = true
+ activityStopped = true
+ Log.d(TAG, "onStop()--")
+ }
+
+ override fun onDestroy() {
+ Log.d(TAG, "onDestroy()++")
+ super.onDestroy()
+ previewSurface.release()
+
+ imageSaveTerminationFuture.addListener({ stillImageReader?.close() }, mainExecutor)
+ Log.d(TAG, "onDestroy()--")
+ }
+
+ private fun closeCamera(): Deferred<Unit> = lifecycleScope.async(cameraTaskDispatcher) {
+ Log.d(TAG, "closeCamera()++")
+ cameraDevice?.close()
+ cameraDevice = null
+ Log.d(TAG, "closeCamera()--")
+ }
+
+ private fun closeCaptureSession(): Deferred<Unit> = lifecycleScope.async(cameraTaskDispatcher) {
+ Log.d(TAG, "closeCaptureSession()++")
+ try {
+ cameraExtensionSession?.close()
+ cameraExtensionSession = null
+ } catch (e: Exception) {
+ Log.e(TAG, e.toString())
+ }
+ Log.d(TAG, "closeCaptureSession()--")
+ }
+
+ private fun openCameraWithExtensionMode(cameraId: String) =
+ lifecycleScope.launch(cameraTaskDispatcher) {
+ Log.d(TAG, "openCameraWithExtensionMode()++ cameraId: $cameraId")
+ cameraDevice = openCamera(cameraManager, cameraId)
+ cameraExtensionSession = openCaptureSession()
+
+ lifecycleScope.launch(Dispatchers.Main) {
+ if (activityStopped) {
+ closeCaptureSession()
+ closeCamera()
+ }
+ }
+ Log.d(TAG, "openCameraWithExtensionMode()--")
+ }
+
+ /**
+ * Opens and returns the camera (as the result of the suspend coroutine)
+ */
+ @SuppressLint("MissingPermission")
+ suspend fun openCamera(
+ manager: CameraManager,
+ cameraId: String,
+ ): CameraDevice = suspendCancellableCoroutine { cont ->
+ Log.d(TAG, "openCamera(): $cameraId")
+ manager.openCamera(
+ cameraId,
+ cameraTaskDispatcher.asExecutor(),
+ object : CameraDevice.StateCallback() {
+ override fun onOpened(device: CameraDevice) = cont.resume(device)
+
+ override fun onDisconnected(device: CameraDevice) {
+ Log.w(TAG, "Camera $cameraId has been disconnected")
+ finish()
+ }
+
+ override fun onClosed(camera: CameraDevice) {
+ Log.d(TAG, "Camera - onClosed: $cameraId")
+ lifecycleScope.launch(Dispatchers.Main) {
+ if (restartCamera) {
+ restartCamera = false
+ updateExtensionInfo()
+ openCameraWithExtensionMode(currentCameraId)
+ }
+ }
+ }
+
+ override fun onError(device: CameraDevice, error: Int) {
+ Log.d(TAG, "Camera - onError: $cameraId")
+ val msg = when (error) {
+ ERROR_CAMERA_DEVICE -> "Fatal (device)"
+ ERROR_CAMERA_DISABLED -> "Device policy"
+ ERROR_CAMERA_IN_USE -> "Camera in use"
+ ERROR_CAMERA_SERVICE -> "Fatal (service)"
+ ERROR_MAX_CAMERAS_IN_USE -> "Maximum cameras in use"
+ else -> "Unknown"
+ }
+ val exc = RuntimeException("Camera $cameraId error: ($error) $msg")
+ Log.e(TAG, exc.message, exc)
+ cont.resumeWithException(exc)
+ }
+ })
+ }
+
+ /**
+ * Opens and returns the extensions session (as the result of the suspend coroutine)
+ */
+ private suspend fun openCaptureSession(): CameraExtensionSession =
+ suspendCancellableCoroutine { cont ->
+ Log.d(TAG, "openCaptureSession")
+ setupPreview()
+
+ if (stillImageReader != null) {
+ val imageReaderToClose = stillImageReader!!
+ imageSaveTerminationFuture.addListener(
+ { imageReaderToClose.close() },
+ mainExecutor
+ )
+ }
+
+ stillImageReader = setupImageReader()
+
+ val outputConfig = ArrayList<OutputConfiguration>()
+ outputConfig.add(OutputConfiguration(stillImageReader!!.surface))
+ outputConfig.add(OutputConfiguration(previewSurface))
+ val extensionConfiguration = ExtensionSessionConfiguration(
+ currentExtensionMode, outputConfig,
+ cameraTaskDispatcher.asExecutor(), object : CameraExtensionSession.StateCallback() {
+ override fun onClosed(session: CameraExtensionSession) {
+ Log.d(TAG, "CaptureSession - onClosed: $session")
+
+ lifecycleScope.launch(Dispatchers.Main) {
+ if (restartPreview) {
+ restartPreview = false
+
+ lifecycleScope.launch(cameraTaskDispatcher) {
+ cameraExtensionSession = openCaptureSession()
+ }
+ }
+ }
+ }
+
+ override fun onConfigured(session: CameraExtensionSession) {
+ Log.d(TAG, "CaptureSession - onConfigured: $session")
+ try {
+ val captureBuilder =
+ session.device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
+ captureBuilder.addTarget(previewSurface)
+ session.setRepeatingRequest(
+ captureBuilder.build(),
+ cameraTaskDispatcher.asExecutor(), captureCallbacks
+ )
+ cont.resume(session)
+ runOnUiThread { enableUiControl(true) }
+ } catch (e: CameraAccessException) {
+ Log.e(TAG, e.toString())
+ cont.resumeWithException(
+ RuntimeException("Failed to create capture session.")
+ )
+ }
+ }
+
+ override fun onConfigureFailed(session: CameraExtensionSession) {
+ Log.e(TAG, "CaptureSession - onConfigureFailed: $session")
+ cont.resumeWithException(
+ RuntimeException("Configure failed when creating capture session.")
+ )
+ }
+ }
+ )
+ try {
+ cameraDevice!!.createExtensionSession(extensionConfiguration)
+ } catch (e: CameraAccessException) {
+ Log.e(TAG, e.toString())
+ cont.resumeWithException(RuntimeException("Failed to create capture session."))
+ }
+ }
+
+ @Suppress("DEPRECATION") /* defaultDisplay */
+ private fun setupPreview() {
+ if (!textureView.isAvailable) {
+ Toast.makeText(
+ this, "TextureView is invalid!!",
+ Toast.LENGTH_SHORT
+ ).show()
+ finish()
+ return
+ }
+
+ val previewResolution = pickPreviewResolution(
+ cameraManager,
+ currentCameraId,
+ resources.displayMetrics,
+ currentExtensionMode
+ )
+
+ if (previewResolution == null) {
+ Toast.makeText(
+ this,
+ "Invalid preview extension sizes!.",
+ Toast.LENGTH_SHORT
+ ).show()
+ finish()
+ return
+ }
+
+ textureView.surfaceTexture?.setDefaultBufferSize(
+ previewResolution.width,
+ previewResolution.height
+ )
+ transformPreview(textureView, previewResolution, windowManager.defaultDisplay.rotation)
+ }
+
+ private fun setupImageReader(): ImageReader {
+ val (size, format) = pickStillImageResolution(
+ extensionCharacteristics,
+ currentExtensionMode
+ )
+
+ return ImageReader.newInstance(size.width, size.height, format, 1)
+ }
+
+ /**
+ * Takes a picture.
+ */
+ private fun takePicture() = lifecycleScope.launch(cameraTaskDispatcher) {
+ Preconditions.checkState(
+ cameraExtensionSession != null,
+ "take picture button is only enabled when session is configured successfully"
+ )
+ val session = cameraExtensionSession!!
+
+ var takePictureCompleter: Completer<Any?>? = null
+
+ imageSaveTerminationFuture = CallbackToFutureAdapter.getFuture<Any?> {
+ takePictureCompleter = it
+ "imageSaveTerminationFuture"
+ }
+
+ stillImageReader!!.setOnImageAvailableListener(
+ { reader: ImageReader ->
+ lifecycleScope.launch(cameraTaskDispatcher) {
+ acquireImageAndSave(reader)
+ stillImageReader!!.setOnImageAvailableListener(null, null)
+ takePictureCompleter?.set(null)
+ lifecycleScope.launch(Dispatchers.Main) {
+ enableUiControl(true)
+ }
+ }
+ }, Handler(Looper.getMainLooper())
+ )
+
+ val captureBuilder = session.device.createCaptureRequest(
+ CameraDevice.TEMPLATE_STILL_CAPTURE
+ )
+ captureBuilder.addTarget(stillImageReader!!.surface)
+
+ session.capture(
+ captureBuilder.build(),
+ cameraTaskDispatcher.asExecutor(),
+ object : CameraExtensionSession.ExtensionCaptureCallback() {
+ override fun onCaptureFailed(
+ session: CameraExtensionSession,
+ request: CaptureRequest
+ ) {
+ takePictureCompleter?.set(null)
+ Log.e(TAG, "Failed to take picture.")
+ }
+
+ override fun onCaptureSequenceCompleted(
+ session: CameraExtensionSession,
+ sequenceId: Int
+ ) {
+ Log.v(TAG, "onCaptureProcessSequenceCompleted: $sequenceId")
+ }
+ }
+ )
+ }
+
+ /**
+ * Acquires the latest image from the image reader and save it to the Pictures folder
+ */
+ private fun acquireImageAndSave(imageReader: ImageReader) {
+ try {
+ val formatter: Format =
+ SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
+ val fileName =
+ "[${formatter.format(Calendar.getInstance().time)}][Camera2]${
+ getExtensionModeStringFromId(currentExtensionMode)
+ }"
+
+ val rotationDegrees = calculateRelativeImageRotationDegrees(
+ (getDisplayRotationDegrees(display!!.rotation)),
+ cameraSensorRotationDegrees,
+ currentCameraId == backCameraId
+ )
+
+ imageReader.acquireLatestImage().let { image ->
+ val uri = FileUtil.saveImage(
+ image,
+ fileName,
+ ".jpg",
+ "Pictures/ExtensionsPictures",
+ contentResolver,
+ rotationDegrees
+ )
+
+ image.close()
+
+ val msg = if (uri != null) {
+ "Saved image to $fileName.jpg"
+ } else {
+ "Failed to save image."
+ }
+
+ lifecycleScope.launch(Dispatchers.Main) {
+ Toast.makeText(this@Camera2ExtensionsActivity, msg, Toast.LENGTH_SHORT).show()
+ }
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, e.toString())
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ val inflater = menuInflater
+ inflater.inflate(R.menu.main_menu_camera2_extensions_activity, menu)
+
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ R.id.menu_camerax_extensions -> {
+ closeCameraAndStartActivity(CameraExtensionsActivity::class.java.name)
+ return true
+ }
+ R.id.menu_validation_tool -> {
+ closeCameraAndStartActivity(CameraValidationResultActivity::class.java.name)
+ return true
+ }
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
+ private fun closeCameraAndStartActivity(className: String) {
+ // Needs to close the camera first. Otherwise, the next activity might be failed to open
+ // the camera and configure the capture session.
+ runBlocking {
+ closeCaptureSession().await()
+ closeCamera().await()
+ }
+
+ val intent = Intent()
+ intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
+ intent.setClassName(this, className)
+ startActivity(intent)
+ }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
index 5a0c0ce..47d80f8 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
@@ -35,6 +35,7 @@
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
+import android.view.ViewStub;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
@@ -273,8 +274,8 @@
captureButton.setOnClickListener((view) -> {
resetTakePictureIdlingResource();
- String fileName = formatter.format(Calendar.getInstance().getTime())
- + extensionModeString + ".jpg";
+ String fileName = "[" + formatter.format(Calendar.getInstance().getTime())
+ + "][CameraX]" + extensionModeString + ".jpg";
File saveFile = new File(dir, fileName);
ImageCapture.OutputFileOptions outputFileOptions;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@@ -332,9 +333,9 @@
sendBroadcast(intent);
}
- Toast.makeText(getApplicationContext(),
- "Saved image to " + saveFile,
- Toast.LENGTH_SHORT).show();
+ Toast.makeText(CameraExtensionsActivity.this,
+ "Saved image to " + fileName,
+ Toast.LENGTH_LONG).show();
}
}
@@ -383,7 +384,9 @@
StrictMode.VmPolicy policy =
new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build();
StrictMode.setVmPolicy(policy);
- mPreviewView = findViewById(R.id.previewView);
+ ViewStub viewFinderStub = findViewById(R.id.viewFinderStub);
+ viewFinderStub.setLayoutResource(R.layout.full_previewview);
+ mPreviewView = (PreviewView) viewFinderStub.inflate();
mFrameInfo = findViewById(R.id.frameInfo);
mPreviewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
setupPinchToZoomAndTapToFocus(mPreviewView);
@@ -427,19 +430,36 @@
@Override
public boolean onCreateOptionsMenu(@Nullable Menu menu) {
- MenuInflater inflater = getMenuInflater();
- inflater.inflate(R.menu.main_menu, menu);
+ if (menu != null) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.main_menu, menu);
+
+ // Remove Camera2Extensions implementation entry if the device API level is less than 32
+ if (Build.VERSION.SDK_INT < 31) {
+ menu.removeItem(R.id.menu_camera2_extensions);
+ }
+ }
return true;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
- if (item.getItemId() == R.id.menu_validation_tool) {
- Intent intent = new Intent(this, CameraValidationResultActivity.class);
- intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(intent);
- finish();
- return true;
+ Intent intent = new Intent();
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
+ switch (item.getItemId()) {
+ case R.id.menu_camera2_extensions:
+ if (Build.VERSION.SDK_INT >= 31) {
+ mCameraProvider.unbindAll();
+ intent.setClassName(this, Camera2ExtensionsActivity.class.getName());
+ startActivity(intent);
+ finish();
+ }
+ return true;
+ case R.id.menu_validation_tool:
+ intent.setClassName(this, CameraValidationResultActivity.class.getName());
+ startActivity(intent);
+ finish();
+ return true;
}
return super.onOptionsItemSelected(item);
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/Camera2ExtensionsUtil.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/Camera2ExtensionsUtil.kt
new file mode 100644
index 0000000..8bbd835
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/Camera2ExtensionsUtil.kt
@@ -0,0 +1,347 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.extensions.utils
+
+import android.annotation.SuppressLint
+import android.graphics.ImageFormat
+import android.graphics.Matrix
+import android.graphics.Point
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraExtensionCharacteristics
+import android.hardware.camera2.CameraExtensionSession
+import android.hardware.camera2.CameraExtensionSession.ExtensionCaptureCallback
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.CaptureRequest
+import android.os.Build
+import android.util.DisplayMetrics
+import android.util.Log
+import android.util.Size
+import android.view.Surface
+import android.view.TextureView
+import androidx.annotation.RequiresApi
+import java.math.BigDecimal
+import java.math.RoundingMode
+import java.util.stream.Collectors
+
+private const val TAG = "Camera2ExtensionsUtil"
+
+/**
+ * Util functions for Camera2 Extensions implementation
+ */
+object Camera2ExtensionsUtil {
+
+ /**
+ * Converts extension mode from integer to string.
+ */
+ @Suppress("DEPRECATION") // EXTENSION_BEAUTY
+ @JvmStatic
+ fun getExtensionModeStringFromId(extension: Int): String {
+ return when (extension) {
+ CameraExtensionCharacteristics.EXTENSION_HDR -> "HDR"
+ CameraExtensionCharacteristics.EXTENSION_NIGHT -> "NIGHT"
+ CameraExtensionCharacteristics.EXTENSION_BOKEH -> "BOKEH"
+ CameraExtensionCharacteristics.EXTENSION_BEAUTY -> "FACE RETOUCH"
+ else -> "AUTO"
+ }
+ }
+
+ /**
+ * Gets the first camera id of the specified lens facing.
+ */
+ @JvmStatic
+ fun getLensFacingCameraId(cameraManager: CameraManager, lensFacing: Int): String {
+ cameraManager.cameraIdList.forEach { cameraId ->
+ val characteristics = cameraManager.getCameraCharacteristics(cameraId)
+ if (characteristics[CameraCharacteristics.LENS_FACING] == lensFacing) {
+ characteristics[CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES]?.let {
+ if (it.contains(
+ CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE
+ )
+ ) {
+ return cameraId
+ }
+ }
+ }
+ }
+
+ throw IllegalArgumentException("Can't find camera of lens facing $lensFacing")
+ }
+
+ /**
+ * Creates a default extension capture callback implementation.
+ */
+ @RequiresApi(Build.VERSION_CODES.S)
+ @JvmStatic
+ fun createExtensionCaptureCallback(): ExtensionCaptureCallback {
+ return object : ExtensionCaptureCallback() {
+ override fun onCaptureStarted(
+ session: CameraExtensionSession,
+ request: CaptureRequest,
+ timestamp: Long
+ ) {
+ }
+
+ override fun onCaptureProcessStarted(
+ session: CameraExtensionSession,
+ request: CaptureRequest
+ ) {
+ }
+
+ override fun onCaptureFailed(
+ session: CameraExtensionSession,
+ request: CaptureRequest
+ ) {
+ Log.v(TAG, "onCaptureProcessFailed")
+ }
+
+ override fun onCaptureSequenceCompleted(
+ session: CameraExtensionSession,
+ sequenceId: Int
+ ) {
+ Log.v(TAG, "onCaptureProcessSequenceCompleted: $sequenceId")
+ }
+
+ override fun onCaptureSequenceAborted(
+ session: CameraExtensionSession,
+ sequenceId: Int
+ ) {
+ Log.v(TAG, "onCaptureProcessSequenceAborted: $sequenceId")
+ }
+ }
+ }
+
+ /**
+ * Picks a preview resolution that is both close/same as the display size and supported by camera
+ * and extensions.
+ */
+ @SuppressLint("ClassVerificationFailure")
+ @RequiresApi(Build.VERSION_CODES.S)
+ @JvmStatic
+ fun pickPreviewResolution(
+ cameraManager: CameraManager,
+ cameraId: String,
+ displayMetrics: DisplayMetrics,
+ extensionMode: Int
+ ): Size? {
+ val characteristics = cameraManager.getCameraCharacteristics(cameraId)
+ val map = characteristics.get(
+ CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
+ )
+ val textureSizes = map!!.getOutputSizes(
+ SurfaceTexture::class.java
+ )
+ val displaySize = Point()
+ displaySize.x = displayMetrics.widthPixels
+ displaySize.y = displayMetrics.heightPixels
+ if (displaySize.x < displaySize.y) {
+ displaySize.x = displayMetrics.heightPixels
+ displaySize.y = displayMetrics.widthPixels
+ }
+ val displayArRatio = displaySize.x.toFloat() / displaySize.y
+ val previewSizes = ArrayList<Size>()
+ for (sz in textureSizes) {
+ val arRatio = sz.width.toFloat() / sz.height
+ if (Math.abs(arRatio - displayArRatio) <= .2f) {
+ previewSizes.add(sz)
+ }
+ }
+ val extensionCharacteristics = cameraManager.getCameraExtensionCharacteristics(cameraId)
+ val extensionSizes = extensionCharacteristics.getExtensionSupportedSizes(
+ extensionMode, SurfaceTexture::class.java
+ )
+ if (extensionSizes.isEmpty()) {
+ return null
+ }
+
+ var previewSize = extensionSizes[0]
+ val supportedPreviewSizes =
+ previewSizes.stream().distinct().filter { o: Size -> extensionSizes.contains(o) }
+ .collect(Collectors.toList())
+ if (supportedPreviewSizes.isNotEmpty()) {
+ var currentDistance = Int.MAX_VALUE
+ for (sz in supportedPreviewSizes) {
+ val distance = Math.abs(sz.width * sz.height - displaySize.x * displaySize.y)
+ if (currentDistance > distance) {
+ currentDistance = distance
+ previewSize = sz
+ }
+ }
+ } else {
+ Log.w(
+ TAG, "No overlap between supported camera and extensions preview sizes using" +
+ " first available!"
+ )
+ }
+
+ return previewSize
+ }
+
+ /**
+ * Picks a resolution for still image capture.
+ */
+ @SuppressLint("ClassVerificationFailure")
+ @RequiresApi(Build.VERSION_CODES.S)
+ @JvmStatic
+ fun pickStillImageResolution(
+ extensionCharacteristics: CameraExtensionCharacteristics,
+ extensionMode: Int
+ ): Pair<Size, Int> {
+ val yuvColorEncodingSystemSizes = extensionCharacteristics.getExtensionSupportedSizes(
+ extensionMode, ImageFormat.YUV_420_888
+ )
+ val jpegSizes = extensionCharacteristics.getExtensionSupportedSizes(
+ extensionMode, ImageFormat.JPEG
+ )
+ val stillFormat = if (jpegSizes.isEmpty()) ImageFormat.YUV_420_888 else ImageFormat.JPEG
+ val stillCaptureSize =
+ if (jpegSizes.isEmpty()) yuvColorEncodingSystemSizes[0] else jpegSizes[0]
+
+ return Pair(stillCaptureSize, stillFormat)
+ }
+
+ /**
+ * Transforms the texture view to display the content of resolution in correct direction and
+ * aspect ratio.
+ */
+ @JvmStatic
+ fun transformPreview(textureView: TextureView, resolution: Size, displayRotation: Int) {
+ if (resolution.width == 0 || resolution.height == 0) {
+ return
+ }
+ if (textureView.width == 0 || textureView.height == 0) {
+ return
+ }
+ val matrix = Matrix()
+ val left: Int = textureView.left
+ val right: Int = textureView.right
+ val top: Int = textureView.top
+ val bottom: Int = textureView.bottom
+
+ // Compute the preview ui size based on the available width, height, and ui orientation.
+ val viewWidth = right - left
+ val viewHeight = bottom - top
+ val displayRotationDegrees: Int = getDisplayRotationDegrees(displayRotation)
+ val scaled: Size = calculatePreviewViewDimens(
+ resolution, viewWidth, viewHeight, displayRotation
+ )
+
+ // Compute the center of the view.
+ val centerX = (viewWidth / 2).toFloat()
+ val centerY = (viewHeight / 2).toFloat()
+
+ // Do corresponding rotation to correct the preview direction
+ matrix.postRotate((-displayRotationDegrees).toFloat(), centerX, centerY)
+
+ // Compute the scale value for center crop mode
+ var xScale = scaled.width / viewWidth.toFloat()
+ var yScale = scaled.height / viewHeight.toFloat()
+ if (displayRotationDegrees % 180 == 90) {
+ xScale = scaled.width / viewHeight.toFloat()
+ yScale = scaled.height / viewWidth.toFloat()
+ }
+
+ // Only two digits after the decimal point are valid for postScale. Need to get ceiling of
+ // two digits floating value to do the scale operation. Otherwise, the result may be scaled
+ // not large enough and will have some blank lines on the screen.
+ xScale = BigDecimal(xScale.toDouble()).setScale(2, RoundingMode.CEILING).toFloat()
+ yScale = BigDecimal(yScale.toDouble()).setScale(2, RoundingMode.CEILING).toFloat()
+
+ // Do corresponding scale to resolve the deformation problem
+ matrix.postScale(xScale, yScale, centerX, centerY)
+ textureView.setTransform(matrix)
+ }
+
+ /**
+ * Converts the display rotation to degrees value.
+ *
+ * @return One of 0, 90, 180, 270.
+ */
+ @JvmStatic
+ fun getDisplayRotationDegrees(displayRotation: Int): Int = when (displayRotation) {
+ Surface.ROTATION_0 -> 0
+ Surface.ROTATION_90 -> 90
+ Surface.ROTATION_180 -> 180
+ Surface.ROTATION_270 -> 270
+ else -> throw UnsupportedOperationException(
+ "Unsupported display rotation: $displayRotation"
+ )
+ }
+
+ /**
+ * Calculates the delta between a source rotation and destination rotation.
+ *
+ * <p>A typical use of this method would be calculating the angular difference between the
+ * display orientation (destRotationDegrees) and camera sensor orientation
+ * (sourceRotationDegrees).
+ *
+ * @param destRotationDegrees The destination rotation relative to the device's natural
+ * rotation.
+ * @param sourceRotationDegrees The source rotation relative to the device's natural rotation.
+ * @param isOppositeFacing Whether the source and destination planes are facing opposite
+ * directions.
+ */
+ @JvmStatic
+ fun calculateRelativeImageRotationDegrees(
+ destRotationDegrees: Int,
+ sourceRotationDegrees: Int,
+ isOppositeFacing: Boolean
+ ): Int {
+ val result: Int = if (isOppositeFacing) {
+ (sourceRotationDegrees - destRotationDegrees + 360) % 360
+ } else {
+ (sourceRotationDegrees + destRotationDegrees) % 360
+ }
+
+ return result
+ }
+
+ /**
+ * Calculates the preview size which can display the source image in correct aspect ratio.
+ */
+ @JvmStatic
+ private fun calculatePreviewViewDimens(
+ srcSize: Size,
+ parentWidth: Int,
+ parentHeight: Int,
+ displayRotation: Int
+ ): Size {
+ var inWidth = srcSize.width
+ var inHeight = srcSize.height
+ if (displayRotation == 0 || displayRotation == 180) {
+ // Need to reverse the width and height since we're in landscape orientation.
+ inWidth = srcSize.height
+ inHeight = srcSize.width
+ }
+ var outWidth = parentWidth
+ var outHeight = parentHeight
+ if (inWidth != 0 && inHeight != 0) {
+ val vfRatio = inWidth / inHeight.toFloat()
+ val parentRatio = parentWidth / parentHeight.toFloat()
+
+ // Match shortest sides together.
+ if (vfRatio < parentRatio) {
+ outWidth = parentWidth
+ outHeight = Math.round(parentWidth / vfRatio)
+ } else {
+ outWidth = Math.round(parentHeight * vfRatio)
+ outHeight = parentHeight
+ }
+ }
+ return Size(outWidth, outHeight)
+ }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/FileUtil.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/FileUtil.kt
new file mode 100644
index 0000000..dbc661d
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/FileUtil.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.extensions.utils
+
+import android.content.ContentResolver
+import android.content.ContentValues
+import android.graphics.ImageFormat
+import android.media.Image
+import android.net.Uri
+import android.os.Build
+import android.provider.MediaStore
+import android.util.Log
+import androidx.camera.core.impl.utils.Exif
+import androidx.core.net.toFile
+import androidx.core.net.toUri
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+
+private const val TAG = "FileUtil"
+
+/**
+ * File util functions
+ */
+object FileUtil {
+
+ /**
+ * Saves an [Image] to the specified file path. The format of the input [Image] must be JPEG or
+ * YUV_420_888 format.
+ */
+ @JvmStatic
+ fun saveImage(
+ image: Image,
+ fileNamePrefix: String,
+ fileNameSuffix: String,
+ relativePath: String,
+ contentResolver: ContentResolver,
+ rotationDegrees: Int
+ ): Uri? {
+ require((image.format == ImageFormat.JPEG) or (image.format == ImageFormat.YUV_420_888)) {
+ "Incorrect image format of the input image proxy: ${image.format}"
+ }
+
+ val fileName = if (fileNameSuffix.isNotEmpty() && fileNameSuffix[0] == '.') {
+ fileNamePrefix + fileNameSuffix
+ } else {
+ "$fileNamePrefix.$fileNameSuffix"
+ }
+
+ // Saves the image to the temp file
+ val tempFileUri =
+ saveImageToTempFile(image, fileNamePrefix, fileNameSuffix) ?: return null
+
+ // Updates Exif rotation tag info
+ val exif = Exif.createFromFile(tempFileUri.toFile())
+ exif.rotate(rotationDegrees)
+ exif.save()
+
+ val contentValues = ContentValues().apply {
+ put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
+ put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
+ put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
+ }
+
+ // Copies the temp file to the final output path
+ return copyTempFileToOutputLocation(
+ contentResolver,
+ tempFileUri,
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ contentValues
+ )
+ }
+
+ /**
+ * Saves an [Image] to a temp file.
+ */
+ @JvmStatic
+ fun saveImageToTempFile(
+ image: Image,
+ prefix: String,
+ suffix: String,
+ cacheDir: File? = null
+ ): Uri? {
+ val tempFile = File.createTempFile(
+ prefix,
+ suffix,
+ cacheDir
+ )
+
+ val byteArray = when (image.format) {
+ ImageFormat.JPEG -> {
+ ImageUtil.jpegImageToJpegByteArray(image)
+ }
+ ImageFormat.YUV_420_888 -> {
+ ImageUtil.yuvImageToJpegByteArray(image, 100)
+ }
+ else -> {
+ Log.e(TAG, "Incorrect image format of the input image proxy: ${image.format}")
+ return null
+ }
+ }
+
+ val outputStream = FileOutputStream(tempFile)
+ outputStream.write(byteArray)
+ outputStream.close()
+
+ return tempFile.toUri()
+ }
+
+ /**
+ * Copies temp file to the destination location.
+ *
+ * @return null if the copy process is failed.
+ */
+ @JvmStatic
+ fun copyTempFileToOutputLocation(
+ contentResolver: ContentResolver,
+ tempFileUri: Uri,
+ targetUrl: Uri,
+ contentValues: ContentValues,
+ ): Uri? {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+ Log.e(TAG, "The known devices which support Extensions should be at least" +
+ " Android Q!")
+ return null
+ }
+
+ contentValues.put(MediaStore.Images.Media.IS_PENDING, 1)
+
+ val outputUri = contentResolver.insert(targetUrl, contentValues) ?: return null
+
+ if (copyTempFileByteArrayToOutputLocation(
+ contentResolver,
+ tempFileUri,
+ outputUri
+ )
+ ) {
+ contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
+ contentResolver.update(outputUri, contentValues, null, null)
+ return outputUri
+ } else {
+ Log.e(TAG, "Failed to copy the temp file to the output path!")
+ }
+
+ return null
+ }
+
+ /**
+ * Copies temp file byte array to output [Uri].
+ *
+ * @return false if the [Uri] is not writable.
+ */
+ @JvmStatic
+ private fun copyTempFileByteArrayToOutputLocation(
+ contentResolver: ContentResolver,
+ tempFileUri: Uri,
+ uri: Uri
+ ): Boolean {
+ contentResolver.openOutputStream(uri).use { outputStream ->
+ if (tempFileUri.path == null || outputStream == null) {
+ return false
+ }
+
+ val tempFile = File(tempFileUri.path!!)
+
+ FileInputStream(tempFile).use { inputStream ->
+ val buf = ByteArray(1024)
+ var len: Int
+ while (inputStream.read(buf).also { len = it } > 0) {
+ outputStream.write(buf, 0, len)
+ }
+ }
+ }
+ return true
+ }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/ImageUtil.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/ImageUtil.kt
new file mode 100644
index 0000000..70c1cb5
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/ImageUtil.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.extensions.utils
+
+import android.graphics.ImageFormat
+import android.graphics.Rect
+import android.graphics.YuvImage
+import android.media.Image
+import androidx.annotation.IntRange
+import androidx.camera.core.ImageProxy
+import java.io.ByteArrayOutputStream
+
+/**
+ * Image util functions
+ */
+object ImageUtil {
+
+ /**
+ * Converts JPEG [Image] to [ByteArray]
+ */
+ @JvmStatic
+ fun jpegImageToJpegByteArray(image: Image): ByteArray {
+ require(image.format == ImageFormat.JPEG) {
+ "Incorrect image format of the input image proxy: ${image.format}"
+ }
+ val planes = image.planes
+ val buffer = planes[0].buffer
+ val data = ByteArray(buffer.capacity())
+ buffer.rewind()
+ buffer[data]
+ return data
+ }
+
+ /**
+ * Converts YUV_420_888 [ImageProxy] to JPEG byte array. The input YUV_420_888 image
+ * will be cropped if a non-null crop rectangle is specified. The output JPEG byte array will
+ * be compressed by the specified quality value.
+ */
+ @JvmStatic
+ fun yuvImageToJpegByteArray(
+ image: Image,
+ @IntRange(from = 1, to = 100) jpegQuality: Int
+ ): ByteArray {
+ require(image.format == ImageFormat.YUV_420_888) {
+ "Incorrect image format of the input image proxy: ${image.format}"
+ }
+ return nv21ToJpeg(
+ yuv_420_888toNv21(image),
+ image.width,
+ image.height,
+ jpegQuality
+ )
+ }
+
+ /**
+ * Converts nv21 byte array to JPEG format.
+ */
+ @JvmStatic
+ private fun nv21ToJpeg(
+ nv21: ByteArray,
+ width: Int,
+ height: Int,
+ @IntRange(from = 1, to = 100) jpegQuality: Int
+ ): ByteArray {
+ val out = ByteArrayOutputStream()
+ val yuv = YuvImage(nv21, ImageFormat.NV21, width, height, null)
+ val success = yuv.compressToJpeg(Rect(0, 0, width, height), jpegQuality, out)
+
+ if (!success) {
+ throw RuntimeException("YuvImage failed to encode jpeg.")
+ }
+ return out.toByteArray()
+ }
+
+ /**
+ * Converts a YUV [Image] to NV21 byte array.
+ */
+ @JvmStatic
+ private fun yuv_420_888toNv21(image: Image): ByteArray {
+ require(image.format == ImageFormat.YUV_420_888) {
+ "Incorrect image format of the input image proxy: ${image.format}"
+ }
+
+ val yPlane = image.planes[0]
+ val uPlane = image.planes[1]
+ val vPlane = image.planes[2]
+ val yBuffer = yPlane.buffer
+ val uBuffer = uPlane.buffer
+ val vBuffer = vPlane.buffer
+ yBuffer.rewind()
+ uBuffer.rewind()
+ vBuffer.rewind()
+ val ySize = yBuffer.remaining()
+ var position = 0
+ // TODO(b/115743986): Pull these bytes from a pool instead of allocating for every image.
+ val nv21 = ByteArray(ySize + image.width * image.height / 2)
+
+ // Add the full y buffer to the array. If rowStride > 1, some padding may be skipped.
+ for (row in 0 until image.height) {
+ yBuffer[nv21, position, image.width]
+ position += image.width
+ yBuffer.position(
+ Math.min(ySize, yBuffer.position() - image.width + yPlane.rowStride)
+ )
+ }
+ val chromaHeight = image.height / 2
+ val chromaWidth = image.width / 2
+ val vRowStride = vPlane.rowStride
+ val uRowStride = uPlane.rowStride
+ val vPixelStride = vPlane.pixelStride
+ val uPixelStride = uPlane.pixelStride
+
+ // Interleave the u and v frames, filling up the rest of the buffer. Use two line buffers to
+ // perform faster bulk gets from the byte buffers.
+ val vLineBuffer = ByteArray(vRowStride)
+ val uLineBuffer = ByteArray(uRowStride)
+ for (row in 0 until chromaHeight) {
+ vBuffer[vLineBuffer, 0, Math.min(vRowStride, vBuffer.remaining())]
+ uBuffer[uLineBuffer, 0, Math.min(uRowStride, uBuffer.remaining())]
+ var vLineBufferPosition = 0
+ var uLineBufferPosition = 0
+ for (col in 0 until chromaWidth) {
+ nv21[position++] = vLineBuffer[vLineBufferPosition]
+ nv21[position++] = uLineBuffer[uLineBufferPosition]
+ vLineBufferPosition += vPixelStride
+ uLineBufferPosition += uPixelStride
+ }
+ }
+ return nv21
+ }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageCaptureActivity.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageCaptureActivity.kt
index 33c44a1..013ef13 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageCaptureActivity.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageCaptureActivity.kt
@@ -19,7 +19,6 @@
import android.annotation.SuppressLint
import android.content.Intent
import android.content.res.Configuration
-import android.graphics.ImageFormat
import android.os.Bundle
import android.util.Log
import android.view.GestureDetector
@@ -30,11 +29,13 @@
import android.widget.Button
import android.widget.ImageButton
import android.widget.Toast
+import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.Camera
import androidx.camera.core.CameraControl
import androidx.camera.core.CameraInfo
import androidx.camera.core.DisplayOrientedMeteringPointFactory
+import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.FocusMeteringAction
import androidx.camera.core.FocusMeteringResult
import androidx.camera.core.ImageCapture
@@ -51,6 +52,7 @@
import androidx.camera.integration.extensions.R
import androidx.camera.integration.extensions.utils.CameraSelectorUtil.createCameraSelectorById
import androidx.camera.integration.extensions.utils.ExtensionModeUtil.getExtensionModeStringFromId
+import androidx.camera.integration.extensions.utils.FileUtil
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_CAMERA_ID
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_ERROR_CODE
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_EXTENSION_MODE
@@ -66,14 +68,11 @@
import androidx.concurrent.futures.await
import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils
-import androidx.core.net.toUri
import androidx.lifecycle.lifecycleScope
import com.google.common.util.concurrent.FutureCallback
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import kotlinx.coroutines.launch
-import java.io.File
-import java.io.FileOutputStream
private const val TAG = "ImageCaptureActivity"
@@ -196,6 +195,7 @@
}
}
+ @OptIn(markerClass = [ExperimentalGetImage::class])
private fun setupUiControls() {
// Sets up the flash toggle button
setUpFlashButton()
@@ -227,21 +227,22 @@
} else {
"$filenamePrefix[Disabled]"
}
- val tempFile = File.createTempFile(
- filename,
- "",
- codeCacheDir
- )
- val outputStream = FileOutputStream(tempFile)
- val byteArray = jpegImageToJpegByteArray(image)
- outputStream.write(byteArray)
- outputStream.close()
- result.putExtra(INTENT_EXTRA_KEY_IMAGE_URI, tempFile.toUri())
- result.putExtra(
- INTENT_EXTRA_KEY_IMAGE_ROTATION_DEGREES,
- image.imageInfo.rotationDegrees
- )
+ val uri =
+ FileUtil.saveImageToTempFile(image.image!!, filename, "", cacheDir)
+
+ if (uri == null) {
+ result.putExtra(
+ INTENT_EXTRA_KEY_ERROR_CODE,
+ ERROR_CODE_SAVE_IMAGE_FAILED
+ )
+ } else {
+ result.putExtra(INTENT_EXTRA_KEY_IMAGE_URI, uri)
+ result.putExtra(
+ INTENT_EXTRA_KEY_IMAGE_ROTATION_DEGREES,
+ image.imageInfo.rotationDegrees
+ )
+ }
finish()
}
@@ -456,25 +457,11 @@
extensionToggleButton.setImageResource(resourceId)
}
- /**
- * Converts JPEG [ImageProxy] to JPEG byte array.
- */
- internal fun jpegImageToJpegByteArray(image: ImageProxy): ByteArray {
- require(image.format == ImageFormat.JPEG) {
- "Incorrect image format of the input image proxy: ${image.format}"
- }
- val planes = image.planes
- val buffer = planes[0].buffer
- val data = ByteArray(buffer.capacity())
- buffer.rewind()
- buffer[data]
- return data
- }
-
companion object {
const val ERROR_CODE_NONE = 0
const val ERROR_CODE_BIND_FAIL = 1
const val ERROR_CODE_EXTENSION_MODE_NOT_SUPPORT = 2
const val ERROR_CODE_TAKE_PICTURE_FAILED = 3
+ const val ERROR_CODE_SAVE_IMAGE_FAILED = 4
}
}
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageValidationActivity.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageValidationActivity.kt
index 5e01f7a..2782d05 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageValidationActivity.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageValidationActivity.kt
@@ -35,6 +35,7 @@
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.integration.extensions.R
import androidx.camera.integration.extensions.utils.ExtensionModeUtil.getExtensionModeStringFromId
+import androidx.camera.integration.extensions.utils.FileUtil.copyTempFileToOutputLocation
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_CAMERA_ID
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_ERROR_CODE
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_EXTENSION_MODE
@@ -48,13 +49,13 @@
import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_BIND_FAIL
import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_EXTENSION_MODE_NOT_SUPPORT
import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_NONE
+import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_SAVE_IMAGE_FAILED
import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_TAKE_PICTURE_FAILED
import androidx.camera.integration.extensions.validation.PhotoFragment.Companion.decodeImageToBitmap
import androidx.camera.integration.extensions.validation.TestResults.Companion.INVALID_EXTENSION_MODE
import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_FAILED
import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_NOT_TESTED
import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_PASSED
-import androidx.camera.integration.extensions.validation.TestResults.Companion.copyTempFileToOutputLocation
import androidx.core.app.ActivityCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
@@ -127,7 +128,8 @@
// Returns with error
if (errorCode == ERROR_CODE_BIND_FAIL ||
errorCode == ERROR_CODE_EXTENSION_MODE_NOT_SUPPORT ||
- errorCode == ERROR_CODE_TAKE_PICTURE_FAILED
+ errorCode == ERROR_CODE_TAKE_PICTURE_FAILED ||
+ errorCode == ERROR_CODE_SAVE_IMAGE_FAILED
) {
result.putExtra(INTENT_EXTRA_KEY_TEST_RESULT, TEST_RESULT_FAILED)
Log.e(TAG, "Failed to take a picture with error code: $errorCode")
@@ -196,13 +198,14 @@
put(MediaStore.MediaColumns.RELATIVE_PATH, "Pictures/ExtensionsValidation")
}
- if (copyTempFileToOutputLocation(
- contentResolver,
- imageUris[viewPager.currentItem].first,
- MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
- contentValues
- )
- ) {
+ val outputUri = copyTempFileToOutputLocation(
+ contentResolver,
+ imageUris[viewPager.currentItem].first,
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ contentValues
+ )
+
+ if (outputUri != null) {
Toast.makeText(
this,
"Image is saved as Pictures/ExtensionsValidation/$savedFileName.",
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/TestResults.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/TestResults.kt
index c4becdf4..1a6fa1b 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/TestResults.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/TestResults.kt
@@ -20,8 +20,6 @@
import android.content.ContentValues
import android.content.Context
import android.hardware.camera2.CameraCharacteristics
-import android.net.Uri
-import android.os.Build
import android.os.Environment.DIRECTORY_DOCUMENTS
import android.provider.MediaStore
import android.util.Log
@@ -33,6 +31,7 @@
import androidx.camera.integration.extensions.utils.ExtensionModeUtil.AVAILABLE_EXTENSION_MODES
import androidx.camera.integration.extensions.utils.ExtensionModeUtil.getExtensionModeIdFromString
import androidx.camera.integration.extensions.utils.ExtensionModeUtil.getExtensionModeStringFromId
+import androidx.camera.integration.extensions.utils.FileUtil.copyTempFileToOutputLocation
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.net.toUri
import java.io.BufferedReader
@@ -127,7 +126,7 @@
testResultsFile.toUri(),
MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL),
contentValues
- )
+ ) != null
) {
return "$DIRECTORY_DOCUMENTS/ExtensionsValidation/$savedFileName"
}
@@ -240,72 +239,6 @@
}
companion object {
-
- /**
- * Copies temp file to the destination location.
- *
- * @return false if the copy process is failed.
- */
- fun copyTempFileToOutputLocation(
- contentResolver: ContentResolver,
- tempFileUri: Uri,
- targetUrl: Uri,
- contentValues: ContentValues,
- ): Boolean {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
- Log.e(TAG, "The known devices which support Extensions should be at least" +
- " Android Q!")
- return false
- }
-
- contentValues.put(MediaStore.Images.Media.IS_PENDING, 1)
-
- val outputUri = contentResolver.insert(targetUrl, contentValues)
-
- if (outputUri != null && copyTempFileToOutputLocation(
- contentResolver,
- tempFileUri,
- outputUri
- )
- ) {
- contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
- contentResolver.update(outputUri, contentValues, null, null)
- return true
- } else {
- Log.e(TAG, "Failed to copy the temp file to the output path!")
- }
-
- return false
- }
-
- /**
- * Copies temp file to output [Uri].
- *
- * @return false if the [Uri] is not writable.
- */
- private fun copyTempFileToOutputLocation(
- contentResolver: ContentResolver,
- tempFileUri: Uri,
- uri: Uri
- ): Boolean {
- contentResolver.openOutputStream(uri).use { outputStream ->
- if (tempFileUri.path == null || outputStream == null) {
- return false
- }
-
- val tempFile = File(tempFileUri.path!!)
-
- FileInputStream(tempFile).use { `in` ->
- val buf = ByteArray(1024)
- var len: Int
- while (`in`.read(buf).also { len = it } > 0) {
- outputStream.write(buf, 0, len)
- }
- }
- }
- return true
- }
-
const val INVALID_EXTENSION_MODE = -1
const val TEST_RESULT_NOT_SUPPORTED = -1
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/layout/activity_camera_extensions.xml b/camera/integration-tests/extensionstestapp/src/main/res/layout/activity_camera_extensions.xml
index 9ed60c5..1a8adfa 100644
--- a/camera/integration-tests/extensionstestapp/src/main/res/layout/activity_camera_extensions.xml
+++ b/camera/integration-tests/extensionstestapp/src/main/res/layout/activity_camera_extensions.xml
@@ -24,14 +24,14 @@
android:layout_height="match_parent"
tools:context="androidx.camera.integration.extensions.CameraExtensionsActivity">
- <androidx.camera.view.PreviewView
- android:id="@+id/previewView"
+ <ViewStub
+ android:id="@+id/viewFinderStub"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintStart_toStartOf="parent"/>
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/layout/full_previewview.xml b/camera/integration-tests/extensionstestapp/src/main/res/layout/full_previewview.xml
new file mode 100644
index 0000000..4c0f530
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/res/layout/full_previewview.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2022 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<androidx.camera.view.PreviewView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/layout/full_textureview.xml b/camera/integration-tests/extensionstestapp/src/main/res/layout/full_textureview.xml
new file mode 100644
index 0000000..18ebf88
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/res/layout/full_textureview.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2022 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<TextureView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu.xml b/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu.xml
index dc9d084..7fe8504 100644
--- a/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu.xml
+++ b/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu.xml
@@ -16,6 +16,9 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
+ android:id="@+id/menu_camera2_extensions"
+ android:title="Camera2 Extensions" />
+ <item
android:id="@+id/menu_validation_tool"
android:title="Validation Tool" />
</menu>
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu_camera2_extensions_activity.xml b/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu_camera2_extensions_activity.xml
new file mode 100644
index 0000000..bc8bdba
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu_camera2_extensions_activity.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_camerax_extensions"
+ android:title="CameraX Extensions" />
+ <item
+ android:id="@+id/menu_validation_tool"
+ android:title="Validation Tool" />
+</menu>
\ No newline at end of file
diff --git a/camera/integration-tests/uiwidgetstestapp/build.gradle b/camera/integration-tests/uiwidgetstestapp/build.gradle
index 072b0f5..9c8eef1 100644
--- a/camera/integration-tests/uiwidgetstestapp/build.gradle
+++ b/camera/integration-tests/uiwidgetstestapp/build.gradle
@@ -67,6 +67,7 @@
implementation(project(":camera:camera-camera2"))
implementation(project(":camera:camera-lifecycle"))
implementation(project(":camera:camera-view"))
+ implementation(project(":camera:camera-video"))
// Android Support Library
implementation("androidx.appcompat:appcompat:1.2.0")
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraNavHost.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraNavHost.kt
index cc6c150..0f0927f 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraNavHost.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraNavHost.kt
@@ -16,7 +16,6 @@
package androidx.camera.integration.uiwidgets.compose.ui.navigation
-import androidx.camera.integration.uiwidgets.compose.ui.screen.gallery.GalleryScreen
import androidx.camera.integration.uiwidgets.compose.ui.screen.imagecapture.ImageCaptureScreen
import androidx.camera.integration.uiwidgets.compose.ui.screen.videocapture.VideoCaptureScreen
import androidx.compose.runtime.Composable
@@ -42,9 +41,5 @@
composable(ComposeCameraScreen.VideoCapture.name) {
VideoCaptureScreen()
}
-
- composable(ComposeCameraScreen.Gallery.name) {
- GalleryScreen()
- }
}
}
\ No newline at end of file
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraScreen.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraScreen.kt
index 8923506..124fe4f 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraScreen.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraScreen.kt
@@ -18,7 +18,6 @@
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CameraAlt
-import androidx.compose.material.icons.filled.PhotoLibrary
import androidx.compose.material.icons.filled.Videocam
import androidx.compose.ui.graphics.vector.ImageVector
@@ -32,9 +31,6 @@
),
VideoCapture(
icon = Icons.Filled.Videocam
- ),
- Gallery(
- icon = Icons.Filled.PhotoLibrary
);
companion object {
@@ -42,7 +38,6 @@
return when (route?.substringBefore("/")) {
ImageCapture.name -> ImageCapture
VideoCapture.name -> VideoCapture
- Gallery.name -> Gallery
null -> defaultRoute
else -> throw IllegalArgumentException("Route $route is not recognized.")
}
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/components/CameraControlButton.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/components/CameraControlButton.kt
index 37c0d18..0f237e5 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/components/CameraControlButton.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/components/CameraControlButton.kt
@@ -20,8 +20,10 @@
import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
+import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
@@ -32,6 +34,7 @@
imageVector: ImageVector,
contentDescription: String,
modifier: Modifier = Modifier,
+ tint: Color = Color.Unspecified,
onClick: () -> Unit
) {
IconButton(
@@ -41,12 +44,24 @@
Icon(
imageVector = imageVector,
contentDescription = contentDescription,
- modifier = modifier.size(CAMERA_CONTROL_BUTTON_SIZE)
+ modifier = modifier.size(CAMERA_CONTROL_BUTTON_SIZE),
+ tint = tint
)
}
}
@Composable
+fun CameraControlText(
+ text: String,
+ modifier: Modifier = Modifier
+) {
+ Text(
+ text = text,
+ modifier = modifier.size(CAMERA_CONTROL_BUTTON_SIZE)
+ )
+}
+
+@Composable
fun CameraControlButtonPlaceholder(modifier: Modifier = Modifier) {
Spacer(modifier = modifier.size(CAMERA_CONTROL_BUTTON_SIZE))
}
\ No newline at end of file
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreen.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreen.kt
index 866ca73..914a04e 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreen.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreen.kt
@@ -17,16 +17,24 @@
package androidx.camera.integration.uiwidgets.compose.ui.screen.imagecapture
import android.view.ViewGroup
+import androidx.camera.core.MeteringPoint
+import androidx.camera.core.Preview.SurfaceProvider
import androidx.camera.integration.uiwidgets.compose.ui.screen.components.CameraControlButton
import androidx.camera.integration.uiwidgets.compose.ui.screen.components.CameraControlButtonPlaceholder
import androidx.camera.integration.uiwidgets.compose.ui.screen.components.CameraControlRow
import androidx.camera.view.PreviewView
+import androidx.compose.foundation.background
import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Slider
+import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.sharp.FlipCameraAndroid
import androidx.compose.material.icons.sharp.Lens
@@ -35,7 +43,9 @@
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
@@ -44,10 +54,54 @@
@Composable
fun ImageCaptureScreen(
modifier: Modifier = Modifier,
- stateHolder: ImageCaptureScreenStateHolder = rememberImageCaptureScreenStateHolder()
+ state: ImageCaptureScreenState = rememberImageCaptureScreenState()
) {
val lifecycleOwner = LocalLifecycleOwner.current
val localContext = LocalContext.current
+
+ LaunchedEffect(key1 = state.lensFacing) {
+ state.startCamera(context = localContext, lifecycleOwner = lifecycleOwner)
+ }
+
+ ImageCaptureScreen(
+ modifier = modifier,
+ zoomRatio = state.zoomRatio,
+ linearZoom = state.linearZoom,
+ onLinearZoomChange = state::setLinearZoom,
+ isCameraReady = state.isCameraReady,
+ hasFlashUnit = state.hasFlashUnit,
+ flashModeIcon = state.flashModeIcon,
+ onFlashModeIconClicked = state::toggleFlashMode,
+ onFlipCameraIconClicked = state::toggleLensFacing,
+ onImageCaptureIconClicked = {
+ state.takePhoto(localContext)
+ },
+ onSurfaceProviderReady = state::setSurfaceProvider,
+ onTouch = state::startTapToFocus
+ )
+}
+
+@Composable
+fun ImageCaptureScreen(
+ modifier: Modifier,
+ zoomRatio: Float,
+ linearZoom: Float,
+ onLinearZoomChange: (Float) -> Unit,
+ isCameraReady: Boolean,
+ hasFlashUnit: Boolean,
+ flashModeIcon: ImageVector,
+ onFlashModeIconClicked: () -> Unit,
+ onFlipCameraIconClicked: () -> Unit,
+ onImageCaptureIconClicked: () -> Unit,
+ onSurfaceProviderReady: (SurfaceProvider) -> Unit,
+ onTouch: (MeteringPoint) -> Unit
+) {
+ val localContext = LocalContext.current
+
+ // Saving an instance of PreviewView outside of AndroidView
+ // This allows us to access properties of PreviewView (e.g. ViewPort and OutputTransform)
+ // Allows us to support functionalities such as UseCaseGroup in bindToLifecycle()
+ // This instance needs to be carefully used in controlled environments (e.g. LaunchedEffect)
val previewView = remember {
PreviewView(localContext).apply {
layoutParams = ViewGroup.LayoutParams(
@@ -55,51 +109,78 @@
ViewGroup.LayoutParams.MATCH_PARENT
)
- stateHolder.setSurfaceProvider(this.surfaceProvider)
- }
- }
+ onSurfaceProviderReady(this.surfaceProvider)
- LaunchedEffect(key1 = stateHolder.lensFacing) {
- stateHolder.startCamera(context = localContext, lifecycleOwner = lifecycleOwner)
+ setOnTouchListener { view, motionEvent ->
+ val meteringPointFactory = (view as PreviewView).meteringPointFactory
+ val meteringPoint = meteringPointFactory.createPoint(motionEvent.x, motionEvent.y)
+ onTouch(meteringPoint)
+
+ return@setOnTouchListener true
+ }
+ }
}
Box(modifier = modifier.fillMaxSize()) {
AndroidView(
- factory = {
- previewView
- }
+ factory = { previewView }
)
- CameraControlRow(modifier = Modifier.align(Alignment.BottomCenter)) {
- CameraControlButton(
- imageVector = Icons.Sharp.FlipCameraAndroid,
- contentDescription = "Toggle Camera Lens",
- ) {
- stateHolder.toggleLensFacing()
+ Column(
+ modifier = Modifier.align(Alignment.BottomCenter),
+ verticalArrangement = Arrangement.Bottom
+ ) {
+
+ // Display Zoom Slider only when Camera is ready
+ if (isCameraReady) {
+ Row(
+ modifier = Modifier.padding(horizontal = 20.dp, vertical = 10.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(modifier = Modifier.weight(1f)) {
+ Slider(
+ value = linearZoom,
+ onValueChange = onLinearZoomChange
+ )
+ }
+
+ Text(
+ text = "%.2f x".format(zoomRatio),
+ modifier = Modifier
+ .padding(horizontal = 10.dp)
+ .background(Color.White)
+ )
+ }
}
- CameraControlButton(
- imageVector = Icons.Sharp.Lens,
- contentDescription = "Image Capture",
- modifier = Modifier
- .padding(1.dp)
- .border(1.dp, MaterialTheme.colors.onSecondary, CircleShape)
- ) {
- stateHolder.takePhoto(localContext)
- }
-
- if (stateHolder.hasFlashMode) {
+ CameraControlRow {
CameraControlButton(
- imageVector = stateHolder.flashModeIcon,
- contentDescription = "Toggle Flash Mode",
+ imageVector = Icons.Sharp.FlipCameraAndroid,
+ contentDescription = "Toggle Camera Lens",
+ onClick = onFlipCameraIconClicked
+ )
+
+ CameraControlButton(
+ imageVector = Icons.Sharp.Lens,
+ contentDescription = "Image Capture",
modifier = Modifier
.padding(1.dp)
- .border(1.dp, MaterialTheme.colors.onSecondary, RectangleShape)
- ) {
- stateHolder.toggleFlashMode()
+ .border(1.dp, MaterialTheme.colors.onSecondary, CircleShape),
+ onClick = onImageCaptureIconClicked
+ )
+
+ if (hasFlashUnit) {
+ CameraControlButton(
+ imageVector = flashModeIcon,
+ contentDescription = "Toggle Flash Mode",
+ modifier = Modifier
+ .padding(1.dp)
+ .border(1.dp, MaterialTheme.colors.onSecondary, RectangleShape),
+ onClick = onFlashModeIconClicked
+ )
+ } else {
+ CameraControlButtonPlaceholder()
}
- } else {
- CameraControlButtonPlaceholder()
}
}
}
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreenStateHolder.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreenState.kt
similarity index 71%
rename from camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreenStateHolder.kt
rename to camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreenState.kt
index a3d1349..ee8bd3c 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreenStateHolder.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreenState.kt
@@ -22,9 +22,13 @@
import android.provider.MediaStore
import android.util.Log
import android.widget.Toast
+import androidx.camera.core.Camera
+import androidx.camera.core.CameraControl.OperationCanceledException
import androidx.camera.core.CameraSelector
+import androidx.camera.core.FocusMeteringAction
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
+import androidx.camera.core.MeteringPoint
import androidx.camera.core.Preview
import androidx.camera.core.Preview.SurfaceProvider
import androidx.camera.lifecycle.ProcessCameraProvider
@@ -40,22 +44,28 @@
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.concurrent.futures.await
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import java.text.SimpleDateFormat
import java.util.Locale
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
private const val DEFAULT_LENS_FACING = CameraSelector.LENS_FACING_FRONT
private const val DEFAULT_FLASH_MODE = ImageCapture.FLASH_MODE_OFF
-class ImageCaptureScreenStateHolder(
+class ImageCaptureScreenState(
initialLensFacing: Int = DEFAULT_LENS_FACING,
initialFlashMode: Int = DEFAULT_FLASH_MODE
) {
var lensFacing by mutableStateOf(initialLensFacing)
private set
- var hasFlashMode by mutableStateOf(false)
+ var hasFlashUnit by mutableStateOf(false)
+ private set
+
+ var isCameraReady by mutableStateOf(false)
private set
var flashMode: Int by mutableStateOf(getValidInitialFlashMode(initialFlashMode))
@@ -65,17 +75,49 @@
private set
get() = getFlashModeImageVector()
+ var linearZoom by mutableStateOf(0f)
+ private set
+
+ var zoomRatio by mutableStateOf(1f)
+ private set
+
private val preview = Preview.Builder().build()
private val imageCapture = ImageCapture
.Builder()
.setFlashMode(flashMode)
.build()
+ private var camera: Camera? = null
+
+ private val mainScope = MainScope()
+
fun setSurfaceProvider(surfaceProvider: SurfaceProvider) {
Log.d(TAG, "Setting Surface Provider")
preview.setSurfaceProvider(surfaceProvider)
}
+ @JvmName("setLinearZoomFunction")
+ fun setLinearZoom(linearZoom: Float) {
+ Log.d(TAG, "Setting Linear Zoom $linearZoom")
+
+ if (camera == null) {
+ Log.d(TAG, "Camera is not ready to set Linear Zoom")
+ return
+ }
+
+ val future = camera!!.cameraControl.setLinearZoom(linearZoom)
+ mainScope.launch {
+ try {
+ future.await()
+ } catch (exc: Exception) {
+ // Log errors not related to CameraControl.OperationCanceledException
+ if (exc !is OperationCanceledException) {
+ Log.w(TAG, "setLinearZoom: $linearZoom failed. ${exc.message}")
+ }
+ }
+ }
+ }
+
fun toggleLensFacing() {
Log.d(TAG, "Toggling Lens")
lensFacing = if (lensFacing == CameraSelector.LENS_FACING_BACK) {
@@ -104,8 +146,14 @@
imageCapture.flashMode = flashMode
}
+ fun startTapToFocus(meteringPoint: MeteringPoint) {
+ val action = FocusMeteringAction.Builder(meteringPoint).build()
+ camera?.cameraControl?.startFocusAndMetering(action)
+ }
+
fun startCamera(context: Context, lifecycleOwner: LifecycleOwner) {
Log.d(TAG, "Starting Camera")
+
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
@@ -116,6 +164,14 @@
.requireLensFacing(lensFacing)
.build()
+ // Remove observers from the old camera instance
+ removeZoomStateObservers(lifecycleOwner)
+
+ // Reset internal State of Camera
+ camera = null
+ hasFlashUnit = false
+ isCameraReady = false
+
try {
cameraProvider.unbindAll()
val camera = cameraProvider.bindToLifecycle(
@@ -126,7 +182,10 @@
)
// Setup components that require Camera
- this.hasFlashMode = camera.cameraInfo.hasFlashUnit()
+ this.camera = camera
+ setupZoomStateObserver(lifecycleOwner)
+ hasFlashUnit = camera.cameraInfo.hasFlashUnit()
+ isCameraReady = true
} catch (exc: Exception) {
Log.e(TAG, "Use Cases binding failed", exc)
}
@@ -201,6 +260,32 @@
}
}
+ private fun setupZoomStateObserver(lifecycleOwner: LifecycleOwner) {
+ Log.d(TAG, "Setting up Zoom State Observer")
+
+ if (camera == null) {
+ Log.d(TAG, "Camera is not ready to set up observer")
+ return
+ }
+
+ removeZoomStateObservers(lifecycleOwner)
+ camera!!.cameraInfo.zoomState.observe(lifecycleOwner) { state ->
+ linearZoom = state.linearZoom
+ zoomRatio = state.zoomRatio
+ }
+ }
+
+ private fun removeZoomStateObservers(lifecycleOwner: LifecycleOwner) {
+ Log.d(TAG, "Removing Observers")
+
+ if (camera == null) {
+ Log.d(TAG, "Camera is not present to remove observers")
+ return
+ }
+
+ camera!!.cameraInfo.zoomState.removeObservers(lifecycleOwner)
+ }
+
companion object {
private const val TAG = "ImageCaptureScreenState"
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
@@ -209,12 +294,12 @@
ImageCapture.FLASH_MODE_OFF,
ImageCapture.FLASH_MODE_AUTO
)
- val saver: Saver<ImageCaptureScreenStateHolder, *> = listSaver(
+ val saver: Saver<ImageCaptureScreenState, *> = listSaver(
save = {
listOf(it.lensFacing, it.flashMode)
},
restore = {
- ImageCaptureScreenStateHolder(
+ ImageCaptureScreenState(
initialLensFacing = it[0],
initialFlashMode = it[1]
)
@@ -224,16 +309,16 @@
}
@Composable
-fun rememberImageCaptureScreenStateHolder(
+fun rememberImageCaptureScreenState(
initialLensFacing: Int = DEFAULT_LENS_FACING,
initialFlashMode: Int = DEFAULT_FLASH_MODE
-): ImageCaptureScreenStateHolder {
+): ImageCaptureScreenState {
return rememberSaveable(
initialLensFacing,
initialFlashMode,
- saver = ImageCaptureScreenStateHolder.saver
+ saver = ImageCaptureScreenState.saver
) {
- ImageCaptureScreenStateHolder(
+ ImageCaptureScreenState(
initialLensFacing = initialLensFacing,
initialFlashMode = initialFlashMode
)
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreen.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreen.kt
index 726739f..acc60e6 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreen.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreen.kt
@@ -16,10 +16,171 @@
package androidx.camera.integration.uiwidgets.compose.ui.screen.videocapture
+import android.view.ViewGroup
+import androidx.camera.core.MeteringPoint
+import androidx.camera.core.Preview.SurfaceProvider
+import androidx.camera.integration.uiwidgets.compose.ui.screen.components.CameraControlButton
+import androidx.camera.integration.uiwidgets.compose.ui.screen.components.CameraControlRow
+import androidx.camera.integration.uiwidgets.compose.ui.screen.components.CameraControlText
+import androidx.camera.view.PreviewView
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Slider
import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.sharp.FlipCameraAndroid
+import androidx.compose.material.icons.sharp.Lens
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
@Composable
-fun VideoCaptureScreen() {
- Text("Video Capture Screen")
+fun VideoCaptureScreen(
+ modifier: Modifier = Modifier,
+ state: VideoCaptureScreenState = rememberVideoCaptureScreenState()
+) {
+ val lifecycleOwner = LocalLifecycleOwner.current
+ val localContext = LocalContext.current
+
+ LaunchedEffect(key1 = state.lensFacing) {
+ state.startCamera(context = localContext, lifecycleOwner = lifecycleOwner)
+ }
+
+ VideoCaptureScreen(
+ modifier = modifier,
+ zoomRatio = state.zoomRatio,
+ linearZoom = state.linearZoom,
+ onLinearZoomChange = state::setLinearZoom,
+ isCameraReady = state.isCameraReady,
+ recordState = state.recordState,
+ recordingStatsMsg = state.recordingStatsMsg,
+ onFlipCameraIconClicked = state::toggleLensFacing,
+ onVideoCaptureIconClicked = {
+ state.captureVideo(localContext)
+ },
+ onSurfaceProviderReady = state::setSurfaceProvider,
+ onTouch = state::startTapToFocus
+ )
+}
+
+@Composable
+fun VideoCaptureScreen(
+ modifier: Modifier = Modifier,
+ zoomRatio: Float,
+ linearZoom: Float,
+ onLinearZoomChange: (Float) -> Unit,
+ isCameraReady: Boolean,
+ recordState: VideoCaptureScreenState.RecordState,
+ recordingStatsMsg: String,
+ onFlipCameraIconClicked: () -> Unit,
+ onVideoCaptureIconClicked: () -> Unit,
+ onSurfaceProviderReady: (SurfaceProvider) -> Unit,
+ onTouch: (MeteringPoint) -> Unit
+) {
+ val localContext = LocalContext.current
+
+ val previewView = remember {
+ PreviewView(localContext).apply {
+ layoutParams = ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+
+ onSurfaceProviderReady(this.surfaceProvider)
+
+ setOnTouchListener { view, motionEvent ->
+ val meteringPointFactory = (view as PreviewView).meteringPointFactory
+ val meteringPoint = meteringPointFactory.createPoint(motionEvent.x, motionEvent.y)
+ onTouch(meteringPoint)
+
+ return@setOnTouchListener true
+ }
+ }
+ }
+
+ Box(modifier = modifier.fillMaxSize()) {
+ AndroidView(
+ factory = { previewView }
+ )
+
+ Column(
+ modifier = Modifier.align(Alignment.BottomCenter),
+ verticalArrangement = Arrangement.Bottom
+ ) {
+
+ // Display Zoom Slider only when Camera is ready
+ if (isCameraReady) {
+ Row(
+ modifier = Modifier.padding(horizontal = 20.dp, vertical = 10.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(modifier = Modifier.weight(1f)) {
+ Slider(
+ value = linearZoom,
+ onValueChange = onLinearZoomChange
+ )
+ }
+
+ Text(
+ text = "%.2f x".format(zoomRatio),
+ modifier = Modifier
+ .padding(horizontal = 10.dp)
+ .background(Color.White)
+ )
+ }
+ }
+
+ CameraControlRow {
+ CameraControlButton(
+ imageVector = Icons.Sharp.FlipCameraAndroid,
+ contentDescription = "Toggle Camera Lens",
+ onClick = onFlipCameraIconClicked
+ )
+
+ VideoRecordButton(
+ recordState = recordState,
+ onVideoCaptureIconClicked = onVideoCaptureIconClicked
+ )
+
+ CameraControlText(text = recordingStatsMsg)
+ }
+ }
+ }
+}
+
+@Composable
+private fun VideoRecordButton(
+ recordState: VideoCaptureScreenState.RecordState,
+ onVideoCaptureIconClicked: () -> Unit
+) {
+ val iconColor = when (recordState) {
+ VideoCaptureScreenState.RecordState.IDLE -> Color.Black
+ VideoCaptureScreenState.RecordState.RECORDING -> Color.Red
+ VideoCaptureScreenState.RecordState.STOPPING -> Color.Gray
+ }
+
+ CameraControlButton(
+ imageVector = Icons.Sharp.Lens,
+ contentDescription = "Video Capture",
+ modifier = Modifier
+ .padding(1.dp)
+ .border(1.dp, MaterialTheme.colors.onSecondary, CircleShape),
+ tint = iconColor,
+ onClick = onVideoCaptureIconClicked
+ )
}
\ No newline at end of file
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreenState.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreenState.kt
new file mode 100644
index 0000000..bd6b3cc
--- /dev/null
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreenState.kt
@@ -0,0 +1,322 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.uiwidgets.compose.ui.screen.videocapture
+
+import android.Manifest
+import android.content.ContentValues
+import android.content.Context
+import android.os.Build
+import android.provider.MediaStore
+import android.util.Log
+import android.widget.Toast
+import androidx.camera.core.Camera
+import androidx.camera.core.CameraControl
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.FocusMeteringAction
+import androidx.camera.core.MeteringPoint
+import androidx.camera.core.Preview
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.video.MediaStoreOutputOptions
+import androidx.camera.video.Quality
+import androidx.camera.video.QualitySelector
+import androidx.camera.video.Recorder
+import androidx.camera.video.Recording
+import androidx.camera.video.VideoCapture
+import androidx.camera.video.VideoRecordEvent
+import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_NONE
+import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_SOURCE_INACTIVE
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.concurrent.futures.await
+import androidx.core.content.ContextCompat
+import androidx.core.content.PermissionChecker
+import androidx.lifecycle.LifecycleOwner
+import java.text.SimpleDateFormat
+import java.util.Locale
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+
+private const val DEFAULT_LENS_FACING = CameraSelector.LENS_FACING_FRONT
+
+class VideoCaptureScreenState(
+ initialLensFacing: Int = DEFAULT_LENS_FACING
+) {
+ var lensFacing by mutableStateOf(initialLensFacing)
+ private set
+
+ var isCameraReady by mutableStateOf(false)
+ private set
+
+ var linearZoom by mutableStateOf(0f)
+ private set
+
+ var zoomRatio by mutableStateOf(1f)
+ private set
+
+ private var recording: Recording? = null
+
+ var recordState by mutableStateOf(RecordState.IDLE)
+ private set
+
+ var recordingStatsMsg by mutableStateOf("")
+ private set
+
+ private val preview = Preview.Builder().build()
+ private lateinit var recorder: Recorder
+ private lateinit var videoCapture: VideoCapture<Recorder>
+
+ private var camera: Camera? = null
+
+ private val mainScope = MainScope()
+
+ fun setSurfaceProvider(surfaceProvider: Preview.SurfaceProvider) {
+ Log.d(TAG, "Setting Surface Provider")
+ preview.setSurfaceProvider(surfaceProvider)
+ }
+
+ @JvmName("setLinearZoomFunction")
+ fun setLinearZoom(linearZoom: Float) {
+ Log.d(TAG, "Setting Linear Zoom $linearZoom")
+
+ if (camera == null) {
+ Log.d(TAG, "Camera is not ready to set Linear Zoom")
+ return
+ }
+
+ val future = camera!!.cameraControl.setLinearZoom(linearZoom)
+ mainScope.launch {
+ try {
+ future.await()
+ } catch (exc: Exception) {
+ // Log errors not related to CameraControl.OperationCanceledException
+ if (exc !is CameraControl.OperationCanceledException) {
+ Log.w(TAG, "setLinearZoom: $linearZoom failed. ${exc.message}")
+ }
+ }
+ }
+ }
+
+ fun toggleLensFacing() {
+ Log.d(TAG, "Toggling Lens")
+ lensFacing = if (lensFacing == CameraSelector.LENS_FACING_BACK) {
+ CameraSelector.LENS_FACING_FRONT
+ } else {
+ CameraSelector.LENS_FACING_BACK
+ }
+ }
+
+ fun startTapToFocus(meteringPoint: MeteringPoint) {
+ val action = FocusMeteringAction.Builder(meteringPoint).build()
+ camera?.cameraControl?.startFocusAndMetering(action)
+ }
+
+ fun startCamera(context: Context, lifecycleOwner: LifecycleOwner) {
+ Log.d(TAG, "Starting Camera")
+ val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
+
+ cameraProviderFuture.addListener({
+ val cameraProvider = cameraProviderFuture.get()
+
+ // Create a new recorder. CameraX currently does not support re-use of Recorder
+ recorder =
+ Recorder.Builder().setQualitySelector(QualitySelector.from(Quality.HIGHEST)).build()
+ videoCapture = VideoCapture.withOutput(recorder)
+
+ val cameraSelector = CameraSelector
+ .Builder()
+ .requireLensFacing(lensFacing)
+ .build()
+
+ // Remove observers from the old camera instance
+ removeZoomStateObservers(lifecycleOwner)
+
+ // Reset internal State of Camera
+ camera = null
+ isCameraReady = false
+
+ try {
+ cameraProvider.unbindAll()
+ val camera = cameraProvider.bindToLifecycle(
+ lifecycleOwner,
+ cameraSelector,
+ preview,
+ videoCapture
+ )
+
+ this.camera = camera
+ setupZoomStateObserver(lifecycleOwner)
+ isCameraReady = true
+ } catch (exc: Exception) {
+ Log.e(TAG, "Use Cases binding failed", exc)
+ }
+ }, ContextCompat.getMainExecutor(context))
+ }
+
+ fun captureVideo(context: Context) {
+ Log.d(TAG, "Capture Video")
+
+ // Disable button if CameraX is already stopping the recording
+ if (recordState == RecordState.STOPPING) {
+ return
+ }
+
+ // Stop current recording session
+ val curRecording = recording
+ if (curRecording != null) {
+ Log.d(TAG, "Recording session exists. Stop recording")
+ recordState = RecordState.STOPPING
+ curRecording.stop()
+ return
+ }
+
+ Log.d(TAG, "Start recording video")
+ val mediaStoreOutputOptions = getMediaStoreOutputOptions(context)
+
+ recording = videoCapture.output
+ .prepareRecording(context, mediaStoreOutputOptions)
+ .apply {
+ val recordAudioPermission = PermissionChecker.checkSelfPermission(
+ context,
+ Manifest.permission.RECORD_AUDIO
+ )
+
+ if (recordAudioPermission == PermissionChecker.PERMISSION_GRANTED) {
+ withAudioEnabled()
+ }
+ }
+ .start(ContextCompat.getMainExecutor(context)) { recordEvent ->
+ // Update record stats
+ val recordingStats = recordEvent.recordingStats
+ val durationMs = TimeUnit.NANOSECONDS.toMillis(recordingStats.recordedDurationNanos)
+ val sizeMb = recordingStats.numBytesRecorded / (1000f * 1000f)
+ val msg = "%.2f s\n%.2f MB".format(durationMs / 1000f, sizeMb)
+ recordingStatsMsg = msg
+
+ when (recordEvent) {
+ is VideoRecordEvent.Start -> {
+ recordState = RecordState.RECORDING
+ }
+ is VideoRecordEvent.Finalize -> {
+ // Once finalized, save the file if it is created
+ val cause = recordEvent.cause
+ when (val errorCode = recordEvent.error) {
+ ERROR_NONE, ERROR_SOURCE_INACTIVE -> { // Save Output
+ val uri = recordEvent.outputResults.outputUri
+ val successMsg = "Video saved at $uri. Code: $errorCode"
+ Log.d(TAG, successMsg, cause)
+ Toast.makeText(context, successMsg, Toast.LENGTH_SHORT).show()
+ }
+ else -> { // Handle Error
+ val failureMsg = "VideoCapture Error($errorCode): $cause"
+ Log.e(TAG, failureMsg, cause)
+ }
+ }
+
+ // Tear down recording
+ recordState = RecordState.IDLE
+ recording = null
+ recordingStatsMsg = ""
+ }
+ }
+ }
+ }
+
+ private fun getMediaStoreOutputOptions(context: Context): MediaStoreOutputOptions {
+ val contentResolver = context.contentResolver
+ val displayName = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
+ .format(System.currentTimeMillis())
+ val contentValues = ContentValues().apply {
+ put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
+ put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
+ put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
+ }
+ }
+
+ return MediaStoreOutputOptions
+ .Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
+ .setContentValues(contentValues)
+ .build()
+ }
+
+ private fun setupZoomStateObserver(lifecycleOwner: LifecycleOwner) {
+ Log.d(TAG, "Setting up Zoom State Observer")
+
+ if (camera == null) {
+ Log.d(TAG, "Camera is not ready to set up observer")
+ return
+ }
+
+ removeZoomStateObservers(lifecycleOwner)
+ camera!!.cameraInfo.zoomState.observe(lifecycleOwner) { state ->
+ linearZoom = state.linearZoom
+ zoomRatio = state.zoomRatio
+ }
+ }
+
+ private fun removeZoomStateObservers(lifecycleOwner: LifecycleOwner) {
+ Log.d(TAG, "Removing Observers")
+
+ if (camera == null) {
+ Log.d(TAG, "Camera is not present to remove observers")
+ return
+ }
+
+ camera!!.cameraInfo.zoomState.removeObservers(lifecycleOwner)
+ }
+
+ enum class RecordState {
+ IDLE,
+ RECORDING,
+ STOPPING
+ }
+
+ companion object {
+ private const val TAG = "VideoCaptureScreenState"
+ private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
+ val saver: Saver<VideoCaptureScreenState, *> = listSaver(
+ save = {
+ listOf(it.lensFacing)
+ },
+ restore = {
+ VideoCaptureScreenState(
+ initialLensFacing = it[0]
+ )
+ }
+ )
+ }
+}
+
+@Composable
+fun rememberVideoCaptureScreenState(
+ initialLensFacing: Int = DEFAULT_LENS_FACING
+): VideoCaptureScreenState {
+ return rememberSaveable(
+ initialLensFacing,
+ saver = VideoCaptureScreenState.saver
+ ) {
+ VideoCaptureScreenState(
+ initialLensFacing = initialLensFacing
+ )
+ }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt b/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
index 28fe8db..408f301 100644
--- a/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
+++ b/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
@@ -135,6 +135,21 @@
}
@Test
+ fun enableEffect_effectIsEnabled() {
+ // Arrange: launch app and verify effect is inactive.
+ fragment.assertPreviewIsStreaming()
+ assertThat(fragment.mSurfaceEffect.isSurfaceRequestedAndProvided()).isFalse()
+
+ // Act: turn on effect.
+ val effectToggleId = "androidx.camera.integration.view:id/effect_toggle"
+ uiDevice.findObject(UiSelector().resourceId(effectToggleId)).click()
+ instrumentation.waitForIdleSync()
+
+ // Assert: verify that effect is active.
+ assertThat(fragment.mSurfaceEffect.isSurfaceRequestedAndProvided()).isTrue()
+ }
+
+ @Test
fun controllerBound_canGetCameraControl() {
fragment.assertPreviewIsStreaming()
instrumentation.runOnMainSync {
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
index e765274..14833f1 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
@@ -16,6 +16,8 @@
package androidx.camera.integration.view;
+import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
+
import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.ContentValues;
@@ -40,12 +42,13 @@
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.CameraSelector;
+import androidx.camera.core.EffectBundle;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.Logger;
+import androidx.camera.core.SurfaceEffect;
import androidx.camera.core.ZoomState;
-import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.impl.utils.futures.FutureCallback;
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.camera.view.CameraController;
@@ -86,6 +89,7 @@
private FrameLayout mContainer;
private Button mFlashMode;
private ToggleButton mCameraToggle;
+ private ToggleButton mEffectToggle;
private ExecutorService mExecutorService;
private ToggleButton mCaptureEnabledToggle;
private ToggleButton mAnalysisEnabledToggle;
@@ -106,6 +110,9 @@
@Nullable
private ImageAnalysis.Analyzer mWrappedAnalyzer;
+ @VisibleForTesting
+ ToneMappingSurfaceEffect mSurfaceEffect;
+
private final ImageAnalysis.Analyzer mAnalyzer = image -> {
byte[] bytes = new byte[image.getPlanes()[0].getBuffer().remaining()];
image.getPlanes()[0].getBuffer().get(bytes);
@@ -134,7 +141,7 @@
mExecutorService = Executors.newSingleThreadExecutor();
mRotationProvider = new RotationProvider(requireContext());
boolean canDetectRotation = mRotationProvider.addListener(
- CameraXExecutors.mainThreadExecutor(), mRotationListener);
+ mainThreadExecutor(), mRotationListener);
if (!canDetectRotation) {
Logger.e(TAG, "The device cannot detect rotation with motion sensor.");
}
@@ -159,6 +166,12 @@
}
});
+ // Set up post-processing effects.
+ mSurfaceEffect = new ToneMappingSurfaceEffect();
+ mEffectToggle = view.findViewById(R.id.effect_toggle);
+ mEffectToggle.setOnCheckedChangeListener((compoundButton, isChecked) -> onEffectsToggled());
+ onEffectsToggled();
+
// Set up the button to change the PreviewView's size.
view.findViewById(R.id.shrink).setOnClickListener(v -> {
// Shrinks PreviewView by 10% each time it's clicked.
@@ -341,6 +354,17 @@
mExecutorService.shutdown();
}
mRotationProvider.removeListener(mRotationListener);
+ mSurfaceEffect.release();
+ }
+
+ private void onEffectsToggled() {
+ if (mEffectToggle.isChecked()) {
+ mCameraController.setEffectBundle(new EffectBundle.Builder(mainThreadExecutor())
+ .addEffect(SurfaceEffect.PREVIEW, mSurfaceEffect)
+ .build());
+ } else if (mSurfaceEffect != null) {
+ mCameraController.setEffectBundle(null);
+ }
}
void checkFailedFuture(ListenableFuture<Void> voidFuture) {
@@ -355,7 +379,7 @@
public void onFailure(@NonNull Throwable t) {
toast(t.getMessage());
}
- }, CameraXExecutors.mainThreadExecutor());
+ }, mainThreadExecutor());
}
// Synthetic access
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/ToneMappingSurfaceEffect.kt b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/ToneMappingSurfaceEffect.kt
new file mode 100644
index 0000000..2ed13ae
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/ToneMappingSurfaceEffect.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.view
+
+import android.graphics.SurfaceTexture
+import android.graphics.SurfaceTexture.OnFrameAvailableListener
+import android.os.Handler
+import android.os.Looper
+import android.view.Surface
+import androidx.annotation.VisibleForTesting
+import androidx.camera.core.SurfaceEffect
+import androidx.camera.core.SurfaceOutput
+import androidx.camera.core.SurfaceRequest
+import androidx.camera.core.impl.utils.Threads.checkMainThread
+import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
+import androidx.camera.core.processing.OpenGlRenderer
+import androidx.camera.core.processing.ShaderProvider
+
+/**
+ * A effect that applies tone mapping on camera output.
+ *
+ * <p>The thread safety is guaranteed by using the main thread.
+ */
+class ToneMappingSurfaceEffect : SurfaceEffect, OnFrameAvailableListener {
+
+ companion object {
+ // A fragment shader that applies a yellow hue.
+ private val TONE_MAPPING_SHADER_PROVIDER = object : ShaderProvider {
+ override fun createFragmentShader(sampler: String, fragCoords: String): String {
+ return """
+ #extension GL_OES_EGL_image_external : require
+ precision mediump float;
+ uniform samplerExternalOES $sampler;
+ varying vec2 $fragCoords;
+ void main() {
+ vec4 sampleColor = texture2D($sampler, $fragCoords);
+ gl_FragColor = vec4(
+ sampleColor.r * 0.5 + sampleColor.g * 0.8 + sampleColor.b * 0.3,
+ sampleColor.r * 0.4 + sampleColor.g * 0.7 + sampleColor.b * 0.2,
+ sampleColor.r * 0.3 + sampleColor.g * 0.5 + sampleColor.b * 0.1,
+ 1.0);
+ }
+ """
+ }
+ }
+ }
+
+ private val mainThreadHandler: Handler = Handler(Looper.getMainLooper())
+ private val glRenderer: OpenGlRenderer = OpenGlRenderer()
+ private val outputSurfaces: MutableMap<SurfaceOutput, Surface> = mutableMapOf()
+ private val textureTransform: FloatArray = FloatArray(16)
+ private val surfaceTransform: FloatArray = FloatArray(16)
+ private var isReleased = false
+
+ // For testing.
+ private var surfaceRequested = false
+ // For testing.
+ private var outputSurfaceProvided = false
+
+ init {
+ mainThreadExecutor().execute {
+ glRenderer.init(TONE_MAPPING_SHADER_PROVIDER)
+ }
+ }
+
+ override fun onInputSurface(surfaceRequest: SurfaceRequest) {
+ checkMainThread()
+ if (isReleased) {
+ surfaceRequest.willNotProvideSurface()
+ return
+ }
+ surfaceRequested = true
+ val surfaceTexture = SurfaceTexture(glRenderer.textureName)
+ surfaceTexture.setDefaultBufferSize(
+ surfaceRequest.resolution.width, surfaceRequest.resolution.height
+ )
+ val surface = Surface(surfaceTexture)
+ surfaceRequest.provideSurface(surface, mainThreadExecutor()) {
+ surfaceTexture.setOnFrameAvailableListener(null)
+ surfaceTexture.release()
+ surface.release()
+ }
+ surfaceTexture.setOnFrameAvailableListener(this, mainThreadHandler)
+ }
+
+ override fun onOutputSurface(surfaceOutput: SurfaceOutput) {
+ checkMainThread()
+ outputSurfaceProvided = true
+ if (isReleased) {
+ surfaceOutput.close()
+ return
+ }
+ outputSurfaces[surfaceOutput] = surfaceOutput.getSurface(mainThreadExecutor()) {
+ surfaceOutput.close()
+ outputSurfaces.remove(surfaceOutput)
+ }
+ }
+
+ @VisibleForTesting
+ fun isSurfaceRequestedAndProvided(): Boolean {
+ return surfaceRequested && outputSurfaceProvided
+ }
+
+ fun release() {
+ checkMainThread()
+ if (isReleased) {
+ return
+ }
+ glRenderer.release()
+ isReleased = true
+ }
+
+ override fun onFrameAvailable(surfaceTexture: SurfaceTexture) {
+ checkMainThread()
+ if (isReleased) {
+ return
+ }
+ surfaceTexture.updateTexImage()
+ surfaceTexture.getTransformMatrix(textureTransform)
+ for (entry in outputSurfaces.entries.iterator()) {
+ val surface = entry.value
+ val surfaceOutput = entry.key
+ glRenderer.setOutputSurface(surface)
+ surfaceOutput.updateTransformMatrix(surfaceTransform, textureTransform)
+ glRenderer.render(surfaceTexture.timestamp, surfaceTransform)
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/viewtestapp/src/main/res/layout-land/camera_controller_view.xml b/camera/integration-tests/viewtestapp/src/main/res/layout-land/camera_controller_view.xml
index 1bce16f..47fad34 100644
--- a/camera/integration-tests/viewtestapp/src/main/res/layout-land/camera_controller_view.xml
+++ b/camera/integration-tests/viewtestapp/src/main/res/layout-land/camera_controller_view.xml
@@ -51,6 +51,12 @@
android:layout_height="wrap_content"
android:textOff="@string/toggle_camera_front"
android:textOn="@string/toggle_camera_back" />
+ <ToggleButton
+ android:id="@+id/effect_toggle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textOff="@string/toggle_effect_off"
+ android:textOn="@string/toggle_effect_on" />
</LinearLayout>
<LinearLayout
diff --git a/camera/integration-tests/viewtestapp/src/main/res/layout/camera_controller_view.xml b/camera/integration-tests/viewtestapp/src/main/res/layout/camera_controller_view.xml
index 9b97b3f..e7680b2 100644
--- a/camera/integration-tests/viewtestapp/src/main/res/layout/camera_controller_view.xml
+++ b/camera/integration-tests/viewtestapp/src/main/res/layout/camera_controller_view.xml
@@ -48,6 +48,12 @@
android:layout_height="wrap_content"
android:textOff="@string/toggle_camera_front"
android:textOn="@string/toggle_camera_back" />
+ <ToggleButton
+ android:id="@+id/effect_toggle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textOff="@string/toggle_effect_off"
+ android:textOn="@string/toggle_effect_on" />
</LinearLayout>
<LinearLayout
diff --git a/camera/integration-tests/viewtestapp/src/main/res/values/donottranslate-strings.xml b/camera/integration-tests/viewtestapp/src/main/res/values/donottranslate-strings.xml
index d4d6509..01ea7dac 100644
--- a/camera/integration-tests/viewtestapp/src/main/res/values/donottranslate-strings.xml
+++ b/camera/integration-tests/viewtestapp/src/main/res/values/donottranslate-strings.xml
@@ -36,6 +36,8 @@
<string name="toggle_analyzer_not_set">Analyzer not set</string>
<string name="toggle_camera_front">Front</string>
<string name="toggle_camera_back">Back</string>
+ <string name="toggle_effect_on">Effect On</string>
+ <string name="toggle_effect_off">Effect Off</string>
<string name="btn_remove_or_add">Remove/Add</string>
<string name="btn_shrink">Shrink</string>
<string name="btn_switch">Switch</string>
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
index 6be5b38..ca257d7 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
@@ -984,4 +984,93 @@
@Composable fun Text(s: String) {}
"""
)
+
+ @Test
+ fun memoizeLambdaInsideFunctionReturningValue() = verifyComposeIrTransform(
+ """
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun Test(foo: Foo): Int =
+ Consume { foo.value }
+ """,
+ """
+ @Composable
+ fun Test(foo: Foo, %composer: Composer?, %changed: Int): Int {
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C(Test)<{>,<Consum...>:Test.kt")
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ val tmp0 = Consume(remember(foo, {
+ {
+ foo.value
+ }
+ }, %composer, 0b1110 and %changed), %composer, 0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ %composer.endReplaceableGroup()
+ return tmp0
+ }
+
+ """.trimIndent(),
+ """
+ import androidx.compose.runtime.Composable
+ import androidx.compose.runtime.Stable
+
+ @Composable
+ fun Consume(block: () -> Int): Int = block()
+
+ @Stable
+ class Foo {
+ val value: Int = 0
+ }
+ """.trimIndent()
+ )
+
+ @Test
+ fun testComposableCaptureInDelegates() = verifyComposeIrTransform(
+ """
+ import androidx.compose.runtime.*
+
+ class Test(val value: Int) : Delegate by Impl({
+ value
+ })
+ """,
+ """
+ @StabilityInferred(parameters = 0)
+ class Test(val value: Int) : Delegate {
+ private val %%delegate_0: Impl = Impl(composableLambdaInstance(<>, true) { %composer: Composer?, %changed: Int ->
+ sourceInformation(%composer, "C:Test.kt")
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ value
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ }
+ )
+ val content: Function2<Composer, Int, Unit>
+ get() {
+ return <this>.%%delegate_0.content
+ }
+ static val %stable: Int = 0
+ }
+ """,
+ """
+ import androidx.compose.runtime.Composable
+
+ interface Delegate {
+ val content: @Composable () -> Unit
+ }
+
+ class Impl(override val content: @Composable () -> Unit) : Delegate
+ """
+ )
}
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
index 53460d2..c642dff 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
@@ -82,7 +82,7 @@
7100 to "1.2.0-rc01",
7101 to "1.2.0-rc02",
7102 to "1.2.0-rc03",
- 7103 to "1.2.0-rc04",
+ 7103 to "1.2.0",
8000 to "1.3.0-alpha01",
8100 to "1.3.0-alpha02",
)
@@ -97,7 +97,7 @@
* The maven version string of this compiler. This string should be updated before/after every
* release.
*/
- const val compilerVersion: String = "1.3.0-beta01"
+ const val compilerVersion: String = "1.3.0-rc01"
private val minimumRuntimeVersion: String
get() = versionTable[minimumRuntimeVersionInt] ?: "unknown"
}
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
index f47eedb..c3410b9 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
@@ -259,15 +259,16 @@
override fun recordCapture(local: IrValueDeclaration?): Boolean {
val isThis = local == thisParam
val isCtorParam = (local?.parent as? IrConstructor)?.parent === declaration
- if (local != null && collectors.isNotEmpty() && isThis) {
+ val isClassParam = isThis || isCtorParam
+ if (local != null && collectors.isNotEmpty() && isClassParam) {
for (collector in collectors) {
collector.recordCapture(local)
}
}
- if (local != null && declaration.isLocal && !isThis && !isCtorParam) {
+ if (local != null && declaration.isLocal && !isClassParam) {
captures.add(local)
}
- return isThis || isCtorParam
+ return isClassParam
}
override fun recordCapture(local: IrSymbolOwner?) { }
override fun pushCollector(collector: CaptureCollector) {
@@ -415,10 +416,7 @@
val composable = declaration.allowsComposableCalls
val canRemember = composable &&
// Don't use remember in an inline function
- !descriptor.isInline &&
- // Don't use remember if in a composable that returns a value
- // TODO(b/150390108): Consider allowing remember in effects
- descriptor.returnType.let { it != null && it.isUnit() }
+ !descriptor.isInline
val context = FunctionContext(declaration, composable, canRemember)
declarationContextStack.push(context)
diff --git a/compose/foundation/foundation/api/current.ignore b/compose/foundation/foundation/api/current.ignore
index 358fd62..a0279de 100644
--- a/compose/foundation/foundation/api/current.ignore
+++ b/compose/foundation/foundation/api/current.ignore
@@ -1,7 +1,5 @@
// Baseline format: 1.0
-RemovedClass: androidx.compose.foundation.gestures.AndroidOverScrollKt:
- Removed class androidx.compose.foundation.gestures.AndroidOverScrollKt
-RemovedClass: androidx.compose.foundation.gestures.OverScrollConfigurationKt:
- Removed class androidx.compose.foundation.gestures.OverScrollConfigurationKt
-RemovedClass: androidx.compose.foundation.lazy.LazyGridDeprecatedKt:
- Removed class androidx.compose.foundation.lazy.LazyGridDeprecatedKt
+RemovedClass: androidx.compose.foundation.lazy.LazyListItemProviderImplKt:
+ Removed class androidx.compose.foundation.lazy.LazyListItemProviderImplKt
+RemovedClass: androidx.compose.foundation.lazy.grid.LazyGridItemProviderImplKt:
+ Removed class androidx.compose.foundation.lazy.grid.LazyGridItemProviderImplKt
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index b57caaa..0ea30cf 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -36,6 +36,7 @@
}
public final class CheckScrollableContainerConstraintsKt {
+ method public static void checkScrollableContainerConstraints(long constraints, androidx.compose.foundation.gestures.Orientation orientation);
}
public final class ClickableKt {
@@ -47,6 +48,7 @@
}
public final class ClipScrollableContainerKt {
+ method public static androidx.compose.ui.Modifier clipScrollableContainer(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.Orientation orientation);
}
public final class DarkThemeKt {
@@ -231,6 +233,7 @@
public final class ScrollableDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior flingBehavior();
+ method public boolean reverseDirection(androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.foundation.gestures.Orientation orientation, boolean reverseScrolling);
field public static final androidx.compose.foundation.gestures.ScrollableDefaults INSTANCE;
}
@@ -446,7 +449,7 @@
public final class LazyListItemPlacementAnimatorKt {
}
- public final class LazyListItemProviderImplKt {
+ public final class LazyListItemProviderKt {
}
public final class LazyListKt {
@@ -581,7 +584,7 @@
public final class LazyGridItemPlacementAnimatorKt {
}
- public final class LazyGridItemProviderImplKt {
+ public final class LazyGridItemProviderKt {
}
@androidx.compose.foundation.lazy.grid.LazyGridScopeMarker @androidx.compose.runtime.Stable public sealed interface LazyGridItemScope {
@@ -674,6 +677,9 @@
public final class IntervalListKt {
}
+ public final class LazyLayoutItemProviderKt {
+ }
+
public final class LazyLayoutKt {
}
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 254933a..091d626 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -37,7 +37,7 @@
}
public final class CheckScrollableContainerConstraintsKt {
- method @androidx.compose.foundation.ExperimentalFoundationApi public static void checkScrollableContainerConstraints(long constraints, androidx.compose.foundation.gestures.Orientation orientation);
+ method public static void checkScrollableContainerConstraints(long constraints, androidx.compose.foundation.gestures.Orientation orientation);
}
public final class ClickableKt {
@@ -51,7 +51,7 @@
}
public final class ClipScrollableContainerKt {
- method @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier clipScrollableContainer(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.Orientation orientation);
+ method public static androidx.compose.ui.Modifier clipScrollableContainer(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.Orientation orientation);
}
public final class DarkThemeKt {
@@ -285,6 +285,7 @@
public final class ScrollableDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior flingBehavior();
method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public androidx.compose.foundation.OverscrollEffect overscrollEffect();
+ method public boolean reverseDirection(androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.foundation.gestures.Orientation orientation, boolean reverseScrolling);
field public static final androidx.compose.foundation.gestures.ScrollableDefaults INSTANCE;
}
@@ -502,7 +503,7 @@
public final class LazyListItemPlacementAnimatorKt {
}
- public final class LazyListItemProviderImplKt {
+ public final class LazyListItemProviderKt {
}
public final class LazyListKt {
@@ -638,7 +639,7 @@
public final class LazyGridItemPlacementAnimatorKt {
}
- public final class LazyGridItemProviderImplKt {
+ public final class LazyGridItemProviderKt {
}
@androidx.compose.foundation.lazy.grid.LazyGridScopeMarker @androidx.compose.runtime.Stable public sealed interface LazyGridItemScope {
@@ -730,7 +731,7 @@
package androidx.compose.foundation.lazy.layout {
@androidx.compose.foundation.ExperimentalFoundationApi public sealed interface IntervalList<T> {
- method public void forEach(optional int fromIndex, optional int toIndex, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.layout.IntervalList.Interval<T>,kotlin.Unit> block);
+ method public void forEach(optional int fromIndex, optional int toIndex, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.layout.IntervalList.Interval<? extends T>,kotlin.Unit> block);
method public operator androidx.compose.foundation.lazy.layout.IntervalList.Interval<T> get(int index);
method public int getSize();
property public abstract int size;
@@ -748,6 +749,13 @@
public final class IntervalListKt {
}
+ @androidx.compose.foundation.ExperimentalFoundationApi public interface LazyLayoutIntervalContent {
+ method public default kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object>? getKey();
+ method public default kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object> getType();
+ property public default kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object>? key;
+ property public default kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object> type;
+ }
+
@androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public interface LazyLayoutItemProvider {
method @androidx.compose.runtime.Composable public void Item(int index);
method public default Object? getContentType(int index);
@@ -758,6 +766,12 @@
property public default java.util.Map<java.lang.Object,java.lang.Integer> keyToIndexMap;
}
+ public final class LazyLayoutItemProviderKt {
+ method @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider DelegatingLazyLayoutItemProvider(androidx.compose.runtime.State<? extends androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider> delegate);
+ method @androidx.compose.foundation.ExperimentalFoundationApi public static <T extends androidx.compose.foundation.lazy.layout.LazyLayoutIntervalContent> androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider LazyLayoutItemProvider(androidx.compose.foundation.lazy.layout.IntervalList<? extends T> intervals, kotlin.ranges.IntRange nearestItemsRange, kotlin.jvm.functions.Function2<? super T,? super java.lang.Integer,kotlin.Unit> itemContent);
+ method @androidx.compose.foundation.ExperimentalFoundationApi public static Object! getDefaultLazyLayoutKey(int index);
+ }
+
public final class LazyLayoutKt {
method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void LazyLayout(androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider itemProvider, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState? prefetchState, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
}
@@ -787,6 +801,7 @@
}
public final class LazyNearestItemsRangeKt {
+ method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static androidx.compose.runtime.State<kotlin.ranges.IntRange> rememberLazyNearestItemsRangeState(kotlin.jvm.functions.Function0<java.lang.Integer> firstVisibleItemIndex, kotlin.jvm.functions.Function0<java.lang.Integer> slidingWindowSize, kotlin.jvm.functions.Function0<java.lang.Integer> extraItemCount);
}
public final class Lazy_androidKt {
@@ -796,7 +811,7 @@
@androidx.compose.foundation.ExperimentalFoundationApi public final class MutableIntervalList<T> implements androidx.compose.foundation.lazy.layout.IntervalList<T> {
ctor public MutableIntervalList();
method public void addInterval(int size, T? value);
- method public void forEach(int fromIndex, int toIndex, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.layout.IntervalList.Interval<T>,kotlin.Unit> block);
+ method public void forEach(int fromIndex, int toIndex, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.layout.IntervalList.Interval<? extends T>,kotlin.Unit> block);
method public androidx.compose.foundation.lazy.layout.IntervalList.Interval<T> get(int index);
method public int getSize();
property public int size;
diff --git a/compose/foundation/foundation/api/restricted_current.ignore b/compose/foundation/foundation/api/restricted_current.ignore
index 358fd62..a0279de 100644
--- a/compose/foundation/foundation/api/restricted_current.ignore
+++ b/compose/foundation/foundation/api/restricted_current.ignore
@@ -1,7 +1,5 @@
// Baseline format: 1.0
-RemovedClass: androidx.compose.foundation.gestures.AndroidOverScrollKt:
- Removed class androidx.compose.foundation.gestures.AndroidOverScrollKt
-RemovedClass: androidx.compose.foundation.gestures.OverScrollConfigurationKt:
- Removed class androidx.compose.foundation.gestures.OverScrollConfigurationKt
-RemovedClass: androidx.compose.foundation.lazy.LazyGridDeprecatedKt:
- Removed class androidx.compose.foundation.lazy.LazyGridDeprecatedKt
+RemovedClass: androidx.compose.foundation.lazy.LazyListItemProviderImplKt:
+ Removed class androidx.compose.foundation.lazy.LazyListItemProviderImplKt
+RemovedClass: androidx.compose.foundation.lazy.grid.LazyGridItemProviderImplKt:
+ Removed class androidx.compose.foundation.lazy.grid.LazyGridItemProviderImplKt
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index b57caaa..0ea30cf 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -36,6 +36,7 @@
}
public final class CheckScrollableContainerConstraintsKt {
+ method public static void checkScrollableContainerConstraints(long constraints, androidx.compose.foundation.gestures.Orientation orientation);
}
public final class ClickableKt {
@@ -47,6 +48,7 @@
}
public final class ClipScrollableContainerKt {
+ method public static androidx.compose.ui.Modifier clipScrollableContainer(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.Orientation orientation);
}
public final class DarkThemeKt {
@@ -231,6 +233,7 @@
public final class ScrollableDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior flingBehavior();
+ method public boolean reverseDirection(androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.foundation.gestures.Orientation orientation, boolean reverseScrolling);
field public static final androidx.compose.foundation.gestures.ScrollableDefaults INSTANCE;
}
@@ -446,7 +449,7 @@
public final class LazyListItemPlacementAnimatorKt {
}
- public final class LazyListItemProviderImplKt {
+ public final class LazyListItemProviderKt {
}
public final class LazyListKt {
@@ -581,7 +584,7 @@
public final class LazyGridItemPlacementAnimatorKt {
}
- public final class LazyGridItemProviderImplKt {
+ public final class LazyGridItemProviderKt {
}
@androidx.compose.foundation.lazy.grid.LazyGridScopeMarker @androidx.compose.runtime.Stable public sealed interface LazyGridItemScope {
@@ -674,6 +677,9 @@
public final class IntervalListKt {
}
+ public final class LazyLayoutItemProviderKt {
+ }
+
public final class LazyLayoutKt {
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/CheckScrollableContainerConstraints.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/CheckScrollableContainerConstraints.kt
index 0c76fe5..0750a5c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/CheckScrollableContainerConstraints.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/CheckScrollableContainerConstraints.kt
@@ -27,7 +27,6 @@
* @param constraints [Constraints] used to measure the scrollable container
* @param orientation orientation of the scrolling
*/
-@ExperimentalFoundationApi
fun checkScrollableContainerConstraints(
constraints: Constraints,
orientation: Orientation
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ClipScrollableContainer.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ClipScrollableContainer.kt
index 8add015..b779125 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ClipScrollableContainer.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ClipScrollableContainer.kt
@@ -33,7 +33,6 @@
*
* @param orientation orientation of the scrolling
*/
-@ExperimentalFoundationApi
fun Modifier.clipScrollableContainer(orientation: Orientation) =
then(
if (orientation == Orientation.Vertical) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
index afc8293..8b10b1a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
@@ -54,7 +54,6 @@
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.verticalScrollAxisRange
import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.LayoutDirection
import kotlin.math.roundToInt
import kotlinx.coroutines.launch
@@ -286,17 +285,11 @@
val orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal
val scrolling = Modifier.scrollable(
orientation = orientation,
- reverseDirection = run {
- // A finger moves with the content, not with the viewport. Therefore,
- // always reverse once to have "natural" gesture that goes reversed to layout
- var reverseDirection = !reverseScrolling
- // But if rtl and horizontal, things move the other way around
- val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
- if (isRtl && !isVertical) {
- reverseDirection = !reverseDirection
- }
- reverseDirection
- },
+ reverseDirection = ScrollableDefaults.reverseDirection(
+ LocalLayoutDirection.current,
+ orientation,
+ reverseScrolling
+ ),
enabled = isScrollable,
interactionSource = state.internalInteractionSource,
flingBehavior = flingBehavior,
@@ -328,7 +321,6 @@
val isVertical: Boolean,
val overscrollEffect: OverscrollEffect
) : LayoutModifier {
- @OptIn(ExperimentalFoundationApi::class)
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
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 7b54efd..02e20cc 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
@@ -59,9 +59,11 @@
import androidx.compose.ui.layout.OnRemeasuredModifier
import androidx.compose.ui.modifier.ModifierLocalProvider
import androidx.compose.ui.modifier.modifierLocalOf
+import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.toSize
import androidx.compose.ui.util.fastAll
@@ -211,6 +213,32 @@
fun overscrollEffect(): OverscrollEffect {
return rememberOverscrollEffect()
}
+
+ /**
+ * Used to determine the value of `reverseDirection` parameter of [Modifier.scrollable]
+ * in scrollable layouts.
+ *
+ * @param layoutDirection current layout direction (e.g. from [LocalLayoutDirection])
+ * @param orientation orientation of scroll
+ * @param reverseScrolling whether scrolling direction should be reversed
+ *
+ * @return `true` if scroll direction should be reversed, `false` otherwise.
+ */
+ fun reverseDirection(
+ layoutDirection: LayoutDirection,
+ orientation: Orientation,
+ reverseScrolling: Boolean
+ ): Boolean {
+ // A finger moves with the content, not with the viewport. Therefore,
+ // always reverse once to have "natural" gesture that goes reversed to layout
+ var reverseDirection = !reverseScrolling
+ // But if rtl and horizontal, things move the other way around
+ val isRtl = layoutDirection == LayoutDirection.Rtl
+ if (isRtl && orientation != Orientation.Vertical) {
+ reverseDirection = !reverseDirection
+ }
+ return reverseDirection
+ }
}
internal interface ScrollConfig {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
index e205b5e..de9fa2e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
@@ -41,7 +41,6 @@
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.constrainHeight
import androidx.compose.ui.unit.constrainWidth
import androidx.compose.ui.unit.offset
@@ -75,7 +74,7 @@
content: LazyListScope.() -> Unit
) {
val overscrollEffect = ScrollableDefaults.overscrollEffect()
- val itemProvider = rememberItemProvider(state, content)
+ val itemProvider = rememberLazyListItemProvider(state, content)
val beyondBoundsInfo = remember { LazyListBeyondBoundsInfo() }
val scope = rememberCoroutineScope()
val placementAnimator = remember(state, isVertical) {
@@ -119,17 +118,11 @@
.overscroll(overscrollEffect)
.scrollable(
orientation = orientation,
- reverseDirection = run {
- // A finger moves with the content, not with the viewport. Therefore,
- // always reverse once to have "natural" gesture that goes reversed to layout
- var reverseDirection = !reverseLayout
- // But if rtl and horizontal, things move the other way around
- val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
- if (isRtl && !isVertical) {
- reverseDirection = !reverseDirection
- }
- reverseDirection
- },
+ reverseDirection = ScrollableDefaults.reverseDirection(
+ LocalLayoutDirection.current,
+ orientation,
+ reverseLayout
+ ),
interactionSource = state.internalInteractionSource,
flingBehavior = flingBehavior,
state = state,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
index 75d380ad..8c28355 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
@@ -17,7 +17,14 @@
package androidx.compose.foundation.lazy
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.DelegatingLazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.IntervalList
import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.rememberLazyNearestItemsRangeState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
@ExperimentalFoundationApi
internal interface LazyListItemProvider : LazyLayoutItemProvider {
@@ -26,3 +33,59 @@
/** The scope used by the item content lambdas */
val itemScope: LazyItemScopeImpl
}
+
+@ExperimentalFoundationApi
+@Composable
+internal fun rememberLazyListItemProvider(
+ state: LazyListState,
+ content: LazyListScope.() -> Unit
+): LazyListItemProvider {
+ val latestContent = rememberUpdatedState(content)
+ val nearestItemsRangeState = rememberLazyNearestItemsRangeState(
+ firstVisibleItemIndex = remember(state) { { state.firstVisibleItemIndex } },
+ slidingWindowSize = { NearestItemsSlidingWindowSize },
+ extraItemCount = { NearestItemsExtraItemCount }
+ )
+
+ return remember(nearestItemsRangeState) {
+ val itemProviderState = derivedStateOf {
+ val listScope = LazyListScopeImpl().apply(latestContent.value)
+ LazyListItemProviderImpl(
+ listScope.intervals,
+ nearestItemsRangeState.value,
+ listScope.headerIndexes,
+ )
+ }
+ object : LazyListItemProvider,
+ LazyLayoutItemProvider by DelegatingLazyLayoutItemProvider(itemProviderState) {
+ override val headerIndexes: List<Int> get() = itemProviderState.value.headerIndexes
+ override val itemScope: LazyItemScopeImpl get() = itemProviderState.value.itemScope
+ }
+ }
+}
+
+@ExperimentalFoundationApi
+private class LazyListItemProviderImpl(
+ intervals: IntervalList<LazyListIntervalContent>,
+ nearestItemsRange: IntRange,
+ override val headerIndexes: List<Int>,
+ override val itemScope: LazyItemScopeImpl = LazyItemScopeImpl()
+) : LazyListItemProvider,
+ LazyLayoutItemProvider by LazyLayoutItemProvider(
+ intervals = intervals,
+ nearestItemsRange = nearestItemsRange,
+ itemContent = { interval: LazyListIntervalContent, index: Int ->
+ interval.item.invoke(itemScope, index)
+ }
+ )
+
+/**
+ * We use the idea of sliding window as an optimization, so user can scroll up to this number of
+ * items until we have to regenerate the key to index map.
+ */
+private const val NearestItemsSlidingWindowSize = 30
+
+/**
+ * The minimum amount of items near the current first visible item we want to have mapping for.
+ */
+private const val NearestItemsExtraItemCount = 100
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScopeImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScopeImpl.kt
index 4ea8125..ef1c2f2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScopeImpl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScopeImpl.kt
@@ -18,6 +18,7 @@
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.layout.IntervalList
+import androidx.compose.foundation.lazy.layout.LazyLayoutIntervalContent
import androidx.compose.foundation.lazy.layout.MutableIntervalList
import androidx.compose.runtime.Composable
@@ -72,8 +73,9 @@
}
}
+@OptIn(ExperimentalFoundationApi::class)
internal class LazyListIntervalContent(
- val key: ((index: Int) -> Any)?,
- val type: ((index: Int) -> Any?),
+ override val key: ((index: Int) -> Any)?,
+ override val type: ((index: Int) -> Any?),
val item: @Composable LazyItemScope.(index: Int) -> Unit
-)
+) : LazyLayoutIntervalContent
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazySemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazySemantics.kt
index d785a83..719d441 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazySemantics.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazySemantics.kt
@@ -20,6 +20,7 @@
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.CollectionInfo
@@ -53,10 +54,9 @@
userScrollEnabled
) {
val indexForKeyMapping: (Any) -> Int = { needle ->
- val key = itemProvider::getKey
var result = -1
for (index in 0 until itemProvider.itemCount) {
- if (key(index) == needle) {
+ if (itemProvider.getKey(index) == needle) {
result = index
break
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
index 1c41760..a114e0a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
@@ -41,7 +41,6 @@
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.constrainHeight
import androidx.compose.ui.unit.constrainWidth
import androidx.compose.ui.unit.dp
@@ -76,7 +75,7 @@
) {
val overscrollEffect = ScrollableDefaults.overscrollEffect()
- val itemProvider = rememberItemProvider(state, content)
+ val itemProvider = rememberLazyGridItemProvider(state, content)
val scope = rememberCoroutineScope()
val placementAnimator = remember(state, isVertical) {
@@ -118,17 +117,11 @@
.overscroll(overscrollEffect)
.scrollable(
orientation = orientation,
- reverseDirection = run {
- // A finger moves with the content, not with the viewport. Therefore,
- // always reverse once to have "natural" gesture that goes reversed to layout
- var reverseDirection = !reverseLayout
- // But if rtl and horizontal, things move the other way around
- val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
- if (isRtl && !isVertical) {
- reverseDirection = !reverseDirection
- }
- reverseDirection
- },
+ reverseDirection = ScrollableDefaults.reverseDirection(
+ LocalLayoutDirection.current,
+ orientation,
+ reverseLayout
+ ),
interactionSource = state.internalInteractionSource,
flingBehavior = flingBehavior,
state = state,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
index df4d695..4b27068 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
@@ -17,9 +17,94 @@
package androidx.compose.foundation.lazy.grid
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.DelegatingLazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.IntervalList
import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.rememberLazyNearestItemsRangeState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
@ExperimentalFoundationApi
internal interface LazyGridItemProvider : LazyLayoutItemProvider {
val spanLayoutProvider: LazyGridSpanLayoutProvider
+ val hasCustomSpans: Boolean
+
+ fun LazyGridItemSpanScope.getSpan(index: Int): GridItemSpan
}
+
+@ExperimentalFoundationApi
+@Composable
+internal fun rememberLazyGridItemProvider(
+ state: LazyGridState,
+ content: LazyGridScope.() -> Unit,
+): LazyGridItemProvider {
+ val latestContent = rememberUpdatedState(content)
+ val nearestItemsRangeState = rememberLazyNearestItemsRangeState(
+ firstVisibleItemIndex = remember(state) {
+ { state.firstVisibleItemIndex }
+ },
+ slidingWindowSize = { NearestItemsSlidingWindowSize },
+ extraItemCount = { NearestItemsExtraItemCount }
+ )
+
+ return remember(nearestItemsRangeState) {
+ val itemProviderState: State<LazyGridItemProvider> = derivedStateOf {
+ val gridScope = LazyGridScopeImpl().apply(latestContent.value)
+ LazyGridItemProviderImpl(
+ gridScope.intervals,
+ gridScope.hasCustomSpans,
+ nearestItemsRangeState.value
+ )
+ }
+
+ object : LazyGridItemProvider,
+ LazyLayoutItemProvider by DelegatingLazyLayoutItemProvider(itemProviderState) {
+ override val spanLayoutProvider: LazyGridSpanLayoutProvider
+ get() = itemProviderState.value.spanLayoutProvider
+
+ override val hasCustomSpans: Boolean
+ get() = itemProviderState.value.hasCustomSpans
+
+ override fun LazyGridItemSpanScope.getSpan(index: Int): GridItemSpan =
+ with(itemProviderState.value) {
+ getSpan(index)
+ }
+ }
+ }
+}
+
+@ExperimentalFoundationApi
+private class LazyGridItemProviderImpl(
+ private val intervals: IntervalList<LazyGridIntervalContent>,
+ override val hasCustomSpans: Boolean,
+ nearestItemsRange: IntRange
+) : LazyGridItemProvider, LazyLayoutItemProvider by LazyLayoutItemProvider(
+ intervals = intervals,
+ nearestItemsRange = nearestItemsRange,
+ itemContent = { interval, index ->
+ interval.item.invoke(LazyGridItemScopeImpl, index)
+ }
+) {
+ override val spanLayoutProvider: LazyGridSpanLayoutProvider =
+ LazyGridSpanLayoutProvider(this)
+
+ override fun LazyGridItemSpanScope.getSpan(index: Int): GridItemSpan {
+ val interval = intervals[index]
+ val localIntervalIndex = index - interval.startIndex
+ return interval.value.span.invoke(this, localIntervalIndex)
+ }
+}
+
+/**
+ * We use the idea of sliding window as an optimization, so user can scroll up to this number of
+ * items until we have to regenerate the key to index map.
+ */
+private const val NearestItemsSlidingWindowSize = 90
+
+/**
+ * The minimum amount of items near the current first visible item we want to have mapping for.
+ */
+private const val NearestItemsExtraItemCount = 200
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScopeImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScopeImpl.kt
index 281996a..e05ff92 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScopeImpl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScopeImpl.kt
@@ -17,6 +17,7 @@
package androidx.compose.foundation.lazy.grid
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.LazyLayoutIntervalContent
import androidx.compose.foundation.lazy.layout.MutableIntervalList
import androidx.compose.runtime.Composable
@@ -67,8 +68,8 @@
@OptIn(ExperimentalFoundationApi::class)
internal class LazyGridIntervalContent(
- val key: ((index: Int) -> Any)?,
+ override val key: ((index: Int) -> Any)?,
val span: LazyGridItemSpanScope.(Int) -> GridItemSpan,
- val type: ((index: Int) -> Any?),
+ override val type: ((index: Int) -> Any?),
val item: @Composable LazyGridItemScope.(Int) -> Unit
-)
+) : LazyLayoutIntervalContent
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
index 18b32e7..6c0f594 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
@@ -21,7 +21,7 @@
import kotlin.math.sqrt
@OptIn(ExperimentalFoundationApi::class)
-internal class LazyGridSpanLayoutProvider(private val itemsSnapshot: LazyGridItemsSnapshot) {
+internal class LazyGridSpanLayoutProvider(private val itemProvider: LazyGridItemProvider) {
class LineConfiguration(val firstItemIndex: Int, val spans: List<GridItemSpan>)
/** Caches the bucket info on lines 0, [bucketSize], 2 * [bucketSize], etc. */
@@ -60,7 +60,7 @@
List(currentSlotsPerLine) { GridItemSpan(1) }.also { previousDefaultSpans = it }
}
- val totalSize get() = itemsSnapshot.itemsCount
+ val totalSize get() = itemProvider.itemCount
/** The number of slots on one grid line e.g. the number of columns of a vertical grid. */
var slotsPerLine = 0
@@ -72,7 +72,7 @@
}
fun getLineConfiguration(lineIndex: Int): LineConfiguration {
- if (!itemsSnapshot.hasCustomSpans) {
+ if (!itemProvider.hasCustomSpans) {
// Quick return when all spans are 1x1 - in this case we can easily calculate positions.
val firstItemIndex = lineIndex * slotsPerLine
return LineConfiguration(
@@ -172,7 +172,7 @@
return LineIndex(0)
}
require(itemIndex < totalSize)
- if (!itemsSnapshot.hasCustomSpans) {
+ if (!itemProvider.hasCustomSpans) {
return LineIndex(itemIndex / slotsPerLine)
}
@@ -210,7 +210,7 @@
return LineIndex(currentLine)
}
- private fun spanOf(itemIndex: Int, maxSpan: Int) = with(itemsSnapshot) {
+ private fun spanOf(itemIndex: Int, maxSpan: Int) = with(itemProvider) {
with(LazyGridItemSpanScopeImpl) {
maxCurrentLineSpan = maxSpan
maxLineSpan = slotsPerLine
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt
index 54cafcd..b331a9e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt
@@ -20,6 +20,7 @@
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.CollectionInfo
@@ -53,10 +54,9 @@
userScrollEnabled
) {
val indexForKeyMapping: (Any) -> Int = { needle ->
- val key = itemProvider::getKey
var result = -1
for (index in 0 until itemProvider.itemCount) {
- if (key(index) == needle) {
+ if (itemProvider.getKey(index) == needle) {
result = index
break
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/IntervalList.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/IntervalList.kt
index f855bea..0a6ca89 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/IntervalList.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/IntervalList.kt
@@ -31,7 +31,7 @@
* @param T type of values each interval contains in [Interval.value].
*/
@ExperimentalFoundationApi
-sealed interface IntervalList<T> {
+sealed interface IntervalList<out T> {
/**
* The total amount of items in all the intervals.
@@ -69,7 +69,7 @@
*
* @see get
*/
- class Interval<T> internal constructor(
+ class Interval<out T> internal constructor(
/**
* The index of the first item in the interval.
*/
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemProvider.kt
index 9534b0e..828bdfe 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemProvider.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
/**
* Provides all the needed info about the items which could be later composed and displayed as
@@ -67,4 +68,138 @@
* 3) This objects can't be equals to any object which could be provided by a user as a custom key.
*/
@ExperimentalFoundationApi
+@Suppress("MissingNullability")
expect fun getDefaultLazyLayoutKey(index: Int): Any
+
+/**
+ * Common content holder to back interval-based `item` DSL of lazy layouts.
+ */
+@ExperimentalFoundationApi
+interface LazyLayoutIntervalContent {
+ /**
+ * Returns item key based on a local index for the current interval.
+ */
+ val key: ((index: Int) -> Any)? get() = null
+
+ /**
+ * Returns item type based on a local index for the current interval.
+ */
+ val type: ((index: Int) -> Any?) get() = { null }
+}
+
+/**
+ * Default implementation of [LazyLayoutItemProvider] shared by lazy layout implementations.
+ *
+ * @param intervals [IntervalList] of [LazyLayoutIntervalContent] defined by lazy list DSL
+ * @param nearestItemsRange range of indices considered near current viewport
+ * @param itemContent composable content based on index inside provided interval
+ */
+@ExperimentalFoundationApi
+fun <T : LazyLayoutIntervalContent> LazyLayoutItemProvider(
+ intervals: IntervalList<T>,
+ nearestItemsRange: IntRange,
+ itemContent: @Composable (interval: T, index: Int) -> Unit,
+): LazyLayoutItemProvider =
+ DefaultLazyLayoutItemsProvider(itemContent, intervals, nearestItemsRange)
+
+@ExperimentalFoundationApi
+private class DefaultLazyLayoutItemsProvider<IntervalContent : LazyLayoutIntervalContent>(
+ val itemContentProvider: @Composable IntervalContent.(index: Int) -> Unit,
+ val intervals: IntervalList<IntervalContent>,
+ nearestItemsRange: IntRange
+) : LazyLayoutItemProvider {
+ override val itemCount get() = intervals.size
+
+ override val keyToIndexMap: Map<Any, Int> = generateKeyToIndexMap(nearestItemsRange, intervals)
+
+ @Composable
+ override fun Item(index: Int) {
+ withLocalIntervalIndex(index) { localIndex, content ->
+ content.itemContentProvider(localIndex)
+ }
+ }
+
+ override fun getKey(index: Int): Any =
+ withLocalIntervalIndex(index) { localIndex, content ->
+ content.key?.invoke(localIndex) ?: getDefaultLazyLayoutKey(index)
+ }
+
+ override fun getContentType(index: Int): Any? =
+ withLocalIntervalIndex(index) { localIndex, content ->
+ content.type.invoke(localIndex)
+ }
+
+ private inline fun <T> withLocalIntervalIndex(
+ index: Int,
+ block: (localIndex: Int, content: IntervalContent) -> T
+ ): T {
+ val interval = intervals[index]
+ val localIntervalIndex = index - interval.startIndex
+ return block(localIntervalIndex, interval.value)
+ }
+
+ /**
+ * Traverses the interval [list] in order to create a mapping from the key to the index for all
+ * the indexes in the passed [range].
+ * The returned map will not contain the values for intervals with no key mapping provided.
+ */
+ @ExperimentalFoundationApi
+ private fun generateKeyToIndexMap(
+ range: IntRange,
+ list: IntervalList<LazyLayoutIntervalContent>
+ ): Map<Any, Int> {
+ val first = range.first
+ check(first >= 0)
+ val last = minOf(range.last, list.size - 1)
+ return if (last < first) {
+ emptyMap()
+ } else {
+ hashMapOf<Any, Int>().also { map ->
+ list.forEach(
+ fromIndex = first,
+ toIndex = last,
+ ) {
+ if (it.value.key != null) {
+ val keyFactory = requireNotNull(it.value.key)
+ val start = maxOf(first, it.startIndex)
+ val end = minOf(last, it.startIndex + it.size - 1)
+ for (i in start..end) {
+ map[keyFactory(i - it.startIndex)] = i
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Delegating version of [LazyLayoutItemProvider], abstracting internal [State] access.
+ * This way, passing [LazyLayoutItemProvider] will not trigger recomposition unless
+ * its methods are called within composable functions.
+ *
+ * @param delegate [State] to delegate [LazyLayoutItemProvider] functionality to.
+ */
+@ExperimentalFoundationApi
+fun DelegatingLazyLayoutItemProvider(
+ delegate: State<LazyLayoutItemProvider>
+): LazyLayoutItemProvider =
+ DefaultDelegatingLazyLayoutItemProvider(delegate)
+
+@ExperimentalFoundationApi
+private class DefaultDelegatingLazyLayoutItemProvider(
+ private val delegate: State<LazyLayoutItemProvider>
+) : LazyLayoutItemProvider {
+ override val itemCount: Int get() = delegate.value.itemCount
+
+ @Composable
+ override fun Item(index: Int) {
+ delegate.value.Item(index)
+ }
+
+ override val keyToIndexMap: Map<Any, Int> get() = delegate.value.keyToIndexMap
+
+ override fun getKey(index: Int): Any = delegate.value.getKey(index)
+
+ override fun getContentType(index: Int): Any? = delegate.value.getContentType(index)
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyNearestItemsRange.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyNearestItemsRange.kt
index f34ae9f..c602a6c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyNearestItemsRange.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyNearestItemsRange.kt
@@ -16,19 +16,48 @@
package androidx.compose.foundation.lazy.layout
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.structuralEqualityPolicy
+
+/**
+ * Calculate and memoize range of indexes which contains at least [extraItemCount] items near
+ * the first visible item. It is optimized to return the same range for small changes in the
+ * firstVisibleItem value so we do not regenerate the map on each scroll.
+ *
+ * @param firstVisibleItemIndex Provider of the first item index currently visible on screen.
+ * @param slidingWindowSize Number items user can scroll up to this number of items until we have to
+ * regenerate item mapping.
+ * @param extraItemCount The minimum amount of items near the first visible item we want
+ * to have mapping for.
+ * @return range of indexes with items near current the first visible position.
+ */
+@ExperimentalFoundationApi
+@Composable
+fun rememberLazyNearestItemsRangeState(
+ firstVisibleItemIndex: () -> Int,
+ slidingWindowSize: () -> Int,
+ extraItemCount: () -> Int
+): State<IntRange> =
+ remember(firstVisibleItemIndex, slidingWindowSize, extraItemCount) {
+ derivedStateOf(structuralEqualityPolicy()) {
+ calculateNearestItemsRange(
+ firstVisibleItemIndex(),
+ slidingWindowSize(),
+ extraItemCount()
+ )
+ }
+ }
+
/**
* Returns a range of indexes which contains at least [extraItemCount] items near
* the first visible item. It is optimized to return the same range for small changes in the
- * [firstVisibleItem] value so we do not regenerate the map on each scroll.
- *
- * It uses the idea of sliding window as an optimization, so user can scroll up to this number of
- * items until we have to regenerate the key to index map.
- *
- * @param firstVisibleItem currently visible item
- * @param slidingWindowSize size of the sliding window for the nearest item calculation
- * @param extraItemCount minimum amount of items near the first item we want to have mapping for.
+ * firstVisibleItem value so we do not regenerate the map on each scroll.
*/
-internal fun calculateNearestItemsRange(
+private fun calculateNearestItemsRange(
firstVisibleItem: Int,
slidingWindowSize: Int,
extraItemCount: Int
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextControllerTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextControllerTest.kt
index 56e0e66..29a5f39 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextControllerTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextControllerTest.kt
@@ -16,8 +16,10 @@
package androidx.compose.foundation.text
+import androidx.compose.ui.text.AnnotatedString
import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.mock
+import com.nhaarman.mockitokotlin2.whenever
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -27,8 +29,14 @@
class TextControllerTest {
@Test
fun `semantics modifier recreated when TextDelegate is set`() {
- val textDelegateBefore = mock<TextDelegate>()
- val textDelegateAfter = mock<TextDelegate>()
+ val textDelegateBefore = mock<TextDelegate>() {
+ whenever(it.text).thenReturn(AnnotatedString("Example Text String 1"))
+ }
+
+ val textDelegateAfter = mock<TextDelegate>() {
+ whenever(it.text).thenReturn(AnnotatedString("Example Text String 2"))
+ }
+
// Make sure that mock doesn't do smart memory management:
assertThat(textDelegateAfter).isNotSameInstanceAs(textDelegateBefore)
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
index 9d55522..05d810f 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
@@ -23,6 +23,7 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.TextLayoutInput
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
@@ -68,6 +69,7 @@
private lateinit var gesture: TextDragObserver
private lateinit var layoutCoordinates: LayoutCoordinates
private lateinit var state: TextState
+ private lateinit var fontFamilyResolver: FontFamily.Resolver
@Before
fun setup() {
@@ -76,8 +78,15 @@
layoutCoordinates = mock {
on { isAttached } doReturn true
}
+ fontFamilyResolver = mock()
- state = TextState(mock(), selectableId)
+ val delegate = TextDelegate(
+ text = AnnotatedString(""),
+ style = TextStyle(),
+ density = Density(1.0f),
+ fontFamilyResolver = fontFamilyResolver
+ )
+ state = TextState(delegate, selectableId)
state.layoutCoordinates = layoutCoordinates
state.layoutResult = TextLayoutResult(
TextLayoutInput(
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialTracingBenchmark.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialTracingBenchmark.kt
index db0d20d..6c3d32d 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialTracingBenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialTracingBenchmark.kt
@@ -23,29 +23,30 @@
import androidx.benchmark.macro.TraceSectionMetric
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.benchmark.perfetto.PerfettoCapture
-import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameters
@LargeTest
-@RunWith(AndroidJUnit4::class)
+@RunWith(Parameterized::class)
/**
* End-to-end test for compose-runtime-tracing verifying that names of Composables show up in
* a Perfetto trace.
*/
-class TrivialTracingBenchmark {
+@OptIn(ExperimentalMetricApi::class)
+class TrivialTracingBenchmark(private val composableName: String) {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@RequiresApi(Build.VERSION_CODES.R) // TODO(234351579): Support API < 30
- @OptIn(ExperimentalMetricApi::class)
@Test
fun test_composable_names_present_in_trace() {
- val metrics = COMPOSABLE_NAMES.map { composableName ->
+ val metrics = listOf(
TraceSectionMetric("%$PACKAGE_NAME.$composableName %$FILE_NAME:%")
- }
+ )
benchmarkRule.measureRepeated(
packageName = PACKAGE_NAME,
metrics = metrics,
@@ -74,5 +75,9 @@
"Bar_4888EA32_ABC5_4550_BA78_1247FEC1AAC9",
"Baz_609801AB_F5A9_47C3_9405_2E82542F21B8"
)
+
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun parameters() = COMPOSABLE_NAMES
}
}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt
index 35f5b24..ac4def2 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt
@@ -17,7 +17,6 @@
package androidx.compose.material.textfield
import android.os.Build
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredWidth
@@ -38,7 +37,6 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
@@ -82,15 +80,13 @@
@Test
fun outlinedTextField_withInput() {
rule.setMaterialContent {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- val text = "Text"
- OutlinedTextField(
- value = TextFieldValue(text = text, selection = TextRange(text.length)),
- onValueChange = {},
- label = { Text("Label") },
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ val text = "Text"
+ OutlinedTextField(
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
+ onValueChange = {},
+ label = { Text("Label") },
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
assertAgainstGolden("outlined_textField_withInput")
@@ -99,14 +95,12 @@
@Test
fun outlinedTextField_notFocused() {
rule.setMaterialContent {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = "",
- onValueChange = {},
- label = { Text("Label") },
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ label = { Text("Label") },
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
assertAgainstGolden("outlined_textField_not_focused")
@@ -115,14 +109,12 @@
@Test
fun outlinedTextField_focused() {
rule.setMaterialContent {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = "",
- onValueChange = {},
- label = { Text("Label") },
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ label = { Text("Label") },
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
rule.onNodeWithTag(TextFieldTag).focus()
@@ -134,14 +126,12 @@
fun outlinedTextField_focused_rtl() {
rule.setMaterialContent {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = "",
- onValueChange = {},
- label = { Text("Label") },
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ label = { Text("Label") },
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
}
@@ -153,16 +143,14 @@
@Test
fun outlinedTextField_error_focused() {
rule.setMaterialContent {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- val text = "Input"
- OutlinedTextField(
- value = TextFieldValue(text = text, selection = TextRange(text.length)),
- onValueChange = {},
- label = { Text("Label") },
- isError = true,
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ val text = "Input"
+ OutlinedTextField(
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
+ onValueChange = {},
+ label = { Text("Label") },
+ isError = true,
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
rule.onNodeWithTag(TextFieldTag).focus()
@@ -173,15 +161,13 @@
@Test
fun outlinedTextField_error_notFocused() {
rule.setMaterialContent {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = "",
- onValueChange = {},
- label = { Text("Label") },
- isError = true,
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ label = { Text("Label") },
+ isError = true,
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
assertAgainstGolden("outlined_textField_notFocused_errorState")
@@ -374,15 +360,13 @@
@Test
fun outlinedTextField_disabled() {
rule.setMaterialContent {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = TextFieldValue("Text"),
- onValueChange = {},
- singleLine = true,
- enabled = false,
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = TextFieldValue("Text"),
+ onValueChange = {},
+ singleLine = true,
+ enabled = false,
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
assertAgainstGolden("outlinedTextField_disabled")
@@ -391,15 +375,13 @@
@Test
fun outlinedTextField_disabled_notFocusable() {
rule.setMaterialContent {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = TextFieldValue("Text"),
- onValueChange = {},
- singleLine = true,
- enabled = false,
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = TextFieldValue("Text"),
+ onValueChange = {},
+ singleLine = true,
+ enabled = false,
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
rule.onNodeWithTag(TextFieldTag).focus()
@@ -410,15 +392,13 @@
@Test
fun outlinedTextField_disabled_notScrolled() {
rule.setMaterialContent {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = longText,
- onValueChange = { },
- singleLine = true,
- modifier = Modifier.requiredWidth(300.dp),
- enabled = false
- )
- }
+ OutlinedTextField(
+ value = longText,
+ onValueChange = { },
+ singleLine = true,
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(300.dp),
+ enabled = false
+ )
}
rule.mainClock.autoAdvance = false
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
index 197954f..0cbdd15 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
@@ -49,6 +49,7 @@
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
@@ -160,7 +161,11 @@
BasicTextField(
value = value,
modifier = if (label != null) {
- modifier.padding(top = OutlinedTextFieldTopPadding)
+ modifier
+ // Merge semantics at the beginning of the modifier chain to ensure padding is
+ // considered part of the text field.
+ .semantics(mergeDescendants = true) {}
+ .padding(top = OutlinedTextFieldTopPadding)
} else {
modifier
}
@@ -304,7 +309,11 @@
BasicTextField(
value = value,
modifier = if (label != null) {
- modifier.padding(top = OutlinedTextFieldTopPadding)
+ modifier
+ // Merge semantics at the beginning of the modifier chain to ensure padding is
+ // considered part of the text field.
+ .semantics(mergeDescendants = true) {}
+ .padding(top = OutlinedTextFieldTopPadding)
} else {
modifier
}
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index 7a57b3e..88e4217 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -8,12 +8,12 @@
method @androidx.compose.runtime.Composable public long getTextContentColor();
method @androidx.compose.runtime.Composable public long getTitleContentColor();
method public float getTonalElevation();
- property @androidx.compose.runtime.Composable public final long ContainerColor;
- property @androidx.compose.runtime.Composable public final long IconContentColor;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
- property @androidx.compose.runtime.Composable public final long TextContentColor;
- property @androidx.compose.runtime.Composable public final long TitleContentColor;
property public final float TonalElevation;
+ property @androidx.compose.runtime.Composable public final long containerColor;
+ property @androidx.compose.runtime.Composable public final long iconContentColor;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
+ property @androidx.compose.runtime.Composable public final long textContentColor;
+ property @androidx.compose.runtime.Composable public final long titleContentColor;
field public static final androidx.compose.material3.AlertDialogDefaults INSTANCE;
}
@@ -36,7 +36,7 @@
public final class BadgeDefaults {
method @androidx.compose.runtime.Composable public long getContainerColor();
- property @androidx.compose.runtime.Composable public final long ContainerColor;
+ property @androidx.compose.runtime.Composable public final long containerColor;
field public static final androidx.compose.material3.BadgeDefaults INSTANCE;
}
@@ -46,26 +46,21 @@
}
public final class BottomAppBarDefaults {
- method @androidx.compose.runtime.Composable public void FloatingActionButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, optional androidx.compose.material3.FloatingActionButtonElevation elevation, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public long getBottomAppBarFabColor();
method @androidx.compose.runtime.Composable public long getContainerColor();
method public float getContainerElevation();
method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
- method @androidx.compose.runtime.Composable public long getFloatingActionButtonContainerColor();
- method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getFloatingActionButtonShape();
- property @androidx.compose.runtime.Composable public final long ContainerColor;
property public final float ContainerElevation;
property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
- property @androidx.compose.runtime.Composable public final long FloatingActionButtonContainerColor;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape FloatingActionButtonShape;
+ property @androidx.compose.runtime.Composable public final long bottomAppBarFabColor;
+ property @androidx.compose.runtime.Composable public final long containerColor;
field public static final androidx.compose.material3.BottomAppBarDefaults INSTANCE;
}
- public static final class BottomAppBarDefaults.FloatingActionButtonElevation implements androidx.compose.material3.FloatingActionButtonElevation {
- method public androidx.compose.runtime.MutableState<androidx.compose.ui.unit.Dp> getElevation();
+ public static final class BottomAppBarDefaults.BottomAppBarFabElevation implements androidx.compose.material3.FloatingActionButtonElevation {
method @androidx.compose.runtime.Composable public androidx.compose.runtime.MutableState<androidx.compose.ui.unit.Dp> shadowElevation(androidx.compose.foundation.interaction.InteractionSource interactionSource);
method @androidx.compose.runtime.Composable public androidx.compose.runtime.MutableState<androidx.compose.ui.unit.Dp> tonalElevation(androidx.compose.foundation.interaction.InteractionSource interactionSource);
- property public final androidx.compose.runtime.MutableState<androidx.compose.ui.unit.Dp> elevation;
- field public static final androidx.compose.material3.BottomAppBarDefaults.FloatingActionButtonElevation INSTANCE;
+ field public static final androidx.compose.material3.BottomAppBarDefaults.BottomAppBarFabElevation INSTANCE;
}
@androidx.compose.runtime.Stable public interface ButtonColors {
@@ -97,17 +92,17 @@
method @androidx.compose.runtime.Composable public androidx.compose.material3.ButtonColors textButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
property public final androidx.compose.foundation.layout.PaddingValues ButtonWithIconContentPadding;
property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape ElevatedShape;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape FilledTonalShape;
property public final float IconSize;
property public final float IconSpacing;
property public final float MinHeight;
property public final float MinWidth;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape OutlinedShape;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
property public final androidx.compose.foundation.layout.PaddingValues TextButtonContentPadding;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape TextShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape elevatedShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape filledTonalShape;
property @androidx.compose.runtime.Composable public final androidx.compose.foundation.BorderStroke outlinedButtonBorder;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape outlinedShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape textShape;
field public static final androidx.compose.material3.ButtonDefaults INSTANCE;
}
@@ -140,9 +135,9 @@
method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke outlinedCardBorder(optional boolean enabled);
method @androidx.compose.runtime.Composable public androidx.compose.material3.CardColors outlinedCardColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
method @androidx.compose.runtime.Composable public androidx.compose.material3.CardElevation outlinedCardElevation(optional float defaultElevation, optional float pressedElevation, optional float focusedElevation, optional float hoveredElevation, optional float draggedElevation, optional float disabledElevation);
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape ElevatedShape;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape OutlinedShape;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape elevatedShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape outlinedShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
field public static final androidx.compose.material3.CardDefaults INSTANCE;
}
@@ -177,8 +172,8 @@
}
@androidx.compose.runtime.Stable public final class ColorScheme {
- ctor public ColorScheme(long primary, long onPrimary, long primaryContainer, long onPrimaryContainer, long inversePrimary, long secondary, long onSecondary, long secondaryContainer, long onSecondaryContainer, long tertiary, long onTertiary, long tertiaryContainer, long onTertiaryContainer, long background, long onBackground, long surface, long onSurface, long surfaceVariant, long onSurfaceVariant, long surfaceTint, long inverseSurface, long inverseOnSurface, long error, long onError, long errorContainer, long onErrorContainer, long outline);
- method public androidx.compose.material3.ColorScheme copy(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline);
+ ctor public ColorScheme(long primary, long onPrimary, long primaryContainer, long onPrimaryContainer, long inversePrimary, long secondary, long onSecondary, long secondaryContainer, long onSecondaryContainer, long tertiary, long onTertiary, long tertiaryContainer, long onTertiaryContainer, long background, long onBackground, long surface, long onSurface, long surfaceVariant, long onSurfaceVariant, long surfaceTint, long inverseSurface, long inverseOnSurface, long error, long onError, long errorContainer, long onErrorContainer, long outline, long outlineVariant, long scrim);
+ method public androidx.compose.material3.ColorScheme copy(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim);
method public long getBackground();
method public long getError();
method public long getErrorContainer();
@@ -197,8 +192,10 @@
method public long getOnTertiary();
method public long getOnTertiaryContainer();
method public long getOutline();
+ method public long getOutlineVariant();
method public long getPrimary();
method public long getPrimaryContainer();
+ method public long getScrim();
method public long getSecondary();
method public long getSecondaryContainer();
method public long getSurface();
@@ -224,8 +221,10 @@
property public final long onTertiary;
property public final long onTertiaryContainer;
property public final long outline;
+ property public final long outlineVariant;
property public final long primary;
property public final long primaryContainer;
+ property public final long scrim;
property public final long secondary;
property public final long secondaryContainer;
property public final long surface;
@@ -238,8 +237,8 @@
public final class ColorSchemeKt {
method public static long contentColorFor(androidx.compose.material3.ColorScheme, long backgroundColor);
method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static long contentColorFor(long backgroundColor);
- method public static androidx.compose.material3.ColorScheme darkColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline);
- method public static androidx.compose.material3.ColorScheme lightColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline);
+ method public static androidx.compose.material3.ColorScheme darkColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim);
+ method public static androidx.compose.material3.ColorScheme lightColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim);
method public static long surfaceColorAtElevation(androidx.compose.material3.ColorScheme, float elevation);
}
@@ -251,8 +250,8 @@
public final class DividerDefaults {
method @androidx.compose.runtime.Composable public long getColor();
method public float getThickness();
- property @androidx.compose.runtime.Composable public final long Color;
property public final float Thickness;
+ property @androidx.compose.runtime.Composable public final long color;
field public static final androidx.compose.material3.DividerDefaults INSTANCE;
}
@@ -283,12 +282,12 @@
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getSmallShape();
method @androidx.compose.runtime.Composable public androidx.compose.material3.FloatingActionButtonElevation loweredElevation(optional float defaultElevation, optional float pressedElevation, optional float focusedElevation, optional float hoveredElevation);
- property @androidx.compose.runtime.Composable public final long ContainerColor;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape ExtendedFabShape;
property public final float LargeIconSize;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape LargeShape;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape SmallShape;
+ property @androidx.compose.runtime.Composable public final long containerColor;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape extendedFabShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape largeShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape smallShape;
field public static final androidx.compose.material3.FloatingActionButtonDefaults INSTANCE;
}
@@ -323,8 +322,8 @@
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors outlinedIconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke? outlinedIconToggleButtonBorder(boolean enabled, boolean checked);
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors outlinedIconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape FilledShape;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape OutlinedShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape filledShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape outlinedShape;
field public static final androidx.compose.material3.IconButtonDefaults INSTANCE;
}
@@ -384,8 +383,8 @@
public final class NavigationBarDefaults {
method @androidx.compose.runtime.Composable public long getContainerColor();
method public float getElevation();
- property @androidx.compose.runtime.Composable public final long ContainerColor;
property public final float Elevation;
+ property @androidx.compose.runtime.Composable public final long containerColor;
field public static final androidx.compose.material3.NavigationBarDefaults INSTANCE;
}
@@ -441,11 +440,11 @@
method @androidx.compose.runtime.Composable public long getLinearColor();
method @androidx.compose.runtime.Composable public long getLinearTrackColor();
method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getProgressAnimationSpec();
- property @androidx.compose.runtime.Composable public final long CircularColor;
property public final float CircularStrokeWidth;
- property @androidx.compose.runtime.Composable public final long LinearColor;
- property @androidx.compose.runtime.Composable public final long LinearTrackColor;
property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> ProgressAnimationSpec;
+ property @androidx.compose.runtime.Composable public final long circularColor;
+ property @androidx.compose.runtime.Composable public final long linearColor;
+ property @androidx.compose.runtime.Composable public final long linearTrackColor;
field public static final androidx.compose.material3.ProgressIndicatorDefaults INSTANCE;
}
@@ -533,12 +532,12 @@
method @androidx.compose.runtime.Composable public long getContentColor();
method @androidx.compose.runtime.Composable public long getDismissActionContentColor();
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
- property @androidx.compose.runtime.Composable public final long ActionColor;
- property @androidx.compose.runtime.Composable public final long ActionContentColor;
- property @androidx.compose.runtime.Composable public final long Color;
- property @androidx.compose.runtime.Composable public final long ContentColor;
- property @androidx.compose.runtime.Composable public final long DismissActionContentColor;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
+ property @androidx.compose.runtime.Composable public final long actionColor;
+ property @androidx.compose.runtime.Composable public final long actionContentColor;
+ property @androidx.compose.runtime.Composable public final long color;
+ property @androidx.compose.runtime.Composable public final long contentColor;
+ property @androidx.compose.runtime.Composable public final long dismissActionContentColor;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
field public static final androidx.compose.material3.SnackbarDefaults INSTANCE;
}
@@ -632,11 +631,11 @@
public final class TabRowDefaults {
method @androidx.compose.runtime.Composable public void Divider(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
method @androidx.compose.runtime.Composable public void Indicator(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
- method @androidx.compose.runtime.Composable public long getColor();
+ method @androidx.compose.runtime.Composable public long getContainerColor();
method @androidx.compose.runtime.Composable public long getContentColor();
method public androidx.compose.ui.Modifier tabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material3.TabPosition currentTabPosition);
- property @androidx.compose.runtime.Composable public final long Color;
- property @androidx.compose.runtime.Composable public final long ContentColor;
+ property @androidx.compose.runtime.Composable public final long containerColor;
+ property @androidx.compose.runtime.Composable public final long contentColor;
field public static final androidx.compose.material3.TabRowDefaults INSTANCE;
}
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index 94eb7e0..e9d5bf1 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -8,12 +8,12 @@
method @androidx.compose.runtime.Composable public long getTextContentColor();
method @androidx.compose.runtime.Composable public long getTitleContentColor();
method public float getTonalElevation();
- property @androidx.compose.runtime.Composable public final long ContainerColor;
- property @androidx.compose.runtime.Composable public final long IconContentColor;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
- property @androidx.compose.runtime.Composable public final long TextContentColor;
- property @androidx.compose.runtime.Composable public final long TitleContentColor;
property public final float TonalElevation;
+ property @androidx.compose.runtime.Composable public final long containerColor;
+ property @androidx.compose.runtime.Composable public final long iconContentColor;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
+ property @androidx.compose.runtime.Composable public final long textContentColor;
+ property @androidx.compose.runtime.Composable public final long titleContentColor;
field public static final androidx.compose.material3.AlertDialogDefaults INSTANCE;
}
@@ -50,13 +50,13 @@
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
property public final float Height;
property public final float IconSize;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
field public static final androidx.compose.material3.AssistChipDefaults INSTANCE;
}
public final class BadgeDefaults {
method @androidx.compose.runtime.Composable public long getContainerColor();
- property @androidx.compose.runtime.Composable public final long ContainerColor;
+ property @androidx.compose.runtime.Composable public final long containerColor;
field public static final androidx.compose.material3.BadgeDefaults INSTANCE;
}
@@ -66,26 +66,21 @@
}
public final class BottomAppBarDefaults {
- method @androidx.compose.runtime.Composable public void FloatingActionButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, optional androidx.compose.material3.FloatingActionButtonElevation elevation, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public long getBottomAppBarFabColor();
method @androidx.compose.runtime.Composable public long getContainerColor();
method public float getContainerElevation();
method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
- method @androidx.compose.runtime.Composable public long getFloatingActionButtonContainerColor();
- method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getFloatingActionButtonShape();
- property @androidx.compose.runtime.Composable public final long ContainerColor;
property public final float ContainerElevation;
property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
- property @androidx.compose.runtime.Composable public final long FloatingActionButtonContainerColor;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape FloatingActionButtonShape;
+ property @androidx.compose.runtime.Composable public final long bottomAppBarFabColor;
+ property @androidx.compose.runtime.Composable public final long containerColor;
field public static final androidx.compose.material3.BottomAppBarDefaults INSTANCE;
}
- public static final class BottomAppBarDefaults.FloatingActionButtonElevation implements androidx.compose.material3.FloatingActionButtonElevation {
- method public androidx.compose.runtime.MutableState<androidx.compose.ui.unit.Dp> getElevation();
+ public static final class BottomAppBarDefaults.BottomAppBarFabElevation implements androidx.compose.material3.FloatingActionButtonElevation {
method @androidx.compose.runtime.Composable public androidx.compose.runtime.MutableState<androidx.compose.ui.unit.Dp> shadowElevation(androidx.compose.foundation.interaction.InteractionSource interactionSource);
method @androidx.compose.runtime.Composable public androidx.compose.runtime.MutableState<androidx.compose.ui.unit.Dp> tonalElevation(androidx.compose.foundation.interaction.InteractionSource interactionSource);
- property public final androidx.compose.runtime.MutableState<androidx.compose.ui.unit.Dp> elevation;
- field public static final androidx.compose.material3.BottomAppBarDefaults.FloatingActionButtonElevation INSTANCE;
+ field public static final androidx.compose.material3.BottomAppBarDefaults.BottomAppBarFabElevation INSTANCE;
}
@androidx.compose.runtime.Stable public interface ButtonColors {
@@ -117,17 +112,17 @@
method @androidx.compose.runtime.Composable public androidx.compose.material3.ButtonColors textButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
property public final androidx.compose.foundation.layout.PaddingValues ButtonWithIconContentPadding;
property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape ElevatedShape;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape FilledTonalShape;
property public final float IconSize;
property public final float IconSpacing;
property public final float MinHeight;
property public final float MinWidth;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape OutlinedShape;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
property public final androidx.compose.foundation.layout.PaddingValues TextButtonContentPadding;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape TextShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape elevatedShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape filledTonalShape;
property @androidx.compose.runtime.Composable public final androidx.compose.foundation.BorderStroke outlinedButtonBorder;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape outlinedShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape textShape;
field public static final androidx.compose.material3.ButtonDefaults INSTANCE;
}
@@ -160,9 +155,9 @@
method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke outlinedCardBorder(optional boolean enabled);
method @androidx.compose.runtime.Composable public androidx.compose.material3.CardColors outlinedCardColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
method @androidx.compose.runtime.Composable public androidx.compose.material3.CardElevation outlinedCardElevation(optional float defaultElevation, optional float pressedElevation, optional float focusedElevation, optional float hoveredElevation, optional float draggedElevation, optional float disabledElevation);
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape ElevatedShape;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape OutlinedShape;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape elevatedShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape outlinedShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
field public static final androidx.compose.material3.CardDefaults INSTANCE;
}
@@ -215,16 +210,16 @@
public final class ChipKt {
method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void AssistChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.ChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ChipBorder? border, optional androidx.compose.material3.ChipColors colors);
method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ElevatedAssistChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.ChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ChipBorder? border, optional androidx.compose.material3.ChipColors colors);
- method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ElevatedFilterChip(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? selectedIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.SelectableChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SelectableChipBorder? border, optional androidx.compose.material3.SelectableChipColors colors);
+ method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ElevatedFilterChip(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.SelectableChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SelectableChipBorder? border, optional androidx.compose.material3.SelectableChipColors colors);
method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ElevatedSuggestionChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.ChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ChipBorder? border, optional androidx.compose.material3.ChipColors colors);
- method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void FilterChip(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? selectedIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.SelectableChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SelectableChipBorder? border, optional androidx.compose.material3.SelectableChipColors colors);
+ method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void FilterChip(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.SelectableChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SelectableChipBorder? border, optional androidx.compose.material3.SelectableChipColors colors);
method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void InputChip(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? avatar, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.SelectableChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SelectableChipBorder? border, optional androidx.compose.material3.SelectableChipColors colors);
method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SuggestionChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.ChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ChipBorder? border, optional androidx.compose.material3.ChipColors colors);
}
@androidx.compose.runtime.Stable public final class ColorScheme {
- ctor public ColorScheme(long primary, long onPrimary, long primaryContainer, long onPrimaryContainer, long inversePrimary, long secondary, long onSecondary, long secondaryContainer, long onSecondaryContainer, long tertiary, long onTertiary, long tertiaryContainer, long onTertiaryContainer, long background, long onBackground, long surface, long onSurface, long surfaceVariant, long onSurfaceVariant, long surfaceTint, long inverseSurface, long inverseOnSurface, long error, long onError, long errorContainer, long onErrorContainer, long outline);
- method public androidx.compose.material3.ColorScheme copy(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline);
+ ctor public ColorScheme(long primary, long onPrimary, long primaryContainer, long onPrimaryContainer, long inversePrimary, long secondary, long onSecondary, long secondaryContainer, long onSecondaryContainer, long tertiary, long onTertiary, long tertiaryContainer, long onTertiaryContainer, long background, long onBackground, long surface, long onSurface, long surfaceVariant, long onSurfaceVariant, long surfaceTint, long inverseSurface, long inverseOnSurface, long error, long onError, long errorContainer, long onErrorContainer, long outline, long outlineVariant, long scrim);
+ method public androidx.compose.material3.ColorScheme copy(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim);
method public long getBackground();
method public long getError();
method public long getErrorContainer();
@@ -243,8 +238,10 @@
method public long getOnTertiary();
method public long getOnTertiaryContainer();
method public long getOutline();
+ method public long getOutlineVariant();
method public long getPrimary();
method public long getPrimaryContainer();
+ method public long getScrim();
method public long getSecondary();
method public long getSecondaryContainer();
method public long getSurface();
@@ -270,8 +267,10 @@
property public final long onTertiary;
property public final long onTertiaryContainer;
property public final long outline;
+ property public final long outlineVariant;
property public final long primary;
property public final long primaryContainer;
+ property public final long scrim;
property public final long secondary;
property public final long secondaryContainer;
property public final long surface;
@@ -284,8 +283,8 @@
public final class ColorSchemeKt {
method public static long contentColorFor(androidx.compose.material3.ColorScheme, long backgroundColor);
method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static long contentColorFor(long backgroundColor);
- method public static androidx.compose.material3.ColorScheme darkColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline);
- method public static androidx.compose.material3.ColorScheme lightColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline);
+ method public static androidx.compose.material3.ColorScheme darkColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim);
+ method public static androidx.compose.material3.ColorScheme lightColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim);
method public static long surfaceColorAtElevation(androidx.compose.material3.ColorScheme, float elevation);
}
@@ -297,8 +296,8 @@
public final class DividerDefaults {
method @androidx.compose.runtime.Composable public long getColor();
method public float getThickness();
- property @androidx.compose.runtime.Composable public final long Color;
property public final float Thickness;
+ property @androidx.compose.runtime.Composable public final long color;
field public static final androidx.compose.material3.DividerDefaults INSTANCE;
}
@@ -316,12 +315,12 @@
method public float getPermanentDrawerElevation();
method @androidx.compose.runtime.Composable public long getScrimColor();
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
- property @androidx.compose.runtime.Composable public final long ContainerColor;
property public final float DismissibleDrawerElevation;
property public final float ModalDrawerElevation;
property public final float PermanentDrawerElevation;
- property @androidx.compose.runtime.Composable public final long ScrimColor;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
+ property @androidx.compose.runtime.Composable public final long containerColor;
+ property @androidx.compose.runtime.Composable public final long scrimColor;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
field public static final androidx.compose.material3.DrawerDefaults INSTANCE;
}
@@ -406,7 +405,7 @@
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
property public final float Height;
property public final float IconSize;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
field public static final androidx.compose.material3.FilterChipDefaults INSTANCE;
}
@@ -419,12 +418,12 @@
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getSmallShape();
method @androidx.compose.runtime.Composable public androidx.compose.material3.FloatingActionButtonElevation loweredElevation(optional float defaultElevation, optional float pressedElevation, optional float focusedElevation, optional float hoveredElevation);
- property @androidx.compose.runtime.Composable public final long ContainerColor;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape ExtendedFabShape;
property public final float LargeIconSize;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape LargeShape;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape SmallShape;
+ property @androidx.compose.runtime.Composable public final long containerColor;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape extendedFabShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape largeShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape smallShape;
field public static final androidx.compose.material3.FloatingActionButtonDefaults INSTANCE;
}
@@ -459,8 +458,8 @@
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors outlinedIconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke? outlinedIconToggleButtonBorder(boolean enabled, boolean checked);
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors outlinedIconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape FilledShape;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape OutlinedShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape filledShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape outlinedShape;
field public static final androidx.compose.material3.IconButtonDefaults INSTANCE;
}
@@ -500,7 +499,7 @@
property public final float AvatarSize;
property public final float Height;
property public final float IconSize;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
field public static final androidx.compose.material3.InputChipDefaults INSTANCE;
}
@@ -519,10 +518,10 @@
method @androidx.compose.runtime.Composable public long getContentColor();
method public float getElevation();
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
- property @androidx.compose.runtime.Composable public final long ContainerColor;
- property @androidx.compose.runtime.Composable public final long ContentColor;
property public final float Elevation;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
+ property @androidx.compose.runtime.Composable public final long containerColor;
+ property @androidx.compose.runtime.Composable public final long contentColor;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
field public static final androidx.compose.material3.ListItemDefaults INSTANCE;
}
@@ -564,8 +563,8 @@
public final class NavigationBarDefaults {
method @androidx.compose.runtime.Composable public long getContainerColor();
method public float getElevation();
- property @androidx.compose.runtime.Composable public final long ContainerColor;
property public final float Elevation;
+ property @androidx.compose.runtime.Composable public final long containerColor;
field public static final androidx.compose.material3.NavigationBarDefaults INSTANCE;
}
@@ -643,11 +642,11 @@
method @androidx.compose.runtime.Composable public long getLinearColor();
method @androidx.compose.runtime.Composable public long getLinearTrackColor();
method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getProgressAnimationSpec();
- property @androidx.compose.runtime.Composable public final long CircularColor;
property public final float CircularStrokeWidth;
- property @androidx.compose.runtime.Composable public final long LinearColor;
- property @androidx.compose.runtime.Composable public final long LinearTrackColor;
property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> ProgressAnimationSpec;
+ property @androidx.compose.runtime.Composable public final long circularColor;
+ property @androidx.compose.runtime.Composable public final long linearColor;
+ property @androidx.compose.runtime.Composable public final long linearTrackColor;
field public static final androidx.compose.material3.ProgressIndicatorDefaults INSTANCE;
}
@@ -753,12 +752,12 @@
method @androidx.compose.runtime.Composable public long getContentColor();
method @androidx.compose.runtime.Composable public long getDismissActionContentColor();
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
- property @androidx.compose.runtime.Composable public final long ActionColor;
- property @androidx.compose.runtime.Composable public final long ActionContentColor;
- property @androidx.compose.runtime.Composable public final long Color;
- property @androidx.compose.runtime.Composable public final long ContentColor;
- property @androidx.compose.runtime.Composable public final long DismissActionContentColor;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
+ property @androidx.compose.runtime.Composable public final long actionColor;
+ property @androidx.compose.runtime.Composable public final long actionContentColor;
+ property @androidx.compose.runtime.Composable public final long color;
+ property @androidx.compose.runtime.Composable public final long contentColor;
+ property @androidx.compose.runtime.Composable public final long dismissActionContentColor;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
field public static final androidx.compose.material3.SnackbarDefaults INSTANCE;
}
@@ -819,7 +818,7 @@
method @androidx.compose.runtime.Composable public androidx.compose.material3.ChipElevation suggestionChipElevation(optional float defaultElevation, optional float pressedElevation, optional float focusedElevation, optional float hoveredElevation, optional float draggedElevation, optional float disabledElevation);
property public final float Height;
property public final float IconSize;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
field public static final androidx.compose.material3.SuggestionChipDefaults INSTANCE;
}
@@ -871,11 +870,11 @@
public final class TabRowDefaults {
method @androidx.compose.runtime.Composable public void Divider(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
method @androidx.compose.runtime.Composable public void Indicator(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
- method @androidx.compose.runtime.Composable public long getColor();
+ method @androidx.compose.runtime.Composable public long getContainerColor();
method @androidx.compose.runtime.Composable public long getContentColor();
method public androidx.compose.ui.Modifier tabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material3.TabPosition currentTabPosition);
- property @androidx.compose.runtime.Composable public final long Color;
- property @androidx.compose.runtime.Composable public final long ContentColor;
+ property @androidx.compose.runtime.Composable public final long containerColor;
+ property @androidx.compose.runtime.Composable public final long contentColor;
field public static final androidx.compose.material3.TabRowDefaults INSTANCE;
}
@@ -913,12 +912,12 @@
method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.TextFieldColors textFieldColors(optional long textColor, optional long disabledTextColor, optional long containerColor, optional long cursorColor, optional long errorCursorColor, optional androidx.compose.foundation.text.selection.TextSelectionColors selectionColors, optional long focusedIndicatorColor, optional long unfocusedIndicatorColor, optional long disabledIndicatorColor, optional long errorIndicatorColor, optional long focusedLeadingIconColor, optional long unfocusedLeadingIconColor, optional long disabledLeadingIconColor, optional long errorLeadingIconColor, optional long focusedTrailingIconColor, optional long unfocusedTrailingIconColor, optional long disabledTrailingIconColor, optional long errorTrailingIconColor, optional long focusedLabelColor, optional long unfocusedLabelColor, optional long disabledLabelColor, optional long errorLabelColor, optional long placeholderColor, optional long disabledPlaceholderColor);
method @androidx.compose.material3.ExperimentalMaterial3Api public androidx.compose.foundation.layout.PaddingValues textFieldWithLabelPadding(optional float start, optional float end, optional float top, optional float bottom);
method @androidx.compose.material3.ExperimentalMaterial3Api public androidx.compose.foundation.layout.PaddingValues textFieldWithoutLabelPadding(optional float start, optional float top, optional float end, optional float bottom);
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape FilledShape;
property public final float FocusedBorderThickness;
property public final float MinHeight;
property public final float MinWidth;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape OutlinedShape;
property public final float UnfocusedBorderThickness;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape filledShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape outlinedShape;
field public static final androidx.compose.material3.TextFieldDefaults INSTANCE;
}
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index 7a57b3e..88e4217 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -8,12 +8,12 @@
method @androidx.compose.runtime.Composable public long getTextContentColor();
method @androidx.compose.runtime.Composable public long getTitleContentColor();
method public float getTonalElevation();
- property @androidx.compose.runtime.Composable public final long ContainerColor;
- property @androidx.compose.runtime.Composable public final long IconContentColor;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
- property @androidx.compose.runtime.Composable public final long TextContentColor;
- property @androidx.compose.runtime.Composable public final long TitleContentColor;
property public final float TonalElevation;
+ property @androidx.compose.runtime.Composable public final long containerColor;
+ property @androidx.compose.runtime.Composable public final long iconContentColor;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
+ property @androidx.compose.runtime.Composable public final long textContentColor;
+ property @androidx.compose.runtime.Composable public final long titleContentColor;
field public static final androidx.compose.material3.AlertDialogDefaults INSTANCE;
}
@@ -36,7 +36,7 @@
public final class BadgeDefaults {
method @androidx.compose.runtime.Composable public long getContainerColor();
- property @androidx.compose.runtime.Composable public final long ContainerColor;
+ property @androidx.compose.runtime.Composable public final long containerColor;
field public static final androidx.compose.material3.BadgeDefaults INSTANCE;
}
@@ -46,26 +46,21 @@
}
public final class BottomAppBarDefaults {
- method @androidx.compose.runtime.Composable public void FloatingActionButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional long contentColor, optional androidx.compose.material3.FloatingActionButtonElevation elevation, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public long getBottomAppBarFabColor();
method @androidx.compose.runtime.Composable public long getContainerColor();
method public float getContainerElevation();
method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
- method @androidx.compose.runtime.Composable public long getFloatingActionButtonContainerColor();
- method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getFloatingActionButtonShape();
- property @androidx.compose.runtime.Composable public final long ContainerColor;
property public final float ContainerElevation;
property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
- property @androidx.compose.runtime.Composable public final long FloatingActionButtonContainerColor;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape FloatingActionButtonShape;
+ property @androidx.compose.runtime.Composable public final long bottomAppBarFabColor;
+ property @androidx.compose.runtime.Composable public final long containerColor;
field public static final androidx.compose.material3.BottomAppBarDefaults INSTANCE;
}
- public static final class BottomAppBarDefaults.FloatingActionButtonElevation implements androidx.compose.material3.FloatingActionButtonElevation {
- method public androidx.compose.runtime.MutableState<androidx.compose.ui.unit.Dp> getElevation();
+ public static final class BottomAppBarDefaults.BottomAppBarFabElevation implements androidx.compose.material3.FloatingActionButtonElevation {
method @androidx.compose.runtime.Composable public androidx.compose.runtime.MutableState<androidx.compose.ui.unit.Dp> shadowElevation(androidx.compose.foundation.interaction.InteractionSource interactionSource);
method @androidx.compose.runtime.Composable public androidx.compose.runtime.MutableState<androidx.compose.ui.unit.Dp> tonalElevation(androidx.compose.foundation.interaction.InteractionSource interactionSource);
- property public final androidx.compose.runtime.MutableState<androidx.compose.ui.unit.Dp> elevation;
- field public static final androidx.compose.material3.BottomAppBarDefaults.FloatingActionButtonElevation INSTANCE;
+ field public static final androidx.compose.material3.BottomAppBarDefaults.BottomAppBarFabElevation INSTANCE;
}
@androidx.compose.runtime.Stable public interface ButtonColors {
@@ -97,17 +92,17 @@
method @androidx.compose.runtime.Composable public androidx.compose.material3.ButtonColors textButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
property public final androidx.compose.foundation.layout.PaddingValues ButtonWithIconContentPadding;
property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape ElevatedShape;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape FilledTonalShape;
property public final float IconSize;
property public final float IconSpacing;
property public final float MinHeight;
property public final float MinWidth;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape OutlinedShape;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
property public final androidx.compose.foundation.layout.PaddingValues TextButtonContentPadding;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape TextShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape elevatedShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape filledTonalShape;
property @androidx.compose.runtime.Composable public final androidx.compose.foundation.BorderStroke outlinedButtonBorder;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape outlinedShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape textShape;
field public static final androidx.compose.material3.ButtonDefaults INSTANCE;
}
@@ -140,9 +135,9 @@
method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke outlinedCardBorder(optional boolean enabled);
method @androidx.compose.runtime.Composable public androidx.compose.material3.CardColors outlinedCardColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
method @androidx.compose.runtime.Composable public androidx.compose.material3.CardElevation outlinedCardElevation(optional float defaultElevation, optional float pressedElevation, optional float focusedElevation, optional float hoveredElevation, optional float draggedElevation, optional float disabledElevation);
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape ElevatedShape;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape OutlinedShape;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape elevatedShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape outlinedShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
field public static final androidx.compose.material3.CardDefaults INSTANCE;
}
@@ -177,8 +172,8 @@
}
@androidx.compose.runtime.Stable public final class ColorScheme {
- ctor public ColorScheme(long primary, long onPrimary, long primaryContainer, long onPrimaryContainer, long inversePrimary, long secondary, long onSecondary, long secondaryContainer, long onSecondaryContainer, long tertiary, long onTertiary, long tertiaryContainer, long onTertiaryContainer, long background, long onBackground, long surface, long onSurface, long surfaceVariant, long onSurfaceVariant, long surfaceTint, long inverseSurface, long inverseOnSurface, long error, long onError, long errorContainer, long onErrorContainer, long outline);
- method public androidx.compose.material3.ColorScheme copy(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline);
+ ctor public ColorScheme(long primary, long onPrimary, long primaryContainer, long onPrimaryContainer, long inversePrimary, long secondary, long onSecondary, long secondaryContainer, long onSecondaryContainer, long tertiary, long onTertiary, long tertiaryContainer, long onTertiaryContainer, long background, long onBackground, long surface, long onSurface, long surfaceVariant, long onSurfaceVariant, long surfaceTint, long inverseSurface, long inverseOnSurface, long error, long onError, long errorContainer, long onErrorContainer, long outline, long outlineVariant, long scrim);
+ method public androidx.compose.material3.ColorScheme copy(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim);
method public long getBackground();
method public long getError();
method public long getErrorContainer();
@@ -197,8 +192,10 @@
method public long getOnTertiary();
method public long getOnTertiaryContainer();
method public long getOutline();
+ method public long getOutlineVariant();
method public long getPrimary();
method public long getPrimaryContainer();
+ method public long getScrim();
method public long getSecondary();
method public long getSecondaryContainer();
method public long getSurface();
@@ -224,8 +221,10 @@
property public final long onTertiary;
property public final long onTertiaryContainer;
property public final long outline;
+ property public final long outlineVariant;
property public final long primary;
property public final long primaryContainer;
+ property public final long scrim;
property public final long secondary;
property public final long secondaryContainer;
property public final long surface;
@@ -238,8 +237,8 @@
public final class ColorSchemeKt {
method public static long contentColorFor(androidx.compose.material3.ColorScheme, long backgroundColor);
method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static long contentColorFor(long backgroundColor);
- method public static androidx.compose.material3.ColorScheme darkColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline);
- method public static androidx.compose.material3.ColorScheme lightColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline);
+ method public static androidx.compose.material3.ColorScheme darkColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim);
+ method public static androidx.compose.material3.ColorScheme lightColorScheme(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim);
method public static long surfaceColorAtElevation(androidx.compose.material3.ColorScheme, float elevation);
}
@@ -251,8 +250,8 @@
public final class DividerDefaults {
method @androidx.compose.runtime.Composable public long getColor();
method public float getThickness();
- property @androidx.compose.runtime.Composable public final long Color;
property public final float Thickness;
+ property @androidx.compose.runtime.Composable public final long color;
field public static final androidx.compose.material3.DividerDefaults INSTANCE;
}
@@ -283,12 +282,12 @@
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getSmallShape();
method @androidx.compose.runtime.Composable public androidx.compose.material3.FloatingActionButtonElevation loweredElevation(optional float defaultElevation, optional float pressedElevation, optional float focusedElevation, optional float hoveredElevation);
- property @androidx.compose.runtime.Composable public final long ContainerColor;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape ExtendedFabShape;
property public final float LargeIconSize;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape LargeShape;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape SmallShape;
+ property @androidx.compose.runtime.Composable public final long containerColor;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape extendedFabShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape largeShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape smallShape;
field public static final androidx.compose.material3.FloatingActionButtonDefaults INSTANCE;
}
@@ -323,8 +322,8 @@
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors outlinedIconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke? outlinedIconToggleButtonBorder(boolean enabled, boolean checked);
method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors outlinedIconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape FilledShape;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape OutlinedShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape filledShape;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape outlinedShape;
field public static final androidx.compose.material3.IconButtonDefaults INSTANCE;
}
@@ -384,8 +383,8 @@
public final class NavigationBarDefaults {
method @androidx.compose.runtime.Composable public long getContainerColor();
method public float getElevation();
- property @androidx.compose.runtime.Composable public final long ContainerColor;
property public final float Elevation;
+ property @androidx.compose.runtime.Composable public final long containerColor;
field public static final androidx.compose.material3.NavigationBarDefaults INSTANCE;
}
@@ -441,11 +440,11 @@
method @androidx.compose.runtime.Composable public long getLinearColor();
method @androidx.compose.runtime.Composable public long getLinearTrackColor();
method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getProgressAnimationSpec();
- property @androidx.compose.runtime.Composable public final long CircularColor;
property public final float CircularStrokeWidth;
- property @androidx.compose.runtime.Composable public final long LinearColor;
- property @androidx.compose.runtime.Composable public final long LinearTrackColor;
property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> ProgressAnimationSpec;
+ property @androidx.compose.runtime.Composable public final long circularColor;
+ property @androidx.compose.runtime.Composable public final long linearColor;
+ property @androidx.compose.runtime.Composable public final long linearTrackColor;
field public static final androidx.compose.material3.ProgressIndicatorDefaults INSTANCE;
}
@@ -533,12 +532,12 @@
method @androidx.compose.runtime.Composable public long getContentColor();
method @androidx.compose.runtime.Composable public long getDismissActionContentColor();
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
- property @androidx.compose.runtime.Composable public final long ActionColor;
- property @androidx.compose.runtime.Composable public final long ActionContentColor;
- property @androidx.compose.runtime.Composable public final long Color;
- property @androidx.compose.runtime.Composable public final long ContentColor;
- property @androidx.compose.runtime.Composable public final long DismissActionContentColor;
- property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
+ property @androidx.compose.runtime.Composable public final long actionColor;
+ property @androidx.compose.runtime.Composable public final long actionContentColor;
+ property @androidx.compose.runtime.Composable public final long color;
+ property @androidx.compose.runtime.Composable public final long contentColor;
+ property @androidx.compose.runtime.Composable public final long dismissActionContentColor;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape shape;
field public static final androidx.compose.material3.SnackbarDefaults INSTANCE;
}
@@ -632,11 +631,11 @@
public final class TabRowDefaults {
method @androidx.compose.runtime.Composable public void Divider(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
method @androidx.compose.runtime.Composable public void Indicator(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
- method @androidx.compose.runtime.Composable public long getColor();
+ method @androidx.compose.runtime.Composable public long getContainerColor();
method @androidx.compose.runtime.Composable public long getContentColor();
method public androidx.compose.ui.Modifier tabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material3.TabPosition currentTabPosition);
- property @androidx.compose.runtime.Composable public final long Color;
- property @androidx.compose.runtime.Composable public final long ContentColor;
+ property @androidx.compose.runtime.Composable public final long containerColor;
+ property @androidx.compose.runtime.Composable public final long contentColor;
field public static final androidx.compose.material3.TabRowDefaults INSTANCE;
}
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index f756603..c8eb38d 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -31,6 +31,7 @@
import androidx.compose.material3.samples.ButtonWithIconSample
import androidx.compose.material3.samples.CardSample
import androidx.compose.material3.samples.CheckboxSample
+import androidx.compose.material3.samples.CheckboxWithTextSample
import androidx.compose.material3.samples.ChipGroupSingleLineSample
import androidx.compose.material3.samples.CircularProgressIndicatorSample
import androidx.compose.material3.samples.ClickableCardSample
@@ -237,6 +238,13 @@
CheckboxSample()
},
Example(
+ name = ::CheckboxWithTextSample.name,
+ description = CheckboxesExampleDescription,
+ sourceUrl = CheckboxesExampleSourceUrl
+ ) {
+ CheckboxWithTextSample()
+ },
+ Example(
name = ::TriStateCheckboxSample.name,
description = CheckboxesExampleDescription,
sourceUrl = CheckboxesExampleSourceUrl
@@ -886,6 +894,7 @@
Box(
Modifier
.wrapContentWidth()
- .width(280.dp)) { it.content() }
+ .width(280.dp)
+ ) { it.content() }
})
}
diff --git a/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/ColorSchemeDemo.kt b/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/ColorSchemeDemo.kt
index 9dbadaf..8df1367 100644
--- a/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/ColorSchemeDemo.kt
+++ b/compose/material3/material3/integration-tests/material3-demos/src/main/java/androidx/compose/material3/demos/ColorSchemeDemo.kt
@@ -139,9 +139,19 @@
onColorContainerText = "On Error Container")
Spacer(modifier = Modifier.height(16.dp))
Text("Utility", style = MaterialTheme.typography.bodyLarge)
- ColorTile(
- text = "Outline",
- color = colorScheme.outline,
+ DoubleTile(
+ leftTile = {
+ ColorTile(
+ text = "Outline",
+ color = colorScheme.outline,
+ )
+ },
+ rightTile = {
+ ColorTile(
+ text = "Outline Variant",
+ color = colorScheme.outlineVariant,
+ )
+ }
)
}
}
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt
index 3ba030d..8cfa713d 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt
@@ -32,6 +32,7 @@
import androidx.compose.material3.BottomAppBarDefaults
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeTopAppBar
@@ -404,10 +405,12 @@
}
},
floatingActionButton = {
- BottomAppBarDefaults.FloatingActionButton(
+ FloatingActionButton(
onClick = { /* do something */ },
+ containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
+ elevation = BottomAppBarDefaults.BottomAppBarFabElevation
) {
- Icon(Icons.Filled.Add, contentDescription = "Localized description")
+ Icon(Icons.Filled.Add, "Localized description")
}
}
)
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CheckboxSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CheckboxSamples.kt
index b363467..b057f4d 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CheckboxSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CheckboxSamples.kt
@@ -18,15 +18,23 @@
import androidx.annotation.Sampled
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.selection.toggleable
import androidx.compose.material3.Checkbox
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
import androidx.compose.material3.TriStateCheckbox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.Role
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.unit.dp
@@ -42,6 +50,34 @@
@Sampled
@Composable
+fun CheckboxWithTextSample() {
+ val (checkedState, onStateChange) = remember { mutableStateOf(true) }
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .height(56.dp)
+ .toggleable(
+ value = checkedState,
+ onValueChange = { onStateChange(!checkedState) },
+ role = Role.Checkbox
+ )
+ .padding(horizontal = 16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Checkbox(
+ checked = checkedState,
+ onCheckedChange = null // null recommended for accessibility with screenreaders
+ )
+ Text(
+ text = "Option selection",
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier.padding(start = 16.dp)
+ )
+ }
+}
+
+@Sampled
+@Composable
fun TriStateCheckboxSample() {
Column {
// define dependent checkboxes states
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ChipSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ChipSamples.kt
index 22c5d76..2837fd9 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ChipSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ChipSamples.kt
@@ -93,12 +93,16 @@
selected = selected,
onClick = { selected = !selected },
label = { Text("Filter chip") },
- selectedIcon = {
- Icon(
- imageVector = Icons.Filled.Done,
- contentDescription = "Localized Description",
- modifier = Modifier.size(FilterChipDefaults.IconSize)
- )
+ leadingIcon = if (selected) {
+ {
+ Icon(
+ imageVector = Icons.Filled.Done,
+ contentDescription = "Localized Description",
+ modifier = Modifier.size(FilterChipDefaults.IconSize)
+ )
+ }
+ } else {
+ null
}
)
}
@@ -112,12 +116,16 @@
selected = selected,
onClick = { selected = !selected },
label = { Text("Filter chip") },
- selectedIcon = {
- Icon(
- imageVector = Icons.Filled.Done,
- contentDescription = "Localized Description",
- modifier = Modifier.size(FilterChipDefaults.IconSize)
- )
+ leadingIcon = if (selected) {
+ {
+ Icon(
+ imageVector = Icons.Filled.Done,
+ contentDescription = "Localized Description",
+ modifier = Modifier.size(FilterChipDefaults.IconSize)
+ )
+ }
+ } else {
+ null
}
)
}
@@ -131,19 +139,22 @@
selected = selected,
onClick = { selected = !selected },
label = { Text("Filter chip") },
- leadingIcon = {
- Icon(
- imageVector = Icons.Filled.Home,
- contentDescription = "Localized description",
- modifier = Modifier.size(FilterChipDefaults.IconSize)
- )
- },
- selectedIcon = {
- Icon(
- imageVector = Icons.Filled.Done,
- contentDescription = "Localized Description",
- modifier = Modifier.size(FilterChipDefaults.IconSize)
- )
+ leadingIcon = if (selected) {
+ {
+ Icon(
+ imageVector = Icons.Filled.Done,
+ contentDescription = "Localized Description",
+ modifier = Modifier.size(FilterChipDefaults.IconSize)
+ )
+ }
+ } else {
+ {
+ Icon(
+ imageVector = Icons.Filled.Home,
+ contentDescription = "Localized description",
+ modifier = Modifier.size(FilterChipDefaults.IconSize)
+ )
+ }
}
)
}
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/RadioButtonSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/RadioButtonSamples.kt
index 82b81d6..b0af8fd 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/RadioButtonSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/RadioButtonSamples.kt
@@ -35,6 +35,8 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
@Sampled
@@ -42,15 +44,20 @@
fun RadioButtonSample() {
// We have two radio buttons and only one can be selected
var state by remember { mutableStateOf(true) }
- // Note that Modifier.selectableGroup() is essential to ensure correct accessibility behavior
+ // Note that Modifier.selectableGroup() is essential to ensure correct accessibility behavior.
+ // We also set a content description for this sample, but note that a RadioButton would usually
+ // be part of a higher level component, such as a raw with text, and that component would need
+ // to provide an appropriate content description. See RadioGroupSample.
Row(Modifier.selectableGroup()) {
RadioButton(
selected = state,
- onClick = { state = true }
+ onClick = { state = true },
+ modifier = Modifier.semantics { contentDescription = "Localized Description" }
)
RadioButton(
selected = !state,
- onClick = { state = false }
+ onClick = { state = false },
+ modifier = Modifier.semantics { contentDescription = "Localized Description" }
)
}
}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
index 0b29639..10c2353 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
@@ -312,8 +312,10 @@
}
},
floatingActionButton = {
- BottomAppBarDefaults.FloatingActionButton(
- onClick = { /* do something */ }
+ FloatingActionButton(
+ onClick = { /* do something */ },
+ containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
+ elevation = BottomAppBarDefaults.BottomAppBarFabElevation
) {
Icon(Icons.Filled.Add, "Localized description")
}
@@ -342,8 +344,10 @@
}
},
floatingActionButton = {
- BottomAppBarDefaults.FloatingActionButton(
- onClick = { /* do something */ }
+ FloatingActionButton(
+ onClick = { /* do something */ },
+ containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
+ elevation = BottomAppBarDefaults.BottomAppBarFabElevation
) {
Icon(Icons.Filled.Add, "Localized description")
}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt
index a9c1692..a83167cb 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt
@@ -814,8 +814,10 @@
BottomAppBar(
icons = {},
floatingActionButton = {
- BottomAppBarDefaults.FloatingActionButton(
- onClick = { /* do something */ }
+ FloatingActionButton(
+ onClick = { /* do something */ },
+ containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
+ elevation = BottomAppBarDefaults.BottomAppBarFabElevation
) {
Icon(Icons.Filled.Add, "Localized description")
}
@@ -863,9 +865,11 @@
icons = {},
Modifier.testTag("bar"),
floatingActionButton = {
- BottomAppBarDefaults.FloatingActionButton(
- modifier = Modifier.testTag("FAB"),
+ FloatingActionButton(
onClick = { /* do something */ },
+ modifier = Modifier.testTag("FAB"),
+ containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
+ elevation = BottomAppBarDefaults.BottomAppBarFabElevation
) {
Icon(Icons.Filled.Add, "Localized description")
}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipScreenshotTest.kt
index e1d448a..2df1372 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipScreenshotTest.kt
@@ -20,7 +20,6 @@
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done
-import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Search
import androidx.compose.testutils.assertAgainstGolden
@@ -329,7 +328,7 @@
onClick = {},
label = { Text("Filter Chip") },
modifier = Modifier.testTag(TestTag),
- selectedIcon = {
+ leadingIcon = {
Icon(
imageVector = Icons.Filled.Done,
contentDescription = "Localized Description",
@@ -342,58 +341,6 @@
}
@Test
- fun filterChip_flat_withLeadingIcon_selected_lightTheme() {
- rule.setMaterialContent(lightColorScheme()) {
- FilterChip(
- selected = true,
- onClick = {},
- label = { Text("Filter Chip") },
- modifier = Modifier.testTag(TestTag),
- leadingIcon = {
- Icon(
- Icons.Filled.Home,
- contentDescription = "Localized Description"
- )
- },
- selectedIcon = {
- Icon(
- imageVector = Icons.Filled.Done,
- contentDescription = "Localized Description",
- modifier = Modifier.requiredSize(FilterChipDefaults.IconSize)
- )
- }
- )
- }
- assertChipAgainstGolden("filterChip_flat_withLeadingIcon_selected_lightTheme")
- }
-
- @Test
- fun filterChip_flat_withLeadingIcon_selected_darkTheme() {
- rule.setMaterialContent(darkColorScheme()) {
- FilterChip(
- selected = true,
- onClick = {},
- label = { Text("Filter Chip") },
- modifier = Modifier.testTag(TestTag),
- leadingIcon = {
- Icon(
- Icons.Filled.Home,
- contentDescription = "Localized Description"
- )
- },
- selectedIcon = {
- Icon(
- imageVector = Icons.Filled.Done,
- contentDescription = "Localized Description",
- modifier = Modifier.requiredSize(FilterChipDefaults.IconSize)
- )
- }
- )
- }
- assertChipAgainstGolden("filterChip_flat_withLeadingIcon_selected_darkTheme")
- }
-
- @Test
fun filterChip_flat_notSelected() {
rule.setMaterialContent(lightColorScheme()) {
FilterChip(
@@ -415,7 +362,7 @@
label = { Text("Filter Chip") },
enabled = false,
modifier = Modifier.testTag(TestTag),
- selectedIcon = {
+ leadingIcon = {
Icon(
imageVector = Icons.Filled.Done,
tint = LocalContentColor.current,
@@ -429,33 +376,6 @@
}
@Test
- fun filterChip_flat_withLeadingIcon_disabled_selected() {
- rule.setMaterialContent(lightColorScheme()) {
- FilterChip(
- selected = true,
- onClick = {},
- label = { Text("Filter Chip") },
- enabled = false,
- modifier = Modifier.testTag(TestTag),
- leadingIcon = {
- Icon(
- Icons.Filled.Home,
- contentDescription = "Localized Description"
- )
- },
- selectedIcon = {
- Icon(
- imageVector = Icons.Filled.Done,
- contentDescription = "Localized Description",
- modifier = Modifier.requiredSize(FilterChipDefaults.IconSize)
- )
- }
- )
- }
- assertChipAgainstGolden("filterChip_flat_withLeadingIcon_disabled_selected")
- }
-
- @Test
fun filterChip_flat_disabled_notSelected() {
rule.setMaterialContent(lightColorScheme()) {
FilterChip(
@@ -477,7 +397,7 @@
onClick = {},
label = { Text("Filter Chip") },
modifier = Modifier.testTag(TestTag),
- selectedIcon = {
+ leadingIcon = {
Icon(
imageVector = Icons.Filled.Done,
contentDescription = "Localized Description",
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipTest.kt
index 1a173e3..2dd3963 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipTest.kt
@@ -360,13 +360,6 @@
"Filter chip",
Modifier.testTag(TestChipTag)
)
- },
- selectedIcon = {
- Icon(
- imageVector = Icons.Filled.Done,
- contentDescription = "Localized Description",
- modifier = Modifier.size(FilterChipDefaults.IconSize)
- )
})
}
@@ -395,7 +388,7 @@
Modifier.testTag(TestChipTag)
)
},
- selectedIcon = {
+ leadingIcon = {
Icon(
imageVector = Icons.Filled.Done,
contentDescription = "Localized Description",
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldScreenshotTest.kt
index 8a49d2f..cccb678 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldScreenshotTest.kt
@@ -17,7 +17,6 @@
package androidx.compose.material3
import android.os.Build
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredWidth
@@ -33,7 +32,6 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
@@ -78,15 +76,13 @@
@Test
fun outlinedTextField_withInput() {
rule.setMaterialContent(lightColorScheme()) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- val text = "Text"
- OutlinedTextField(
- value = TextFieldValue(text = text, selection = TextRange(text.length)),
- onValueChange = {},
- label = { Text("Label") },
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ val text = "Text"
+ OutlinedTextField(
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
+ onValueChange = {},
+ label = { Text("Label") },
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
assertAgainstGolden("outlined_textField_withInput")
@@ -95,14 +91,12 @@
@Test
fun outlinedTextField_notFocused() {
rule.setMaterialContent(lightColorScheme()) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = "",
- onValueChange = {},
- label = { Text("Label") },
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ label = { Text("Label") },
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
assertAgainstGolden("outlined_textField_not_focused")
@@ -111,14 +105,12 @@
@Test
fun outlinedTextField_focused() {
rule.setMaterialContent(lightColorScheme()) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = "",
- onValueChange = {},
- label = { Text("Label") },
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ label = { Text("Label") },
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
rule.onNodeWithTag(TextFieldTag).focus()
@@ -130,14 +122,12 @@
fun outlinedTextField_focused_rtl() {
rule.setMaterialContent(lightColorScheme()) {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = "",
- onValueChange = {},
- label = { Text("Label") },
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ label = { Text("Label") },
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
}
@@ -149,16 +139,14 @@
@Test
fun outlinedTextField_error_focused() {
rule.setMaterialContent(lightColorScheme()) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- val text = "Input"
- OutlinedTextField(
- value = TextFieldValue(text = text, selection = TextRange(text.length)),
- onValueChange = {},
- label = { Text("Label") },
- isError = true,
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ val text = "Input"
+ OutlinedTextField(
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
+ onValueChange = {},
+ label = { Text("Label") },
+ isError = true,
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
rule.onNodeWithTag(TextFieldTag).focus()
@@ -169,15 +157,13 @@
@Test
fun outlinedTextField_error_notFocused() {
rule.setMaterialContent(lightColorScheme()) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = "",
- onValueChange = {},
- label = { Text("Label") },
- isError = true,
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ label = { Text("Label") },
+ isError = true,
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
assertAgainstGolden("outlined_textField_notFocused_errorState")
@@ -391,15 +377,13 @@
@Test
fun outlinedTextField_disabled() {
rule.setMaterialContent(lightColorScheme()) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = TextFieldValue("Text"),
- onValueChange = {},
- singleLine = true,
- enabled = false,
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = TextFieldValue("Text"),
+ onValueChange = {},
+ singleLine = true,
+ enabled = false,
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
assertAgainstGolden("outlinedTextField_disabled")
@@ -408,15 +392,13 @@
@Test
fun outlinedTextField_disabled_notFocusable() {
rule.setMaterialContent(lightColorScheme()) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = TextFieldValue("Text"),
- onValueChange = {},
- singleLine = true,
- enabled = false,
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = TextFieldValue("Text"),
+ onValueChange = {},
+ singleLine = true,
+ enabled = false,
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
rule.onNodeWithTag(TextFieldTag).focus()
@@ -427,15 +409,13 @@
@Test
fun outlinedTextField_disabled_notScrolled() {
rule.setMaterialContent(lightColorScheme()) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = longText,
- onValueChange = { },
- singleLine = true,
- modifier = Modifier.requiredWidth(300.dp),
- enabled = false
- )
- }
+ OutlinedTextField(
+ value = longText,
+ onValueChange = { },
+ singleLine = true,
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(300.dp),
+ enabled = false
+ )
}
rule.mainClock.autoAdvance = false
@@ -589,15 +569,13 @@
@Test
fun outlinedTextField_withInput_darkTheme() {
rule.setMaterialContent(darkColorScheme()) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- val text = "Text"
- OutlinedTextField(
- value = TextFieldValue(text = text, selection = TextRange(text.length)),
- onValueChange = {},
- label = { Text("Label") },
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ val text = "Text"
+ OutlinedTextField(
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
+ onValueChange = {},
+ label = { Text("Label") },
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
assertAgainstGolden("outlined_textField_withInput_dark")
@@ -606,14 +584,12 @@
@Test
fun outlinedTextField_focused_darkTheme() {
rule.setMaterialContent(darkColorScheme()) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = "",
- onValueChange = {},
- label = { Text("Label") },
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ label = { Text("Label") },
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
rule.onNodeWithTag(TextFieldTag).focus()
@@ -624,16 +600,14 @@
@Test
fun outlinedTextField_error_focused_darkTheme() {
rule.setMaterialContent(darkColorScheme()) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- val text = "Input"
- OutlinedTextField(
- value = TextFieldValue(text = text, selection = TextRange(text.length)),
- onValueChange = {},
- label = { Text("Label") },
- isError = true,
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ val text = "Input"
+ OutlinedTextField(
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
+ onValueChange = {},
+ label = { Text("Label") },
+ isError = true,
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
rule.onNodeWithTag(TextFieldTag).focus()
@@ -644,15 +618,13 @@
@Test
fun outlinedTextField_disabled_darkTheme() {
rule.setMaterialContent(darkColorScheme()) {
- Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
- OutlinedTextField(
- value = TextFieldValue("Text"),
- onValueChange = {},
- singleLine = true,
- enabled = false,
- modifier = Modifier.requiredWidth(280.dp)
- )
- }
+ OutlinedTextField(
+ value = TextFieldValue("Text"),
+ onValueChange = {},
+ singleLine = true,
+ enabled = false,
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+ )
}
assertAgainstGolden("outlinedTextField_disabled_dark")
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SwitchScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SwitchScreenshotTest.kt
index 8a7c1bf..3d80b1c 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SwitchScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SwitchScreenshotTest.kt
@@ -188,7 +188,7 @@
.performTouchInput { move(); up() }
rule.waitForIdle()
- rule.mainClock.advanceTimeBy(milliseconds = 96)
+ rule.mainClock.advanceTimeBy(milliseconds = 100)
// Ripples are drawn on the RenderThread, not the main (UI) thread, so we can't wait for
// synchronization. Instead just wait until after the ripples are finished animating.
@@ -217,7 +217,7 @@
.performTouchInput { move(); up() }
rule.waitForIdle()
- rule.mainClock.advanceTimeBy(milliseconds = 96)
+ rule.mainClock.advanceTimeBy(milliseconds = 100)
// Ripples are drawn on the RenderThread, not the main (UI) thread, so we can't wait for
// synchronization. Instead just wait until after the ripples are finished animating.
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SwitchTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SwitchTest.kt
index be98b5c..9eb0f3d 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SwitchTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SwitchTest.kt
@@ -296,6 +296,27 @@
.assertLeftPositionInRootIsEqualTo(8.dp)
}
+ @Test
+ fun switch_constantState_doesNotAnimate() {
+ rule.setMaterialContent(lightColorScheme()) {
+ val spacer = @Composable { Spacer(Modifier.size(16.dp).testTag("spacer")) }
+ Switch(
+ modifier = Modifier.testTag(defaultSwitchTag),
+ checked = false,
+ thumbContent = spacer,
+ onCheckedChange = {},
+ )
+ }
+
+ rule.onNodeWithTag(defaultSwitchTag)
+ .performTouchInput {
+ click(center)
+ }
+
+ rule.onNodeWithTag("spacer", useUnmergedTree = true)
+ .assertLeftPositionInRootIsEqualTo(8.dp)
+ }
+
// regression test for b/191375128
@Test
fun switch_stateRestoration_stateChangeWhileSaved() {
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidAlertDialog.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidAlertDialog.android.kt
index 8469858..cd77885 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidAlertDialog.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidAlertDialog.android.kt
@@ -77,12 +77,12 @@
icon: @Composable (() -> Unit)? = null,
title: @Composable (() -> Unit)? = null,
text: @Composable (() -> Unit)? = null,
- shape: Shape = AlertDialogDefaults.Shape,
- containerColor: Color = AlertDialogDefaults.ContainerColor,
+ shape: Shape = AlertDialogDefaults.shape,
+ containerColor: Color = AlertDialogDefaults.containerColor,
tonalElevation: Dp = AlertDialogDefaults.TonalElevation,
- iconContentColor: Color = AlertDialogDefaults.IconContentColor,
- titleContentColor: Color = AlertDialogDefaults.TitleContentColor,
- textContentColor: Color = AlertDialogDefaults.TextContentColor,
+ iconContentColor: Color = AlertDialogDefaults.iconContentColor,
+ titleContentColor: Color = AlertDialogDefaults.titleContentColor,
+ textContentColor: Color = AlertDialogDefaults.textContentColor,
properties: DialogProperties = DialogProperties()
) {
Dialog(
@@ -123,22 +123,22 @@
*/
object AlertDialogDefaults {
/** The default shape for alert dialogs */
- val Shape: Shape @Composable get() = DialogTokens.ContainerShape.toShape()
+ val shape: Shape @Composable get() = DialogTokens.ContainerShape.toShape()
/** The default container color for alert dialogs */
- val ContainerColor: Color @Composable get() = DialogTokens.ContainerColor.toColor()
+ val containerColor: Color @Composable get() = DialogTokens.ContainerColor.toColor()
+
+ /** The default icon color for alert dialogs */
+ val iconContentColor: Color @Composable get() = DialogTokens.IconColor.toColor()
+
+ /** The default title color for alert dialogs */
+ val titleContentColor: Color @Composable get() = DialogTokens.SubheadColor.toColor()
+
+ /** The default text color for alert dialogs */
+ val textContentColor: Color @Composable get() = DialogTokens.SupportingTextColor.toColor()
/** The default tonal elevation for alert dialogs */
val TonalElevation: Dp = DialogTokens.ContainerElevation
-
- /** The default icon color for alert dialogs */
- val IconContentColor: Color @Composable get() = DialogTokens.IconColor.toColor()
-
- /** The default title color for alert dialogs */
- val TitleContentColor: Color @Composable get() = DialogTokens.SubheadColor.toColor()
-
- /** The default text color for alert dialogs */
- val TextContentColor: Color @Composable get() = DialogTokens.SupportingTextColor.toColor()
}
private val ButtonsMainAxisSpacing = 8.dp
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
index 55d4be4..efc3e9d 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
@@ -27,9 +27,7 @@
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
-import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.InteractionSource
-import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -41,7 +39,6 @@
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.BottomAppBarDefaults.FloatingActionButton
import androidx.compose.material3.tokens.BottomAppBarTokens
import androidx.compose.material3.tokens.FabSecondaryTokens
import androidx.compose.material3.tokens.TopAppBarLargeTokens
@@ -66,7 +63,6 @@
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
@@ -331,7 +327,7 @@
icons: @Composable RowScope.() -> Unit,
modifier: Modifier = Modifier,
floatingActionButton: @Composable (() -> Unit)? = null,
- containerColor: Color = BottomAppBarDefaults.ContainerColor,
+ containerColor: Color = BottomAppBarDefaults.containerColor,
contentColor: Color = contentColorFor(containerColor),
tonalElevation: Dp = BottomAppBarDefaults.ContainerElevation,
contentPadding: PaddingValues = BottomAppBarDefaults.ContentPadding,
@@ -386,7 +382,7 @@
@Composable
fun BottomAppBar(
modifier: Modifier = Modifier,
- containerColor: Color = BottomAppBarDefaults.ContainerColor,
+ containerColor: Color = BottomAppBarDefaults.containerColor,
contentColor: Color = contentColorFor(containerColor),
tonalElevation: Dp = BottomAppBarDefaults.ContainerElevation,
contentPadding: PaddingValues = BottomAppBarDefaults.ContentPadding,
@@ -863,7 +859,7 @@
object BottomAppBarDefaults {
/** Default color used for [BottomAppBar] container **/
- val ContainerColor: Color @Composable get() = BottomAppBarTokens.ContainerColor.toColor()
+ val containerColor: Color @Composable get() = BottomAppBarTokens.ContainerColor.toColor()
/** Default elevation used for [BottomAppBar] **/
val ContainerElevation: Dp = BottomAppBarTokens.ContainerElevation
@@ -883,9 +879,8 @@
* Creates a [FloatingActionButtonElevation] that represents the default elevation of a
* [FloatingActionButton] used for [BottomAppBar] in different states.
*/
- object FloatingActionButtonElevation :
- androidx.compose.material3.FloatingActionButtonElevation {
- val elevation = mutableStateOf(0.dp)
+ object BottomAppBarFabElevation : FloatingActionButtonElevation {
+ private val elevation = mutableStateOf(0.dp)
@Composable
override fun shadowElevation(interactionSource: InteractionSource) = elevation
@@ -895,61 +890,9 @@
}
/** The color of a [BottomAppBar]'s [FloatingActionButton] */
- val FloatingActionButtonContainerColor: Color
+ val bottomAppBarFabColor: Color
@Composable get() =
FabSecondaryTokens.ContainerColor.toColor()
-
- /** The shape of a [BottomAppBar]'s [FloatingActionButton] */
- val FloatingActionButtonShape: Shape
- @Composable get() =
- FabSecondaryTokens.ContainerShape.toShape()
-
- /**
- * The default [FloatingActionButton] for [BottomAppBar]
- *
- * A [BottomAppBar]'s FAB follows a secondary color style, as well as an elevation of zero.
- *
- * @sample androidx.compose.material3.samples.BottomAppBarWithFAB
- *
- * @param onClick callback invoked when this FAB is clicked
- * @param modifier [Modifier] to be applied to this FAB.
- * @param interactionSource the [MutableInteractionSource] representing the stream of
- * [Interaction]s for this FAB. You can create and pass in your own `remember`ed instance to
- * observe [Interaction]s and customize the appearance / behavior of this FAB in different
- * states.
- * @param shape defines the shape of this FAB's container and shadow (when using [elevation])
- * @param containerColor the color used for the background of this FAB. Use [Color.Transparent]
- * to have no color.
- * @param contentColor the preferred color for content inside this FAB. Defaults to either the
- * matching content color for [containerColor], or to the current [LocalContentColor] if
- * [containerColor] is not a color from the theme.
- * @param elevation [FloatingActionButtonElevation] used to resolve the elevation for this FAB
- * in different states. This controls the size of the shadow below the FAB. Additionally, when
- * the container color is [ColorScheme.surface], this controls the amount of primary color
- * applied as an overlay. See also: [Surface].
- * @param content the content of this FAB - this is typically an [Icon].
- */
- @Composable
- fun FloatingActionButton(
- onClick: () -> Unit,
- modifier: Modifier = Modifier,
- interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- shape: Shape = FloatingActionButtonShape,
- containerColor: Color = FloatingActionButtonContainerColor,
- contentColor: Color = contentColorFor(containerColor),
- elevation: androidx.compose.material3.FloatingActionButtonElevation =
- FloatingActionButtonElevation,
- content: @Composable () -> Unit,
- ) = androidx.compose.material3.FloatingActionButton(
- onClick = onClick,
- modifier = modifier,
- interactionSource = interactionSource,
- shape = shape,
- containerColor = containerColor,
- contentColor = contentColor,
- elevation = elevation,
- content = content
- )
}
// Padding minus IconButton's min touch target expansion
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Badge.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Badge.kt
index a19294f..8fe2b12b 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Badge.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Badge.kt
@@ -137,7 +137,7 @@
@Composable
fun Badge(
modifier: Modifier = Modifier,
- containerColor: Color = BadgeDefaults.ContainerColor,
+ containerColor: Color = BadgeDefaults.containerColor,
contentColor: Color = contentColorFor(containerColor),
content: @Composable (RowScope.() -> Unit)? = null,
) {
@@ -185,7 +185,7 @@
/** Default values used for [Badge] implementations. */
object BadgeDefaults {
/** Default container color for a badge. */
- val ContainerColor: Color @Composable get() = BadgeTokens.Color.toColor()
+ val containerColor: Color @Composable get() = BadgeTokens.Color.toColor()
}
/*@VisibleForTesting*/
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
index a0b2199..6eaa263 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
@@ -106,7 +106,7 @@
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
- shape: Shape = ButtonDefaults.Shape,
+ shape: Shape = ButtonDefaults.shape,
border: BorderStroke? = null,
colors: ButtonColors = ButtonDefaults.buttonColors(),
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
@@ -199,7 +199,7 @@
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(),
- shape: Shape = ButtonDefaults.ElevatedShape,
+ shape: Shape = ButtonDefaults.elevatedShape,
border: BorderStroke? = null,
colors: ButtonColors = ButtonDefaults.elevatedButtonColors(),
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
@@ -269,7 +269,7 @@
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ButtonElevation? = ButtonDefaults.filledTonalButtonElevation(),
- shape: Shape = ButtonDefaults.FilledTonalShape,
+ shape: Shape = ButtonDefaults.filledTonalShape,
border: BorderStroke? = null,
colors: ButtonColors = ButtonDefaults.filledTonalButtonColors(),
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
@@ -338,7 +338,7 @@
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ButtonElevation? = null,
- shape: Shape = ButtonDefaults.OutlinedShape,
+ shape: Shape = ButtonDefaults.outlinedShape,
border: BorderStroke? = ButtonDefaults.outlinedButtonBorder,
colors: ButtonColors = ButtonDefaults.outlinedButtonColors(),
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
@@ -409,7 +409,7 @@
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ButtonElevation? = null,
- shape: Shape = ButtonDefaults.TextShape,
+ shape: Shape = ButtonDefaults.textShape,
border: BorderStroke? = null,
colors: ButtonColors = ButtonDefaults.textButtonColors(),
contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding,
@@ -518,21 +518,20 @@
// TODO(b/201344013): Make sure this value stays up to date until replaced with a token.
val IconSpacing = 8.dp
- // Shape Defaults
/** Default shape for a button. */
- val Shape: Shape @Composable get() = FilledButtonTokens.ContainerShape.toShape()
+ val shape: Shape @Composable get() = FilledButtonTokens.ContainerShape.toShape()
/** Default shape for an elevated button. */
- val ElevatedShape: Shape @Composable get() = ElevatedButtonTokens.ContainerShape.toShape()
+ val elevatedShape: Shape @Composable get() = ElevatedButtonTokens.ContainerShape.toShape()
/** Default shape for a filled tonal button. */
- val FilledTonalShape: Shape @Composable get() = FilledTonalButtonTokens.ContainerShape.toShape()
+ val filledTonalShape: Shape @Composable get() = FilledTonalButtonTokens.ContainerShape.toShape()
/** Default shape for an outlined button. */
- val OutlinedShape: Shape @Composable get() = OutlinedButtonTokens.ContainerShape.toShape()
+ val outlinedShape: Shape @Composable get() = OutlinedButtonTokens.ContainerShape.toShape()
/** Default shape for a text button. */
- val TextShape: Shape @Composable get() = TextButtonTokens.ContainerShape.toShape()
+ val textShape: Shape @Composable get() = TextButtonTokens.ContainerShape.toShape()
/**
* Creates a [ButtonColors] that represents the default container and content colors used in a
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Card.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Card.kt
index 398117a..cedd229 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Card.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Card.kt
@@ -46,7 +46,6 @@
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.unit.Dp
-import kotlinx.coroutines.flow.collect
/**
* <a href="https://m3.material.io/components/cards/overview" class="external" target="_blank">Material Design filled card</a>.
@@ -77,7 +76,7 @@
@Composable
fun Card(
modifier: Modifier = Modifier,
- shape: Shape = CardDefaults.Shape,
+ shape: Shape = CardDefaults.shape,
border: BorderStroke? = null,
elevation: CardElevation = CardDefaults.cardElevation(),
colors: CardColors = CardDefaults.cardColors(),
@@ -135,7 +134,7 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- shape: Shape = CardDefaults.Shape,
+ shape: Shape = CardDefaults.shape,
border: BorderStroke? = null,
elevation: CardElevation = CardDefaults.cardElevation(),
colors: CardColors = CardDefaults.cardColors(),
@@ -184,7 +183,7 @@
@Composable
fun ElevatedCard(
modifier: Modifier = Modifier,
- shape: Shape = CardDefaults.ElevatedShape,
+ shape: Shape = CardDefaults.elevatedShape,
elevation: CardElevation = CardDefaults.elevatedCardElevation(),
colors: CardColors = CardDefaults.elevatedCardColors(),
content: @Composable ColumnScope.() -> Unit
@@ -234,7 +233,7 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- shape: Shape = CardDefaults.ElevatedShape,
+ shape: Shape = CardDefaults.elevatedShape,
elevation: CardElevation = CardDefaults.elevatedCardElevation(),
colors: CardColors = CardDefaults.elevatedCardColors(),
content: @Composable ColumnScope.() -> Unit
@@ -278,7 +277,7 @@
@Composable
fun OutlinedCard(
modifier: Modifier = Modifier,
- shape: Shape = CardDefaults.OutlinedShape,
+ shape: Shape = CardDefaults.outlinedShape,
border: BorderStroke = CardDefaults.outlinedCardBorder(),
elevation: CardElevation = CardDefaults.outlinedCardElevation(),
colors: CardColors = CardDefaults.outlinedCardColors(),
@@ -330,7 +329,7 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- shape: Shape = CardDefaults.OutlinedShape,
+ shape: Shape = CardDefaults.outlinedShape,
border: BorderStroke = CardDefaults.outlinedCardBorder(enabled),
elevation: CardElevation = CardDefaults.outlinedCardElevation(),
colors: CardColors = CardDefaults.outlinedCardColors(),
@@ -417,15 +416,15 @@
* Contains the default values used by all card types.
*/
object CardDefaults {
- // Shape Defaults
+ // shape Defaults
/** Default shape for a card. */
- val Shape: Shape @Composable get() = FilledCardTokens.ContainerShape.toShape()
+ val shape: Shape @Composable get() = FilledCardTokens.ContainerShape.toShape()
/** Default shape for an elevated card. */
- val ElevatedShape: Shape @Composable get() = ElevatedCardTokens.ContainerShape.toShape()
+ val elevatedShape: Shape @Composable get() = ElevatedCardTokens.ContainerShape.toShape()
/** Default shape for an outlined card. */
- val OutlinedShape: Shape @Composable get() = OutlinedCardTokens.ContainerShape.toShape()
+ val outlinedShape: Shape @Composable get() = OutlinedCardTokens.ContainerShape.toShape()
/**
* Creates a [CardElevation] that will animate between the provided values according to the
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Checkbox.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Checkbox.kt
index fed31c8..d8483ff 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Checkbox.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Checkbox.kt
@@ -64,8 +64,12 @@
*
* 
*
+ * Simple Checkbox sample:
* @sample androidx.compose.material3.samples.CheckboxSample
*
+ * Combined Checkbox with Text sample:
+ * @sample androidx.compose.material3.samples.CheckboxWithTextSample
+ *
* @see [TriStateCheckbox] if you require support for an indeterminate state.
*
* @param checked whether this checkbox is checked or unchecked
@@ -313,7 +317,10 @@
val checkColor = colors.checkmarkColor(value)
val boxColor = colors.boxColor(enabled, value)
val borderColor = colors.borderColor(enabled, value)
- Canvas(modifier.wrapContentSize(Alignment.Center).requiredSize(CheckboxSize)) {
+ Canvas(
+ modifier
+ .wrapContentSize(Alignment.Center)
+ .requiredSize(CheckboxSize)) {
val strokeWidthPx = floor(StrokeWidth.toPx())
drawBox(
boxColor = boxColor.value,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
index 8f46b4a..71ae1a7 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
@@ -112,7 +112,7 @@
trailingIcon: @Composable (() -> Unit)? = null,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ChipElevation? = AssistChipDefaults.assistChipElevation(),
- shape: Shape = AssistChipDefaults.Shape,
+ shape: Shape = AssistChipDefaults.shape,
border: ChipBorder? = AssistChipDefaults.assistChipBorder(),
colors: ChipColors = AssistChipDefaults.assistChipColors()
) = Chip(
@@ -184,7 +184,7 @@
trailingIcon: @Composable (() -> Unit)? = null,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ChipElevation? = AssistChipDefaults.elevatedAssistChipElevation(),
- shape: Shape = AssistChipDefaults.Shape,
+ shape: Shape = AssistChipDefaults.shape,
border: ChipBorder? = null,
colors: ChipColors = AssistChipDefaults.elevatedAssistChipColors()
) = Chip(
@@ -220,8 +220,8 @@
* This filter chip is applied with a flat style. If you want an elevated style, use the
* [ElevatedFilterChip].
*
- * Tapping on a filter chip selects it, and in case a [selectedIcon] is provided (e.g. a checkmark),
- * it's appended to the starting edge of the chip's label, drawn instead of any given [leadingIcon].
+ * Tapping on a filter chip toggles its selection state. A selection state [leadingIcon] can be
+ * provided (e.g. a checkmark) to be appended at the starting edge of the chip's label.
*
* Example of a flat FilterChip with a trailing icon:
* @sample androidx.compose.material3.samples.FilterChipSample
@@ -236,9 +236,9 @@
* @param enabled controls the enabled state of this chip. When `false`, this component will not
* respond to user input, and it will appear visually disabled and disabled to accessibility
* services.
- * @param leadingIcon optional icon at the start of the chip, preceding the [label] text
- * @param selectedIcon optional icon at the start of the chip, preceding the [label] text, which is
- * displayed when the chip is selected, instead of any given [leadingIcon]
+ * @param leadingIcon optional icon at the start of the chip, preceding the [label] text. When
+ * [selected] is true, this icon may visually indicate that the chip is selected (for example, via a
+ * checkmark icon).
* @param trailingIcon optional icon at the end of the chip
* @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
* for this chip. You can create and pass in your own `remember`ed instance to observe
@@ -263,11 +263,10 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
leadingIcon: @Composable (() -> Unit)? = null,
- selectedIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: SelectableChipElevation? = FilterChipDefaults.filterChipElevation(),
- shape: Shape = FilterChipDefaults.Shape,
+ shape: Shape = FilterChipDefaults.shape,
border: SelectableChipBorder? = FilterChipDefaults.filterChipBorder(),
colors: SelectableChipColors = FilterChipDefaults.filterChipColors()
) = SelectableChip(
@@ -277,7 +276,7 @@
enabled = enabled,
label = label,
labelTextStyle = MaterialTheme.typography.fromToken(FilterChipTokens.LabelTextFont),
- leadingIcon = if (selected) selectedIcon else leadingIcon,
+ leadingIcon = leadingIcon,
avatar = null,
trailingIcon = trailingIcon,
elevation = elevation,
@@ -304,8 +303,8 @@
* This filter chip is applied with an elevated style. If you want a flat style, use the
* [FilterChip].
*
- * Tapping on a filter chip selects it, and in case a [selectedIcon] is provided (e.g. a checkmark),
- * it's appended to the starting edge of the chip's label, drawn instead of any given [leadingIcon].
+ * Tapping on a filter chip toggles its selection state. A selection state [leadingIcon] can be
+ * provided (e.g. a checkmark) to be appended at the starting edge of the chip's label.
*
* Example of an elevated FilterChip with a trailing icon:
* @sample androidx.compose.material3.samples.ElevatedFilterChipSample
@@ -317,9 +316,9 @@
* @param enabled controls the enabled state of this chip. When `false`, this component will not
* respond to user input, and it will appear visually disabled and disabled to accessibility
* services.
- * @param leadingIcon optional icon at the start of the chip, preceding the [label] text
- * @param selectedIcon optional icon at the start of the chip, preceding the [label] text, which is
- * displayed when the chip is selected, instead of any given [leadingIcon]
+ * @param leadingIcon optional icon at the start of the chip, preceding the [label] text. When
+ * [selected] is true, this icon may visually indicate that the chip is selected (for example, via a
+ * checkmark icon).
* @param trailingIcon optional icon at the end of the chip
* @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
* for this chip. You can create and pass in your own `remember`ed instance to observe
@@ -344,11 +343,10 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
leadingIcon: @Composable (() -> Unit)? = null,
- selectedIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: SelectableChipElevation? = FilterChipDefaults.elevatedFilterChipElevation(),
- shape: Shape = FilterChipDefaults.Shape,
+ shape: Shape = FilterChipDefaults.shape,
border: SelectableChipBorder? = null,
colors: SelectableChipColors = FilterChipDefaults.elevatedFilterChipColors()
) = SelectableChip(
@@ -358,7 +356,7 @@
enabled = enabled,
label = label,
labelTextStyle = MaterialTheme.typography.fromToken(FilterChipTokens.LabelTextFont),
- leadingIcon = if (selected) selectedIcon else leadingIcon,
+ leadingIcon = leadingIcon,
avatar = null,
trailingIcon = trailingIcon,
elevation = elevation,
@@ -433,7 +431,7 @@
trailingIcon: @Composable (() -> Unit)? = null,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: SelectableChipElevation? = InputChipDefaults.inputChipElevation(),
- shape: Shape = InputChipDefaults.Shape,
+ shape: Shape = InputChipDefaults.shape,
border: SelectableChipBorder? = InputChipDefaults.inputChipBorder(),
colors: SelectableChipColors = InputChipDefaults.inputChipColors()
) {
@@ -529,7 +527,7 @@
icon: @Composable (() -> Unit)? = null,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ChipElevation? = SuggestionChipDefaults.suggestionChipElevation(),
- shape: Shape = SuggestionChipDefaults.Shape,
+ shape: Shape = SuggestionChipDefaults.shape,
border: ChipBorder? = SuggestionChipDefaults.suggestionChipBorder(),
colors: ChipColors = SuggestionChipDefaults.suggestionChipColors()
) = Chip(
@@ -598,7 +596,7 @@
icon: @Composable (() -> Unit)? = null,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ChipElevation? = SuggestionChipDefaults.elevatedSuggestionChipElevation(),
- shape: Shape = SuggestionChipDefaults.Shape,
+ shape: Shape = SuggestionChipDefaults.shape,
border: ChipBorder? = null,
colors: ChipColors = SuggestionChipDefaults.elevatedSuggestionChipColors()
) = Chip(
@@ -829,9 +827,6 @@
*/
@ExperimentalMaterial3Api
object AssistChipDefaults {
- /** Default shape of an assist chip. */
- val Shape: Shape @Composable get() = AssistChipTokens.ContainerShape.toShape()
-
/**
* The height applied for an assist chip.
* Note that you can override it by applying Modifier.height directly on a chip.
@@ -1024,6 +1019,9 @@
)
}
}
+
+ /** Default shape of an assist chip. */
+ val shape: Shape @Composable get() = AssistChipTokens.ContainerShape.toShape()
}
/**
@@ -1031,9 +1029,6 @@
*/
@ExperimentalMaterial3Api
object FilterChipDefaults {
- /** Default shape of a filter chip. */
- val Shape: Shape @Composable get() = FilterChipTokens.ContainerShape.toShape()
-
/**
* The height applied for a filter chip.
* Note that you can override it by applying Modifier.height directly on a chip.
@@ -1269,6 +1264,9 @@
)
}
}
+
+ /** Default shape of a filter chip. */
+ val shape: Shape @Composable get() = FilterChipTokens.ContainerShape.toShape()
}
/**
@@ -1276,9 +1274,6 @@
*/
@ExperimentalMaterial3Api
object InputChipDefaults {
- /** Default shape of an input chip. */
- val Shape: Shape @Composable get() = InputChipTokens.ContainerShape.toShape()
-
/**
* The height applied for an input chip.
* Note that you can override it by applying Modifier.height directly on a chip.
@@ -1430,6 +1425,9 @@
)
}
}
+
+ /** Default shape of an input chip. */
+ val shape: Shape @Composable get() = InputChipTokens.ContainerShape.toShape()
}
/**
@@ -1437,9 +1435,6 @@
*/
@ExperimentalMaterial3Api
object SuggestionChipDefaults {
- /** Default shape of a suggestion chip. */
- val Shape: Shape @Composable get() = SuggestionChipTokens.ContainerShape.toShape()
-
/**
* The height applied for a suggestion chip.
* Note that you can override it by applying Modifier.height directly on a chip.
@@ -1626,6 +1621,9 @@
)
}
}
+
+ /** Default shape of a suggestion chip. */
+ val shape: Shape @Composable get() = SuggestionChipTokens.ContainerShape.toShape()
}
@ExperimentalMaterial3Api
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt
index 4c9c7a6..f43dc91 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt
@@ -92,6 +92,9 @@
* top of [errorContainer].
* @property outline Subtle color used for boundaries. Outline color role adds contrast for
* accessibility purposes.
+ * @property outlineVariant Utility color used for boundaries for decorative elements when strong
+ * contrast is not required.
+ * @property scrim Color of a scrim that obscures content.
*/
@Stable
class ColorScheme(
@@ -122,6 +125,8 @@
errorContainer: Color,
onErrorContainer: Color,
outline: Color,
+ outlineVariant: Color,
+ scrim: Color,
) {
var primary by mutableStateOf(primary, structuralEqualityPolicy())
internal set
@@ -177,6 +182,10 @@
internal set
var outline by mutableStateOf(outline, structuralEqualityPolicy())
internal set
+ var outlineVariant by mutableStateOf(outlineVariant, structuralEqualityPolicy())
+ internal set
+ var scrim by mutableStateOf(scrim, structuralEqualityPolicy())
+ internal set
/** Returns a copy of this ColorScheme, optionally overriding some of the values. */
fun copy(
@@ -207,6 +216,8 @@
errorContainer: Color = this.errorContainer,
onErrorContainer: Color = this.onErrorContainer,
outline: Color = this.outline,
+ outlineVariant: Color = this.outlineVariant,
+ scrim: Color = this.scrim,
): ColorScheme =
ColorScheme(
primary = primary,
@@ -236,6 +247,8 @@
errorContainer = errorContainer,
onErrorContainer = onErrorContainer,
outline = outline,
+ outlineVariant = outlineVariant,
+ scrim = scrim,
)
override fun toString(): String {
@@ -267,6 +280,8 @@
"errorContainer=$errorContainer" +
"onErrorContainer=$onErrorContainer" +
"outline=$outline" +
+ "outlineVariant=$outlineVariant" +
+ "scrim=$scrim" +
")"
}
}
@@ -302,6 +317,8 @@
errorContainer: Color = ColorLightTokens.ErrorContainer,
onErrorContainer: Color = ColorLightTokens.OnErrorContainer,
outline: Color = ColorLightTokens.Outline,
+ outlineVariant: Color = ColorLightTokens.OutlineVariant,
+ scrim: Color = ColorLightTokens.Scrim,
): ColorScheme =
ColorScheme(
primary = primary,
@@ -331,6 +348,8 @@
errorContainer = errorContainer,
onErrorContainer = onErrorContainer,
outline = outline,
+ outlineVariant = outlineVariant,
+ scrim = scrim,
)
/**
@@ -364,6 +383,8 @@
errorContainer: Color = ColorDarkTokens.ErrorContainer,
onErrorContainer: Color = ColorDarkTokens.OnErrorContainer,
outline: Color = ColorDarkTokens.Outline,
+ outlineVariant: Color = ColorDarkTokens.OutlineVariant,
+ scrim: Color = ColorDarkTokens.Scrim,
): ColorScheme =
ColorScheme(
primary = primary,
@@ -393,6 +414,8 @@
errorContainer = errorContainer,
onErrorContainer = onErrorContainer,
outline = outline,
+ outlineVariant = outlineVariant,
+ scrim = scrim,
)
/**
@@ -525,6 +548,8 @@
errorContainer = other.errorContainer
onErrorContainer = other.onErrorContainer
outline = other.outline
+ outlineVariant = other.outlineVariant
+ scrim = other.scrim
}
/**
@@ -553,8 +578,10 @@
ColorSchemeKeyTokens.OnTertiary -> onTertiary
ColorSchemeKeyTokens.OnTertiaryContainer -> onTertiaryContainer
ColorSchemeKeyTokens.Outline -> outline
+ ColorSchemeKeyTokens.OutlineVariant -> outlineVariant
ColorSchemeKeyTokens.Primary -> primary
ColorSchemeKeyTokens.PrimaryContainer -> primaryContainer
+ ColorSchemeKeyTokens.Scrim -> scrim
ColorSchemeKeyTokens.Secondary -> secondary
ColorSchemeKeyTokens.SecondaryContainer -> secondaryContainer
ColorSchemeKeyTokens.Surface -> surface
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Divider.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Divider.kt
index a0c39cc..5dd5a33 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Divider.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Divider.kt
@@ -46,7 +46,7 @@
@Composable
fun Divider(
modifier: Modifier = Modifier,
- color: Color = DividerDefaults.Color,
+ color: Color = DividerDefaults.color,
thickness: Dp = DividerDefaults.Thickness,
startIndent: Dp = 0.dp
) {
@@ -70,9 +70,9 @@
/** Default values for [Divider] */
object DividerDefaults {
- /** Default color of a divider. */
- val Color: Color @Composable get() = DividerTokens.Color.toColor()
-
/** Default thickness of a divider. */
val Thickness: Dp = DividerTokens.Thickness
+
+ /** Default color of a divider. */
+ val color: Color @Composable get() = DividerTokens.Color.toColor()
}
\ No newline at end of file
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt
index af1cfa4..9ea80f4 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt
@@ -94,8 +94,8 @@
onClick: () -> Unit,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- shape: Shape = FloatingActionButtonDefaults.Shape,
- containerColor: Color = FloatingActionButtonDefaults.ContainerColor,
+ shape: Shape = FloatingActionButtonDefaults.shape,
+ containerColor: Color = FloatingActionButtonDefaults.containerColor,
contentColor: Color = contentColorFor(containerColor),
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
content: @Composable () -> Unit,
@@ -162,8 +162,8 @@
onClick: () -> Unit,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- shape: Shape = FloatingActionButtonDefaults.SmallShape,
- containerColor: Color = FloatingActionButtonDefaults.ContainerColor,
+ shape: Shape = FloatingActionButtonDefaults.smallShape,
+ containerColor: Color = FloatingActionButtonDefaults.containerColor,
contentColor: Color = contentColorFor(containerColor),
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
content: @Composable () -> Unit,
@@ -214,8 +214,8 @@
onClick: () -> Unit,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- shape: Shape = FloatingActionButtonDefaults.LargeShape,
- containerColor: Color = FloatingActionButtonDefaults.ContainerColor,
+ shape: Shape = FloatingActionButtonDefaults.largeShape,
+ containerColor: Color = FloatingActionButtonDefaults.containerColor,
contentColor: Color = contentColorFor(containerColor),
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
content: @Composable () -> Unit,
@@ -269,8 +269,8 @@
onClick: () -> Unit,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- shape: Shape = FloatingActionButtonDefaults.ExtendedFabShape,
- containerColor: Color = FloatingActionButtonDefaults.ContainerColor,
+ shape: Shape = FloatingActionButtonDefaults.extendedFabShape,
+ containerColor: Color = FloatingActionButtonDefaults.containerColor,
contentColor: Color = contentColorFor(containerColor),
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
content: @Composable RowScope.() -> Unit,
@@ -336,8 +336,8 @@
modifier: Modifier = Modifier,
expanded: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- shape: Shape = FloatingActionButtonDefaults.ExtendedFabShape,
- containerColor: Color = FloatingActionButtonDefaults.ContainerColor,
+ shape: Shape = FloatingActionButtonDefaults.extendedFabShape,
+ containerColor: Color = FloatingActionButtonDefaults.containerColor,
contentColor: Color = contentColorFor(containerColor),
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
) {
@@ -419,27 +419,27 @@
* Contains the default values used by [FloatingActionButton]
*/
object FloatingActionButtonDefaults {
- /** Default shape for a floating action button. */
- val Shape: Shape @Composable get() = FabPrimaryTokens.ContainerShape.toShape()
-
- /** Default shape for a small floating action button. */
- val SmallShape: Shape @Composable get() = FabPrimarySmallTokens.ContainerShape.toShape()
-
- /** Default shape for a large floating action button. */
- val LargeShape: Shape @Composable get() = FabPrimaryLargeTokens.ContainerShape.toShape()
-
- /** Default shape for an extended floating action button. */
- val ExtendedFabShape: Shape @Composable get() =
- ExtendedFabPrimaryTokens.ContainerShape.toShape()
-
- /** Default container color for a floating action button. */
- val ContainerColor: Color @Composable get() = FabPrimaryTokens.ContainerColor.toColor()
-
/**
* The recommended size of the icon inside a [LargeFloatingActionButton].
*/
val LargeIconSize = FabPrimaryLargeTokens.IconSize
+ /** Default shape for a floating action button. */
+ val shape: Shape @Composable get() = FabPrimaryTokens.ContainerShape.toShape()
+
+ /** Default shape for a small floating action button. */
+ val smallShape: Shape @Composable get() = FabPrimarySmallTokens.ContainerShape.toShape()
+
+ /** Default shape for a large floating action button. */
+ val largeShape: Shape @Composable get() = FabPrimaryLargeTokens.ContainerShape.toShape()
+
+ /** Default shape for an extended floating action button. */
+ val extendedFabShape: Shape @Composable get() =
+ ExtendedFabPrimaryTokens.ContainerShape.toShape()
+
+ /** Default container color for a floating action button. */
+ val containerColor: Color @Composable get() = FabPrimaryTokens.ContainerColor.toColor()
+
/**
* Creates a [FloatingActionButtonElevation] that represents the elevation of a
* [FloatingActionButton] in different states. For use cases in which a less prominent
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
index fecaaaa..9f39073 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
@@ -200,7 +200,7 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- shape: Shape = IconButtonDefaults.FilledShape,
+ shape: Shape = IconButtonDefaults.filledShape,
colors: IconButtonColors = IconButtonDefaults.filledIconButtonColors(),
content: @Composable () -> Unit
) = Surface(
@@ -261,7 +261,7 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- shape: Shape = IconButtonDefaults.FilledShape,
+ shape: Shape = IconButtonDefaults.filledShape,
colors: IconButtonColors = IconButtonDefaults.filledTonalIconButtonColors(),
content: @Composable () -> Unit
) = Surface(
@@ -319,7 +319,7 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- shape: Shape = IconButtonDefaults.FilledShape,
+ shape: Shape = IconButtonDefaults.filledShape,
colors: IconToggleButtonColors = IconButtonDefaults.filledIconToggleButtonColors(),
content: @Composable () -> Unit
) = Surface(
@@ -383,7 +383,7 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- shape: Shape = IconButtonDefaults.FilledShape,
+ shape: Shape = IconButtonDefaults.filledShape,
colors: IconToggleButtonColors = IconButtonDefaults.filledTonalIconToggleButtonColors(),
content: @Composable () -> Unit
) = Surface(
@@ -448,7 +448,7 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- shape: Shape = IconButtonDefaults.OutlinedShape,
+ shape: Shape = IconButtonDefaults.outlinedShape,
border: BorderStroke? = IconButtonDefaults.outlinedIconButtonBorder(enabled),
colors: IconButtonColors = IconButtonDefaults.outlinedIconButtonColors(),
content: @Composable () -> Unit
@@ -510,7 +510,7 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- shape: Shape = IconButtonDefaults.OutlinedShape,
+ shape: Shape = IconButtonDefaults.outlinedShape,
border: BorderStroke? = IconButtonDefaults.outlinedIconToggleButtonBorder(enabled, checked),
colors: IconToggleButtonColors = IconButtonDefaults.outlinedIconToggleButtonColors(),
content: @Composable () -> Unit
@@ -597,10 +597,10 @@
*/
object IconButtonDefaults {
/** Default shape for a filled icon button. */
- val FilledShape: Shape @Composable get() = FilledIconButtonTokens.ContainerShape.toShape()
+ val filledShape: Shape @Composable get() = FilledIconButtonTokens.ContainerShape.toShape()
/** Default shape for an outlined icon button. */
- val OutlinedShape: Shape @Composable get() =
+ val outlinedShape: Shape @Composable get() =
OutlinedIconButtonTokens.ContainerShape.toShape()
/**
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ListItem.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ListItem.kt
index 071b52b..d07e4eb 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ListItem.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ListItem.kt
@@ -279,9 +279,9 @@
@ExperimentalMaterial3Api
private fun ListItem(
modifier: Modifier = Modifier,
- shape: Shape = ListItemDefaults.Shape,
- containerColor: Color = ListItemDefaults.ContainerColor,
- contentColor: Color = ListItemDefaults.ContentColor,
+ shape: Shape = ListItemDefaults.shape,
+ containerColor: Color = ListItemDefaults.containerColor,
+ contentColor: Color = ListItemDefaults.contentColor,
tonalElevation: Dp = ListItemDefaults.Elevation,
shadowElevation: Dp = ListItemDefaults.Elevation,
content: @Composable RowScope.() -> Unit,
@@ -360,17 +360,17 @@
*/
@ExperimentalMaterial3Api
object ListItemDefaults {
- /** The default shape of a list item */
- val Shape: Shape @Composable get() = ListTokens.ListItemContainerShape.toShape()
-
/** The default elevation of a list item */
val Elevation: Dp = ListTokens.ListItemContainerElevation
+ /** The default shape of a list item */
+ val shape: Shape @Composable get() = ListTokens.ListItemContainerShape.toShape()
+
/** The container color of a list item */
- val ContainerColor: Color @Composable get() = ListTokens.ListItemContainerColor.toColor()
+ val containerColor: Color @Composable get() = ListTokens.ListItemContainerColor.toColor()
/** The content color of a list item */
- val ContentColor: Color @Composable get() = ListTokens.ListItemLabelTextColor.toColor()
+ val contentColor: Color @Composable get() = ListTokens.ListItemLabelTextColor.toColor()
/**
* Creates a [ListItemColors] that represents the default container and content colors used in a
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt
index 42541a1..72f36d4 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt
@@ -92,7 +92,7 @@
@Composable
fun NavigationBar(
modifier: Modifier = Modifier,
- containerColor: Color = NavigationBarDefaults.ContainerColor,
+ containerColor: Color = NavigationBarDefaults.containerColor,
contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor),
tonalElevation: Dp = NavigationBarDefaults.Elevation,
content: @Composable RowScope.() -> Unit
@@ -242,11 +242,11 @@
/** Defaults used in [NavigationBar]. */
object NavigationBarDefaults {
- /** Default color for a navigation bar. */
- val ContainerColor: Color @Composable get() = NavigationBarTokens.ContainerColor.toColor()
-
/** Default elevation for a navigation bar. */
val Elevation: Dp = NavigationBarTokens.ContainerElevation
+
+ /** Default color for a navigation bar. */
+ val containerColor: Color @Composable get() = NavigationBarTokens.ContainerColor.toColor()
}
/** Defaults used in [NavigationBarItem]. */
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 67fd8a8..a4ff31d 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
@@ -37,7 +37,6 @@
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.width
import androidx.compose.material3.tokens.NavigationDrawerTokens
-import androidx.compose.material3.tokens.PaletteTokens
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Stable
@@ -262,11 +261,11 @@
modifier: Modifier = Modifier,
drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
gesturesEnabled: Boolean = true,
- drawerShape: Shape = DrawerDefaults.Shape,
+ drawerShape: Shape = DrawerDefaults.shape,
drawerTonalElevation: Dp = DrawerDefaults.ModalDrawerElevation,
- drawerContainerColor: Color = DrawerDefaults.ContainerColor,
+ drawerContainerColor: Color = DrawerDefaults.containerColor,
drawerContentColor: Color = contentColorFor(drawerContainerColor),
- scrimColor: Color = DrawerDefaults.ScrimColor,
+ scrimColor: Color = DrawerDefaults.scrimColor,
content: @Composable () -> Unit
) {
val scope = rememberCoroutineScope()
@@ -355,11 +354,11 @@
modifier: Modifier = Modifier,
drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
gesturesEnabled: Boolean = true,
- drawerShape: Shape = DrawerDefaults.Shape,
+ drawerShape: Shape = DrawerDefaults.shape,
drawerTonalElevation: Dp = DrawerDefaults.ModalDrawerElevation,
- drawerContainerColor: Color = DrawerDefaults.ContainerColor,
+ drawerContainerColor: Color = DrawerDefaults.containerColor,
drawerContentColor: Color = contentColorFor(drawerContainerColor),
- scrimColor: Color = DrawerDefaults.ScrimColor,
+ scrimColor: Color = DrawerDefaults.scrimColor,
content: @Composable () -> Unit
) {
ModalNavigationDrawer(
@@ -544,9 +543,6 @@
*/
@ExperimentalMaterial3Api
object DrawerDefaults {
- /** Default shape for a navigation drawer. */
- val Shape: Shape @Composable get() = NavigationDrawerTokens.ContainerShape.toShape()
-
/**
* Default Elevation for drawer container in the [ModalNavigationDrawer] as specified in the
* Material specification.
@@ -565,13 +561,15 @@
*/
val DismissibleDrawerElevation = NavigationDrawerTokens.StandardContainerElevation
+ /** Default shape for a navigation drawer. */
+ val shape: Shape @Composable get() = NavigationDrawerTokens.ContainerShape.toShape()
+
/** Default color of the scrim that obscures content when the drawer is open */
- val ScrimColor: Color
- @Composable
- get() = PaletteTokens.NeutralVariant0.copy(alpha = NavigationDrawerTokens.ScrimOpacity)
+ val scrimColor: Color
+ @Composable get() = MaterialTheme.colorScheme.scrim.copy(.32f)
/** Default container color for a navigation drawer */
- val ContainerColor: Color @Composable get() = NavigationDrawerTokens.ContainerColor.toColor()
+ val containerColor: Color @Composable get() = NavigationDrawerTokens.ContainerColor.toColor()
}
/**
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationRail.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationRail.kt
index 586d240..74cb1fc 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationRail.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationRail.kt
@@ -31,8 +31,7 @@
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.material.ripple.rememberRipple
@@ -58,6 +57,7 @@
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.constrainWidth
import androidx.compose.ui.unit.dp
import kotlin.math.roundToInt
@@ -105,8 +105,9 @@
modifier = modifier,
) {
Column(
- Modifier.fillMaxHeight()
- .width(NavigationRailTokens.ContainerWidth)
+ Modifier
+ .fillMaxHeight()
+ .widthIn(min = NavigationRailTokens.ContainerWidth)
.padding(vertical = NavigationRailVerticalPadding)
.selectableGroup(),
horizontalAlignment = Alignment.CenterHorizontally,
@@ -185,7 +186,8 @@
interactionSource = interactionSource,
indication = null,
)
- .size(width = NavigationRailItemWidth, height = NavigationRailItemHeight),
+ .height(height = NavigationRailItemHeight)
+ .widthIn(min = NavigationRailItemWidth),
contentAlignment = Alignment.Center
) {
val animationProgress: Float by animateFloatAsState(
@@ -216,14 +218,16 @@
// ripple, which is why they are separate composables
val indicatorRipple = @Composable {
Box(
- Modifier.layoutId(IndicatorRippleLayoutIdTag)
+ Modifier
+ .layoutId(IndicatorRippleLayoutIdTag)
.clip(indicatorShape)
.indication(offsetInteractionSource, rememberRipple())
)
}
val indicator = @Composable {
Box(
- Modifier.layoutId(IndicatorLayoutIdTag)
+ Modifier
+ .layoutId(IndicatorLayoutIdTag)
.background(
color = colors.indicatorColor.copy(alpha = animationProgress),
shape = indicatorShape
@@ -370,7 +374,8 @@
if (label != null) {
Box(
- Modifier.layoutId(LabelLayoutIdTag)
+ Modifier
+ .layoutId(LabelLayoutIdTag)
.alpha(if (alwaysShowLabel) 1f else animationProgress)
) { label() }
}
@@ -442,7 +447,13 @@
indicatorPlaceable: Placeable?,
constraints: Constraints,
): MeasureResult {
- val width = constraints.maxWidth
+ val width = constraints.constrainWidth(
+ maxOf(
+ iconPlaceable.width,
+ indicatorRipplePlaceable.width,
+ indicatorPlaceable?.width ?: 0
+ )
+ )
val height = constraints.maxHeight
val iconX = (width - iconPlaceable.width) / 2
@@ -519,8 +530,13 @@
// The interpolated fraction of iconDistance that all placeables need to move based on
// animationProgress, since the icon is higher in the selected state.
val offset = (iconDistance * (1 - animationProgress)).roundToInt()
-
- val width = constraints.maxWidth
+ val width = constraints.constrainWidth(
+ maxOf(
+ iconPlaceable.width,
+ labelPlaceable.width,
+ indicatorPlaceable?.width ?: 0
+ )
+ )
val labelX = (width - labelPlaceable.width) / 2
val iconX = (width - iconPlaceable.width) / 2
val rippleX = (width - indicatorRipplePlaceable.width) / 2
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
index 5f4f735..66c86c1 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
@@ -51,6 +51,7 @@
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
@@ -147,7 +148,7 @@
singleLine: Boolean = false,
maxLines: Int = Int.MAX_VALUE,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- shape: Shape = TextFieldDefaults.OutlinedShape,
+ shape: Shape = TextFieldDefaults.outlinedShape,
colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
) {
// If color is not provided via the text style, use content color as a default
@@ -161,7 +162,11 @@
BasicTextField(
value = value,
modifier = if (label != null) {
- modifier.padding(top = OutlinedTextFieldTopPadding)
+ modifier
+ // Merge semantics at the beginning of the modifier chain to ensure padding is
+ // considered part of the text field.
+ .semantics(mergeDescendants = true) {}
+ .padding(top = OutlinedTextFieldTopPadding)
} else {
modifier
}
@@ -292,7 +297,7 @@
singleLine: Boolean = false,
maxLines: Int = Int.MAX_VALUE,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- shape: Shape = TextFieldDefaults.OutlinedShape,
+ shape: Shape = TextFieldDefaults.outlinedShape,
colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
) {
// If color is not provided via the text style, use content color as a default
@@ -306,7 +311,11 @@
BasicTextField(
value = value,
modifier = if (label != null) {
- modifier.padding(top = OutlinedTextFieldTopPadding)
+ modifier
+ // Merge semantics at the beginning of the modifier chain to ensure padding is
+ // considered part of the text field.
+ .semantics(mergeDescendants = true) {}
+ .padding(top = OutlinedTextFieldTopPadding)
} else {
modifier
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt
index 6cc69449..945b55c 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt
@@ -73,8 +73,8 @@
fun LinearProgressIndicator(
progress: Float,
modifier: Modifier = Modifier,
- color: Color = ProgressIndicatorDefaults.LinearColor,
- trackColor: Color = ProgressIndicatorDefaults.LinearTrackColor,
+ color: Color = ProgressIndicatorDefaults.linearColor,
+ trackColor: Color = ProgressIndicatorDefaults.linearTrackColor,
) {
Canvas(
modifier
@@ -105,8 +105,8 @@
@Composable
fun LinearProgressIndicator(
modifier: Modifier = Modifier,
- color: Color = ProgressIndicatorDefaults.LinearColor,
- trackColor: Color = ProgressIndicatorDefaults.LinearTrackColor,
+ color: Color = ProgressIndicatorDefaults.linearColor,
+ trackColor: Color = ProgressIndicatorDefaults.linearTrackColor,
) {
val infiniteTransition = rememberInfiniteTransition()
// Fractional position of the 'head' and 'tail' of the two lines drawn, i.e. if the head is 0.8
@@ -230,7 +230,7 @@
fun CircularProgressIndicator(
progress: Float,
modifier: Modifier = Modifier,
- color: Color = ProgressIndicatorDefaults.CircularColor,
+ color: Color = ProgressIndicatorDefaults.circularColor,
strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth
) {
val stroke = with(LocalDensity.current) {
@@ -265,7 +265,7 @@
@Composable
fun CircularProgressIndicator(
modifier: Modifier = Modifier,
- color: Color = ProgressIndicatorDefaults.CircularColor,
+ color: Color = ProgressIndicatorDefaults.circularColor,
strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth
) {
val stroke = with(LocalDensity.current) {
@@ -398,15 +398,15 @@
*/
object ProgressIndicatorDefaults {
/** Default color for a linear progress indicator. */
- val LinearColor: Color @Composable get() =
+ val linearColor: Color @Composable get() =
LinearProgressIndicatorTokens.ActiveIndicatorColor.toColor()
/** Default color for a circular progress indicator. */
- val CircularColor: Color @Composable get() =
+ val circularColor: Color @Composable get() =
CircularProgressIndicatorTokens.ActiveIndicatorColor.toColor()
/** Default track color for a linear progress indicator. */
- val LinearTrackColor: Color @Composable get() =
+ val linearTrackColor: Color @Composable get() =
LinearProgressIndicatorTokens.TrackColor.toColor()
/** Default stroke width for a circular progress indicator. */
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt
index 6e6ef5e..86cc3b3 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt
@@ -94,11 +94,11 @@
action: @Composable (() -> Unit)? = null,
dismissAction: @Composable (() -> Unit)? = null,
actionOnNewLine: Boolean = false,
- shape: Shape = SnackbarDefaults.Shape,
- containerColor: Color = SnackbarDefaults.Color,
- contentColor: Color = SnackbarDefaults.ContentColor,
- actionContentColor: Color = SnackbarDefaults.ActionContentColor,
- dismissActionContentColor: Color = SnackbarDefaults.DismissActionContentColor,
+ shape: Shape = SnackbarDefaults.shape,
+ containerColor: Color = SnackbarDefaults.color,
+ contentColor: Color = SnackbarDefaults.contentColor,
+ actionContentColor: Color = SnackbarDefaults.actionContentColor,
+ dismissActionContentColor: Color = SnackbarDefaults.dismissActionContentColor,
content: @Composable () -> Unit
) {
Surface(
@@ -196,12 +196,12 @@
snackbarData: SnackbarData,
modifier: Modifier = Modifier,
actionOnNewLine: Boolean = false,
- shape: Shape = SnackbarDefaults.Shape,
- containerColor: Color = SnackbarDefaults.Color,
- contentColor: Color = SnackbarDefaults.ContentColor,
- actionColor: Color = SnackbarDefaults.ActionColor,
- actionContentColor: Color = SnackbarDefaults.ActionContentColor,
- dismissActionContentColor: Color = SnackbarDefaults.DismissActionContentColor,
+ shape: Shape = SnackbarDefaults.shape,
+ containerColor: Color = SnackbarDefaults.color,
+ contentColor: Color = SnackbarDefaults.contentColor,
+ actionColor: Color = SnackbarDefaults.actionColor,
+ actionContentColor: Color = SnackbarDefaults.actionContentColor,
+ dismissActionContentColor: Color = SnackbarDefaults.dismissActionContentColor,
) {
val actionLabel = snackbarData.visuals.actionLabel
val actionComposable: (@Composable () -> Unit)? = if (actionLabel != null) {
@@ -403,22 +403,22 @@
*/
object SnackbarDefaults {
/** Default shape of a snackbar. */
- val Shape: Shape @Composable get() = SnackbarTokens.ContainerShape.toShape()
+ val shape: Shape @Composable get() = SnackbarTokens.ContainerShape.toShape()
/** Default color of a snackbar. */
- val Color: Color @Composable get() = SnackbarTokens.ContainerColor.toColor()
+ val color: Color @Composable get() = SnackbarTokens.ContainerColor.toColor()
/** Default content color of a snackbar. */
- val ContentColor: Color @Composable get() = SnackbarTokens.SupportingTextColor.toColor()
+ val contentColor: Color @Composable get() = SnackbarTokens.SupportingTextColor.toColor()
/** Default action color of a snackbar. */
- val ActionColor: Color @Composable get() = SnackbarTokens.ActionLabelTextColor.toColor()
+ val actionColor: Color @Composable get() = SnackbarTokens.ActionLabelTextColor.toColor()
/** Default action content color of a snackbar. */
- val ActionContentColor: Color @Composable get() = SnackbarTokens.ActionLabelTextColor.toColor()
+ val actionContentColor: Color @Composable get() = SnackbarTokens.ActionLabelTextColor.toColor()
/** Default dismiss action content color of a snackbar. */
- val DismissActionContentColor: Color @Composable get() = SnackbarTokens.IconColor.toColor()
+ val dismissActionContentColor: Color @Composable get() = SnackbarTokens.IconColor.toColor()
}
private val ContainerMaxWidth = 600.dp
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Switch.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Switch.kt
index 6db393d..306b2f2 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Switch.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Switch.kt
@@ -123,7 +123,7 @@
DisposableEffect(checked) {
if (offset.targetValue != targetValue) {
scope.launch {
- offset.animateTo(targetValue)
+ offset.animateTo(targetValue, AnimationSpec)
}
}
onDispose { }
@@ -134,12 +134,7 @@
if (onCheckedChange != null) {
Modifier.toggleable(
value = checked,
- onValueChange = { value: Boolean ->
- onCheckedChange(value)
- scope.launch {
- offset.animateTo(valueToOffset(value), AnimationSpec)
- }
- },
+ onValueChange = onCheckedChange,
enabled = enabled,
role = Role.Switch,
interactionSource = interactionSource,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TabRow.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TabRow.kt
index ddc2e93..66afa3a 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TabRow.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TabRow.kt
@@ -126,8 +126,8 @@
fun TabRow(
selectedTabIndex: Int,
modifier: Modifier = Modifier,
- containerColor: Color = TabRowDefaults.Color,
- contentColor: Color = TabRowDefaults.ContentColor,
+ containerColor: Color = TabRowDefaults.containerColor,
+ contentColor: Color = TabRowDefaults.contentColor,
indicator: @Composable (tabPositions: List<TabPosition>) -> Unit = @Composable { tabPositions ->
TabRowDefaults.Indicator(
Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
@@ -214,8 +214,8 @@
fun ScrollableTabRow(
selectedTabIndex: Int,
modifier: Modifier = Modifier,
- containerColor: Color = TabRowDefaults.Color,
- contentColor: Color = TabRowDefaults.ContentColor,
+ containerColor: Color = TabRowDefaults.containerColor,
+ contentColor: Color = TabRowDefaults.contentColor,
edgePadding: Dp = ScrollableTabRowPadding,
indicator: @Composable (tabPositions: List<TabPosition>) -> Unit = @Composable { tabPositions ->
TabRowDefaults.Indicator(
@@ -343,10 +343,11 @@
*/
object TabRowDefaults {
/** Default color of a tab row. */
- val Color: Color @Composable get() = PrimaryNavigationTabTokens.ContainerColor.toColor()
+ val containerColor: Color @Composable get() =
+ PrimaryNavigationTabTokens.ContainerColor.toColor()
/** Default content color of a tab row. */
- val ContentColor: Color @Composable get() =
+ val contentColor: Color @Composable get() =
PrimaryNavigationTabTokens.ActiveLabelTextColor.toColor()
/**
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
index a023710..63fb471 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
@@ -173,7 +173,7 @@
singleLine: Boolean = false,
maxLines: Int = Int.MAX_VALUE,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- shape: Shape = TextFieldDefaults.FilledShape,
+ shape: Shape = TextFieldDefaults.filledShape,
colors: TextFieldColors = TextFieldDefaults.textFieldColors()
) {
// If color is not provided via the text style, use content color as a default
@@ -308,7 +308,7 @@
singleLine: Boolean = false,
maxLines: Int = Int.MAX_VALUE,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- shape: Shape = TextFieldDefaults.FilledShape,
+ shape: Shape = TextFieldDefaults.filledShape,
colors: TextFieldColors = TextFieldDefaults.textFieldColors()
) {
// If color is not provided via the text style, use content color as a default
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt
index 4a21219..4461392 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt
@@ -167,10 +167,10 @@
@Immutable
object TextFieldDefaults {
/** Default shape for an outlined text field. */
- val OutlinedShape: Shape @Composable get() = OutlinedTextFieldTokens.ContainerShape.toShape()
+ val outlinedShape: Shape @Composable get() = OutlinedTextFieldTokens.ContainerShape.toShape()
/** Default shape for a filled text field. */
- val FilledShape: Shape @Composable get() = FilledTextFieldTokens.ContainerShape.toShape()
+ val filledShape: Shape @Composable get() = FilledTextFieldTokens.ContainerShape.toShape()
/**
* The default min width applied for a [TextField] and [OutlinedTextField].
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorDarkTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorDarkTokens.kt
index c7f7094..82a7273 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorDarkTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorDarkTokens.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-// VERSION: v0_92
+// VERSION: v0_103
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.compose.material3.tokens
@@ -37,8 +37,10 @@
val OnTertiary = PaletteTokens.Tertiary20
val OnTertiaryContainer = PaletteTokens.Tertiary90
val Outline = PaletteTokens.NeutralVariant60
+ val OutlineVariant = PaletteTokens.NeutralVariant30
val Primary = PaletteTokens.Primary80
val PrimaryContainer = PaletteTokens.Primary30
+ val Scrim = PaletteTokens.Neutral0
val Secondary = PaletteTokens.Secondary80
val SecondaryContainer = PaletteTokens.Secondary30
val Surface = PaletteTokens.Neutral10
@@ -46,4 +48,4 @@
val SurfaceVariant = PaletteTokens.NeutralVariant30
val Tertiary = PaletteTokens.Tertiary80
val TertiaryContainer = PaletteTokens.Tertiary30
-}
+}
\ No newline at end of file
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorLightTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorLightTokens.kt
index 54a28b6..765b65d 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorLightTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorLightTokens.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-// VERSION: v0_92
+// VERSION: v0_103
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.compose.material3.tokens
@@ -37,8 +37,10 @@
val OnTertiary = PaletteTokens.Tertiary100
val OnTertiaryContainer = PaletteTokens.Tertiary10
val Outline = PaletteTokens.NeutralVariant50
+ val OutlineVariant = PaletteTokens.NeutralVariant80
val Primary = PaletteTokens.Primary40
val PrimaryContainer = PaletteTokens.Primary90
+ val Scrim = PaletteTokens.Neutral0
val Secondary = PaletteTokens.Secondary40
val SecondaryContainer = PaletteTokens.Secondary90
val Surface = PaletteTokens.Neutral99
@@ -46,4 +48,4 @@
val SurfaceVariant = PaletteTokens.NeutralVariant90
val Tertiary = PaletteTokens.Tertiary40
val TertiaryContainer = PaletteTokens.Tertiary90
-}
+}
\ No newline at end of file
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorSchemeKeyTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorSchemeKeyTokens.kt
index ee5bf8f..c1c1104 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorSchemeKeyTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ColorSchemeKeyTokens.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-// VERSION: v0_92
+// VERSION: v0_103
// GENERATED CODE - DO NOT MODIFY BY HAND
package androidx.compose.material3.tokens
@@ -37,8 +37,10 @@
OnTertiary,
OnTertiaryContainer,
Outline,
+ OutlineVariant,
Primary,
PrimaryContainer,
+ Scrim,
Secondary,
SecondaryContainer,
Surface,
@@ -46,4 +48,4 @@
SurfaceVariant,
Tertiary,
TertiaryContainer,
-}
+}
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
index cbc5577..489920f 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
@@ -452,7 +452,8 @@
previousSnapshot = currentSnapshot as? MutableSnapshot,
specifiedReadObserver = readObserver,
specifiedWriteObserver = writeObserver,
- mergeParentObservers = true
+ mergeParentObservers = true,
+ ownsPreviousSnapshot = false
)
else if (readObserver == null) return block()
else currentSnapshot.takeNestedSnapshot(readObserver)
@@ -1434,7 +1435,8 @@
private val previousSnapshot: MutableSnapshot?,
internal val specifiedReadObserver: ((Any) -> Unit)?,
internal val specifiedWriteObserver: ((Any) -> Unit)?,
- private val mergeParentObservers: Boolean
+ private val mergeParentObservers: Boolean,
+ private val ownsPreviousSnapshot: Boolean
) : MutableSnapshot(
INVALID_SNAPSHOT,
SnapshotIdSet.EMPTY,
@@ -1454,6 +1456,9 @@
override fun dispose() {
// Explicitly don't call super.dispose()
disposed = true
+ if (ownsPreviousSnapshot) {
+ previousSnapshot?.dispose()
+ }
}
override var id: Int
@@ -1486,7 +1491,8 @@
return if (!mergeParentObservers) {
createTransparentSnapshotWithNoParentReadObserver(
previousSnapshot = currentSnapshot.takeNestedSnapshot(null),
- readObserver = readObserver
+ readObserver = mergedReadObserver,
+ ownsPreviousSnapshot = true
)
} else {
currentSnapshot.takeNestedSnapshot(mergedReadObserver)
@@ -1508,7 +1514,8 @@
previousSnapshot = nestedSnapshot,
specifiedReadObserver = mergedReadObserver,
specifiedWriteObserver = mergedWriteObserver,
- mergeParentObservers = false
+ mergeParentObservers = false,
+ ownsPreviousSnapshot = true
)
} else {
currentSnapshot.takeNestedMutableSnapshot(
@@ -1532,7 +1539,8 @@
internal class TransparentObserverSnapshot(
private val previousSnapshot: Snapshot?,
specifiedReadObserver: ((Any) -> Unit)?,
- private val mergeParentObservers: Boolean
+ private val mergeParentObservers: Boolean,
+ private val ownsPreviousSnapshot: Boolean
) : Snapshot(
INVALID_SNAPSHOT,
SnapshotIdSet.EMPTY,
@@ -1552,6 +1560,9 @@
override fun dispose() {
// Explicitly don't call super.dispose()
disposed = true
+ if (ownsPreviousSnapshot) {
+ previousSnapshot?.dispose()
+ }
}
override var id: Int
@@ -1580,8 +1591,9 @@
val mergedReadObserver = mergedReadObserver(readObserver, this.readObserver)
return if (!mergeParentObservers) {
createTransparentSnapshotWithNoParentReadObserver(
- previousSnapshot = currentSnapshot.takeNestedSnapshot(null),
- readObserver = readObserver
+ currentSnapshot.takeNestedSnapshot(null),
+ mergedReadObserver,
+ ownsPreviousSnapshot = true
)
} else {
currentSnapshot.takeNestedSnapshot(mergedReadObserver)
@@ -1599,18 +1611,21 @@
private fun createTransparentSnapshotWithNoParentReadObserver(
previousSnapshot: Snapshot?,
readObserver: ((Any) -> Unit)? = null,
+ ownsPreviousSnapshot: Boolean = false
): Snapshot = if (previousSnapshot is MutableSnapshot || previousSnapshot == null) {
TransparentObserverMutableSnapshot(
previousSnapshot = previousSnapshot as? MutableSnapshot,
specifiedReadObserver = readObserver,
specifiedWriteObserver = null,
- mergeParentObservers = false
+ mergeParentObservers = false,
+ ownsPreviousSnapshot = ownsPreviousSnapshot
)
} else {
TransparentObserverSnapshot(
previousSnapshot = previousSnapshot,
specifiedReadObserver = readObserver,
- mergeParentObservers = false
+ mergeParentObservers = false,
+ ownsPreviousSnapshot = ownsPreviousSnapshot
)
}
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
index 8d279be..cdff066 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
@@ -936,6 +936,126 @@
}
}
+ @Test
+ fun testNestedWithinTransparentSnapshotDisposedCorrectly() {
+ val outerSnapshot = TransparentObserverSnapshot(
+ previousSnapshot = currentSnapshot(),
+ specifiedReadObserver = null,
+ mergeParentObservers = false,
+ ownsPreviousSnapshot = false
+ )
+
+ try {
+ outerSnapshot.enter {
+ val innerSnapshot = outerSnapshot.takeNestedSnapshot()
+
+ try {
+ innerSnapshot.enter { }
+ } finally {
+ innerSnapshot.dispose()
+ }
+ }
+ } finally {
+ outerSnapshot.dispose()
+ }
+ }
+
+ @Test
+ fun testNestedWithinTransparentMutableSnapshotDisposedCorrectly() {
+ val outerSnapshot = TransparentObserverMutableSnapshot(
+ previousSnapshot = currentSnapshot() as? MutableSnapshot,
+ specifiedReadObserver = null,
+ specifiedWriteObserver = null,
+ mergeParentObservers = false,
+ ownsPreviousSnapshot = false
+ )
+
+ try {
+ outerSnapshot.enter {
+ val innerSnapshot = outerSnapshot.takeNestedSnapshot()
+
+ try {
+ innerSnapshot.enter { }
+ } finally {
+ innerSnapshot.dispose()
+ }
+ }
+ } finally {
+ outerSnapshot.dispose()
+ }
+ }
+
+ @Test
+ fun testTransparentSnapshotMergedWithNestedReadObserver() {
+ var outerChanges = 0
+ var innerChanges = 0
+ val state by mutableStateOf(0)
+
+ val outerSnapshot = TransparentObserverSnapshot(
+ previousSnapshot = currentSnapshot(),
+ specifiedReadObserver = { outerChanges++ },
+ mergeParentObservers = false,
+ ownsPreviousSnapshot = false
+ )
+
+ try {
+ outerSnapshot.enter {
+ val innerSnapshot = outerSnapshot.takeNestedSnapshot(
+ readObserver = { innerChanges++ }
+ )
+
+ try {
+ innerSnapshot.enter {
+ state // read
+ }
+ } finally {
+ innerSnapshot.dispose()
+ }
+ }
+ } finally {
+ outerSnapshot.dispose()
+ }
+
+ assertEquals(1, outerChanges)
+ assertEquals(1, innerChanges)
+ }
+
+ @Test
+ fun testTransparentMutableSnapshotMergedWithNestedReadObserver() {
+ var outerChanges = 0
+ var innerChanges = 0
+ val state by mutableStateOf(0)
+
+ val outerSnapshot = TransparentObserverMutableSnapshot(
+ previousSnapshot = currentSnapshot() as? MutableSnapshot,
+ specifiedReadObserver = { outerChanges++ },
+ specifiedWriteObserver = null,
+ mergeParentObservers = false,
+ ownsPreviousSnapshot = false
+ )
+
+ try {
+ outerSnapshot.enter {
+ val innerSnapshot = outerSnapshot.takeNestedSnapshot(
+ readObserver = { innerChanges++ }
+ )
+
+ try {
+ innerSnapshot.enter {
+ state // read
+ }
+ } finally {
+ innerSnapshot.dispose()
+ }
+ }
+ } finally {
+ outerSnapshot.dispose()
+ }
+
+ assertEquals(1, outerChanges)
+ assertEquals(1, innerChanges)
+ }
+
private var count = 0
@BeforeTest
diff --git a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/LambdaLocationTest.kt b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/LambdaLocationTest.kt
index f5dcd6c..b98fc6fdd 100644
--- a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/LambdaLocationTest.kt
+++ b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/LambdaLocationTest.kt
@@ -42,5 +42,7 @@
.isEqualTo(LambdaLocation("TestLambdas.kt", 29, 30))
assertThat(LambdaLocation.resolve(TestLambdas.inlinedParameter))
.isEqualTo(LambdaLocation("TestLambdas.kt", 33, 33))
+ assertThat(LambdaLocation.resolve(TestLambdas.unnamed))
+ .isEqualTo(LambdaLocation("TestLambdas.kt", 35, 35))
}
}
\ No newline at end of file
diff --git a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/testdata/TestLambdas.kt b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/testdata/TestLambdas.kt
index b127774a..5ff2b27 100644
--- a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/testdata/TestLambdas.kt
+++ b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/testdata/TestLambdas.kt
@@ -32,6 +32,7 @@
val inlinedParameter = { o: IntOffset ->
o.x * 2
}
+ val unnamed: (Int, Int) -> Float = { _, _ -> 0f }
/**
* This inline function will appear at a line numbers
diff --git a/compose/ui/ui-inspection/src/main/cpp/lambda_location_java_jni.cpp b/compose/ui/ui-inspection/src/main/cpp/lambda_location_java_jni.cpp
index 1a52bfb..11e4a1a 100644
--- a/compose/ui/ui-inspection/src/main/cpp/lambda_location_java_jni.cpp
+++ b/compose/ui/ui-inspection/src/main/cpp/lambda_location_java_jni.cpp
@@ -169,7 +169,8 @@
InlineRange *ranges = nullptr;
for (int i=0; i<variableCount; i++) {
jvmtiLocalVariableEntry *variable = &variables[i];
- if (strncmp("$i$f$", variable->name, 5) == 0) {
+ char* name = variable->name;
+ if (name != nullptr && strncmp("$i$f$", name, 5) == 0) {
if (ranges == nullptr) {
jvmti->Allocate(sizeof(InlineRange) * (variableCount-i), (unsigned char **)&ranges);
}
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
index 62dd84e..43fea44 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
@@ -553,13 +553,10 @@
.flatMap { config -> config.map { RawParameter(it.key.name, it.value) } }
)
- node.mergedSemantics.addAll(
- modifierInfo.asSequence()
- .map { it.modifier }
- .filterIsInstance<SemanticsModifier>()
- .map { it.id }
- .flatMap { semanticsMap[it].orEmpty() }
- )
+ val mergedSemantics = semanticsMap.get(layoutInfo.semanticsId)
+ if (mergedSemantics != null) {
+ node.mergedSemantics.addAll(mergedSemantics)
+ }
node.id = modifierInfo.asSequence()
.map { it.extra }
diff --git a/compose/ui/ui-tooling-data/build.gradle b/compose/ui/ui-tooling-data/build.gradle
index ab2b6fa..79fe224 100644
--- a/compose/ui/ui-tooling-data/build.gradle
+++ b/compose/ui/ui-tooling-data/build.gradle
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+
+import androidx.build.AndroidXComposePlugin
import androidx.build.LibraryType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
@@ -21,30 +23,99 @@
id("AndroidXPlugin")
id("com.android.library")
id("AndroidXComposePlugin")
- id("org.jetbrains.kotlin.android")
}
-dependencies {
+AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
- implementation(libs.kotlinStdlib)
+if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- api "androidx.annotation:annotation:1.1.0"
+ dependencies {
+ /*
+ * When updating dependencies, make sure to make the an an analogous update in the
+ * corresponding block below
+ */
- api("androidx.compose.runtime:runtime:1.2.0-rc02")
- api(project(":compose:ui:ui"))
+ implementation(libs.kotlinStdlib)
- androidTestImplementation project(":compose:ui:ui-test-junit4")
+ api "androidx.annotation:annotation:1.1.0"
- androidTestImplementation(libs.junit)
- androidTestImplementation(libs.testCore)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.testRules)
+ api("androidx.compose.runtime:runtime:1.2.0-rc02")
+ api(project(":compose:ui:ui"))
- androidTestImplementation(libs.truth)
- androidTestImplementation(project(":compose:foundation:foundation-layout"))
- androidTestImplementation(project(":compose:foundation:foundation"))
- androidTestImplementation(project(":compose:material:material"))
- androidTestImplementation("androidx.activity:activity-compose:1.3.1")
+ androidTestImplementation project(":compose:ui:ui-test-junit4")
+
+ androidTestImplementation(libs.junit)
+ androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.testRules)
+
+ androidTestImplementation(libs.truth)
+ androidTestImplementation(project(":compose:foundation:foundation-layout"))
+ androidTestImplementation(project(":compose:foundation:foundation"))
+ androidTestImplementation(project(":compose:material:material"))
+ androidTestImplementation("androidx.activity:activity-compose:1.3.1")
+ }
+}
+
+if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
+ androidXComposeMultiplatform {
+ android()
+ desktop()
+ }
+
+ kotlin {
+
+ /*
+ * When updating dependencies, make sure to make the an an analogous update in the
+ * corresponding block above
+ */
+ sourceSets {
+ commonMain.dependencies {
+
+ implementation(libs.kotlinStdlib)
+
+ api "androidx.annotation:annotation:1.1.0"
+
+ api("androidx.compose.runtime:runtime:1.2.0-rc02")
+ api(project(":compose:ui:ui"))
+ }
+ jvmMain.dependencies {
+ implementation(libs.kotlinStdlib)
+ }
+ androidMain.dependencies {
+ api("androidx.annotation:annotation:1.1.0")
+ }
+
+ commonTest.dependencies {
+ implementation(kotlin("test-junit"))
+ }
+
+ // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+ // need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+ // level dependencies block instead:
+ // `dependencies { testImplementation(libs.robolectric) }`
+ androidTest.dependencies {
+ implementation(libs.truth)
+ }
+ androidAndroidTest.dependencies {
+ implementation(project(":compose:ui:ui-test-junit4"))
+
+ implementation(libs.junit)
+ implementation(libs.testCore)
+ implementation(libs.testRunner)
+ implementation(libs.testRules)
+
+ implementation(libs.truth)
+ implementation(project(":compose:foundation:foundation-layout"))
+ implementation(project(":compose:foundation:foundation"))
+ implementation(project(":compose:material:material"))
+ implementation("androidx.activity:activity-compose:1.3.1")
+ }
+ }
+ }
+ dependencies {
+ samples(projectOrArtifact(":compose:ui:ui-unit:ui-unit-samples"))
+ }
}
androidx {
diff --git a/compose/ui/ui-tooling-data/src/androidTest/AndroidManifest.xml b/compose/ui/ui-tooling-data/src/androidAndroidTest/AndroidManifest.xml
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/AndroidManifest.xml
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/AndroidManifest.xml
diff --git a/compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/BoundsTest.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/BoundsTest.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/BoundsTest.kt
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/BoundsTest.kt
diff --git a/compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/Inspectable.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/Inspectable.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/Inspectable.kt
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/Inspectable.kt
diff --git a/compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/InspectableTests.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/InspectableTests.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/InspectableTests.kt
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/InspectableTests.kt
diff --git a/compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/ModifierInfoTest.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/ModifierInfoTest.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/ModifierInfoTest.kt
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/ModifierInfoTest.kt
diff --git a/compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/OffsetData.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/OffsetData.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/OffsetData.kt
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/OffsetData.kt
diff --git a/compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/OffsetInformationTest.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/OffsetInformationTest.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/OffsetInformationTest.kt
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/OffsetInformationTest.kt
diff --git a/compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/TestActivity.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/TestActivity.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/TestActivity.kt
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/TestActivity.kt
diff --git a/compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/ToolingTest.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/ToolingTest.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/ToolingTest.kt
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/ToolingTest.kt
diff --git a/compose/ui/ui-tooling-data/src/main/AndroidManifest.xml b/compose/ui/ui-tooling-data/src/androidMain/AndroidManifest.xml
similarity index 100%
rename from compose/ui/ui-tooling-data/src/main/AndroidManifest.xml
rename to compose/ui/ui-tooling-data/src/androidMain/AndroidManifest.xml
diff --git a/compose/ui/ui-tooling-data/src/main/java/androidx/compose/ui/tooling/data/SlotTree.kt b/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/SlotTree.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/main/java/androidx/compose/ui/tooling/data/SlotTree.kt
rename to compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/SlotTree.kt
diff --git a/compose/ui/ui-tooling-data/src/main/java/androidx/compose/ui/tooling/data/UiToolingDataApi.kt b/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/UiToolingDataApi.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/main/java/androidx/compose/ui/tooling/data/UiToolingDataApi.kt
rename to compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/UiToolingDataApi.kt
diff --git a/compose/ui/ui/api/current.ignore b/compose/ui/ui/api/current.ignore
index 47b464a..0a42ea8 100644
--- a/compose/ui/ui/api/current.ignore
+++ b/compose/ui/ui/api/current.ignore
@@ -1,4 +1,8 @@
// Baseline format: 1.0
+AddedAbstractMethod: androidx.compose.ui.layout.LayoutInfo#getSemanticsId():
+ Added method androidx.compose.ui.layout.LayoutInfo.getSemanticsId()
+
+
RemovedDeprecatedMethod: androidx.compose.ui.graphics.vector.ImageVector.Builder#Builder(String, float, float, float, float, long, int):
Removed deprecated method androidx.compose.ui.graphics.vector.ImageVector.Builder.Builder(String,float,float,float,float,long,int)
RemovedDeprecatedMethod: androidx.compose.ui.input.pointer.PointerInputChange#PointerInputChange(long, long, long, boolean, long, long, boolean, androidx.compose.ui.input.pointer.ConsumedData, int):
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index bf169d7..6d14189 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -1871,6 +1871,7 @@
method public long localPositionOf(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, long relativeToSource);
method public long localToRoot(long relativeToLocal);
method public long localToWindow(long relativeToLocal);
+ method public default void transformFrom(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, float[] matrix);
method public long windowToLocal(long relativeToWindow);
property public abstract boolean isAttached;
property public abstract androidx.compose.ui.layout.LayoutCoordinates? parentCoordinates;
@@ -1905,6 +1906,7 @@
method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
method public androidx.compose.ui.layout.LayoutInfo? getParentInfo();
+ method public int getSemanticsId();
method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
method public int getWidth();
method public boolean isAttached();
@@ -1916,6 +1918,7 @@
property public abstract boolean isPlaced;
property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
property public abstract androidx.compose.ui.layout.LayoutInfo? parentInfo;
+ property public abstract int semanticsId;
property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
property public abstract int width;
}
@@ -2769,9 +2772,9 @@
}
@kotlin.jvm.JvmDefaultWithCompatibility public interface SemanticsModifier extends androidx.compose.ui.Modifier.Element {
- method public int getId();
+ method @Deprecated public default int getId();
method public androidx.compose.ui.semantics.SemanticsConfiguration getSemanticsConfiguration();
- property public abstract int id;
+ property @Deprecated public default int id;
property public abstract androidx.compose.ui.semantics.SemanticsConfiguration semanticsConfiguration;
}
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index f6fb19e..32bc836 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -2017,6 +2017,7 @@
method public long localPositionOf(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, long relativeToSource);
method public long localToRoot(long relativeToLocal);
method public long localToWindow(long relativeToLocal);
+ method public default void transformFrom(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, float[] matrix);
method public long windowToLocal(long relativeToWindow);
property public abstract boolean isAttached;
property public abstract androidx.compose.ui.layout.LayoutCoordinates? parentCoordinates;
@@ -2051,6 +2052,7 @@
method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
method public androidx.compose.ui.layout.LayoutInfo? getParentInfo();
+ method public int getSemanticsId();
method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
method public int getWidth();
method public boolean isAttached();
@@ -2062,6 +2064,7 @@
property public abstract boolean isPlaced;
property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
property public abstract androidx.compose.ui.layout.LayoutInfo? parentInfo;
+ property public abstract int semanticsId;
property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
property public abstract int width;
}
@@ -2982,9 +2985,9 @@
}
@kotlin.jvm.JvmDefaultWithCompatibility public interface SemanticsModifier extends androidx.compose.ui.Modifier.Element {
- method public int getId();
+ method @Deprecated public default int getId();
method public androidx.compose.ui.semantics.SemanticsConfiguration getSemanticsConfiguration();
- property public abstract int id;
+ property @Deprecated public default int id;
property public abstract androidx.compose.ui.semantics.SemanticsConfiguration semanticsConfiguration;
}
diff --git a/compose/ui/ui/api/restricted_current.ignore b/compose/ui/ui/api/restricted_current.ignore
index 47b464a..0a42ea8 100644
--- a/compose/ui/ui/api/restricted_current.ignore
+++ b/compose/ui/ui/api/restricted_current.ignore
@@ -1,4 +1,8 @@
// Baseline format: 1.0
+AddedAbstractMethod: androidx.compose.ui.layout.LayoutInfo#getSemanticsId():
+ Added method androidx.compose.ui.layout.LayoutInfo.getSemanticsId()
+
+
RemovedDeprecatedMethod: androidx.compose.ui.graphics.vector.ImageVector.Builder#Builder(String, float, float, float, float, long, int):
Removed deprecated method androidx.compose.ui.graphics.vector.ImageVector.Builder.Builder(String,float,float,float,float,long,int)
RemovedDeprecatedMethod: androidx.compose.ui.input.pointer.PointerInputChange#PointerInputChange(long, long, long, boolean, long, long, boolean, androidx.compose.ui.input.pointer.ConsumedData, int):
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index f6326ba..2b300fe 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -1871,6 +1871,7 @@
method public long localPositionOf(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, long relativeToSource);
method public long localToRoot(long relativeToLocal);
method public long localToWindow(long relativeToLocal);
+ method public default void transformFrom(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, float[] matrix);
method public long windowToLocal(long relativeToWindow);
property public abstract boolean isAttached;
property public abstract androidx.compose.ui.layout.LayoutCoordinates? parentCoordinates;
@@ -1905,6 +1906,7 @@
method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
method public androidx.compose.ui.layout.LayoutInfo? getParentInfo();
+ method public int getSemanticsId();
method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
method public int getWidth();
method public boolean isAttached();
@@ -1916,6 +1918,7 @@
property public abstract boolean isPlaced;
property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
property public abstract androidx.compose.ui.layout.LayoutInfo? parentInfo;
+ property public abstract int semanticsId;
property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
property public abstract int width;
}
@@ -2805,9 +2808,9 @@
}
@kotlin.jvm.JvmDefaultWithCompatibility public interface SemanticsModifier extends androidx.compose.ui.Modifier.Element {
- method public int getId();
+ method @Deprecated public default int getId();
method public androidx.compose.ui.semantics.SemanticsConfiguration getSemanticsConfiguration();
- property public abstract int id;
+ property @Deprecated public default int id;
property public abstract androidx.compose.ui.semantics.SemanticsConfiguration semanticsConfiguration;
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
index 5adc1c1..c77098c 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
@@ -904,7 +904,6 @@
val nodes = SemanticsOwner(
LayoutNode().also {
it.modifier = SemanticsModifierCore(
- id = SemanticsModifierCore.generateSemanticsId(),
mergeDescendants = false,
clearAndSetSemantics = false,
properties = {}
@@ -1264,9 +1263,9 @@
mergeDescendants: Boolean,
properties: (SemanticsPropertyReceiver.() -> Unit)
): SemanticsNode {
- val semanticsModifier = SemanticsModifierCore(id, mergeDescendants, false, properties)
+ val semanticsModifier = SemanticsModifierCore(mergeDescendants, false, properties)
return SemanticsNode(
- SemanticsEntity(InnerPlaceable(LayoutNode()), semanticsModifier),
+ SemanticsEntity(InnerPlaceable(LayoutNode(semanticsId = id)), semanticsModifier),
true
)
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
index a7ea12c..0fe6b5d 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
@@ -28,6 +28,7 @@
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.RenderEffect
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.TransformOrigin
@@ -3755,6 +3756,12 @@
) {
}
+ override fun transform(matrix: Matrix) {
+ }
+
+ override fun inverseTransform(matrix: Matrix) {
+ }
+
override fun mapOffset(point: Offset, inverse: Boolean) = point
}
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadLayoutTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadLayoutTest.kt
index b75e054..4deac3c 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadLayoutTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadLayoutTest.kt
@@ -61,6 +61,7 @@
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.platform.AndroidOwnerExtraAssertionsRule
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.test.junit4.createAndroidComposeRule
@@ -976,6 +977,38 @@
}
}
+ @Test
+ fun lookaheadLayoutTransformFrom() {
+ val matrix = Matrix()
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides Density(1f)) {
+ LookaheadLayout(
+ measurePolicy = { measurables, constraints ->
+ val placeable = measurables[0].measure(constraints)
+ // Position the children.
+ layout(placeable.width + 10, placeable.height + 10) {
+ placeable.place(10, 10)
+ }
+ },
+ content = {
+ Box(
+ Modifier
+ .onPlaced { lookaheadScopeCoordinates, layoutCoordinates ->
+ layoutCoordinates.transformFrom(
+ lookaheadScopeCoordinates,
+ matrix
+ )
+ }
+ .size(10.dp))
+ }
+ )
+ }
+ }
+ rule.waitForIdle()
+ val posInChild = matrix.map(Offset(10f, 10f))
+ assertEquals(Offset.Zero, posInChild)
+ }
+
private fun assertSameLayoutWithAndWithoutLookahead(
content: @Composable (
modifier: Modifier
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt
index 241e5cd..86a6f47 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt
@@ -406,6 +406,33 @@
shapeColor = Color.Red
)
}
+
+ @Test
+ fun introducingChildIntrinsicsViaModifierWhenParentUsedIntrinsicSizes() {
+ var childModifier by mutableStateOf(Modifier as Modifier)
+
+ rule.setContent {
+ LayoutUsingIntrinsics() {
+ Box(
+ Modifier
+ .testTag("child")
+ .then(childModifier)
+ )
+ }
+ }
+
+ rule.onNodeWithTag("child")
+ .assertWidthIsEqualTo(0.dp)
+ .assertHeightIsEqualTo(0.dp)
+
+ rule.runOnIdle {
+ childModifier = Modifier.withIntrinsics(30.dp, 20.dp)
+ }
+
+ rule.onNodeWithTag("child")
+ .assertWidthIsEqualTo(30.dp)
+ .assertHeightIsEqualTo(20.dp)
+ }
}
@Composable
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
index 085a1f7..55e8f7e 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
@@ -70,7 +70,6 @@
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
import kotlin.math.max
@MediumTest
@@ -91,16 +90,6 @@
isDebugInspectorInfoEnabled = false
}
- private fun executeUpdateBlocking(updateFunction: () -> Unit) {
- val latch = CountDownLatch(1)
- rule.runOnUiThread {
- updateFunction()
- latch.countDown()
- }
-
- latch.await()
- }
-
@Test
fun unchangedSemanticsDoesNotCauseRelayout() {
val layoutCounter = Counter(0)
@@ -118,6 +107,22 @@
}
@Test
+ fun valueSemanticsAreEqual() {
+ assertEquals(
+ Modifier.semantics {
+ text = AnnotatedString("text")
+ contentDescription = "foo"
+ popup()
+ },
+ Modifier.semantics {
+ text = AnnotatedString("text")
+ contentDescription = "foo"
+ popup()
+ }
+ )
+ }
+
+ @Test
fun depthFirstPropertyConcat() {
val root = "root"
val child1 = "child1"
@@ -533,6 +538,12 @@
val isAfter = mutableStateOf(false)
+ val content: @Composable () -> Unit = {
+ SimpleTestLayout {
+ nodeCount++
+ }
+ }
+
rule.setContent {
SimpleTestLayout(
Modifier.testTag(TestTag).semantics {
@@ -543,12 +554,9 @@
return@onClick true
}
)
- }
- ) {
- SimpleTestLayout {
- nodeCount++
- }
- }
+ },
+ content = content
+ )
}
// This isn't the important part, just makes sure everything is behaving as expected
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
index 9344d54..0885058 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
@@ -17,6 +17,8 @@
package androidx.compose.ui.viewinterop
import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
@@ -33,6 +35,7 @@
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
@@ -45,8 +48,10 @@
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.runtime.setValue
import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.AbsoluteAlignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalDensity
@@ -272,7 +277,10 @@
}
}
rule.setContent {
- AndroidView({ frameLayout }, Modifier.testTag("view").background(color = Color.Blue))
+ AndroidView({ frameLayout },
+ Modifier
+ .testTag("view")
+ .background(color = Color.Blue))
}
rule.onNodeWithTag("view").captureToImage().assertPixels(IntSize(size, size)) {
@@ -356,9 +364,11 @@
CompositionLocalProvider(LocalDensity provides density) {
AndroidView(
{ FrameLayout(it) },
- Modifier.requiredSize(size).onGloballyPositioned {
- assertThat(it.size).isEqualTo(IntSize(sizeIpx, sizeIpx))
- }
+ Modifier
+ .requiredSize(size)
+ .onGloballyPositioned {
+ assertThat(it.size).isEqualTo(IntSize(sizeIpx, sizeIpx))
+ }
)
}
}
@@ -566,7 +576,11 @@
val sizeDp = with(rule.density) { size.toDp() }
rule.setContent {
Column {
- Box(Modifier.size(sizeDp).background(Color.Blue).testTag("box"))
+ Box(
+ Modifier
+ .size(sizeDp)
+ .background(Color.Blue)
+ .testTag("box"))
AndroidView(factory = { SurfaceView(it) })
}
}
@@ -619,6 +633,40 @@
}
}
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ fun androidView_noClip() {
+ rule.setContent {
+ Box(Modifier.fillMaxSize().background(Color.White)) {
+ with(LocalDensity.current) {
+ Box(Modifier.requiredSize(150.toDp()).testTag("box")) {
+ Box(
+ Modifier.size(100.toDp(), 100.toDp()).align(AbsoluteAlignment.TopLeft)
+ ) {
+ AndroidView(factory = { context ->
+ object : View(context) {
+ init {
+ clipToOutline = false
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ val paint = Paint()
+ paint.color = Color.Blue.toArgb()
+ paint.style = Paint.Style.FILL
+ canvas.drawRect(0f, 0f, 150f, 150f, paint)
+ }
+ }
+ })
+ }
+ }
+ }
+ }
+ }
+ rule.onNodeWithTag("box").captureToImage().assertPixels(IntSize(150, 150)) {
+ Color.Blue
+ }
+ }
+
private class StateSavingView(
private val key: String,
private val value: String,
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index a60a1e2..532f0d5 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -189,7 +189,6 @@
private set
private val semanticsModifier = SemanticsModifierCore(
- id = SemanticsModifierCore.generateSemanticsId(),
mergeDescendants = false,
clearAndSetSemantics = false,
properties = {}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index 130f682..b7ff6d8 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -1558,7 +1558,8 @@
) {
val androidView = view.androidViewsHandler.layoutNodeToHolder[wrapper.layoutNode]
if (androidView == null) {
- virtualViewId = semanticsNodeIdToAccessibilityVirtualNodeId(wrapper.modifier.id)
+ virtualViewId =
+ semanticsNodeIdToAccessibilityVirtualNodeId(wrapper.layoutNode.semanticsId)
}
}
}
@@ -1719,7 +1720,7 @@
?.isMergingSemanticsOfDescendants == true
}?.outerSemantics?.let { semanticsWrapper = it }
}
- val id = semanticsWrapper.modifier.id
+ val id = semanticsWrapper.layoutNode.semanticsId
if (!subtreeChangedSemanticsNodesIds.add(id)) {
return
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt
index a62b572..07f4b2e 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt
@@ -25,6 +25,7 @@
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.CanvasHolder
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.RenderEffect
@@ -341,6 +342,17 @@
this.invalidateParentLayer = invalidateParentLayer
}
+ override fun transform(matrix: Matrix) {
+ matrix.timesAssign(matrixCache.calculateMatrix(renderNode))
+ }
+
+ override fun inverseTransform(matrix: Matrix) {
+ val inverse = matrixCache.calculateInverseMatrix(renderNode)
+ if (inverse != null) {
+ matrix.timesAssign(inverse)
+ }
+ }
+
companion object {
private val getMatrix: (DeviceRenderNode, android.graphics.Matrix) -> Unit = { rn, matrix ->
rn.getMatrix(matrix)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
index 912ff1f..55d1dcd 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
@@ -27,6 +27,7 @@
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.CanvasHolder
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.RenderEffect
@@ -356,6 +357,17 @@
this.invalidateParentLayer = invalidateParentLayer
}
+ override fun transform(matrix: Matrix) {
+ matrix.timesAssign(matrixCache.calculateMatrix(this))
+ }
+
+ override fun inverseTransform(matrix: Matrix) {
+ val inverse = matrixCache.calculateInverseMatrix(this)
+ if (inverse != null) {
+ matrix.timesAssign(inverse)
+ }
+ }
+
companion object {
private val getMatrix: (View, android.graphics.Matrix) -> Unit = { view, matrix ->
val newMatrix = view.matrix
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/InputEventCallback2.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/InputEventCallback2.android.kt
index 2d81082..36a4079 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/InputEventCallback2.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/InputEventCallback2.android.kt
@@ -40,4 +40,11 @@
* Called when IME triggered a KeyEvent
*/
fun onKeyEvent(event: KeyEvent)
+
+ /**
+ * Called when IME closed the input connection.
+ *
+ * @param ic a closed input connection
+ */
+ fun onConnectionClosed(ic: RecordingInputConnection)
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/RecordingInputConnection.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/RecordingInputConnection.android.kt
index 66a63a2..97ee5954 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/RecordingInputConnection.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/RecordingInputConnection.android.kt
@@ -168,6 +168,7 @@
editCommands.clear()
batchDepth = 0
isActive = false
+ eventCallback.onConnectionClosed(this)
}
// /////////////////////////////////////////////////////////////////////////////////////////////
@@ -276,7 +277,8 @@
if (DEBUG) {
with(extractedText) {
logDebug(
- "getExtractedText() return: text: $text" +
+
+ "getExtractedText() return: text: \"$text\"" +
",partialStartOffset $partialStartOffset" +
",partialEndOffset $partialEndOffset" +
",selectionStart $selectionStart" +
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
index 83b2f84..1dbb467 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
@@ -31,6 +31,7 @@
import androidx.compose.ui.text.input.TextInputServiceAndroid.TextInputCommand.StartInput
import androidx.compose.ui.text.input.TextInputServiceAndroid.TextInputCommand.StopInput
import androidx.core.view.inputmethod.EditorInfoCompat
+import java.lang.ref.WeakReference
import kotlin.math.roundToInt
import kotlinx.coroutines.channels.Channel
@@ -72,7 +73,12 @@
internal var state = TextFieldValue(text = "", selection = TextRange.Zero)
private set
private var imeOptions = ImeOptions.Default
- private var ic: RecordingInputConnection? = null
+
+ // RecordingInputConnection has strong reference to the View through TextInputServiceAndroid and
+ // event callback. The connection should be closed when IME has changed and removed from this
+ // list in onConnectionClosed callback, but not clear it is guaranteed the close connection is
+ // called any time. So, keep it in WeakReference just in case.
+ private var ics = mutableListOf<WeakReference<RecordingInputConnection>>()
// used for sendKeyEvent delegation
private val baseInputConnection by lazy(LazyThreadSafetyMode.NONE) {
@@ -120,10 +126,19 @@
override fun onKeyEvent(event: KeyEvent) {
baseInputConnection.sendKeyEvent(event)
}
+
+ override fun onConnectionClosed(ic: RecordingInputConnection) {
+ for (i in 0 until ics.size) {
+ if (ics[i].get() == ic) {
+ ics.removeAt(i)
+ return // No duplicated instances should be in the list.
+ }
+ }
+ }
}
).also {
- ic = it
- if (DEBUG) { Log.d(TAG, "$DEBUG_CLASS.createInputConnection: $ic") }
+ ics.add(WeakReference(it))
+ if (DEBUG) { Log.d(TAG, "$DEBUG_CLASS.createInputConnection: $ics") }
}
}
@@ -298,7 +313,9 @@
this.state.composition != newValue.composition
this.state = newValue
// update the latest TextFieldValue in InputConnection
- ic?.mTextFieldValue = newValue
+ for (i in 0 until ics.size) {
+ ics[i].get()?.mTextFieldValue = newValue
+ }
if (oldValue == newValue) {
if (DEBUG) {
@@ -330,7 +347,9 @@
if (restartInput) {
restartInputImmediately()
} else {
- ic?.updateInputState(this.state, inputMethodManager, view)
+ for (i in 0 until ics.size) {
+ ics[i].get()?.updateInputState(this.state, inputMethodManager, view)
+ }
}
}
@@ -349,7 +368,7 @@
// use, i.e. InputConnection has created.
// Even if we miss all the timing of requesting rectangle during initial text field focus,
// focused rectangle will be requested when software keyboard has shown.
- if (ic == null) {
+ if (ics.isEmpty()) {
focusedRect?.let {
// Notice that view.requestRectangleOnScreen may modify the input Rect, we have to
// create another Rect and then pass it.
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
index fe922bc..9b725ee 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
@@ -167,6 +167,10 @@
override val viewRoot: View get() = this
+ init {
+ clipChildren = false
+ }
+
var factory: ((Context) -> T)? = null
set(value) {
field = value
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt
index 6f7cfe9..2a8772f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt
@@ -18,6 +18,7 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.node.LayoutNodeWrapper
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.internal.JvmDefaultWithCompatibility
@@ -93,6 +94,12 @@
fun localBoundingBoxOf(sourceCoordinates: LayoutCoordinates, clipBounds: Boolean = true): Rect
/**
+ * Modifies [matrix] to be a transform to convert a coordinate in [sourceCoordinates]
+ * to a coordinate in `this` [LayoutCoordinates].
+ */
+ fun transformFrom(sourceCoordinates: LayoutCoordinates, matrix: Matrix) {}
+
+ /**
* Returns the position in pixels of an [alignment line][AlignmentLine],
* or [AlignmentLine.Unspecified] if the line is not provided.
*/
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutInfo.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutInfo.kt
index 571029f..cb2fd62 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutInfo.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutInfo.kt
@@ -77,6 +77,11 @@
* Returns true if this layout is currently a part of the layout tree.
*/
val isAttached: Boolean
+
+ /**
+ * Unique and stable id representing this node to the semantics system.
+ */
+ val semanticsId: Int
}
/**
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt
index 828213c..dc47e9e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt
@@ -21,6 +21,7 @@
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.node.LayoutNodeWrapper
import androidx.compose.ui.node.LookaheadDelegate
import androidx.compose.ui.unit.IntSize
@@ -107,6 +108,10 @@
clipBounds: Boolean
): Rect = wrapper.localBoundingBoxOf(sourceCoordinates, clipBounds)
+ override fun transformFrom(sourceCoordinates: LayoutCoordinates, matrix: Matrix) {
+ wrapper.transformFrom(sourceCoordinates, matrix)
+ }
+
override fun get(alignmentLine: AlignmentLine): Int = wrapper.get(alignmentLine)
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index c973b88..a5b4070 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -52,6 +52,7 @@
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.platform.simpleIdentityToString
import androidx.compose.ui.semantics.SemanticsEntity
+import androidx.compose.ui.semantics.SemanticsModifierCore.Companion.generateSemanticsId
import androidx.compose.ui.semantics.outerSemantics
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
@@ -74,7 +75,9 @@
// virtual nodes will be treated as the direct children of the virtual node parent.
// This whole concept will be replaced with a proper subcomposition logic which allows to
// subcompose multiple times into the same LayoutNode and define offsets.
- private val isVirtual: Boolean = false
+ private val isVirtual: Boolean = false,
+ // The unique semantics ID that is used by all semantics modifiers attached to this LayoutNode.
+ override val semanticsId: Int = generateSemanticsId()
) : Remeasurement, OwnerScope, LayoutInfo, ComposeUiNode,
Owner.OnLayoutCompletedListener {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
index 6191cb2..34fef50 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
@@ -31,6 +31,7 @@
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.DefaultCameraDistance
import androidx.compose.ui.graphics.GraphicsLayerScope
+import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.ReusableGraphicsLayerScope
import androidx.compose.ui.graphics.TransformOrigin
@@ -717,6 +718,43 @@
return ancestorToLocal(commonAncestor, position)
}
+ override fun transformFrom(sourceCoordinates: LayoutCoordinates, matrix: Matrix) {
+ val layoutNodeWrapper = sourceCoordinates.toWrapper()
+ val commonAncestor = findCommonAncestor(layoutNodeWrapper)
+
+ matrix.reset()
+ // Transform from the source to the common ancestor
+ layoutNodeWrapper.transformToAncestor(commonAncestor, matrix)
+ // Transform from the common ancestor to this
+ transformFromAncestor(commonAncestor, matrix)
+ }
+
+ private fun transformToAncestor(ancestor: LayoutNodeWrapper, matrix: Matrix) {
+ var wrapper = this
+ while (wrapper != ancestor) {
+ wrapper.layer?.transform(matrix)
+ val position = wrapper.position
+ if (position != IntOffset.Zero) {
+ tmpMatrix.reset()
+ tmpMatrix.translate(position.x.toFloat(), position.y.toFloat())
+ matrix.timesAssign(tmpMatrix)
+ }
+ wrapper = wrapper.wrappedBy!!
+ }
+ }
+
+ private fun transformFromAncestor(ancestor: LayoutNodeWrapper, matrix: Matrix) {
+ if (ancestor != this) {
+ wrappedBy!!.transformFromAncestor(ancestor, matrix)
+ if (position != IntOffset.Zero) {
+ tmpMatrix.reset()
+ tmpMatrix.translate(-position.x.toFloat(), -position.y.toFloat())
+ matrix.timesAssign(tmpMatrix)
+ }
+ layer?.inverseTransform(matrix)
+ }
+ }
+
override fun localBoundingBoxOf(
sourceCoordinates: LayoutCoordinates,
clipBounds: Boolean
@@ -1139,6 +1177,10 @@
private val graphicsLayerScope = ReusableGraphicsLayerScope()
private val tmpLayerPositionalProperties = LayerPositionalProperties()
+ // Used for matrix calculations. It should not be used for anything that could lead to
+ // reentrancy.
+ private val tmpMatrix = Matrix()
+
/**
* Hit testing specifics for pointer input.
*/
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt
index 73d3ce0..a12e478 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt
@@ -121,4 +121,16 @@
* as new after this call.
*/
fun reuseLayer(drawBlock: (Canvas) -> Unit, invalidateParentLayer: () -> Unit)
+
+ /**
+ * Calculates the transform from the parent to the local coordinates and multiplies
+ * [matrix] by the transform.
+ */
+ fun transform(matrix: Matrix)
+
+ /**
+ * Calculates the transform from the layer to the parent and multiplies [matrix] by
+ * the transform.
+ */
+ fun inverseTransform(matrix: Matrix)
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsEntity.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsEntity.kt
index 164d52e..54e3dfa 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsEntity.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsEntity.kt
@@ -26,6 +26,9 @@
wrapped: LayoutNodeWrapper,
modifier: SemanticsModifier
) : LayoutNodeEntity<SemanticsEntity, SemanticsModifier>(wrapped, modifier) {
+ val id: Int
+ get() = layoutNode.semanticsId
+
private val useMinimumTouchTarget: Boolean
get() = modifier.semanticsConfiguration.getOrNull(SemanticsActions.OnClick) != null
@@ -56,7 +59,7 @@
}
override fun toString(): String {
- return "${super.toString()} id: ${modifier.id} config: ${modifier.semanticsConfiguration}"
+ return "${super.toString()} semanticsId: $id config: ${modifier.semanticsConfiguration}"
}
fun touchBoundsInRoot(): Rect {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt
index 142f6fd..de40d11 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt
@@ -16,10 +16,11 @@
package androidx.compose.ui.semantics
-import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
import androidx.compose.ui.platform.AtomicInt
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.InspectorValueInfo
+import androidx.compose.ui.platform.NoInspectorInfo
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.internal.JvmDefaultWithCompatibility
@@ -29,12 +30,12 @@
*/
@JvmDefaultWithCompatibility
interface SemanticsModifier : Modifier.Element {
- /**
- * The unique id of this semantics.
- *
- * Should be generated from SemanticsModifierCore.generateSemanticsId().
- */
- val id: Int
+ @Deprecated(
+ message = "SemanticsModifier.id is now unused and has been set to a fixed value. " +
+ "Retrieve the id from LayoutInfo instead.",
+ replaceWith = ReplaceWith("")
+ )
+ val id: Int get() = -1
/**
* The SemanticsConfiguration holds substantive data, especially a list of key/value pairs
@@ -44,18 +45,18 @@
}
internal class SemanticsModifierCore(
- override val id: Int,
mergeDescendants: Boolean,
clearAndSetSemantics: Boolean,
- properties: (SemanticsPropertyReceiver.() -> Unit)
-) : SemanticsModifier {
+ properties: (SemanticsPropertyReceiver.() -> Unit),
+ inspectorInfo: InspectorInfo.() -> Unit = NoInspectorInfo
+) : SemanticsModifier, InspectorValueInfo(inspectorInfo) {
override val semanticsConfiguration: SemanticsConfiguration =
SemanticsConfiguration().also {
it.isMergingSemanticsOfDescendants = mergeDescendants
it.isClearingSemantics = clearAndSetSemantics
-
it.properties()
}
+
companion object {
private var lastIdentifier = AtomicInt(0)
fun generateSemanticsId() = lastIdentifier.addAndGet(1)
@@ -64,15 +65,12 @@
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is SemanticsModifierCore) return false
-
- if (id != other.id) return false
if (semanticsConfiguration != other.semanticsConfiguration) return false
-
return true
}
override fun hashCode(): Int {
- return 31 * semanticsConfiguration.hashCode() + id.hashCode()
+ return semanticsConfiguration.hashCode()
}
}
@@ -109,16 +107,16 @@
fun Modifier.semantics(
mergeDescendants: Boolean = false,
properties: (SemanticsPropertyReceiver.() -> Unit)
-): Modifier = composed(
+): Modifier = this then SemanticsModifierCore(
+ mergeDescendants = mergeDescendants,
+ clearAndSetSemantics = false,
+ properties = properties,
inspectorInfo = debugInspectorInfo {
name = "semantics"
this.properties["mergeDescendants"] = mergeDescendants
this.properties["properties"] = properties
}
-) {
- val id = remember { SemanticsModifierCore.generateSemanticsId() }
- SemanticsModifierCore(id, mergeDescendants, clearAndSetSemantics = false, properties)
-}
+)
/**
* Clears the semantics of all the descendant nodes and sets new semantics.
@@ -137,12 +135,12 @@
*/
fun Modifier.clearAndSetSemantics(
properties: (SemanticsPropertyReceiver.() -> Unit)
-): Modifier = composed(
+): Modifier = this then SemanticsModifierCore(
+ mergeDescendants = false,
+ clearAndSetSemantics = true,
+ properties = properties,
inspectorInfo = debugInspectorInfo {
name = "clearAndSetSemantics"
this.properties["properties"] = properties
}
-) {
- val id = remember { SemanticsModifierCore.generateSemanticsId() }
- SemanticsModifierCore(id, mergeDescendants = false, clearAndSetSemantics = true, properties)
-}
+)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
index 1701b58..1a45eb5d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
@@ -64,7 +64,6 @@
private var fakeNodeParent: SemanticsNode? = null
internal val unmergedConfig = outerSemanticsEntity.collapsedSemanticsConfiguration()
- val id: Int = outerSemanticsEntity.modifier.id
/**
* The [LayoutInfo] that this is associated with.
@@ -81,6 +80,8 @@
*/
internal val layoutNode: LayoutNode = outerSemanticsEntity.layoutNode
+ val id: Int = layoutNode.semanticsId
+
// GEOMETRY
/**
@@ -379,9 +380,12 @@
): SemanticsNode {
val fakeNode = SemanticsNode(
outerSemanticsEntity = SemanticsEntity(
- wrapped = LayoutNode(isVirtual = true).innerLayoutNodeWrapper,
+ wrapped = LayoutNode(
+ isVirtual = true,
+ semanticsId =
+ if (role != null) roleFakeNodeId() else contentDescriptionFakeNodeId()
+ ).innerLayoutNodeWrapper,
modifier = SemanticsModifierCore(
- if (role != null) this.roleFakeNodeId() else contentDescriptionFakeNodeId(),
mergeDescendants = false,
clearAndSetSemantics = false,
properties = properties
diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
index c672f75..b420d49 100644
--- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
+++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
@@ -115,7 +115,6 @@
override val sharedDrawScope = LayoutNodeDrawScope()
private val semanticsModifier = SemanticsModifierCore(
- id = SemanticsModifierCore.generateSemanticsId(),
mergeDescendants = false,
clearAndSetSemantics = false,
properties = {}
diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaLayer.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaLayer.skiko.kt
index b219330..49c7b5d 100644
--- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaLayer.skiko.kt
+++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaLayer.skiko.kt
@@ -232,6 +232,14 @@
canvas.restore()
}
+ override fun transform(matrix: Matrix) {
+ matrix.timesAssign(getMatrix(inverse = false))
+ }
+
+ override fun inverseTransform(matrix: Matrix) {
+ matrix.timesAssign(getMatrix(inverse = true))
+ }
+
private fun performDrawLayer(canvas: Canvas, bounds: Rect) {
if (alpha > 0) {
if (shadowElevation > 0) {
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index b561f1d..cd84292 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -28,6 +28,7 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.RenderEffect
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.TransformOrigin
@@ -46,6 +47,7 @@
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.RootMeasurePolicy.measure
import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.AccessibilityManager
@@ -53,6 +55,7 @@
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
+import androidx.compose.ui.platform.invertTo
import androidx.compose.ui.semantics.SemanticsConfiguration
import androidx.compose.ui.semantics.SemanticsEntity
import androidx.compose.ui.semantics.SemanticsModifier
@@ -824,6 +827,204 @@
}
@Test
+ fun layoutNodeWrapper_transformFrom_offsets() {
+ val parent = ZeroSizedLayoutNode()
+ parent.attach(MockOwner())
+ val child = ZeroSizedLayoutNode()
+ parent.insertAt(0, child)
+ parent.place(-100, 10)
+ child.place(50, 80)
+
+ val matrix = Matrix()
+ child.innerLayoutNodeWrapper.transformFrom(parent.innerLayoutNodeWrapper, matrix)
+
+ assertEquals(Offset(-50f, -80f), matrix.map(Offset.Zero))
+
+ parent.innerLayoutNodeWrapper.transformFrom(child.innerLayoutNodeWrapper, matrix)
+
+ assertEquals(Offset(50f, 80f), matrix.map(Offset.Zero))
+ }
+
+ @Test
+ fun layoutNodeWrapper_transformFrom_translation() {
+ val parent = ZeroSizedLayoutNode()
+ parent.attach(MockOwner())
+ val child = ZeroSizedLayoutNode()
+ parent.insertAt(0, child)
+ child.modifier = Modifier.graphicsLayer {
+ translationX = 5f
+ translationY = 2f
+ }
+ parent.outerLayoutNodeWrapper.measureScope
+ .measure(listOf(parent.outerLayoutNodeWrapper), Constraints())
+ child.outerLayoutNodeWrapper
+ .measureScope.measure(listOf(child.outerLayoutNodeWrapper), Constraints())
+ parent.place(0, 0)
+ child.place(0, 0)
+
+ val matrix = Matrix()
+ child.innerLayoutNodeWrapper.transformFrom(parent.innerLayoutNodeWrapper, matrix)
+
+ assertEquals(-5f, matrix.map(Offset.Zero).x, 0.001f)
+ assertEquals(-2f, matrix.map(Offset.Zero).y, 0.001f)
+
+ parent.innerLayoutNodeWrapper.transformFrom(child.innerLayoutNodeWrapper, matrix)
+
+ assertEquals(5f, matrix.map(Offset.Zero).x, 0.001f)
+ assertEquals(2f, matrix.map(Offset.Zero).y, 0.001f)
+ }
+
+ @Test
+ fun layoutNodeWrapper_transformFrom_rotation() {
+ val parent = ZeroSizedLayoutNode()
+ parent.attach(MockOwner())
+ val child = ZeroSizedLayoutNode()
+ parent.insertAt(0, child)
+ child.modifier = Modifier.graphicsLayer {
+ rotationZ = 90f
+ }
+ parent.outerLayoutNodeWrapper.measureScope
+ .measure(listOf(parent.outerLayoutNodeWrapper), Constraints())
+ child.outerLayoutNodeWrapper
+ .measureScope.measure(listOf(child.outerLayoutNodeWrapper), Constraints())
+ parent.place(0, 0)
+ child.place(0, 0)
+
+ val matrix = Matrix()
+ child.innerLayoutNodeWrapper.transformFrom(parent.innerLayoutNodeWrapper, matrix)
+
+ assertEquals(0f, matrix.map(Offset(1f, 0f)).x, 0.001f)
+ assertEquals(-1f, matrix.map(Offset(1f, 0f)).y, 0.001f)
+
+ parent.innerLayoutNodeWrapper.transformFrom(child.innerLayoutNodeWrapper, matrix)
+
+ assertEquals(0f, matrix.map(Offset(1f, 0f)).x, 0.001f)
+ assertEquals(1f, matrix.map(Offset(1f, 0f)).y, 0.001f)
+ }
+
+ @Test
+ fun layoutNodeWrapper_transformFrom_scale() {
+ val parent = ZeroSizedLayoutNode()
+ parent.attach(MockOwner())
+ val child = ZeroSizedLayoutNode()
+ parent.insertAt(0, child)
+ child.modifier = Modifier.graphicsLayer {
+ scaleX = 0f
+ }
+ parent.outerLayoutNodeWrapper.measureScope
+ .measure(listOf(parent.outerLayoutNodeWrapper), Constraints())
+ child.outerLayoutNodeWrapper
+ .measureScope.measure(listOf(child.outerLayoutNodeWrapper), Constraints())
+ parent.place(0, 0)
+ child.place(0, 0)
+
+ val matrix = Matrix()
+ child.innerLayoutNodeWrapper.transformFrom(parent.innerLayoutNodeWrapper, matrix)
+
+ // The X coordinate is somewhat nonsensical since it is scaled to 0
+ // We've chosen to make it not transform when there's a nonsensical inverse.
+ assertEquals(1f, matrix.map(Offset(1f, 1f)).x, 0.001f)
+ assertEquals(1f, matrix.map(Offset(1f, 1f)).y, 0.001f)
+
+ parent.innerLayoutNodeWrapper.transformFrom(child.innerLayoutNodeWrapper, matrix)
+
+ // This direction works, so we can expect the normal scaling
+ assertEquals(0f, matrix.map(Offset(1f, 1f)).x, 0.001f)
+ assertEquals(1f, matrix.map(Offset(1f, 1f)).y, 0.001f)
+
+ child.innerLayoutNodeWrapper.onLayerBlockUpdated {
+ scaleX = 0.5f
+ scaleY = 0.25f
+ }
+
+ child.innerLayoutNodeWrapper.transformFrom(parent.innerLayoutNodeWrapper, matrix)
+
+ assertEquals(2f, matrix.map(Offset(1f, 1f)).x, 0.001f)
+ assertEquals(4f, matrix.map(Offset(1f, 1f)).y, 0.001f)
+
+ parent.innerLayoutNodeWrapper.transformFrom(child.innerLayoutNodeWrapper, matrix)
+
+ assertEquals(0.5f, matrix.map(Offset(1f, 1f)).x, 0.001f)
+ assertEquals(0.25f, matrix.map(Offset(1f, 1f)).y, 0.001f)
+ }
+
+ @Test
+ fun layoutNodeWrapper_transformFrom_siblings() {
+ val parent = ZeroSizedLayoutNode()
+ parent.attach(MockOwner())
+ val child1 = ZeroSizedLayoutNode()
+ parent.insertAt(0, child1)
+ child1.modifier = Modifier.graphicsLayer {
+ scaleX = 0.5f
+ scaleY = 0.25f
+ transformOrigin = TransformOrigin(0f, 0f)
+ }
+ val child2 = ZeroSizedLayoutNode()
+ parent.insertAt(0, child2)
+ child2.modifier = Modifier.graphicsLayer {
+ scaleX = 5f
+ scaleY = 2f
+ transformOrigin = TransformOrigin(0f, 0f)
+ }
+ parent.outerLayoutNodeWrapper.measureScope
+ .measure(listOf(parent.outerLayoutNodeWrapper), Constraints())
+ child1.outerLayoutNodeWrapper
+ .measureScope.measure(listOf(child1.outerLayoutNodeWrapper), Constraints())
+ child2.outerLayoutNodeWrapper
+ .measureScope.measure(listOf(child2.outerLayoutNodeWrapper), Constraints())
+ parent.place(0, 0)
+ child1.place(100, 200)
+ child2.place(5, 11)
+
+ val matrix = Matrix()
+ child2.innerLayoutNodeWrapper.transformFrom(child1.innerLayoutNodeWrapper, matrix)
+
+ // (20, 36) should be (10, 9) in real coordinates due to scaling
+ // Translate to (110, 209) in the parent
+ // Translate to (105, 198) in child2's coordinates, discounting scale
+ // Scaled to (21, 99)
+ val offset = matrix.map(Offset(20f, 36f))
+ assertEquals(21f, offset.x, 0.001f)
+ assertEquals(99f, offset.y, 0.001f)
+
+ child1.innerLayoutNodeWrapper.transformFrom(child2.innerLayoutNodeWrapper, matrix)
+ val offset2 = matrix.map(Offset(21f, 99f))
+ assertEquals(20f, offset2.x, 0.001f)
+ assertEquals(36f, offset2.y, 0.001f)
+ }
+
+ @Test
+ fun layoutNodeWrapper_transformFrom_cousins() {
+ val parent = ZeroSizedLayoutNode()
+ parent.attach(MockOwner())
+ val child1 = ZeroSizedLayoutNode()
+ parent.insertAt(0, child1)
+ val child2 = ZeroSizedLayoutNode()
+ parent.insertAt(1, child2)
+
+ val grandChild1 = ZeroSizedLayoutNode()
+ child1.insertAt(0, grandChild1)
+ val grandChild2 = ZeroSizedLayoutNode()
+ child2.insertAt(0, grandChild2)
+
+ parent.place(-100, 10)
+ child1.place(10, 11)
+ child2.place(22, 33)
+ grandChild1.place(45, 27)
+ grandChild2.place(17, 59)
+
+ val matrix = Matrix()
+ grandChild1.innerLayoutNodeWrapper.transformFrom(grandChild2.innerLayoutNodeWrapper, matrix)
+
+ // (17, 59) + (22, 33) - (10, 11) - (45, 27) = (-16, 54)
+ assertEquals(Offset(-16f, 54f), matrix.map(Offset.Zero))
+
+ grandChild2.innerLayoutNodeWrapper.transformFrom(grandChild1.innerLayoutNodeWrapper, matrix)
+
+ assertEquals(Offset(16f, -54f), matrix.map(Offset.Zero))
+ }
+
+ @Test
fun hitTest_pointerInBounds_pointerInputFilterHit() {
val pointerInputFilter: PointerInputFilter = mockPointerInputFilter()
val layoutNode =
@@ -1134,7 +1335,6 @@
fun hitTestSemantics_pointerInMinimumTouchTarget_pointerInputFilterHit() {
val semanticsConfiguration = SemanticsConfiguration()
val semanticsModifier = object : SemanticsModifier {
- override val id: Int = 1
override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
}
val layoutNode =
@@ -1157,7 +1357,6 @@
fun hitTestSemantics_pointerInMinimumTouchTarget_pointerInputFilterHit_nestedNodes() {
val semanticsConfiguration = SemanticsConfiguration()
val semanticsModifier = object : SemanticsModifier {
- override val id: Int = 1
override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
}
val outerNode = LayoutNode(0, 0, 1, 1).apply { attach(MockOwner()) }
@@ -1176,11 +1375,9 @@
fun hitTestSemantics_pointerInMinimumTouchTarget_closestHit() {
val semanticsConfiguration = SemanticsConfiguration()
val semanticsModifier1 = object : SemanticsModifier {
- override val id: Int = 1
override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
}
val semanticsModifier2 = object : SemanticsModifier {
- override val id: Int = 1
override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
}
val layoutNode1 = LayoutNode(0, 0, 5, 5, semanticsModifier1, DpSize(48.dp, 48.dp))
@@ -1238,11 +1435,9 @@
fun hitTestSemantics_pointerInMinimumTouchTarget_closestHitWithOverlap() {
val semanticsConfiguration = SemanticsConfiguration()
val semanticsModifier1 = object : SemanticsModifier {
- override val id: Int = 1
override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
}
val semanticsModifier2 = object : SemanticsModifier {
- override val id: Int = 1
override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
}
val layoutNode1 = LayoutNode(0, 0, 5, 5, semanticsModifier1, DpSize(48.dp, 48.dp))
@@ -2384,6 +2579,8 @@
drawBlock: (Canvas) -> Unit,
invalidateParentLayer: () -> Unit
): OwnedLayer {
+ val transform = Matrix()
+ val inverseTransform = Matrix()
return object : OwnedLayer {
override fun updateLayerProperties(
scaleX: Float,
@@ -2405,6 +2602,12 @@
layoutDirection: LayoutDirection,
density: Density
) {
+ transform.reset()
+ // This is not expected to be 100% accurate
+ transform.scale(scaleX, scaleY)
+ transform.rotateZ(rotationZ)
+ transform.translate(translationX, translationY)
+ transform.invertTo(inverseTransform)
}
override fun isInLayer(position: Offset) = true
@@ -2437,6 +2640,14 @@
) {
}
+ override fun transform(matrix: Matrix) {
+ matrix.timesAssign(transform)
+ }
+
+ override fun inverseTransform(matrix: Matrix) {
+ matrix.timesAssign(inverseTransform)
+ }
+
override fun mapOffset(point: Offset, inverse: Boolean) = point
}
}
diff --git a/datastore/datastore-core-okio/api/current.txt b/datastore/datastore-core-okio/api/current.txt
new file mode 100644
index 0000000..bbcb949
--- /dev/null
+++ b/datastore/datastore-core-okio/api/current.txt
@@ -0,0 +1,17 @@
+// Signature format: 4.0
+package androidx.datastore.core.okio {
+
+ public interface OkioSerializer<T> {
+ method public T! getDefaultValue();
+ method public suspend Object? readFrom(okio.BufferedSource source, kotlin.coroutines.Continuation<? super T>);
+ method public suspend Object? writeTo(T? t, okio.BufferedSink sink, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ property public abstract T! defaultValue;
+ }
+
+ public final class OkioStorage<T> implements androidx.datastore.core.Storage<T> {
+ ctor public OkioStorage(okio.FileSystem fileSystem, androidx.datastore.core.okio.OkioSerializer<T> serializer, kotlin.jvm.functions.Function0<okio.Path> producePath);
+ method public androidx.datastore.core.StorageConnection<T> createConnection();
+ }
+
+}
+
diff --git a/datastore/datastore-core-okio/api/public_plus_experimental_current.txt b/datastore/datastore-core-okio/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..bbcb949
--- /dev/null
+++ b/datastore/datastore-core-okio/api/public_plus_experimental_current.txt
@@ -0,0 +1,17 @@
+// Signature format: 4.0
+package androidx.datastore.core.okio {
+
+ public interface OkioSerializer<T> {
+ method public T! getDefaultValue();
+ method public suspend Object? readFrom(okio.BufferedSource source, kotlin.coroutines.Continuation<? super T>);
+ method public suspend Object? writeTo(T? t, okio.BufferedSink sink, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ property public abstract T! defaultValue;
+ }
+
+ public final class OkioStorage<T> implements androidx.datastore.core.Storage<T> {
+ ctor public OkioStorage(okio.FileSystem fileSystem, androidx.datastore.core.okio.OkioSerializer<T> serializer, kotlin.jvm.functions.Function0<okio.Path> producePath);
+ method public androidx.datastore.core.StorageConnection<T> createConnection();
+ }
+
+}
+
diff --git a/datastore/datastore-core-okio/api/restricted_current.txt b/datastore/datastore-core-okio/api/restricted_current.txt
new file mode 100644
index 0000000..bbcb949
--- /dev/null
+++ b/datastore/datastore-core-okio/api/restricted_current.txt
@@ -0,0 +1,17 @@
+// Signature format: 4.0
+package androidx.datastore.core.okio {
+
+ public interface OkioSerializer<T> {
+ method public T! getDefaultValue();
+ method public suspend Object? readFrom(okio.BufferedSource source, kotlin.coroutines.Continuation<? super T>);
+ method public suspend Object? writeTo(T? t, okio.BufferedSink sink, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ property public abstract T! defaultValue;
+ }
+
+ public final class OkioStorage<T> implements androidx.datastore.core.Storage<T> {
+ ctor public OkioStorage(okio.FileSystem fileSystem, androidx.datastore.core.okio.OkioSerializer<T> serializer, kotlin.jvm.functions.Function0<okio.Path> producePath);
+ method public androidx.datastore.core.StorageConnection<T> createConnection();
+ }
+
+}
+
diff --git a/datastore/datastore-core-okio/build.gradle b/datastore/datastore-core-okio/build.gradle
index 9054366..a6755ca 100644
--- a/datastore/datastore-core-okio/build.gradle
+++ b/datastore/datastore-core-okio/build.gradle
@@ -99,9 +99,6 @@
androidx {
name = "Android DataStore Core Okio"
- // temporarily disabled for parity with state prior to library type refactor b/235209373
- publish = Publish.NONE
- runApiTasks = new RunApiTasks.No("Temporarily disabled, but should be re-enabled b/235209373")
type = LibraryType.KMP_LIBRARY
mavenGroup = LibraryGroups.DATASTORE
inceptionYear = "2020"
diff --git a/datastore/datastore-core-okio/src/commonMain/kotlin/androidx/datastore/core/okio/OkioSerializer.kt b/datastore/datastore-core-okio/src/commonMain/kotlin/androidx/datastore/core/okio/OkioSerializer.kt
index f7728d2..6b969da 100644
--- a/datastore/datastore-core-okio/src/commonMain/kotlin/androidx/datastore/core/okio/OkioSerializer.kt
+++ b/datastore/datastore-core-okio/src/commonMain/kotlin/androidx/datastore/core/okio/OkioSerializer.kt
@@ -25,19 +25,19 @@
* The type T MUST be immutable. Mutable types will result in broken DataStore functionality.
*
*/
-interface OkioSerializer<T> {
+public interface OkioSerializer<T> {
/**
* Value to return if there is no data on disk.
*/
- val defaultValue: T
+ public val defaultValue: T
/**
* Unmarshal object from source.
*
* @param source the BufferedSource with the data to deserialize
*/
- suspend fun readFrom(source: BufferedSource): T
+ public suspend fun readFrom(source: BufferedSource): T
/**
* Marshal object to a Sink.
@@ -45,5 +45,5 @@
* @param t the data to write to output
* @output the BufferedSink to serialize data to
*/
- suspend fun writeTo(t: T, sink: BufferedSink)
+ public suspend fun writeTo(t: T, sink: BufferedSink)
}
\ No newline at end of file
diff --git a/datastore/datastore-core-okio/src/commonMain/kotlin/androidx/datastore/core/okio/OkioStorage.kt b/datastore/datastore-core-okio/src/commonMain/kotlin/androidx/datastore/core/okio/OkioStorage.kt
index a1cfbfbe..a3b6bcb 100644
--- a/datastore/datastore-core-okio/src/commonMain/kotlin/androidx/datastore/core/okio/OkioStorage.kt
+++ b/datastore/datastore-core-okio/src/commonMain/kotlin/androidx/datastore/core/okio/OkioStorage.kt
@@ -36,7 +36,7 @@
/**
* OKIO implementation of the Storage interface, providing cross platform IO using the OKIO library.
*/
-class OkioStorage<T>(
+public class OkioStorage<T>(
private val fileSystem: FileSystem,
private val serializer: OkioSerializer<T>,
private val producePath: () -> Path
diff --git a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/SharedCounterTest.kt b/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/SharedCounterTest.kt
index e3d6b20..707ae1b 100644
--- a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/SharedCounterTest.kt
+++ b/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/SharedCounterTest.kt
@@ -54,12 +54,18 @@
}
@Test
- fun testCreate_success() {
+ fun testCreate_success_enableMlock() {
val counter: SharedCounter = SharedCounter.create { testFile }
assertThat(counter).isNotNull()
}
@Test
+ fun testCreate_success_disableMlock() {
+ val counter: SharedCounter = SharedCounter.create(false) { testFile }
+ assertThat(counter).isNotNull()
+ }
+
+ @Test
fun testCreate_failure() {
val tempFile = tempFolder.newFile()
tempFile.setReadable(false)
@@ -72,13 +78,13 @@
@Test
fun testGetValue() {
- val counter: SharedCounter = SharedCounter.create { testFile }
+ val counter: SharedCounter = SharedCounter.create(false) { testFile }
assertThat(counter.getValue()).isEqualTo(0)
}
@Test
fun testIncrementAndGet() {
- val counter: SharedCounter = SharedCounter.create { testFile }
+ val counter: SharedCounter = SharedCounter.create(false) { testFile }
for (count in 1..100) {
assertThat(counter.incrementAndGetValue()).isEqualTo(count)
}
@@ -86,7 +92,7 @@
@Test
fun testIncrementInParallel() = runTest {
- val counter: SharedCounter = SharedCounter.create { testFile }
+ val counter: SharedCounter = SharedCounter.create(false) { testFile }
val valueToAdd = 100
val numCoroutines = 10
val numbers: MutableSet<Int> = mutableSetOf()
@@ -105,4 +111,20 @@
assertThat(numbers).contains(num)
}
}
+
+ @Test
+ fun testManyInstancesWithMlockDisabled() = runTest {
+ // More than 16
+ val numCoroutines = 5000
+ val counters = mutableListOf<SharedCounter>()
+ val deferred = async {
+ repeat(numCoroutines) {
+ val tempFile = tempFolder.newFile()
+ val counter = SharedCounter.create(false) { tempFile }
+ assertThat(counter.getValue()).isEqualTo(0)
+ counters.add(counter)
+ }
+ }
+ deferred.await()
+ }
}
\ No newline at end of file
diff --git a/datastore/datastore-multiprocess/src/main/cpp/jni/androidx_datastore_multiprocess_SharedCounter.cc b/datastore/datastore-multiprocess/src/main/cpp/jni/androidx_datastore_multiprocess_SharedCounter.cc
index 892157e..61cddca 100644
--- a/datastore/datastore-multiprocess/src/main/cpp/jni/androidx_datastore_multiprocess_SharedCounter.cc
+++ b/datastore/datastore-multiprocess/src/main/cpp/jni/androidx_datastore_multiprocess_SharedCounter.cc
@@ -35,10 +35,19 @@
extern "C" {
JNIEXPORT jlong JNICALL
-Java_androidx_datastore_multiprocess_NativeSharedCounter_nativeCreateSharedCounter(
+Java_androidx_datastore_multiprocess_NativeSharedCounter_nativeTruncateFile(
JNIEnv *env, jclass clazz, jint fd) {
+ if (int errNum = datastore::TruncateFile(fd)) {
+ return ThrowIoException(env, strerror(errNum));
+ }
+ return 0;
+}
+
+JNIEXPORT jlong JNICALL
+Java_androidx_datastore_multiprocess_NativeSharedCounter_nativeCreateSharedCounter(
+ JNIEnv *env, jclass clazz, jint fd, jboolean enable_mlock) {
void* address = nullptr;
- if (int errNum = datastore::CreateSharedCounter(fd, &address)) {
+ if (int errNum = datastore::CreateSharedCounter(fd, &address, enable_mlock)) {
return ThrowIoException(env, strerror(errNum));
}
return reinterpret_cast<jlong>(address);
diff --git a/datastore/datastore-multiprocess/src/main/cpp/shared_counter.cc b/datastore/datastore-multiprocess/src/main/cpp/shared_counter.cc
index 9dcf9da..68f5ce1 100644
--- a/datastore/datastore-multiprocess/src/main/cpp/shared_counter.cc
+++ b/datastore/datastore-multiprocess/src/main/cpp/shared_counter.cc
@@ -39,16 +39,24 @@
namespace datastore {
+int TruncateFile(int fd) {
+ return (ftruncate(fd, NUM_BYTES) == 0) ? 0 : errno;
+}
+
/*
- * This function returns non-zero errno if fails to create the counter. Caller should use
- * "strerror(errno)" to get error message.
+ * This function returns non-zero errno if fails to create the counter. Caller should have called
+ * "TruncateFile" before calling this method. Caller should use "strerror(errno)" to get error
+ * message.
*/
-int CreateSharedCounter(int fd, void** counter_address) {
- if (ftruncate(fd, NUM_BYTES) != 0) {
- return errno;
- }
- void* mmap_result = mmap(nullptr, NUM_BYTES, PROT_READ | PROT_WRITE,
- MAP_SHARED | MAP_LOCKED, fd, 0);
+int CreateSharedCounter(int fd, void** counter_address, bool enable_mlock) {
+ // Map with MAP_SHARED so the memory region is shared with other processes.
+ // MAP_LOCKED may cause memory starvation (b/233902124) so is configurable.
+ int map_flags = MAP_SHARED;
+ // TODO(b/233902124): the impact of MAP_POPULATE is still unclear, experiment
+ // with it when possible.
+ map_flags |= enable_mlock ? MAP_LOCKED : MAP_POPULATE;
+
+ void* mmap_result = mmap(nullptr, NUM_BYTES, PROT_READ | PROT_WRITE, map_flags, fd, 0);
if (mmap_result == MAP_FAILED) {
return errno;
diff --git a/datastore/datastore-multiprocess/src/main/cpp/shared_counter.h b/datastore/datastore-multiprocess/src/main/cpp/shared_counter.h
index 756e2fe..cf73095 100644
--- a/datastore/datastore-multiprocess/src/main/cpp/shared_counter.h
+++ b/datastore/datastore-multiprocess/src/main/cpp/shared_counter.h
@@ -21,7 +21,8 @@
#define DATASTORE_SHARED_COUNTER_H
namespace datastore {
-int CreateSharedCounter(int fd, void** counter_address);
+int TruncateFile(int fd);
+int CreateSharedCounter(int fd, void** counter_address, bool enable_mlock);
uint32_t GetCounterValue(std::atomic<uint32_t>* counter);
uint32_t IncrementAndGetCounterValue(std::atomic<uint32_t>* counter);
} // namespace datastore
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt
index eb64a3c..e01662d 100644
--- a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt
+++ b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt
@@ -25,7 +25,8 @@
* Put the JNI methods in a separate class to make them internal to the package.
*/
internal class NativeSharedCounter {
- external fun nativeCreateSharedCounter(fd: Int): Long
+ external fun nativeTruncateFile(fd: Int): Int
+ external fun nativeCreateSharedCounter(fd: Int, enableMlock: Boolean): Long
external fun nativeGetCounterValue(address: Long): Int
external fun nativeIncrementAndGetCounterValue(address: Long): Int
}
@@ -57,22 +58,28 @@
fun loadLib() = System.loadLibrary("datastore_shared_counter")
@SuppressLint("SyntheticAccessor")
- private fun createCounterFromFd(pfd: ParcelFileDescriptor): SharedCounter {
+ private fun createCounterFromFd(
+ pfd: ParcelFileDescriptor,
+ enableMlock: Boolean
+ ): SharedCounter {
val nativeFd = pfd.getFd()
- val address = nativeSharedCounter.nativeCreateSharedCounter(nativeFd)
+ if (nativeSharedCounter.nativeTruncateFile(nativeFd) != 0) {
+ throw IOException("Failed to truncate counter file")
+ }
+ val address = nativeSharedCounter.nativeCreateSharedCounter(nativeFd, enableMlock)
if (address < 0) {
- throw IOException("Failed to mmap or truncate counter file")
+ throw IOException("Failed to mmap counter file")
}
return SharedCounter(address)
}
- internal fun create(produceFile: () -> File): SharedCounter {
+ internal fun create(enableMlock: Boolean = true, produceFile: () -> File): SharedCounter {
val file = produceFile()
return ParcelFileDescriptor.open(
file,
ParcelFileDescriptor.MODE_READ_WRITE or ParcelFileDescriptor.MODE_CREATE
).use {
- createCounterFromFd(it)
+ createCounterFromFd(it, enableMlock)
}
}
}
diff --git a/development/JetpadClient.py b/development/JetpadClient.py
index 1e15f07..19b74bc 100644
--- a/development/JetpadClient.py
+++ b/development/JetpadClient.py
@@ -33,7 +33,7 @@
return None
rawJetpadReleaseOutputLines = rawJetpadReleaseOutput.splitlines()
if len(rawJetpadReleaseOutputLines) <= 2:
- print_e("Error: Date %s returned zero results from Jetpad. Please check your date" % args.date)
+ print_e("Error: Date %s returned zero results from Jetpad. Please check your date" % date)
return None
jetpadReleaseOutput = iter(rawJetpadReleaseOutputLines)
return jetpadReleaseOutput
diff --git a/development/auto-version-updater/update_versions_for_release.py b/development/auto-version-updater/update_versions_for_release.py
index 013c163..8ce954a 100755
--- a/development/auto-version-updater/update_versions_for_release.py
+++ b/development/auto-version-updater/update_versions_for_release.py
@@ -19,10 +19,6 @@
import argparse
from datetime import date
import subprocess
-from shutil import rmtree
-from shutil import copyfile
-from distutils.dir_util import copy_tree
-from distutils.dir_util import DistutilsFileError
import toml
# Import the JetpadClient from the parent directory
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 36e5380..95b9e85 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -299,6 +299,7 @@
# > Task :glance:glance:reportLibraryMetrics
Info: Stripped invalid locals information from [0-9]+ methods?\.
Info: Methods with invalid locals information:
+void androidx\.tv\.foundation\.lazy\.list\.LazyListKt\.LazyList\(androidx\.compose\.ui\.Modifier, androidx\.tv\.foundation\.lazy\.list\.TvLazyListState, androidx\.compose\.foundation\.layout\.PaddingValues, boolean, boolean, boolean, androidx\.tv\.foundation\.PivotOffsets, androidx\.compose\.ui\.Alignment\$Horizontal, androidx\.compose\.foundation\.layout\.Arrangement\$Vertical, androidx\.compose\.ui\.Alignment\$Vertical, androidx\.compose\.foundation\.layout\.Arrangement\$Horizontal, kotlin\.jvm\.functions\.Function[0-9]+, androidx\.compose\.runtime\.Composer, int, int, int\)
androidx\.compose\.ui\.Modifier androidx\.compose\.animation\.AnimationModifierKt\$animateContentSize\$[0-9]+\.invoke\(androidx\.compose\.ui\.Modifier, androidx\.compose\.runtime\.Composer, int\)
androidx\.compose\.ui\.Modifier androidx\.compose\.material[0-9]+\.SliderKt\$sliderTapModifier\$[0-9]+\.invoke\(androidx\.compose\.ui\.Modifier, androidx\.compose\.runtime\.Composer, int\)
androidx\.compose\.ui\.Modifier androidx\.compose\.animation\.demos\.layoutanimation\.AnimatedPlacementDemoKt\$animatePlacement\$[0-9]+\.invoke\(androidx\.compose\.ui\.Modifier, androidx\.compose\.runtime\.Composer, int\)
@@ -321,6 +322,7 @@
void androidx\.compose\.foundation\.demos\.relocation\.BringIntoViewAndroidInteropDemoKt\.BringIntoViewAndroidInteropDemo\(androidx\.compose\.runtime\.Composer, int\)
void androidx\.compose\.ui\.demos\.keyinput\.InterceptEnterToSendMessageDemoKt\.InterceptEnterToSendMessageDemo\(androidx\.compose\.runtime\.Composer, int\)
Information in locals\-table is invalid with respect to the stack map table\. Local refers to non\-present stack map type for register: [0-9]+ with constraint [\-A-Z]*\.
+void androidx\.tv\.foundation\.lazy\.grid\.LazyGridKt\.LazyGrid\(androidx\.compose\.ui\.Modifier, androidx\.tv\.foundation\.lazy\.grid\.TvLazyGridState, kotlin\.jvm\.functions\.Function[0-9]+, androidx\.compose\.foundation\.layout\.PaddingValues, boolean, boolean, boolean, androidx\.compose\.foundation\.layout\.Arrangement\$Vertical, androidx\.compose\.foundation\.layout\.Arrangement\$Horizontal, androidx\.tv\.foundation\.PivotOffsets, kotlin\.jvm\.functions\.Function[0-9]+, androidx\.compose\.runtime\.Composer, int, int, int\)
androidx\.compose\.ui\.Modifier androidx\.compose\.material\.SliderKt\$sliderTapModifier\$[0-9]+\.invoke\(androidx\.compose\.ui\.Modifier, androidx\.compose\.runtime\.Composer, int\)
androidx\.compose\.ui\.Modifier androidx\.compose\.foundation\.FocusableKt\$focusable\$[0-9]+\.invoke\(androidx\.compose\.ui\.Modifier, androidx\.compose\.runtime\.Composer, int\)
androidx\.compose\.ui\.Modifier androidx\.compose\.foundation\.ScrollKt\$scroll\$[0-9]+\.invoke\(androidx\.compose\.ui\.Modifier, androidx\.compose\.runtime\.Composer, int\)
@@ -439,6 +441,8 @@
# > Task :tv:tv-material:processReleaseManifest
# > Task :tv:tv-foundation:processReleaseManifest
package="androidx\.tv\..*" found in source AndroidManifest\.xml: \$OUT_DIR/androidx/tv/tv\-[a-z]+/build/intermediates/tmp/ProcessLibraryManifest/[a-z]+/tempAndroidManifest[0-9]+\.xml\.
+void androidx.tv.foundation.lazy.list.LazyListKt.LazyList(androidx.compose.ui.Modifier, androidx.tv.foundation.lazy.list.TvLazyListState, androidx.compose.foundation.layout.PaddingValues, boolean, boolean, boolean, androidx.tv.foundation.PivotOffsets, androidx.compose.ui.Alignment$Horizontal, androidx.compose.foundation.layout.Arrangement$Vertical, androidx.compose.ui.Alignment$Vertical, androidx.compose.foundation.layout.Arrangement$Horizontal, kotlin.jvm.functions.Function1, androidx.compose.runtime.Composer, int, int, int)
+void androidx.tv.foundation.lazy.grid.LazyGridKt.LazyGrid(androidx.compose.ui.Modifier, androidx.tv.foundation.lazy.grid.TvLazyGridState, kotlin.jvm.functions.Function2, androidx.compose.foundation.layout.PaddingValues, boolean, boolean, boolean, androidx.compose.foundation.layout.Arrangement$Vertical, androidx.compose.foundation.layout.Arrangement$Horizontal, androidx.tv.foundation.PivotOffsets, kotlin.jvm.functions.Function1, androidx.compose.runtime.Composer, int, int, int)
# > Task :room:integration-tests:room-testapp:mergeDexWithExpandProjectionDebugAndroidTest
WARNING:D[0-9]+: Application does not contain `androidx\.tracing\.Trace` as referenced in main\-dex\-list\.
# > Task :hilt:hilt-compiler:kaptTestKotlin
@@ -468,3 +472,4 @@
# > Task :buildSrc-tests:test
WARNING: Illegal reflective access using Lookup on org\.gradle\.internal\.classloader\.ClassLoaderUtils\$AbstractClassLoaderLookuper .* to class java\.lang\.ClassLoader
WARNING: Please consider reporting this to the maintainers of org\.gradle\.internal\.classloader\.ClassLoaderUtils\$AbstractClassLoaderLookuper
+
diff --git a/development/referenceDocs/stageReferenceDocsWithDackka.sh b/development/referenceDocs/stageReferenceDocsWithDackka.sh
index 91ba9a3..f12d43e 100755
--- a/development/referenceDocs/stageReferenceDocsWithDackka.sh
+++ b/development/referenceDocs/stageReferenceDocsWithDackka.sh
@@ -48,7 +48,7 @@
#
# Each directory's spelling must match the library's directory in
# frameworks/support.
-readonly javaLibraryDirs=(
+readonly javaLibraryDirsThatDontUseDackka=(
"android/support/v4"
"androidx/ads"
"androidx/appcompat"
@@ -71,7 +71,6 @@
"androidx/heifwriter"
"androidx/hilt"
"androidx/leanback"
- "androidx/loader"
"androidx/media"
"androidx/media2"
"androidx/mediarouter"
@@ -98,7 +97,7 @@
"androidx/webkit"
"androidx/work"
)
-readonly kotlinLibraryDirs=(
+readonly kotlinLibraryDirsThatDontUseDackka=(
"android/support/v4"
"androidx/ads"
"androidx/appcompat"
@@ -123,7 +122,6 @@
"androidx/heifwriter"
"androidx/hilt"
"androidx/leanback"
- "androidx/loader"
"androidx/media"
"androidx/media2"
"androidx/mediarouter"
@@ -236,13 +234,13 @@
# generated by Doclava/Dokka (such as Java refdocs based on Kotlin sources)
cd $outDir
-for dir in "${javaLibraryDirs[@]}"
+for dir in "${javaLibraryDirsThatDontUseDackka[@]}"
do
printf "Copying Java refdocs for $dir\n"
cp -r $doclavaNewDir/reference/$dir/* $newDir/reference/$dir
done
-for dir in "${kotlinLibraryDirs[@]}"
+for dir in "${kotlinLibraryDirsThatDontUseDackka[@]}"
do
printf "Copying Kotlin refdocs for $dir\n"
cp -r $dokkaNewDir/reference/kotlin/$dir/* $newDir/reference/kotlin/$dir
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 5f4aeb0..f8b0381 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -8,10 +8,10 @@
}
dependencies {
- docs("androidx.activity:activity:1.5.0")
- docs("androidx.activity:activity-compose:1.5.0")
- samples("androidx.activity:activity-compose-samples:1.5.0")
- docs("androidx.activity:activity-ktx:1.5.0")
+ docs("androidx.activity:activity:1.5.1")
+ docs("androidx.activity:activity-compose:1.5.1")
+ samples("androidx.activity:activity-compose-samples:1.5.1")
+ docs("androidx.activity:activity-ktx:1.5.1")
docs("androidx.ads:ads-identifier:1.0.0-alpha04")
docs("androidx.ads:ads-identifier-provider:1.0.0-alpha04")
docs("androidx.annotation:annotation:1.4.0")
@@ -140,9 +140,9 @@
docs("androidx.enterprise:enterprise-feedback:1.1.0")
docs("androidx.enterprise:enterprise-feedback-testing:1.1.0")
docs("androidx.exifinterface:exifinterface:1.3.3")
- docs("androidx.fragment:fragment:1.5.0")
- docs("androidx.fragment:fragment-ktx:1.5.0")
- docs("androidx.fragment:fragment-testing:1.5.0")
+ docs("androidx.fragment:fragment:1.5.1")
+ docs("androidx.fragment:fragment-ktx:1.5.1")
+ docs("androidx.fragment:fragment-testing:1.5.1")
docs("androidx.glance:glance:1.0.0-alpha03")
docs("androidx.glance:glance-appwidget:1.0.0-alpha03")
docs("androidx.glance:glance-appwidget-proto:1.0.0-alpha03")
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index fe556bc..ba9f1b5 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -168,6 +168,7 @@
docs(project(":hilt:hilt-navigation-fragment"))
docs(project(":hilt:hilt-work"))
docs(project(":interpolator:interpolator"))
+ docs(project(":javascriptengine:javascriptengine"))
docs(project(":metrics:metrics-performance"))
docs(project(":leanback:leanback"))
docs(project(":leanback:leanback-paging"))
diff --git a/fragment/fragment-ktx/build.gradle b/fragment/fragment-ktx/build.gradle
index 982978f..4c8fc2f 100644
--- a/fragment/fragment-ktx/build.gradle
+++ b/fragment/fragment-ktx/build.gradle
@@ -25,7 +25,7 @@
dependencies {
api(project(":fragment:fragment"))
- api("androidx.activity:activity-ktx:1.5.0") {
+ api("androidx.activity:activity-ktx:1.5.1") {
because "Mirror fragment dependency graph for -ktx artifacts"
}
api("androidx.core:core-ktx:1.2.0") {
@@ -34,10 +34,10 @@
api("androidx.collection:collection-ktx:1.1.0") {
because "Mirror fragment dependency graph for -ktx artifacts"
}
- api("androidx.lifecycle:lifecycle-livedata-core-ktx:2.5.0") {
+ api("androidx.lifecycle:lifecycle-livedata-core-ktx:2.5.1") {
because 'Mirror fragment dependency graph for -ktx artifacts'
}
- api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0")
+ api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
api("androidx.savedstate:savedstate-ktx:1.2.0") {
because 'Mirror fragment dependency graph for -ktx artifacts'
}
diff --git a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetector.kt b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetector.kt
index 500c462..b852d05 100644
--- a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetector.kt
+++ b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetector.kt
@@ -28,7 +28,9 @@
import com.intellij.psi.PsiType
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UClass
+import org.jetbrains.uast.UMethod
import org.jetbrains.uast.getParentOfType
+import org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression
/**
* Lint check for detecting calls to the suspend `repeatOnLifecycle` APIs using `lifecycleOwner`
@@ -58,12 +60,27 @@
override fun getApplicableMethodNames() = listOf("repeatOnLifecycle")
+ private val lifecycleMethods = setOf(
+ "onCreateView", "onViewCreated", "onActivityCreated",
+ "onViewStateRestored"
+ )
+
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
// Check that repeatOnLifecycle is called in a Fragment
- if (!hasFragmentAsAncestorType(node.getParentOfType<UClass>())) return
+ if (!hasFragmentAsAncestorType(node.getParentOfType())) return
- // Report issue if the receiver is not using viewLifecycleOwner
- if (node.receiver?.sourcePsi?.text?.contains(SAFE_RECEIVER, ignoreCase = true) != true) {
+ // Check that repeatOnLifecycle is called in the proper Lifecycle function
+ if (!isCalledInViewLifecycleFunction(node.getParentOfType())) return
+
+ // Look at the entire launch scope
+ var launchScope = node.getParentOfType<KotlinUFunctionCallExpression>()?.receiver
+ while (
+ launchScope != null && !containsViewLifecycleOwnerCall(launchScope.sourcePsi?.text)
+ ) {
+ launchScope = launchScope.getParentOfType()
+ }
+ // Report issue if there is no viewLifecycleOwner in the launch scope
+ if (!containsViewLifecycleOwnerCall(launchScope?.sourcePsi?.text)) {
context.report(
ISSUE,
context.getLocation(node),
@@ -72,6 +89,17 @@
}
}
+ private fun containsViewLifecycleOwnerCall(sourceText: String?): Boolean {
+ if (sourceText == null) return false
+ return sourceText.contains(VIEW_LIFECYCLE_KOTLIN_PROP, ignoreCase = true) ||
+ sourceText.contains(VIEW_LIFECYCLE_FUN, ignoreCase = true)
+ }
+
+ private fun isCalledInViewLifecycleFunction(uMethod: UMethod?): Boolean {
+ if (uMethod == null) return false
+ return lifecycleMethods.contains(uMethod.name)
+ }
+
/**
* Check if `uClass` has FRAGMENT as a super type but not DIALOG_FRAGMENT
*/
@@ -91,6 +119,7 @@
}
}
-private const val SAFE_RECEIVER = "viewLifecycleOwner"
+private const val VIEW_LIFECYCLE_KOTLIN_PROP = "viewLifecycleOwner"
+private const val VIEW_LIFECYCLE_FUN = "getViewLifecycleOwner"
private const val FRAGMENT_CLASS = "androidx.fragment.app.Fragment"
private const val DIALOG_FRAGMENT_CLASS = "androidx.fragment.app.DialogFragment"
diff --git a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetectorTest.kt b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetectorTest.kt
index 93b8bfc..2de49ca 100644
--- a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetectorTest.kt
+++ b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetectorTest.kt
@@ -202,4 +202,66 @@
.run()
.expectClean()
}
+
+ @Test
+ fun `viewLifecycleOwner in with outside of launch`() {
+ lint().files(
+ *REPEAT_ON_LIFECYCLE_STUBS,
+ kotlin(
+ """
+ package foo
+
+ import androidx.lifecycle.Lifecycle
+ import androidx.lifecycle.LifecycleOwner
+ import androidx.lifecycle.repeatOnLifecycle
+ import kotlinx.coroutines.CoroutineScope
+ import kotlinx.coroutines.GlobalScope
+ import androidx.fragment.app.Fragment
+
+ class MyFragment : Fragment() {
+ fun onCreateView() {
+ with(viewLifecycleOwner) {
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {}
+ }
+ }
+ }
+ }
+ """.trimIndent()
+ )
+ )
+ .allowCompilationErrors(false)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun `viewLifecycleOwner scope directly`() {
+ lint().files(
+ *REPEAT_ON_LIFECYCLE_STUBS,
+ kotlin(
+ """
+ package foo
+
+ import androidx.lifecycle.Lifecycle
+ import androidx.lifecycle.LifecycleOwner
+ import androidx.lifecycle.repeatOnLifecycle
+ import kotlinx.coroutines.CoroutineScope
+ import kotlinx.coroutines.GlobalScope
+ import androidx.fragment.app.Fragment
+
+ class MyFragment : Fragment() {
+ fun onCreateView() {
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {}
+ }
+ }
+ }
+ """.trimIndent()
+ )
+ )
+ .allowCompilationErrors(false)
+ .run()
+ .expectClean()
+ }
}
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index 309118a..02e8a8a 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -29,10 +29,10 @@
api("androidx.collection:collection:1.1.0")
api("androidx.viewpager:viewpager:1.0.0")
api("androidx.loader:loader:1.0.0")
- api("androidx.activity:activity:1.5.0")
- api("androidx.lifecycle:lifecycle-livedata-core:2.5.0")
- api("androidx.lifecycle:lifecycle-viewmodel:2.5.0")
- api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.0")
+ api("androidx.activity:activity:1.5.1")
+ api("androidx.lifecycle:lifecycle-livedata-core:2.5.1")
+ api("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
+ api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1")
api("androidx.savedstate:savedstate:1.2.0")
api("androidx.annotation:annotation-experimental:1.0.0")
api(libs.kotlinStdlib)
diff --git a/glance/glance-appwidget/api/current.txt b/glance/glance-appwidget/api/current.txt
index 3434245..baf72d7 100644
--- a/glance/glance-appwidget/api/current.txt
+++ b/glance/glance-appwidget/api/current.txt
@@ -68,7 +68,10 @@
public final class GlanceAppWidgetManager {
ctor public GlanceAppWidgetManager(android.content.Context context);
+ method public int getAppWidgetId(androidx.glance.GlanceId glanceId);
method public suspend Object? getAppWidgetSizes(androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.compose.ui.unit.DpSize>>);
+ method public androidx.glance.GlanceId getGlanceIdBy(int appWidgetId);
+ method public androidx.glance.GlanceId? getGlanceIdBy(android.content.Intent configurationIntent);
method public suspend <T extends androidx.glance.appwidget.GlanceAppWidget> Object? getGlanceIds(Class<T> provider, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.glance.GlanceId>>);
}
diff --git a/glance/glance-appwidget/api/public_plus_experimental_current.txt b/glance/glance-appwidget/api/public_plus_experimental_current.txt
index 31daed5..77c2f0f 100644
--- a/glance/glance-appwidget/api/public_plus_experimental_current.txt
+++ b/glance/glance-appwidget/api/public_plus_experimental_current.txt
@@ -71,7 +71,10 @@
public final class GlanceAppWidgetManager {
ctor public GlanceAppWidgetManager(android.content.Context context);
+ method public int getAppWidgetId(androidx.glance.GlanceId glanceId);
method public suspend Object? getAppWidgetSizes(androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.compose.ui.unit.DpSize>>);
+ method public androidx.glance.GlanceId getGlanceIdBy(int appWidgetId);
+ method public androidx.glance.GlanceId? getGlanceIdBy(android.content.Intent configurationIntent);
method public suspend <T extends androidx.glance.appwidget.GlanceAppWidget> Object? getGlanceIds(Class<T> provider, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.glance.GlanceId>>);
}
diff --git a/glance/glance-appwidget/api/restricted_current.txt b/glance/glance-appwidget/api/restricted_current.txt
index 3434245..baf72d7 100644
--- a/glance/glance-appwidget/api/restricted_current.txt
+++ b/glance/glance-appwidget/api/restricted_current.txt
@@ -68,7 +68,10 @@
public final class GlanceAppWidgetManager {
ctor public GlanceAppWidgetManager(android.content.Context context);
+ method public int getAppWidgetId(androidx.glance.GlanceId glanceId);
method public suspend Object? getAppWidgetSizes(androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.compose.ui.unit.DpSize>>);
+ method public androidx.glance.GlanceId getGlanceIdBy(int appWidgetId);
+ method public androidx.glance.GlanceId? getGlanceIdBy(android.content.Intent configurationIntent);
method public suspend <T extends androidx.glance.appwidget.GlanceAppWidget> Object? getGlanceIds(Class<T> provider, kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.glance.GlanceId>>);
}
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/SingleEntityWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/SingleEntityWidget.kt
index ef8e33a..ede3b8a 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/SingleEntityWidget.kt
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/SingleEntityWidget.kt
@@ -30,13 +30,13 @@
import androidx.glance.appwidget.action.actionRunCallback
import androidx.glance.appwidget.state.updateAppWidgetState
import androidx.glance.appwidget.template.GlanceTemplateAppWidget
-import androidx.glance.currentState
import androidx.glance.appwidget.template.SingleEntityTemplate
+import androidx.glance.currentState
import androidx.glance.template.SingleEntityTemplateData
import androidx.glance.template.TemplateImageWithDescription
import androidx.glance.template.TemplateText
import androidx.glance.template.TemplateTextButton
-import androidx.glance.template.TemplateText.Type
+import androidx.glance.template.TextType
private val PressedKey = booleanPreferencesKey("pressedKey")
@@ -70,13 +70,13 @@
}
private fun createData(title: String) = SingleEntityTemplateData(
- header = TemplateText("Single Entity demo", Type.Title),
+ header = TemplateText("Single Entity demo", TextType.Title),
headerIcon = TemplateImageWithDescription(
ImageProvider(R.drawable.compose),
"Header icon"
),
- text1 = TemplateText(title, Type.Title),
- text2 = TemplateText("Subtitle test", Type.Title),
+ text1 = TemplateText(title, TextType.Title),
+ text2 = TemplateText("Subtitle test", TextType.Title),
button = TemplateTextButton(actionRunCallback<ButtonAction>(), "toggle"),
image = TemplateImageWithDescription(
ImageProvider(R.drawable.compose),
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/DemoOverrideWidget.kt b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/DemoOverrideWidget.kt
index 0e3d961..e7d93f4 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/DemoOverrideWidget.kt
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/DemoOverrideWidget.kt
@@ -23,21 +23,22 @@
import androidx.glance.ImageProvider
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
+import androidx.glance.appwidget.template.GlanceTemplateAppWidget
+import androidx.glance.appwidget.template.SingleEntityTemplate
import androidx.glance.background
import androidx.glance.layout.Alignment
import androidx.glance.layout.Column
import androidx.glance.layout.fillMaxSize
+import androidx.glance.template.LocalTemplateMode
+import androidx.glance.template.SingleEntityTemplateData
+import androidx.glance.template.TemplateImageWithDescription
+import androidx.glance.template.TemplateMode
+import androidx.glance.template.TemplateText
+import androidx.glance.template.TextType
import androidx.glance.text.Text
import androidx.glance.text.TextAlign
import androidx.glance.text.TextStyle
import androidx.glance.unit.ColorProvider
-import androidx.glance.appwidget.template.GlanceTemplateAppWidget
-import androidx.glance.appwidget.template.SingleEntityTemplate
-import androidx.glance.template.LocalTemplateMode
-import androidx.glance.template.SingleEntityTemplateData
-import androidx.glance.template.TemplateMode
-import androidx.glance.template.TemplateImageWithDescription
-import androidx.glance.template.TemplateText
/**
* A widget implementation that uses [SingleEntityTemplate] with a custom layout override for
@@ -52,16 +53,16 @@
} else {
SingleEntityTemplate(
SingleEntityTemplateData(
- header = TemplateText("Single Entity Demo", TemplateText.Type.Title),
+ header = TemplateText("Single Entity Demo", TextType.Title),
headerIcon = TemplateImageWithDescription(
ImageProvider(R.drawable.compose),
"icon"
),
- text1 = TemplateText("title", TemplateText.Type.Title),
- text2 = TemplateText("Subtitle", TemplateText.Type.Label),
+ text1 = TemplateText("title", TextType.Title),
+ text2 = TemplateText("Subtitle", TextType.Label),
text3 = TemplateText(
"Body Lorem ipsum dolor sit amet, consectetur adipiscing",
- TemplateText.Type.Label
+ TextType.Label
),
image = TemplateImageWithDescription(ImageProvider(R.drawable.compose), "image")
)
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/GalleryDemoWidget.kt b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/GalleryDemoWidget.kt
index 917f568..cb4eac1 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/GalleryDemoWidget.kt
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/GalleryDemoWidget.kt
@@ -17,15 +17,19 @@
package androidx.glance.appwidget.template.demos
import androidx.compose.runtime.Composable
+import androidx.glance.ImageProvider
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
-import androidx.glance.ImageProvider
import androidx.glance.appwidget.SizeMode
-import androidx.glance.unit.ColorProvider
import androidx.glance.appwidget.template.GalleryTemplate
import androidx.glance.appwidget.template.GlanceTemplateAppWidget
import androidx.glance.template.GalleryTemplateData
+import androidx.glance.template.HeaderBlock
+import androidx.glance.template.ImageBlock
import androidx.glance.template.TemplateImageWithDescription
+import androidx.glance.template.TemplateText
+import androidx.glance.template.TextBlock
+import androidx.glance.template.TextType
/**
* A widget that uses [GalleryTemplate].
@@ -35,17 +39,42 @@
@Composable
override fun TemplateContent() {
+ val galleryContent = mutableListOf<TemplateImageWithDescription>()
+ for (i in 1..8) {
+ galleryContent.add(
+ TemplateImageWithDescription(
+ ImageProvider(R.drawable.compose),
+ "gallery image $i"
+ )
+ )
+ }
GalleryTemplate(
GalleryTemplateData(
- header = "Gallery Template example",
- title = "Gallery Template title",
- headline = "Gallery Template headline",
- image = TemplateImageWithDescription(
- ImageProvider(R.drawable.compose),
- "test image"
+ header = HeaderBlock(
+ text = TemplateText("Gallery Template example"),
+ icon = TemplateImageWithDescription(
+ ImageProvider(R.drawable.compose),
+ "test logo"
+ ),
),
- logo = TemplateImageWithDescription(ImageProvider(R.drawable.compose), "test logo"),
- backgroundColor = ColorProvider(R.color.default_widget_background)
+ mainTextBlock = TextBlock(
+ text1 = TemplateText("Gallery Template title", TextType.Title),
+ text2 = TemplateText("Gallery Template headline", TextType.Headline),
+ priority = 0,
+ ),
+ mainImageBlock = ImageBlock(
+ images = listOf(
+ TemplateImageWithDescription(
+ ImageProvider(R.drawable.compose),
+ "test image"
+ )
+ ),
+ priority = 1,
+ ),
+ galleryImageBlock = ImageBlock(
+ images = galleryContent,
+ priority = 2,
+ ),
)
)
}
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/ListDemoWidget.kt b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/ListDemoWidget.kt
index d55bac6..23bcab3 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/ListDemoWidget.kt
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/ListDemoWidget.kt
@@ -43,6 +43,7 @@
import androidx.glance.template.TemplateImageButton
import androidx.glance.template.TemplateImageWithDescription
import androidx.glance.template.TemplateText
+import androidx.glance.template.TextType
import androidx.glance.unit.ColorProvider
/**
@@ -144,12 +145,12 @@
}
content.add(
ListTemplateItem(
- title = TemplateText("Title Medium", TemplateText.Type.Title),
+ title = TemplateText("Title Medium", TextType.Title),
body = TemplateText(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit",
- TemplateText.Type.Body
+ TextType.Body
),
- label = TemplateText(label, TemplateText.Type.Label),
+ label = TemplateText(label, TextType.Label),
image = TemplateImageWithDescription(ImageProvider(R.drawable.compose), "$i"),
button = TemplateImageButton(
itemSelectAction(
@@ -168,7 +169,7 @@
ListTemplateData(
header = if (showHeader) TemplateText(
"List Demo",
- TemplateText.Type.Title
+ TextType.Title
) else null,
headerIcon = if (showHeader) TemplateImageWithDescription(
ImageProvider(R.drawable.ic_widget),
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/SingleEntityDemoWidget.kt b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/SingleEntityDemoWidget.kt
index 4dae917..aad35b6 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/SingleEntityDemoWidget.kt
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/SingleEntityDemoWidget.kt
@@ -35,8 +35,8 @@
import androidx.glance.template.SingleEntityTemplateData
import androidx.glance.template.TemplateImageWithDescription
import androidx.glance.template.TemplateText
-import androidx.glance.template.TemplateText.Type
import androidx.glance.template.TemplateTextButton
+import androidx.glance.template.TextType
/**
* Demo app widget using [SingleEntityTemplate] to define layout.
@@ -48,18 +48,18 @@
override fun TemplateContent() {
SingleEntityTemplate(
SingleEntityTemplateData(
- header = TemplateText("Single Entity Demo", Type.Title),
+ header = TemplateText("Single Entity Demo", TextType.Title),
headerIcon = TemplateImageWithDescription(
ImageProvider(R.drawable.compose),
"Header icon"
),
text1 = TemplateText(
- getTitle(currentState<Preferences>()[ToggleKey] == true), Type.Title
+ getTitle(currentState<Preferences>()[ToggleKey] == true), TextType.Title
),
- text2 = TemplateText("Subtitle", Type.Label),
+ text2 = TemplateText("Subtitle", TextType.Label),
text3 = TemplateText(
"Body Lorem ipsum dolor sit amet, consectetur adipiscing elit",
- Type.Body
+ TextType.Body
),
button = TemplateTextButton(
actionRunCallback<SEButtonAction>(),
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetManagerTest.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetManagerTest.kt
index 724e0c973..4855624 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetManagerTest.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetManagerTest.kt
@@ -61,6 +61,8 @@
runBlocking {
val glanceIds = manager.getGlanceIds(TestGlanceAppWidget::class.java)
assertThat(glanceIds).hasSize(1)
+ val glanceId = manager.getGlanceIdBy((glanceIds[0] as AppWidgetId).appWidgetId)
+ assertThat(glanceId).isEqualTo(glanceIds[0])
val sizes = manager.getAppWidgetSizes(glanceIds[0])
assertThat(sizes).containsExactly(
mHostRule.portraitSize,
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidgetManager.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidgetManager.kt
index 1c02d20..0fb0915 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidgetManager.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidgetManager.kt
@@ -19,6 +19,7 @@
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
+import android.content.Intent
import androidx.annotation.VisibleForTesting
import androidx.compose.ui.unit.DpSize
import androidx.datastore.core.DataStore
@@ -116,6 +117,47 @@
return bundle.extractAllSizes { DpSize.Zero }
}
+ /**
+ * Retrieve the platform AppWidget ID from the provided GlanceId
+ *
+ * Important: Do NOT use appwidget ID as identifier, instead create your own and store them in
+ * the GlanceStateDefinition. This method should only be used for compatibility or IPC
+ * communication reasons in conjunction with [getGlanceIdBy]
+ */
+ fun getAppWidgetId(glanceId: GlanceId): Int {
+ require(glanceId is AppWidgetId) { "This method only accepts App Widget Glance Id" }
+ return glanceId.appWidgetId
+ }
+
+ /**
+ * Retrieve the GlanceId of the provided AppWidget ID.
+ *
+ * @throws IllegalArgumentException if the provided id is not associated with an existing
+ * GlanceId
+ */
+ fun getGlanceIdBy(appWidgetId: Int): GlanceId {
+ requireNotNull(appWidgetManager.getAppWidgetInfo(appWidgetId)) {
+ "Invalid AppWidget ID."
+ }
+ return AppWidgetId(appWidgetId)
+ }
+
+ /**
+ * Retrieve the GlanceId from the configuration activity intent or null if not valid
+ */
+ fun getGlanceIdBy(configurationIntent: Intent): GlanceId? {
+ val appWidgetId = configurationIntent.extras?.getInt(
+ AppWidgetManager.EXTRA_APPWIDGET_ID,
+ AppWidgetManager.INVALID_APPWIDGET_ID
+ ) ?: AppWidgetManager.INVALID_APPWIDGET_ID
+
+ if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
+ return null
+ }
+
+ return AppWidgetId(appWidgetId)
+ }
+
/** Check which receivers still exist, and clean the data store to only keep those. */
internal suspend fun cleanReceivers() {
val packageName = context.packageName
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/FreeformTemplateLayouts.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/FreeformTemplateLayouts.kt
index ac33065..115feec 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/FreeformTemplateLayouts.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/FreeformTemplateLayouts.kt
@@ -30,6 +30,7 @@
import androidx.glance.template.LocalTemplateMode
import androidx.glance.template.TemplateMode
import androidx.glance.template.TemplateText
+import androidx.glance.template.TextType
import androidx.glance.unit.ColorProvider
/**
@@ -100,10 +101,10 @@
): List<TemplateText> {
val result = mutableListOf<TemplateText>()
title?.let {
- result.add(TemplateText(it.text, TemplateText.Type.Title))
+ result.add(TemplateText(it.text, TextType.Title))
}
subtitle?.let {
- result.add(TemplateText(it.text, TemplateText.Type.Label))
+ result.add(TemplateText(it.text, TextType.Label))
}
return result
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GalleryTemplateLayouts.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GalleryTemplateLayouts.kt
index ea5443d..2489d93 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GalleryTemplateLayouts.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GalleryTemplateLayouts.kt
@@ -23,12 +23,14 @@
import androidx.glance.background
import androidx.glance.layout.Alignment
import androidx.glance.layout.Column
+import androidx.glance.layout.ContentScale
import androidx.glance.layout.Row
import androidx.glance.layout.Spacer
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.padding
import androidx.glance.layout.width
import androidx.glance.template.GalleryTemplateData
+import androidx.glance.template.LocalTemplateColors
import androidx.glance.template.LocalTemplateMode
import androidx.glance.template.TemplateMode
import androidx.glance.text.Text
@@ -49,15 +51,18 @@
@Composable
private fun WidgetLayoutCollapsed(data: GalleryTemplateData) {
- Column(
- modifier = GlanceModifier.fillMaxSize().padding(8.dp).background(data.backgroundColor),
- ) {
- Row {
- Image(provider = data.image.image, contentDescription = data.image.description)
- Image(provider = data.image.image, contentDescription = data.image.description)
- }
- Text(data.title)
- Text(data.headline)
+ val modifier = createTopLevelModifier(data, true)
+
+ Column(modifier = modifier) {
+ data.header?.let { AppWidgetTemplateHeader(it) }
+ Spacer(modifier = GlanceModifier.defaultWeight())
+ AppWidgetTextSection(
+ listOfNotNull(
+ data.mainTextBlock.text1,
+ data.mainTextBlock.text2,
+ data.mainTextBlock.text3
+ )
+ )
}
}
@@ -65,27 +70,70 @@
@Composable
private fun WidgetLayoutVertical(data: GalleryTemplateData) {
Column(
- modifier = GlanceModifier.fillMaxSize().padding(8.dp).background(data.backgroundColor),
+ modifier = GlanceModifier.fillMaxSize().padding(8.dp),
) {
+ Row(
+ modifier = GlanceModifier.fillMaxSize().padding(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column {
+ MainImageBlock(data)
+ }
+ Spacer(GlanceModifier.width(8.dp))
+ Column {
+ Text(data.mainTextBlock.text1.text)
+ data.mainTextBlock.text2?.let { headline ->
+ Text(headline.text)
+ }
+ }
+ Column(verticalAlignment = Alignment.Top) {
+ MainImageBlock(data)
+ }
+ }
}
}
@Composable
private fun WidgetLayoutHorizontal(data: GalleryTemplateData) {
Row(
- modifier = GlanceModifier.fillMaxSize().padding(8.dp).background(data.backgroundColor),
+ modifier = GlanceModifier.fillMaxSize().padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column {
- Image(provider = data.image.image, contentDescription = data.image.description)
+ MainImageBlock(data)
}
Spacer(GlanceModifier.width(8.dp))
Column {
- Text(data.title)
- Text(data.headline)
+ Text(data.mainTextBlock.text1.text)
+ data.mainTextBlock.text2?.let { headline ->
+ Text(headline.text)
+ }
}
Column(verticalAlignment = Alignment.Top) {
- Image(provider = data.image.image, contentDescription = data.image.description)
+ MainImageBlock(data)
}
}
}
+
+@Composable
+private fun MainImageBlock(data: GalleryTemplateData) {
+ if (data.mainImageBlock.images.isNotEmpty()) {
+ val mainImage = data.mainImageBlock.images[0]
+ Image(provider = mainImage.image, contentDescription = mainImage.description)
+ }
+}
+
+@Composable
+private fun createTopLevelModifier(
+ data: GalleryTemplateData,
+ isImmersive: Boolean = false
+): GlanceModifier {
+ var modifier = GlanceModifier
+ .fillMaxSize().padding(16.dp).background(LocalTemplateColors.current.surface)
+ if (isImmersive && data.mainImageBlock.images.isNotEmpty()) {
+ val mainImage = data.mainImageBlock.images[0]
+ modifier = modifier.background(mainImage.image, ContentScale.Crop)
+ }
+
+ return modifier
+}
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GlanceAppWidgetTemplates.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GlanceAppWidgetTemplates.kt
index 6060207..c355fdb 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GlanceAppWidgetTemplates.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GlanceAppWidgetTemplates.kt
@@ -33,12 +33,14 @@
import androidx.glance.layout.fillMaxWidth
import androidx.glance.layout.height
import androidx.glance.layout.width
+import androidx.glance.template.HeaderBlock
import androidx.glance.template.LocalTemplateColors
import androidx.glance.template.TemplateButton
import androidx.glance.template.TemplateImageButton
import androidx.glance.template.TemplateImageWithDescription
import androidx.glance.template.TemplateText
import androidx.glance.template.TemplateTextButton
+import androidx.glance.template.TextType
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
@@ -74,13 +76,14 @@
Spacer(modifier = GlanceModifier.width(8.dp))
}
val size =
- textSize(TemplateText.Type.Title, DisplaySize.fromDpSize(LocalSize.current))
+ textSize(TextType.Title, DisplaySize.fromDpSize(LocalSize.current))
Text(
modifier = GlanceModifier.defaultWeight(),
text = header.text,
style = TextStyle(
fontSize = size,
- color = LocalTemplateColors.current.onSurface),
+ color = LocalTemplateColors.current.onSurface
+ ),
maxLines = 1
)
}
@@ -94,8 +97,23 @@
}
/**
+ * Default header template layout implementation for AppWidgets, usually displayed at the top of the
+ * glanceable in default layout implementations by [HeaderBlock].
+ *
+ * @param headerBlock The glanceable header block to display
+ */
+@Composable
+internal fun AppWidgetTemplateHeader(headerBlock: HeaderBlock) {
+ AppWidgetTemplateHeader(
+ headerBlock.icon,
+ headerBlock.text,
+ headerBlock.actionBlock?.actionButtons?.get(0)
+ )
+}
+
+/**
* Default text section layout for AppWidgets. Displays an ordered list of text fields, styled
- * according to the [TemplateText.Type] of each field.
+ * according to the [TextType] of each field.
*
* @param textList the ordered list of text fields to display in the block
*/
@@ -168,37 +186,47 @@
}
}
-private fun textSize(textClass: TemplateText.Type, displaySize: DisplaySize): TextUnit =
+private fun textSize(textClass: TextType, displaySize: DisplaySize): TextUnit =
when (textClass) {
// TODO: Does display scale?
- TemplateText.Type.Display -> 45.sp
- TemplateText.Type.Title -> {
+ TextType.Display -> 45.sp
+ TextType.Title -> {
when (displaySize) {
DisplaySize.Small -> 14.sp
DisplaySize.Medium -> 16.sp
DisplaySize.Large -> 22.sp
}
}
- TemplateText.Type.Body -> {
+ TextType.Headline -> {
+ when (displaySize) {
+ DisplaySize.Small -> 12.sp
+ DisplaySize.Medium -> 14.sp
+ DisplaySize.Large -> 18.sp
+ }
+ }
+ TextType.Body -> {
when (displaySize) {
DisplaySize.Small -> 12.sp
DisplaySize.Medium -> 14.sp
DisplaySize.Large -> 14.sp
}
}
- TemplateText.Type.Label -> {
+ TextType.Label -> {
when (displaySize) {
DisplaySize.Small -> 11.sp
DisplaySize.Medium -> 12.sp
DisplaySize.Large -> 14.sp
}
}
+ else -> 12.sp
}
-private fun maxLines(textClass: TemplateText.Type): Int =
+private fun maxLines(textClass: TextType): Int =
when (textClass) {
- TemplateText.Type.Display -> 1
- TemplateText.Type.Title -> 3
- TemplateText.Type.Body -> 3
- TemplateText.Type.Label -> 1
+ TextType.Display -> 1
+ TextType.Title -> 3
+ TextType.Body -> 3
+ TextType.Label -> 1
+ TextType.Headline -> 1
+ else -> 1
}
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/SingleEntityTemplateLayouts.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/SingleEntityTemplateLayouts.kt
index de9c45f..27cc583 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/SingleEntityTemplateLayouts.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/SingleEntityTemplateLayouts.kt
@@ -37,6 +37,7 @@
import androidx.glance.template.SingleEntityTemplateData
import androidx.glance.template.TemplateMode
import androidx.glance.template.TemplateText
+import androidx.glance.template.TextType
// TODO: Define template layouts for other surfaces
/**
@@ -141,9 +142,9 @@
body: TemplateText? = null
): List<TemplateText> {
val result = mutableListOf<TemplateText>()
- title?.let { result.add(TemplateText(it.text, TemplateText.Type.Title)) }
- subtitle?.let { result.add(TemplateText(it.text, TemplateText.Type.Label)) }
- body?.let { result.add(TemplateText(it.text, TemplateText.Type.Body)) }
+ title?.let { result.add(TemplateText(it.text, TextType.Title)) }
+ subtitle?.let { result.add(TemplateText(it.text, TextType.Label)) }
+ body?.let { result.add(TemplateText(it.text, TextType.Body)) }
return result
}
diff --git a/glance/glance-wear-tiles/integration-tests/template-demos/src/main/java/androidx/glance/wear/tiles/template/demos/DemoTile.kt b/glance/glance-wear-tiles/integration-tests/template-demos/src/main/java/androidx/glance/wear/tiles/template/demos/DemoTile.kt
index 5b75fa8..7a85f81 100644
--- a/glance/glance-wear-tiles/integration-tests/template-demos/src/main/java/androidx/glance/wear/tiles/template/demos/DemoTile.kt
+++ b/glance/glance-wear-tiles/integration-tests/template-demos/src/main/java/androidx/glance/wear/tiles/template/demos/DemoTile.kt
@@ -18,10 +18,11 @@
import androidx.compose.runtime.Composable
import androidx.glance.ImageProvider
-import androidx.glance.wear.tiles.GlanceTileService
import androidx.glance.template.SingleEntityTemplateData
import androidx.glance.template.TemplateImageWithDescription
import androidx.glance.template.TemplateText
+import androidx.glance.template.TextType
+import androidx.glance.wear.tiles.GlanceTileService
import androidx.glance.wear.tiles.template.SingleEntityTemplate
/** Simple demo tile, displays [SingleEntityTemplate] */
@@ -31,14 +32,14 @@
override fun Content() {
SingleEntityTemplate(
SingleEntityTemplateData(
- header = TemplateText("Single Entity Demo", TemplateText.Type.Title),
+ header = TemplateText("Single Entity Demo", TextType.Title),
headerIcon = TemplateImageWithDescription(
ImageProvider(R.drawable.compose),
"image"
),
- text1 = TemplateText("Title", TemplateText.Type.Title),
- text2 = TemplateText("Subtitle", TemplateText.Type.Label),
- text3 = TemplateText("Here's the body", TemplateText.Type.Body),
+ text1 = TemplateText("Title", TextType.Title),
+ text2 = TemplateText("Subtitle", TextType.Label),
+ text3 = TemplateText("Here's the body", TextType.Body),
image = TemplateImageWithDescription(ImageProvider(R.drawable.compose), "image"),
)
)
diff --git a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/template/SingleEntityTemplateLayouts.kt b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/template/SingleEntityTemplateLayouts.kt
index 252e254..10d131d 100644
--- a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/template/SingleEntityTemplateLayouts.kt
+++ b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/template/SingleEntityTemplateLayouts.kt
@@ -37,6 +37,7 @@
import androidx.glance.template.SingleEntityTemplateData
import androidx.glance.template.TemplateImageWithDescription
import androidx.glance.template.TemplateText
+import androidx.glance.template.TextType
import androidx.glance.text.Text
import androidx.glance.text.TextAlign
import androidx.glance.text.TextStyle
@@ -117,7 +118,7 @@
item.text,
style = TextStyle(
color = ColorProvider(Color.White),
- fontSize = if (item.type == TemplateText.Type.Title) 24.sp else 16.sp,
+ fontSize = if (item.type == TextType.Title) 24.sp else 16.sp,
textAlign = TextAlign.Center)
)
}
@@ -130,9 +131,9 @@
body: TemplateText? = null
): List<TemplateText> {
val result = mutableListOf<TemplateText>()
- title?.let { result.add(TemplateText(it.text, TemplateText.Type.Title)) }
- subtitle?.let { result.add(TemplateText(it.text, TemplateText.Type.Label)) }
- body?.let { result.add(TemplateText(it.text, TemplateText.Type.Body)) }
+ title?.let { result.add(TemplateText(it.text, TextType.Title)) }
+ subtitle?.let { result.add(TemplateText(it.text, TextType.Label)) }
+ body?.let { result.add(TemplateText(it.text, TextType.Body)) }
return result
}
diff --git a/glance/glance/api/current.txt b/glance/glance/api/current.txt
index bac023a..8dbc0bb 100644
--- a/glance/glance/api/current.txt
+++ b/glance/glance/api/current.txt
@@ -410,6 +410,42 @@
package androidx.glance.template {
+ public final class ActionBlock {
+ ctor public ActionBlock(optional java.util.List<? extends androidx.glance.template.TemplateButton> actionButtons, optional int type);
+ method public java.util.List<androidx.glance.template.TemplateButton> getActionButtons();
+ method public int getType();
+ property public final java.util.List<androidx.glance.template.TemplateButton> actionButtons;
+ property public final int type;
+ }
+
+ @kotlin.jvm.JvmInline public final value class AspectRatio {
+ field public static final androidx.glance.template.AspectRatio.Companion Companion;
+ }
+
+ public static final class AspectRatio.Companion {
+ method public int getRatio16x9();
+ method public int getRatio1x1();
+ method public int getRatio2x3();
+ property public final int Ratio16x9;
+ property public final int Ratio1x1;
+ property public final int Ratio2x3;
+ }
+
+ @kotlin.jvm.JvmInline public final value class ButtonType {
+ field public static final androidx.glance.template.ButtonType.Companion Companion;
+ }
+
+ public static final class ButtonType.Companion {
+ method public int getFab();
+ method public int getIcon();
+ method public int getText();
+ method public int getTextIcon();
+ property public final int Fab;
+ property public final int Icon;
+ property public final int Text;
+ property public final int TextIcon;
+ }
+
public final class CompositionLocalsKt {
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.glance.color.ColorProviders> getLocalTemplateColors();
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.glance.template.TemplateMode> getLocalTemplateMode();
@@ -436,19 +472,52 @@
}
public final class GalleryTemplateData {
- ctor public GalleryTemplateData(String header, String title, String headline, androidx.glance.template.TemplateImageWithDescription image, androidx.glance.template.TemplateImageWithDescription logo, androidx.glance.unit.ColorProvider backgroundColor);
- method public androidx.glance.unit.ColorProvider getBackgroundColor();
- method public String getHeader();
- method public String getHeadline();
- method public androidx.glance.template.TemplateImageWithDescription getImage();
- method public androidx.glance.template.TemplateImageWithDescription getLogo();
- method public String getTitle();
- property public final androidx.glance.unit.ColorProvider backgroundColor;
- property public final String header;
- property public final String headline;
- property public final androidx.glance.template.TemplateImageWithDescription image;
- property public final androidx.glance.template.TemplateImageWithDescription logo;
- property public final String title;
+ ctor public GalleryTemplateData(optional androidx.glance.template.HeaderBlock? header, androidx.glance.template.TextBlock mainTextBlock, androidx.glance.template.ImageBlock mainImageBlock, optional androidx.glance.template.ActionBlock? mainActionBlock, androidx.glance.template.ImageBlock galleryImageBlock);
+ method public androidx.glance.template.ImageBlock getGalleryImageBlock();
+ method public androidx.glance.template.HeaderBlock? getHeader();
+ method public androidx.glance.template.ActionBlock? getMainActionBlock();
+ method public androidx.glance.template.ImageBlock getMainImageBlock();
+ method public androidx.glance.template.TextBlock getMainTextBlock();
+ property public final androidx.glance.template.ImageBlock galleryImageBlock;
+ property public final androidx.glance.template.HeaderBlock? header;
+ property public final androidx.glance.template.ActionBlock? mainActionBlock;
+ property public final androidx.glance.template.ImageBlock mainImageBlock;
+ property public final androidx.glance.template.TextBlock mainTextBlock;
+ }
+
+ public final class HeaderBlock {
+ ctor public HeaderBlock(androidx.glance.template.TemplateText text, optional androidx.glance.template.TemplateImageWithDescription? icon, optional androidx.glance.template.ActionBlock? actionBlock);
+ method public androidx.glance.template.ActionBlock? getActionBlock();
+ method public androidx.glance.template.TemplateImageWithDescription? getIcon();
+ method public androidx.glance.template.TemplateText getText();
+ property public final androidx.glance.template.ActionBlock? actionBlock;
+ property public final androidx.glance.template.TemplateImageWithDescription? icon;
+ property public final androidx.glance.template.TemplateText text;
+ }
+
+ public final class ImageBlock {
+ ctor public ImageBlock(optional java.util.List<androidx.glance.template.TemplateImageWithDescription> images, optional int aspectRatio, optional int size, optional int priority);
+ method public int getAspectRatio();
+ method public java.util.List<androidx.glance.template.TemplateImageWithDescription> getImages();
+ method public int getPriority();
+ method public int getSize();
+ property public final int aspectRatio;
+ property public final java.util.List<androidx.glance.template.TemplateImageWithDescription> images;
+ property public final int priority;
+ property public final int size;
+ }
+
+ @kotlin.jvm.JvmInline public final value class ImageSize {
+ field public static final androidx.glance.template.ImageSize.Companion Companion;
+ }
+
+ public static final class ImageSize.Companion {
+ method public int getLarge();
+ method public int getMedium();
+ method public int getSmall();
+ property public final int Large;
+ property public final int Medium;
+ property public final int Small;
}
@kotlin.jvm.JvmInline public final value class ListStyle {
@@ -546,20 +615,11 @@
}
public final class TemplateText {
- ctor public TemplateText(String text, androidx.glance.template.TemplateText.Type type);
+ ctor public TemplateText(String text, optional int type);
method public String getText();
- method public androidx.glance.template.TemplateText.Type getType();
+ method public int getType();
property public final String text;
- property public final androidx.glance.template.TemplateText.Type type;
- }
-
- public enum TemplateText.Type {
- method public static androidx.glance.template.TemplateText.Type valueOf(String name) throws java.lang.IllegalArgumentException;
- method public static androidx.glance.template.TemplateText.Type[] values();
- enum_constant public static final androidx.glance.template.TemplateText.Type Body;
- enum_constant public static final androidx.glance.template.TemplateText.Type Display;
- enum_constant public static final androidx.glance.template.TemplateText.Type Label;
- enum_constant public static final androidx.glance.template.TemplateText.Type Title;
+ property public final int type;
}
public final class TemplateTextButton extends androidx.glance.template.TemplateButton {
@@ -568,6 +628,35 @@
property public final String text;
}
+ public final class TextBlock {
+ ctor public TextBlock(androidx.glance.template.TemplateText text1, optional androidx.glance.template.TemplateText? text2, optional androidx.glance.template.TemplateText? text3, optional int priority);
+ method public int getPriority();
+ method public androidx.glance.template.TemplateText getText1();
+ method public androidx.glance.template.TemplateText? getText2();
+ method public androidx.glance.template.TemplateText? getText3();
+ property public final int priority;
+ property public final androidx.glance.template.TemplateText text1;
+ property public final androidx.glance.template.TemplateText? text2;
+ property public final androidx.glance.template.TemplateText? text3;
+ }
+
+ @kotlin.jvm.JvmInline public final value class TextType {
+ field public static final androidx.glance.template.TextType.Companion Companion;
+ }
+
+ public static final class TextType.Companion {
+ method public int getBody();
+ method public int getDisplay();
+ method public int getHeadline();
+ method public int getLabel();
+ method public int getTitle();
+ property public final int Body;
+ property public final int Display;
+ property public final int Headline;
+ property public final int Label;
+ property public final int Title;
+ }
+
}
package androidx.glance.text {
diff --git a/glance/glance/api/public_plus_experimental_current.txt b/glance/glance/api/public_plus_experimental_current.txt
index bac023a..8dbc0bb 100644
--- a/glance/glance/api/public_plus_experimental_current.txt
+++ b/glance/glance/api/public_plus_experimental_current.txt
@@ -410,6 +410,42 @@
package androidx.glance.template {
+ public final class ActionBlock {
+ ctor public ActionBlock(optional java.util.List<? extends androidx.glance.template.TemplateButton> actionButtons, optional int type);
+ method public java.util.List<androidx.glance.template.TemplateButton> getActionButtons();
+ method public int getType();
+ property public final java.util.List<androidx.glance.template.TemplateButton> actionButtons;
+ property public final int type;
+ }
+
+ @kotlin.jvm.JvmInline public final value class AspectRatio {
+ field public static final androidx.glance.template.AspectRatio.Companion Companion;
+ }
+
+ public static final class AspectRatio.Companion {
+ method public int getRatio16x9();
+ method public int getRatio1x1();
+ method public int getRatio2x3();
+ property public final int Ratio16x9;
+ property public final int Ratio1x1;
+ property public final int Ratio2x3;
+ }
+
+ @kotlin.jvm.JvmInline public final value class ButtonType {
+ field public static final androidx.glance.template.ButtonType.Companion Companion;
+ }
+
+ public static final class ButtonType.Companion {
+ method public int getFab();
+ method public int getIcon();
+ method public int getText();
+ method public int getTextIcon();
+ property public final int Fab;
+ property public final int Icon;
+ property public final int Text;
+ property public final int TextIcon;
+ }
+
public final class CompositionLocalsKt {
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.glance.color.ColorProviders> getLocalTemplateColors();
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.glance.template.TemplateMode> getLocalTemplateMode();
@@ -436,19 +472,52 @@
}
public final class GalleryTemplateData {
- ctor public GalleryTemplateData(String header, String title, String headline, androidx.glance.template.TemplateImageWithDescription image, androidx.glance.template.TemplateImageWithDescription logo, androidx.glance.unit.ColorProvider backgroundColor);
- method public androidx.glance.unit.ColorProvider getBackgroundColor();
- method public String getHeader();
- method public String getHeadline();
- method public androidx.glance.template.TemplateImageWithDescription getImage();
- method public androidx.glance.template.TemplateImageWithDescription getLogo();
- method public String getTitle();
- property public final androidx.glance.unit.ColorProvider backgroundColor;
- property public final String header;
- property public final String headline;
- property public final androidx.glance.template.TemplateImageWithDescription image;
- property public final androidx.glance.template.TemplateImageWithDescription logo;
- property public final String title;
+ ctor public GalleryTemplateData(optional androidx.glance.template.HeaderBlock? header, androidx.glance.template.TextBlock mainTextBlock, androidx.glance.template.ImageBlock mainImageBlock, optional androidx.glance.template.ActionBlock? mainActionBlock, androidx.glance.template.ImageBlock galleryImageBlock);
+ method public androidx.glance.template.ImageBlock getGalleryImageBlock();
+ method public androidx.glance.template.HeaderBlock? getHeader();
+ method public androidx.glance.template.ActionBlock? getMainActionBlock();
+ method public androidx.glance.template.ImageBlock getMainImageBlock();
+ method public androidx.glance.template.TextBlock getMainTextBlock();
+ property public final androidx.glance.template.ImageBlock galleryImageBlock;
+ property public final androidx.glance.template.HeaderBlock? header;
+ property public final androidx.glance.template.ActionBlock? mainActionBlock;
+ property public final androidx.glance.template.ImageBlock mainImageBlock;
+ property public final androidx.glance.template.TextBlock mainTextBlock;
+ }
+
+ public final class HeaderBlock {
+ ctor public HeaderBlock(androidx.glance.template.TemplateText text, optional androidx.glance.template.TemplateImageWithDescription? icon, optional androidx.glance.template.ActionBlock? actionBlock);
+ method public androidx.glance.template.ActionBlock? getActionBlock();
+ method public androidx.glance.template.TemplateImageWithDescription? getIcon();
+ method public androidx.glance.template.TemplateText getText();
+ property public final androidx.glance.template.ActionBlock? actionBlock;
+ property public final androidx.glance.template.TemplateImageWithDescription? icon;
+ property public final androidx.glance.template.TemplateText text;
+ }
+
+ public final class ImageBlock {
+ ctor public ImageBlock(optional java.util.List<androidx.glance.template.TemplateImageWithDescription> images, optional int aspectRatio, optional int size, optional int priority);
+ method public int getAspectRatio();
+ method public java.util.List<androidx.glance.template.TemplateImageWithDescription> getImages();
+ method public int getPriority();
+ method public int getSize();
+ property public final int aspectRatio;
+ property public final java.util.List<androidx.glance.template.TemplateImageWithDescription> images;
+ property public final int priority;
+ property public final int size;
+ }
+
+ @kotlin.jvm.JvmInline public final value class ImageSize {
+ field public static final androidx.glance.template.ImageSize.Companion Companion;
+ }
+
+ public static final class ImageSize.Companion {
+ method public int getLarge();
+ method public int getMedium();
+ method public int getSmall();
+ property public final int Large;
+ property public final int Medium;
+ property public final int Small;
}
@kotlin.jvm.JvmInline public final value class ListStyle {
@@ -546,20 +615,11 @@
}
public final class TemplateText {
- ctor public TemplateText(String text, androidx.glance.template.TemplateText.Type type);
+ ctor public TemplateText(String text, optional int type);
method public String getText();
- method public androidx.glance.template.TemplateText.Type getType();
+ method public int getType();
property public final String text;
- property public final androidx.glance.template.TemplateText.Type type;
- }
-
- public enum TemplateText.Type {
- method public static androidx.glance.template.TemplateText.Type valueOf(String name) throws java.lang.IllegalArgumentException;
- method public static androidx.glance.template.TemplateText.Type[] values();
- enum_constant public static final androidx.glance.template.TemplateText.Type Body;
- enum_constant public static final androidx.glance.template.TemplateText.Type Display;
- enum_constant public static final androidx.glance.template.TemplateText.Type Label;
- enum_constant public static final androidx.glance.template.TemplateText.Type Title;
+ property public final int type;
}
public final class TemplateTextButton extends androidx.glance.template.TemplateButton {
@@ -568,6 +628,35 @@
property public final String text;
}
+ public final class TextBlock {
+ ctor public TextBlock(androidx.glance.template.TemplateText text1, optional androidx.glance.template.TemplateText? text2, optional androidx.glance.template.TemplateText? text3, optional int priority);
+ method public int getPriority();
+ method public androidx.glance.template.TemplateText getText1();
+ method public androidx.glance.template.TemplateText? getText2();
+ method public androidx.glance.template.TemplateText? getText3();
+ property public final int priority;
+ property public final androidx.glance.template.TemplateText text1;
+ property public final androidx.glance.template.TemplateText? text2;
+ property public final androidx.glance.template.TemplateText? text3;
+ }
+
+ @kotlin.jvm.JvmInline public final value class TextType {
+ field public static final androidx.glance.template.TextType.Companion Companion;
+ }
+
+ public static final class TextType.Companion {
+ method public int getBody();
+ method public int getDisplay();
+ method public int getHeadline();
+ method public int getLabel();
+ method public int getTitle();
+ property public final int Body;
+ property public final int Display;
+ property public final int Headline;
+ property public final int Label;
+ property public final int Title;
+ }
+
}
package androidx.glance.text {
diff --git a/glance/glance/api/restricted_current.txt b/glance/glance/api/restricted_current.txt
index bac023a..8dbc0bb 100644
--- a/glance/glance/api/restricted_current.txt
+++ b/glance/glance/api/restricted_current.txt
@@ -410,6 +410,42 @@
package androidx.glance.template {
+ public final class ActionBlock {
+ ctor public ActionBlock(optional java.util.List<? extends androidx.glance.template.TemplateButton> actionButtons, optional int type);
+ method public java.util.List<androidx.glance.template.TemplateButton> getActionButtons();
+ method public int getType();
+ property public final java.util.List<androidx.glance.template.TemplateButton> actionButtons;
+ property public final int type;
+ }
+
+ @kotlin.jvm.JvmInline public final value class AspectRatio {
+ field public static final androidx.glance.template.AspectRatio.Companion Companion;
+ }
+
+ public static final class AspectRatio.Companion {
+ method public int getRatio16x9();
+ method public int getRatio1x1();
+ method public int getRatio2x3();
+ property public final int Ratio16x9;
+ property public final int Ratio1x1;
+ property public final int Ratio2x3;
+ }
+
+ @kotlin.jvm.JvmInline public final value class ButtonType {
+ field public static final androidx.glance.template.ButtonType.Companion Companion;
+ }
+
+ public static final class ButtonType.Companion {
+ method public int getFab();
+ method public int getIcon();
+ method public int getText();
+ method public int getTextIcon();
+ property public final int Fab;
+ property public final int Icon;
+ property public final int Text;
+ property public final int TextIcon;
+ }
+
public final class CompositionLocalsKt {
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.glance.color.ColorProviders> getLocalTemplateColors();
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.glance.template.TemplateMode> getLocalTemplateMode();
@@ -436,19 +472,52 @@
}
public final class GalleryTemplateData {
- ctor public GalleryTemplateData(String header, String title, String headline, androidx.glance.template.TemplateImageWithDescription image, androidx.glance.template.TemplateImageWithDescription logo, androidx.glance.unit.ColorProvider backgroundColor);
- method public androidx.glance.unit.ColorProvider getBackgroundColor();
- method public String getHeader();
- method public String getHeadline();
- method public androidx.glance.template.TemplateImageWithDescription getImage();
- method public androidx.glance.template.TemplateImageWithDescription getLogo();
- method public String getTitle();
- property public final androidx.glance.unit.ColorProvider backgroundColor;
- property public final String header;
- property public final String headline;
- property public final androidx.glance.template.TemplateImageWithDescription image;
- property public final androidx.glance.template.TemplateImageWithDescription logo;
- property public final String title;
+ ctor public GalleryTemplateData(optional androidx.glance.template.HeaderBlock? header, androidx.glance.template.TextBlock mainTextBlock, androidx.glance.template.ImageBlock mainImageBlock, optional androidx.glance.template.ActionBlock? mainActionBlock, androidx.glance.template.ImageBlock galleryImageBlock);
+ method public androidx.glance.template.ImageBlock getGalleryImageBlock();
+ method public androidx.glance.template.HeaderBlock? getHeader();
+ method public androidx.glance.template.ActionBlock? getMainActionBlock();
+ method public androidx.glance.template.ImageBlock getMainImageBlock();
+ method public androidx.glance.template.TextBlock getMainTextBlock();
+ property public final androidx.glance.template.ImageBlock galleryImageBlock;
+ property public final androidx.glance.template.HeaderBlock? header;
+ property public final androidx.glance.template.ActionBlock? mainActionBlock;
+ property public final androidx.glance.template.ImageBlock mainImageBlock;
+ property public final androidx.glance.template.TextBlock mainTextBlock;
+ }
+
+ public final class HeaderBlock {
+ ctor public HeaderBlock(androidx.glance.template.TemplateText text, optional androidx.glance.template.TemplateImageWithDescription? icon, optional androidx.glance.template.ActionBlock? actionBlock);
+ method public androidx.glance.template.ActionBlock? getActionBlock();
+ method public androidx.glance.template.TemplateImageWithDescription? getIcon();
+ method public androidx.glance.template.TemplateText getText();
+ property public final androidx.glance.template.ActionBlock? actionBlock;
+ property public final androidx.glance.template.TemplateImageWithDescription? icon;
+ property public final androidx.glance.template.TemplateText text;
+ }
+
+ public final class ImageBlock {
+ ctor public ImageBlock(optional java.util.List<androidx.glance.template.TemplateImageWithDescription> images, optional int aspectRatio, optional int size, optional int priority);
+ method public int getAspectRatio();
+ method public java.util.List<androidx.glance.template.TemplateImageWithDescription> getImages();
+ method public int getPriority();
+ method public int getSize();
+ property public final int aspectRatio;
+ property public final java.util.List<androidx.glance.template.TemplateImageWithDescription> images;
+ property public final int priority;
+ property public final int size;
+ }
+
+ @kotlin.jvm.JvmInline public final value class ImageSize {
+ field public static final androidx.glance.template.ImageSize.Companion Companion;
+ }
+
+ public static final class ImageSize.Companion {
+ method public int getLarge();
+ method public int getMedium();
+ method public int getSmall();
+ property public final int Large;
+ property public final int Medium;
+ property public final int Small;
}
@kotlin.jvm.JvmInline public final value class ListStyle {
@@ -546,20 +615,11 @@
}
public final class TemplateText {
- ctor public TemplateText(String text, androidx.glance.template.TemplateText.Type type);
+ ctor public TemplateText(String text, optional int type);
method public String getText();
- method public androidx.glance.template.TemplateText.Type getType();
+ method public int getType();
property public final String text;
- property public final androidx.glance.template.TemplateText.Type type;
- }
-
- public enum TemplateText.Type {
- method public static androidx.glance.template.TemplateText.Type valueOf(String name) throws java.lang.IllegalArgumentException;
- method public static androidx.glance.template.TemplateText.Type[] values();
- enum_constant public static final androidx.glance.template.TemplateText.Type Body;
- enum_constant public static final androidx.glance.template.TemplateText.Type Display;
- enum_constant public static final androidx.glance.template.TemplateText.Type Label;
- enum_constant public static final androidx.glance.template.TemplateText.Type Title;
+ property public final int type;
}
public final class TemplateTextButton extends androidx.glance.template.TemplateButton {
@@ -568,6 +628,35 @@
property public final String text;
}
+ public final class TextBlock {
+ ctor public TextBlock(androidx.glance.template.TemplateText text1, optional androidx.glance.template.TemplateText? text2, optional androidx.glance.template.TemplateText? text3, optional int priority);
+ method public int getPriority();
+ method public androidx.glance.template.TemplateText getText1();
+ method public androidx.glance.template.TemplateText? getText2();
+ method public androidx.glance.template.TemplateText? getText3();
+ property public final int priority;
+ property public final androidx.glance.template.TemplateText text1;
+ property public final androidx.glance.template.TemplateText? text2;
+ property public final androidx.glance.template.TemplateText? text3;
+ }
+
+ @kotlin.jvm.JvmInline public final value class TextType {
+ field public static final androidx.glance.template.TextType.Companion Companion;
+ }
+
+ public static final class TextType.Companion {
+ method public int getBody();
+ method public int getDisplay();
+ method public int getHeadline();
+ method public int getLabel();
+ method public int getTitle();
+ property public final int Body;
+ property public final int Display;
+ property public final int Headline;
+ property public final int Label;
+ property public final int Title;
+ }
+
}
package androidx.glance.text {
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/template/GalleryTemplateData.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/template/GalleryTemplateData.kt
index db61ba0..381b851 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/template/GalleryTemplateData.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/template/GalleryTemplateData.kt
@@ -16,26 +16,31 @@
package androidx.glance.template
-import androidx.glance.unit.ColorProvider
-
/**
* The semantic data required to build Gallery Template layouts
*
- * @param header header of the template
- * @param title title of the template
- * @param headline headline of the template
- * @param image image of the template
- * @param logo logo of the template
- * @param backgroundColor The background color to apply to the template
+ * @param header The header of the template.
+ * @param mainTextBlock The head block for title, body, and other texts of the main gallery object.
+ * @param mainImageBlock The head block for an image of the main gallery object.
+ * @param mainActionBlock The head block for a list of action buttons for the main gallery object.
+ * @param galleryImageBlock The gallery block for a list of gallery images.
*/
class GalleryTemplateData(
- val header: String,
- val title: String,
- val headline: String,
- val image: TemplateImageWithDescription,
- val logo: TemplateImageWithDescription,
- val backgroundColor: ColorProvider,
+ val header: HeaderBlock? = null,
+ val mainTextBlock: TextBlock,
+ val mainImageBlock: ImageBlock,
+ val mainActionBlock: ActionBlock? = null,
+ val galleryImageBlock: ImageBlock,
) {
+ override fun hashCode(): Int {
+ var result = mainTextBlock.hashCode()
+ result = 31 * result + (header?.hashCode() ?: 0)
+ result = 31 * result + mainImageBlock.hashCode()
+ result = 31 * result + (mainActionBlock?.hashCode() ?: 0)
+ result = 31 * result + galleryImageBlock.hashCode()
+ return result
+ }
+
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@@ -43,22 +48,11 @@
other as GalleryTemplateData
if (header != other.header) return false
- if (title != other.title) return false
- if (headline != other.headline) return false
- if (image != other.image) return false
- if (logo != other.logo) return false
- if (backgroundColor != other.backgroundColor) return false
+ if (mainTextBlock != other.mainTextBlock) return false
+ if (mainImageBlock != other.mainImageBlock) return false
+ if (mainActionBlock != other.mainActionBlock) return false
+ if (galleryImageBlock != other.galleryImageBlock) return false
return true
}
-
- override fun hashCode(): Int {
- var result = header.hashCode()
- result = 31 * result + title.hashCode()
- result = 31 * result + headline.hashCode()
- result = 31 * result + image.hashCode()
- result = 31 * result + logo.hashCode()
- result = 31 * result + backgroundColor.hashCode()
- return result
- }
}
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/template/GlanceTemplate.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/template/GlanceTemplate.kt
index d4825c3..04d38a3 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/template/GlanceTemplate.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/template/GlanceTemplate.kt
@@ -16,10 +16,10 @@
package androidx.glance.template
-import androidx.glance.action.Action
-import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
import androidx.glance.ImageProvider
+import androidx.glance.action.Action
// TODO: Expand display context to include features other than orientation
/** The glanceable display orientation */
@@ -32,10 +32,10 @@
/**
* Contains the information required to display a string on a template.
*
- * @param text string to be displayed
- * @param type the [Type] of the item, used for styling
+ * @param text The string to be displayed.
+ * @param type The [TextType] of the item, used for styling. Default to [TextType.Title].
*/
-class TemplateText(val text: String, val type: Type) {
+class TemplateText(val text: String, val type: TextType = TextType.Title) {
override fun hashCode(): Int {
var result = text.hashCode()
@@ -54,17 +54,6 @@
return true
}
-
- /**
- * The text types that can be used with templates. Types are set on [TemplateText] items and
- * can be used by templates to determine text styling.
- */
- enum class Type {
- Display,
- Title,
- Label,
- Body
- }
}
/**
@@ -95,9 +84,9 @@
}
/**
- * Contains the information required to display a button on a template.
+ * Base class for a button taking an [Action] without display oriented information.
*
- * @param action The onClick action
+ * @param action The action to take when this button is clicked.
*/
sealed class TemplateButton(val action: Action) {
@@ -151,3 +140,252 @@
return image == (other as TemplateImageButton).image
}
}
+
+/**
+ * A block of text with up to three different [TextType] of text lines that are displayed by the
+ * text index order (for example, text1 is displayed first). The block also has a priority number
+ * relative to other blocks such as an [ImageBlock]. Lower numbered block has higher priority to be
+ * displayed first.
+ *
+ * @param text1 The text displayed first within the block.
+ * @param text2 The text displayed second within the block.
+ * @param text3 The text displayed third within the block.
+ * @param priority The display priority number relative to other blocks. Default to the highest: 0.
+ */
+class TextBlock(
+ val text1: TemplateText,
+ val text2: TemplateText? = null,
+ val text3: TemplateText? = null,
+ val priority: Int = 0,
+) {
+ override fun hashCode(): Int {
+ var result = text1.hashCode()
+ result = 31 * result + (text2?.hashCode() ?: 0)
+ result = 31 * result + (text3?.hashCode() ?: 0)
+ result = 31 * result + priority.hashCode()
+ return result
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as TextBlock
+
+ if (text1 != other.text1) return false
+ if (text2 != other.text2) return false
+ if (text3 != other.text3) return false
+ if (priority != other.priority) return false
+
+ return true
+ }
+}
+
+/**
+ * A block of image sequence by certain size and aspect ratio preferences and display priority
+ * relative to other blocks such as a [TextBlock].
+ *
+ * @param images The sequence of images or just one image for display. Default to empty list.
+ * @param aspectRatio The preferred aspect ratio of the images. Default to [AspectRatio.Ratio1x1].
+ * @param size The preferred size type of the images. Default to [ImageSize.Small].
+ * @param priority The display priority number relative to other blocks such as a [TextBlock].
+ * Default to the highest priority number 0.
+ */
+class ImageBlock(
+ val images: List<TemplateImageWithDescription> = listOf(),
+ val aspectRatio: AspectRatio = AspectRatio.Ratio1x1,
+ val size: ImageSize = ImageSize.Small,
+ val priority: Int = 0,
+) {
+ override fun hashCode(): Int {
+ var result = images.hashCode()
+ result = 31 * result + aspectRatio.hashCode()
+ result = 31 * result + size.hashCode()
+ result = 31 * result + priority.hashCode()
+ return result
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as ImageBlock
+
+ if (images != other.images) return false
+ if (aspectRatio != other.aspectRatio) return false
+ if (size != other.size) return false
+ if (priority != other.priority) return false
+
+ return true
+ }
+}
+
+/**
+ * Block of action list of text or image buttons.
+ *
+ * @param actionButtons The list of action buttons. Default to empty list.
+ * @param type The type of action buttons. Default to [ButtonType.Icon]
+ */
+class ActionBlock(
+ val actionButtons: List<TemplateButton> = listOf(),
+ val type: ButtonType = ButtonType.Icon,
+) {
+ override fun hashCode(): Int {
+ var result = actionButtons.hashCode()
+ result = 31 * result + type.hashCode()
+ return result
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as ActionBlock
+
+ if (actionButtons != other.actionButtons) return false
+ if (type != other.type) return false
+
+ return true
+ }
+}
+
+/**
+ * A header for the whole template.
+ *
+ * @param text The header text.
+ * @param icon The header image icon.
+ * @param actionBlock The header action buttons.
+ */
+class HeaderBlock(
+ val text: TemplateText,
+ val icon: TemplateImageWithDescription? = null,
+ val actionBlock: ActionBlock? = null,
+) {
+ override fun hashCode(): Int {
+ var result = text.hashCode()
+ result = 31 * result + (icon?.hashCode() ?: 0)
+ result = 31 * result + (actionBlock?.hashCode() ?: 0)
+ return result
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as HeaderBlock
+
+ if (text != other.text) return false
+ if (icon != other.icon) return false
+ if (actionBlock != other.actionBlock) return false
+
+ return true
+ }
+}
+
+/**
+ * The aspect ratio of an image.
+ */
+@JvmInline
+value class AspectRatio private constructor(private val value: Int) {
+ companion object {
+ /**
+ * The aspect ratio of 1 x 1.
+ */
+ val Ratio1x1: AspectRatio = AspectRatio(0)
+
+ /**
+ * The aspect ratio of 16 x 9.
+ */
+ val Ratio16x9: AspectRatio = AspectRatio(1)
+
+ /**
+ * The aspect ratio of 2 x 3.
+ */
+ val Ratio2x3: AspectRatio = AspectRatio(2)
+ }
+}
+
+/**
+ * The relative image size as a hint.
+ */
+@JvmInline
+value class ImageSize private constructor(private val value: Int) {
+ companion object {
+ /**
+ * Relative small sized image.
+ */
+ val Small: ImageSize = ImageSize(0)
+
+ /**
+ * Relative medium sized image.
+ */
+ val Medium: ImageSize = ImageSize(1)
+
+ /**
+ * Relative large sized image.
+ */
+ val Large: ImageSize = ImageSize(2)
+ }
+}
+
+/**
+ * The type of button such as FAB/Icon/Text/IconText types
+ */
+@JvmInline
+value class ButtonType private constructor(private val value: Int) {
+ companion object {
+ /**
+ * FAB (Floating Action Button) type of image button.
+ */
+ val Fab: ButtonType = ButtonType(0)
+
+ /**
+ * Icon image button type.
+ */
+ val Icon: ButtonType = ButtonType(1)
+
+ /**
+ * Text button type.
+ */
+ val Text: ButtonType = ButtonType(2)
+
+ /**
+ * Button with Text and Icon type.
+ */
+ val TextIcon: ButtonType = ButtonType(3)
+ }
+}
+
+/**
+ * The text types that can be used with templates such as set in [TemplateText] items to determine
+ * text styling.
+ */
+@JvmInline
+value class TextType private constructor(private val value: Int) {
+ companion object {
+ /**
+ * The text is for display with large font size.
+ */
+ val Display: TextType = TextType(0)
+
+ /**
+ * The text is for title content with medium font size.
+ */
+ val Title: TextType = TextType(1)
+
+ /**
+ * The text is for label content with small font size.
+ */
+ val Label: TextType = TextType(2)
+
+ /**
+ * The text is for body content with small font size.
+ */
+ val Body: TextType = TextType(3)
+
+ /**
+ * The text is headline with small font size.
+ */
+ val Headline: TextType = TextType(4)
+ }
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 97b429e..dc99e9d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -150,7 +150,7 @@
multidex = { module = "androidx.multidex:multidex", version = "2.0.1" }
nullaway = { module = "com.uber.nullaway:nullaway", version = "0.3.7" }
okhttpMockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version = "3.14.7" }
-okio = { module = "com.squareup.okio:okio", version = "3.0.0" }
+okio = { module = "com.squareup.okio:okio", version = "3.1.0" }
playCore = { module = "com.google.android.play:core", version = "1.10.3" }
playServicesBase = { module = "com.google.android.gms:play-services-base", version = "17.0.0" }
playServicesBasement = { module = "com.google.android.gms:play-services-basement", version = "17.0.0" }
diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys
index 5d3ed73..05c098b 100644
--- a/gradle/verification-keyring.keys
+++ b/gradle/verification-keyring.keys
@@ -323,6 +323,43 @@
-----END PGP PUBLIC KEY BLOCK-----
+pub 07D3516820BCF6B1
+uid Ben Manes <[email protected]>
+
+sub 11F4CE313A637CC1
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+mQENBF3HgdMBCAC3ET5ipFXdZ9GGMbtsCQ3HGT40saajsNDOdov2nMJxzKkVe3wk
+sN3bpgbsqBU9ykVkIhX8zV5+v8DOBzkV0pJ2eLjFa9jBPvNjV+KoK2BAI5pzNzYg
+sHPwo1aRXdI0MvCy+7iaIiiGF4/O16AhU4LmALHnaRQZCyuN6VOQ8rlqNvcczwUf
+J2DQeLHqR/tsch7S01hGpPAptBeu19PyAlQsntYN0yLCLKoe9dFXWCDkvd1So5LF
+6So+ryPqupumBbh4WxCmTp9qwDJYJItjAE0zyPe890FurOtxrFTwtRtX6d6qGKkY
+/B4T3r0tTE1EiOUpmSnxmGNItMh7/l5UtnHjABEBAAG0H0JlbiBNYW5lcyA8YmVu
+Lm1hbmVzQGdtYWlsLmNvbT6JAU4EEwEIADgWIQRjXuYnNF88HdQisuIH01FoILz2
+sQUCXceB0wIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRAH01FoILz2sdoo
+B/0YUh73jUMl14MjWvp9zrFHN8h+LqB4NMQcP93RdPTtDKi0a+0h8gQtm0D+K49Q
+BQbFztOObfZS3kdJ3VOqmodScWrGtMU3HsYT2ioQalqbYvl9FIPDrlOjHaZgwgyJ
+We0DVKHRApbtIh+NxTpQUJtanxgF60ZtOoToZe8XMGc9LaCZcrFxK/AlMdDMgUCx
+qzBbXhAcvut2bJVL5B4kLNMABrbUuFMjTNI4JxvgTXKL/jNk6XPtCjdmgIh7mT/G
+Mpu9t3i1zegAPdM5N/MAgiGHqm+blANLniSAbZja8Ny7211fwOYoJ546VPwDjL7B
+rBlymB3COoYZhql2DcBBg39cuQENBF3HgdMBCACu3VQKKmagcPbcMZOqbDXE5iK3
+0G742rCpf/j3ywnwTZJQ/58HtAi8+/fXxUhTHswoON2TwiiHrHAkObe+K9A+jv0E
+xjKVMmQ/sOCYWZDEGMth4yJnzDbT1Tlm/l2i5Lv0ZaD7fTEhtprQNuU06dveTeJs
+zDyqtK9T80mvI4+GH59wM80l1y6uj8KA4pY0PdSFgbyS9iAFADGsUsc6t1KiZ5W1
+9odMjDPlQtJ20pm5CvJlDZbYNRJ54CSldZikRvmNRg5mWdRLNfbRMFDLFfcdYLdO
+WJXnAt9cKFJC9P//ItZFrlhu3akTH//HF2kxQNW61Sd92/xtFUD/2tN1GlXfABEB
+AAGJATYEGAEIACAWIQRjXuYnNF88HdQisuIH01FoILz2sQUCXceB0wIbDAAKCRAH
+01FoILz2saySCACibIpnls5wJkfX1B/7tDjWk2hEGZYcASr0xp/DDwSgJ5edByuQ
+NQF7RHuCk0ke6IQGfytMLJlXeEIu79DvgPakxBP5iG+c095FbhRu+9nCEkRqQvop
+4fA7ZdhuerOyuObWz8+o3Z2RywWPXlK+F/9iJiO/qtvmdORuikJtN9VxgvAUvANZ
+RtlzjL296p0TJzGqXhyer46CHl/Yj7TtX6EpnZDgiaQbOWRFOZ5x81xI79bQD7Ew
+DzfrwQHbjQDkqhkwOoV6Wq239ZaHh6p7GXHnQkDMQ0H/7Y2tw6PH5VM8fDJkJKF2
+PIukJrUXa06KqrdZ9YxqvSmu5UY6tMSRwGWp
+=/wFN
+-----END PGP PUBLIC KEY BLOCK-----
+
+
pub 083891AD4774845A
uid Eclipse Project for JAXB <[email protected]>
@@ -3715,6 +3752,46 @@
-----END PGP PUBLIC KEY BLOCK-----
+pub 5B05CCDE140C2876
+sub 9D29AE4A6B50E01F
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+mQMuBEwVZOURCADNnKQzSjFuI9/IGj3WTJcPU2B/H8NbZaTsz5WE91WumgZulK2q
+YeD4u6zdOyFK7DEScgxk7dicox9cNEgYKQnQXctDhfqER9bnvA2iJ+AFxjRAWyvs
+en3ClYLXT5UVx0H1ZfDVKCvmaZVirZInfkqbi3OiPQoWrUfu02c3DiHQJ+Y34kdB
+egH2sIShNH8WLfEZ3YDQ4XaWHVuN1C7VwCBM8R3OeTTfyDrTsuyqJ0SeZXRR/6df
+2pEckjF9DNSXyjzFg24MrZhuhgbnj0oR1zmRh1EF+KlBfF4DF4acfxWqqcJVJx/3
+FTtOkLe3Xjj+inyJgxOW52gD4DsJpyf1tIbjAQDZvOdlRRCqZB4FnzzIb/1GmkGD
+JpDLC4MQmqgxkm0n8wgAmmHLpqDTdmuyJXvdX9RdGycpW64sljd1mpzTWJ8UKDhj
+uFQVHSSEdLoHoVj8ItnBV2kXd2uoQ/tWzbxFBST7wE/tX0e9G5XWaPKogvOKeDus
+u9XTIds2krYp80UTYWFZ88oNwGikdIrEYURSYDsYt15miROtKHWbSOHeLVbZqgVx
+dtWPqQVfH4gJGEH97/OSmozqDVog1aZDKBLGZQosng5h4j2RAQpjkaIdxKl8m7CQ
+x0Yi9tA8yD1QhRGggANQIb4n00G/vm7RMU/7NBvvjAQ/nAFjbsyO5oX1rBY1M6Xo
+NFqIBrHSBzV9MmhS3nXU+ZjAktCRhyJ7TsoHM0OYEAf8CduM5Zzp5w02iVYkFQBB
+wAoKHMpycW5LhMMMS4w7gmOfP7y04rtg6+zVe41y6bOqn/SxHCcCgnE/nhdexlzH
+ElYE1H7+HpphoI5vEwS6uElF67CoO5r74Zrb6nshGEj2AoOqjbrsdQm0noBBNYAu
+f9RsjU0sQQFzLW8+2xahqK3oZkLWOkSxzLtVwJbm7EGaGIYxEBjg87OnGQkAi9vv
+tVPwdO3VWyvgKLuPHudLDhTpeH3AMbzKgnru1Pnh/ZpiRhPzsbuFtFPEX8PMuCyE
+n4OLzUALl98kXuPjG5ww+24UsNgKMbKbu8qq/zRu7IHlpZvd730RoCWU2/i18tnY
+zLkCDQRMFWTlEAgA+MQFGIhyA4Ww9g7J8ZiEltwSzRblrjM1q9anexsBIGsWH37A
+92rlVK1RzMVfhj5yl+BzIBGO+zHbgycX7iB5/Fwsm+6R/2Uich6NDm1Qai9rc/jg
+3MS0phOAQzgxlGKOTS2GzdbDJCBQMijDObNe+Cs5DNB/E29/nzzCTQvtRzSeplZN
+r+8Q8lWz6efXmm5EeeZxN4x1YXjjzMJCHbc3yGxOjTgYQOs962yUYsg9UDRJm1OH
+9NKZe1m3dTRIMUcZvL12dq/kyiHHR9V/6CkdiNw1AFMi3tvEdvX4D1k1/Qr/2ORZ
+E4lRzgug4sKkpgaclLnkJZ9EMczmUFTGbbkx3wADBQf/Y+2nZCJSuHiDv/+SdhQh
+OBapZ2hYPDvg29mpPqin/LwH7eFTNv/oos1wzuzGtTHHGEP5mUQLOxjwdAXsWMMj
+scSbCs66ytTN7X4O8qh+1yN7vrM6ZBL12Ix7Ku40cgkWyvTVLBXKaEGm4ElhAmSL
+Fpu+/fJw0riR6rIuwHcGB4R1IJtMWcj+b1odgw9QmJ8AGpHh2WVdXspoCGnTUN4m
+DEswZjplkKXCgLypU13SrHVOqhjd4caK5GNZUfWtCKtwNcJMnvgp2truMvh9BBn6
+widfK48hEknQtXzGjui+bZz2/AD7/OT/T1CqDspB8IQlBCMBn8J4U1grSrZ1wTJf
+HIhnBBgRCAAPBQJMFWTlAhsMBQkLRzUAAAoJEFsFzN4UDCh23wsBANDSDn2KWz7H
+b5geDwUTX4T8Uqn21eFbp54tFTfopCd/AP4nTdX1iahsClr9q6G+CWQBuQWHVmq3
+FlPU/jTn6vXQwA==
+=dKtU
+-----END PGP PUBLIC KEY BLOCK-----
+
+
pub 5D67BFFCBA1F9A39
sub DBE749136BF76809
-----BEGIN PGP PUBLIC KEY BLOCK-----
@@ -3833,6 +3910,39 @@
-----END PGP PUBLIC KEY BLOCK-----
+pub DEE12B9896F97E34
+sub 9A716F957BC42546
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+mQENBFAxQKwBCADJGPv6pmFEq0SDwAKESEgCdnXycbR0bNXpNa/3VGboNto1xKgd
+AQ/sI5x+CmN0hpUjklEwff6QIt3MlofEMkAzSfRmTobhJTK9W7r4+p5DuhJpi5Wz
+ITdbNCMT3Cvp13rRE+dx9qY+WFQmTYPf3gq+C6T8Q1i35ePNlCTN2RayaFxxR77D
+W93zKZDdd7I1qH0Vx7GGcSwBgBlEB8jmhNAkz/zAhv53S6px3ZttqYYmuwRtg6Fi
+i/u9VoDR/c9tyUq8L6oAUtg0mo4CP/tfUF/uZnibshEsLzbRP961VQXduhn8HcRp
+k6QPTj37B1vsNWJ9U7XXJ6pYnkizQo7sl5XxABEBAAG5AQ0EUDFArAEIALyNR+z1
+eBBF4S+dOEWKXz2ANmsp6RRhvR09QeQwNycVdbdEXpOiSZUCAkw/EhuJWmHBngat
+0KBO+7CIHyQqwHnqyatizzKXi1OuaEhMzPsQMwPRfYyWHgN0aklc5oOzB2RbSJN4
+et/oVvfAplVSjgR0v+56+qXw9TFlp4kxqFeJLycZ+5ImKQ+XclsBokKuE7cjeF+g
+O5oY/CFHdkxD8d+cLF8FSNUFMypuDQ4IH9zPYGkUJqsb2t67iMyxi14RqyN2YNqK
+JcwxTL42VBlUFlTBoF2Y3w0LNll6pR2WSNvpcj+5/uBjtY1qAj5e7yVts+d1YZsX
+7D76AV742RQ31kkAEQEAAYkCRAQYAQIADwUCUDFArAIbLgUJB4YfgAEpCRDe4SuY
+lvl+NMBdIAQZAQIABgUCUDFArAAKCRCacW+Ve8QlRhFDB/9xE/cXf5fVaLa598xL
+muXiD9U1B04dPdz445/chdDS9iGWBB+5QVvAqv2Jt0hyPN0+n9Mk/4lLStEEL8TP
+NLdTBP1JRvVWC1c+G3kTJq05Abj8CGFFm1UZhFRwCTJ+vrv8fSb15s+YYxBLIUdl
+tKld6OupTHm8A4XJQOhYxd5PHs72bJ3bXs4GmPLKD/RpYmXYJ9EZHQHKnrhZKJ8R
+JKTM6sxBrgdVeI1K0ekA0o5HAVpNEXgY1gG8Pa14jqK0iwlcI02ntqeJkobvv1wN
+vh+nJT2wM5QyLH737kdPrUdi63PfCYLOEHYhI6sFkzI/DAtI/C3wmHtTuRam3aLs
+Rnb7GNQH/i07ndoI4trmUor3X1JBbcjw2BVS+idCtML3jhKtziwK2/kz0rJqBQKa
+Z/zxgEfwkRPqhXLaBW8a1G/d1mGphazHqSaDqylz07XqR31ZtGCc6256anaVbWaW
+9HXUsU5ADNrAK9PdD0EibGB8YumuSTtApICUqN5SVz+h3Mi1MXVsmbiVSAZPzLTD
+0YRwzPJ3jiXIrKDUmZMM7oWwGx6nzW++tW8aKyLKm7x1/y8g+XHvySQiVOKAvvxj
+yPStkEW38Rls5nucpyLzLjoA5vlyIcOkeKCy2jlUmM56YrAIWNn/eCRFPHMOY1DO
+B1nUXMr+2W21xZO+/sWrEEysY0mdGU0=
+=uzFx
+-----END PGP PUBLIC KEY BLOCK-----
+
+
pub 5F69AD087600B22C
uid Eric Bruneton <[email protected]>
@@ -9496,6 +9606,49 @@
-----END PGP PUBLIC KEY BLOCK-----
+pub D364ABAA39A47320
+sub 3F606403DCA455C8
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+mQINBGH0NlsBEACnLJ3vl/aV+4ytkJ6QSfDFHrwzSo1eEXyuFZ85mLijvgGuaKRr
+c9/lKed0MuyhLJ7YD752kcFCEIyPbjeqEFsBcgU/RWa1AEfaay4eMLBzLSOwCvhD
+m+1zSFswH2bOqeLSbFZPQ9sVIOzO6AInaOTOoecHChHnUztAhRIOIUYmhABJGiu5
+jCP5SStoXm8YtRWT1unJcduHQ51EztQe02k+RTratQ31OSkeJORle7k7cudCS+yp
+z5gTaS1Bx02v0Y8Qaw17vY9Pn8DmsECRvXL6K7ItX6zKkSdJYVGMtiF/kp4rg94I
+XodrlzrMGPGPga9fTcqMPvx/3ffwgIsgtgaKg7te++L3db/xx48XgZ2qYAU8GssE
+N14xRFQmr8sg+QiCIHL0Az88v9mILYOqgxa3RvQ79tTqAKwPg0o2w/wF/WU0Rw53
+mdNy9JTUjetWKuoTmDaXVZO4LQ2g4W2dQTbgHyomiIgV7BnLFUiqOLPo+imruSCs
+W31Arjpb8q6XGTwjySa8waJxHhyV2AvEdAHUIdNuhD4dmPKXszlfFZwXbo1OOuIF
+tUZ9lsOQiCpuO7IpIprLc8L9d1TRnCrfM8kxMbX4KVGajWL+c8FlLnUwR4gSxT1G
+qIgZZ09wL5QiTeGF3biS5mxvn+gF9ns2Ahr2QmMqA2k5AMBTJimmY/OSWwARAQAB
+uQINBGH0NlsBEAC9o6m+D2LubGjOJxLQB1BnfBOkFHadsbkb82QFdrCNsd44fJie
+aqZVP+6XHKVRHSPktwpE1FnjThBJJsLwwcvwWXwDwvED57n4bATPlrPGuG7x+LRV
+bxFBTd+LQUCcHd3puruvbEjQdV54mbgdMqAp5dSA4Fc6h2hMWVBX4EdLiH/0ui3l
+UoqYTJcB73U1/jbKcbs0+cVuXIpmAPQpIs30p0wWLOKiJqn9tTZpwfntnrdfLvKL
+3FZcRQeWZjqH1Ywt4zWlCRqGEp7yVqhK5gn4nfEdSX2koxr53OOsGk2Pjhzs/5XJ
+Li1FTOcnja5kkqOPiPGB/BxAnjPCEsSiOFmF3Af4WdYa3+TK8+ggBSEeLjjLa5zy
+qexfhADwgb5ASZitUErJZDhAvqHGwfz3VPENy3K2kJLH+maWwOT1ZRoJnz3fxwIu
+gKhPx1MzlwhTclIknK7q2CNcB61pC9lg70ICW090NgknE2DtmjrRMONhcSkuWGLZ
+BKBgRqNwITJFcAdg6+ffZzGLsnEd+6A29PdsXfLS9KJqiabvpiBg8RaAAWiv5Tqs
+Nu9YSWUQUzBZO43u8AxTtThuHYZrxasoC3sCGIcRy2V9eaq480DRJ9uotONMutIH
+UDVSdqViPmmit0+PyRiCX/DOeBHumaEOm+RqIxPE8h6W8sHrYAQ7J1a3AQARAQAB
+iQI2BBgBCgAgFiEE7gyocwdAkvgG9Ztl02SrqjmkcyAFAmH0NlsCGwwACgkQ02Sr
+qjmkcyAsehAAps6j+qpjyNGUet/B6Z7nJcobSxnCIP/c+uUPD1oB6Uuht6NTYWQd
+wmEqL5BGz8WNTsBd0cQYvSztrMiz5tCDoiGGrWcgWxrrNxc1EVydhBbT4PpiG6CB
+WFCoEXN76/f0ndxZbjjobElTXbQ6oaLh2812OavgMdiJUVBgXrtfgi5/h49Wpc5o
+/IDM3bfujfrn5nvPIkd7Ee+GaK2YSCT7pfK4N/eW1g1SusqRQxBKCU3C5MVgVjkp
+Ba82U0kTxUGDFYUUcS+Yjhi/w4uynwIXW0pSl5wvxVVxNBfGFH5fkprkpcuVXp9B
+6SRVM85uUoZJFaIFyoAhU9uQQfVe6ugwP9BbhzRzDpJe9tiOcaazwzNnP5Zj31nI
+V6UltZu7mVSl1JwIcWxW3b36p4Ht9G5jIPQc8xS+oMd//p8r4sYFB4KOYas1ukRN
+iCshn9tJfeohkKj9ewxyUNf1rS8uOUJvZC3c3XRF8CJXRpxmHu2pPNf0QxFVhghL
+Y2cJU1OWGi6NyZN65EdfmkTbeDxdlSNv89STD4Vp6MmFtrA4JZDSR0Bp1zEPKiSx
+jpG5FpfVv6lXmFboa5qkXAHG9+bcaRYoXun+wJ3ioWo+cQEdy/bsX03+MHMsms8l
+ikmfPIGVw73RF3HXjJ8GVqTkqbo4ZpgTw/7Z3+fAYE/vxquhnpl2HvE=
+=5tlI
+-----END PGP PUBLIC KEY BLOCK-----
+
+
pub D57506CD188FD842
sub 63F72A7A8658D653
-----BEGIN PGP PUBLIC KEY BLOCK-----
@@ -10915,3 +11068,50 @@
Pt2uco8an9pO9/oqU6vlZUr38w==
=alQS
-----END PGP PUBLIC KEY BLOCK-----
+
+
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Comment: Hostname:
+Version: Hockeypuck 2.1.0-152-ga266fd3
+
+xsDNBFJQhigBDADpuhND/VUQwJT0nnJxfjAIur59hyaZZ3Ph/KIgmCneyq7lzYO6
+xa1ucH8mqNBVNLLBhs4CjihBddU/ZKTX3WnZyhQKQMZr3Tg+TCNFmAR4/hnZ3NjZ
+N5N5gUj/dqVI2rIvypIuxUApl88BYMsxYpn2+8FKeMd8oBJLqFRJ3WNjB4Op2tRO
+XRWoxs1ypubS/IV1zkphHHpi6VSABlTyTWu4kXEj/1/GpsdtHRa9kvdWw7yKQbnM
+XuwOxtzZFJcyu0P2jYVfHHvxcjxuklc9edmCGdNxgKIoo0LXZOeFIi6OWtwzD0pn
+O6ovJ+PL9QscMdnQlPwsiCwjNUNue20GBv3aUIYc+Z8Gq0SqSan5V0IiKRHMJkzd
+FAhnpkSFBvHhPJn07BCcb1kctqL+xnLxIdi7arq3WNA/6bJjsojc/x3FdIvORIeP
+sqejhtL8mCBvbMAMHSBrFxclMp+HSz2ouHEEPIQam0KeN8t1yEqIy3/aYKMzHj9c
+C3s8XOaBCbJbKpMAEQEAAc09VG9iaWFzIFdhcm5la2UgKGZvciBkZXZlbG9wbWVu
+dCBwdXJwb3NlcykgPHQud2FybmVrZUBnbXgubmV0PsLBFgQTAQgAQAIbDwcLCQgH
+AwIBBhUIAgkKCwQWAgMBAh4BAheAFiEE1HfVGBLmkgEdsR5mpuouK/IuBUMFAl+f
+HewFCREQdggACgkQpuouK/IuBUPAjgv+IvGD8arZP2epxB10nNxehgdB3vVGRvCz
+AWyw/d56KBwGN1czmlHINP/Ejfh4bRZgFXILISqcf+8rATvISsCgKzzfluOfDuFR
+puqZisrlaqEpDqUGK2R8x7kxARaB2G3g4dy6xyJZwv/5dfFPQJ/aQjeNkRSoXI4W
+WLNexZB3E0Gx9a3F32Xvr87vu9GchsoftxQft9joFupRg+kCipQ+w36D9gWmFXtj
+pYT3Wdrm0AcP6lezq+SpcwVn3+DW79p0/WOLhRr6NNQsRBIuM5nNIbCt8hnj9ule
+PZGctzwCTY8suID4Ru18NOiU8NKztoXII7XRloB9v5ezwktKoDzwTBgwm2+XM/vv
+GFlB09LaICdiuPQaiqSZbeLKKmBT1hTEtEHiPdMld2Hlji/rVYS3Ceiv0YUoOnmo
+AAEmtAG7ghpIJxyVtWZchZ55Hrb4oU5AntshrwYMWNRe0toxjQds5Ds2I2lqkjeU
+paUjQXEmPDS1hnckKAxI2PiOeifiLljxwsEWBBMBAgBAAhsPBwsJCAcDAgEGFQgC
+CQoLBBYCAwECHgECF4AWIQTUd9UYEuaSAR2xHmam6i4r8i4FQwUCW5n2GQUJDSpo
+eAAKCRCm6i4r8i4FQ9byDAC6yPry/EBRyJgpWXgLca8Dy56Oe9XtRA+kuAxq+c3q
+GmLy8JdBYxWeBI/dnjwzU6jCLLnY6eTigjSemHZRMPOoyxXYF47LpaoWL52JDi4R
+7xft+GD5Hy+tbDlYW5RVeMzR2Okg3XpvTmsYlcgSr6HCL0L7D25tpcFZMZrls9LN
+z80HetFk4LrR1LvVL8GpFv74xyWullpQU2QwnwXCzUpsXa9qOzwZltNIUfs4gVNG
+KhzfabYmMtlBAXzpi20bRWmJY4W+vGJKC9yWL1L4iu7LrIgMedqsKoMrl4Bg8xKE
+JGU0JEHWgfRopSr0FccP1bxWOaoJ2iN/v3Lifrk0T24vBA9cbTrnQmwrbNftJBLb
+7ccgkvkaFk+8qBe5t/OFgoV5zvmJ6xNEojpFnOtLfrPVpu8b7t3mcGVq1jQJ8afa
+8yIlQrLsA+ubA71pqgdv2ZhoWvL3R2wyxZGMX3xefqavJNxaziHGQorddrg9dyEO
+0xqXKDzjN5vuDTgSJimmZiHCwP8EEwECACkFAlJQhigCGw8FCQlmr/gHCwkIBwMC
+AQYVCAIJCgsEFgIDAQIeAQIXgAAKCRCm6i4r8i4FQ//CC/oD2LxmXHedlqlKl5WU
+EEFoXjDRpcSnfOTFdCn9U5bpBxM2gtlxNB4890TVga6C9kGfgkf9e11/ftdFQgHQ
+2LQKwpRaPOQdfk8Ek/oONmO6x6oIYXrVvY57xsW5AiFHUtPd84NJBoAyTePxstrJ
+TrFo0KQ8wX84rsU2XF/5CRCUuvx+Xomv1ALEed8Ajf9dhY85UTwIWXFINKwMTbNC
+neoBeUy3xugYEYWZCkrIk/iUvwA2pwqCwzHeDRomf1OTwW3VZ0U9/cfFyt3RgkU5
+goF55YOIpnKAjSkyygESaAs4kPrMtAJ6gy8lKsBEpxQfJWH6c5Q6MZn3RVb2S5Dx
+vlpCeiKIqnKtX1DnZrCZntt4Dwrrt4aFemLJ7+iaYndbMun3mAxG6Nqm+CfEOicG
+uTmFS6yakutYNOxJrxtz7yEIIt6yr5T3fQk6LhczhjXpVlvExPutlIsbtVZSsSlE
+lFV5uuVOVYcfjnQJtuUj5JtwP6mhn0Njj/YiJPzG2ugpM0M=
+=BDYe
+-----END PGP PUBLIC KEY BLOCK-----
\ No newline at end of file
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index c8f18a2..f923e74 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -218,6 +218,7 @@
<trusting group="com.squareup.leakcanary"/>
</trusted-key>
<trusted-key id="62c82e50836eb3ee" group="com.github.gundy"/>
+ <trusted-key id="635ee627345f3c1dd422b2e207d3516820bcf6b1" group="com.github.ben-manes.caffeine"/>
<trusted-key id="6525fd70cc303655" group="org.codehaus.mojo"/>
<trusted-key id="666a4692ce11b7b3f4eb7b3410066a9707090cf9" group="org.javassist" name="javassist"/>
<trusted-key id="694621a7227d8d5289699830abe9f3126bb741c1">
@@ -257,6 +258,7 @@
<trusted-key id="79156e0351af8604de9b186b09a79e1e15a04694" group="org.vafer" name="jdependency"/>
<trusted-key id="7999befba1039e8b" group="net.bytebuddy"/>
<trusted-key id="7a8860944fad5f62" group="org.apache.commons"/>
+ <trusted-key id="7c669810892cbd3148fa92995b05ccde140c2876" group="org.eclipse.jgit"/>
<trusted-key id="7c7d8456294423ba" group="org.objenesis"/>
<trusted-key id="7cb548acfe3d47e92afa566dc29b11246382a4d7" group="com.charleskorn.kaml"/>
<trusted-key id="7cd52b5a8295137c88fb5748dddafa7674e54418" group="org.testng" name="testng"/>
@@ -373,6 +375,8 @@
<trusted-key id="c6f7d1c804c821f49af3bfc13ad93c3c677a106e" group="io.perfmark" name="perfmark-api"/>
<trusted-key id="c70b844f002f21f6d2b9c87522e44ac0622b91c3" group="com.beust" name="jcommander"/>
<trusted-key id="c7be5bcc9fec15518cfda882b0f3710fa64900e7">
+ <trusting group="com.google.auto"/>
+ <trusting group="com.google.auto.service"/>
<trusting group="com.google.auto.value"/>
<trusting group="com.google.code.gson"/>
</trusted-key>
@@ -398,6 +402,7 @@
<trusted-key id="cfae163b64ac9189" group="org.jetbrains.skiko"/>
<trusted-key id="d041cad2e452550f" group="com.google.protobuf"/>
<trusted-key id="d196a5e3e70732eeb2e5007f1861c322c56014b2" group="commons-lang"/>
+ <trusted-key id="d477d51812e692011db11e66a6ea2e2bf22e0543" group="io.github.java-diff-utils"/>
<trusted-key id="d4c89ea4aaf455fd88b22087efe8086f9e93774e" group="junit"/>
<trusted-key id="d4da5eab3cd7e958" group="com.google.devtools.ksp"/>
<trusted-key id="d4fb0b7b5e8c18c993a8a386eb9d04a9a679fe18" group="com.uber.nullaway" name="nullaway"/>
@@ -413,6 +418,7 @@
<trusted-key id="dddafa7674e54418" group="org.testng"/>
<trusted-key id="e0130a3ed5a2079e" group="org.webjars"/>
<trusted-key id="e0cb7823cfd00fbf" group="com.jakewharton.android.repackaged"/>
+ <trusted-key id="e0d98c5fd55a8af232290e58dee12b9896f97e34" group="org.pcollections"/>
<trusted-key id="e16ab52d79fd224f" group="com.google.api.grpc"/>
<trusted-key id="e62231331bca7e1f292c9b88c1b12a5d99c0729d" group="org.jetbrains"/>
<trusted-key id="e77417ac194160a3fabd04969a259c7ee636c5ed">
@@ -427,6 +433,7 @@
<trusted-key id="eb380dc13c39f675" group="com.intellij"/>
<trusted-key id="eb9d04a9a679fe18" group="com.uber.nullaway"/>
<trusted-key id="ecdfea3cb4493b94" group="jline"/>
+ <trusted-key id="ee0ca873074092f806f59b65d364abaa39a47320" group="com.google.errorprone"/>
<trusted-key id="ee9e7dc9d92fc896" group="com.google.errorprone"/>
<trusted-key id="eef9ecc7d5d90518" group="com.google.dagger"/>
<trusted-key id="efe8086f9e93774e" group="junit"/>
@@ -461,8 +468,7 @@
</trusted-keys>
</configuration>
<components>
- <!-- Unsigned -->
- <component group="backport-util-concurrent" name="backport-util-concurrent" version="3.1">
+ <component group="backport-util-concurrent" name="backport-util-concurrent" version="3.1" androidx:reason="Unsigned">
<artifact name="backport-util-concurrent-3.1.jar">
<sha256 value="f5759b7fcdfc83a525a036deedcbd32e5b536b625ebc282426f16ca137eb5902" origin="Generated by Gradle"/>
</artifact>
@@ -470,8 +476,7 @@
<sha256 value="770471090ca40a17b9e436ee2ec00819be42042da6f4085ece1d37916dc08ff9" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="classworlds" name="classworlds" version="1.1-alpha-2">
+ <component group="classworlds" name="classworlds" version="1.1-alpha-2" androidx:reason="Unsigned">
<artifact name="classworlds-1.1-alpha-2.jar">
<sha256 value="2bf4e59f3acd106fea6145a9a88fe8956509f8b9c0fdd11eb96fee757269e3f3" origin="Generated by Gradle"/>
</artifact>
@@ -479,8 +484,7 @@
<sha256 value="0cc647963b74ad1d7a37c9868e9e5a8f474e49297e1863582253a08a4c719cb1" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned https://github.com/gundy/semver4j/issues/6 -->
- <component group="com.github.gundy" name="semver4j" version="0.16.4">
+ <component group="com.github.gundy" name="semver4j" version="0.16.4" androidx:reason="Unsigned https://github.com/gundy/semver4j/issues/6">
<artifact name="semver4j-0.16.4-nodeps.jar">
<sha256 value="3f59eca516374ccd4fd3551625bf50f8a4b191f700508f7ce4866460a6128af0" origin="Generated by Gradle"/>
</artifact>
@@ -489,8 +493,7 @@
<sha256 value="32001db2443b339dd21f5b79ff29d1ade722d1ba080c214bde819f0f72d1604d" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="com.google" name="google" version="1">
+ <component group="com.google" name="google" version="1" androidx:reason="Unsigned">
<artifact name="google-1.pom">
<sha256 value="cd6db17a11a31ede794ccbd1df0e4d9750f640234731f21cff885a9997277e81" origin="Generated by Gradle"/>
</artifact>
@@ -543,8 +546,7 @@
<sha256 value="c6898b1f71e69b15bf90c31fc3ef2de1cffbf454a770700f755b5a47ea48b540" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="com.google.code.findbugs" name="jsr305" version="1.3.9">
+ <component group="com.google.code.findbugs" name="jsr305" version="1.3.9" androidx:reason="Unsigned">
<artifact name="jsr305-1.3.9.jar">
<sha256 value="905721a0eea90a81534abb7ee6ef4ea2e5e645fa1def0a5cd88402df1b46c9ed" origin="Generated by Gradle"/>
</artifact>
@@ -552,8 +554,7 @@
<sha256 value="feab9191311c3d7aeef2b66d6064afc80d3d1d52d980fb07ae43c78c987ba93a" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="com.google.code.findbugs" name="jsr305" version="2.0.1">
+ <component group="com.google.code.findbugs" name="jsr305" version="2.0.1" androidx:reason="Unsigned">
<artifact name="jsr305-2.0.1.jar">
<sha256 value="1e7f53fa5b8b5c807e986ba335665da03f18d660802d8bf061823089d1bee468" origin="Generated by Gradle"/>
</artifact>
@@ -561,8 +562,7 @@
<sha256 value="02c12c3c2ae12dd475219ff691c82a4d9ea21f44bc594a181295bf6d43dcfbb0" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="com.google.prefab" name="cli" version="2.0.0">
+ <component group="com.google.prefab" name="cli" version="2.0.0" androidx:reason="Unsigned">
<artifact name="cli-2.0.0-all.jar">
<sha256 value="d9bd89f68446b82be038aae774771ad85922d0b375209b17625a2734b5317e29" origin="Generated by Gradle"/>
</artifact>
@@ -570,8 +570,7 @@
<sha256 value="4856401a263b39c5394b36a16e0d99628cf05c68008a0cda9691c72bb101e1df" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="com.googlecode.json-simple" name="json-simple" version="1.1">
+ <component group="com.googlecode.json-simple" name="json-simple" version="1.1" androidx:reason="Unsigned">
<artifact name="json-simple-1.1.jar">
<sha256 value="2d9484f4c649f708f47f9a479465fc729770ee65617dca3011836602264f6439" origin="Generated by Gradle"/>
</artifact>
@@ -579,20 +578,17 @@
<sha256 value="47a89be0fa0fedd476db5fd2c83487654d2a119c391f83a142be876667cf7dab" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned https://github.com/gradle/gradle/issues/20349 -->
- <component group="com.gradle" name="common-custom-user-data-gradle-plugin" version="1.7.2">
+ <component group="com.gradle" name="common-custom-user-data-gradle-plugin" version="1.7.2" androidx:reason="Unsigned https://github.com/gradle/gradle/issues/20349">
<artifact name="common-custom-user-data-gradle-plugin-1.7.2.pom">
<sha256 value="c70db912c8b127b1b9a6c0cccac1a9353e9fc3b063a3be0114a5208f43c09c31" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned https://github.com/gradle/gradle/issues/20349 -->
- <component group="com.gradle" name="gradle-enterprise-gradle-plugin" version="3.10.2">
+ <component group="com.gradle" name="gradle-enterprise-gradle-plugin" version="3.10.2" androidx:reason="Unsigned https://github.com/gradle/gradle/issues/20349">
<artifact name="gradle-enterprise-gradle-plugin-3.10.2.pom">
<sha256 value="57603c9a75a9ef86ce30b1cb2db728d3cd9caf1be967343f1fc2316c85df5653" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="com.squareup.okio" name="okio" version="2.8.0">
+ <component group="com.squareup.okio" name="okio" version="2.8.0" androidx:reason="Unsigned">
<artifact name="okio-2.8.0.module">
<sha256 value="17baab7270389a5fa63ab12811864d0a00f381611bc4eb042fa1bd5918ed0965" origin="Generated by Gradle"/>
</artifact>
@@ -600,20 +596,17 @@
<sha256 value="4496b06e73982fcdd8a5393f46e5df2ce2fa4465df5895454cac68a32f09bbc8" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="com.squareup.okio" name="okio" version="2.10.0">
+ <component group="com.squareup.okio" name="okio" version="2.10.0" androidx:reason="Unsigned">
<artifact name="okio-jvm-2.10.0.jar">
<sha256 value="a27f091d34aa452e37227e2cfa85809f29012a8ef2501a9b5a125a978e4fcbc1" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="com.squareup.sqldelight" name="coroutines-extensions-jvm" version="1.3.0">
+ <component group="com.squareup.sqldelight" name="coroutines-extensions-jvm" version="1.3.0" androidx:reason="Unsigned">
<artifact name="sqldelight-coroutines-extensions-jvm-1.3.0.jar">
<sha256 value="47305eab44f8b2aef533d8ce76cec9eb5175715cac26b538b6bff5b106ed0ba1" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="com.squareup.wire" name="wire-grpc-client" version="3.6.0">
+ <component group="com.squareup.wire" name="wire-grpc-client" version="3.6.0" androidx:reason="Unsigned">
<artifact name="wire-grpc-client-3.6.0.module">
<sha256 value="f4d91b43e5ce4603d63842652f063f16c0827abda1922dfb9551a4ac23ba4462" origin="Generated by Gradle"/>
</artifact>
@@ -621,8 +614,7 @@
<sha256 value="96904172b35af353e4459786a7d02f1550698cd03b249799ecb563cea3b4c277" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="com.squareup.wire" name="wire-runtime" version="3.6.0">
+ <component group="com.squareup.wire" name="wire-runtime" version="3.6.0" androidx:reason="Unsigned">
<artifact name="wire-runtime-3.6.0.module">
<sha256 value="3b99891842fdec80e7b24ae7f7c485ae41ca35b47c902ca2043cc948aaf58010" origin="Generated by Gradle"/>
</artifact>
@@ -630,8 +622,7 @@
<sha256 value="ac41d3f9b8a88046788c6827b0519bf0c53dcc271f598f48aa666c6f5a9523d0" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="com.squareup.wire" name="wire-schema" version="3.6.0">
+ <component group="com.squareup.wire" name="wire-schema" version="3.6.0" androidx:reason="Unsigned">
<artifact name="wire-schema-3.6.0.module">
<sha256 value="85abd765f2efca0545889c935d8c240e31736a22221231a59bcc4510358b6aaa" origin="Generated by Gradle"/>
</artifact>
@@ -639,8 +630,7 @@
<sha256 value="108bc4bafe7024a41460a1a60e72b6a95b69e5afd29c9f11ba7d8e0de2207976" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Invalid signature https://github.com/michel-kraemer/gradle-download-task/issues/187 -->
- <component group="de.undercouch" name="gradle-download-task" version="4.1.1">
+ <component group="de.undercouch" name="gradle-download-task" version="4.1.1" androidx:reason="Invalid signature https://github.com/michel-kraemer/gradle-download-task/issues/187">
<artifact name="gradle-download-task-4.1.1.jar">
<ignored-keys>
<ignored-key id="1fa37fbe4453c1073e7ef61d6449005f96bc97a3" reason="PGP verification failed"/>
@@ -658,8 +648,7 @@
</sha256>
</artifact>
</component>
- <!-- Unsigned https://github.com/johnrengelman/shadow/issues/760 -->
- <component group="gradle.plugin.com.github.johnrengelman" name="shadow" version="7.1.1">
+ <component group="gradle.plugin.com.github.johnrengelman" name="shadow" version="7.1.1" androidx:reason="Unsigned https://github.com/johnrengelman/shadow/issues/760">
<artifact name="shadow-7.1.1.jar">
<sha256 value="a870861a7a3d54ffd97822051a27b2f1b86dd5c480317f0b97f3b27581b742af" origin="Generated by Gradle"/>
</artifact>
@@ -667,8 +656,7 @@
<sha256 value="683be0cd32af9c80a6d4a143b9a6ac2eb45ebc3ccd16db4ca11b94e55fc5e52f" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="gradle.plugin.com.google.protobuf" name="protobuf-gradle-plugin" version="0.8.13">
+ <component group="gradle.plugin.com.google.protobuf" name="protobuf-gradle-plugin" version="0.8.13" androidx:reason="Unsigned">
<artifact name="protobuf-gradle-plugin-0.8.13.jar">
<sha256 value="8a04b6eee4eab68c73b6e61cc8e00206753691b781d042afbae746f97e8c6f2d" origin="Generated by Gradle"/>
</artifact>
@@ -676,8 +664,7 @@
<sha256 value="d8c46016037cda6360561b9c6a21a6c2a4847cad15c3c63903e15328fbcccc45" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="javax.activation" name="activation" version="1.1">
+ <component group="javax.activation" name="activation" version="1.1" androidx:reason="Unsigned">
<artifact name="activation-1.1.jar">
<sha256 value="2881c79c9d6ef01c58e62beea13e9d1ac8b8baa16f2fc198ad6e6776defdcdd3" origin="Generated by Gradle"/>
</artifact>
@@ -685,8 +672,7 @@
<sha256 value="d490e540a11504b9d71718b1c85fef7b3de6802361290824539b076d58faa8a0" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="javax.annotation" name="jsr250-api" version="1.0">
+ <component group="javax.annotation" name="jsr250-api" version="1.0" androidx:reason="Unsigned">
<artifact name="jsr250-api-1.0.jar">
<sha256 value="a1a922d0d9b6d183ed3800dfac01d1e1eb159f0e8c6f94736931c1def54a941f" origin="Generated by Gradle"/>
</artifact>
@@ -694,8 +680,7 @@
<sha256 value="548b0ef6f04356ef2283af5140d9404f38fd3891a509d468537abf2f9462944d" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="javax.inject" name="javax.inject" version="1">
+ <component group="javax.inject" name="javax.inject" version="1" androidx:reason="Unsigned">
<artifact name="javax.inject-1.jar">
<sha256 value="91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff" origin="Generated by Gradle"/>
</artifact>
@@ -703,8 +688,7 @@
<sha256 value="943e12b100627804638fa285805a0ab788a680266531e650921ebfe4621a8bfa" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="javax.xml.stream" name="stax-api" version="1.0-2">
+ <component group="javax.xml.stream" name="stax-api" version="1.0-2" androidx:reason="Unsigned">
<artifact name="stax-api-1.0-2.jar">
<sha256 value="e8c70ebd76f982c9582a82ef82cf6ce14a7d58a4a4dca5cb7b7fc988c80089b7" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
@@ -712,8 +696,7 @@
<sha256 value="2864f19da84fd52763d75a197a71779b2decbccaac3eb4e4760ffc884c5af4a2" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="me.champeau.gradle" name="japicmp-gradle-plugin" version="0.2.9">
+ <component group="me.champeau.gradle" name="japicmp-gradle-plugin" version="0.2.9" androidx:reason="Unsigned">
<artifact name="japicmp-gradle-plugin-0.2.9.jar">
<sha256 value="320944e8f3a42a38a5e0f08c6e1e8ae11a63fc82e1f7bf0429a6b7d89d26fac3" origin="Generated by Gradle"/>
</artifact>
@@ -721,8 +704,7 @@
<sha256 value="41fc0c243907c241cffa24a06a8cb542747c848ebad5feb6b0413d61b4a0ebc2" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="nekohtml" name="nekohtml" version="1.9.6.2">
+ <component group="nekohtml" name="nekohtml" version="1.9.6.2" androidx:reason="Unsigned">
<artifact name="nekohtml-1.9.6.2.jar">
<sha256 value="fdff6cfa9ed9cc911c842a5d2395f209ec621ef1239d46810e9e495809d3ae09" origin="Generated by Gradle"/>
</artifact>
@@ -730,8 +712,7 @@
<sha256 value="f5655d331af6afcd4dbaedaa739b889380c771a7e83f7aea5c8544a05074cf0b" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="nekohtml" name="xercesMinimal" version="1.9.6.2">
+ <component group="nekohtml" name="xercesMinimal" version="1.9.6.2" androidx:reason="Unsigned">
<artifact name="xercesMinimal-1.9.6.2.jar">
<sha256 value="95b8b357d19f63797dd7d67622fd3f18374d64acbc6584faba1c7759a31e8438" origin="Generated by Gradle"/>
</artifact>
@@ -739,20 +720,17 @@
<sha256 value="c219d697fa9c8f243d8f6e347499b6d4e8af1d0cac4bbc7b3907d338a2024c13" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="net.java" name="jvnet-parent" version="1">
+ <component group="net.java" name="jvnet-parent" version="1" androidx:reason="Unsigned">
<artifact name="jvnet-parent-1.pom">
<sha256 value="281440811268e65d9e266b3cc898297e214e04f09740d0386ceeb4a8923d63bf" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="net.java" name="jvnet-parent" version="3">
+ <component group="net.java" name="jvnet-parent" version="3" androidx:reason="Unsigned">
<artifact name="jvnet-parent-3.pom">
<sha256 value="30f5789efa39ddbf96095aada3fc1260c4561faf2f714686717cb2dc5049475a" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="net.java" name="jvnet-parent" version="4">
+ <component group="net.java" name="jvnet-parent" version="4" androidx:reason="Unsigned">
<artifact name="jvnet-parent-4.pom">
<sha256 value="471395735549495297c8ff939b9a32e08b91302020ff773586d27e497abb8fbb" origin="Generated by Gradle"/>
<!-- Gradle doesn't add keyring files for parent poms so we need to explicitly specify it here to trust -->
@@ -760,14 +738,12 @@
<pgp value="44fbdbbc1a00fe414f1c1873586654072ead6677"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="net.java" name="jvnet-parent" version="5">
+ <component group="net.java" name="jvnet-parent" version="5" androidx:reason="Unsigned">
<artifact name="jvnet-parent-5.pom">
<sha256 value="1af699f8d9ddab67f9a0d202fbd7915eb0362a5a6dfd5ffc54cafa3465c9cb0a" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="net.sf.kxml" name="kxml2" version="2.3.0">
+ <component group="net.sf.kxml" name="kxml2" version="2.3.0" androidx:reason="Unsigned">
<artifact name="kxml2-2.3.0.jar">
<sha256 value="f264dd9f79a1fde10ce5ecc53221eff24be4c9331c830b7d52f2f08a7b633de2" origin="Generated by Gradle"/>
</artifact>
@@ -775,8 +751,7 @@
<sha256 value="31ce606f4e9518936299bb0d27c978fa61e185fd1de7c9874fe959a53e34a685" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.ccil.cowan.tagsoup" name="tagsoup" version="1.2">
+ <component group="org.ccil.cowan.tagsoup" name="tagsoup" version="1.2" androidx:reason="Unsigned">
<artifact name="tagsoup-1.2.jar">
<sha256 value="10d12b82c9a58a7842765a1152a56fbbd11eac9122a621f5a86a087503297266" origin="Generated by Gradle"/>
</artifact>
@@ -784,8 +759,7 @@
<sha256 value="186fd460ee13150e31188703a2c871bf86e20332636f3ede4ab959cd5568da78" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.codehaus.plexus" name="plexus-utils" version="1.5.15">
+ <component group="org.codehaus.plexus" name="plexus-utils" version="1.5.15" androidx:reason="Unsigned">
<artifact name="plexus-utils-1.5.15.jar">
<sha256 value="2ca121831e597b4d8f2cb22d17c5c041fc23a7777ceb6bfbdd4dfb34bbe7d997" origin="Generated by Gradle"/>
</artifact>
@@ -793,26 +767,22 @@
<sha256 value="12a3c9a32b82fdc95223cab1f9d344e14ef3e396da14c4d0013451646f3280e7" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.codehaus.plexus" name="plexus" version="1.0.4">
+ <component group="org.codehaus.plexus" name="plexus" version="1.0.4" androidx:reason="Unsigned">
<artifact name="plexus-1.0.4.pom">
<sha256 value="2242fd02d12b1ca73267fb3d89863025517200e7a4ee426cba4667d0172c74c3" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.codehaus.plexus" name="plexus" version="2.0.2">
+ <component group="org.codehaus.plexus" name="plexus" version="2.0.2" androidx:reason="Unsigned">
<artifact name="plexus-2.0.2.pom">
<sha256 value="e246e2a062b5d989fdefc521c9c56431ba5554ff8d2344edee9218a34a546a33" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.codehaus.plexus" name="plexus-components" version="1.1.14">
+ <component group="org.codehaus.plexus" name="plexus-components" version="1.1.14" androidx:reason="Unsigned">
<artifact name="plexus-components-1.1.14.pom">
<sha256 value="381d72c526be217b770f9f8c3f749a86d3b1548ac5c1fcb48d267530ec60d43f" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.codehaus.plexus" name="plexus-container-default" version="1.0-alpha-9-stable-1">
+ <component group="org.codehaus.plexus" name="plexus-container-default" version="1.0-alpha-9-stable-1" androidx:reason="Unsigned">
<artifact name="plexus-container-default-1.0-alpha-9-stable-1.jar">
<sha256 value="7c758612888782ccfe376823aee7cdcc7e0cdafb097f7ef50295a0b0c3a16edf" origin="Generated by Gradle"/>
</artifact>
@@ -820,14 +790,12 @@
<sha256 value="ef71d45a49edfe76be0f520312a76bc2aae73ec0743a5ffffe10d30122c6e2b2" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.codehaus.plexus" name="plexus-containers" version="1.0.3">
+ <component group="org.codehaus.plexus" name="plexus-containers" version="1.0.3" androidx:reason="Unsigned">
<artifact name="plexus-containers-1.0.3.pom">
<sha256 value="7c75075badcb014443ee94c8c4cad2f4a9905be3ce9430fe7b220afc7fa3a80f" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.codehaus.plexus" name="plexus-interpolation" version="1.11">
+ <component group="org.codehaus.plexus" name="plexus-interpolation" version="1.11" androidx:reason="Unsigned">
<artifact name="plexus-interpolation-1.11.jar">
<sha256 value="fd9507feb858fa620d1b4aa4b7039fdea1a77e09d3fd28cfbddfff468d9d8c28" origin="Generated by Gradle"/>
</artifact>
@@ -835,8 +803,7 @@
<sha256 value="b84d281f59b9da528139e0752a0e1cab0bd98d52c58442b00e45c9748e1d9eee" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.jetbrains.dokka" name="dokka-android-gradle-plugin" version="0.9.17-g014">
+ <component group="org.jetbrains.dokka" name="dokka-android-gradle-plugin" version="0.9.17-g014" androidx:reason="Unsigned">
<artifact name="dokka-android-gradle-plugin-0.9.17-g014.jar">
<sha256 value="64b2e96fd20762351c74f08d598d49c25a490a3b685b8a09446e81d6db36fe81" origin="Generated by Gradle"/>
</artifact>
@@ -844,8 +811,7 @@
<sha256 value="956ff381c6c775161a82823bb52d0aa40a8f6a37ab85059f149531f5e5efb7da" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.jetbrains.dokka" name="dokka-fatjar" version="0.9.17-g014">
+ <component group="org.jetbrains.dokka" name="dokka-fatjar" version="0.9.17-g014" androidx:reason="Unsigned">
<artifact name="dokka-fatjar-0.9.17-g014.jar">
<sha256 value="47cf09501402a101e555588cf5fa9ed83f8572bce9fd60db29e74b5d079628e3" origin="Generated by Gradle"/>
</artifact>
@@ -853,8 +819,7 @@
<sha256 value="ceb601f55f14337261fea474bb061407dc0e52146f80d74cd0b43d66febd401f" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.jetbrains.dokka" name="dokka-gradle-plugin" version="0.9.17-g014">
+ <component group="org.jetbrains.dokka" name="dokka-gradle-plugin" version="0.9.17-g014" androidx:reason="Unsigned">
<artifact name="dokka-gradle-plugin-0.9.17-g014.jar">
<sha256 value="643a7eddeb521832c6021508b7477b603517438481bc06633dca12eb1f339422" origin="Generated by Gradle"/>
</artifact>
@@ -867,8 +832,7 @@
<sha256 value="0f8a1b116e760b8fe6389c51b84e4b07a70fc11082d4f936e453b583dd50b43b" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.ow2.asm" name="asm" version="7.0">
+ <component group="org.ow2.asm" name="asm" version="7.0" androidx:reason="Unsigned">
<artifact name="asm-7.0.jar">
<sha256 value="b88ef66468b3c978ad0c97fd6e90979e56155b4ac69089ba7a44e9aa7ffe9acf" origin="Generated by Gradle"/>
</artifact>
@@ -876,8 +840,7 @@
<sha256 value="83f65b1083d5ce4f8ba7f9545cfe9ff17824589c9a7cc82c3a4695801e4f5f68" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.ow2.asm" name="asm-analysis" version="7.0">
+ <component group="org.ow2.asm" name="asm-analysis" version="7.0" androidx:reason="Unsigned">
<artifact name="asm-analysis-7.0.jar">
<sha256 value="e981f8f650c4d900bb033650b18e122fa6b161eadd5f88978d08751f72ee8474" origin="Generated by Gradle"/>
</artifact>
@@ -885,8 +848,7 @@
<sha256 value="c6b54477e9d5bae1e7addff2e24cbf92aaff2ff08fd6bc0596c3933c3fadc2cb" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.ow2.asm" name="asm-commons" version="7.0">
+ <component group="org.ow2.asm" name="asm-commons" version="7.0" androidx:reason="Unsigned">
<artifact name="asm-commons-7.0.jar">
<sha256 value="fed348ef05958e3e846a3ac074a12af5f7936ef3d21ce44a62c4fa08a771927d" origin="Generated by Gradle"/>
</artifact>
@@ -894,8 +856,7 @@
<sha256 value="f4c697886cdb4a5b2472054a0b5e34371e9b48e620be40c3ed48e1f4b6d51eb4" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.ow2.asm" name="asm-tree" version="7.0">
+ <component group="org.ow2.asm" name="asm-tree" version="7.0" androidx:reason="Unsigned">
<artifact name="asm-tree-7.0.jar">
<sha256 value="cfd7a0874f9de36a999c127feeadfbfe6e04d4a71ee954d7af3d853f0be48a6c" origin="Generated by Gradle"/>
</artifact>
@@ -903,8 +864,7 @@
<sha256 value="d39e7dd12f4ff535a0839d1949c39c7644355a4470220c94b76a5c168c57a068" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.ow2.asm" name="asm-util" version="7.0">
+ <component group="org.ow2.asm" name="asm-util" version="7.0" androidx:reason="Unsigned">
<artifact name="asm-util-7.0.jar">
<sha256 value="75fbbca440ef463f41c2b0ab1a80abe67e910ac486da60a7863cbcb5bae7e145" origin="Generated by Gradle"/>
</artifact>
@@ -912,14 +872,12 @@
<sha256 value="e07bce4bb55d5a06f4c10d912fc9dee8a9b9c04ec549bbb8db4f20db34706f75" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="org.sonatype.oss" name="oss-parent" version="7">
+ <component group="org.sonatype.oss" name="oss-parent" version="7" androidx:reason="Unsigned">
<artifact name="oss-parent-7.pom">
<sha256 value="b51f8867c92b6a722499557fc3a1fdea77bdf9ef574722fe90ce436a29559454" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Invalid signature -->
- <component group="org.tensorflow" name="tensorflow-lite-metadata" version="0.1.0-rc2">
+ <component group="org.tensorflow" name="tensorflow-lite-metadata" version="0.1.0-rc2" androidx:reason="Invalid signature">
<artifact name="tensorflow-lite-metadata-0.1.0-rc2.jar">
<pgp value="db0597e3144342256bc81e3ec727d053c4481cf5"/>
<sha256 value="2c2a264f842498c36d34d2a7b91342490d9a962862c85baac1acd54ec2fca6d9" origin="Generated by Gradle"/>
@@ -933,8 +891,7 @@
</sha256>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="pull-parser" name="pull-parser" version="2">
+ <component group="pull-parser" name="pull-parser" version="2" androidx:reason="Unsigned">
<artifact name="pull-parser-2.jar">
<sha256 value="b20c1e56513faeffb9b01d9d03ba1a36128ac3f9be39b2d0edbe2e240b029d3f" origin="Generated by Gradle"/>
</artifact>
@@ -942,8 +899,7 @@
<sha256 value="4823677670797c2b71e8ebbe5437c41151f4e8edb7c6c0d473279715070f36d3" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="xmlpull" name="xmlpull" version="1.1.3.1">
+ <component group="xmlpull" name="xmlpull" version="1.1.3.1" androidx:reason="Unsigned">
<artifact name="xmlpull-1.1.3.1.jar">
<sha256 value="34e08ee62116071cbb69c0ed70d15a7a5b208d62798c59f2120bb8929324cb63" origin="Generated by Gradle"/>
</artifact>
@@ -951,8 +907,7 @@
<sha256 value="8f10ffd8df0d3e9819c8cc8402709c6b248bc53a954ef6e45470d9ae3a5735fb" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned -->
- <component group="xpp3" name="xpp3" version="1.1.4c">
+ <component group="xpp3" name="xpp3" version="1.1.4c" androidx:reason="Unsigned">
<artifact name="xpp3-1.1.4c.jar">
<sha256 value="0341395a481bb887803957145a6a37879853dd625e9244c2ea2509d9bb7531b9" origin="Generated by Gradle"/>
</artifact>
@@ -960,8 +915,7 @@
<sha256 value="4e54622f5dc0f8b6c51e28650268f001e3b55d076c8e3a9d9731c050820c0a3d" origin="Generated by Gradle"/>
</artifact>
</component>
- <!-- Unsigned, b/227204920 -->
- <component group="" name="kotlin-native-prebuilt-linux-x86_64" version="1.7.10">
+ <component group="" name="kotlin-native-prebuilt-linux-x86_64" version="1.7.10" androidx:reason="Unsigned, b/227204920">
<artifact name="kotlin-native-prebuilt-linux-x86_64-1.7.10.tar.gz">
<sha256 value="f3bd13bc0089fe95609109604d5993a49838828787f15e0e79eef6612b587dc1" origin="Hand-built using sha256sum kotlin-native-prebuilt-linux-x86_64-1.7.10.tar.gz"/>
</artifact>
diff --git a/javascriptengine/OWNERS b/javascriptengine/OWNERS
new file mode 100644
index 0000000..e3fd7e0
--- /dev/null
+++ b/javascriptengine/OWNERS
@@ -0,0 +1,2 @@
[email protected]
[email protected]
diff --git a/javascriptengine/javascriptengine/api/current.txt b/javascriptengine/javascriptengine/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/javascriptengine/javascriptengine/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/javascriptengine/javascriptengine/api/public_plus_experimental_current.txt b/javascriptengine/javascriptengine/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/javascriptengine/javascriptengine/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/javascriptengine/javascriptengine/api/res-current.txt b/javascriptengine/javascriptengine/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/javascriptengine/javascriptengine/api/res-current.txt
diff --git a/javascriptengine/javascriptengine/api/restricted_current.txt b/javascriptengine/javascriptengine/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/javascriptengine/javascriptengine/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/javascriptengine/javascriptengine/build.gradle b/javascriptengine/javascriptengine/build.gradle
new file mode 100644
index 0000000..69ad2ab
--- /dev/null
+++ b/javascriptengine/javascriptengine/build.gradle
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+}
+
+dependencies {
+ annotationProcessor(libs.nullaway)
+ // Add dependencies here
+}
+
+android {
+ namespace "androidx.javascriptengine"
+}
+
+androidx {
+ name = "JavaScript Engine"
+ type = LibraryType.PUBLISHED_LIBRARY
+ mavenGroup = LibraryGroups.JAVASCRIPTENGINE
+ inceptionYear = "2022"
+ description = "Javascript Engine is a static library you can add to your Android application in order to evaluate JavaScript."
+}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/ExperimentalWindowExtensionsApi.java b/javascriptengine/javascriptengine/src/main/androidx/javascriptengine/package-info.java
similarity index 67%
copy from window/extensions/extensions/src/main/java/androidx/window/extensions/ExperimentalWindowExtensionsApi.java
copy to javascriptengine/javascriptengine/src/main/androidx/javascriptengine/package-info.java
index 0da12db..4082445 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/ExperimentalWindowExtensionsApi.java
+++ b/javascriptengine/javascriptengine/src/main/androidx/javascriptengine/package-info.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,13 +14,8 @@
* limitations under the License.
*/
-package androidx.window.extensions;
-
-import androidx.annotation.RequiresOptIn;
-
/**
- * Denotes that the API uses experimental WindowManager extension APIs.
+ * The androidx.javascriptengine library is a static library you can add to your Android application
+ * in order to evaluate JavaScript.
*/
-@RequiresOptIn
-public @interface ExperimentalWindowExtensionsApi {
-}
+package androidx.javascriptengine;
diff --git a/libraryversions.toml b/libraryversions.toml
index 80909c7..9377182 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -1,7 +1,7 @@
[versions]
ACTIVITY = "1.7.0-alpha01"
ADS_IDENTIFIER = "1.0.0-alpha05"
-ANNOTATION = "1.4.0-rc01"
+ANNOTATION = "1.5.0-alpha01"
ANNOTATION_EXPERIMENTAL = "1.3.0-alpha01"
APPCOMPAT = "1.5.0-rc01"
APPSEARCH = "1.0.0-alpha05"
@@ -19,7 +19,7 @@
CAR_APP = "1.3.0-alpha01"
COLLECTION = "1.3.0-alpha02"
COMPOSE = "1.3.0-alpha02"
-COMPOSE_COMPILER = "1.3.0-beta01"
+COMPOSE_COMPILER = "1.3.0-rc01"
COMPOSE_MATERIAL3 = "1.0.0-alpha15"
COMPOSE_RUNTIME_TRACING = "1.0.0-alpha01"
CONTENTPAGER = "1.1.0-alpha01"
@@ -60,6 +60,7 @@
HILT_NAVIGATION_COMPOSE = "1.1.0-alpha01"
INSPECTION = "1.0.0"
INTERPOLATOR = "1.1.0-alpha01"
+JAVASCRIPTENGINE = "1.0.0-alpha01"
LEANBACK = "1.2.0-alpha03"
LEANBACK_GRID = "1.0.0-alpha02"
LEANBACK_PAGING = "1.1.0-alpha10"
@@ -73,7 +74,7 @@
MEDIA = "1.7.0-alpha01"
MEDIA2 = "1.3.0-alpha01"
MEDIAROUTER = "1.4.0-alpha01"
-METRICS = "1.0.0-alpha03"
+METRICS = "1.0.0-alpha04"
NAVIGATION = "2.6.0-alpha01"
PAGING = "3.2.0-alpha02"
PAGING_COMPOSE = "1.0.0-alpha16"
@@ -126,9 +127,9 @@
WEAR_ONGOING = "1.1.0-alpha01"
WEAR_PHONE_INTERACTIONS = "1.1.0-alpha04"
WEAR_REMOTE_INTERACTIONS = "1.1.0-alpha01"
-WEAR_TILES = "1.1.0-alpha10"
+WEAR_TILES = "1.1.0-beta01"
WEAR_WATCHFACE = "1.2.0-alpha01"
-WEBKIT = "1.5.0-beta02"
+WEBKIT = "1.5.0-rc01"
WINDOW = "1.1.0-alpha03"
WINDOW_EXTENSIONS = "1.1.0-alpha01"
WINDOW_SIDECAR = "1.0.0-rc01"
@@ -186,6 +187,7 @@
INSPECTION = { group = "androidx.inspection", atomicGroupVersion = "versions.INSPECTION" }
INSPECTION_EXTENSIONS = { group = "androidx.inspection.extensions", atomicGroupVersion = "versions.SQLITE_INSPECTOR" }
INTERPOLATOR = { group = "androidx.interpolator", atomicGroupVersion = "versions.INTERPOLATOR" }
+JAVASCRIPTENGINE = { group = "androidx.javascriptengine", atomicGroupVersion = "versions.JAVASCRIPTENGINE" }
LEANBACK = { group = "androidx.leanback" }
LEGACY = { group = "androidx.legacy" }
LIBYUV = { group = "libyuv", atomicGroupVersion = "versions.LIBYUV" }
diff --git a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt
index 1ec1fe6b..8bfb087 100644
--- a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt
+++ b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt
@@ -48,7 +48,7 @@
* Temporary per-frame to track UI and user state.
* Unlike the states tracked in `states`, any state in this structure is only valid until
* the next frame, at which point it is cleared. Any state data added here is automatically
- * removed; there is no matching "remove" method for [.addSingleFrameState]
+ * removed; there is no matching "remove" method for [.putSingleFrameState]
*
* @see putSingleFrameState
*/
@@ -164,7 +164,13 @@
* State information can be about UI elements that are currently active (such as the current
* [Activity] or layout) or a user interaction like flinging a list.
* If the PerformanceMetricsState object already contains an entry with the same key,
- * the old value is replaced by the new one.
+ * the old value is replaced by the new one. Note that this means apps with several
+ * instances of similar objects (such as multipe `RecyclerView`s) should
+ * therefore use unique keys for these instances to avoid clobbering state values
+ * for other instances and to provide enough information for later analysis which
+ * allows for disambiguation between these objects. For example, using "RVHeaders" and
+ * "RVContent" might be more helpful than just "RecyclerView" for a messaging app using
+ * `RecyclerView` objects for both a headers list and a list of message contents.
*
* Some state may be provided automatically by other AndroidX libraries.
* But applications are encouraged to add user state specific to those applications
diff --git a/navigation/navigation-common/api/current.txt b/navigation/navigation-common/api/current.txt
index abab5da..3d8788e 100644
--- a/navigation/navigation-common/api/current.txt
+++ b/navigation/navigation-common/api/current.txt
@@ -200,6 +200,7 @@
method public final void addArgument(String argumentName, androidx.navigation.NavArgument argument);
method public final void addDeepLink(String uriPattern);
method public final void addDeepLink(androidx.navigation.NavDeepLink navDeepLink);
+ method public final String? fillInLabel(android.content.Context context, android.os.Bundle? bundle);
method public final androidx.navigation.NavAction? getAction(@IdRes int id);
method public final java.util.Map<java.lang.String,androidx.navigation.NavArgument> getArguments();
method public static final kotlin.sequences.Sequence<androidx.navigation.NavDestination> getHierarchy(androidx.navigation.NavDestination);
diff --git a/navigation/navigation-common/api/public_plus_experimental_current.txt b/navigation/navigation-common/api/public_plus_experimental_current.txt
index abab5da..3d8788e 100644
--- a/navigation/navigation-common/api/public_plus_experimental_current.txt
+++ b/navigation/navigation-common/api/public_plus_experimental_current.txt
@@ -200,6 +200,7 @@
method public final void addArgument(String argumentName, androidx.navigation.NavArgument argument);
method public final void addDeepLink(String uriPattern);
method public final void addDeepLink(androidx.navigation.NavDeepLink navDeepLink);
+ method public final String? fillInLabel(android.content.Context context, android.os.Bundle? bundle);
method public final androidx.navigation.NavAction? getAction(@IdRes int id);
method public final java.util.Map<java.lang.String,androidx.navigation.NavArgument> getArguments();
method public static final kotlin.sequences.Sequence<androidx.navigation.NavDestination> getHierarchy(androidx.navigation.NavDestination);
diff --git a/navigation/navigation-common/api/restricted_current.txt b/navigation/navigation-common/api/restricted_current.txt
index abab5da..3d8788e 100644
--- a/navigation/navigation-common/api/restricted_current.txt
+++ b/navigation/navigation-common/api/restricted_current.txt
@@ -200,6 +200,7 @@
method public final void addArgument(String argumentName, androidx.navigation.NavArgument argument);
method public final void addDeepLink(String uriPattern);
method public final void addDeepLink(androidx.navigation.NavDeepLink navDeepLink);
+ method public final String? fillInLabel(android.content.Context context, android.os.Bundle? bundle);
method public final androidx.navigation.NavAction? getAction(@IdRes int id);
method public final java.util.Map<java.lang.String,androidx.navigation.NavArgument> getArguments();
method public static final kotlin.sequences.Sequence<androidx.navigation.NavDestination> getHierarchy(androidx.navigation.NavDestination);
diff --git a/navigation/navigation-common/build.gradle b/navigation/navigation-common/build.gradle
index c872393..8bc8969 100644
--- a/navigation/navigation-common/build.gradle
+++ b/navigation/navigation-common/build.gradle
@@ -32,10 +32,10 @@
dependencies {
api("androidx.annotation:annotation:1.1.0")
- api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.0")
- api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0")
+ api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
+ api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
api("androidx.savedstate:savedstate-ktx:1.2.0")
- api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.0")
+ api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1")
implementation("androidx.core:core-ktx:1.1.0")
implementation("androidx.collection:collection-ktx:1.1.0")
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
index 10d30a9..77dcb58 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
@@ -28,6 +28,7 @@
import androidx.collection.valueIterator
import androidx.core.content.res.use
import androidx.navigation.common.R
+import java.util.regex.Pattern
import kotlin.reflect.KClass
/**
@@ -508,6 +509,50 @@
return defaultArgs
}
+ /**
+ * Parses a dynamic label containing arguments into a String.
+ *
+ * Supports String Resource arguments by parsing `R.string` values of `ReferenceType`
+ * arguments found in `android:label` into their String values.
+ *
+ * Returns `null` if label is null.
+ *
+ * Returns the original label if the label was a static string.
+ *
+ * @param context Context used to resolve a resource's name
+ * @param bundle Bundle containing the arguments used in the label
+ * @return The parsed string or null if the label is null
+ * @throws IllegalArgumentException if an argument provided in the label cannot be found in
+ * the bundle, or if the label contains a string template but the bundle is null
+ */
+ public fun fillInLabel(context: Context, bundle: Bundle?): String? {
+ val label = label ?: return null
+
+ val fillInPattern = Pattern.compile("\\{(.+?)\\}")
+ val matcher = fillInPattern.matcher(label)
+ val builder = StringBuffer()
+
+ while (matcher.find()) {
+ val argName = matcher.group(1)
+ if (bundle != null && bundle.containsKey(argName)) {
+ matcher.appendReplacement(builder, "")
+ val argType = argName?.let { arguments[argName]?.type }
+ if (argType == NavType.ReferenceType) {
+ val value = context.getString(bundle.getInt(argName))
+ builder.append(value)
+ } else {
+ builder.append(bundle.getString(argName))
+ }
+ } else {
+ throw IllegalArgumentException(
+ "Could not find \"$argName\" in $bundle to fill label \"$label\""
+ )
+ }
+ }
+ matcher.appendTail(builder)
+ return builder.toString()
+ }
+
override fun toString(): String {
val sb = StringBuilder()
sb.append(javaClass.simpleName)
diff --git a/navigation/navigation-compose/build.gradle b/navigation/navigation-compose/build.gradle
index f26a72d..3d7416c 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -28,18 +28,18 @@
implementation(libs.kotlinStdlib)
implementation("androidx.compose.foundation:foundation-layout:1.0.1")
- api("androidx.activity:activity-compose:1.5.0")
+ api("androidx.activity:activity-compose:1.5.1")
api("androidx.compose.animation:animation:1.0.1")
api("androidx.compose.runtime:runtime:1.0.1")
api("androidx.compose.runtime:runtime-saveable:1.0.1")
api("androidx.compose.ui:ui:1.0.1")
- api("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0")
+ api("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1")
// old version of common-java8 conflicts with newer version, because both have
// DefaultLifecycleEventObserver.
// Outside of androidx this is resolved via constraint added to lifecycle-common,
// but it doesn't work in androidx.
// See aosp/1804059
- implementation "androidx.lifecycle:lifecycle-common-java8:2.5.0"
+ implementation "androidx.lifecycle:lifecycle-common-java8:2.5.1"
api(projectOrArtifact(":navigation:navigation-runtime-ktx"))
androidTestImplementation(projectOrArtifact(":compose:material:material"))
diff --git a/navigation/navigation-fragment/build.gradle b/navigation/navigation-fragment/build.gradle
index 9a64a90..12ced4a 100644
--- a/navigation/navigation-fragment/build.gradle
+++ b/navigation/navigation-fragment/build.gradle
@@ -23,7 +23,7 @@
}
dependencies {
- api("androidx.fragment:fragment-ktx:1.5.0")
+ api("androidx.fragment:fragment-ktx:1.5.1")
api(project(":navigation:navigation-runtime"))
api("androidx.slidingpanelayout:slidingpanelayout:1.2.0")
api(libs.kotlinStdlib)
diff --git a/navigation/navigation-runtime/build.gradle b/navigation/navigation-runtime/build.gradle
index 6478105..94c45c4 100644
--- a/navigation/navigation-runtime/build.gradle
+++ b/navigation/navigation-runtime/build.gradle
@@ -25,9 +25,9 @@
dependencies {
api(project(":navigation:navigation-common"))
- api("androidx.activity:activity-ktx:1.5.0")
- api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.0")
- api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0")
+ api("androidx.activity:activity-ktx:1.5.1")
+ api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
+ api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
api("androidx.annotation:annotation-experimental:1.1.0")
implementation('androidx.collection:collection:1.0.0')
diff --git a/navigation/navigation-ui/src/androidTest/java/androidx/navigation/ui/NavigationUITest.kt b/navigation/navigation-ui/src/androidTest/java/androidx/navigation/ui/NavigationUITest.kt
index 7c4c68e..356161d 100644
--- a/navigation/navigation-ui/src/androidTest/java/androidx/navigation/ui/NavigationUITest.kt
+++ b/navigation/navigation-ui/src/androidTest/java/androidx/navigation/ui/NavigationUITest.kt
@@ -17,8 +17,12 @@
package androidx.navigation.ui
import android.content.Context
+import android.graphics.drawable.Drawable
+import android.os.Bundle
import androidx.appcompat.widget.Toolbar
import androidx.core.content.res.TypedArrayUtils.getString
+import androidx.navigation.NavController
+import androidx.navigation.NavDestination
import androidx.navigation.NavGraph
import androidx.navigation.NavGraphNavigator
import androidx.navigation.NavHostController
@@ -51,7 +55,7 @@
@UiThreadTest
@Test
- fun navigateWithStringReferenceArgs() {
+ fun navigateWithSingleStringReferenceArg() {
val context = ApplicationProvider.getApplicationContext<Context>()
val navController = NavHostController(context)
navController.navigatorProvider.addNavigator(TestNavigator())
@@ -74,7 +78,155 @@
endDestination + "/${R.string.dest_title}"
)
- val expected = context.resources.getString(R.string.dest_title)
+ val expected = "${context.resources.getString(R.string.dest_title)}"
assertThat(toolbar.title.toString()).isEqualTo(expected)
}
+
+ @UiThreadTest
+ @Test
+ fun navigateWithMultiStringReferenceArgs() {
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ val navController = NavHostController(context)
+ navController.navigatorProvider.addNavigator(TestNavigator())
+
+ val startDestination = "start_destination"
+ val endDestination = "end_destination"
+
+ navController.graph = navController.createGraph(startDestination = startDestination) {
+ test(startDestination)
+ test("$endDestination/{test}") {
+ label = "start/{test}/end/{test}"
+ argument(name = "test") {
+ type = NavType.ReferenceType
+ }
+ }
+ }
+
+ val toolbar = Toolbar(context).apply { setupWithNavController(navController) }
+ navController.navigate(
+ endDestination + "/${R.string.dest_title}"
+ )
+
+ val argString = context.resources.getString(R.string.dest_title)
+ val expected = "start/$argString/end/$argString"
+ assertThat(toolbar.title.toString()).isEqualTo(expected)
+ }
+
+ @UiThreadTest
+ @Test(expected = IllegalArgumentException::class)
+ fun navigateWithArg_NotFoundInBundleThrows() {
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ val navController = NavHostController(context)
+ navController.navigatorProvider.addNavigator(TestNavigator())
+
+ val startDestination = "start_destination"
+ val endDestination = "end_destination"
+ val labelString = "end/{test}"
+
+ navController.graph = navController.createGraph(startDestination = startDestination) {
+ test(startDestination)
+ test(endDestination) {
+ label = labelString
+ }
+ }
+
+ val toolbar = Toolbar(context).apply { setupWithNavController(navController) }
+
+ // empty bundle
+ val testListener = createToolbarOnDestinationChangedListener(
+ toolbar = toolbar, bundle = Bundle(), context = context, navController = navController
+ )
+
+ // navigate to destination. Since the argument {test} is not present in the bundle,
+ // this should throw an IllegalArgumentException
+ navController.apply {
+ addOnDestinationChangedListener(testListener)
+ navigate(route = endDestination)
+ }
+ }
+
+ @UiThreadTest
+ @Test(expected = IllegalArgumentException::class)
+ fun navigateWithArg_NullBundleThrows() {
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ val navController = NavHostController(context)
+ navController.navigatorProvider.addNavigator(TestNavigator())
+
+ val startDestination = "start_destination"
+ val endDestination = "end_destination"
+ val labelString = "end/{test}"
+
+ navController.graph = navController.createGraph(startDestination = startDestination) {
+ test(startDestination)
+ test("$endDestination/{test}") {
+ label = labelString
+ argument(name = "test") {
+ type = NavType.ReferenceType
+ }
+ }
+ }
+
+ val toolbar = Toolbar(context).apply { setupWithNavController(navController) }
+
+ // null Bundle
+ val testListener = createToolbarOnDestinationChangedListener(
+ toolbar = toolbar, bundle = null, context = context, navController = navController
+ )
+
+ // navigate to destination, should throw due to template found but null bundle
+ navController.apply {
+ addOnDestinationChangedListener(testListener)
+ navigate(route = endDestination + "/${R.string.dest_title}")
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun navigateWithStaticLabel() {
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ val navController = NavHostController(context)
+ navController.navigatorProvider.addNavigator(TestNavigator())
+
+ val startDestination = "start_destination"
+ val endDestination = "end_destination"
+ val labelString = "end/test"
+
+ navController.graph = navController.createGraph(startDestination = startDestination) {
+ test(startDestination)
+ test(endDestination) {
+ label = labelString
+ }
+ }
+
+ val toolbar = Toolbar(context).apply { setupWithNavController(navController) }
+
+ // navigate to destination, static label should be returned directly
+ navController.navigate(route = endDestination)
+ assertThat(toolbar.title.toString()).isEqualTo(labelString)
+ }
+
+ private fun createToolbarOnDestinationChangedListener(
+ toolbar: Toolbar,
+ bundle: Bundle?,
+ context: Context,
+ navController: NavController
+ ): NavController.OnDestinationChangedListener {
+ return object : AbstractAppBarOnDestinationChangedListener(
+ context, AppBarConfiguration.Builder(navController.graph).build()
+ ) {
+ override fun setTitle(title: CharSequence?) {
+ toolbar.title = title
+ }
+
+ override fun onDestinationChanged(
+ controller: NavController,
+ destination: NavDestination,
+ arguments: Bundle?
+ ) {
+ super.onDestinationChanged(controller, destination, bundle)
+ }
+
+ override fun setNavigationIcon(icon: Drawable?, contentDescription: Int) {}
+ }
+ }
}
diff --git a/navigation/navigation-ui/src/main/java/androidx/navigation/ui/AbstractAppBarOnDestinationChangedListener.kt b/navigation/navigation-ui/src/main/java/androidx/navigation/ui/AbstractAppBarOnDestinationChangedListener.kt
index 2c73ae9..9962d7b 100644
--- a/navigation/navigation-ui/src/main/java/androidx/navigation/ui/AbstractAppBarOnDestinationChangedListener.kt
+++ b/navigation/navigation-ui/src/main/java/androidx/navigation/ui/AbstractAppBarOnDestinationChangedListener.kt
@@ -26,10 +26,8 @@
import androidx.navigation.FloatingWindow
import androidx.navigation.NavController
import androidx.navigation.NavDestination
-import androidx.navigation.NavType
import androidx.navigation.ui.NavigationUI.matchDestinations
import java.lang.ref.WeakReference
-import java.util.regex.Pattern
/**
* The abstract OnDestinationChangedListener for keeping any type of app bar updated.
@@ -51,7 +49,6 @@
protected abstract fun setNavigationIcon(icon: Drawable?, @StringRes contentDescription: Int)
- @Suppress("DEPRECATION")
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
@@ -65,33 +62,12 @@
controller.removeOnDestinationChangedListener(this)
return
}
- val label = destination.label
- if (label != null) {
- // Fill in the data pattern with the args to build a valid URI
- val title = StringBuffer()
- val fillInPattern = Pattern.compile("\\{(.+?)\\}")
- val matcher = fillInPattern.matcher(label)
- while (matcher.find()) {
- val argName = matcher.group(1)
- if (arguments != null && arguments.containsKey(argName)) {
- matcher.appendReplacement(title, "")
- val argType = argName?.let { destination.arguments[argName]?.type }
- if (argType == NavType.ReferenceType) {
- val value = context.resources.getString(arguments[argName] as Int)
- title.append(value)
- } else {
- title.append(arguments[argName].toString())
- }
- } else {
- throw IllegalArgumentException(
- "Could not find \"$argName\" in $arguments to fill label \"$label\""
- )
- }
- }
- matcher.appendTail(title)
- setTitle(title)
+ val label = destination.fillInLabel(context, arguments)
+ if (label != null) {
+ setTitle(label)
}
+
val isTopLevelDestination = destination.matchDestinations(topLevelDestinations)
if (openableLayout == null && isTopLevelDestination) {
setNavigationIcon(null, 0)
diff --git a/paging/paging-common/build.gradle b/paging/paging-common/build.gradle
index fdc7e4b..8f4aacb5 100644
--- a/paging/paging-common/build.gradle
+++ b/paging/paging-common/build.gradle
@@ -23,6 +23,11 @@
}
dependencies {
+ // Atomic Group
+ constraints {
+ implementation(project(":paging:paging-runtime"))
+ }
+
api("androidx.annotation:annotation:1.3.0")
api("androidx.arch.core:core-common:2.1.0")
api(libs.kotlinStdlib)
diff --git a/paging/paging-common/src/main/kotlin/androidx/paging/PageEvent.kt b/paging/paging-common/src/main/kotlin/androidx/paging/PageEvent.kt
index 5acefce..cfbc967 100644
--- a/paging/paging-common/src/main/kotlin/androidx/paging/PageEvent.kt
+++ b/paging/paging-common/src/main/kotlin/androidx/paging/PageEvent.kt
@@ -67,6 +67,16 @@
mediatorLoadStates = mediatorLoadStates,
)
}
+
+ override fun toString(): String {
+ return appendMediatorStatesIfNotNull(mediatorLoadStates) {
+ """PageEvent.StaticList with ${data.size} items (
+ | first item: ${data.firstOrNull()}
+ | last item: ${data.lastOrNull()}
+ | sourceLoadStates: $sourceLoadStates
+ """
+ }
+ }
}
// Intentional to prefer Refresh, Prepend, Append constructors from Companion.
@@ -217,6 +227,21 @@
),
)
}
+
+ override fun toString(): String {
+ val itemCount = pages.fold(0) { total, page -> total + page.data.size }
+ val placeholdersBefore = if (placeholdersBefore != -1) "$placeholdersBefore" else "none"
+ val placeholdersAfter = if (placeholdersAfter != -1) "$placeholdersAfter" else "none"
+ return appendMediatorStatesIfNotNull(mediatorLoadStates) {
+ """PageEvent.Insert for $loadType, with $itemCount items (
+ | first item: ${pages.firstOrNull()?.data?.firstOrNull()}
+ | last item: ${pages.lastOrNull()?.data?.lastOrNull()}
+ | placeholdersBefore: $placeholdersBefore
+ | placeholdersAfter: $placeholdersAfter
+ | sourceLoadStates: $sourceLoadStates
+ """
+ }
+ }
}
// TODO: b/195658070 consider refactoring Drop events to carry full source/mediator states.
@@ -242,6 +267,21 @@
}
val pageCount get() = maxPageOffset - minPageOffset + 1
+
+ override fun toString(): String {
+ val direction = when (loadType) {
+ APPEND -> "end"
+ PREPEND -> "front"
+ else -> throw IllegalArgumentException(
+ "Drop load type must be PREPEND or APPEND"
+ )
+ }
+ return """PageEvent.Drop from the $direction (
+ | minPageOffset: $minPageOffset
+ | maxPageOffset: $maxPageOffset
+ | placeholdersRemaining: $placeholdersRemaining
+ |)""".trimMargin()
+ }
}
/**
@@ -253,7 +293,16 @@
data class LoadStateUpdate<T : Any>(
val source: LoadStates,
val mediator: LoadStates? = null,
- ) : PageEvent<T>()
+ ) : PageEvent<T>() {
+
+ override fun toString(): String {
+ return appendMediatorStatesIfNotNull(mediator) {
+ """PageEvent.LoadStateUpdate (
+ | sourceLoadStates: $source
+ """
+ }
+ }
+ }
@Suppress("UNCHECKED_CAST")
open suspend fun <R : Any> map(transform: suspend (T) -> R): PageEvent<R> = this as PageEvent<R>
@@ -264,4 +313,15 @@
}
open suspend fun filter(predicate: suspend (T) -> Boolean): PageEvent<T> = this
+
+ protected inline fun appendMediatorStatesIfNotNull(
+ mediatorStates: LoadStates?,
+ log: () -> String
+ ): String {
+ var newLog = log()
+ if (mediatorStates != null) {
+ newLog = newLog.plus("""| mediatorLoadStates: $mediatorStates${"\n"}""")
+ }
+ return newLog.plus("|)").trimMargin()
+ }
}
diff --git a/paging/paging-common/src/main/kotlin/androidx/paging/PageFetcher.kt b/paging/paging-common/src/main/kotlin/androidx/paging/PageFetcher.kt
index 57a93dc..5997922 100644
--- a/paging/paging-common/src/main/kotlin/androidx/paging/PageFetcher.kt
+++ b/paging/paging-common/src/main/kotlin/androidx/paging/PageFetcher.kt
@@ -26,6 +26,7 @@
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
internal class PageFetcher<Key : Any, Value : Any>(
@@ -122,6 +123,7 @@
.simpleMapLatest { generation ->
val downstreamFlow = generation.snapshot
.injectRemoteEvents(generation.job, remoteMediatorAccessor)
+ .onEach { log(VERBOSE) { "Sent $it" } }
PagingData(
flow = downstreamFlow,
diff --git a/paging/paging-common/src/main/kotlin/androidx/paging/PagingDataDiffer.kt b/paging/paging-common/src/main/kotlin/androidx/paging/PagingDataDiffer.kt
index 5305247..f061f40 100644
--- a/paging/paging-common/src/main/kotlin/androidx/paging/PagingDataDiffer.kt
+++ b/paging/paging-common/src/main/kotlin/androidx/paging/PagingDataDiffer.kt
@@ -142,6 +142,7 @@
collectFromRunner.runInIsolation {
uiReceiver = pagingData.uiReceiver
pagingData.flow.collect { event ->
+ log(VERBOSE) { "Collected $event" }
withContext(mainContext) {
/**
* The hint receiver of a new generation is set only after it has been
diff --git a/paging/paging-runtime/build.gradle b/paging/paging-runtime/build.gradle
index e7cc847..eb2de3b 100644
--- a/paging/paging-runtime/build.gradle
+++ b/paging/paging-runtime/build.gradle
@@ -31,6 +31,11 @@
}
dependencies {
+ //Atomic Group
+ constraints {
+ implementation(project(":paging:paging-common"))
+ }
+
api(project(":paging:paging-common"))
// Ensure that the -ktx dependency graph mirrors the Java dependency graph
api(project(":paging:paging-common-ktx"))
diff --git a/recyclerview/recyclerview/api/1.3.0-beta02.txt b/recyclerview/recyclerview/api/1.3.0-beta02.txt
index b4c70ae..ba238e0 100644
--- a/recyclerview/recyclerview/api/1.3.0-beta02.txt
+++ b/recyclerview/recyclerview/api/1.3.0-beta02.txt
@@ -272,8 +272,8 @@
}
public class LinearLayoutManager extends androidx.recyclerview.widget.RecyclerView.LayoutManager implements androidx.recyclerview.widget.ItemTouchHelper.ViewDropHandler androidx.recyclerview.widget.RecyclerView.SmoothScroller.ScrollVectorProvider {
- ctor public LinearLayoutManager(android.content.Context);
- ctor public LinearLayoutManager(android.content.Context, int, boolean);
+ ctor public LinearLayoutManager(android.content.Context!);
+ ctor public LinearLayoutManager(android.content.Context!, int, boolean);
ctor public LinearLayoutManager(android.content.Context, android.util.AttributeSet?, int, int);
method protected void calculateExtraLayoutSpace(androidx.recyclerview.widget.RecyclerView.State, int[]);
method public android.graphics.PointF? computeScrollVectorForPosition(int);
diff --git a/recyclerview/recyclerview/api/current.ignore b/recyclerview/recyclerview/api/current.ignore
index 453fe1d..de0ee4c 100644
--- a/recyclerview/recyclerview/api/current.ignore
+++ b/recyclerview/recyclerview/api/current.ignore
@@ -1,3 +1,5 @@
// Baseline format: 1.0
-RemovedMethod: androidx.recyclerview.widget.SortedListAdapterCallback#SortedListAdapterCallback(androidx.recyclerview.widget.RecyclerView.Adapter):
- Removed constructor androidx.recyclerview.widget.SortedListAdapterCallback(androidx.recyclerview.widget.RecyclerView.Adapter)
+InvalidNullConversion: androidx.recyclerview.widget.LinearLayoutManager#LinearLayoutManager(android.content.Context) parameter #0:
+ Attempted to remove @NonNull annotation from parameter arg1 in androidx.recyclerview.widget.LinearLayoutManager(android.content.Context arg1)
+InvalidNullConversion: androidx.recyclerview.widget.LinearLayoutManager#LinearLayoutManager(android.content.Context, int, boolean) parameter #0:
+ Attempted to remove @NonNull annotation from parameter arg1 in androidx.recyclerview.widget.LinearLayoutManager(android.content.Context arg1, int arg2, boolean arg3)
diff --git a/recyclerview/recyclerview/api/current.txt b/recyclerview/recyclerview/api/current.txt
index b4c70ae..ba238e0 100644
--- a/recyclerview/recyclerview/api/current.txt
+++ b/recyclerview/recyclerview/api/current.txt
@@ -272,8 +272,8 @@
}
public class LinearLayoutManager extends androidx.recyclerview.widget.RecyclerView.LayoutManager implements androidx.recyclerview.widget.ItemTouchHelper.ViewDropHandler androidx.recyclerview.widget.RecyclerView.SmoothScroller.ScrollVectorProvider {
- ctor public LinearLayoutManager(android.content.Context);
- ctor public LinearLayoutManager(android.content.Context, int, boolean);
+ ctor public LinearLayoutManager(android.content.Context!);
+ ctor public LinearLayoutManager(android.content.Context!, int, boolean);
ctor public LinearLayoutManager(android.content.Context, android.util.AttributeSet?, int, int);
method protected void calculateExtraLayoutSpace(androidx.recyclerview.widget.RecyclerView.State, int[]);
method public android.graphics.PointF? computeScrollVectorForPosition(int);
diff --git a/recyclerview/recyclerview/api/public_plus_experimental_1.3.0-beta02.txt b/recyclerview/recyclerview/api/public_plus_experimental_1.3.0-beta02.txt
index b4c70ae..ba238e0 100644
--- a/recyclerview/recyclerview/api/public_plus_experimental_1.3.0-beta02.txt
+++ b/recyclerview/recyclerview/api/public_plus_experimental_1.3.0-beta02.txt
@@ -272,8 +272,8 @@
}
public class LinearLayoutManager extends androidx.recyclerview.widget.RecyclerView.LayoutManager implements androidx.recyclerview.widget.ItemTouchHelper.ViewDropHandler androidx.recyclerview.widget.RecyclerView.SmoothScroller.ScrollVectorProvider {
- ctor public LinearLayoutManager(android.content.Context);
- ctor public LinearLayoutManager(android.content.Context, int, boolean);
+ ctor public LinearLayoutManager(android.content.Context!);
+ ctor public LinearLayoutManager(android.content.Context!, int, boolean);
ctor public LinearLayoutManager(android.content.Context, android.util.AttributeSet?, int, int);
method protected void calculateExtraLayoutSpace(androidx.recyclerview.widget.RecyclerView.State, int[]);
method public android.graphics.PointF? computeScrollVectorForPosition(int);
diff --git a/recyclerview/recyclerview/api/public_plus_experimental_current.txt b/recyclerview/recyclerview/api/public_plus_experimental_current.txt
index b4c70ae..ba238e0 100644
--- a/recyclerview/recyclerview/api/public_plus_experimental_current.txt
+++ b/recyclerview/recyclerview/api/public_plus_experimental_current.txt
@@ -272,8 +272,8 @@
}
public class LinearLayoutManager extends androidx.recyclerview.widget.RecyclerView.LayoutManager implements androidx.recyclerview.widget.ItemTouchHelper.ViewDropHandler androidx.recyclerview.widget.RecyclerView.SmoothScroller.ScrollVectorProvider {
- ctor public LinearLayoutManager(android.content.Context);
- ctor public LinearLayoutManager(android.content.Context, int, boolean);
+ ctor public LinearLayoutManager(android.content.Context!);
+ ctor public LinearLayoutManager(android.content.Context!, int, boolean);
ctor public LinearLayoutManager(android.content.Context, android.util.AttributeSet?, int, int);
method protected void calculateExtraLayoutSpace(androidx.recyclerview.widget.RecyclerView.State, int[]);
method public android.graphics.PointF? computeScrollVectorForPosition(int);
diff --git a/recyclerview/recyclerview/api/restricted_1.3.0-beta02.txt b/recyclerview/recyclerview/api/restricted_1.3.0-beta02.txt
index a3a2ebf..4086071 100644
--- a/recyclerview/recyclerview/api/restricted_1.3.0-beta02.txt
+++ b/recyclerview/recyclerview/api/restricted_1.3.0-beta02.txt
@@ -272,8 +272,8 @@
}
public class LinearLayoutManager extends androidx.recyclerview.widget.RecyclerView.LayoutManager implements androidx.recyclerview.widget.ItemTouchHelper.ViewDropHandler androidx.recyclerview.widget.RecyclerView.SmoothScroller.ScrollVectorProvider {
- ctor public LinearLayoutManager(android.content.Context);
- ctor public LinearLayoutManager(android.content.Context, @androidx.recyclerview.widget.RecyclerView.Orientation int, boolean);
+ ctor public LinearLayoutManager(android.content.Context!);
+ ctor public LinearLayoutManager(android.content.Context!, @androidx.recyclerview.widget.RecyclerView.Orientation int, boolean);
ctor public LinearLayoutManager(android.content.Context, android.util.AttributeSet?, int, int);
method protected void calculateExtraLayoutSpace(androidx.recyclerview.widget.RecyclerView.State, int[]);
method public android.graphics.PointF? computeScrollVectorForPosition(int);
diff --git a/recyclerview/recyclerview/api/restricted_current.ignore b/recyclerview/recyclerview/api/restricted_current.ignore
index 453fe1d..de0ee4c 100644
--- a/recyclerview/recyclerview/api/restricted_current.ignore
+++ b/recyclerview/recyclerview/api/restricted_current.ignore
@@ -1,3 +1,5 @@
// Baseline format: 1.0
-RemovedMethod: androidx.recyclerview.widget.SortedListAdapterCallback#SortedListAdapterCallback(androidx.recyclerview.widget.RecyclerView.Adapter):
- Removed constructor androidx.recyclerview.widget.SortedListAdapterCallback(androidx.recyclerview.widget.RecyclerView.Adapter)
+InvalidNullConversion: androidx.recyclerview.widget.LinearLayoutManager#LinearLayoutManager(android.content.Context) parameter #0:
+ Attempted to remove @NonNull annotation from parameter arg1 in androidx.recyclerview.widget.LinearLayoutManager(android.content.Context arg1)
+InvalidNullConversion: androidx.recyclerview.widget.LinearLayoutManager#LinearLayoutManager(android.content.Context, int, boolean) parameter #0:
+ Attempted to remove @NonNull annotation from parameter arg1 in androidx.recyclerview.widget.LinearLayoutManager(android.content.Context arg1, int arg2, boolean arg3)
diff --git a/recyclerview/recyclerview/api/restricted_current.txt b/recyclerview/recyclerview/api/restricted_current.txt
index a3a2ebf..4086071 100644
--- a/recyclerview/recyclerview/api/restricted_current.txt
+++ b/recyclerview/recyclerview/api/restricted_current.txt
@@ -272,8 +272,8 @@
}
public class LinearLayoutManager extends androidx.recyclerview.widget.RecyclerView.LayoutManager implements androidx.recyclerview.widget.ItemTouchHelper.ViewDropHandler androidx.recyclerview.widget.RecyclerView.SmoothScroller.ScrollVectorProvider {
- ctor public LinearLayoutManager(android.content.Context);
- ctor public LinearLayoutManager(android.content.Context, @androidx.recyclerview.widget.RecyclerView.Orientation int, boolean);
+ ctor public LinearLayoutManager(android.content.Context!);
+ ctor public LinearLayoutManager(android.content.Context!, @androidx.recyclerview.widget.RecyclerView.Orientation int, boolean);
ctor public LinearLayoutManager(android.content.Context, android.util.AttributeSet?, int, int);
method protected void calculateExtraLayoutSpace(androidx.recyclerview.widget.RecyclerView.State, int[]);
method public android.graphics.PointF? computeScrollVectorForPosition(int);
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
index 8386fcf..771487a 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
@@ -156,7 +156,11 @@
*
* @param context Current context, will be used to access resources.
*/
- public LinearLayoutManager(@NonNull Context context) {
+ public LinearLayoutManager(
+ // Suppressed because fixing it requires a source-incompatible change to a very
+ // commonly used constructor, for no benefit: the context parameter is unused
+ @SuppressLint("UnknownNullness") Context context
+ ) {
this(context, RecyclerView.DEFAULT_ORIENTATION, false);
}
@@ -166,8 +170,13 @@
* #VERTICAL}.
* @param reverseLayout When set to true, layouts from end to start.
*/
- public LinearLayoutManager(@NonNull Context context, @RecyclerView.Orientation int orientation,
- boolean reverseLayout) {
+ public LinearLayoutManager(
+ // Suppressed because fixing it requires a source-incompatible change to a very
+ // commonly used constructor, for no benefit: the context parameter is unused
+ @SuppressLint("UnknownNullness") Context context,
+ @RecyclerView.Orientation int orientation,
+ boolean reverseLayout
+ ) {
setOrientation(orientation);
setReverseLayout(reverseLayout);
}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java
index e7cf93d..afbb5e3 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/AutoMigrationTest.java
@@ -19,6 +19,7 @@
import static com.google.common.truth.Truth.assertThat;
+import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteException;
import androidx.annotation.NonNull;
@@ -85,7 +86,7 @@
3,
true
);
- } catch (IllegalStateException e) {
+ } catch (SQLiteConstraintException e) {
assertThat(e.getMessage()).isEqualTo("Foreign key violation(s) detected in 'Entity9'."
+ "\nNumber of different violations discovered: 1"
+ "\nNumber of rows in violation: 2"
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/WriteAheadLoggingTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/WriteAheadLoggingTest.java
index a5ba88e..1e2df59 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/WriteAheadLoggingTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/WriteAheadLoggingTest.java
@@ -50,6 +50,7 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -154,6 +155,7 @@
stopObserver(user1, observer);
}
+ @Ignore("b/239575607")
@Test
public void parallelWrites() throws InterruptedException, ExecutionException {
int numberOfThreads = 10;
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/DeclarationCollector.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/DeclarationCollector.kt
index 038f117..db24932 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/DeclarationCollector.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/DeclarationCollector.kt
@@ -26,15 +26,9 @@
val existingFieldNames = mutableSetOf<String>()
suspend fun SequenceScope<XFieldElement>.yieldAllFields(type: XTypeElement) {
// yield all fields declared directly on this type
- type.getDeclaredFields().forEach {
- if (existingFieldNames.add(it.name)) {
- if (type == xTypeElement) {
- yield(it)
- } else {
- yield(it.copyTo(xTypeElement))
- }
- }
- }
+ type.getDeclaredFields()
+ .filter { existingFieldNames.add(it.name) }
+ .forEach { yield(it) }
// visit all declared fields on super types
type.superClass?.typeElement?.let { parent ->
yieldAllFields(parent)
@@ -78,7 +72,6 @@
type.getDeclaredMethods()
.filter { it.isAccessibleFrom(xTypeElement.packageName) }
.filterNot { it.isStaticInterfaceMethod() }
- .map { it.copyTo(xTypeElement) }
.forEach {
methodsByName.getOrPut(it.name) { linkedSetOf() }.add(it)
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XFieldElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XFieldElement.kt
index 19fa7ef..0a7095d 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XFieldElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XFieldElement.kt
@@ -38,9 +38,4 @@
override val fallbackLocationText: String
get() = "$name in ${enclosingElement.fallbackLocationText}"
-
- /**
- * Creates a new [XFieldElement] where containing element is replaced with [newContainer].
- */
- fun copyTo(newContainer: XTypeElement): XFieldElement
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XMethodElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XMethodElement.kt
index a023f0d..96fcfc5 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XMethodElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XMethodElement.kt
@@ -117,11 +117,6 @@
fun overrides(other: XMethodElement, owner: XTypeElement): Boolean
/**
- * Creates a new [XMethodElement] where containing element is replaced with [newContainer].
- */
- fun copyTo(newContainer: XTypeElement): XMethodElement
-
- /**
* If true, this method can be invoked from Java sources. This is especially important for
* Kotlin functions that receive value class as a parameter as they cannot be called from Java
* sources.
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacConstructorElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacConstructorElement.kt
index ef6c920..135970e 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacConstructorElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacConstructorElement.kt
@@ -27,13 +27,8 @@
internal class JavacConstructorElement(
env: JavacProcessingEnv,
- containing: JavacTypeElement,
element: ExecutableElement
-) : JavacExecutableElement(
- env,
- containing,
- element
-),
+) : JavacExecutableElement(env, element),
XConstructorElement {
init {
check(element.kind == ElementKind.CONSTRUCTOR) {
@@ -56,7 +51,6 @@
JavacMethodParameter(
env = env,
enclosingElement = this,
- containing = containing,
element = variable,
kotlinMetadataFactory = { kotlinMetadata?.parameters?.getOrNull(index) },
argIndex = index
@@ -65,16 +59,15 @@
}
override val executableType: XConstructorType by lazy {
- val asMemberOf = env.typeUtils.asMemberOf(containing.type.typeMirror, element)
JavacConstructorType(
env = env,
element = this,
- executableType = MoreTypes.asExecutable(asMemberOf)
+ executableType = MoreTypes.asExecutable(element.asType())
)
}
override fun asMemberOf(other: XType): XConstructorType {
- return if (other !is JavacDeclaredType || containing.type.isSameType(other)) {
+ return if (other !is JavacDeclaredType || enclosingElement.type.isSameType(other)) {
executableType
} else {
val asMemberOf = env.typeUtils.asMemberOf(other.typeMirror, element)
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt
index ff96b7e..e02045a 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt
@@ -79,6 +79,10 @@
return element.toString()
}
+ final override val equalityItems: Array<out Any?> by lazy {
+ arrayOf(element)
+ }
+
override fun equals(other: Any?): Boolean {
return XEquality.equals(this, other)
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacEnumEntry.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacEnumEntry.kt
index afee3e4..8a9469e 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacEnumEntry.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacEnumEntry.kt
@@ -29,10 +29,6 @@
override val name: String
get() = element.simpleName.toString()
- override val equalityItems: Array<out Any?> by lazy {
- arrayOf(name, enclosingElement)
- }
-
override val fallbackLocationText: String
get() = "$name enum entry in ${enclosingElement.fallbackLocationText}"
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacExecutableElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacExecutableElement.kt
index 2583b9b..5dc5b2e 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacExecutableElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacExecutableElement.kt
@@ -25,12 +25,8 @@
internal abstract class JavacExecutableElement(
env: JavacProcessingEnv,
- val containing: JavacTypeElement,
override val element: ExecutableElement
-) : JavacElement(
- env,
- element
-),
+) : JavacElement(env, element),
XExecutableElement,
XHasModifiers by JavacHasModifiers(element) {
abstract val kotlinMetadata: KmExecutable?
@@ -41,10 +37,6 @@
abstract override val parameters: List<JavacMethodParameter>
- override val equalityItems: Array<out Any?> by lazy {
- arrayOf(element, containing)
- }
-
override val enclosingElement: JavacTypeElement by lazy {
element.requireEnclosingType(env)
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFieldElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFieldElement.kt
index 1cefc90..bc8dbd7 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFieldElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFieldElement.kt
@@ -18,16 +18,14 @@
import androidx.room.compiler.processing.XFieldElement
import androidx.room.compiler.processing.XHasModifiers
-import androidx.room.compiler.processing.XTypeElement
import androidx.room.compiler.processing.javac.kotlin.KmProperty
import androidx.room.compiler.processing.javac.kotlin.KmType
import javax.lang.model.element.VariableElement
internal class JavacFieldElement(
env: JavacProcessingEnv,
- containing: JavacTypeElement,
element: VariableElement
-) : JavacVariableElement(env, containing, element),
+) : JavacVariableElement(env, element),
XFieldElement,
XHasModifiers by JavacHasModifiers(element) {
@@ -44,15 +42,4 @@
override val closestMemberContainer: JavacTypeElement
get() = enclosingElement
-
- override fun copyTo(newContainer: XTypeElement): JavacFieldElement {
- check(newContainer is JavacTypeElement) {
- "Unexpected container (${newContainer::class}), expected JavacTypeElement"
- }
- return JavacFieldElement(
- env = env,
- containing = newContainer,
- element = element
- )
- }
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodElement.kt
index c06ca00..68b8097 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodElement.kt
@@ -33,13 +33,8 @@
internal class JavacMethodElement(
env: JavacProcessingEnv,
- containing: JavacTypeElement,
element: ExecutableElement
-) : JavacExecutableElement(
- env,
- containing,
- element
-),
+) : JavacExecutableElement(env, element),
XMethodElement {
init {
check(element.kind == ElementKind.METHOD) {
@@ -66,7 +61,6 @@
JavacMethodParameter(
env = env,
enclosingElement = this,
- containing = containing,
element = variable,
kotlinMetadataFactory = {
val metadataParamIndex = if (isExtensionFunction()) index - 1 else index
@@ -82,19 +76,16 @@
}
override val executableType: JavacMethodType by lazy {
- val asMemberOf = env.typeUtils.asMemberOf(containing.type.typeMirror, element)
JavacMethodType.create(
env = env,
element = this,
- executableType = MoreTypes.asExecutable(asMemberOf)
+ executableType = MoreTypes.asExecutable(element.asType())
)
}
override val returnType: JavacType by lazy {
- val asMember = env.typeUtils.asMemberOf(containing.type.typeMirror, element)
- val asExec = MoreTypes.asExecutable(asMember)
- env.wrap<JavacType>(
- typeMirror = asExec.returnType,
+ env.wrap(
+ typeMirror = element.returnType,
kotlinType = if (isSuspendFunction()) {
// Don't use Kotlin metadata for suspend functions since we want the Java
// perspective. In Java, a suspend function returns Object and contains an extra
@@ -109,7 +100,7 @@
}
override fun asMemberOf(other: XType): XMethodType {
- return if (other !is JavacDeclaredType || containing.type.isSameType(other)) {
+ return if (other !is JavacDeclaredType || enclosingElement.type.isSameType(other)) {
executableType
} else {
val asMemberOf = env.typeUtils.asMemberOf(other.typeMirror, element)
@@ -142,15 +133,6 @@
return MoreElements.overrides(element, other.element, owner.element, env.typeUtils)
}
- override fun copyTo(newContainer: XTypeElement): XMethodElement {
- check(newContainer is JavacTypeElement)
- return JavacMethodElement(
- env = env,
- containing = newContainer,
- element = element
- )
- }
-
override fun hasKotlinDefaultImpl(): Boolean {
fun paramsMatch(
ourParams: List<JavacMethodParameter>,
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodParameter.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodParameter.kt
index b6a4163..7288af9 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodParameter.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacMethodParameter.kt
@@ -26,11 +26,10 @@
internal class JavacMethodParameter(
env: JavacProcessingEnv,
override val enclosingElement: JavacExecutableElement,
- containing: JavacTypeElement,
element: VariableElement,
kotlinMetadataFactory: () -> KmValueParameter?,
val argIndex: Int
-) : JavacVariableElement(env, containing, element), XExecutableParameterElement {
+) : JavacVariableElement(env, element), XExecutableParameterElement {
private val kotlinMetadata by lazy { kotlinMetadataFactory() }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
index 25912f6..0d0a3b0 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
@@ -259,20 +259,16 @@
}
fun wrapExecutableElement(element: ExecutableElement): JavacExecutableElement {
- val enclosingType = element.requireEnclosingType(this)
-
return when (element.kind) {
ElementKind.CONSTRUCTOR -> {
JavacConstructorElement(
env = this,
- containing = enclosingType,
element = element
)
}
ElementKind.METHOD -> {
JavacMethodElement(
env = this,
- containing = enclosingType,
element = element
)
}
@@ -288,9 +284,7 @@
param.element.simpleName == element.simpleName
} ?: error("Unable to create variable element for $element")
}
- is TypeElement -> {
- JavacFieldElement(this, wrapTypeElement(enclosingElement), element)
- }
+ is TypeElement -> JavacFieldElement(this, element)
else -> error("Unsupported enclosing type $enclosingElement for $element")
}
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
index 4911f16..0eb5474 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
@@ -88,7 +88,6 @@
JavacFieldElement(
env = env,
element = it,
- containing = this
)
}
}
@@ -145,7 +144,6 @@
ElementFilter.methodsIn(element.enclosedElements).map {
JavacMethodElement(
env = env,
- containing = this,
element = it
)
}.filterMethodsByConfig(env)
@@ -159,7 +157,6 @@
return ElementFilter.constructorsIn(element.enclosedElements).map {
JavacConstructorElement(
env = env,
- containing = this,
element = it
)
}
@@ -178,7 +175,7 @@
}
override val type: JavacDeclaredType by lazy {
- env.wrap<JavacDeclaredType>(
+ env.wrap(
typeMirror = element.asType(),
kotlinType = kotlinMetadata?.kmType,
elementNullability = element.nullability
@@ -225,10 +222,6 @@
}
}
- override val equalityItems: Array<out Any?> by lazy {
- arrayOf(element)
- }
-
class DefaultJavacTypeElement(
env: JavacProcessingEnv,
element: TypeElement
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeParameterElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeParameterElement.kt
index 42d85fb..fdf8bb3 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeParameterElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeParameterElement.kt
@@ -47,7 +47,4 @@
override val closestMemberContainer: XMemberContainer
get() = enclosingElement.closestMemberContainer
-
- override val equalityItems: Array<out Any?>
- get() = arrayOf(element)
}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacVariableElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacVariableElement.kt
index e999b14..daec9e9 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacVariableElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacVariableElement.kt
@@ -24,7 +24,6 @@
internal abstract class JavacVariableElement(
env: JavacProcessingEnv,
- val containing: JavacTypeElement,
override val element: VariableElement
) : JavacElement(env, element), XVariableElement {
@@ -34,30 +33,24 @@
get() = element.simpleName.toString()
override val type: JavacType by lazy {
- MoreTypes.asMemberOf(env.typeUtils, containing.type.typeMirror, element).let {
- env.wrap<JavacType>(
- typeMirror = it,
- kotlinType = kotlinType,
- elementNullability = element.nullability
- )
- }
+ env.wrap(
+ typeMirror = element.asType(),
+ kotlinType = kotlinType,
+ elementNullability = element.nullability
+ )
}
override fun asMemberOf(other: XType): JavacType {
- return if (containing.type.isSameType(other)) {
+ return if (closestMemberContainer.type?.isSameType(other) == true) {
type
} else {
check(other is JavacDeclaredType)
val asMember = MoreTypes.asMemberOf(env.typeUtils, other.typeMirror, element)
- env.wrap<JavacType>(
+ env.wrap(
typeMirror = asMember,
kotlinType = kotlinType,
elementNullability = element.nullability
)
}
}
-
- override val equalityItems: Array<out Any?> by lazy {
- arrayOf(element, containing)
- }
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspConstructorElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspConstructorElement.kt
index 614d19c..a452ced 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspConstructorElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspConstructorElement.kt
@@ -24,14 +24,8 @@
internal class KspConstructorElement(
env: KspProcessingEnv,
- override val containing: KspTypeElement,
declaration: KSFunctionDeclaration
-) : KspExecutableElement(
- env = env,
- containing = containing,
- declaration = declaration
-),
- XConstructorElement {
+) : KspExecutableElement(env, declaration), XConstructorElement {
override val name: String
get() = "<init>"
@@ -55,7 +49,7 @@
KspConstructorType(
env = env,
origin = this,
- containing = this.containing.type
+ containing = this.enclosingElement.type
)
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspElement.kt
index 3f63842..077063f 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspElement.kt
@@ -41,6 +41,10 @@
}
}
+ final override val equalityItems: Array<out Any?> by lazy {
+ arrayOf(declaration)
+ }
+
override fun equals(other: Any?): Boolean {
return XEquality.equals(this, other)
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
index ac41dfe..818856d 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
@@ -31,12 +31,8 @@
internal abstract class KspExecutableElement(
env: KspProcessingEnv,
- open val containing: KspMemberContainer,
override val declaration: KSFunctionDeclaration
-) : KspElement(
- env = env,
- declaration = declaration
-),
+) : KspElement(env, declaration),
XExecutableElement,
XHasModifiers by KspHasModifiers.create(declaration),
XAnnotated by KspAnnotated.create(
@@ -45,10 +41,6 @@
filter = NO_USE_SITE
) {
- override val equalityItems: Array<out Any?> by lazy {
- arrayOf(containing, declaration)
- }
-
override val enclosingElement: KspMemberContainer by lazy {
declaration.requireEnclosingMemberContainer(env)
}
@@ -94,22 +86,8 @@
}
return when {
- declaration.isConstructor() -> {
- KspConstructorElement(
- env = env,
- containing = enclosingContainer as? KspTypeElement ?: error(
- "The container for $declaration should be a type element"
- ),
- declaration = declaration
- )
- }
- else -> {
- KspMethodElement.create(
- env = env,
- containing = enclosingContainer,
- declaration = declaration
- )
- }
+ declaration.isConstructor() -> KspConstructorElement(env, declaration)
+ else -> KspMethodElement.create(env, declaration)
}
}
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt
index 135ef76..8a6ed9a 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt
@@ -22,8 +22,10 @@
import androidx.room.compiler.processing.XType
import androidx.room.compiler.processing.ksp.KspAnnotated.UseSiteFilter.Companion.NO_USE_SITE_OR_METHOD_PARAMETER
import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticPropertyMethodElement
+import com.google.devtools.ksp.symbol.KSDeclaration
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
import com.google.devtools.ksp.symbol.KSPropertySetter
+import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.KSValueParameter
internal class KspExecutableParameterElement(
@@ -35,33 +37,23 @@
XExecutableParameterElement,
XAnnotated by KspAnnotated.create(env, parameter, NO_USE_SITE_OR_METHOD_PARAMETER) {
- override val equalityItems: Array<out Any?>
- get() = arrayOf(enclosingElement, parameter)
-
override val name: String
get() = parameter.name?.asString() ?: "_no_param_name"
override val hasDefaultValue: Boolean
get() = parameter.hasDefault
- private val jvmTypeResolver by lazy {
- KspJvmTypeResolutionScope.MethodParameter(
+ private fun jvmTypeResolver(container: KSDeclaration?): KspJvmTypeResolutionScope {
+ return KspJvmTypeResolutionScope.MethodParameter(
kspExecutableElement = enclosingElement,
parameterIndex = parameterIndex,
- annotated = parameter.type
+ annotated = parameter.type,
+ container = container
)
}
override val type: KspType by lazy {
- parameter.typeAsMemberOf(
- functionDeclaration = enclosingElement.declaration,
- ksType = enclosingElement.containing.type?.ksType
- ).let {
- env.wrap(
- originatingReference = parameter.type,
- ksType = it
- ).withJvmTypeResolver(jvmTypeResolver)
- }
+ asMemberOf(enclosingElement.enclosingElement.type?.ksType)
}
override val closestMemberContainer: XMemberContainer by lazy {
@@ -72,19 +64,25 @@
get() = "$name in ${enclosingElement.fallbackLocationText}"
override fun asMemberOf(other: XType): KspType {
- if (enclosingElement.containing.type?.isSameType(other) != false) {
+ if (closestMemberContainer.type?.isSameType(other) != false) {
return type
}
check(other is KspType)
- return parameter.typeAsMemberOf(
- functionDeclaration = enclosingElement.declaration,
- ksType = other.ksType
- ).let {
- env.wrap(
- originatingReference = parameter.type,
- ksType = it
- ).withJvmTypeResolver(jvmTypeResolver)
- }
+ return asMemberOf(other.ksType)
+ }
+
+ private fun asMemberOf(ksType: KSType?): KspType {
+ return env.wrap(
+ originatingReference = parameter.type,
+ ksType = parameter.typeAsMemberOf(
+ functionDeclaration = enclosingElement.declaration,
+ ksType = ksType
+ )
+ ).withJvmTypeResolver(
+ jvmTypeResolver(
+ container = ksType?.declaration
+ )
+ )
}
override fun kindName(): String {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFieldElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFieldElement.kt
index dafbc4d..8f8b31d 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFieldElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFieldElement.kt
@@ -20,27 +20,21 @@
import androidx.room.compiler.processing.XFieldElement
import androidx.room.compiler.processing.XHasModifiers
import androidx.room.compiler.processing.XType
-import androidx.room.compiler.processing.XTypeElement
import androidx.room.compiler.processing.ksp.KspAnnotated.UseSiteFilter.Companion.NO_USE_SITE_OR_FIELD
import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticPropertyMethodElement
-import com.google.devtools.ksp.closestClassDeclaration
import com.google.devtools.ksp.isPrivate
import com.google.devtools.ksp.symbol.KSPropertyDeclaration
+import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.Modifier
internal class KspFieldElement(
env: KspProcessingEnv,
override val declaration: KSPropertyDeclaration,
- val containing: KspMemberContainer
) : KspElement(env, declaration),
XFieldElement,
XHasModifiers by KspHasModifiers.create(declaration),
XAnnotated by KspAnnotated.create(env, declaration, NO_USE_SITE_OR_FIELD) {
- override val equalityItems: Array<out Any?> by lazy {
- arrayOf(declaration, containing)
- }
-
override val enclosingElement: KspMemberContainer by lazy {
declaration.requireEnclosingMemberContainer(env)
}
@@ -54,29 +48,7 @@
}
override val type: KspType by lazy {
- env.wrap(
- originatingReference = declaration.type,
- ksType = declaration.typeAsMemberOf(containing.type?.ksType)
- )
- }
-
- /**
- * The original field from the declaration. For instance, if you have `val x:String` declared
- * in `BaseClass` and inherited in `SubClass`, if `this` is the instance in `SubClass`,
- * [declarationField] will be the instance in `BaseClass`. If `this` is the instance in
- * `BaseClass`, [declarationField] will be `null`.
- */
- val declarationField: KspFieldElement? by lazy {
- val declaredIn = declaration.closestClassDeclaration()
- if (declaredIn == null || declaredIn == containing.declaration) {
- null
- } else {
- KspFieldElement(
- env = env,
- declaration = declaration,
- containing = env.wrapClassDeclaration(declaredIn)
- )
- }
+ asMemberOf(enclosingElement.type?.ksType)
}
val syntheticAccessors: List<KspSyntheticPropertyMethodElement> by lazy {
@@ -123,38 +95,23 @@
}
override fun asMemberOf(other: XType): KspType {
- if (containing.type?.isSameType(other) != false) {
+ if (enclosingElement.type?.isSameType(other) != false) {
return type
}
check(other is KspType)
- val asMember = declaration.typeAsMemberOf(other.ksType)
- return env.wrap(
- originatingReference = declaration.type,
- ksType = asMember
- )
+ return asMemberOf(other.ksType)
}
- override fun copyTo(newContainer: XTypeElement): KspFieldElement {
- check(newContainer is KspTypeElement) {
- "Unexpected container (${newContainer::class}), expected KspTypeElement"
- }
- return KspFieldElement(
- env = env,
- declaration = declaration,
- containing = newContainer
+ private fun asMemberOf(ksType: KSType?): KspType {
+ return env.wrap(
+ originatingReference = declaration.type,
+ ksType = declaration.typeAsMemberOf(ksType)
)
}
companion object {
- fun create(
- env: KspProcessingEnv,
- declaration: KSPropertyDeclaration
- ): KspFieldElement {
- return KspFieldElement(
- env = env,
- declaration = declaration,
- containing = declaration.requireEnclosingMemberContainer(env)
- )
+ fun create(env: KspProcessingEnv, declaration: KSPropertyDeclaration): KspFieldElement {
+ return KspFieldElement(env, declaration)
}
}
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspJvmTypeResolver.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspJvmTypeResolver.kt
index 489f6fa..eda347f 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspJvmTypeResolver.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspJvmTypeResolver.kt
@@ -114,17 +114,14 @@
private val kspExecutableElement: KspExecutableElement,
private val parameterIndex: Int,
annotated: KSAnnotated,
- ) : KspJvmTypeResolutionScope(
- annotated = annotated,
- container = kspExecutableElement.containing.declaration
- ) {
+ container: KSDeclaration?,
+ ) : KspJvmTypeResolutionScope(annotated, container) {
override fun findDeclarationType(): XType? {
- val declarationMethodType = if (kspExecutableElement is KspMethodElement) {
- kspExecutableElement.declarationMethodType
+ return if (kspExecutableElement is KspMethodElement) {
+ kspExecutableElement.executableType.parameterTypes.getOrNull(parameterIndex)
} else {
null
}
- return declarationMethodType?.parameterTypes?.getOrNull(parameterIndex)
}
}
@@ -132,13 +129,12 @@
val declaration: KspSyntheticPropertyMethodElement
) : KspJvmTypeResolutionScope(
annotated = declaration.accessor,
- container = declaration.field.containing.declaration
+ container = declaration.field.enclosingElement.declaration
) {
override fun findDeclarationType(): XType? {
// We return the declaration from the setter, not the field because the setter parameter
// will have a different type in jvm (due to jvm wildcard resolution)
- return declaration.field.declarationField
- ?.syntheticSetter?.parameters?.firstOrNull()?.type
+ return declaration.field.syntheticSetter?.parameters?.firstOrNull()?.type
}
}
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodElement.kt
index b833951..761a615 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodElement.kt
@@ -16,7 +16,6 @@
package androidx.room.compiler.processing.ksp
-import androidx.room.compiler.processing.XEnumTypeElement
import androidx.room.compiler.processing.XExecutableParameterElement
import androidx.room.compiler.processing.XMethodElement
import androidx.room.compiler.processing.XMethodType
@@ -25,7 +24,6 @@
import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticContinuationParameterElement
import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticReceiverParameterElement
import com.google.devtools.ksp.KspExperimental
-import com.google.devtools.ksp.closestClassDeclaration
import com.google.devtools.ksp.symbol.ClassKind
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
@@ -33,14 +31,8 @@
internal sealed class KspMethodElement(
env: KspProcessingEnv,
- containing: KspMemberContainer,
declaration: KSFunctionDeclaration
-) : KspExecutableElement(
- env = env,
- containing = containing,
- declaration = declaration
-),
- XMethodElement {
+) : KspExecutableElement(env, declaration), XMethodElement {
override val name: String
get() = declaration.simpleName.asString()
@@ -55,7 +47,7 @@
}
override val parameters: List<XExecutableParameterElement> by lazy {
- buildList<XExecutableParameterElement> {
+ buildList {
val extensionReceiver = declaration.extensionReceiver
if (extensionReceiver != null) {
// Synthesize the receiver parameter to be consistent with KAPT
@@ -84,38 +76,10 @@
KspMethodType.create(
env = env,
origin = this,
- containing = this.containing.type
+ containing = this.enclosingElement.type
)
}
- /**
- * The method type for the declaration if it is inherited from a super.
- * If this method is declared in the containing class (or in a file), it will be null.
- */
- val declarationMethodType: XMethodType? by lazy {
- declaration.closestClassDeclaration()?.let { declaredIn ->
- if (declaredIn == containing.declaration) {
- executableType
- } else {
- create(
- env = env,
- containing = env.wrapClassDeclaration(declaredIn),
- declaration = declaration
- ).executableType
- }
- }
- }
-
- override val enclosingElement: KspMemberContainer
- // KSFunctionDeclarationJavaImpl.parent returns null for generated static enum functions
- // `values` and `valueOf` in Java source(https://github.com/google/ksp/issues/816).
- // To bypass this we use `containing` for these functions.
- get() = if (containing is XEnumTypeElement && (name == "values" || name == "valueOf")) {
- containing
- } else {
- super.enclosingElement
- }
-
override fun isJavaDefault(): Boolean {
return declaration.modifiers.contains(Modifier.JAVA_DEFAULT) ||
declaration.hasJvmDefaultAnnotation()
@@ -146,26 +110,14 @@
return env.resolver.overrides(this, other)
}
- override fun copyTo(newContainer: XTypeElement): KspMethodElement {
- check(newContainer is KspTypeElement)
- return create(
- env = env,
- containing = newContainer,
- declaration = declaration
- )
- }
-
private class KspNormalMethodElement(
env: KspProcessingEnv,
- containing: KspMemberContainer,
declaration: KSFunctionDeclaration
- ) : KspMethodElement(
- env, containing, declaration
- ) {
+ ) : KspMethodElement(env, declaration) {
override val returnType: XType by lazy {
declaration.returnKspType(
env = env,
- containing = containing.type
+ containing = enclosingElement.type
)
}
override fun isSuspendFunction() = false
@@ -173,11 +125,8 @@
private class KspSuspendMethodElement(
env: KspProcessingEnv,
- containing: KspMemberContainer,
declaration: KSFunctionDeclaration
- ) : KspMethodElement(
- env, containing, declaration
- ) {
+ ) : KspMethodElement(env, declaration) {
override fun isSuspendFunction() = true
override val returnType: XType by lazy {
@@ -197,13 +146,12 @@
companion object {
fun create(
env: KspProcessingEnv,
- containing: KspMemberContainer,
declaration: KSFunctionDeclaration
): KspMethodElement {
return if (declaration.modifiers.contains(Modifier.SUSPEND)) {
- KspSuspendMethodElement(env, containing, declaration)
+ KspSuspendMethodElement(env, declaration)
} else {
- KspNormalMethodElement(env, containing, declaration)
+ KspNormalMethodElement(env, declaration)
}
}
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
index 94d35a7..7366726 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
@@ -74,10 +74,6 @@
declaration.typeParameters.map { KspTypeParameterElement(env, it) }
}
- override val equalityItems: Array<out Any?> by lazy {
- arrayOf(declaration)
- }
-
override val qualifiedName: String by lazy {
(declaration.qualifiedName ?: declaration.simpleName).asString()
}
@@ -164,7 +160,6 @@
KspFieldElement(
env = env,
declaration = it,
- containing = this
)
}.let {
// only order instance properties with backing fields, we don't care about the order
@@ -185,7 +180,6 @@
KspFieldElement(
env = env,
declaration = it,
- containing = this
)
}
declaredProperties + companionProperties
@@ -256,7 +250,6 @@
return declaration.primaryConstructor?.let {
KspConstructorElement(
env = env,
- containing = this,
declaration = it
)
}
@@ -280,7 +273,6 @@
}.map {
KspMethodElement.create(
env = env,
- containing = this,
declaration = it
)
}.toList()
@@ -297,7 +289,6 @@
return declaration.getConstructors().map {
KspConstructorElement(
env = env,
- containing = this,
declaration = it
)
}.toList()
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeParameterElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeParameterElement.kt
index 8d9f24d6..fe3f77e 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeParameterElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeParameterElement.kt
@@ -53,7 +53,4 @@
override val closestMemberContainer: XMemberContainer
get() = enclosingElement.closestMemberContainer
-
- override val equalityItems: Array<out Any?>
- get() = arrayOf(declaration)
}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt
index 980fda2..9fe597a 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt
@@ -30,6 +30,8 @@
import androidx.room.compiler.processing.ksp.requireContinuationClass
import androidx.room.compiler.processing.ksp.returnTypeAsMemberOf
import androidx.room.compiler.processing.ksp.swapResolvedType
+import com.google.devtools.ksp.symbol.KSDeclaration
+import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.Variance
/**
@@ -67,37 +69,17 @@
override val hasDefaultValue: Boolean
get() = false
- private val jvmTypeResolutionScope by lazy {
- KspJvmTypeResolutionScope.MethodParameter(
+ private fun jvmTypeResolutionScope(container: KSDeclaration?): KspJvmTypeResolutionScope {
+ return KspJvmTypeResolutionScope.MethodParameter(
kspExecutableElement = enclosingElement,
parameterIndex = enclosingElement.parameters.size - 1,
- annotated = enclosingElement.declaration
+ annotated = enclosingElement.declaration,
+ container = container
)
}
- override val type: XType by lazy {
- val continuation = env.resolver.requireContinuationClass()
- val asMember = enclosingElement.declaration.returnTypeAsMemberOf(
- ksType = enclosingElement.containing.type?.ksType
- )
- val returnTypeRef = checkNotNull(enclosingElement.declaration.returnType) {
- "cannot find return type reference for $this"
- }
- val returnTypeAsTypeArgument = env.resolver.getTypeArgument(
- returnTypeRef.swapResolvedType(asMember),
- // even though this will be CONTRAVARIANT when resolved to the JVM type, in Kotlin, it
- // is still INVARIANT. (see [KSTypeVarianceResolver]
- Variance.INVARIANT
- )
- val contType = continuation.asType(
- listOf(
- returnTypeAsTypeArgument
- )
- )
- env.wrap(
- ksType = contType,
- allowPrimitives = false
- ).withJvmTypeResolver(jvmTypeResolutionScope)
+ override val type: KspType by lazy {
+ asMemberOf(enclosingElement.enclosingElement.type?.ksType)
}
override val fallbackLocationText: String
@@ -111,10 +93,17 @@
}
override fun asMemberOf(other: XType): KspType {
+ if (enclosingElement.enclosingElement.type?.isSameType(other) != false) {
+ return type
+ }
check(other is KspType)
+ return asMemberOf(other.ksType)
+ }
+
+ private fun asMemberOf(ksType: KSType?): KspType {
val continuation = env.resolver.requireContinuationClass()
val asMember = enclosingElement.declaration.returnTypeAsMemberOf(
- ksType = other.ksType
+ ksType = ksType
)
val returnTypeRef = checkNotNull(enclosingElement.declaration.returnType) {
"cannot find return type reference for $this"
@@ -130,7 +119,9 @@
ksType = contType,
allowPrimitives = false
).withJvmTypeResolver(
- jvmTypeResolutionScope
+ jvmTypeResolutionScope(
+ container = ksType?.declaration
+ )
)
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
index 3b2dbd0..af58c59 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
@@ -35,7 +35,6 @@
import androidx.room.compiler.processing.ksp.KspHasModifiers
import androidx.room.compiler.processing.ksp.KspJvmTypeResolutionScope
import androidx.room.compiler.processing.ksp.KspProcessingEnv
-import androidx.room.compiler.processing.ksp.KspTypeElement
import androidx.room.compiler.processing.ksp.KspType
import androidx.room.compiler.processing.ksp.findEnclosingMemberContainer
import androidx.room.compiler.processing.ksp.overrides
@@ -96,7 +95,7 @@
final override val executableType: XMethodType by lazy {
KspSyntheticPropertyMethodType.create(
element = this,
- container = field.containing.type
+ container = field.enclosingElement.type
)
}
@@ -136,15 +135,6 @@
return env.resolver.overrides(this, other)
}
- override fun copyTo(newContainer: XTypeElement): XMethodElement {
- check(newContainer is KspTypeElement)
- return create(
- env = env,
- field = field.copyTo(newContainer),
- accessor = accessor
- )
- }
-
private class Getter(
env: KspProcessingEnv,
field: KspFieldElement,
@@ -282,9 +272,8 @@
}
val field = KspFieldElement(
- env,
- accessor.receiver,
- enclosingType
+ env = env,
+ declaration = accessor.receiver,
)
return create(
env = env,
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticReceiverParameterElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticReceiverParameterElement.kt
index d7b4a67..1c7bf1e 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticReceiverParameterElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticReceiverParameterElement.kt
@@ -26,6 +26,8 @@
import androidx.room.compiler.processing.ksp.KspMethodElement
import androidx.room.compiler.processing.ksp.KspProcessingEnv
import androidx.room.compiler.processing.ksp.KspType
+import com.google.devtools.ksp.symbol.KSDeclaration
+import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.KSTypeReference
internal class KspSyntheticReceiverParameterElement(
@@ -52,16 +54,17 @@
override val hasDefaultValue: Boolean
get() = false
- private val jvmTypeResolutionScope by lazy {
- KspJvmTypeResolutionScope.MethodParameter(
+ private fun jvmTypeResolutionScope(container: KSDeclaration?): KspJvmTypeResolutionScope {
+ return KspJvmTypeResolutionScope.MethodParameter(
kspExecutableElement = enclosingElement,
parameterIndex = 0, // Receiver param is the 1st one
- annotated = enclosingElement.declaration
+ annotated = enclosingElement.declaration,
+ container = container
)
}
- override val type: XType by lazy {
- env.wrap(receiverType).withJvmTypeResolver(jvmTypeResolutionScope)
+ override val type: KspType by lazy {
+ asMemberOf(enclosingElement.enclosingElement.type?.ksType)
}
override val fallbackLocationText: String
@@ -75,18 +78,29 @@
}
override fun asMemberOf(other: XType): KspType {
+ if (closestMemberContainer.type?.isSameType(other) != false) {
+ return type
+ }
check(other is KspType)
+ return asMemberOf(other.ksType)
+ }
+
+ private fun asMemberOf(ksType: KSType?): KspType {
val asMemberReceiverType = receiverType.resolve().let {
- if (it.isError) {
+ if (ksType == null || it.isError) {
return@let it
}
- val asMember = enclosingElement.declaration.asMemberOf(other.ksType)
+ val asMember = enclosingElement.declaration.asMemberOf(ksType)
checkNotNull(asMember.extensionReceiverType)
}
return env.wrap(
originatingReference = receiverType,
ksType = asMemberReceiverType,
- ).withJvmTypeResolver(jvmTypeResolutionScope)
+ ).withJvmTypeResolver(
+ jvmTypeResolutionScope(
+ container = ksType?.declaration
+ )
+ )
}
override fun kindName(): String {
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/TopLevelMembersTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/TopLevelMembersTest.kt
index 4054f04..0dcb133 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/TopLevelMembersTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/TopLevelMembersTest.kt
@@ -72,7 +72,7 @@
declaration = method
)
assertWithMessage(pkg).that(
- element.containing.isTypeElement()
+ element.enclosingElement.isTypeElement()
).isFalse()
assertWithMessage(pkg).that(element.isStatic()).isTrue()
}
@@ -86,7 +86,7 @@
declaration = it
)
assertWithMessage(pkg).that(
- element.containing.isTypeElement()
+ element.enclosingElement.isTypeElement()
).isFalse()
assertWithMessage(pkg).that(element.isStatic()).isTrue()
}
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/TypeInheritanceTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/TypeInheritanceTest.kt
index 9599c6c..7843716 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/TypeInheritanceTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/TypeInheritanceTest.kt
@@ -62,7 +62,7 @@
private fun XTestInvocation.assertFieldType(fieldName: String, expectedTypeName: String) {
val sub = processingEnv.requireTypeElement("SubClass")
- val subField = sub.getField(fieldName).type.typeName.toString()
+ val subField = sub.getField(fieldName).asMemberOf(sub.type).typeName.toString()
assertThat(subField).isEqualTo(expectedTypeName)
val base = processingEnv.requireTypeElement("BaseClass")
@@ -77,19 +77,19 @@
) {
val sub = processingEnv.requireTypeElement("SubClass")
val subMethod = sub.getMethodByJvmName(methodName)
- val subParam = subMethod.getParameter(paramName)
- assertThat(subParam.type.typeName.toString()).isEqualTo(expectedTypeName)
+ val paramIndex = subMethod.parameters.indexOf(subMethod.getParameter(paramName))
+ val subMethodParam = subMethod.asMemberOf(sub.type).parameterTypes[paramIndex]
+ assertThat(subMethodParam.typeName.toString()).isEqualTo(expectedTypeName)
val base = processingEnv.requireTypeElement("BaseClass")
- val baseMethod = base.getMethodByJvmName(methodName).asMemberOf(sub.type)
- val paramIndex = subMethod.parameters.indexOf(subParam)
- assertThat(baseMethod.parameterTypes[paramIndex].typeName.toString())
- .isEqualTo(expectedTypeName)
+ val baseMethod = base.getMethodByJvmName(methodName)
+ val baseMethodParam = baseMethod.asMemberOf(sub.type).parameterTypes[paramIndex]
+ assertThat(baseMethodParam.typeName.toString()).isEqualTo(expectedTypeName)
}
private fun XTestInvocation.assertReturnType(methodName: String, expectedTypeName: String) {
val sub = processingEnv.requireTypeElement("SubClass")
- val subMethod = sub.getMethodByJvmName(methodName)
+ val subMethod = sub.getMethodByJvmName(methodName).asMemberOf(sub.type)
assertThat(subMethod.returnType.typeName.toString()).isEqualTo(expectedTypeName)
val base = processingEnv.requireTypeElement("BaseClass")
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
index a607bcc..1f1a834 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
@@ -189,21 +189,21 @@
runProcessorTest(
listOf(genericBase, boundedChild)
) {
- fun validateElement(element: XTypeElement, tTypeName: TypeName, rTypeName: TypeName) {
+ fun validateMethodElement(
+ element: XTypeElement,
+ tTypeName: TypeName,
+ rTypeName: TypeName
+ ) {
element.getMethodByJvmName("returnT").let { method ->
assertThat(method.parameters).isEmpty()
assertThat(method.returnType.typeName).isEqualTo(tTypeName)
}
element.getMethodByJvmName("receiveT").let { method ->
- method.getParameter("param1").let { param ->
- assertThat(param.type.typeName).isEqualTo(tTypeName)
- }
+ assertThat(method.getParameter("param1").type.typeName).isEqualTo(tTypeName)
assertThat(method.returnType.typeName).isEqualTo(TypeName.INT)
}
element.getMethodByJvmName("receiveR").let { method ->
- method.getParameter("param1").let { param ->
- assertThat(param.type.typeName).isEqualTo(rTypeName)
- }
+ assertThat(method.getParameter("param1").type.typeName).isEqualTo(rTypeName)
assertThat(method.returnType.typeName).isEqualTo(TypeName.INT)
}
element.getMethodByJvmName("returnR").let { method ->
@@ -211,12 +211,48 @@
assertThat(method.returnType.typeName).isEqualTo(rTypeName)
}
}
- validateElement(
+ fun validateMethodTypeAsMemberOf(
+ element: XTypeElement,
+ tTypeName: TypeName,
+ rTypeName: TypeName
+ ) {
+ element.getMethodByJvmName("returnT").asMemberOf(element.type).let { method ->
+ assertThat(method.parameterTypes).isEmpty()
+ assertThat(method.returnType.typeName).isEqualTo(tTypeName)
+ }
+ element.getMethodByJvmName("receiveT").asMemberOf(element.type).let { method ->
+ assertThat(method.parameterTypes).hasSize(1)
+ assertThat(method.parameterTypes[0].typeName).isEqualTo(tTypeName)
+ assertThat(method.returnType.typeName).isEqualTo(TypeName.INT)
+ }
+ element.getMethodByJvmName("receiveR").asMemberOf(element.type).let { method ->
+ assertThat(method.parameterTypes).hasSize(1)
+ assertThat(method.parameterTypes[0].typeName).isEqualTo(rTypeName)
+ assertThat(method.returnType.typeName).isEqualTo(TypeName.INT)
+ }
+ element.getMethodByJvmName("returnR").let { method ->
+ assertThat(method.parameters).isEmpty()
+ assertThat(method.returnType.typeName).isEqualTo(rTypeName)
+ }
+ }
+
+ validateMethodElement(
element = it.processingEnv.requireTypeElement("foo.bar.Base"),
tTypeName = TypeVariableName.get("T"),
rTypeName = TypeVariableName.get("R")
)
- validateElement(
+ validateMethodElement(
+ element = it.processingEnv.requireTypeElement("foo.bar.Child"),
+ tTypeName = TypeVariableName.get("T"),
+ rTypeName = TypeVariableName.get("R")
+ )
+
+ validateMethodTypeAsMemberOf(
+ element = it.processingEnv.requireTypeElement("foo.bar.Base"),
+ tTypeName = TypeVariableName.get("T"),
+ rTypeName = TypeVariableName.get("R")
+ )
+ validateMethodTypeAsMemberOf(
element = it.processingEnv.requireTypeElement("foo.bar.Child"),
tTypeName = String::class.className(),
rTypeName = TypeVariableName.get("R")
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
index fda9147..9233b1a 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
@@ -733,16 +733,6 @@
}
@Test
- fun genericToPrimitiveOverrides_methodElement() {
- genericToPrimitiveOverrides(asMemberOf = false)
- }
-
- @Test
- fun genericToPrimitiveOverrides_asMemberOf() {
- genericToPrimitiveOverrides(asMemberOf = true)
- }
-
- @Test
fun defaultMethodParameters() {
fun buildSource(pkg: String) = Source.kotlin(
"Foo.kt",
@@ -1047,7 +1037,8 @@
}
// see b/160258066
- private fun genericToPrimitiveOverrides(asMemberOf: Boolean) {
+ @Test
+ public fun genericToPrimitiveOverrides() {
val source = Source.kotlin(
"Foo.kt",
"""
@@ -1090,21 +1081,14 @@
buildString {
append(methodElement.jvmName)
append("(")
- val paramTypes = if (asMemberOf) {
- methodElement.asMemberOf([email protected]).parameterTypes
- } else {
- methodElement.parameters.map { it.type }
- }
+ val enclosingType = [email protected]
+ val paramTypes = methodElement.asMemberOf(enclosingType).parameterTypes
val paramsSignature = paramTypes.joinToString(",") {
it.typeName.toString()
}
append(paramsSignature)
append("):")
- val returnType = if (asMemberOf) {
- methodElement.asMemberOf([email protected]).returnType
- } else {
- methodElement.returnType
- }
+ val returnType = methodElement.asMemberOf(enclosingType).returnType
append(returnType.typeName)
}
}
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableTypeTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableTypeTest.kt
index 93eff55..324eeb6 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableTypeTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableTypeTest.kt
@@ -102,7 +102,7 @@
assertThat(subjects).isNotEmpty()
subjects.forEach {
callback(myInterface.getMethodByJvmName(methodName).asMemberOf(it.type))
- callback(it.getMethodByJvmName(methodName).executableType)
+ callback(it.getMethodByJvmName(methodName).asMemberOf(it.type))
}
}
@@ -169,7 +169,7 @@
assertThat(subjects).isNotEmpty()
subjects.forEach {
callback(myInterface.getMethodByJvmName(methodName).asMemberOf(it.type))
- callback(it.getMethodByJvmName(methodName).executableType)
+ callback(it.getMethodByJvmName(methodName).asMemberOf(it.type))
}
}
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
index 0c1d116..c1134d4 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
@@ -395,7 +395,7 @@
subClass.getField("genericProp").let { field ->
// this is tricky because even though it is non-null it, it should still be boxed
- assertThat(field.type.typeName).isEqualTo(TypeName.INT.box())
+ assertThat(field.asMemberOf(subClass.type).typeName).isEqualTo(TypeName.INT.box())
}
}
}
@@ -1561,6 +1561,239 @@
}
}
+ @Test
+ fun inheritedGenericMethod() {
+ runProcessorTest(
+ sources = listOf(
+ Source.kotlin(
+ "test.ConcreteClass.kt",
+ """
+ package test;
+ class ConcreteClass: AbstractClass<Foo, Bar>() {}
+ abstract class AbstractClass<T1, T2> {
+ fun method(t1: T1, t2: T2): T2 {
+ return t2
+ }
+ }
+ class Foo
+ class Bar
+ """.trimIndent()
+ )
+ )
+ ) { invocation ->
+ val concreteClass = invocation.processingEnv.requireTypeElement("test.ConcreteClass")
+ val abstractClass = invocation.processingEnv.requireTypeElement("test.AbstractClass")
+ val concreteClassMethod = concreteClass.getMethodByJvmName("method")
+ val abstractClassMethod = abstractClass.getMethodByJvmName("method")
+ val fooType = invocation.processingEnv.requireType("test.Foo")
+ val barType = invocation.processingEnv.requireType("test.Bar")
+
+ fun checkMethodElement(method: XMethodElement) {
+ checkMethodElement(
+ method = method,
+ name = "method",
+ enclosingElement = abstractClass,
+ returnType = TypeVariableName.get("T2"),
+ parameterTypes = arrayOf(TypeVariableName.get("T1"), TypeVariableName.get("T2"))
+ )
+ checkMethodType(
+ methodType = method.executableType,
+ returnType = TypeVariableName.get("T2"),
+ parameterTypes = arrayOf(TypeVariableName.get("T1"), TypeVariableName.get("T2"))
+ )
+ checkMethodType(
+ methodType = method.asMemberOf(abstractClass.type),
+ returnType = TypeVariableName.get("T2"),
+ parameterTypes = arrayOf(TypeVariableName.get("T1"), TypeVariableName.get("T2"))
+ )
+ checkMethodType(
+ methodType = method.asMemberOf(concreteClass.type),
+ returnType = barType.typeName,
+ parameterTypes = arrayOf(fooType.typeName, barType.typeName)
+ )
+ }
+
+ assertThat(concreteClassMethod).isEqualTo(abstractClassMethod)
+ checkMethodElement(concreteClassMethod)
+ checkMethodElement(abstractClassMethod)
+ }
+ }
+
+ @Test
+ fun overriddenGenericMethod() {
+ runProcessorTest(
+ sources = listOf(
+ Source.kotlin(
+ "test.ConcreteClass.kt",
+ """
+ package test;
+ class ConcreteClass: AbstractClass<Foo, Bar>() {
+ override fun method(t1: Foo, t2: Bar): Bar {
+ return t2
+ }
+ }
+ abstract class AbstractClass<T1, T2> {
+ abstract fun method(t1: T1, t2: T2): T2
+ }
+ class Foo
+ class Bar
+ """.trimIndent()
+ )
+ )
+ ) { invocation ->
+ val concreteClass = invocation.processingEnv.requireTypeElement("test.ConcreteClass")
+ val abstractClass = invocation.processingEnv.requireTypeElement("test.AbstractClass")
+ val concreteClassMethod = concreteClass.getMethodByJvmName("method")
+ val abstractClassMethod = abstractClass.getMethodByJvmName("method")
+ val fooType = invocation.processingEnv.requireType("test.Foo")
+ val barType = invocation.processingEnv.requireType("test.Bar")
+
+ assertThat(concreteClassMethod).isNotEqualTo(abstractClassMethod)
+ assertThat(concreteClassMethod.overrides(abstractClassMethod, concreteClass)).isTrue()
+
+ // Check the abstract method and method type
+ checkMethodElement(
+ method = abstractClassMethod,
+ name = "method",
+ enclosingElement = abstractClass,
+ returnType = TypeVariableName.get("T2"),
+ parameterTypes = arrayOf(TypeVariableName.get("T1"), TypeVariableName.get("T2"))
+ )
+ checkMethodType(
+ methodType = abstractClassMethod.executableType,
+ returnType = TypeVariableName.get("T2"),
+ parameterTypes = arrayOf(TypeVariableName.get("T1"), TypeVariableName.get("T2"))
+ )
+ checkMethodType(
+ methodType = abstractClassMethod.asMemberOf(abstractClass.type),
+ returnType = TypeVariableName.get("T2"),
+ parameterTypes = arrayOf(TypeVariableName.get("T1"), TypeVariableName.get("T2"))
+ )
+ checkMethodType(
+ methodType = abstractClassMethod.asMemberOf(concreteClass.type),
+ returnType = barType.typeName,
+ parameterTypes = arrayOf(fooType.typeName, barType.typeName)
+ )
+
+ // Check the concrete method and method type
+ checkMethodElement(
+ method = concreteClassMethod,
+ name = "method",
+ enclosingElement = concreteClass,
+ returnType = barType.typeName,
+ parameterTypes = arrayOf(fooType.typeName, barType.typeName)
+ )
+ checkMethodType(
+ methodType = concreteClassMethod.executableType,
+ returnType = barType.typeName,
+ parameterTypes = arrayOf(fooType.typeName, barType.typeName)
+ )
+ checkMethodType(
+ methodType = concreteClassMethod.asMemberOf(concreteClass.type),
+ returnType = barType.typeName,
+ parameterTypes = arrayOf(fooType.typeName, barType.typeName)
+ )
+ }
+ }
+
+ private fun checkMethodElement(
+ method: XMethodElement,
+ name: String,
+ enclosingElement: XTypeElement,
+ returnType: TypeName,
+ parameterTypes: Array<TypeName>
+ ) {
+ assertThat(method.name).isEqualTo(name)
+ assertThat(method.enclosingElement).isEqualTo(enclosingElement)
+ assertThat(method.returnType.typeName).isEqualTo(returnType)
+ assertThat(method.parameters.map { it.type.typeName })
+ .containsExactly(*parameterTypes)
+ .inOrder()
+ }
+ private fun checkMethodType(
+ methodType: XMethodType,
+ returnType: TypeName,
+ parameterTypes: Array<TypeName>
+ ) {
+ assertThat(methodType.returnType.typeName).isEqualTo(returnType)
+ assertThat(methodType.parameterTypes.map { it.typeName })
+ .containsExactly(*parameterTypes)
+ .inOrder()
+ }
+
+ @Test
+ fun overriddenGenericConstructor() {
+ runProcessorTest(
+ sources = listOf(
+ Source.kotlin(
+ "test.ConcreteClass.kt",
+ """
+ package test;
+ class ConcreteClass(foo: Foo): AbstractClass<Foo>(foo) {}
+ abstract class AbstractClass<T>(t: T)
+ class Foo
+ """.trimIndent()
+ )
+ )
+ ) { invocation ->
+ val concreteClass = invocation.processingEnv.requireTypeElement("test.ConcreteClass")
+ val abstractClass = invocation.processingEnv.requireTypeElement("test.AbstractClass")
+ val fooType = invocation.processingEnv.requireType("test.Foo")
+
+ fun checkConstructorParameters(
+ typeElement: XTypeElement,
+ expectedParameters: Array<TypeName>
+ ) {
+ assertThat(typeElement.getConstructors()).hasSize(1)
+ val constructor = typeElement.getConstructors()[0]
+ assertThat(constructor.parameters.map { it.type.typeName })
+ .containsExactly(*expectedParameters)
+ .inOrder()
+ }
+
+ checkConstructorParameters(abstractClass, arrayOf(TypeVariableName.get("T")))
+ checkConstructorParameters(concreteClass, arrayOf(fooType.typeName))
+ }
+ }
+
+ @Test
+ fun inheritedGenericField() {
+ runProcessorTest(
+ sources = listOf(
+ Source.kotlin(
+ "test.ConcreteClass.kt",
+ """
+ package test;
+ class ConcreteClass: AbstractClass<Foo>()
+ abstract class AbstractClass<T> {
+ val field: T = TODO()
+ }
+ class Foo
+ """.trimIndent()
+ )
+ )
+ ) { invocation ->
+ val concreteClass = invocation.processingEnv.requireTypeElement("test.ConcreteClass")
+ val abstractClass = invocation.processingEnv.requireTypeElement("test.AbstractClass")
+ val concreteClassField = concreteClass.getField("field")
+ val abstractClassField = abstractClass.getField("field")
+ val fooType = invocation.processingEnv.requireType("test.Foo")
+
+ fun checkFieldElement(field: XFieldElement) {
+ assertThat(field.name).isEqualTo("field")
+ assertThat(field.type.typeName).isEqualTo(TypeVariableName.get("T"))
+ assertThat(field.asMemberOf(abstractClass.type).typeName)
+ .isEqualTo(TypeVariableName.get("T"))
+ assertThat(field.asMemberOf(concreteClass.type).typeName)
+ .isEqualTo(fooType.typeName)
+ }
+
+ assertThat(concreteClassField).isEqualTo(abstractClassField)
+ checkFieldElement(abstractClassField)
+ checkFieldElement(concreteClassField)
+ }
+ }
+
/**
* it is good to exclude methods coming from Object when testing as they differ between KSP
* and KAPT but irrelevant for Room.
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
index 5115be6..f14cebf 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
@@ -92,9 +92,10 @@
}
.forEach { method ->
val testKey = method.createNewUniqueKey(klass.qualifiedName)
+ val methodType = method.asMemberOf(klass.type)
val types = listOf(
- method.returnType
- ) + method.parameters.map { it.type }
+ methodType.returnType
+ ) + methodType.parameterTypes
output[testKey] = types.map {
it.typeName
}
diff --git a/room/room-compiler/build.gradle b/room/room-compiler/build.gradle
index 9a4310c..13d97c6 100644
--- a/room/room-compiler/build.gradle
+++ b/room/room-compiler/build.gradle
@@ -260,19 +260,23 @@
}
def checkArtifactContentsTask = tasks.register("checkArtifactTask", CheckArtifactTask) {
- it.artifactInputs.from {
- ((PublishToMavenRepository) project.tasks
- .named("publishMavenPublicationToMavenRepository").get()).getPublication()
- .artifacts.matching {
- it.classifier == null
- }.collect {
- it.file
+ def pomTask = (GenerateMavenPom) project.tasks.named("generatePomFileForMavenPublication").get()
+ it.pomFile = pomTask.destination
+}
+
+afterEvaluate {
+ def publishTaskProvider = project.tasks.named("publishMavenPublicationToMavenRepository")
+ checkArtifactContentsTask.configure {
+ it.artifactInputs.from {
+ publishTaskProvider.map {
+ ((PublishToMavenRepository) it).getPublication().artifacts.matching {
+ it.classifier == null
+ }.collect {
+ it.file
+ }
+ }
}
}
- def pomTask = (GenerateMavenPom) project.tasks
- .named("generatePomFileForMavenPublication").get()
- it.pomFile = pomTask.destination
- it.dependsOn("publishMavenPublicationToMavenRepository")
}
// make sure we validate published artifacts on the build server.
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/DaoProcessor.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/DaoProcessor.kt
index bd01c36..104295d 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/DaoProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/DaoProcessor.kt
@@ -268,14 +268,24 @@
if (it.jvmName != unannotated.jvmName) {
return@firstOrNull false
}
- if (!it.returnType.boxed().isSameType(unannotated.returnType.boxed())) {
- return@firstOrNull false
- }
if (it.parameters.size != unannotated.parameters.size) {
return@firstOrNull false
}
+
+ // Get unannotated as a member of annotated's enclosing type before comparing
+ // in case unannotated contains type parameters that need to be resolved.
+ val annotatedEnclosingType = it.enclosingElement.type
+ val unannotatedType = if (annotatedEnclosingType == null) {
+ unannotated.executableType
+ } else {
+ unannotated.asMemberOf(annotatedEnclosingType)
+ }
+
+ if (!it.returnType.boxed().isSameType(unannotatedType.returnType.boxed())) {
+ return@firstOrNull false
+ }
for (i in it.parameters.indices) {
- if (it.parameters[i].type.boxed() != unannotated.parameters[i].type.boxed()) {
+ if (it.parameters[i].type.boxed() != unannotatedType.parameterTypes[i].boxed()) {
return@firstOrNull false
}
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
index e43619f..c500f7e 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
@@ -36,8 +36,10 @@
import androidx.room.solver.query.result.QueryResultBinder
import androidx.room.solver.shortcut.binder.CallableDeleteOrUpdateMethodBinder.Companion.createDeleteOrUpdateBinder
import androidx.room.solver.shortcut.binder.CallableInsertMethodBinder.Companion.createInsertBinder
+import androidx.room.solver.shortcut.binder.CallableUpsertMethodBinder.Companion.createUpsertBinder
import androidx.room.solver.shortcut.binder.DeleteOrUpdateMethodBinder
import androidx.room.solver.shortcut.binder.InsertMethodBinder
+import androidx.room.solver.shortcut.binder.UpsertMethodBinder
import androidx.room.solver.transaction.binder.CoroutineTransactionMethodBinder
import androidx.room.solver.transaction.binder.InstantTransactionMethodBinder
import androidx.room.solver.transaction.binder.TransactionMethodBinder
@@ -91,6 +93,11 @@
abstract fun findDeleteOrUpdateMethodBinder(returnType: XType): DeleteOrUpdateMethodBinder
+ abstract fun findUpsertMethodBinder(
+ returnType: XType,
+ params: List<ShortcutQueryParameter>
+ ): UpsertMethodBinder
+
abstract fun findTransactionMethodBinder(
callType: TransactionMethod.CallType
): TransactionMethodBinder
@@ -176,6 +183,11 @@
override fun findDeleteOrUpdateMethodBinder(returnType: XType) =
context.typeAdapterStore.findDeleteOrUpdateMethodBinder(returnType)
+ override fun findUpsertMethodBinder(
+ returnType: XType,
+ params: List<ShortcutQueryParameter>
+ ) = context.typeAdapterStore.findUpsertMethodBinder(returnType, params)
+
override fun findTransactionMethodBinder(callType: TransactionMethod.CallType) =
InstantTransactionMethodBinder(
TransactionMethodAdapter(executableElement.jvmName, callType)
@@ -255,6 +267,23 @@
)
}
+ override fun findUpsertMethodBinder(
+ returnType: XType,
+ params: List<ShortcutQueryParameter>
+ ) = createUpsertBinder(
+ typeArg = returnType,
+ adapter = context.typeAdapterStore.findUpsertAdapter(returnType, params)
+ ) { callableImpl, dbField ->
+ addStatement(
+ "return $T.execute($N, $L, $L, $N)",
+ RoomCoroutinesTypeNames.COROUTINES_ROOM,
+ dbField,
+ "true", // inTransaction
+ callableImpl,
+ continuationParam.name
+ )
+ }
+
override fun findDeleteOrUpdateMethodBinder(returnType: XType) =
createDeleteOrUpdateBinder(
typeArg = returnType,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
index 2f1aff8..879a7b3 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
@@ -208,6 +208,11 @@
params: List<ShortcutQueryParameter>
) = delegate.findInsertMethodBinder(returnType, params)
+ fun findUpsertMethodBinder(
+ returnType: XType,
+ params: List<ShortcutQueryParameter>
+ ) = delegate.findUpsertMethodBinder(returnType, params)
+
fun findDeleteOrUpdateMethodBinder(returnType: XType) =
delegate.findDeleteOrUpdateMethodBinder(returnType)
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
index 1e1ac0d..cc06a34 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
@@ -81,16 +81,22 @@
import androidx.room.solver.query.result.SingleNamedColumnRowAdapter
import androidx.room.solver.shortcut.binder.DeleteOrUpdateMethodBinder
import androidx.room.solver.shortcut.binder.InsertMethodBinder
+import androidx.room.solver.shortcut.binder.UpsertMethodBinder
import androidx.room.solver.shortcut.binderprovider.DeleteOrUpdateMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureDeleteOrUpdateMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureInsertMethodBinderProvider
+import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureUpsertMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.InsertMethodBinderProvider
+import androidx.room.solver.shortcut.binderprovider.UpsertMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.InstantDeleteOrUpdateMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.InstantInsertMethodBinderProvider
+import androidx.room.solver.shortcut.binderprovider.InstantUpsertMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.RxCallableDeleteOrUpdateMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.RxCallableInsertMethodBinderProvider
+import androidx.room.solver.shortcut.binderprovider.RxCallableUpsertMethodBinderProvider
import androidx.room.solver.shortcut.result.DeleteOrUpdateMethodAdapter
import androidx.room.solver.shortcut.result.InsertMethodAdapter
+import androidx.room.solver.shortcut.result.UpsertMethodAdapter
import androidx.room.solver.types.BoxedBooleanToBoxedIntConverter
import androidx.room.solver.types.BoxedPrimitiveColumnTypeAdapter
import androidx.room.solver.types.ByteArrayColumnTypeAdapter
@@ -233,6 +239,13 @@
add(InstantDeleteOrUpdateMethodBinderProvider(context))
}
+ val upsertBinderProviders: List<UpsertMethodBinderProvider> =
+ mutableListOf<UpsertMethodBinderProvider>().apply {
+ addAll(RxCallableUpsertMethodBinderProvider.getAll(context))
+ add(GuavaListenableFutureUpsertMethodBinderProvider(context))
+ add(InstantUpsertMethodBinderProvider(context))
+ }
+
/**
* Searches 1 way to bind a value into a statement.
*/
@@ -391,6 +404,15 @@
}.provide(typeMirror, params)
}
+ fun findUpsertMethodBinder(
+ typeMirror: XType,
+ params: List<ShortcutQueryParameter>
+ ): UpsertMethodBinder {
+ return upsertBinderProviders.first {
+ it.matches(typeMirror)
+ }.provide(typeMirror, params)
+ }
+
fun findQueryResultBinder(
typeMirror: XType,
query: ParsedQuery,
@@ -432,6 +454,15 @@
return InsertMethodAdapter.create(typeMirror, params)
}
+ @Suppress("UNUSED_PARAMETER") // param will be used in a future change
+ fun findUpsertAdapter(
+ typeMirror: XType,
+ params: List<ShortcutQueryParameter>
+ ): UpsertMethodAdapter? {
+ // TODO: change for UpsertMethodAdapter when bind has been created
+ return null
+ }
+
fun findQueryResultAdapter(
typeMirror: XType,
query: ParsedQuery,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/CallableUpsertMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/CallableUpsertMethodBinder.kt
new file mode 100644
index 0000000..37aa163
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/CallableUpsertMethodBinder.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.solver.shortcut.binder
+
+import androidx.room.ext.CallableTypeSpecBuilder
+import androidx.room.compiler.processing.XType
+import androidx.room.solver.CodeGenScope
+import androidx.room.solver.shortcut.result.UpsertMethodAdapter
+import androidx.room.vo.ShortcutQueryParameter
+import com.squareup.javapoet.CodeBlock
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.TypeSpec
+
+/**
+ * Binder for deferred upsert methods.
+ *
+ * This binder will create a Callable implementation that delegates to the
+ * [UpsertMethodAdapter]. Usage of the Callable impl is then delegate to the [addStmntBlock]
+ * function.
+ */
+class CallableUpsertMethodBinder(
+ val typeArg: XType,
+ val addStmntBlock: CodeBlock.Builder.(callableImpl: TypeSpec, dbField: FieldSpec) -> Unit,
+ adapter: UpsertMethodAdapter?
+) : UpsertMethodBinder(adapter) {
+
+ companion object {
+ fun createUpsertBinder(
+ typeArg: XType,
+ adapter: UpsertMethodAdapter?,
+ addCodeBlock: CodeBlock.Builder.(callableImpl: TypeSpec, dbField: FieldSpec) -> Unit
+ ) = CallableUpsertMethodBinder(typeArg, addCodeBlock, adapter)
+ }
+
+ override fun convertAndReturn(
+ parameters: List<ShortcutQueryParameter>,
+ upsertionAdapters: Map<String, Pair<FieldSpec, TypeSpec>>,
+ dbField: FieldSpec,
+ scope: CodeGenScope
+ ) {
+ val adapterScope = scope.fork()
+ val callableImpl = CallableTypeSpecBuilder(typeArg.typeName) {
+ // TODO add the createMethodBody in UpsertMethodAdapter
+ addCode(adapterScope.generate())
+ }.build()
+
+ scope.builder().apply {
+ addStmntBlock(callableImpl, dbField)
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantUpsertMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantUpsertMethodBinder.kt
new file mode 100644
index 0000000..06860f7
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantUpsertMethodBinder.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.solver.shortcut.binder
+
+import androidx.room.ext.N
+import androidx.room.solver.CodeGenScope
+import androidx.room.solver.shortcut.result.UpsertMethodAdapter
+import androidx.room.vo.ShortcutQueryParameter
+import androidx.room.writer.DaoWriter
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.TypeSpec
+
+/**
+ * Binder that knows how to write instant (blocking) upsert methods.
+ */
+class InstantUpsertMethodBinder(adapter: UpsertMethodAdapter?) : UpsertMethodBinder(adapter) {
+
+ override fun convertAndReturn(
+ parameters: List<ShortcutQueryParameter>,
+ upsertionAdapters: Map<String, Pair<FieldSpec, TypeSpec>>,
+ dbField: FieldSpec,
+ scope: CodeGenScope
+ ) {
+ scope.builder().apply {
+ addStatement("$N.assertNotSuspendingTransaction()", DaoWriter.dbField)
+ }
+ // TODO: createUpsertionMethodBody
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/UpsertMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/UpsertMethodBinder.kt
index ff8c34f..9ec0168 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/UpsertMethodBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/UpsertMethodBinder.kt
@@ -21,8 +21,33 @@
import androidx.room.vo.ShortcutQueryParameter
import com.squareup.javapoet.FieldSpec
import com.squareup.javapoet.TypeSpec
-
+/**
+ * Connects the upsert method, the database and the [UpsertMethodAdapter].
+ */
abstract class UpsertMethodBinder(val adapter: UpsertMethodAdapter?) {
+
+ /**
+ * Received the upsert method parameters, the upsertion adapters and generations the code that
+ * runs the upsert and returns the result.
+ *
+ * For example, for the DAO method:
+ * ```
+ * @Upsert
+ * fun addPublishers(vararg publishers: Publisher): List<Long>
+ * ```
+ * The following code will be generated:
+ *
+ * ```
+ * __db.beginTransaction();
+ * try {
+ * List<Long> _result = __upsertionAdapterOfPublisher.upsertAndReturnIdsList(publishers);
+ * __db.setTransactionSuccessful();
+ * return _result;
+ * } finally {
+ * __db.endTransaction();
+ * }
+ * ```
+ */
abstract fun convertAndReturn(
parameters: List<ShortcutQueryParameter>,
upsertionAdapters: Map<String, Pair<FieldSpec, TypeSpec>>,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureUpsertMethodBinderProvider.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureUpsertMethodBinderProvider.kt
new file mode 100644
index 0000000..8f8cee0
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureUpsertMethodBinderProvider.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.solver.shortcut.binderprovider
+
+import androidx.room.compiler.processing.XType
+import androidx.room.ext.GuavaUtilConcurrentTypeNames
+import androidx.room.ext.L
+import androidx.room.ext.N
+import androidx.room.ext.RoomGuavaTypeNames
+import androidx.room.ext.T
+import androidx.room.processor.Context
+import androidx.room.processor.ProcessorErrors
+import androidx.room.solver.shortcut.binder.CallableUpsertMethodBinder.Companion.createUpsertBinder
+import androidx.room.solver.shortcut.binder.UpsertMethodBinder
+import androidx.room.vo.ShortcutQueryParameter
+
+/**
+ * Provider for Guava ListenableFuture binders.
+ */
+class GuavaListenableFutureUpsertMethodBinderProvider(
+ private val context: Context
+) : UpsertMethodBinderProvider {
+
+ private val hasGuavaRoom by lazy {
+ context.processingEnv.findTypeElement(RoomGuavaTypeNames.GUAVA_ROOM) != null
+ }
+
+ override fun matches(declared: XType): Boolean =
+ declared.typeArguments.size == 1 &&
+ declared.rawType.typeName == GuavaUtilConcurrentTypeNames.LISTENABLE_FUTURE
+
+ override fun provide(
+ declared: XType,
+ params: List<ShortcutQueryParameter>
+ ): UpsertMethodBinder {
+ if (!hasGuavaRoom) {
+ context.logger.e(ProcessorErrors.MISSING_ROOM_GUAVA_ARTIFACT)
+ }
+
+ val typeArg = declared.typeArguments.first()
+ val adapter = context.typeAdapterStore.findUpsertAdapter(typeArg, params)
+ return createUpsertBinder(typeArg, adapter) { callableImpl, dbField ->
+ addStatement(
+ "return $T.createListenableFuture($N, $L, $L)",
+ RoomGuavaTypeNames.GUAVA_ROOM,
+ dbField,
+ "true", // inTransaction
+ callableImpl
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/InstantUpsertMethodBinderProvider.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/InstantUpsertMethodBinderProvider.kt
new file mode 100644
index 0000000..4a3ef91
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/InstantUpsertMethodBinderProvider.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.solver.shortcut.binderprovider
+
+import androidx.room.compiler.processing.XType
+import androidx.room.processor.Context
+import androidx.room.solver.shortcut.binder.UpsertMethodBinder
+import androidx.room.solver.shortcut.binder.InstantUpsertMethodBinder
+import androidx.room.vo.ShortcutQueryParameter
+
+/**
+ * Provider for instant (blocking) upsert method binder.
+ */
+class InstantUpsertMethodBinderProvider(private val context: Context) : UpsertMethodBinderProvider {
+
+ override fun matches(declared: XType) = true
+
+ override fun provide(
+ declared: XType,
+ params: List<ShortcutQueryParameter>
+ ): UpsertMethodBinder {
+ return InstantUpsertMethodBinder(
+ context.typeAdapterStore.findUpsertAdapter(declared, params)
+ )
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/RxCallableUpsertMethodBinderProvider.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/RxCallableUpsertMethodBinderProvider.kt
new file mode 100644
index 0000000..df1911e
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/RxCallableUpsertMethodBinderProvider.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.solver.shortcut.binderprovider
+
+import androidx.room.compiler.processing.XRawType
+import androidx.room.compiler.processing.XType
+import androidx.room.ext.L
+import androidx.room.ext.T
+import androidx.room.processor.Context
+import androidx.room.solver.RxType
+import androidx.room.solver.shortcut.binder.CallableUpsertMethodBinder
+import androidx.room.solver.shortcut.binder.UpsertMethodBinder
+import androidx.room.vo.ShortcutQueryParameter
+
+/**
+ * Provider for Rx Callable binders.
+ */
+open class RxCallableUpsertMethodBinderProvider internal constructor(
+ val context: Context,
+ private val rxType: RxType
+) : UpsertMethodBinderProvider {
+
+ /**
+ * [Single] and [Maybe] are generics but [Completable] is not so each implementation of this
+ * class needs to define how to extract the type argument.
+ */
+ open fun extractTypeArg(declared: XType): XType = declared.typeArguments.first()
+
+ override fun matches(declared: XType): Boolean =
+ declared.typeArguments.size == 1 && matchesRxType(declared)
+
+ private fun matchesRxType(declared: XType): Boolean {
+ return declared.rawType.typeName == rxType.className
+ }
+
+ override fun provide(
+ declared: XType,
+ params: List<ShortcutQueryParameter>
+ ): UpsertMethodBinder {
+ val typeArg = extractTypeArg(declared)
+ val adapter = context.typeAdapterStore.findUpsertAdapter(typeArg, params)
+ return CallableUpsertMethodBinder.createUpsertBinder(typeArg, adapter) { callableImpl, _ ->
+ addStatement("return $T.fromCallable($L)", rxType.className, callableImpl)
+ }
+ }
+
+ companion object {
+ fun getAll(context: Context) = listOf(
+ RxCallableUpsertMethodBinderProvider(context, RxType.RX2_SINGLE),
+ RxCallableUpsertMethodBinderProvider(context, RxType.RX2_MAYBE),
+ RxCompletableUpsertMethodBinderProvider(context, RxType.RX2_COMPLETABLE),
+ RxCallableUpsertMethodBinderProvider(context, RxType.RX3_SINGLE),
+ RxCallableUpsertMethodBinderProvider(context, RxType.RX3_MAYBE),
+ RxCompletableUpsertMethodBinderProvider(context, RxType.RX3_COMPLETABLE)
+ )
+ }
+}
+
+private class RxCompletableUpsertMethodBinderProvider(
+ context: Context,
+ rxType: RxType
+) : RxCallableUpsertMethodBinderProvider(context, rxType) {
+
+ private val completableType: XRawType? by lazy {
+ context.processingEnv.findType(rxType.className)?.rawType
+ }
+
+ /**
+ * Since Completable is not a generic, the supported return type should be Void.
+ * Like this, the generated Callable.call method will return Void.
+ */
+ override fun extractTypeArg(declared: XType): XType =
+ context.COMMON_TYPES.VOID
+
+ override fun matches(declared: XType): Boolean = isCompletable(declared)
+
+ private fun isCompletable(declared: XType): Boolean {
+ if (completableType == null) {
+ return false
+ }
+ return declared.rawType.isAssignableFrom(completableType!!)
+ }
+}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/UpsertMethodBinderProvider.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/UpsertMethodBinderProvider.kt
new file mode 100644
index 0000000..0858f92
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/UpsertMethodBinderProvider.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.solver.shortcut.binderprovider
+
+import androidx.room.compiler.processing.XType
+import androidx.room.solver.shortcut.binder.UpsertMethodBinder
+import androidx.room.vo.ShortcutQueryParameter
+
+/**
+ * Provider for upsert method binders.
+ */
+interface UpsertMethodBinderProvider {
+
+ /**
+ * Check whether the [XType] can be handled by the [UpsertMethodBinder]
+ */
+ fun matches(declared: XType): Boolean
+
+ /**
+ * Provider of [UpsertMethodBinder], based on the [XType] and the list of parameters
+ */
+ fun provide(declared: XType, params: List<ShortcutQueryParameter>): UpsertMethodBinder
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
index 993cdd2..2fa5dbb 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
@@ -56,8 +56,10 @@
import androidx.room.solver.query.result.MultiTypedPagingSourceQueryResultBinder
import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureDeleteOrUpdateMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureInsertMethodBinderProvider
+import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureUpsertMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.RxCallableDeleteOrUpdateMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.RxCallableInsertMethodBinderProvider
+import androidx.room.solver.shortcut.binderprovider.RxCallableUpsertMethodBinderProvider
import androidx.room.solver.types.BoxedPrimitiveColumnTypeAdapter
import androidx.room.solver.types.CompositeAdapter
import androidx.room.solver.types.CustomTypeConverterWrapper
@@ -793,6 +795,69 @@
}
@Test
+ fun testFindUpsertSingle() {
+ listOf(
+ Triple(COMMON.RX2_SINGLE, COMMON.RX2_ROOM, RxJava2TypeNames.SINGLE),
+ Triple(COMMON.RX3_SINGLE, COMMON.RX3_ROOM, RxJava3TypeNames.SINGLE)
+ ).forEach { (rxTypeSrc, _, rxTypeClassName) ->
+ @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
+ runProcessorTest(sources = listOf(rxTypeSrc)) { invocation ->
+ val single = invocation.processingEnv.requireTypeElement(rxTypeClassName)
+ assertThat(single).isNotNull()
+ assertThat(
+ RxCallableUpsertMethodBinderProvider.getAll(invocation.context).any {
+ it.matches(single.type)
+ }).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun testFindUpsertMaybe() {
+ listOf(
+ Triple(COMMON.RX2_MAYBE, COMMON.RX2_ROOM, RxJava2TypeNames.MAYBE),
+ Triple(COMMON.RX3_MAYBE, COMMON.RX3_ROOM, RxJava3TypeNames.MAYBE)
+ ).forEach { (rxTypeSrc, _, rxTypeClassName) ->
+ runProcessorTest(sources = listOf(rxTypeSrc)) { invocation ->
+ val maybe = invocation.processingEnv.requireTypeElement(rxTypeClassName)
+ assertThat(
+ RxCallableUpsertMethodBinderProvider.getAll(invocation.context).any {
+ it.matches(maybe.type)
+ }).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun testFindUpsertCompletable() {
+ listOf(
+ Triple(COMMON.RX2_COMPLETABLE, COMMON.RX2_ROOM, RxJava2TypeNames.COMPLETABLE),
+ Triple(COMMON.RX3_COMPLETABLE, COMMON.RX3_ROOM, RxJava3TypeNames.COMPLETABLE)
+ ).forEach { (rxTypeSrc, _, rxTypeClassName) ->
+ runProcessorTest(sources = listOf(rxTypeSrc)) { invocation ->
+ val completable = invocation.processingEnv.requireTypeElement(rxTypeClassName)
+ assertThat(
+ RxCallableUpsertMethodBinderProvider.getAll(invocation.context).any {
+ it.matches(completable.type)
+ }).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun testFindUpsertListenableFuture() {
+ runProcessorTest(sources = listOf(COMMON.LISTENABLE_FUTURE)) {
+ invocation ->
+ val future = invocation.processingEnv
+ .requireTypeElement(GuavaUtilConcurrentTypeNames.LISTENABLE_FUTURE)
+ assertThat(
+ GuavaListenableFutureUpsertMethodBinderProvider(invocation.context).matches(
+ future.type
+ )).isTrue()
+ }
+ }
+
+ @Test
fun testFindLiveData() {
runProcessorTest(
sources = listOf(COMMON.COMPUTABLE_LIVE_DATA, COMMON.LIVE_DATA)
diff --git a/room/room-runtime/src/main/java/androidx/room/util/DBUtil.kt b/room/room-runtime/src/main/java/androidx/room/util/DBUtil.kt
index b4768fc..bc82786 100644
--- a/room/room-runtime/src/main/java/androidx/room/util/DBUtil.kt
+++ b/room/room-runtime/src/main/java/androidx/room/util/DBUtil.kt
@@ -20,6 +20,7 @@
import android.database.AbstractWindowedCursor
import android.database.Cursor
+import android.database.sqlite.SQLiteConstraintException
import android.os.Build
import android.os.CancellationSignal
import androidx.annotation.RestrictTo
@@ -30,7 +31,6 @@
import java.io.File
import java.io.FileInputStream
import java.io.IOException
-import java.lang.IllegalStateException
import java.nio.ByteBuffer
/**
@@ -121,7 +121,7 @@
db.query("PRAGMA foreign_key_check(`$tableName`)").useCursor { cursor ->
if (cursor.count > 0) {
val errorMsg = processForeignKeyCheckFailure(cursor)
- throw IllegalStateException(errorMsg)
+ throw SQLiteConstraintException(errorMsg)
}
}
}
diff --git a/settings.gradle b/settings.gradle
index d3d7d62..374bd27 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -671,6 +671,7 @@
includeProject(":inspection:inspection-gradle-plugin", [BuildType.MAIN])
includeProject(":inspection:inspection-testing", [BuildType.MAIN, BuildType.COMPOSE])
includeProject(":interpolator:interpolator", [BuildType.MAIN])
+includeProject(":javascriptengine:javascriptengine", [BuildType.MAIN])
includeProject(":leanback:leanback", [BuildType.MAIN])
includeProject(":leanback:leanback-grid", [BuildType.MAIN])
includeProject(":leanback:leanback-paging", [BuildType.MAIN])
@@ -844,8 +845,8 @@
includeProject(":tracing:tracing-perfetto-common")
includeProject(":transition:transition", [BuildType.MAIN, BuildType.FLAN])
includeProject(":transition:transition-ktx", [BuildType.MAIN, BuildType.FLAN])
-includeProject(":tv:tv-foundation", [BuildType.MAIN, BuildType.COMPOSE])
-includeProject(":tv:tv-material", [BuildType.MAIN, BuildType.COMPOSE])
+includeProject(":tv:tv-foundation", [BuildType.COMPOSE])
+includeProject(":tv:tv-material", [BuildType.COMPOSE])
includeProject(":tvprovider:tvprovider", [BuildType.MAIN])
includeProject(":vectordrawable:integration-tests:testapp", [BuildType.MAIN])
includeProject(":vectordrawable:vectordrawable", [BuildType.MAIN])
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java
index 0e44a89..07c45f1 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java
@@ -364,7 +364,7 @@
return new SinglyLinkedList<T>(new Node<T>(data, rest.mHead));
}
- @SuppressWarnings("MissingOverride")
+ @Override
public Iterator<T> iterator() {
return new Iterator<T>() {
private Node<T> mNext = mHead;
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Direction.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Direction.java
index 4467ef9..7e68ac9 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Direction.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Direction.java
@@ -19,21 +19,23 @@
import androidx.annotation.NonNull;
/** An enumeration used to specify the primary direction of certain gestures. */
-@SuppressWarnings("ImmutableEnumChecker")
public enum Direction {
LEFT, RIGHT, UP, DOWN;
- private Direction mOpposite;
- static {
- LEFT.mOpposite = RIGHT;
- RIGHT.mOpposite = LEFT;
- UP.mOpposite = DOWN;
- DOWN.mOpposite = UP;
- }
-
/** Returns the reverse of the given direction. */
@NonNull
public static Direction reverse(@NonNull Direction direction) {
- return direction.mOpposite;
+ switch (direction) {
+ case LEFT:
+ return RIGHT;
+ case RIGHT:
+ return LEFT;
+ case UP:
+ return DOWN;
+ case DOWN:
+ return UP;
+ default:
+ throw new AssertionError(direction);
+ }
}
}
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/EventCondition.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/EventCondition.java
index 89cabe7..10c14c5 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/EventCondition.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/EventCondition.java
@@ -24,8 +24,6 @@
*/
@SuppressWarnings("TypeNameShadowing")
public abstract class EventCondition<R> extends Condition<AccessibilityEvent, Boolean> {
- @SuppressWarnings({"HiddenAbstractMethod", "MissingOverride"})
- abstract Boolean apply(AccessibilityEvent event);
@SuppressWarnings("HiddenAbstractMethod")
abstract R getResult();
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java
index 4bc07bf..8464613 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java
@@ -61,25 +61,11 @@
/** Comparator for sorting PointerGestures by start times. */
private static final Comparator<PointerGesture> START_TIME_COMPARATOR =
- new Comparator<PointerGesture>() {
-
- @Override
- @SuppressWarnings("BadComparable")
- public int compare(PointerGesture o1, PointerGesture o2) {
- return (int)(o1.delay() - o2.delay());
- }
- };
+ (o1, o2) -> Long.compare(o1.delay(), o2.delay());
/** Comparator for sorting PointerGestures by end times. */
private static final Comparator<PointerGesture> END_TIME_COMPARATOR =
- new Comparator<PointerGesture>() {
-
- @Override
- @SuppressWarnings("BadComparable")
- public int compare(PointerGesture o1, PointerGesture o2) {
- return (int)((o1.delay() + o2.duration()) - (o2.delay() + o2.duration()));
- }
- };
+ (o1, o2) -> Long.compare(o1.delay() + o1.duration(), o2.delay() + o2.duration());
// Private constructor.
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java
index 2997a86..3177ad2 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java
@@ -75,8 +75,7 @@
/**
* Predicate for waiting for any of the events specified in the mask
*/
- @SuppressWarnings("ClassCanBeStatic")
- class WaitForAnyEventPredicate implements AccessibilityEventFilter {
+ static class WaitForAnyEventPredicate implements AccessibilityEventFilter {
int mMask;
WaitForAnyEventPredicate(int mask) {
mMask = mask;
@@ -98,8 +97,7 @@
* a ctor passed list with matching events. User of this predicate must recycle
* all populated events in the events list.
*/
- @SuppressWarnings("ClassCanBeStatic")
- class EventCollectingPredicate implements AccessibilityEventFilter {
+ static class EventCollectingPredicate implements AccessibilityEventFilter {
int mMask;
List<AccessibilityEvent> mEventsList;
@@ -125,8 +123,7 @@
/**
* Predicate for waiting for every event specified in the mask to be matched at least once
*/
- @SuppressWarnings("ClassCanBeStatic")
- class WaitForAllEventPredicate implements AccessibilityEventFilter {
+ static class WaitForAllEventPredicate implements AccessibilityEventFilter {
int mMask;
WaitForAllEventPredicate(int mask) {
mMask = mask;
@@ -474,12 +471,10 @@
* @param drag when true, the swipe becomes a drag swipe
* @return true if the swipe executed successfully
*/
- @SuppressWarnings("UnusedVariable")
public boolean swipe(int downX, int downY, int upX, int upY, int steps, boolean drag) {
- boolean ret = false;
+ boolean ret;
int swipeSteps = steps;
- double xStep = 0;
- double yStep = 0;
+ double xStep, yStep;
// avoid a divide by zero
if(swipeSteps == 0)
@@ -515,12 +510,10 @@
* @param segmentSteps steps to inject between two Points
* @return true on success
*/
- @SuppressWarnings("UnusedVariable")
public boolean swipe(Point[] segments, int segmentSteps) {
- boolean ret = false;
+ boolean ret;
int swipeSteps = segmentSteps;
- double xStep = 0;
- double yStep = 0;
+ double xStep, yStep;
// avoid a divide by zero
if(segmentSteps == 0)
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/PointerGesture.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/PointerGesture.java
index c2dadc8..40883a1 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/PointerGesture.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/PointerGesture.java
@@ -57,21 +57,19 @@
}
/** Adds an action which pauses for the specified amount of {@code time} in milliseconds. */
- @SuppressWarnings("UnnecessaryParentheses")
public PointerGesture pause(long time) {
if (time < 0) {
throw new IllegalArgumentException("time cannot be negative");
}
mActions.addLast(new PointerPauseAction(mActions.peekLast().end, time));
- mDuration += (mActions.peekLast().duration);
+ mDuration += mActions.peekLast().duration;
return this;
}
/** Adds an action that moves the pointer to {@code dest} at {@code speed} pixels per second. */
- @SuppressWarnings("UnnecessaryParentheses")
public PointerGesture move(Point dest, int speed) {
mActions.addLast(new PointerLinearMoveAction(mActions.peekLast().end, dest, speed));
- mDuration += (mActions.peekLast().duration);
+ mDuration += mActions.peekLast().duration;
return this;
}
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
index d6b00fa..ab8466c1 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
@@ -136,7 +136,7 @@
}
/** Returns whether there is a match for the given {@code selector} criteria. */
- @SuppressWarnings("MissingOverride")
+ @Override
public boolean hasObject(BySelector selector) {
AccessibilityNodeInfo node = ByMatcher.findMatch(this, selector, getWindowRoots());
if (node != null) {
@@ -150,14 +150,14 @@
* Returns the first object to match the {@code selector} criteria,
* or null if no matching objects are found.
*/
- @SuppressWarnings("MissingOverride")
+ @Override
public UiObject2 findObject(BySelector selector) {
AccessibilityNodeInfo node = ByMatcher.findMatch(this, selector, getWindowRoots());
return node != null ? new UiObject2(this, selector, node) : null;
}
/** Returns all objects that match the {@code selector} criteria. */
- @SuppressWarnings("MissingOverride")
+ @Override
public List<UiObject2> findObjects(BySelector selector) {
List<UiObject2> ret = new ArrayList<UiObject2>();
for (AccessibilityNodeInfo node : ByMatcher.findMatches(this, selector, getWindowRoots())) {
@@ -964,7 +964,6 @@
* window does not have the specified package name
* @since API Level 16
*/
- @SuppressWarnings("UndefinedEquals")
public boolean waitForWindowUpdate(final String packageName, long timeout) {
Tracer.trace(packageName, timeout);
if (packageName != null) {
@@ -981,7 +980,7 @@
@Override
public boolean accept(AccessibilityEvent t) {
if (t.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
- return packageName == null || packageName.equals(t.getPackageName());
+ return packageName == null || packageName.contentEquals(t.getPackageName());
}
return false;
}
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java
index f1202f9..86ca021 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java
@@ -1080,7 +1080,6 @@
* <code>false</code> otherwise
* @since API Level 18
*/
- @SuppressWarnings("NarrowingCompoundAssignment")
public boolean performTwoPointerGesture(Point startPoint1, Point startPoint2, Point endPoint1,
Point endPoint2, int steps) {
@@ -1119,10 +1118,10 @@
p2.size = 1;
points2[i] = p2;
- eventX1 += stepX1;
- eventY1 += stepY1;
- eventX2 += stepX2;
- eventY2 += stepY2;
+ eventX1 = (int) (eventX1 + stepX1);
+ eventY1 = (int) (eventY1 + stepY1);
+ eventX2 = (int) (eventX2 + stepX2);
+ eventY2 = (int) (eventY2 + stepY2);
}
// ending pointers coordinates
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
index 2e1618a..a4332c6 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
@@ -99,19 +99,15 @@
/** {@inheritDoc} */
@Override
- @SuppressWarnings("EqualsGetClass")
public boolean equals(Object object) {
if (this == object) {
return true;
}
- if (object == null) {
- return false;
- }
- if (getClass() != object.getClass()) {
+ if (!(object instanceof UiObject2)) {
return false;
}
try {
- UiObject2 other = (UiObject2)object;
+ UiObject2 other = (UiObject2) object;
return getAccessibilityNodeInfo().equals(other.getAccessibilityNodeInfo());
} catch (StaleObjectException e) {
return false;
@@ -209,7 +205,7 @@
* Searches all elements under this object and returns the first object to match the criteria,
* or null if no matching objects are found.
*/
- @SuppressWarnings("MissingOverride")
+ @Override
public UiObject2 findObject(BySelector selector) {
AccessibilityNodeInfo node =
ByMatcher.findMatch(getDevice(), selector, getAccessibilityNodeInfo());
@@ -217,7 +213,7 @@
}
/** Searches all elements under this object and returns all objects that match the criteria. */
- @SuppressWarnings("MissingOverride")
+ @Override
public List<UiObject2> findObjects(BySelector selector) {
List<UiObject2> ret = new ArrayList<UiObject2>();
for (AccessibilityNodeInfo node :
@@ -695,7 +691,6 @@
* Set the text content by sending individual key codes.
* @hide
*/
- @SuppressWarnings("UndefinedEquals")
public void legacySetText(String text) {
AccessibilityNodeInfo node = getAccessibilityNodeInfo();
@@ -705,7 +700,7 @@
}
CharSequence currentText = node.getText();
- if (!text.equals(currentText)) {
+ if (!text.contentEquals(currentText)) {
InteractionController ic = getDevice().getInteractionController();
// Long click left + center
@@ -725,7 +720,6 @@
}
/** Sets the text content if this object is an editable field. */
- @SuppressWarnings("UndefinedEquals")
public void setText(String text) {
AccessibilityNodeInfo node = getAccessibilityNodeInfo();
@@ -745,7 +739,7 @@
}
} else {
CharSequence currentText = node.getText();
- if (!text.equals(currentText)) {
+ if (!text.contentEquals(currentText)) {
// Give focus to the object. Expect this to fail if the object already has focus.
if (!node.performAction(AccessibilityNodeInfo.ACTION_FOCUS) && !node.isFocused()) {
// TODO: Decide if we should throw here
diff --git a/tracing/tracing-perfetto-common/api/current.txt b/tracing/tracing-perfetto-common/api/current.txt
index d830d16..6615b79 100644
--- a/tracing/tracing-perfetto-common/api/current.txt
+++ b/tracing/tracing-perfetto-common/api/current.txt
@@ -9,10 +9,10 @@
public static final class PerfettoHandshake.EnableTracingResponse {
method public int getExitCode();
method public String? getMessage();
- method public String getRequiredVersion();
+ method public String? getRequiredVersion();
property public final int exitCode;
property public final String? message;
- property public final String requiredVersion;
+ property public final String? requiredVersion;
}
public static final class PerfettoHandshake.ExternalLibraryProvider {
@@ -22,6 +22,7 @@
public static final class PerfettoHandshake.ResponseExitCodes {
field public static final androidx.tracing.perfetto.PerfettoHandshake.ResponseExitCodes INSTANCE;
field public static final int RESULT_CODE_ALREADY_ENABLED = 2; // 0x2
+ field public static final int RESULT_CODE_CANCELLED = 0; // 0x0
field public static final int RESULT_CODE_ERROR_BINARY_MISSING = 11; // 0xb
field public static final int RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR = 13; // 0xd
field public static final int RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH = 12; // 0xc
diff --git a/tracing/tracing-perfetto-common/api/public_plus_experimental_current.txt b/tracing/tracing-perfetto-common/api/public_plus_experimental_current.txt
index d830d16..6615b79 100644
--- a/tracing/tracing-perfetto-common/api/public_plus_experimental_current.txt
+++ b/tracing/tracing-perfetto-common/api/public_plus_experimental_current.txt
@@ -9,10 +9,10 @@
public static final class PerfettoHandshake.EnableTracingResponse {
method public int getExitCode();
method public String? getMessage();
- method public String getRequiredVersion();
+ method public String? getRequiredVersion();
property public final int exitCode;
property public final String? message;
- property public final String requiredVersion;
+ property public final String? requiredVersion;
}
public static final class PerfettoHandshake.ExternalLibraryProvider {
@@ -22,6 +22,7 @@
public static final class PerfettoHandshake.ResponseExitCodes {
field public static final androidx.tracing.perfetto.PerfettoHandshake.ResponseExitCodes INSTANCE;
field public static final int RESULT_CODE_ALREADY_ENABLED = 2; // 0x2
+ field public static final int RESULT_CODE_CANCELLED = 0; // 0x0
field public static final int RESULT_CODE_ERROR_BINARY_MISSING = 11; // 0xb
field public static final int RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR = 13; // 0xd
field public static final int RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH = 12; // 0xc
diff --git a/tracing/tracing-perfetto-common/api/restricted_current.txt b/tracing/tracing-perfetto-common/api/restricted_current.txt
index d830d16..6615b79 100644
--- a/tracing/tracing-perfetto-common/api/restricted_current.txt
+++ b/tracing/tracing-perfetto-common/api/restricted_current.txt
@@ -9,10 +9,10 @@
public static final class PerfettoHandshake.EnableTracingResponse {
method public int getExitCode();
method public String? getMessage();
- method public String getRequiredVersion();
+ method public String? getRequiredVersion();
property public final int exitCode;
property public final String? message;
- property public final String requiredVersion;
+ property public final String? requiredVersion;
}
public static final class PerfettoHandshake.ExternalLibraryProvider {
@@ -22,6 +22,7 @@
public static final class PerfettoHandshake.ResponseExitCodes {
field public static final androidx.tracing.perfetto.PerfettoHandshake.ResponseExitCodes INSTANCE;
field public static final int RESULT_CODE_ALREADY_ENABLED = 2; // 0x2
+ field public static final int RESULT_CODE_CANCELLED = 0; // 0x0
field public static final int RESULT_CODE_ERROR_BINARY_MISSING = 11; // 0xb
field public static final int RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR = 13; // 0xd
field public static final int RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH = 12; // 0xc
diff --git a/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/PerfettoHandshake.kt b/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/PerfettoHandshake.kt
index 78696b3..d9fc71c 100644
--- a/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/PerfettoHandshake.kt
+++ b/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/PerfettoHandshake.kt
@@ -62,7 +62,14 @@
" $pathExtra " +
"$targetPackage/$RECEIVER_CLASS_NAME"
val rawResponse = executeShellCommand(command)
- return parseResponse(rawResponse)
+
+ return try {
+ parseResponse(rawResponse)
+ } catch (e: IllegalArgumentException) {
+ val message = "Exception occurred while trying to parse a response." +
+ " Error: ${e.message}. Raw response: $rawResponse."
+ EnableTracingResponse(ResponseExitCodes.RESULT_CODE_ERROR_OTHER, null, message)
+ }
}
private fun parseResponse(rawResponse: String): EnableTracingResponse {
@@ -71,6 +78,10 @@
.firstOrNull { it.contains("Broadcast completed: result=") }
?: throw IllegalArgumentException("Cannot parse: $rawResponse")
+ if (line == "Broadcast completed: result=0") return EnableTracingResponse(
+ ResponseExitCodes.RESULT_CODE_CANCELLED, null, null
+ )
+
val matchResult =
Regex("Broadcast completed: (result=.*?)(, data=\".*?\")?(, extras: .*)?")
.matchEntire(line)
@@ -203,6 +214,15 @@
}
public object ResponseExitCodes {
+ /**
+ * Indicates that the broadcast resulted in `result=0`, which is an equivalent
+ * of [android.app.Activity.RESULT_CANCELED].
+ *
+ * This most likely means that the app does not expose a [PerfettoHandshake] compatible
+ * receiver.
+ */
+ public const val RESULT_CODE_CANCELLED: Int = 0
+
public const val RESULT_CODE_SUCCESS: Int = 1
public const val RESULT_CODE_ALREADY_ENABLED: Int = 2
@@ -228,6 +248,7 @@
@Retention(AnnotationRetention.SOURCE)
@IntDef(
+ ResponseExitCodes.RESULT_CODE_CANCELLED,
ResponseExitCodes.RESULT_CODE_SUCCESS,
ResponseExitCodes.RESULT_CODE_ALREADY_ENABLED,
ResponseExitCodes.RESULT_CODE_ERROR_BINARY_MISSING,
@@ -239,7 +260,15 @@
public class EnableTracingResponse @RestrictTo(LIBRARY_GROUP) constructor(
@EnableTracingResultCode public val exitCode: Int,
- public val requiredVersion: String,
+
+ /**
+ * This can be `null` iff we cannot communicate with the broadcast receiver of the target
+ * process (e.g. app does not offer Perfetto tracing) or if we cannot parse the response
+ * from the receiver. In either case, tracing is unlikely to work under these circumstances,
+ * and more context on how to proceed can be found in [exitCode] or [message] properties.
+ */
+ public val requiredVersion: String?,
+
public val message: String?
)
}
diff --git a/tv/tv-foundation/api/current.txt b/tv/tv-foundation/api/current.txt
index e6f50d0..567cf50 100644
--- a/tv/tv-foundation/api/current.txt
+++ b/tv/tv-foundation/api/current.txt
@@ -1 +1,279 @@
// Signature format: 4.0
+package androidx.tv.foundation {
+
+ public final class MarioScrollableKt {
+ method public static androidx.compose.ui.Modifier marioScrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.tv.foundation.PivotOffsets pivotOffsets, optional boolean enabled, optional boolean reverseDirection);
+ }
+
+ public final class PivotOffsets {
+ ctor public PivotOffsets(optional float parentFraction, optional float childFraction);
+ method public float getChildFraction();
+ method public float getParentFraction();
+ property public final float childFraction;
+ property public final float parentFraction;
+ }
+
+}
+
+package androidx.tv.foundation.lazy {
+
+ public final class LazyBeyondBoundsModifierKt {
+ }
+
+ public final class LazyListPinningModifierKt {
+ }
+
+}
+
+package androidx.tv.foundation.lazy.grid {
+
+ public final class LazyGridDslKt {
+ method @androidx.compose.runtime.Composable public static void TvLazyHorizontalGrid(androidx.tv.foundation.lazy.grid.TvGridCells rows, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.grid.TvLazyGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void TvLazyVerticalGrid(androidx.tv.foundation.lazy.grid.TvGridCells columns, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.grid.TvLazyGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridScope,kotlin.Unit> content);
+ method public static inline <T> void items(androidx.tv.foundation.lazy.grid.TvLazyGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void items(androidx.tv.foundation.lazy.grid.TvLazyGridScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.grid.TvLazyGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super java.lang.Integer,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.grid.TvLazyGridScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super java.lang.Integer,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ }
+
+ public final class LazyGridItemPlacementAnimatorKt {
+ }
+
+ public final class LazyGridItemsProviderImplKt {
+ }
+
+ public final class LazyGridKt {
+ }
+
+ public final class LazyGridMeasureKt {
+ }
+
+ public final class LazyGridScrollingKt {
+ }
+
+ public final class LazyGridSpanKt {
+ method public static long TvGridItemSpan(int currentLineSpan);
+ }
+
+ public final class LazySemanticsKt {
+ }
+
+ @androidx.compose.runtime.Stable public interface TvGridCells {
+ method public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
+ }
+
+ public static final class TvGridCells.Adaptive implements androidx.tv.foundation.lazy.grid.TvGridCells {
+ ctor public TvGridCells.Adaptive(float minSize);
+ method public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
+ }
+
+ public static final class TvGridCells.Fixed implements androidx.tv.foundation.lazy.grid.TvGridCells {
+ ctor public TvGridCells.Fixed(int count);
+ method public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
+ }
+
+ @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class TvGridItemSpan {
+ method public int getCurrentLineSpan();
+ property public final int currentLineSpan;
+ }
+
+ public sealed interface TvLazyGridItemInfo {
+ method public int getColumn();
+ method public int getIndex();
+ method public Object getKey();
+ method public long getOffset();
+ method public int getRow();
+ method public long getSize();
+ property public abstract int column;
+ property public abstract int index;
+ property public abstract Object key;
+ property public abstract long offset;
+ property public abstract int row;
+ property public abstract long size;
+ field public static final androidx.tv.foundation.lazy.grid.TvLazyGridItemInfo.Companion Companion;
+ field public static final int UnknownColumn = -1; // 0xffffffff
+ field public static final int UnknownRow = -1; // 0xffffffff
+ }
+
+ public static final class TvLazyGridItemInfo.Companion {
+ field public static final int UnknownColumn = -1; // 0xffffffff
+ field public static final int UnknownRow = -1; // 0xffffffff
+ }
+
+ @androidx.compose.runtime.Stable @androidx.tv.foundation.lazy.grid.TvLazyGridScopeMarker public sealed interface TvLazyGridItemScope {
+ }
+
+ @androidx.tv.foundation.lazy.grid.TvLazyGridScopeMarker public sealed interface TvLazyGridItemSpanScope {
+ method public int getMaxCurrentLineSpan();
+ method public int getMaxLineSpan();
+ property public abstract int maxCurrentLineSpan;
+ property public abstract int maxLineSpan;
+ }
+
+ public sealed interface TvLazyGridLayoutInfo {
+ method public int getAfterContentPadding();
+ method public int getBeforeContentPadding();
+ method public androidx.compose.foundation.gestures.Orientation getOrientation();
+ method public boolean getReverseLayout();
+ method public int getTotalItemsCount();
+ method public int getViewportEndOffset();
+ method public long getViewportSize();
+ method public int getViewportStartOffset();
+ method public java.util.List<androidx.tv.foundation.lazy.grid.TvLazyGridItemInfo> getVisibleItemsInfo();
+ property public abstract int afterContentPadding;
+ property public abstract int beforeContentPadding;
+ property public abstract androidx.compose.foundation.gestures.Orientation orientation;
+ property public abstract boolean reverseLayout;
+ property public abstract int totalItemsCount;
+ property public abstract int viewportEndOffset;
+ property public abstract long viewportSize;
+ property public abstract int viewportStartOffset;
+ property public abstract java.util.List<androidx.tv.foundation.lazy.grid.TvLazyGridItemInfo> visibleItemsInfo;
+ }
+
+ @androidx.tv.foundation.lazy.grid.TvLazyGridScopeMarker public sealed interface TvLazyGridScope {
+ method public void item(optional Object? key, optional kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,kotlin.Unit> content);
+ method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super java.lang.Integer,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+ }
+
+ @kotlin.DslMarker public @interface TvLazyGridScopeMarker {
+ }
+
+ @androidx.compose.runtime.Stable public final class TvLazyGridState implements androidx.compose.foundation.gestures.ScrollableState {
+ ctor public TvLazyGridState(optional int firstVisibleItemIndex, optional int firstVisibleItemScrollOffset);
+ method public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public float dispatchRawDelta(float delta);
+ method public int getFirstVisibleItemIndex();
+ method public int getFirstVisibleItemScrollOffset();
+ method public androidx.compose.foundation.interaction.InteractionSource getInteractionSource();
+ method public androidx.tv.foundation.lazy.grid.TvLazyGridLayoutInfo getLayoutInfo();
+ method public boolean isScrollInProgress();
+ method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ property public final int firstVisibleItemIndex;
+ property public final int firstVisibleItemScrollOffset;
+ property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
+ property public boolean isScrollInProgress;
+ property public final androidx.tv.foundation.lazy.grid.TvLazyGridLayoutInfo layoutInfo;
+ field public static final androidx.tv.foundation.lazy.grid.TvLazyGridState.Companion Companion;
+ }
+
+ public static final class TvLazyGridState.Companion {
+ method public androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.grid.TvLazyGridState,?> getSaver();
+ property public final androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.grid.TvLazyGridState,?> Saver;
+ }
+
+ public final class TvLazyGridStateKt {
+ method @androidx.compose.runtime.Composable public static androidx.tv.foundation.lazy.grid.TvLazyGridState rememberTvLazyGridState(optional int initialFirstVisibleItemIndex, optional int initialFirstVisibleItemScrollOffset);
+ }
+
+}
+
+package androidx.tv.foundation.lazy.list {
+
+ public final class LazyDslKt {
+ method @androidx.compose.runtime.Composable public static void TvLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.list.TvLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void TvLazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.list.TvLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListScope,kotlin.Unit> content);
+ method public static inline <T> void items(androidx.tv.foundation.lazy.list.TvLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void items(androidx.tv.foundation.lazy.list.TvLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.list.TvLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.list.TvLazyListScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ }
+
+ public final class LazyListHeadersKt {
+ }
+
+ public final class LazyListItemPlacementAnimatorKt {
+ }
+
+ public final class LazyListItemsProviderImplKt {
+ }
+
+ public final class LazyListKt {
+ }
+
+ public final class LazyListMeasureKt {
+ }
+
+ public final class LazyListScrollingKt {
+ }
+
+ public final class LazyListStateKt {
+ method @androidx.compose.runtime.Composable public static androidx.tv.foundation.lazy.list.TvLazyListState rememberTvLazyListState(optional int initialFirstVisibleItemIndex, optional int initialFirstVisibleItemScrollOffset);
+ }
+
+ public final class LazySemanticsKt {
+ }
+
+ public interface TvLazyListItemInfo {
+ method public int getIndex();
+ method public Object getKey();
+ method public int getOffset();
+ method public int getSize();
+ property public abstract int index;
+ property public abstract Object key;
+ property public abstract int offset;
+ property public abstract int size;
+ }
+
+ @androidx.compose.runtime.Stable @androidx.tv.foundation.lazy.list.TvLazyListScopeMarker public sealed interface TvLazyListItemScope {
+ method public androidx.compose.ui.Modifier fillParentMaxHeight(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+ method public androidx.compose.ui.Modifier fillParentMaxSize(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+ method public androidx.compose.ui.Modifier fillParentMaxWidth(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+ }
+
+ public sealed interface TvLazyListLayoutInfo {
+ method public int getAfterContentPadding();
+ method public int getBeforeContentPadding();
+ method public androidx.compose.foundation.gestures.Orientation getOrientation();
+ method public boolean getReverseLayout();
+ method public int getTotalItemsCount();
+ method public int getViewportEndOffset();
+ method public long getViewportSize();
+ method public int getViewportStartOffset();
+ method public java.util.List<androidx.tv.foundation.lazy.list.TvLazyListItemInfo> getVisibleItemsInfo();
+ property public abstract int afterContentPadding;
+ property public abstract int beforeContentPadding;
+ property public abstract androidx.compose.foundation.gestures.Orientation orientation;
+ property public abstract boolean reverseLayout;
+ property public abstract int totalItemsCount;
+ property public abstract int viewportEndOffset;
+ property public abstract long viewportSize;
+ property public abstract int viewportStartOffset;
+ property public abstract java.util.List<androidx.tv.foundation.lazy.list.TvLazyListItemInfo> visibleItemsInfo;
+ }
+
+ @androidx.tv.foundation.lazy.list.TvLazyListScopeMarker public sealed interface TvLazyListScope {
+ method public void item(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,kotlin.Unit> content);
+ method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+ }
+
+ @kotlin.DslMarker public @interface TvLazyListScopeMarker {
+ }
+
+ @androidx.compose.runtime.Stable public final class TvLazyListState implements androidx.compose.foundation.gestures.ScrollableState {
+ ctor public TvLazyListState(optional int firstVisibleItemIndex, optional int firstVisibleItemScrollOffset);
+ method public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public float dispatchRawDelta(float delta);
+ method public int getFirstVisibleItemIndex();
+ method public int getFirstVisibleItemScrollOffset();
+ method public androidx.compose.foundation.interaction.InteractionSource getInteractionSource();
+ method public androidx.tv.foundation.lazy.list.TvLazyListLayoutInfo getLayoutInfo();
+ method public boolean isScrollInProgress();
+ method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ property public final int firstVisibleItemIndex;
+ property public final int firstVisibleItemScrollOffset;
+ property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
+ property public boolean isScrollInProgress;
+ property public final androidx.tv.foundation.lazy.list.TvLazyListLayoutInfo layoutInfo;
+ field public static final androidx.tv.foundation.lazy.list.TvLazyListState.Companion Companion;
+ }
+
+ public static final class TvLazyListState.Companion {
+ method public androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.list.TvLazyListState,?> getSaver();
+ property public final androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.list.TvLazyListState,?> Saver;
+ }
+
+}
+
diff --git a/tv/tv-foundation/api/public_plus_experimental_current.txt b/tv/tv-foundation/api/public_plus_experimental_current.txt
index e6f50d0..f6f513b 100644
--- a/tv/tv-foundation/api/public_plus_experimental_current.txt
+++ b/tv/tv-foundation/api/public_plus_experimental_current.txt
@@ -1 +1,281 @@
// Signature format: 4.0
+package androidx.tv.foundation {
+
+ public final class MarioScrollableKt {
+ method public static androidx.compose.ui.Modifier marioScrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.tv.foundation.PivotOffsets pivotOffsets, optional boolean enabled, optional boolean reverseDirection);
+ }
+
+ public final class PivotOffsets {
+ ctor public PivotOffsets(optional float parentFraction, optional float childFraction);
+ method public float getChildFraction();
+ method public float getParentFraction();
+ property public final float childFraction;
+ property public final float parentFraction;
+ }
+
+}
+
+package androidx.tv.foundation.lazy {
+
+ public final class LazyBeyondBoundsModifierKt {
+ }
+
+ public final class LazyListPinningModifierKt {
+ }
+
+}
+
+package androidx.tv.foundation.lazy.grid {
+
+ public final class LazyGridDslKt {
+ method @androidx.compose.runtime.Composable public static void TvLazyHorizontalGrid(androidx.tv.foundation.lazy.grid.TvGridCells rows, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.grid.TvLazyGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void TvLazyVerticalGrid(androidx.tv.foundation.lazy.grid.TvGridCells columns, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.grid.TvLazyGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridScope,kotlin.Unit> content);
+ method public static inline <T> void items(androidx.tv.foundation.lazy.grid.TvLazyGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void items(androidx.tv.foundation.lazy.grid.TvLazyGridScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.grid.TvLazyGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super java.lang.Integer,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.grid.TvLazyGridScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super java.lang.Integer,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ }
+
+ public final class LazyGridItemPlacementAnimatorKt {
+ }
+
+ public final class LazyGridItemsProviderImplKt {
+ }
+
+ public final class LazyGridKt {
+ }
+
+ public final class LazyGridMeasureKt {
+ }
+
+ public final class LazyGridScrollingKt {
+ }
+
+ public final class LazyGridSpanKt {
+ method public static long TvGridItemSpan(int currentLineSpan);
+ }
+
+ public final class LazySemanticsKt {
+ }
+
+ @androidx.compose.runtime.Stable public interface TvGridCells {
+ method public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
+ }
+
+ public static final class TvGridCells.Adaptive implements androidx.tv.foundation.lazy.grid.TvGridCells {
+ ctor public TvGridCells.Adaptive(float minSize);
+ method public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
+ }
+
+ public static final class TvGridCells.Fixed implements androidx.tv.foundation.lazy.grid.TvGridCells {
+ ctor public TvGridCells.Fixed(int count);
+ method public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
+ }
+
+ @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class TvGridItemSpan {
+ method public int getCurrentLineSpan();
+ property public final int currentLineSpan;
+ }
+
+ public sealed interface TvLazyGridItemInfo {
+ method public int getColumn();
+ method public int getIndex();
+ method public Object getKey();
+ method public long getOffset();
+ method public int getRow();
+ method public long getSize();
+ property public abstract int column;
+ property public abstract int index;
+ property public abstract Object key;
+ property public abstract long offset;
+ property public abstract int row;
+ property public abstract long size;
+ field public static final androidx.tv.foundation.lazy.grid.TvLazyGridItemInfo.Companion Companion;
+ field public static final int UnknownColumn = -1; // 0xffffffff
+ field public static final int UnknownRow = -1; // 0xffffffff
+ }
+
+ public static final class TvLazyGridItemInfo.Companion {
+ field public static final int UnknownColumn = -1; // 0xffffffff
+ field public static final int UnknownRow = -1; // 0xffffffff
+ }
+
+ @androidx.compose.runtime.Stable @androidx.tv.foundation.lazy.grid.TvLazyGridScopeMarker public sealed interface TvLazyGridItemScope {
+ method @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.ui.Modifier animateItemPlacement(androidx.compose.ui.Modifier, optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> animationSpec);
+ }
+
+ @androidx.tv.foundation.lazy.grid.TvLazyGridScopeMarker public sealed interface TvLazyGridItemSpanScope {
+ method public int getMaxCurrentLineSpan();
+ method public int getMaxLineSpan();
+ property public abstract int maxCurrentLineSpan;
+ property public abstract int maxLineSpan;
+ }
+
+ public sealed interface TvLazyGridLayoutInfo {
+ method public int getAfterContentPadding();
+ method public int getBeforeContentPadding();
+ method public androidx.compose.foundation.gestures.Orientation getOrientation();
+ method public boolean getReverseLayout();
+ method public int getTotalItemsCount();
+ method public int getViewportEndOffset();
+ method public long getViewportSize();
+ method public int getViewportStartOffset();
+ method public java.util.List<androidx.tv.foundation.lazy.grid.TvLazyGridItemInfo> getVisibleItemsInfo();
+ property public abstract int afterContentPadding;
+ property public abstract int beforeContentPadding;
+ property public abstract androidx.compose.foundation.gestures.Orientation orientation;
+ property public abstract boolean reverseLayout;
+ property public abstract int totalItemsCount;
+ property public abstract int viewportEndOffset;
+ property public abstract long viewportSize;
+ property public abstract int viewportStartOffset;
+ property public abstract java.util.List<androidx.tv.foundation.lazy.grid.TvLazyGridItemInfo> visibleItemsInfo;
+ }
+
+ @androidx.tv.foundation.lazy.grid.TvLazyGridScopeMarker public sealed interface TvLazyGridScope {
+ method public void item(optional Object? key, optional kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,kotlin.Unit> content);
+ method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super java.lang.Integer,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+ }
+
+ @kotlin.DslMarker public @interface TvLazyGridScopeMarker {
+ }
+
+ @androidx.compose.runtime.Stable public final class TvLazyGridState implements androidx.compose.foundation.gestures.ScrollableState {
+ ctor public TvLazyGridState(optional int firstVisibleItemIndex, optional int firstVisibleItemScrollOffset);
+ method public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public float dispatchRawDelta(float delta);
+ method public int getFirstVisibleItemIndex();
+ method public int getFirstVisibleItemScrollOffset();
+ method public androidx.compose.foundation.interaction.InteractionSource getInteractionSource();
+ method public androidx.tv.foundation.lazy.grid.TvLazyGridLayoutInfo getLayoutInfo();
+ method public boolean isScrollInProgress();
+ method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ property public final int firstVisibleItemIndex;
+ property public final int firstVisibleItemScrollOffset;
+ property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
+ property public boolean isScrollInProgress;
+ property public final androidx.tv.foundation.lazy.grid.TvLazyGridLayoutInfo layoutInfo;
+ field public static final androidx.tv.foundation.lazy.grid.TvLazyGridState.Companion Companion;
+ }
+
+ public static final class TvLazyGridState.Companion {
+ method public androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.grid.TvLazyGridState,?> getSaver();
+ property public final androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.grid.TvLazyGridState,?> Saver;
+ }
+
+ public final class TvLazyGridStateKt {
+ method @androidx.compose.runtime.Composable public static androidx.tv.foundation.lazy.grid.TvLazyGridState rememberTvLazyGridState(optional int initialFirstVisibleItemIndex, optional int initialFirstVisibleItemScrollOffset);
+ }
+
+}
+
+package androidx.tv.foundation.lazy.list {
+
+ public final class LazyDslKt {
+ method @androidx.compose.runtime.Composable public static void TvLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.list.TvLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void TvLazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.list.TvLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListScope,kotlin.Unit> content);
+ method public static inline <T> void items(androidx.tv.foundation.lazy.list.TvLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void items(androidx.tv.foundation.lazy.list.TvLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.list.TvLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.list.TvLazyListScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ }
+
+ public final class LazyListHeadersKt {
+ }
+
+ public final class LazyListItemPlacementAnimatorKt {
+ }
+
+ public final class LazyListItemsProviderImplKt {
+ }
+
+ public final class LazyListKt {
+ }
+
+ public final class LazyListMeasureKt {
+ }
+
+ public final class LazyListScrollingKt {
+ }
+
+ public final class LazyListStateKt {
+ method @androidx.compose.runtime.Composable public static androidx.tv.foundation.lazy.list.TvLazyListState rememberTvLazyListState(optional int initialFirstVisibleItemIndex, optional int initialFirstVisibleItemScrollOffset);
+ }
+
+ public final class LazySemanticsKt {
+ }
+
+ public interface TvLazyListItemInfo {
+ method public int getIndex();
+ method public Object getKey();
+ method public int getOffset();
+ method public int getSize();
+ property public abstract int index;
+ property public abstract Object key;
+ property public abstract int offset;
+ property public abstract int size;
+ }
+
+ @androidx.compose.runtime.Stable @androidx.tv.foundation.lazy.list.TvLazyListScopeMarker public sealed interface TvLazyListItemScope {
+ method @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.ui.Modifier animateItemPlacement(androidx.compose.ui.Modifier, optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset> animationSpec);
+ method public androidx.compose.ui.Modifier fillParentMaxHeight(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+ method public androidx.compose.ui.Modifier fillParentMaxSize(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+ method public androidx.compose.ui.Modifier fillParentMaxWidth(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+ }
+
+ public sealed interface TvLazyListLayoutInfo {
+ method public int getAfterContentPadding();
+ method public int getBeforeContentPadding();
+ method public androidx.compose.foundation.gestures.Orientation getOrientation();
+ method public boolean getReverseLayout();
+ method public int getTotalItemsCount();
+ method public int getViewportEndOffset();
+ method public long getViewportSize();
+ method public int getViewportStartOffset();
+ method public java.util.List<androidx.tv.foundation.lazy.list.TvLazyListItemInfo> getVisibleItemsInfo();
+ property public abstract int afterContentPadding;
+ property public abstract int beforeContentPadding;
+ property public abstract androidx.compose.foundation.gestures.Orientation orientation;
+ property public abstract boolean reverseLayout;
+ property public abstract int totalItemsCount;
+ property public abstract int viewportEndOffset;
+ property public abstract long viewportSize;
+ property public abstract int viewportStartOffset;
+ property public abstract java.util.List<androidx.tv.foundation.lazy.list.TvLazyListItemInfo> visibleItemsInfo;
+ }
+
+ @androidx.tv.foundation.lazy.list.TvLazyListScopeMarker public sealed interface TvLazyListScope {
+ method public void item(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,kotlin.Unit> content);
+ method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+ }
+
+ @kotlin.DslMarker public @interface TvLazyListScopeMarker {
+ }
+
+ @androidx.compose.runtime.Stable public final class TvLazyListState implements androidx.compose.foundation.gestures.ScrollableState {
+ ctor public TvLazyListState(optional int firstVisibleItemIndex, optional int firstVisibleItemScrollOffset);
+ method public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public float dispatchRawDelta(float delta);
+ method public int getFirstVisibleItemIndex();
+ method public int getFirstVisibleItemScrollOffset();
+ method public androidx.compose.foundation.interaction.InteractionSource getInteractionSource();
+ method public androidx.tv.foundation.lazy.list.TvLazyListLayoutInfo getLayoutInfo();
+ method public boolean isScrollInProgress();
+ method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ property public final int firstVisibleItemIndex;
+ property public final int firstVisibleItemScrollOffset;
+ property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
+ property public boolean isScrollInProgress;
+ property public final androidx.tv.foundation.lazy.list.TvLazyListLayoutInfo layoutInfo;
+ field public static final androidx.tv.foundation.lazy.list.TvLazyListState.Companion Companion;
+ }
+
+ public static final class TvLazyListState.Companion {
+ method public androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.list.TvLazyListState,?> getSaver();
+ property public final androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.list.TvLazyListState,?> Saver;
+ }
+
+}
+
diff --git a/tv/tv-foundation/api/restricted_current.txt b/tv/tv-foundation/api/restricted_current.txt
index e6f50d0..567cf50 100644
--- a/tv/tv-foundation/api/restricted_current.txt
+++ b/tv/tv-foundation/api/restricted_current.txt
@@ -1 +1,279 @@
// Signature format: 4.0
+package androidx.tv.foundation {
+
+ public final class MarioScrollableKt {
+ method public static androidx.compose.ui.Modifier marioScrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.tv.foundation.PivotOffsets pivotOffsets, optional boolean enabled, optional boolean reverseDirection);
+ }
+
+ public final class PivotOffsets {
+ ctor public PivotOffsets(optional float parentFraction, optional float childFraction);
+ method public float getChildFraction();
+ method public float getParentFraction();
+ property public final float childFraction;
+ property public final float parentFraction;
+ }
+
+}
+
+package androidx.tv.foundation.lazy {
+
+ public final class LazyBeyondBoundsModifierKt {
+ }
+
+ public final class LazyListPinningModifierKt {
+ }
+
+}
+
+package androidx.tv.foundation.lazy.grid {
+
+ public final class LazyGridDslKt {
+ method @androidx.compose.runtime.Composable public static void TvLazyHorizontalGrid(androidx.tv.foundation.lazy.grid.TvGridCells rows, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.grid.TvLazyGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void TvLazyVerticalGrid(androidx.tv.foundation.lazy.grid.TvGridCells columns, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.grid.TvLazyGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridScope,kotlin.Unit> content);
+ method public static inline <T> void items(androidx.tv.foundation.lazy.grid.TvLazyGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void items(androidx.tv.foundation.lazy.grid.TvLazyGridScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.grid.TvLazyGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super java.lang.Integer,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.grid.TvLazyGridScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super java.lang.Integer,? super T,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ }
+
+ public final class LazyGridItemPlacementAnimatorKt {
+ }
+
+ public final class LazyGridItemsProviderImplKt {
+ }
+
+ public final class LazyGridKt {
+ }
+
+ public final class LazyGridMeasureKt {
+ }
+
+ public final class LazyGridScrollingKt {
+ }
+
+ public final class LazyGridSpanKt {
+ method public static long TvGridItemSpan(int currentLineSpan);
+ }
+
+ public final class LazySemanticsKt {
+ }
+
+ @androidx.compose.runtime.Stable public interface TvGridCells {
+ method public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
+ }
+
+ public static final class TvGridCells.Adaptive implements androidx.tv.foundation.lazy.grid.TvGridCells {
+ ctor public TvGridCells.Adaptive(float minSize);
+ method public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
+ }
+
+ public static final class TvGridCells.Fixed implements androidx.tv.foundation.lazy.grid.TvGridCells {
+ ctor public TvGridCells.Fixed(int count);
+ method public java.util.List<java.lang.Integer> calculateCrossAxisCellSizes(androidx.compose.ui.unit.Density, int availableSize, int spacing);
+ }
+
+ @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class TvGridItemSpan {
+ method public int getCurrentLineSpan();
+ property public final int currentLineSpan;
+ }
+
+ public sealed interface TvLazyGridItemInfo {
+ method public int getColumn();
+ method public int getIndex();
+ method public Object getKey();
+ method public long getOffset();
+ method public int getRow();
+ method public long getSize();
+ property public abstract int column;
+ property public abstract int index;
+ property public abstract Object key;
+ property public abstract long offset;
+ property public abstract int row;
+ property public abstract long size;
+ field public static final androidx.tv.foundation.lazy.grid.TvLazyGridItemInfo.Companion Companion;
+ field public static final int UnknownColumn = -1; // 0xffffffff
+ field public static final int UnknownRow = -1; // 0xffffffff
+ }
+
+ public static final class TvLazyGridItemInfo.Companion {
+ field public static final int UnknownColumn = -1; // 0xffffffff
+ field public static final int UnknownRow = -1; // 0xffffffff
+ }
+
+ @androidx.compose.runtime.Stable @androidx.tv.foundation.lazy.grid.TvLazyGridScopeMarker public sealed interface TvLazyGridItemScope {
+ }
+
+ @androidx.tv.foundation.lazy.grid.TvLazyGridScopeMarker public sealed interface TvLazyGridItemSpanScope {
+ method public int getMaxCurrentLineSpan();
+ method public int getMaxLineSpan();
+ property public abstract int maxCurrentLineSpan;
+ property public abstract int maxLineSpan;
+ }
+
+ public sealed interface TvLazyGridLayoutInfo {
+ method public int getAfterContentPadding();
+ method public int getBeforeContentPadding();
+ method public androidx.compose.foundation.gestures.Orientation getOrientation();
+ method public boolean getReverseLayout();
+ method public int getTotalItemsCount();
+ method public int getViewportEndOffset();
+ method public long getViewportSize();
+ method public int getViewportStartOffset();
+ method public java.util.List<androidx.tv.foundation.lazy.grid.TvLazyGridItemInfo> getVisibleItemsInfo();
+ property public abstract int afterContentPadding;
+ property public abstract int beforeContentPadding;
+ property public abstract androidx.compose.foundation.gestures.Orientation orientation;
+ property public abstract boolean reverseLayout;
+ property public abstract int totalItemsCount;
+ property public abstract int viewportEndOffset;
+ property public abstract long viewportSize;
+ property public abstract int viewportStartOffset;
+ property public abstract java.util.List<androidx.tv.foundation.lazy.grid.TvLazyGridItemInfo> visibleItemsInfo;
+ }
+
+ @androidx.tv.foundation.lazy.grid.TvLazyGridScopeMarker public sealed interface TvLazyGridScope {
+ method public void item(optional Object? key, optional kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,kotlin.Unit> content);
+ method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemSpanScope,? super java.lang.Integer,androidx.tv.foundation.lazy.grid.TvGridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.grid.TvLazyGridItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+ }
+
+ @kotlin.DslMarker public @interface TvLazyGridScopeMarker {
+ }
+
+ @androidx.compose.runtime.Stable public final class TvLazyGridState implements androidx.compose.foundation.gestures.ScrollableState {
+ ctor public TvLazyGridState(optional int firstVisibleItemIndex, optional int firstVisibleItemScrollOffset);
+ method public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public float dispatchRawDelta(float delta);
+ method public int getFirstVisibleItemIndex();
+ method public int getFirstVisibleItemScrollOffset();
+ method public androidx.compose.foundation.interaction.InteractionSource getInteractionSource();
+ method public androidx.tv.foundation.lazy.grid.TvLazyGridLayoutInfo getLayoutInfo();
+ method public boolean isScrollInProgress();
+ method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ property public final int firstVisibleItemIndex;
+ property public final int firstVisibleItemScrollOffset;
+ property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
+ property public boolean isScrollInProgress;
+ property public final androidx.tv.foundation.lazy.grid.TvLazyGridLayoutInfo layoutInfo;
+ field public static final androidx.tv.foundation.lazy.grid.TvLazyGridState.Companion Companion;
+ }
+
+ public static final class TvLazyGridState.Companion {
+ method public androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.grid.TvLazyGridState,?> getSaver();
+ property public final androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.grid.TvLazyGridState,?> Saver;
+ }
+
+ public final class TvLazyGridStateKt {
+ method @androidx.compose.runtime.Composable public static androidx.tv.foundation.lazy.grid.TvLazyGridState rememberTvLazyGridState(optional int initialFirstVisibleItemIndex, optional int initialFirstVisibleItemScrollOffset);
+ }
+
+}
+
+package androidx.tv.foundation.lazy.list {
+
+ public final class LazyDslKt {
+ method @androidx.compose.runtime.Composable public static void TvLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.list.TvLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void TvLazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.tv.foundation.lazy.list.TvLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional boolean userScrollEnabled, optional androidx.tv.foundation.PivotOffsets pivotOffsets, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListScope,kotlin.Unit> content);
+ method public static inline <T> void items(androidx.tv.foundation.lazy.list.TvLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void items(androidx.tv.foundation.lazy.list.TvLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.list.TvLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void itemsIndexed(androidx.tv.foundation.lazy.list.TvLazyListScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ }
+
+ public final class LazyListHeadersKt {
+ }
+
+ public final class LazyListItemPlacementAnimatorKt {
+ }
+
+ public final class LazyListItemsProviderImplKt {
+ }
+
+ public final class LazyListKt {
+ }
+
+ public final class LazyListMeasureKt {
+ }
+
+ public final class LazyListScrollingKt {
+ }
+
+ public final class LazyListStateKt {
+ method @androidx.compose.runtime.Composable public static androidx.tv.foundation.lazy.list.TvLazyListState rememberTvLazyListState(optional int initialFirstVisibleItemIndex, optional int initialFirstVisibleItemScrollOffset);
+ }
+
+ public final class LazySemanticsKt {
+ }
+
+ public interface TvLazyListItemInfo {
+ method public int getIndex();
+ method public Object getKey();
+ method public int getOffset();
+ method public int getSize();
+ property public abstract int index;
+ property public abstract Object key;
+ property public abstract int offset;
+ property public abstract int size;
+ }
+
+ @androidx.compose.runtime.Stable @androidx.tv.foundation.lazy.list.TvLazyListScopeMarker public sealed interface TvLazyListItemScope {
+ method public androidx.compose.ui.Modifier fillParentMaxHeight(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+ method public androidx.compose.ui.Modifier fillParentMaxSize(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+ method public androidx.compose.ui.Modifier fillParentMaxWidth(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
+ }
+
+ public sealed interface TvLazyListLayoutInfo {
+ method public int getAfterContentPadding();
+ method public int getBeforeContentPadding();
+ method public androidx.compose.foundation.gestures.Orientation getOrientation();
+ method public boolean getReverseLayout();
+ method public int getTotalItemsCount();
+ method public int getViewportEndOffset();
+ method public long getViewportSize();
+ method public int getViewportStartOffset();
+ method public java.util.List<androidx.tv.foundation.lazy.list.TvLazyListItemInfo> getVisibleItemsInfo();
+ property public abstract int afterContentPadding;
+ property public abstract int beforeContentPadding;
+ property public abstract androidx.compose.foundation.gestures.Orientation orientation;
+ property public abstract boolean reverseLayout;
+ property public abstract int totalItemsCount;
+ property public abstract int viewportEndOffset;
+ property public abstract long viewportSize;
+ property public abstract int viewportStartOffset;
+ property public abstract java.util.List<androidx.tv.foundation.lazy.list.TvLazyListItemInfo> visibleItemsInfo;
+ }
+
+ @androidx.tv.foundation.lazy.list.TvLazyListScopeMarker public sealed interface TvLazyListScope {
+ method public void item(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,kotlin.Unit> content);
+ method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?> contentType, kotlin.jvm.functions.Function2<? super androidx.tv.foundation.lazy.list.TvLazyListItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+ }
+
+ @kotlin.DslMarker public @interface TvLazyListScopeMarker {
+ }
+
+ @androidx.compose.runtime.Stable public final class TvLazyListState implements androidx.compose.foundation.gestures.ScrollableState {
+ ctor public TvLazyListState(optional int firstVisibleItemIndex, optional int firstVisibleItemScrollOffset);
+ method public suspend Object? animateScrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public float dispatchRawDelta(float delta);
+ method public int getFirstVisibleItemIndex();
+ method public int getFirstVisibleItemScrollOffset();
+ method public androidx.compose.foundation.interaction.InteractionSource getInteractionSource();
+ method public androidx.tv.foundation.lazy.list.TvLazyListLayoutInfo getLayoutInfo();
+ method public boolean isScrollInProgress();
+ method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ property public final int firstVisibleItemIndex;
+ property public final int firstVisibleItemScrollOffset;
+ property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
+ property public boolean isScrollInProgress;
+ property public final androidx.tv.foundation.lazy.list.TvLazyListLayoutInfo layoutInfo;
+ field public static final androidx.tv.foundation.lazy.list.TvLazyListState.Companion Companion;
+ }
+
+ public static final class TvLazyListState.Companion {
+ method public androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.list.TvLazyListState,?> getSaver();
+ property public final androidx.compose.runtime.saveable.Saver<androidx.tv.foundation.lazy.list.TvLazyListState,?> Saver;
+ }
+
+}
+
diff --git a/tv/tv-foundation/build.gradle b/tv/tv-foundation/build.gradle
index c4f9fe0..4bd3f05 100644
--- a/tv/tv-foundation/build.gradle
+++ b/tv/tv-foundation/build.gradle
@@ -14,21 +14,57 @@
* limitations under the License.
*/
+import androidx.build.AndroidXComposePlugin
import androidx.build.LibraryType
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+import java.security.MessageDigest
+import java.util.stream.Collectors
plugins {
id("AndroidXPlugin")
+ id("AndroidXComposePlugin")
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
dependencies {
api(libs.kotlinStdlib)
- // Add dependencies here
+
+ api("androidx.annotation:annotation:1.1.0")
+ api(project(":compose:animation:animation"))
+ api(project(':compose:runtime:runtime'))
+ api(project(":compose:ui:ui"))
+
+ implementation(libs.kotlinStdlibCommon)
+ implementation(project(":compose:foundation:foundation"))
+ implementation(project(":compose:foundation:foundation-layout"))
+ implementation(project(":compose:ui:ui-graphics"))
+ implementation(project(":compose:ui:ui-text"))
+ implementation(project(":compose:ui:ui-util"))
+ implementation("androidx.profileinstaller:profileinstaller:1.2.0-alpha02")
+
+ testImplementation(libs.testRules)
+ testImplementation(libs.testRunner)
+ testImplementation(libs.junit)
+ implementation(libs.truth)
+
+ androidTestImplementation(project(":compose:ui:ui-test"))
+ androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(project(":compose:test-utils"))
+ androidTestImplementation(libs.testRunner)
}
android {
namespace "androidx.tv.foundation"
+ defaultConfig {
+ minSdkVersion 28
+ }
+ // Use Robolectric 4.+
+ testOptions.unitTests.includeAndroidResources = true
+ lintOptions {
+ disable 'IllegalExperimentalApiUsage' // TODO (b/233188423): Address before moving to beta
+ }
}
androidx {
@@ -40,4 +76,477 @@
"to write Jetpack Compose applications for TV devices by providing " +
"functionality to support TV specific devices sizes, shapes and d-pad navigation " +
"supported components. It builds upon the Jetpack Compose libraries."
+ targetsJavaConsumers = false
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions {
+ freeCompilerArgs += [
+ "-Xjvm-default=all",
+ ]
+ }
+}
+
+// Functions and tasks to monitor changes in copied files.
+
+task generateMd5 {
+ ext.genMd5 = { fileNameToHash ->
+ MessageDigest digest = MessageDigest.getInstance("MD5")
+ file(fileNameToHash).withInputStream(){is->
+ byte[] buffer = new byte[8192]
+ int read = 0
+ while( (read = is.read(buffer)) > 0) {
+ digest.update(buffer, 0, read);
+ }
+ }
+ byte[] md5sum = digest.digest()
+ BigInteger bigInt = new BigInteger(1, md5sum)
+ bigInt.toString(16).padLeft(32, '0')
+ }
+
+ doLast {
+ String hashValue = genMd5(file)
+ print "value="
+ println hashValue
+ }
+}
+
+List<CopiedClass> copiedClasses = new ArrayList<>();
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/MarioScrollable.kt",
+ "afaf0f2be6b57df076db42d9218f83d9"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/DataIndex.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/DataIndex.kt",
+ "2aa3c6d2dd05057478e723b2247517e1"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScopeImpl.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyItemScopeImpl.kt",
+ "31e6796d0d03cb84483396a39fc5b7e7"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListHeaders.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListHeaders.kt",
+ "4d71c69f9cb38f741da9cfc4109567dd"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemPlacementAnimator.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListItemPlacementAnimator.kt",
+ "a74bfa05e68e2b6c2e108f022dfbfa26"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProviderImpl.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListItemProviderImpl.kt",
+ "57ff505cbdfa854e15b4fbd9d4a574eb"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListItemsProvider.kt",
+ "42a2c446c81fba89fd7b8480d063b308"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyList.kt",
+ "c605794683c01c516674436c9ebc1f44"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListMeasure.kt",
+ "95c14abd0367f0f39218c9bdd175b242"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListMeasureResult.kt",
+ "d4407572c6550d184133f8b3fd37869f"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScopeImpl.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListScopeImpl.kt",
+ "1888e8b115c73b5ea7f33d48d9887845"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScrolling.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListScrolling.kt",
+ "a32b856a1e8740a6a521df04c9d51ed1"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScrollPosition.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListScrollPosition.kt",
+ "82df7d370ba5b20309e5191a0af431a0"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyListState.kt",
+ "45821a5bf14d3e6e25fee63e61930f57"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItem.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyMeasuredItem.kt",
+ "78b09b4d78ec9d761274b9ca8d24f4f7"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItemProvider.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyMeasuredItemProvider.kt",
+ "bec4211cb3d91bb936e9f0872864244b"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazySemantics.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazySemantics.kt",
+ "739205f656bf107604ba7167e3cee7e7"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/ItemIndex.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/ItemIndex.kt",
+ "1031b8b91a81c684b3c4584bc93d3fb0"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridDsl.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridDsl.kt",
+ "6a0b2db56ef38fb1ac004e4fc9847db8"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemInfo.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridItemInfo.kt",
+ "1f3b13ee45de79bc67ace4133e634600"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt",
+ "0bbc162aab675ca2a34350e3044433e7"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemScopeImpl.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridItemScopeImpl.kt",
+ "b3ff4600791c73028b8661c0e2b49110"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemScope.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridItemScope.kt",
+ "1a40313cc5e67b5808586c012bbfb058"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProviderImpl.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridItemsProviderImpl.kt",
+ "48fdfb1dfa5d39c88d4aa96732192421"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridItemProvider.kt",
+ "e5e95e6cad43cec2b0c30bf201e3cae9"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGrid.kt",
+ "69dbab5e83deab809219d4d7a9ee7fa8"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridLayoutInfo.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridLayoutInfo.kt",
+ "b421c5e74856a78982efe0d8a79d10cb"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt",
+ "2b38f5261ad092d9048cfc4f0a841a1a"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasureResult.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridMeasureResult.kt",
+ "1277598d36d8507d7bf0305cc629a11c"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScopeImpl.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridScopeImpl.kt",
+ "3296c6edcbd56450ba919df105cb36c0"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScopeMarker.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridScopeMarker.kt",
+ "0b7ff258a80e2413f89d56ab0ef41b46"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrolling.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridScrolling.kt",
+ "15f2f9bb89c1603aa4b7e7d1f8a2de5a"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollPosition.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridScrollPosition.kt",
+ "9b3d47322ad526fb17a3d9505a80f673"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpan.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridSpan.kt",
+ "cc63cb4f05cc556e8fcf7504ac0ea57c"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt",
+ "894b9f69a27e247bbe609bdac22bb5ed"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyGridState.kt",
+ "1e37d8a6f159aabe11f488121de59b70"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItem.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyMeasuredItem.kt",
+ "09d9b21d33325a94cac738aad58e2422"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItemProvider.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyMeasuredItemProvider.kt",
+ "3acdfddfd06eb17aac5dbdd326482e35"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLine.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyMeasuredLine.kt",
+ "1104f01e8b1f6eced2401b207114f4a4"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLineProvider.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazyMeasuredLineProvider.kt",
+ "b7b731e6e8fdc520064aaef989575bda"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/grid/LazySemantics.kt",
+ "dab277484b4ec57a5275095b505f79d4"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/list/LazyDsl.kt",
+ "8462c0a61f14639f39dd6f76c6a2aebc"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListPinningModifier.kt",
+ "src/commonMain/kotlin/androidx/tv/foundation/lazy/LazyListPinningModifier.kt",
+ "e37450505d13ab0fd1833f136ec8aa3c"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyScopeMarker.kt",
+ "src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeMarker.kt",
+ "f7b72b3c6bad88868153300b9fbdd922"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScope.kt",
+ "src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemScope.kt",
+ "6254294540cfadf2d6da1bbbce1611e8"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemInfo.kt",
+ "src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemInfo.kt",
+ "7571daa18ca079fd6de31d37c3022574"
+ )
+)
+
+copiedClasses.add(
+ new CopiedClass(
+ "../../compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfo.kt",
+ "src/main/java/androidx/tv/foundation/lazy/list/TvLazyListLayoutInfo.kt",
+ "fa1dffc993bdc486e0819c5d8018cda3"
+ )
+)
+
+task doCopiesNeedUpdate {
+ ext.genMd5 = { fileNameToHash ->
+ try {
+ MessageDigest digest = MessageDigest.getInstance("MD5")
+ file(fileNameToHash).withInputStream() { is ->
+ byte[] buffer = new byte[8192]
+ int read
+ while ((read = is.read(buffer)) > 0) {
+ digest.update(buffer, 0, read);
+ }
+ }
+ byte[] md5sum = digest.digest()
+ BigInteger bigInt = new BigInteger(1, md5sum)
+ bigInt.toString(16).padLeft(32, '0')
+ } catch (Exception e) {
+ throw new GradleException("Failed for file=$fileNameToHash", e)
+ }
+ }
+
+
+ doLast {
+ List<String> failureFiles = new ArrayList<>()
+ copiedClasses.forEach(copiedClass -> {
+ try {
+ String actualMd5 = genMd5(copiedClass.originalFilePath)
+ if (copiedClass.lastKnownGoodHash != actualMd5) {
+ failureFiles.add(copiedClass.toString()+ ", actual=" + actualMd5)
+ }
+ } catch (Exception e) {
+ throw new GradleException("Failed for file=${copiedClass.originalFilePath}", e)
+ }
+ })
+
+ if (!failureFiles.isEmpty()) {
+ throw new GradleException(
+ "Files that were copied have been updated at the source. " +
+ "Please update the copy and then" +
+ " update the hash in the compose-foundation build.gradle file." +
+ failureFiles.stream().collect(Collectors.joining("\n", "\n", "")))
+ }
+ }
+}
+
+class CopiedClass {
+ String originalFilePath
+ String copyFilePath
+ String lastKnownGoodHash
+
+ CopiedClass(String originalFilePath, String copyFilePath, String lastKnownGoodHash) {
+ this.originalFilePath = originalFilePath
+ this.copyFilePath = copyFilePath
+ this.lastKnownGoodHash = lastKnownGoodHash
+ }
+
+ @Override
+ String toString() {
+ return "originalFilePath='" + originalFilePath + '\'' +
+ ", copyFilePath='" + copyFilePath + '\'' +
+ ", lastKnownGoodHash='" + lastKnownGoodHash + '\''
+ }
}
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/gallery/GalleryScreen.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/AutoTestFrameClock.kt
similarity index 61%
copy from camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/gallery/GalleryScreen.kt
copy to tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/AutoTestFrameClock.kt
index fdc9e2d..19e2105 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/gallery/GalleryScreen.kt
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/AutoTestFrameClock.kt
@@ -14,12 +14,15 @@
* limitations under the License.
*/
-package androidx.camera.integration.uiwidgets.compose.ui.screen.gallery
+package androidx.tv.compose.foundation.lazy
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MonotonicFrameClock
+import java.util.concurrent.atomic.AtomicLong
-@Composable
-fun GalleryScreen() {
- Text("Gallery Screen")
+class AutoTestFrameClock : MonotonicFrameClock {
+ private val time = AtomicLong(0)
+
+ override suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R {
+ return onFrame(time.getAndAdd(16_000_000))
+ }
}
\ No newline at end of file
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/BaseLazyGridTestWithOrientation.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/BaseLazyGridTestWithOrientation.kt
new file mode 100644
index 0000000..5bf00b4
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/BaseLazyGridTestWithOrientation.kt
@@ -0,0 +1,247 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import androidx.compose.animation.core.snap
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.testutils.assertIsEqualTo
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.PivotOffsets
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+
+open class BaseLazyGridTestWithOrientation(private val orientation: Orientation) {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ val vertical: Boolean
+ get() = orientation == Orientation.Vertical
+
+ @Stable
+ fun Modifier.crossAxisSize(size: Dp) =
+ if (vertical) {
+ this.width(size)
+ } else {
+ this.height(size)
+ }
+
+ @Stable
+ fun Modifier.mainAxisSize(size: Dp) =
+ if (vertical) {
+ this.height(size)
+ } else {
+ this.width(size)
+ }
+
+ @Stable
+ fun Modifier.axisSize(crossAxis: Dp, mainAxis: Dp) =
+ if (vertical) {
+ this.size(crossAxis, mainAxis)
+ } else {
+ this.size(mainAxis, crossAxis)
+ }
+
+ fun SemanticsNodeInteraction.assertMainAxisSizeIsEqualTo(expectedSize: Dp) =
+ if (vertical) {
+ assertHeightIsEqualTo(expectedSize)
+ } else {
+ assertWidthIsEqualTo(expectedSize)
+ }
+
+ fun SemanticsNodeInteraction.assertCrossAxisSizeIsEqualTo(expectedSize: Dp) =
+ if (vertical) {
+ assertWidthIsEqualTo(expectedSize)
+ } else {
+ assertHeightIsEqualTo(expectedSize)
+ }
+
+ fun SemanticsNodeInteraction.assertStartPositionIsAlmost(expected: Dp) {
+ val position = if (vertical) {
+ getUnclippedBoundsInRoot().top
+ } else {
+ getUnclippedBoundsInRoot().left
+ }
+ position.assertIsEqualTo(expected, tolerance = 1.dp)
+ }
+
+ fun SemanticsNodeInteraction.assertMainAxisStartPositionInRootIsEqualTo(expectedStart: Dp) =
+ if (vertical) {
+ assertTopPositionInRootIsEqualTo(expectedStart)
+ } else {
+ assertLeftPositionInRootIsEqualTo(expectedStart)
+ }
+
+ fun SemanticsNodeInteraction.assertCrossAxisStartPositionInRootIsEqualTo(expectedStart: Dp) =
+ if (vertical) {
+ assertLeftPositionInRootIsEqualTo(expectedStart)
+ } else {
+ assertTopPositionInRootIsEqualTo(expectedStart)
+ }
+
+ fun PaddingValues(
+ mainAxis: Dp = 0.dp,
+ crossAxis: Dp = 0.dp
+ ) = PaddingValues(
+ beforeContent = mainAxis,
+ afterContent = mainAxis,
+ beforeContentCrossAxis = crossAxis,
+ afterContentCrossAxis = crossAxis
+ )
+
+ fun PaddingValues(
+ beforeContent: Dp = 0.dp,
+ afterContent: Dp = 0.dp,
+ beforeContentCrossAxis: Dp = 0.dp,
+ afterContentCrossAxis: Dp = 0.dp,
+ ) = if (vertical) {
+ PaddingValues(
+ start = beforeContentCrossAxis,
+ top = beforeContent,
+ end = afterContentCrossAxis,
+ bottom = afterContent
+ )
+ } else {
+ PaddingValues(
+ start = beforeContent,
+ top = beforeContentCrossAxis,
+ end = afterContent,
+ bottom = afterContentCrossAxis
+ )
+ }
+
+ fun TvLazyGridState.scrollBy(offset: Dp) {
+ runBlocking(Dispatchers.Main) {
+ animateScrollBy(with(rule.density) { offset.roundToPx().toFloat() }, snap())
+ }
+ }
+
+ fun TvLazyGridState.scrollTo(index: Int) {
+ runBlocking(Dispatchers.Main) {
+ scrollToItem(index)
+ }
+ }
+
+ fun ComposeContentTestRule.keyPress(numberOfPresses: Int = 1) {
+ rule.keyPress(
+ if (vertical) NativeKeyEvent.KEYCODE_DPAD_DOWN else NativeKeyEvent.KEYCODE_DPAD_RIGHT,
+ numberOfPresses
+ )
+ }
+
+ @Composable
+ fun LazyGrid(
+ cells: Int,
+ modifier: Modifier = Modifier,
+ state: TvLazyGridState = rememberLazyGridState(),
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+ reverseLayout: Boolean = false,
+ userScrollEnabled: Boolean = true,
+ crossAxisSpacedBy: Dp = 0.dp,
+ mainAxisSpacedBy: Dp = 0.dp,
+ content: TvLazyGridScope.() -> Unit
+ ) = LazyGrid(
+ TvGridCells.Fixed(cells),
+ modifier,
+ state,
+ contentPadding,
+ reverseLayout,
+ userScrollEnabled,
+ crossAxisSpacedBy,
+ mainAxisSpacedBy,
+ content
+ )
+
+ @Composable
+ fun LazyGrid(
+ cells: TvGridCells,
+ modifier: Modifier = Modifier,
+ state: TvLazyGridState = rememberLazyGridState(),
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+ reverseLayout: Boolean = false,
+ userScrollEnabled: Boolean = true,
+ crossAxisSpacedBy: Dp = 0.dp,
+ mainAxisSpacedBy: Dp = 0.dp,
+ content: TvLazyGridScope.() -> Unit
+ ) {
+ if (vertical) {
+ val verticalArrangement = when {
+ mainAxisSpacedBy != 0.dp -> Arrangement.spacedBy(mainAxisSpacedBy)
+ !reverseLayout -> Arrangement.Top
+ else -> Arrangement.Bottom
+ }
+ val horizontalArrangement = when {
+ crossAxisSpacedBy != 0.dp -> Arrangement.spacedBy(crossAxisSpacedBy)
+ else -> Arrangement.Start
+ }
+ TvLazyVerticalGrid(
+ columns = cells,
+ modifier = modifier,
+ state = state,
+ contentPadding = contentPadding,
+ reverseLayout = reverseLayout,
+ userScrollEnabled = userScrollEnabled,
+ verticalArrangement = verticalArrangement,
+ horizontalArrangement = horizontalArrangement,
+ pivotOffsets = PivotOffsets(parentFraction = 0f),
+ content = content
+ )
+ } else {
+ val horizontalArrangement = when {
+ mainAxisSpacedBy != 0.dp -> Arrangement.spacedBy(mainAxisSpacedBy)
+ !reverseLayout -> Arrangement.Start
+ else -> Arrangement.End
+ }
+ val verticalArrangement = when {
+ crossAxisSpacedBy != 0.dp -> Arrangement.spacedBy(crossAxisSpacedBy)
+ else -> Arrangement.Top
+ }
+ TvLazyHorizontalGrid(
+ rows = cells,
+ modifier = modifier,
+ state = state,
+ contentPadding = contentPadding,
+ reverseLayout = reverseLayout,
+ userScrollEnabled = userScrollEnabled,
+ horizontalArrangement = horizontalArrangement,
+ verticalArrangement = verticalArrangement,
+ pivotOffsets = PivotOffsets(parentFraction = 0f),
+ content = content
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyArrangementsTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyArrangementsTest.kt
new file mode 100644
index 0000000..0ed6481
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyArrangementsTest.kt
@@ -0,0 +1,617 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.PivotOffsets
+import androidx.tv.foundation.lazy.list.setContentWithTestViewConfiguration
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class LazyArrangementsTest {
+
+ private val ContainerTag = "ContainerTag"
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ private var itemSize: Dp = Dp.Infinity
+ private var smallerItemSize: Dp = Dp.Infinity
+ private var containerSize: Dp = Dp.Infinity
+
+ @Before
+ fun before() {
+ with(rule.density) {
+ itemSize = 50.toDp()
+ }
+ with(rule.density) {
+ smallerItemSize = 40.toDp()
+ }
+ containerSize = itemSize * 5
+ }
+
+ // cases when we have not enough items to fill min constraints:
+
+ @Test
+ fun vertical_defaultArrangementIsTop() {
+ rule.setContent {
+ TvLazyVerticalGrid(
+ modifier = Modifier.requiredSize(containerSize),
+ columns = TvGridCells.Fixed(1)
+ ) {
+ items(2) {
+ Box(Modifier.requiredSize(itemSize).testTag(it.toString()))
+ }
+ }
+ }
+
+ assertArrangementForTwoItems(Arrangement.Top)
+ }
+
+ @Test
+ fun vertical_centerArrangement() {
+ composeVerticalGridWith(Arrangement.Center)
+ assertArrangementForTwoItems(Arrangement.Center)
+ }
+
+ @Test
+ fun vertical_bottomArrangement() {
+ composeVerticalGridWith(Arrangement.Bottom)
+ assertArrangementForTwoItems(Arrangement.Bottom)
+ }
+
+ @Test
+ fun vertical_spacedArrangementNotFillingViewport() {
+ val arrangement = Arrangement.spacedBy(10.dp)
+ composeVerticalGridWith(arrangement)
+ assertArrangementForTwoItems(arrangement)
+ }
+
+ @Test
+ fun horizontal_defaultArrangementIsStart() {
+ rule.setContent {
+ TvLazyHorizontalGrid(
+ modifier = Modifier.requiredSize(containerSize),
+ rows = TvGridCells.Fixed(1)
+ ) {
+ items(2) {
+ Box(Modifier.requiredSize(itemSize).testTag(it.toString()))
+ }
+ }
+ }
+
+ assertArrangementForTwoItems(Arrangement.Start, LayoutDirection.Ltr)
+ }
+
+ @Test
+ fun horizontal_centerArrangement() {
+ composeHorizontalWith(Arrangement.Center, LayoutDirection.Ltr)
+ assertArrangementForTwoItems(Arrangement.Center, LayoutDirection.Ltr)
+ }
+
+ @Test
+ fun horizontal_endArrangement() {
+ composeHorizontalWith(Arrangement.End, LayoutDirection.Ltr)
+ assertArrangementForTwoItems(Arrangement.End, LayoutDirection.Ltr)
+ }
+
+ @Test
+ fun horizontal_spacedArrangementNotFillingViewport() {
+ val arrangement = Arrangement.spacedBy(10.dp)
+ composeHorizontalWith(arrangement, LayoutDirection.Ltr)
+ assertArrangementForTwoItems(arrangement, LayoutDirection.Ltr)
+ }
+
+ @Test
+ fun horizontal_rtl_startArrangement() {
+ composeHorizontalWith(Arrangement.Center, LayoutDirection.Rtl)
+ assertArrangementForTwoItems(Arrangement.Center, LayoutDirection.Rtl)
+ }
+
+ @Test
+ fun horizontal_rtl_endArrangement() {
+ composeHorizontalWith(Arrangement.End, LayoutDirection.Rtl)
+ assertArrangementForTwoItems(Arrangement.End, LayoutDirection.Rtl)
+ }
+
+ @Test
+ fun horizontal_rtl_spacedArrangementNotFillingViewport() {
+ val arrangement = Arrangement.spacedBy(10.dp)
+ composeHorizontalWith(arrangement, LayoutDirection.Rtl)
+ assertArrangementForTwoItems(arrangement, LayoutDirection.Rtl)
+ }
+
+ // wrap content and spacing
+
+ @Test
+ fun vertical_spacing_affects_wrap_content() {
+ rule.setContent {
+ TvLazyVerticalGrid(
+ verticalArrangement = Arrangement.spacedBy(itemSize),
+ modifier = Modifier.width(itemSize).testTag(ContainerTag),
+ columns = TvGridCells.Fixed(1)
+ ) {
+ items(2) {
+ Box(Modifier.requiredSize(itemSize))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(ContainerTag)
+ .assertWidthIsEqualTo(itemSize)
+ .assertHeightIsEqualTo(itemSize * 3)
+ }
+
+ @Test
+ fun horizontal_spacing_affects_wrap_content() {
+ rule.setContent {
+ TvLazyHorizontalGrid(
+ horizontalArrangement = Arrangement.spacedBy(itemSize),
+ modifier = Modifier.height(itemSize).testTag(ContainerTag),
+ rows = TvGridCells.Fixed(1)
+ ) {
+ items(2) {
+ Box(Modifier.requiredSize(itemSize))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(ContainerTag)
+ .assertWidthIsEqualTo(itemSize * 3)
+ .assertHeightIsEqualTo(itemSize)
+ }
+
+ // spacing added when we have enough items to fill the viewport
+
+ @Test
+ fun vertical_spacing_scrolledToTheTop() {
+ rule.setContent {
+ TvLazyVerticalGrid(
+ verticalArrangement = Arrangement.spacedBy(itemSize),
+ modifier = Modifier.requiredSize(itemSize * 3.5f),
+ columns = TvGridCells.Fixed(1)
+ ) {
+ items(3) {
+ Box(Modifier.requiredSize(itemSize).testTag(it.toString()))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize * 2)
+ }
+
+ @Test
+ fun vertical_spacing_scrolledToTheBottom() {
+ rule.setContentWithTestViewConfiguration {
+ TvLazyVerticalGrid(
+ verticalArrangement = Arrangement.spacedBy(itemSize),
+ modifier = Modifier.requiredSize(itemSize * 3.5f).testTag(ContainerTag),
+ columns = TvGridCells.Fixed(1),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(3) {
+ Box(Modifier.requiredSize(itemSize).testTag(it.toString()).focusable())
+ }
+ }
+ }
+
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 3)
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize * 0.5f)
+
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemSize * 2.5f)
+ }
+
+ @Test
+ fun horizontal_spacing_scrolledToTheStart() {
+ rule.setContent {
+ TvLazyHorizontalGrid(
+ horizontalArrangement = Arrangement.spacedBy(itemSize),
+ modifier = Modifier.requiredSize(itemSize * 3.5f),
+ rows = TvGridCells.Fixed(1)
+ ) {
+ items(3) {
+ Box(Modifier.requiredSize(itemSize).testTag(it.toString()))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(itemSize * 2)
+ }
+
+ @Test
+ fun horizontal_spacing_scrolledToTheEnd() {
+ rule.setContentWithTestViewConfiguration {
+ TvLazyHorizontalGrid(
+ horizontalArrangement = Arrangement.spacedBy(itemSize),
+ modifier = Modifier.requiredSize(itemSize * 3.5f).testTag(ContainerTag),
+ rows = TvGridCells.Fixed(1),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(3) {
+ Box(Modifier.requiredSize(itemSize).testTag(it.toString()).focusable())
+ }
+ }
+ }
+
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT, 3)
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(itemSize * 0.5f)
+
+ rule.onNodeWithTag("2")
+ .assertLeftPositionInRootIsEqualTo(itemSize * 2.5f)
+ }
+
+ @Test
+ fun vertical_scrollingByExactlyTheItemSizePlusSpacer_switchesTheFirstVisibleItem() {
+ val itemSizePx = 30
+ val spacingSizePx = 4
+ val itemSize = with(rule.density) { itemSizePx.toDp() }
+ val spacingSize = with(rule.density) { spacingSizePx.toDp() }
+ lateinit var state: TvLazyGridState
+ rule.setContentWithTestViewConfiguration {
+ TvLazyVerticalGrid(
+ modifier = Modifier.size(itemSize * 3),
+ state = rememberLazyGridState().also { state = it },
+ verticalArrangement = Arrangement.spacedBy(spacingSize),
+ columns = TvGridCells.Fixed(1)
+ ) {
+ items(5) {
+ Spacer(
+ Modifier.size(itemSize).testTag("$it")
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy((itemSizePx + spacingSizePx).toFloat())
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ }
+ }
+
+ @Test
+ fun vertical_scrollingByExactlyTheItemSizePlusHalfTheSpacer_staysOnTheSameItem() {
+ val itemSizePx = 30
+ val spacingSizePx = 4
+ val itemSize = with(rule.density) { itemSizePx.toDp() }
+ val spacingSize = with(rule.density) { spacingSizePx.toDp() }
+ lateinit var state: TvLazyGridState
+ rule.setContentWithTestViewConfiguration {
+ TvLazyVerticalGrid(
+ modifier = Modifier.size(itemSize * 3),
+ state = rememberLazyGridState().also { state = it },
+ verticalArrangement = Arrangement.spacedBy(spacingSize),
+ columns = TvGridCells.Fixed(1)
+ ) {
+ items(5) {
+ Spacer(
+ Modifier.size(itemSize).testTag("$it")
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy((itemSizePx + spacingSizePx / 2).toFloat())
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ assertThat(state.firstVisibleItemScrollOffset)
+ .isEqualTo(itemSizePx + spacingSizePx / 2)
+ }
+ }
+
+ @Test
+ fun horizontal_scrollingByExactlyTheItemSizePlusSpacer_switchesTheFirstVisibleItem() {
+ val itemSizePx = 30
+ val spacingSizePx = 4
+ val itemSize = with(rule.density) { itemSizePx.toDp() }
+ val spacingSize = with(rule.density) { spacingSizePx.toDp() }
+ lateinit var state: TvLazyGridState
+ rule.setContentWithTestViewConfiguration {
+ TvLazyHorizontalGrid(
+ TvGridCells.Fixed(1),
+ Modifier.size(itemSize * 3),
+ state = rememberLazyGridState().also { state = it },
+ horizontalArrangement = Arrangement.spacedBy(spacingSize)
+ ) {
+ items(5) {
+ Spacer(
+ Modifier.size(itemSize).testTag("$it")
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy((itemSizePx + spacingSizePx).toFloat())
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ }
+ }
+
+ @Test
+ fun horizontal_scrollingByExactlyTheItemSizePlusHalfTheSpacer_staysOnTheSameItem() {
+ val itemSizePx = 30
+ val spacingSizePx = 4
+ val itemSize = with(rule.density) { itemSizePx.toDp() }
+ val spacingSize = with(rule.density) { spacingSizePx.toDp() }
+ lateinit var state: TvLazyGridState
+ rule.setContentWithTestViewConfiguration {
+ TvLazyHorizontalGrid(
+ TvGridCells.Fixed(1),
+ Modifier.size(itemSize * 3),
+ state = rememberLazyGridState().also { state = it },
+ horizontalArrangement = Arrangement.spacedBy(spacingSize)
+ ) {
+ items(5) {
+ Spacer(
+ Modifier.size(itemSize).testTag("$it")
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy((itemSizePx + spacingSizePx / 2).toFloat())
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ assertThat(state.firstVisibleItemScrollOffset)
+ .isEqualTo(itemSizePx + spacingSizePx / 2)
+ }
+ }
+
+ // with reverseLayout == true
+
+ @Test
+ fun vertical_defaultArrangementIsBottomWithReverseLayout() {
+ rule.setContent {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ reverseLayout = true,
+ modifier = Modifier.size(containerSize)
+ ) {
+ items(2) {
+ Item(it)
+ }
+ }
+ }
+
+ assertArrangementForTwoItems(Arrangement.Bottom, reverseLayout = true)
+ }
+
+ @Test
+ fun horizontal_defaultArrangementIsEndWithReverseLayout() {
+ rule.setContent {
+ TvLazyHorizontalGrid(
+ TvGridCells.Fixed(1),
+ reverseLayout = true,
+ modifier = Modifier.requiredSize(containerSize)
+ ) {
+ items(2) {
+ Item(it)
+ }
+ }
+ }
+
+ assertArrangementForTwoItems(
+ Arrangement.End, LayoutDirection.Ltr, reverseLayout = true
+ )
+ }
+
+ @Test
+ fun vertical_whenArrangementChanges() {
+ var arrangement by mutableStateOf(Arrangement.Top)
+ rule.setContent {
+ TvLazyVerticalGrid(
+ modifier = Modifier.requiredSize(containerSize),
+ verticalArrangement = arrangement,
+ columns = TvGridCells.Fixed(1)
+ ) {
+ items(2) {
+ Item(it)
+ }
+ }
+ }
+
+ assertArrangementForTwoItems(Arrangement.Top)
+
+ rule.runOnIdle {
+ arrangement = Arrangement.Bottom
+ }
+
+ assertArrangementForTwoItems(Arrangement.Bottom)
+ }
+
+ @Test
+ fun horizontal_whenArrangementChanges() {
+ var arrangement by mutableStateOf(Arrangement.Start)
+ rule.setContent {
+ TvLazyHorizontalGrid(
+ TvGridCells.Fixed(1),
+ modifier = Modifier.requiredSize(containerSize),
+ horizontalArrangement = arrangement
+ ) {
+ items(2) {
+ Item(it)
+ }
+ }
+ }
+
+ assertArrangementForTwoItems(Arrangement.Start, LayoutDirection.Ltr)
+
+ rule.runOnIdle {
+ arrangement = Arrangement.End
+ }
+
+ assertArrangementForTwoItems(Arrangement.End, LayoutDirection.Ltr)
+ }
+
+ fun composeVerticalGridWith(arrangement: Arrangement.Vertical) {
+ rule.setContent {
+ TvLazyVerticalGrid(
+ verticalArrangement = arrangement,
+ modifier = Modifier.requiredSize(containerSize),
+ columns = TvGridCells.Fixed(1)
+ ) {
+ items(2) {
+ Item(it)
+ }
+ }
+ }
+ }
+
+ fun composeHorizontalWith(
+ arrangement: Arrangement.Horizontal,
+ layoutDirection: LayoutDirection
+ ) {
+ rule.setContent {
+ CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
+ TvLazyHorizontalGrid(
+ horizontalArrangement = arrangement,
+ modifier = Modifier.requiredSize(containerSize),
+ rows = TvGridCells.Fixed(1)
+ ) {
+ items(2) {
+ Item(it)
+ }
+ }
+ }
+ }
+ }
+
+ @Composable
+ fun Item(index: Int) {
+ require(index < 2)
+ val size = if (index == 0) itemSize else smallerItemSize
+ Box(Modifier.requiredSize(size).testTag(index.toString()))
+ }
+
+ fun assertArrangementForTwoItems(
+ arrangement: Arrangement.Vertical,
+ reverseLayout: Boolean = false
+ ) {
+ with(rule.density) {
+ val sizes = IntArray(2) {
+ val index = if (reverseLayout) if (it == 0) 1 else 0 else it
+ if (index == 0) itemSize.roundToPx() else smallerItemSize.roundToPx()
+ }
+ val outPositions = IntArray(2) { 0 }
+ with(arrangement) { arrange(containerSize.roundToPx(), sizes, outPositions) }
+
+ outPositions.forEachIndexed { index, position ->
+ val realIndex = if (reverseLayout) if (index == 0) 1 else 0 else index
+ rule.onNodeWithTag("$realIndex")
+ .assertTopPositionInRootIsEqualTo(position.toDp())
+ }
+ }
+ }
+
+ fun assertArrangementForTwoItems(
+ arrangement: Arrangement.Horizontal,
+ layoutDirection: LayoutDirection,
+ reverseLayout: Boolean = false
+ ) {
+ with(rule.density) {
+ val sizes = IntArray(2) {
+ val index = if (reverseLayout) if (it == 0) 1 else 0 else it
+ if (index == 0) itemSize.roundToPx() else smallerItemSize.roundToPx()
+ }
+ val outPositions = IntArray(2) { 0 }
+ with(arrangement) {
+ arrange(containerSize.roundToPx(), sizes, layoutDirection, outPositions)
+ }
+
+ outPositions.forEachIndexed { index, position ->
+ val realIndex = if (reverseLayout) if (index == 0) 1 else 0 else index
+ val expectedPosition = position.toDp()
+ rule.onNodeWithTag("$realIndex")
+ .assertLeftPositionInRootIsEqualTo(expectedPosition)
+ }
+ }
+ }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyCustomKeysTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyCustomKeysTest.kt
new file mode 100644
index 0000000..0f444fa
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyCustomKeysTest.kt
@@ -0,0 +1,466 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class LazyCustomKeysTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ val itemSize = with(rule.density) {
+ 100.toDp()
+ }
+ val columns = 2
+
+ @Test
+ fun itemsWithKeysAreLaidOutCorrectly() {
+ val list = listOf(MyClass(0), MyClass(1), MyClass(2))
+
+ rule.setContent {
+ TvLazyVerticalGrid(TvGridCells.Fixed(columns)) {
+ items(list, key = { it.id }) {
+ Item("${it.id}")
+ }
+ }
+ }
+
+ assertItems("0", "1", "2")
+ }
+
+ @Test
+ fun removing_statesAreMoved() {
+ var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
+
+ rule.setContent {
+ TvLazyVerticalGrid(TvGridCells.Fixed(columns)) {
+ items(list, key = { it.id }) {
+ Item(remember { "${it.id}" })
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(list[0], list[2])
+ }
+
+ assertItems("0", "2")
+ }
+
+ @Test
+ fun reordering_statesAreMoved_list() {
+ testReordering { grid ->
+ items(grid, key = { it.id }) {
+ Item(remember { "${it.id}" })
+ }
+ }
+ }
+
+ @Test
+ fun reordering_statesAreMoved_list_indexed() {
+ testReordering { grid ->
+ itemsIndexed(grid, key = { _, item -> item.id }) { _, item ->
+ Item(remember { "${item.id}" })
+ }
+ }
+ }
+
+ @Test
+ fun reordering_statesAreMoved_array() {
+ testReordering { grid ->
+ val array = grid.toTypedArray()
+ items(array, key = { it.id }) {
+ Item(remember { "${it.id}" })
+ }
+ }
+ }
+
+ @Test
+ fun reordering_statesAreMoved_array_indexed() {
+ testReordering { grid ->
+ val array = grid.toTypedArray()
+ itemsIndexed(array, key = { _, item -> item.id }) { _, item ->
+ Item(remember { "${item.id}" })
+ }
+ }
+ }
+
+ @Test
+ fun reordering_statesAreMoved_itemsWithCount() {
+ testReordering { grid ->
+ items(grid.size, key = { grid[it].id }) {
+ Item(remember { "${grid[it].id}" })
+ }
+ }
+ }
+
+ @Test
+ fun fullyReplacingTheList() {
+ var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
+ var counter = 0
+
+ rule.setContent {
+ TvLazyVerticalGrid(TvGridCells.Fixed(columns)) {
+ items(list, key = { it.id }) {
+ Item(remember { counter++ }.toString())
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(MyClass(3), MyClass(4), MyClass(5), MyClass(6))
+ }
+
+ assertItems("3", "4", "5", "6")
+ }
+
+ @Test
+ fun keepingOneItem() {
+ var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
+ var counter = 0
+
+ rule.setContent {
+ TvLazyVerticalGrid(TvGridCells.Fixed(columns)) {
+ items(list, key = { it.id }) {
+ Item(remember { counter++ }.toString())
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(MyClass(1))
+ }
+
+ assertItems("1")
+ }
+
+ @Test
+ fun keepingOneItemAndAddingMore() {
+ var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
+ var counter = 0
+
+ rule.setContent {
+ TvLazyVerticalGrid(TvGridCells.Fixed(columns)) {
+ items(list, key = { it.id }) {
+ Item(remember { counter++ }.toString())
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(MyClass(1), MyClass(3))
+ }
+
+ assertItems("1", "3")
+ }
+
+ @Test
+ fun mixingKeyedItemsAndNot() {
+ testReordering { list ->
+ item {
+ Item("${list.first().id}")
+ }
+ items(list.subList(fromIndex = 1, toIndex = list.size), key = { it.id }) {
+ Item(remember { "${it.id}" })
+ }
+ }
+ }
+
+ @Test
+ fun updatingTheDataSetIsCorrectlyApplied() {
+ val state = mutableStateOf(emptyList<Int>())
+
+ rule.setContent {
+ LaunchedEffect(Unit) {
+ state.value = listOf(4, 1, 3)
+ }
+
+ val list = state.value
+
+ TvLazyVerticalGrid(TvGridCells.Fixed(columns), Modifier.fillMaxSize()) {
+ items(list, key = { it }) {
+ Item(it.toString())
+ }
+ }
+ }
+
+ assertItems("4", "1", "3")
+
+ rule.runOnIdle {
+ state.value = listOf(2, 4, 6, 1, 3, 5)
+ }
+
+ assertItems("2", "4", "6", "1", "3", "5")
+ }
+
+ @Test
+ fun reordering_usingMutableStateListOf() {
+ val list = mutableStateListOf(MyClass(0), MyClass(1), MyClass(2))
+
+ rule.setContent {
+ TvLazyVerticalGrid(TvGridCells.Fixed(columns)) {
+ items(list, key = { it.id }) {
+ Item(remember { "${it.id}" })
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list.add(list.removeAt(1))
+ }
+
+ assertItems("0", "2", "1")
+ }
+
+ @Test
+ fun keysInLazyListItemInfoAreCorrect() {
+ val list = listOf(MyClass(0), MyClass(1), MyClass(2))
+ lateinit var state: TvLazyGridState
+
+ rule.setContent {
+ state = rememberLazyGridState()
+ TvLazyVerticalGrid(TvGridCells.Fixed(columns), state = state) {
+ items(list, key = { it.id }) {
+ Item(remember { "${it.id}" })
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(
+ state.visibleKeys
+ ).isEqualTo(listOf(0, 1, 2))
+ }
+ }
+
+ @Test
+ fun keysInLazyListItemInfoAreCorrectAfterReordering() {
+ var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
+ lateinit var state: TvLazyGridState
+
+ rule.setContent {
+ state = rememberLazyGridState()
+ TvLazyVerticalGrid(columns = TvGridCells.Fixed(columns), state = state) {
+ items(list, key = { it.id }) {
+ Item(remember { "${it.id}" })
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(list[0], list[2], list[1])
+ }
+
+ rule.runOnIdle {
+ assertThat(
+ state.visibleKeys
+ ).isEqualTo(listOf(0, 2, 1))
+ }
+ }
+
+ @Test
+ fun addingItemsBeforeWithoutKeysIsMaintainingTheIndex() {
+ var list by mutableStateOf((10..15).toList())
+ lateinit var state: TvLazyGridState
+
+ rule.setContent {
+ state = rememberLazyGridState()
+ TvLazyVerticalGrid(TvGridCells.Fixed(columns), Modifier.size(itemSize * 2.5f), state) {
+ items(list) {
+ Item(remember { "$it" })
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = (0..15).toList()
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ }
+ }
+
+ @Test
+ fun addingItemsBeforeKeepingThisItemFirst() {
+ var list by mutableStateOf((10..15).toList())
+ lateinit var state: TvLazyGridState
+
+ rule.setContent {
+ state = rememberLazyGridState()
+ TvLazyVerticalGrid(TvGridCells.Fixed(columns), Modifier.size(itemSize * 2.5f), state) {
+ items(list, key = { it }) {
+ Item(remember { "$it" })
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = (0..15).toList()
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(10)
+ assertThat(
+ state.visibleKeys
+ ).isEqualTo(listOf(10, 11, 12, 13, 14, 15))
+ }
+ }
+
+ @Test
+ fun addingItemsRightAfterKeepingThisItemFirst() {
+ var list by mutableStateOf((0..5).toList() + (10..15).toList())
+ lateinit var state: TvLazyGridState
+
+ rule.setContent {
+ state = rememberLazyGridState(5)
+ TvLazyVerticalGrid(TvGridCells.Fixed(columns), Modifier.size(itemSize * 2.5f), state) {
+ items(list, key = { it }) {
+ Item(remember { "$it" })
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = (0..15).toList()
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(4)
+ assertThat(
+ state.visibleKeys
+ ).isEqualTo(listOf(4, 5, 6, 7, 8, 9))
+ }
+ }
+
+ @Test
+ fun addingItemsBeforeWhileCurrentItemIsNotInTheBeginning() {
+ var list by mutableStateOf((10..30).toList())
+ lateinit var state: TvLazyGridState
+
+ rule.setContent {
+ state = rememberLazyGridState(10) // key 20 is the first item
+ TvLazyVerticalGrid(TvGridCells.Fixed(columns), Modifier.size(itemSize * 2.5f), state) {
+ items(list, key = { it }) {
+ Item(remember { "$it" })
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = (0..30).toList()
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(20)
+ assertThat(
+ state.visibleKeys
+ ).isEqualTo(listOf(20, 21, 22, 23, 24, 25))
+ }
+ }
+
+ @Test
+ fun removingTheCurrentItemMaintainsTheIndex() {
+ var list by mutableStateOf((0..20).toList())
+ lateinit var state: TvLazyGridState
+
+ rule.setContent {
+ state = rememberLazyGridState(8)
+ TvLazyVerticalGrid(TvGridCells.Fixed(columns), Modifier.size(itemSize * 2.5f), state) {
+ items(list, key = { it }) {
+ Item(remember { "$it" })
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = (0..20) - 8
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(8)
+ assertThat(state.visibleKeys).isEqualTo(listOf(9, 10, 11, 12, 13, 14))
+ }
+ }
+
+ private fun testReordering(content: TvLazyGridScope.(List<MyClass>) -> Unit) {
+ var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
+
+ rule.setContent {
+ TvLazyVerticalGrid(TvGridCells.Fixed(columns)) {
+ content(list)
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(list[0], list[2], list[1])
+ }
+
+ assertItems("0", "2", "1")
+ }
+
+ private fun assertItems(vararg tags: String) {
+ var currentTop = 0.dp
+ var column = 0
+ tags.forEach {
+ rule.onNodeWithTag(it)
+ .assertTopPositionInRootIsEqualTo(currentTop)
+ .assertHeightIsEqualTo(itemSize)
+ ++column
+ if (column == columns) {
+ currentTop += itemSize
+ column = 0
+ }
+ }
+ }
+
+ @Composable
+ private fun Item(tag: String) {
+ Spacer(
+ Modifier.testTag(tag).size(itemSize)
+ )
+ }
+
+ private class MyClass(val id: Int)
+}
+
+val TvLazyGridState.visibleKeys: List<Any> get() = layoutInfo.visibleItemsInfo.map { it.key }
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
new file mode 100644
index 0000000..d966436
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
@@ -0,0 +1,1348 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.requiredHeightIn
+import androidx.compose.foundation.layout.requiredWidth
+import androidx.compose.foundation.layout.requiredWidthIn
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.getBoundsInRoot
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.height
+import androidx.compose.ui.unit.isSpecified
+import androidx.compose.ui.unit.width
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runners.Parameterized
+import kotlin.math.roundToInt
+import kotlinx.coroutines.runBlocking
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalFoundationApi::class)
+@LargeTest
+@RunWith(Parameterized::class)
+class LazyGridAnimateItemPlacementTest(private val config: Config) {
+
+ private val isVertical: Boolean get() = config.isVertical
+ private val reverseLayout: Boolean get() = config.reverseLayout
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ private val itemSize: Int = 50
+ private var itemSizeDp: Dp = Dp.Infinity
+ private val itemSize2: Int = 30
+ private var itemSize2Dp: Dp = Dp.Infinity
+ private val itemSize3: Int = 20
+ private var itemSize3Dp: Dp = Dp.Infinity
+ private val containerSize: Int = itemSize * 5
+ private var containerSizeDp: Dp = Dp.Infinity
+ private val spacing: Int = 10
+ private var spacingDp: Dp = Dp.Infinity
+ private val itemSizePlusSpacing = itemSize + spacing
+ private var itemSizePlusSpacingDp = Dp.Infinity
+ private lateinit var state: TvLazyGridState
+
+ @Before
+ fun before() {
+ rule.mainClock.autoAdvance = false
+ with(rule.density) {
+ itemSizeDp = itemSize.toDp()
+ itemSize2Dp = itemSize2.toDp()
+ itemSize3Dp = itemSize3.toDp()
+ containerSizeDp = containerSize.toDp()
+ spacingDp = spacing.toDp()
+ itemSizePlusSpacingDp = itemSizePlusSpacing.toDp()
+ }
+ }
+
+ @Test
+ fun reorderTwoItems() {
+ var list by mutableStateOf(listOf(0, 1))
+ rule.setContent {
+ LazyGrid(1) {
+ items(list, key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ assertPositions(
+ 0 to AxisIntOffset(0, 0),
+ 1 to AxisIntOffset(0, itemSize)
+ )
+
+ rule.runOnIdle {
+ list = listOf(1, 0)
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 0 to AxisIntOffset(0, 0 + (itemSize * fraction).roundToInt()),
+ 1 to AxisIntOffset(0, itemSize - (itemSize * fraction).roundToInt()),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun reorderTwoByTwoItems() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3))
+ rule.setContent {
+ LazyGrid(2) {
+ items(list, key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ assertPositions(
+ 0 to AxisIntOffset(0, 0),
+ 1 to AxisIntOffset(itemSize, 0),
+ 2 to AxisIntOffset(0, itemSize),
+ 3 to AxisIntOffset(itemSize, itemSize)
+ )
+
+ rule.runOnIdle {
+ list = listOf(3, 2, 1, 0)
+ }
+
+ onAnimationFrame { fraction ->
+ val increasing = 0 + (itemSize * fraction).roundToInt()
+ val decreasing = itemSize - (itemSize * fraction).roundToInt()
+ assertPositions(
+ 0 to AxisIntOffset(increasing, increasing),
+ 1 to AxisIntOffset(decreasing, increasing),
+ 2 to AxisIntOffset(increasing, decreasing),
+ 3 to AxisIntOffset(decreasing, decreasing),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun reorderTwoItems_layoutInfoHasFinalPositions() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3))
+ rule.setContent {
+ LazyGrid(2) {
+ items(list, key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ assertLayoutInfoPositions(
+ 0 to AxisIntOffset(0, 0),
+ 1 to AxisIntOffset(itemSize, 0),
+ 2 to AxisIntOffset(0, itemSize),
+ 3 to AxisIntOffset(itemSize, itemSize)
+ )
+
+ rule.runOnIdle {
+ list = listOf(3, 2, 1, 0)
+ }
+
+ onAnimationFrame {
+ // fraction doesn't affect the offsets in layout info
+ assertLayoutInfoPositions(
+ 3 to AxisIntOffset(0, 0),
+ 2 to AxisIntOffset(itemSize, 0),
+ 1 to AxisIntOffset(0, itemSize),
+ 0 to AxisIntOffset(itemSize, itemSize)
+ )
+ }
+ }
+
+ @Test
+ fun reorderFirstAndLastItems() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+ rule.setContent {
+ LazyGrid(1) {
+ items(list, key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ assertPositions(
+ 0 to AxisIntOffset(0, 0),
+ 1 to AxisIntOffset(0, itemSize),
+ 2 to AxisIntOffset(0, itemSize * 2),
+ 3 to AxisIntOffset(0, itemSize * 3),
+ 4 to AxisIntOffset(0, itemSize * 4)
+ )
+
+ rule.runOnIdle {
+ list = listOf(4, 1, 2, 3, 0)
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 0 to AxisIntOffset(0, 0 + (itemSize * 4 * fraction).roundToInt()),
+ 1 to AxisIntOffset(0, itemSize),
+ 2 to AxisIntOffset(0, itemSize * 2),
+ 3 to AxisIntOffset(0, itemSize * 3),
+ 4 to AxisIntOffset(0, itemSize * 4 - (itemSize * 4 * fraction).roundToInt()),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun moveFirstItemToEndCausingAllItemsToAnimate() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5))
+ rule.setContent {
+ LazyGrid(2) {
+ items(list, key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ assertPositions(
+ 0 to AxisIntOffset(0, 0),
+ 1 to AxisIntOffset(itemSize, 0),
+ 2 to AxisIntOffset(0, itemSize),
+ 3 to AxisIntOffset(itemSize, itemSize),
+ 4 to AxisIntOffset(0, itemSize * 2),
+ 5 to AxisIntOffset(itemSize, itemSize * 2)
+ )
+
+ rule.runOnIdle {
+ list = listOf(1, 2, 3, 4, 5, 0)
+ }
+
+ onAnimationFrame { fraction ->
+ val increasingX = 0 + (itemSize * fraction).roundToInt()
+ val decreasingX = itemSize - (itemSize * fraction).roundToInt()
+ assertPositions(
+ 0 to AxisIntOffset(increasingX, 0 + (itemSize * 2 * fraction).roundToInt()),
+ 1 to AxisIntOffset(decreasingX, 0),
+ 2 to AxisIntOffset(increasingX, itemSize - (itemSize * fraction).roundToInt()),
+ 3 to AxisIntOffset(decreasingX, itemSize),
+ 4 to AxisIntOffset(increasingX, itemSize * 2 - (itemSize * fraction).roundToInt()),
+ 5 to AxisIntOffset(decreasingX, itemSize * 2),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun itemSizeChangeAnimatesNextItems() {
+ var height by mutableStateOf(itemSizeDp)
+ rule.setContent {
+ LazyGrid(1, minSize = itemSizeDp * 5, maxSize = itemSizeDp * 5) {
+ items(listOf(0, 1, 2, 3), key = { it }) {
+ Item(it, height = if (it == 1) height else itemSizeDp)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ height = itemSizeDp * 2
+ }
+ rule.mainClock.advanceTimeByFrame()
+
+ rule.onNodeWithTag("1")
+ .assertMainAxisSizeIsEqualTo(height)
+
+ onAnimationFrame { fraction ->
+ if (!reverseLayout) {
+ assertPositions(
+ 0 to AxisIntOffset(0, 0),
+ 1 to AxisIntOffset(0, itemSize),
+ 2 to AxisIntOffset(0, itemSize * 2 + (itemSize * fraction).roundToInt()),
+ 3 to AxisIntOffset(0, itemSize * 3 + (itemSize * fraction).roundToInt()),
+ fraction = fraction,
+ autoReverse = false
+ )
+ } else {
+ assertPositions(
+ 3 to AxisIntOffset(0, itemSize - (itemSize * fraction).roundToInt()),
+ 2 to AxisIntOffset(0, itemSize * 2 - (itemSize * fraction).roundToInt()),
+ 1 to AxisIntOffset(0, itemSize * 3 - (itemSize * fraction).roundToInt()),
+ 0 to AxisIntOffset(0, itemSize * 4),
+ fraction = fraction,
+ autoReverse = false
+ )
+ }
+ }
+ }
+
+ @Test
+ fun onlyItemsWithModifierAnimates() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+ rule.setContent {
+ LazyGrid(1) {
+ items(list, key = { it }) {
+ Item(it, animSpec = if (it == 1 || it == 3) AnimSpec else null)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(1, 2, 3, 4, 0)
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 0 to AxisIntOffset(0, itemSize * 4),
+ 1 to AxisIntOffset(0, itemSize - (itemSize * fraction).roundToInt()),
+ 2 to AxisIntOffset(0, itemSize),
+ 3 to AxisIntOffset(0, itemSize * 3 - (itemSize * fraction).roundToInt()),
+ 4 to AxisIntOffset(0, itemSize * 3),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun animationsWithDifferentDurations() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+ rule.setContent {
+ LazyGrid(1) {
+ items(list, key = { it }) {
+ val duration = if (it == 1 || it == 3) Duration * 2 else Duration
+ Item(it, animSpec = tween(duration.toInt(), easing = LinearEasing))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(1, 2, 3, 4, 0)
+ }
+
+ onAnimationFrame(duration = Duration * 2) { fraction ->
+ val shorterAnimFraction = (fraction * 2).coerceAtMost(1f)
+ assertPositions(
+ 0 to AxisIntOffset(0, 0 + (itemSize * 4 * shorterAnimFraction).roundToInt()),
+ 1 to AxisIntOffset(0, itemSize - (itemSize * fraction).roundToInt()),
+ 2 to AxisIntOffset(0, itemSize * 2 - (itemSize * shorterAnimFraction).roundToInt()),
+ 3 to AxisIntOffset(0, itemSize * 3 - (itemSize * fraction).roundToInt()),
+ 4 to AxisIntOffset(0, itemSize * 4 - (itemSize * shorterAnimFraction).roundToInt()),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun multipleChildrenPerItem() {
+ var list by mutableStateOf(listOf(0, 2))
+ rule.setContent {
+ LazyGrid(1) {
+ items(list, key = { it }) {
+ Item(it)
+ Item(it + 1)
+ }
+ }
+ }
+
+ assertPositions(
+ 0 to AxisIntOffset(0, 0),
+ 1 to AxisIntOffset(0, 0),
+ 2 to AxisIntOffset(0, itemSize),
+ 3 to AxisIntOffset(0, itemSize)
+ )
+
+ rule.runOnIdle {
+ list = listOf(2, 0)
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 0 to AxisIntOffset(0, 0 + (itemSize * fraction).roundToInt()),
+ 1 to AxisIntOffset(0, 0 + (itemSize * fraction).roundToInt()),
+ 2 to AxisIntOffset(0, itemSize - (itemSize * fraction).roundToInt()),
+ 3 to AxisIntOffset(0, itemSize - (itemSize * fraction).roundToInt()),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun multipleChildrenPerItemSomeDoNotAnimate() {
+ var list by mutableStateOf(listOf(0, 2))
+ rule.setContent {
+ LazyGrid(1) {
+ items(list, key = { it }) {
+ Item(it)
+ Item(it + 1, animSpec = null)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(2, 0)
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 0 to AxisIntOffset(0, 0 + (itemSize * fraction).roundToInt()),
+ 1 to AxisIntOffset(0, itemSize),
+ 2 to AxisIntOffset(0, itemSize - (itemSize * fraction).roundToInt()),
+ 3 to AxisIntOffset(0, 0),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun animateArrangementChange() {
+ var arrangement by mutableStateOf(Arrangement.Center)
+ rule.setContent {
+ LazyGrid(
+ 1,
+ arrangement = arrangement,
+ minSize = itemSizeDp * 5,
+ maxSize = itemSizeDp * 5
+ ) {
+ items(listOf(1, 2, 3), key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ assertPositions(
+ 1 to AxisIntOffset(0, itemSize),
+ 2 to AxisIntOffset(0, itemSize * 2),
+ 3 to AxisIntOffset(0, itemSize * 3),
+ )
+
+ rule.runOnIdle {
+ arrangement = Arrangement.SpaceBetween
+ }
+ rule.mainClock.advanceTimeByFrame()
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 1 to AxisIntOffset(0, itemSize - (itemSize * fraction).roundToInt()),
+ 2 to AxisIntOffset(0, itemSize * 2),
+ 3 to AxisIntOffset(0, itemSize * 3 + (itemSize * fraction).roundToInt()),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun moveItemToTheBottomOutsideOfBounds() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11))
+ rule.setContent {
+ LazyGrid(2, maxSize = itemSizeDp * 3) {
+ items(list, key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ assertPositions(
+ 0 to AxisIntOffset(0, 0),
+ 1 to AxisIntOffset(itemSize, 0),
+ 2 to AxisIntOffset(0, itemSize),
+ 3 to AxisIntOffset(itemSize, itemSize),
+ 4 to AxisIntOffset(0, itemSize * 2),
+ 5 to AxisIntOffset(itemSize, itemSize * 2)
+ )
+
+ rule.runOnIdle {
+ list = listOf(0, 8, 2, 3, 4, 5, 6, 7, 1, 9, 10, 11)
+ }
+
+ onAnimationFrame { fraction ->
+ val item1Offset = AxisIntOffset(itemSize, 0 + (itemSize * 4 * fraction).roundToInt())
+ val item8Offset =
+ AxisIntOffset(itemSize, itemSize * 4 - (itemSize * 4 * fraction).roundToInt())
+ val expected = mutableListOf<Pair<Any, IntOffset>>().apply {
+ add(0 to AxisIntOffset(0, 0))
+ if (item1Offset.mainAxis < itemSize * 3) {
+ add(1 to item1Offset)
+ } else {
+ rule.onNodeWithTag("1").assertIsNotDisplayed()
+ }
+ add(2 to AxisIntOffset(0, itemSize))
+ add(3 to AxisIntOffset(itemSize, itemSize))
+ add(4 to AxisIntOffset(0, itemSize * 2))
+ add(5 to AxisIntOffset(itemSize, itemSize * 2))
+ if (item8Offset.mainAxis < itemSize * 3) {
+ add(8 to item8Offset)
+ } else {
+ rule.onNodeWithTag("8").assertIsNotDisplayed()
+ }
+ }
+ assertPositions(
+ expected = expected.toTypedArray(),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun moveItemToTheTopOutsideOfBounds() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11))
+ rule.setContent {
+ LazyGrid(2, maxSize = itemSizeDp * 3, startIndex = 6) {
+ items(list, key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ assertPositions(
+ 6 to AxisIntOffset(0, 0),
+ 7 to AxisIntOffset(itemSize, 0),
+ 8 to AxisIntOffset(0, itemSize),
+ 9 to AxisIntOffset(itemSize, itemSize),
+ 10 to AxisIntOffset(0, itemSize * 2),
+ 11 to AxisIntOffset(itemSize, itemSize * 2)
+ )
+
+ rule.runOnIdle {
+ list = listOf(0, 8, 2, 3, 4, 5, 6, 7, 1, 9, 10, 11)
+ }
+
+ onAnimationFrame { fraction ->
+ val item8Offset = AxisIntOffset(0, itemSize - (itemSize * 4 * fraction).roundToInt())
+ val item1Offset = AxisIntOffset(
+ 0,
+ itemSize * -3 + (itemSize * 4 * fraction).roundToInt()
+ )
+ val expected = mutableListOf<Pair<Any, IntOffset>>().apply {
+ if (item1Offset.mainAxis > -itemSize) {
+ add(1 to item1Offset)
+ } else {
+ rule.onNodeWithTag("1").assertIsNotDisplayed()
+ }
+ add(6 to AxisIntOffset(0, 0))
+ add(7 to AxisIntOffset(itemSize, 0))
+ if (item8Offset.mainAxis > -itemSize) {
+ add(8 to item8Offset)
+ } else {
+ rule.onNodeWithTag("8").assertIsNotDisplayed()
+ }
+ add(9 to AxisIntOffset(itemSize, itemSize))
+ add(10 to AxisIntOffset(0, itemSize * 2))
+ add(11 to AxisIntOffset(itemSize, itemSize * 2))
+ }
+ assertPositions(
+ expected = expected.toTypedArray(),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun moveFirstItemToEndCausingAllItemsToAnimate_withSpacing() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7))
+ rule.setContent {
+ LazyGrid(2, arrangement = Arrangement.spacedBy(spacingDp)) {
+ items(list, key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(1, 2, 3, 4, 5, 6, 7, 0)
+ }
+
+ onAnimationFrame { fraction ->
+ val increasingX = (fraction * itemSize).roundToInt()
+ val decreasingX = (itemSize - itemSize * fraction).roundToInt()
+ assertPositions(
+ 0 to AxisIntOffset(increasingX, (itemSizePlusSpacing * 3 * fraction).roundToInt()),
+ 1 to AxisIntOffset(decreasingX, 0),
+ 2 to AxisIntOffset(
+ increasingX,
+ itemSizePlusSpacing - (itemSizePlusSpacing * fraction).roundToInt()
+ ),
+ 3 to AxisIntOffset(decreasingX, itemSizePlusSpacing),
+ 4 to AxisIntOffset(
+ increasingX,
+ itemSizePlusSpacing * 2 - (itemSizePlusSpacing * fraction).roundToInt()
+ ),
+ 5 to AxisIntOffset(decreasingX, itemSizePlusSpacing * 2),
+ 6 to AxisIntOffset(
+ increasingX,
+ itemSizePlusSpacing * 3 - (itemSizePlusSpacing * fraction).roundToInt()
+ ),
+ 7 to AxisIntOffset(decreasingX, itemSizePlusSpacing * 3),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun moveItemToTheBottomOutsideOfBounds_withSpacing() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9))
+ rule.setContent {
+ LazyGrid(
+ 2,
+ maxSize = itemSizeDp * 3 + spacingDp * 2,
+ arrangement = Arrangement.spacedBy(spacingDp)
+ ) {
+ items(list, key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ assertPositions(
+ 0 to AxisIntOffset(0, 0),
+ 1 to AxisIntOffset(itemSize, 0),
+ 2 to AxisIntOffset(0, itemSizePlusSpacing),
+ 3 to AxisIntOffset(itemSize, itemSizePlusSpacing),
+ 4 to AxisIntOffset(0, itemSizePlusSpacing * 2),
+ 5 to AxisIntOffset(itemSize, itemSizePlusSpacing * 2)
+ )
+
+ rule.runOnIdle {
+ list = listOf(0, 8, 2, 3, 4, 5, 6, 7, 1, 9)
+ }
+
+ onAnimationFrame { fraction ->
+ val item1Offset = AxisIntOffset(
+ itemSize,
+ (itemSizePlusSpacing * 4 * fraction).roundToInt()
+ )
+ val item8Offset = AxisIntOffset(
+ itemSize,
+ itemSizePlusSpacing * 4 - (itemSizePlusSpacing * 4 * fraction).roundToInt()
+ )
+ val screenSize = itemSize * 3 + spacing * 2
+ val expected = mutableListOf<Pair<Any, IntOffset>>().apply {
+ add(0 to AxisIntOffset(0, 0))
+ if (item1Offset.mainAxis < screenSize) {
+ add(1 to item1Offset)
+ }
+ add(2 to AxisIntOffset(0, itemSizePlusSpacing))
+ add(3 to AxisIntOffset(itemSize, itemSizePlusSpacing))
+ add(4 to AxisIntOffset(0, itemSizePlusSpacing * 2))
+ add(5 to AxisIntOffset(itemSize, itemSizePlusSpacing * 2))
+ if (item8Offset.mainAxis < screenSize) {
+ add(8 to item8Offset)
+ }
+ }
+ assertPositions(
+ expected = expected.toTypedArray(),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun moveItemToTheTopOutsideOfBounds_withSpacing() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11))
+ rule.setContent {
+ LazyGrid(
+ 2,
+ maxSize = itemSizeDp * 3 + spacingDp * 2,
+ arrangement = Arrangement.spacedBy(spacingDp),
+ startIndex = 4
+ ) {
+ items(list, key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ assertPositions(
+ 4 to AxisIntOffset(0, 0),
+ 5 to AxisIntOffset(itemSize, 0),
+ 6 to AxisIntOffset(0, itemSizePlusSpacing),
+ 7 to AxisIntOffset(itemSize, itemSizePlusSpacing),
+ 8 to AxisIntOffset(0, itemSizePlusSpacing * 2),
+ 9 to AxisIntOffset(itemSize, itemSizePlusSpacing * 2)
+ )
+
+ rule.runOnIdle {
+ list = listOf(0, 8, 2, 3, 4, 5, 6, 7, 1, 9, 10, 11)
+ }
+
+ onAnimationFrame { fraction ->
+ val item1Offset = AxisIntOffset(
+ 0,
+ itemSizePlusSpacing * -2 + (itemSizePlusSpacing * 4 * fraction).roundToInt()
+ )
+ val item8Offset = AxisIntOffset(
+ 0,
+ itemSizePlusSpacing * 2 - (itemSizePlusSpacing * 4 * fraction).roundToInt()
+ )
+ val expected = mutableListOf<Pair<Any, IntOffset>>().apply {
+ if (item1Offset.mainAxis > -itemSize) {
+ add(1 to item1Offset)
+ }
+ add(4 to AxisIntOffset(0, 0))
+ add(5 to AxisIntOffset(itemSize, 0))
+ add(6 to AxisIntOffset(0, itemSizePlusSpacing))
+ add(7 to AxisIntOffset(itemSize, itemSizePlusSpacing))
+ if (item8Offset.mainAxis > -itemSize) {
+ add(8 to item8Offset)
+ }
+ add(9 to AxisIntOffset(itemSize, itemSizePlusSpacing * 2))
+ }
+ assertPositions(
+ expected = expected.toTypedArray(),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun moveItemToTheTopOutsideOfBounds_differentSizes() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11))
+ rule.setContent {
+ LazyGrid(2, maxSize = itemSize2Dp + itemSize3Dp + itemSizeDp, startIndex = 6) {
+ items(list, key = { it }) {
+ val height = when (it) {
+ 2 -> itemSize3Dp
+ 3 -> itemSize3Dp / 2
+ 6 -> itemSize2Dp
+ 7 -> itemSize2Dp / 2
+ else -> {
+ if (it % 2 == 0) itemSizeDp else itemSize3Dp / 2
+ }
+ }
+ Item(it, height = height)
+ }
+ }
+ }
+
+ val line3Size = itemSize2
+ val line4Size = itemSize
+ assertPositions(
+ 6 to AxisIntOffset(0, 0),
+ 7 to AxisIntOffset(itemSize, 0),
+ 8 to AxisIntOffset(0, line3Size),
+ 9 to AxisIntOffset(itemSize, line3Size),
+ 10 to AxisIntOffset(0, line3Size + line4Size),
+ 11 to AxisIntOffset(itemSize, line3Size + line4Size)
+ )
+
+ rule.runOnIdle {
+ // swap 8 and 2
+ list = listOf(0, 1, 8, 3, 4, 5, 6, 7, 2, 9, 10, 11)
+ }
+
+ onAnimationFrame { fraction ->
+ rule.onNodeWithTag("4").assertDoesNotExist()
+ rule.onNodeWithTag("5").assertDoesNotExist()
+ // items 4,5 were between lines 1 and 3 but we don't compose them and don't know the
+ // real size, so we use an average size.
+ val line2Size = (itemSize + itemSize2 + itemSize3) / 3
+ val line1Size = itemSize3 /* the real size of the item 2 */
+ val startItem2Offset = -line1Size - line2Size
+ val item2Offset =
+ startItem2Offset + ((itemSize2 - startItem2Offset) * fraction).roundToInt()
+ val endItem8Offset = -line2Size - itemSize
+ val item8Offset = line3Size - ((line3Size - endItem8Offset) * fraction).roundToInt()
+ val expected = mutableListOf<Pair<Any, IntOffset>>().apply {
+ if (item8Offset > -line4Size) {
+ add(8 to AxisIntOffset(0, item8Offset))
+ } else {
+ rule.onNodeWithTag("8").assertIsNotDisplayed()
+ }
+ add(6 to AxisIntOffset(0, 0))
+ add(7 to AxisIntOffset(itemSize, 0))
+ if (item2Offset > -line1Size) {
+ add(2 to AxisIntOffset(0, item2Offset))
+ } else {
+ rule.onNodeWithTag("2").assertIsNotDisplayed()
+ }
+ add(9 to AxisIntOffset(itemSize, line3Size))
+ add(10 to AxisIntOffset(
+ 0,
+ line3Size + line4Size - ((itemSize - itemSize3) * fraction).roundToInt()
+ ))
+ add(11 to AxisIntOffset(
+ itemSize,
+ line3Size + line4Size - ((itemSize - itemSize3) * fraction).roundToInt()
+ ))
+ }
+ assertPositions(
+ expected = expected.toTypedArray(),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun moveItemToTheBottomOutsideOfBounds_differentSizes() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11))
+ val gridSize = itemSize2 + itemSize3 + itemSize - 1
+ val gridSizeDp = with(rule.density) { gridSize.toDp() }
+ rule.setContent {
+ LazyGrid(2, maxSize = gridSizeDp) {
+ items(list, key = { it }) {
+ val height = when (it) {
+ 0 -> itemSize2Dp
+ 8 -> itemSize3Dp
+ else -> {
+ if (it % 2 == 0) itemSizeDp else itemSize3Dp / 2
+ }
+ }
+ Item(it, height = height)
+ }
+ }
+ }
+
+ val line0Size = itemSize2
+ val line1Size = itemSize
+ assertPositions(
+ 0 to AxisIntOffset(0, 0),
+ 1 to AxisIntOffset(itemSize, 0),
+ 2 to AxisIntOffset(0, line0Size),
+ 3 to AxisIntOffset(itemSize, line0Size),
+ 4 to AxisIntOffset(0, line0Size + line1Size),
+ 5 to AxisIntOffset(itemSize, line0Size + line1Size),
+ )
+
+ rule.runOnIdle {
+ list = listOf(0, 1, 8, 3, 4, 5, 6, 7, 2, 9, 10, 11)
+ }
+
+ onAnimationFrame { fraction ->
+ val line2Size = itemSize
+ val line4Size = itemSize3
+ // line 3 was between 2 and 4 but we don't compose it and don't know the real size,
+ // so we use an average size.
+ val line3Size = (itemSize + itemSize2 + itemSize3) / 3
+ val startItem8Offset = line0Size + line1Size + line2Size + line3Size
+ val endItem2Offset = line0Size + line4Size + line2Size + line3Size
+ val item2Offset =
+ line0Size + ((endItem2Offset - line0Size) * fraction).roundToInt()
+ val item8Offset =
+ startItem8Offset - ((startItem8Offset - line0Size) * fraction).roundToInt()
+ val expected = mutableListOf<Pair<Any, IntOffset>>().apply {
+ add(0 to AxisIntOffset(0, 0))
+ add(1 to AxisIntOffset(itemSize, 0))
+ if (item8Offset < gridSize) {
+ add(8 to AxisIntOffset(0, item8Offset))
+ } else {
+ // rule.onNodeWithTag("8").assertIsNotDisplayed()
+ }
+ add(3 to AxisIntOffset(itemSize, line0Size))
+ add(4 to AxisIntOffset(
+ 0,
+ line0Size + line1Size - ((line1Size - line4Size) * fraction).roundToInt()
+ ))
+ add(5 to AxisIntOffset(
+ itemSize,
+ line0Size + line1Size - ((line1Size - line4Size) * fraction).roundToInt()
+ ))
+ if (item2Offset < gridSize) {
+ add(2 to AxisIntOffset(0, item2Offset))
+ } else {
+ // rule.onNodeWithTag("2").assertIsNotDisplayed()
+ }
+ }
+ assertPositions(
+ expected = expected.toTypedArray(),
+ fraction = fraction
+ )
+ }
+ }
+
+ // @Test
+ // fun animateAlignmentChange() {
+ // var alignment by mutableStateOf(CrossAxisAlignment.End)
+ // rule.setContent {
+ // LazyList(
+ // crossAxisAlignment = alignment,
+ // crossAxisSize = itemSizeDp
+ // ) {
+ // items(listOf(1, 2, 3), key = { it }) {
+ // val crossAxisSize =
+ // if (it == 1) itemSizeDp else if (it == 2) itemSize2Dp else itemSize3Dp
+ // Item(it, crossAxisSize = crossAxisSize)
+ // }
+ // }
+ // }
+
+ // val item2Start = itemSize - itemSize2
+ // val item3Start = itemSize - itemSize3
+ // assertPositions(
+ // 1 to 0,
+ // 2 to itemSize,
+ // 3 to itemSize * 2,
+ // crossAxis = listOf(
+ // 1 to 0,
+ // 2 to item2Start,
+ // 3 to item3Start,
+ // )
+ // )
+
+ // rule.runOnIdle {
+ // alignment = CrossAxisAlignment.Center
+ // }
+ // rule.mainClock.advanceTimeByFrame()
+
+ // val item2End = itemSize / 2 - itemSize2 / 2
+ // val item3End = itemSize / 2 - itemSize3 / 2
+ // onAnimationFrame { fraction ->
+ // assertPositions(
+ // 1 to 0,
+ // 2 to itemSize,
+ // 3 to itemSize * 2,
+ // crossAxis = listOf(
+ // 1 to 0,
+ // 2 to item2Start + ((item2End - item2Start) * fraction).roundToInt(),
+ // 3 to item3Start + ((item3End - item3Start) * fraction).roundToInt(),
+ // ),
+ // fraction = fraction
+ // )
+ // }
+ // }
+
+ // @Test
+ // fun animateAlignmentChange_multipleChildrenPerItem() {
+ // var alignment by mutableStateOf(CrossAxisAlignment.Start)
+ // rule.setContent {
+ // LazyList(
+ // crossAxisAlignment = alignment,
+ // crossAxisSize = itemSizeDp * 2
+ // ) {
+ // items(1) {
+ // listOf(1, 2, 3).forEach {
+ // val crossAxisSize =
+ // if (it == 1) itemSizeDp else if (it == 2) itemSize2Dp else itemSize3Dp
+ // Item(it, crossAxisSize = crossAxisSize)
+ // }
+ // }
+ // }
+ // }
+
+ // rule.runOnIdle {
+ // alignment = CrossAxisAlignment.End
+ // }
+ // rule.mainClock.advanceTimeByFrame()
+
+ // val containerSize = itemSize * 2
+ // onAnimationFrame { fraction ->
+ // assertPositions(
+ // 1 to 0,
+ // 2 to itemSize,
+ // 3 to itemSize * 2,
+ // crossAxis = listOf(
+ // 1 to ((containerSize - itemSize) * fraction).roundToInt(),
+ // 2 to ((containerSize - itemSize2) * fraction).roundToInt(),
+ // 3 to ((containerSize - itemSize3) * fraction).roundToInt()
+ // ),
+ // fraction = fraction
+ // )
+ // }
+ // }
+
+ // @Test
+ // fun animateAlignmentChange_rtl() {
+ // // this test is not applicable to LazyRow
+ // assumeTrue(isVertical)
+
+ // var alignment by mutableStateOf(CrossAxisAlignment.End)
+ // rule.setContent {
+ // CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+ // LazyList(
+ // crossAxisAlignment = alignment,
+ // crossAxisSize = itemSizeDp
+ // ) {
+ // items(listOf(1, 2, 3), key = { it }) {
+ // val crossAxisSize =
+ // if (it == 1) itemSizeDp else if (it == 2) itemSize2Dp else itemSize3Dp
+ // Item(it, crossAxisSize = crossAxisSize)
+ // }
+ // }
+ // }
+ // }
+
+ // assertPositions(
+ // 1 to 0,
+ // 2 to itemSize,
+ // 3 to itemSize * 2,
+ // crossAxis = listOf(
+ // 1 to 0,
+ // 2 to 0,
+ // 3 to 0,
+ // )
+ // )
+
+ // rule.runOnIdle {
+ // alignment = CrossAxisAlignment.Center
+ // }
+ // rule.mainClock.advanceTimeByFrame()
+
+ // onAnimationFrame { fraction ->
+ // assertPositions(
+ // 1 to 0,
+ // 2 to itemSize,
+ // 3 to itemSize * 2,
+ // crossAxis = listOf(
+ // 1 to 0,
+ // 2 to ((itemSize / 2 - itemSize2 / 2) * fraction).roundToInt(),
+ // 3 to ((itemSize / 2 - itemSize3 / 2) * fraction).roundToInt(),
+ // ),
+ // fraction = fraction
+ // )
+ // }
+ // }
+
+ @Test
+ fun moveItemToEndCausingNextItemsToAnimate_withContentPadding() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+ val rawStartPadding = 8
+ val rawEndPadding = 12
+ val (startPaddingDp, endPaddingDp) = with(rule.density) {
+ rawStartPadding.toDp() to rawEndPadding.toDp()
+ }
+ rule.setContent {
+ LazyGrid(1, startPadding = startPaddingDp, endPadding = endPaddingDp) {
+ items(list, key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ val startPadding = if (reverseLayout) rawEndPadding else rawStartPadding
+ assertPositions(
+ 0 to AxisIntOffset(0, startPadding),
+ 1 to AxisIntOffset(0, startPadding + itemSize),
+ 2 to AxisIntOffset(0, startPadding + itemSize * 2),
+ 3 to AxisIntOffset(0, startPadding + itemSize * 3),
+ 4 to AxisIntOffset(0, startPadding + itemSize * 4),
+ )
+
+ rule.runOnIdle {
+ list = listOf(0, 2, 3, 4, 1)
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 0 to AxisIntOffset(0, startPadding),
+ 1 to AxisIntOffset(
+ 0,
+ startPadding + itemSize + (itemSize * 3 * fraction).roundToInt()
+ ),
+ 2 to AxisIntOffset(
+ 0,
+ startPadding + itemSize * 2 - (itemSize * fraction).roundToInt()
+ ),
+ 3 to AxisIntOffset(
+ 0,
+ startPadding + itemSize * 3 - (itemSize * fraction).roundToInt()
+ ),
+ 4 to AxisIntOffset(
+ 0,
+ startPadding + itemSize * 4 - (itemSize * fraction).roundToInt()
+ ),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun reorderFirstAndLastItems_noNewLayoutInfoProduced() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+
+ var measurePasses = 0
+ rule.setContent {
+ LazyGrid(1) {
+ items(list, key = { it }) {
+ Item(it)
+ }
+ }
+ LaunchedEffect(Unit) {
+ snapshotFlow { state.layoutInfo }
+ .collect {
+ measurePasses++
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(4, 1, 2, 3, 0)
+ }
+
+ var startMeasurePasses = Int.MIN_VALUE
+ onAnimationFrame { fraction ->
+ if (fraction == 0f) {
+ startMeasurePasses = measurePasses
+ }
+ }
+ rule.mainClock.advanceTimeByFrame()
+ // new layoutInfo is produced on every remeasure of Lazy lists.
+ // but we want to avoid remeasuring and only do relayout on each animation frame.
+ // two extra measures are possible as we switch inProgress flag.
+ assertThat(measurePasses).isAtMost(startMeasurePasses + 2)
+ }
+
+ @Test
+ fun noAnimationWhenScrollOtherPosition() {
+ rule.setContent {
+ LazyGrid(1, maxSize = itemSizeDp * 3) {
+ items(listOf(0, 1, 2, 3, 4, 5, 6, 7), key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(0, itemSize / 2)
+ }
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 0 to AxisIntOffset(0, -itemSize / 2),
+ 1 to AxisIntOffset(0, itemSize / 2),
+ 2 to AxisIntOffset(0, itemSize * 3 / 2),
+ 3 to AxisIntOffset(0, itemSize * 5 / 2),
+ fraction = fraction
+ )
+ }
+ }
+
+ private fun AxisIntOffset(crossAxis: Int, mainAxis: Int) =
+ if (isVertical) IntOffset(crossAxis, mainAxis) else IntOffset(mainAxis, crossAxis)
+
+ private val IntOffset.mainAxis: Int get() = if (isVertical) y else x
+
+ private fun assertPositions(
+ vararg expected: Pair<Any, IntOffset>,
+ crossAxis: List<Pair<Any, Int>>? = null,
+ fraction: Float? = null,
+ autoReverse: Boolean = reverseLayout
+ ) {
+ with(rule.density) {
+ val actual = expected.map {
+ val actualOffset = rule.onNodeWithTag(it.first.toString())
+ .getUnclippedBoundsInRoot().let { bounds ->
+ IntOffset(
+ if (bounds.left.isSpecified) bounds.left.roundToPx() else Int.MIN_VALUE,
+ if (bounds.top.isSpecified) bounds.top.roundToPx() else Int.MIN_VALUE
+ )
+ }
+ it.first to actualOffset
+ }
+ val subject = if (fraction == null) {
+ assertThat(actual)
+ } else {
+ assertWithMessage("Fraction=$fraction").that(actual)
+ }
+ subject.isEqualTo(
+ listOf(*expected).let { list ->
+ if (!autoReverse) {
+ list
+ } else {
+ val containerBounds = rule.onNodeWithTag(ContainerTag).getBoundsInRoot()
+ val containerSize = with(rule.density) {
+ IntSize(
+ containerBounds.width.roundToPx(),
+ containerBounds.height.roundToPx()
+ )
+ }
+ list.map {
+ val itemSize = rule.onNodeWithTag(it.first.toString())
+ .getUnclippedBoundsInRoot().let {
+ IntSize(it.width.roundToPx(), it.height.roundToPx())
+ }
+ it.first to
+ IntOffset(
+ if (isVertical) {
+ it.second.x
+ } else {
+ containerSize.width - itemSize.width - it.second.x
+ },
+ if (!isVertical) {
+ it.second.y
+ } else {
+ containerSize.height - itemSize.height - it.second.y
+ }
+ )
+ }
+ }
+ }
+ )
+ if (crossAxis != null) {
+ val actualCross = expected.map {
+ val actualOffset = rule.onNodeWithTag(it.first.toString())
+ .getUnclippedBoundsInRoot().let { bounds ->
+ val offset = if (isVertical) bounds.left else bounds.top
+ if (offset == Dp.Unspecified) Int.MIN_VALUE else offset.roundToPx()
+ }
+ it.first to actualOffset
+ }
+ assertWithMessage(
+ "CrossAxis" + if (fraction != null) "for fraction=$fraction" else ""
+ )
+ .that(actualCross)
+ .isEqualTo(crossAxis)
+ }
+ }
+ }
+
+ private fun assertLayoutInfoPositions(vararg offsets: Pair<Any, IntOffset>) {
+ rule.runOnIdle {
+ assertThat(visibleItemsOffsets).isEqualTo(listOf(*offsets))
+ }
+ }
+
+ private val visibleItemsOffsets: List<Pair<Any, IntOffset>>
+ get() = state.layoutInfo.visibleItemsInfo.map {
+ it.key to it.offset
+ }
+
+ private fun onAnimationFrame(duration: Long = Duration, onFrame: (fraction: Float) -> Unit) {
+ require(duration.mod(FrameDuration) == 0L)
+ rule.waitForIdle()
+ rule.mainClock.advanceTimeByFrame()
+ var expectedTime = rule.mainClock.currentTime
+ for (i in 0..duration step FrameDuration) {
+ onFrame(i / duration.toFloat())
+ rule.mainClock.advanceTimeBy(FrameDuration)
+ expectedTime += FrameDuration
+ assertThat(expectedTime).isEqualTo(rule.mainClock.currentTime)
+ rule.waitForIdle()
+ }
+ }
+
+ @Composable
+ private fun LazyGrid(
+ columns: Int,
+ arrangement: Arrangement.HorizontalOrVertical? = null,
+ minSize: Dp = 0.dp,
+ maxSize: Dp = containerSizeDp,
+ startIndex: Int = 0,
+ startPadding: Dp = 0.dp,
+ endPadding: Dp = 0.dp,
+ content: TvLazyGridScope.() -> Unit
+ ) {
+ state = rememberLazyGridState(startIndex)
+ if (isVertical) {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(columns),
+ Modifier
+ .requiredHeightIn(minSize, maxSize)
+ .requiredWidth(itemSizeDp * columns)
+ .testTag(ContainerTag),
+ state = state,
+ verticalArrangement = arrangement as? Arrangement.Vertical
+ ?: if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
+ reverseLayout = reverseLayout,
+ contentPadding = PaddingValues(top = startPadding, bottom = endPadding),
+ content = content
+ )
+ } else {
+ TvLazyHorizontalGrid(
+ TvGridCells.Fixed(columns),
+ Modifier
+ .requiredWidthIn(minSize, maxSize)
+ .requiredHeight(itemSizeDp * columns)
+ .testTag(ContainerTag),
+ state = state,
+ horizontalArrangement = arrangement as? Arrangement.Horizontal
+ ?: if (!reverseLayout) Arrangement.Start else Arrangement.End,
+ reverseLayout = reverseLayout,
+ contentPadding = PaddingValues(start = startPadding, end = endPadding),
+ content = content
+ )
+ }
+ }
+
+ @Composable
+ private fun TvLazyGridItemScope.Item(
+ tag: Int,
+ height: Dp = itemSizeDp,
+ animSpec: FiniteAnimationSpec<IntOffset>? = AnimSpec
+ ) {
+ Box(
+ Modifier
+ .then(
+ if (isVertical) {
+ Modifier.requiredHeight(height)
+ } else {
+ Modifier.requiredWidth(height)
+ }
+ )
+ .testTag(tag.toString())
+ .then(
+ if (animSpec != null) {
+ Modifier.animateItemPlacement(animSpec)
+ } else {
+ Modifier
+ }
+ )
+ )
+ }
+
+ private fun SemanticsNodeInteraction.assertMainAxisSizeIsEqualTo(
+ expected: Dp
+ ): SemanticsNodeInteraction {
+ return if (isVertical) assertHeightIsEqualTo(expected) else assertWidthIsEqualTo(expected)
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun params() = arrayOf(
+ Config(isVertical = true, reverseLayout = false),
+ Config(isVertical = false, reverseLayout = false),
+ Config(isVertical = true, reverseLayout = true),
+ Config(isVertical = false, reverseLayout = true),
+ )
+
+ class Config(
+ val isVertical: Boolean,
+ val reverseLayout: Boolean
+ ) {
+ override fun toString() =
+ (if (isVertical) "LazyVerticalGrid" else "LazyHorizontalGrid") +
+ (if (reverseLayout) "(reverse)" else "")
+ }
+ }
+}
+
+private val FrameDuration = 16L
+private val Duration = 400L
+private val AnimSpec = tween<IntOffset>(Duration.toInt(), easing = LinearEasing)
+private val ContainerTag = "container"
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt
new file mode 100644
index 0000000..b2e1a5a
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt
@@ -0,0 +1,397 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Remeasurement
+import androidx.compose.ui.layout.RemeasurementModifier
+import androidx.compose.ui.layout.SubcomposeLayout
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.LargeTest
+import androidx.tv.foundation.lazy.AutoTestFrameClock
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class LazyGridPrefetcherTest(
+ orientation: Orientation
+) : BaseLazyGridTestWithOrientation(orientation) {
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun initParameters(): Array<Any> = arrayOf(
+ Orientation.Vertical,
+ Orientation.Horizontal,
+ )
+ }
+
+ val itemsSizePx = 30
+ val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
+
+ lateinit var state: TvLazyGridState
+
+ @Test
+ fun notPrefetchingForwardInitially() {
+ composeList()
+
+ rule.onNodeWithTag("4")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun notPrefetchingBackwardInitially() {
+ composeList(firstItem = 4)
+
+ rule.onNodeWithTag("0")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun prefetchingForwardAfterSmallScroll() {
+ composeList()
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(5f)
+ }
+ }
+
+ waitForPrefetch(4)
+
+ rule.onNodeWithTag("4")
+ .assertExists()
+ rule.onNodeWithTag("5")
+ .assertExists()
+ rule.onNodeWithTag("6")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun prefetchingBackwardAfterSmallScroll() {
+ composeList(firstItem = 4, itemOffset = 10)
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(-5f)
+ }
+ }
+
+ waitForPrefetch(2)
+
+ rule.onNodeWithTag("2")
+ .assertExists()
+ rule.onNodeWithTag("3")
+ .assertExists()
+ rule.onNodeWithTag("0")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun prefetchingForwardAndBackward() {
+ composeList(firstItem = 2)
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(5f)
+ }
+ }
+
+ waitForPrefetch(6)
+
+ rule.onNodeWithTag("6")
+ .assertExists()
+ rule.onNodeWithTag("7")
+ .assertExists()
+ rule.onNodeWithTag("0")
+ .assertDoesNotExist()
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(-2f)
+ state.scrollBy(-1f)
+ }
+ }
+
+ waitForPrefetch(0)
+
+ rule.onNodeWithTag("0")
+ .assertExists()
+ rule.onNodeWithTag("1")
+ .assertExists()
+ rule.onNodeWithTag("6")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun prefetchingForwardTwice() {
+ composeList()
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(5f)
+ }
+ }
+
+ waitForPrefetch(4)
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(itemsSizePx / 2f)
+ state.scrollBy(itemsSizePx / 2f)
+ }
+ }
+
+ waitForPrefetch(6)
+
+ rule.onNodeWithTag("4")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("6")
+ .assertExists()
+ rule.onNodeWithTag("8")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun prefetchingBackwardTwice() {
+ composeList(firstItem = 8)
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(-5f)
+ }
+ }
+
+ waitForPrefetch(4)
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(-itemsSizePx / 2f)
+ state.scrollBy(-itemsSizePx / 2f)
+ }
+ }
+
+ waitForPrefetch(2)
+
+ rule.onNodeWithTag("4")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("6")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("2")
+ .assertExists()
+ rule.onNodeWithTag("0")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun prefetchingForwardAndBackwardReverseLayout() {
+ composeList(firstItem = 2, reverseLayout = true)
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(5f)
+ }
+ }
+
+ waitForPrefetch(6)
+
+ rule.onNodeWithTag("6")
+ .assertExists()
+ rule.onNodeWithTag("7")
+ .assertExists()
+ rule.onNodeWithTag("0")
+ .assertDoesNotExist()
+ rule.onNodeWithTag("1")
+ .assertDoesNotExist()
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(-2f)
+ state.scrollBy(-1f)
+ }
+ }
+
+ waitForPrefetch(0)
+
+ rule.onNodeWithTag("0")
+ .assertExists()
+ rule.onNodeWithTag("1")
+ .assertExists()
+ rule.onNodeWithTag("6")
+ .assertDoesNotExist()
+ rule.onNodeWithTag("7")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun prefetchingForwardAndBackwardWithContentPadding() {
+ val halfItemSize = itemsSizeDp / 2f
+ composeList(
+ firstItem = 4,
+ itemOffset = 5,
+ contentPadding = PaddingValues(mainAxis = halfItemSize)
+ )
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("4")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("6")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("0")
+ .assertDoesNotExist()
+ rule.onNodeWithTag("8")
+ .assertDoesNotExist()
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(5f)
+ }
+ }
+
+ waitForPrefetch(6)
+
+ rule.onNodeWithTag("8")
+ .assertExists()
+ rule.onNodeWithTag("0")
+ .assertDoesNotExist()
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(-2f)
+ }
+ }
+
+ waitForPrefetch(0)
+
+ rule.onNodeWithTag("0")
+ .assertExists()
+ }
+
+ @Test
+ fun disposingWhilePrefetchingScheduled() {
+ var emit = true
+ lateinit var remeasure: Remeasurement
+ rule.setContent {
+ SubcomposeLayout(
+ modifier = object : RemeasurementModifier {
+ override fun onRemeasurementAvailable(remeasurement: Remeasurement) {
+ remeasure = remeasurement
+ }
+ }
+ ) { constraints ->
+ val placeable = if (emit) {
+ subcompose(Unit) {
+ state = rememberLazyGridState()
+ LazyGrid(
+ 2,
+ Modifier.mainAxisSize(itemsSizeDp * 1.5f),
+ state,
+ ) {
+ items(1000) {
+ Spacer(
+ Modifier.mainAxisSize(itemsSizeDp)
+ )
+ }
+ }
+ }.first().measure(constraints)
+ } else {
+ null
+ }
+ layout(constraints.maxWidth, constraints.maxHeight) {
+ placeable?.place(0, 0)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ // this will schedule the prefetching
+ runBlocking(AutoTestFrameClock()) {
+ state.scrollBy(itemsSizePx.toFloat())
+ }
+ // then we synchronously dispose LazyColumn
+ emit = false
+ remeasure.forceRemeasure()
+ }
+
+ rule.runOnIdle { }
+ }
+
+ private fun waitForPrefetch(index: Int) {
+ rule.waitUntil {
+ activeNodes.contains(index) && activeMeasuredNodes.contains(index)
+ }
+ }
+
+ private val activeNodes = mutableSetOf<Int>()
+ private val activeMeasuredNodes = mutableSetOf<Int>()
+
+ private fun composeList(
+ firstItem: Int = 0,
+ itemOffset: Int = 0,
+ reverseLayout: Boolean = false,
+ contentPadding: PaddingValues = PaddingValues(0.dp)
+ ) {
+ rule.setContent {
+ state = rememberLazyGridState(
+ initialFirstVisibleItemIndex = firstItem,
+ initialFirstVisibleItemScrollOffset = itemOffset
+ )
+ LazyGrid(
+ 2,
+ Modifier.mainAxisSize(itemsSizeDp * 1.5f),
+ state,
+ reverseLayout = reverseLayout,
+ contentPadding = contentPadding
+ ) {
+ items(100) {
+ DisposableEffect(it) {
+ activeNodes.add(it)
+ onDispose {
+ activeNodes.remove(it)
+ activeMeasuredNodes.remove(it)
+ }
+ }
+ Spacer(
+ Modifier
+ .mainAxisSize(itemsSizeDp)
+ .testTag("$it")
+ .layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ activeMeasuredNodes.add(it)
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridSlotsReuseTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridSlotsReuseTest.kt
new file mode 100644
index 0000000..c9f6943
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridSlotsReuseTest.kt
@@ -0,0 +1,486 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class LazyGridSlotsReuseTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ val itemsSizePx = 30f
+ val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
+
+ @Test
+ fun scroll1ItemScrolledOffItemIsKeptForReuse() {
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ Modifier.height(itemsSizeDp * 1.5f),
+ state
+ ) {
+ items(100) {
+ Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertIsDisplayed()
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(1)
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun scroll2ItemsScrolledOffItemsAreKeptForReuse() {
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ Modifier.height(itemsSizeDp * 1.5f),
+ state
+ ) {
+ items(100) {
+ Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(2)
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule.onNodeWithTag("1")
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun checkMaxItemsKeptForReuse() {
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ Modifier.height(itemsSizeDp * (DefaultMaxItemsToRetain + 0.5f)),
+ state
+ ) {
+ items(100) {
+ Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it"))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(DefaultMaxItemsToRetain + 1)
+ }
+ }
+
+ repeat(DefaultMaxItemsToRetain) {
+ rule.onNodeWithTag("$it")
+ .assertExists()
+ .assertIsNotDisplayed()
+ }
+ rule.onNodeWithTag("$DefaultMaxItemsToRetain")
+ .assertDoesNotExist()
+ rule.onNodeWithTag("${DefaultMaxItemsToRetain + 1}")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun scroll3Items2OfScrolledOffItemsAreKeptForReuse() {
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ Modifier.height(itemsSizeDp * 1.5f),
+ state
+ ) {
+ items(100) {
+ Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+
+ rule.runOnIdle {
+ runBlocking {
+ // after this step 0 and 1 are in reusable buffer
+ state.scrollToItem(2)
+
+ // this step requires one item and will take the last item from the buffer - item
+ // 1 plus will put 2 in the buffer. so expected buffer is items 2 and 0
+ state.scrollToItem(3)
+ }
+ }
+
+ // recycled
+ rule.onNodeWithTag("1")
+ .assertDoesNotExist()
+
+ // in buffer
+ rule.onNodeWithTag("0")
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule.onNodeWithTag("2")
+ .assertExists()
+ .assertIsNotDisplayed()
+
+ // visible
+ rule.onNodeWithTag("3")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("4")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun doMultipleScrollsOneByOne() {
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ Modifier.height(itemsSizeDp * 1.5f),
+ state
+ ) {
+ items(100) {
+ Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it"))
+ }
+ }
+ }
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(1) // buffer is [0]
+ state.scrollToItem(2) // 0 used, buffer is [1]
+ state.scrollToItem(3) // 1 used, buffer is [2]
+ state.scrollToItem(4) // 2 used, buffer is [3]
+ }
+ }
+
+ // recycled
+ rule.onNodeWithTag("0")
+ .assertDoesNotExist()
+ rule.onNodeWithTag("1")
+ .assertDoesNotExist()
+ rule.onNodeWithTag("2")
+ .assertDoesNotExist()
+
+ // in buffer
+ rule.onNodeWithTag("3")
+ .assertExists()
+ .assertIsNotDisplayed()
+
+ // visible
+ rule.onNodeWithTag("4")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("5")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun scrollBackwardOnce() {
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ state = rememberLazyGridState(10)
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ Modifier.height(itemsSizeDp * 1.5f),
+ state
+ ) {
+ items(100) {
+ Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it"))
+ }
+ }
+ }
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(8) // buffer is [10, 11]
+ }
+ }
+
+ // in buffer
+ rule.onNodeWithTag("10")
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule.onNodeWithTag("11")
+ .assertExists()
+ .assertIsNotDisplayed()
+
+ // visible
+ rule.onNodeWithTag("8")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("9")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun scrollBackwardOneByOne() {
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ state = rememberLazyGridState(10)
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ Modifier.height(itemsSizeDp * 1.5f),
+ state
+ ) {
+ items(100) {
+ Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it"))
+ }
+ }
+ }
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(9) // buffer is [11]
+ state.scrollToItem(7) // 11 reused, buffer is [9]
+ state.scrollToItem(6) // 9 reused, buffer is [8]
+ }
+ }
+
+ // in buffer
+ rule.onNodeWithTag("8")
+ .assertExists()
+ .assertIsNotDisplayed()
+
+ // visible
+ rule.onNodeWithTag("6")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("7")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun scrollingBackReusesTheSameSlot() {
+ lateinit var state: TvLazyGridState
+ var counter0 = 0
+ var counter1 = 10
+ var rememberedValue0 = -1
+ var rememberedValue1 = -1
+ rule.setContent {
+ state = rememberLazyGridState()
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ Modifier.height(itemsSizeDp * 1.5f),
+ state
+ ) {
+ items(100) {
+ if (it == 0) {
+ rememberedValue0 = remember { counter0++ }
+ }
+ if (it == 1) {
+ rememberedValue1 = remember { counter1++ }
+ }
+ Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it"))
+ }
+ }
+ }
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(2) // buffer is [0, 1]
+ state.scrollToItem(0) // scrolled back, 0 and 1 are reused back. buffer: [2, 3]
+ }
+ }
+
+ rule.runOnIdle {
+ Truth.assertWithMessage("Item 0 restored remembered value is $rememberedValue0")
+ .that(rememberedValue0).isEqualTo(0)
+ Truth.assertWithMessage("Item 1 restored remembered value is $rememberedValue1")
+ .that(rememberedValue1).isEqualTo(10)
+ }
+
+ rule.onNodeWithTag("0")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("2")
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule.onNodeWithTag("3")
+ .assertExists()
+ .assertIsNotDisplayed()
+ }
+
+ @Test
+ fun differentContentTypes() {
+ lateinit var state: TvLazyGridState
+ val visibleItemsCount = (DefaultMaxItemsToRetain + 1) * 2
+ val startOfType1 = DefaultMaxItemsToRetain + 1
+ rule.setContent {
+ state = rememberLazyGridState()
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ Modifier.height(itemsSizeDp * (visibleItemsCount - 0.5f)),
+ state
+ ) {
+ items(
+ 100,
+ contentType = { if (it >= startOfType1) 1 else 0 }
+ ) {
+ Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it"))
+ }
+ }
+ }
+
+ for (i in 0 until visibleItemsCount) {
+ rule.onNodeWithTag("$i")
+ .assertIsDisplayed()
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(visibleItemsCount)
+ }
+ }
+
+ rule.onNodeWithTag("$visibleItemsCount")
+ .assertIsDisplayed()
+
+ // [DefaultMaxItemsToRetain] items of type 0 are left for reuse
+ for (i in 0 until DefaultMaxItemsToRetain) {
+ rule.onNodeWithTag("$i")
+ .assertExists()
+ .assertIsNotDisplayed()
+ }
+ rule.onNodeWithTag("$DefaultMaxItemsToRetain")
+ .assertDoesNotExist()
+
+ // and 7 items of type 1
+ for (i in startOfType1 until startOfType1 + DefaultMaxItemsToRetain) {
+ rule.onNodeWithTag("$i")
+ .assertExists()
+ .assertIsNotDisplayed()
+ }
+ rule.onNodeWithTag("${startOfType1 + DefaultMaxItemsToRetain}")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun differentTypesFromDifferentItemCalls() {
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ Modifier.height(itemsSizeDp * 2.5f),
+ state
+ ) {
+ val content = @Composable { tag: String ->
+ Spacer(Modifier.height(itemsSizeDp).width(10.dp).testTag(tag))
+ }
+ item(contentType = "not-to-reuse-0") {
+ content("0")
+ }
+ item(contentType = "reuse") {
+ content("1")
+ }
+ items(
+ List(100) { it + 2 },
+ contentType = { if (it == 10) "reuse" else "not-to-reuse-$it" }) {
+ content("$it")
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(2)
+ // now items 0 and 1 are put into reusables
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule.onNodeWithTag("1")
+ .assertExists()
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(9)
+ // item 10 should reuse slot 1
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule.onNodeWithTag("1")
+ .assertDoesNotExist()
+ rule.onNodeWithTag("9")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("10")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("11")
+ .assertIsDisplayed()
+ }
+}
+
+private val DefaultMaxItemsToRetain = 7
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridSpanTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridSpanTest.kt
new file mode 100644
index 0000000..3d4c09e
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridSpanTest.kt
@@ -0,0 +1,301 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class LazyGridSpanTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun spans() {
+ val columns = 4
+ val columnWidth = with(rule.density) { 5.toDp() }
+ val itemHeight = with(rule.density) { 10.toDp() }
+ rule.setContent {
+ TvLazyVerticalGrid(
+ columns = TvGridCells.Fixed(columns),
+ modifier = Modifier.requiredSize(columnWidth * columns, itemHeight * 3)
+ ) {
+ items(
+ count = 6,
+ span = { index ->
+ when (index) {
+ 0 -> {
+ Truth.assertThat(maxLineSpan).isEqualTo(4)
+ Truth.assertThat(maxCurrentLineSpan).isEqualTo(4)
+ TvGridItemSpan(3)
+ }
+ 1 -> {
+ Truth.assertThat(maxLineSpan).isEqualTo(4)
+ Truth.assertThat(maxCurrentLineSpan).isEqualTo(1)
+ TvGridItemSpan(1)
+ }
+ 2 -> {
+ Truth.assertThat(maxLineSpan).isEqualTo(4)
+ Truth.assertThat(maxCurrentLineSpan).isEqualTo(4)
+ TvGridItemSpan(1)
+ }
+ 3 -> {
+ Truth.assertThat(maxLineSpan).isEqualTo(4)
+ Truth.assertThat(maxCurrentLineSpan).isEqualTo(3)
+ TvGridItemSpan(3)
+ }
+ 4 -> {
+ Truth.assertThat(maxLineSpan).isEqualTo(4)
+ Truth.assertThat(maxCurrentLineSpan).isEqualTo(4)
+ TvGridItemSpan(1)
+ }
+ 5 -> {
+ Truth.assertThat(maxLineSpan).isEqualTo(4)
+ Truth.assertThat(maxCurrentLineSpan).isEqualTo(3)
+ TvGridItemSpan(1)
+ }
+ else -> error("Out of index span queried")
+ }
+ },
+ ) {
+ Box(Modifier.height(itemHeight).testTag("$it"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(columnWidth * 3)
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemHeight)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(itemHeight)
+ .assertLeftPositionInRootIsEqualTo(columnWidth)
+ rule.onNodeWithTag("4")
+ .assertTopPositionInRootIsEqualTo(itemHeight * 2)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("5")
+ .assertTopPositionInRootIsEqualTo(itemHeight * 2)
+ .assertLeftPositionInRootIsEqualTo(columnWidth)
+ }
+
+ @Test
+ fun spansWithHorizontalSpacing() {
+ val columns = 4
+ val columnWidth = with(rule.density) { 5.toDp() }
+ val itemHeight = with(rule.density) { 10.toDp() }
+ val spacing = with(rule.density) { 4.toDp() }
+ rule.setContent {
+ TvLazyVerticalGrid(
+ columns = TvGridCells.Fixed(columns),
+ modifier = Modifier.requiredSize(
+ columnWidth * columns + spacing * (columns - 1),
+ itemHeight
+ ),
+ horizontalArrangement = Arrangement.spacedBy(spacing)
+ ) {
+ items(
+ count = 2,
+ span = { index ->
+ when (index) {
+ 0 -> TvGridItemSpan(1)
+ 1 -> TvGridItemSpan(3)
+ else -> error("Out of index span queried")
+ }
+ }
+ ) {
+ Box(Modifier.height(itemHeight).testTag("$it"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ .assertWidthIsEqualTo(columnWidth)
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(columnWidth + spacing)
+ .assertWidthIsEqualTo(columnWidth * 3 + spacing * 2)
+ }
+
+ @Test
+ fun spansMultipleBlocks() {
+ val columns = 4
+ val columnWidth = with(rule.density) { 5.toDp() }
+ val itemHeight = with(rule.density) { 10.toDp() }
+ rule.setContent {
+ TvLazyVerticalGrid(
+ columns = TvGridCells.Fixed(columns),
+ modifier = Modifier.requiredSize(columnWidth * columns, itemHeight)
+ ) {
+ items(
+ count = 1,
+ span = { index ->
+ when (index) {
+ 0 -> TvGridItemSpan(1)
+ else -> error("Out of index span queried")
+ }
+ }
+ ) {
+ Box(Modifier.height(itemHeight).testTag("0"))
+ }
+ item(span = {
+ if (maxCurrentLineSpan != 3) error("Wrong maxSpan")
+ TvGridItemSpan(2)
+ }) {
+ Box(Modifier.height(itemHeight).testTag("1"))
+ }
+ items(
+ count = 1,
+ span = { index ->
+ if (maxCurrentLineSpan != 1 || index != 0) {
+ error("Wrong span calculation parameters")
+ }
+ TvGridItemSpan(1)
+ }
+ ) {
+ if (it != 0) error("Wrong index")
+ Box(Modifier.height(itemHeight).testTag("2"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ .assertWidthIsEqualTo(columnWidth)
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(columnWidth)
+ .assertWidthIsEqualTo(columnWidth * 2)
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(columnWidth * 3)
+ .assertWidthIsEqualTo(columnWidth)
+ }
+
+ @Test
+ fun spansLineBreak() {
+ val columns = 4
+ val columnWidth = with(rule.density) { 5.toDp() }
+ val itemHeight = with(rule.density) { 10.toDp() }
+ rule.setContent {
+ TvLazyVerticalGrid(
+ columns = TvGridCells.Fixed(columns),
+ modifier = Modifier.requiredSize(columnWidth * columns, itemHeight * 3)
+ ) {
+ item(span = {
+ if (maxCurrentLineSpan != 4) error("Wrong maxSpan")
+ TvGridItemSpan(3)
+ }) {
+ Box(Modifier.height(itemHeight).testTag("0"))
+ }
+ items(
+ count = 4,
+ span = { index ->
+ if (maxCurrentLineSpan != when (index) {
+ 0 -> 1
+ 1 -> 2
+ 2 -> 1
+ 3 -> 2
+ else -> error("Wrong index")
+ }
+ ) error("Wrong maxSpan")
+ TvGridItemSpan(listOf(2, 1, 2, 2)[index])
+ }
+ ) {
+ Box(Modifier.height(itemHeight).testTag((it + 1).toString()))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ .assertWidthIsEqualTo(columnWidth * 3)
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemHeight)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ .assertWidthIsEqualTo(columnWidth * 2)
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemHeight)
+ .assertLeftPositionInRootIsEqualTo(columnWidth * 2)
+ .assertWidthIsEqualTo(columnWidth)
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(itemHeight * 2)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ .assertWidthIsEqualTo(columnWidth * 2)
+ rule.onNodeWithTag("4")
+ .assertTopPositionInRootIsEqualTo(itemHeight * 2)
+ .assertLeftPositionInRootIsEqualTo(columnWidth * 2)
+ .assertWidthIsEqualTo(columnWidth * 2)
+ }
+
+ @Test
+ fun spansCalculationDoesntCrash() {
+ // regression from b/222530458
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ TvLazyVerticalGrid(
+ columns = TvGridCells.Fixed(2),
+ state = state,
+ modifier = Modifier.size(100.dp)
+ ) {
+ repeat(100) {
+ item(span = { TvGridItemSpan(maxLineSpan) }) {
+ Box(Modifier.fillMaxWidth().height(1.dp))
+ }
+ items(10) {
+ Box(Modifier.fillMaxWidth().height(1.dp))
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(state.layoutInfo.totalItemsCount)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridTest.kt
new file mode 100644
index 0000000..4f170ec
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridTest.kt
@@ -0,0 +1,1070 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import android.os.Build
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.SemanticsMatcher.Companion.keyIsDefined
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.zIndex
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.tv.foundation.lazy.AutoTestFrameClock
+import androidx.tv.foundation.lazy.list.setContentWithTestViewConfiguration
+import com.google.common.collect.Range
+import com.google.common.truth.IntegerSubject
+import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@MediumTest
+@RunWith(Parameterized::class)
+class LazyGridTest(
+ private val orientation: Orientation
+) : BaseLazyGridTestWithOrientation(orientation) {
+ private val LazyGridTag = "LazyGridTag"
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun initParameters(): Array<Any> = arrayOf(
+ Orientation.Vertical,
+ Orientation.Horizontal,
+ )
+ }
+
+ @Test
+ fun lazyGridShowsOneItem() {
+ val itemTestTag = "itemTestTag"
+
+ rule.setContent {
+ LazyGrid(
+ cells = 3
+ ) {
+ item {
+ Spacer(
+ Modifier.size(10.dp).testTag(itemTestTag)
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag(itemTestTag)
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun lazyGridShowsOneLine() {
+ val items = (1..5).map { it.toString() }
+
+ rule.setContent {
+ LazyGrid(
+ cells = 3,
+ modifier = Modifier.axisSize(300.dp, 100.dp)
+ ) {
+ items(items) {
+ Spacer(Modifier.mainAxisSize(101.dp).testTag(it))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("3")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("4")
+ .assertDoesNotExist()
+
+ rule.onNodeWithTag("5")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun lazyGridShowsSecondLineOnScroll() {
+ val items = (1..12).map { it.toString() }
+
+ rule.setContentWithTestViewConfiguration {
+ LazyGrid(
+ cells = 3,
+ modifier = Modifier.mainAxisSize(200.dp).testTag(LazyGridTag)
+ ) {
+ items(items) {
+ Box(Modifier.mainAxisSize(101.dp).testTag(it).focusable())
+ }
+ }
+ }
+
+ rule.keyPress(3)
+
+ rule.onNodeWithTag("4")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("5")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("6")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("10")
+ .assertIsNotDisplayed()
+
+ rule.onNodeWithTag("11")
+ .assertIsNotDisplayed()
+
+ rule.onNodeWithTag("12")
+ .assertIsNotDisplayed()
+ }
+
+ @Test
+ fun lazyGridScrollHidesFirstLine() {
+ val items = (1..9).map { it.toString() }
+
+ rule.setContentWithTestViewConfiguration {
+ LazyGrid(
+ cells = 3,
+ modifier = Modifier.mainAxisSize(200.dp).testTag(LazyGridTag),
+ ) {
+ items(items) {
+ Spacer(Modifier.mainAxisSize(101.dp).testTag(it).focusable())
+ }
+ }
+ }
+
+ rule.keyPress(3)
+
+ rule.onNodeWithTag("1")
+ .assertIsNotDisplayed()
+
+ rule.onNodeWithTag("2")
+ .assertIsNotDisplayed()
+
+ rule.onNodeWithTag("3")
+ .assertIsNotDisplayed()
+
+ rule.onNodeWithTag("4")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("5")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("6")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("7")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("8")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("9")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun adaptiveLazyGridFillsAllCrossAxisSize() {
+ val items = (1..5).map { it.toString() }
+
+ rule.setContent {
+ LazyGrid(
+ cells = TvGridCells.Adaptive(130.dp),
+ modifier = Modifier.axisSize(300.dp, 100.dp)
+ ) {
+ items(items) {
+ Spacer(Modifier.mainAxisSize(101.dp).testTag(it))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("2")
+ .assertCrossAxisStartPositionInRootIsEqualTo(150.dp)
+
+ rule.onNodeWithTag("3")
+ .assertDoesNotExist()
+
+ rule.onNodeWithTag("4")
+ .assertDoesNotExist()
+
+ rule.onNodeWithTag("5")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun adaptiveLazyGridAtLeastOneSlot() {
+ val items = (1..3).map { it.toString() }
+
+ rule.setContent {
+ LazyGrid(
+ cells = TvGridCells.Adaptive(301.dp),
+ modifier = Modifier.axisSize(300.dp, 100.dp)
+ ) {
+ items(items) {
+ Spacer(Modifier.mainAxisSize(101.dp).testTag(it))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("2")
+ .assertDoesNotExist()
+
+ rule.onNodeWithTag("3")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun adaptiveLazyGridAppliesHorizontalSpacings() {
+ val items = (1..3).map { it.toString() }
+
+ val spacing = with(rule.density) { 10.toDp() }
+ val itemSize = with(rule.density) { 100.toDp() }
+
+ rule.setContent {
+ LazyGrid(
+ cells = TvGridCells.Adaptive(itemSize),
+ modifier = Modifier.axisSize(itemSize * 3 + spacing * 2, itemSize),
+ crossAxisSpacedBy = spacing
+ ) {
+ items(items) {
+ Spacer(Modifier.size(itemSize).testTag(it))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+ .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertCrossAxisSizeIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+ .assertCrossAxisStartPositionInRootIsEqualTo(itemSize + spacing)
+ .assertCrossAxisSizeIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("3")
+ .assertIsDisplayed()
+ .assertCrossAxisStartPositionInRootIsEqualTo(itemSize * 2 + spacing * 2)
+ .assertCrossAxisSizeIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun adaptiveLazyGridAppliesHorizontalSpacingsWithContentPaddings() {
+ val items = (1..3).map { it.toString() }
+
+ val spacing = with(rule.density) { 8.toDp() }
+ val itemSize = with(rule.density) { 40.toDp() }
+
+ rule.setContent {
+ LazyGrid(
+ cells = TvGridCells.Adaptive(itemSize),
+ modifier = Modifier.axisSize(itemSize * 3 + spacing * 4, itemSize),
+ crossAxisSpacedBy = spacing,
+ contentPadding = PaddingValues(crossAxis = spacing)
+ ) {
+ items(items) {
+ Spacer(Modifier.size(itemSize).testTag(it))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+ .assertCrossAxisStartPositionInRootIsEqualTo(spacing)
+ .assertCrossAxisSizeIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+ .assertCrossAxisStartPositionInRootIsEqualTo(itemSize + spacing * 2)
+ .assertCrossAxisSizeIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("3")
+ .assertIsDisplayed()
+ .assertCrossAxisStartPositionInRootIsEqualTo(itemSize * 2 + spacing * 3)
+ .assertCrossAxisSizeIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun adaptiveLazyGridAppliesVerticalSpacings() {
+ val items = (1..3).map { it.toString() }
+
+ val spacing = with(rule.density) { 4.toDp() }
+ val itemSize = with(rule.density) { 32.toDp() }
+
+ rule.setContent {
+ LazyGrid(
+ cells = TvGridCells.Adaptive(itemSize),
+ modifier = Modifier.axisSize(itemSize, itemSize * 3 + spacing * 2),
+ mainAxisSpacedBy = spacing
+ ) {
+ items(items) {
+ Spacer(Modifier.size(itemSize).testTag(it))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertMainAxisSizeIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(itemSize + spacing)
+ .assertMainAxisSizeIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("3")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(itemSize * 2 + spacing * 2)
+ .assertMainAxisSizeIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun adaptiveLazyGridAppliesVerticalSpacingsWithContentPadding() {
+ val items = (1..3).map { it.toString() }
+
+ val spacing = with(rule.density) { 16.toDp() }
+ val itemSize = with(rule.density) { 72.toDp() }
+
+ rule.setContent {
+ LazyGrid(
+ cells = TvGridCells.Adaptive(itemSize),
+ modifier = Modifier.axisSize(itemSize, itemSize * 3 + spacing * 2),
+ mainAxisSpacedBy = spacing,
+ contentPadding = PaddingValues(mainAxis = spacing)
+ ) {
+ items(items) {
+ Spacer(Modifier.size(itemSize).testTag(it))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(spacing)
+ .assertMainAxisSizeIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(spacing * 2 + itemSize)
+ .assertMainAxisSizeIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("3")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(spacing * 3 + itemSize * 2)
+ .assertMainAxisSizeIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun fixedLazyGridAppliesVerticalSpacings() {
+ val items = (1..4).map { it.toString() }
+
+ val spacing = with(rule.density) { 24.toDp() }
+ val itemSize = with(rule.density) { 80.toDp() }
+
+ rule.setContent {
+ LazyGrid(
+ cells = 2,
+ modifier = Modifier.axisSize(itemSize, itemSize * 2 + spacing),
+ mainAxisSpacedBy = spacing,
+ ) {
+ items(items) {
+ Spacer(Modifier.size(itemSize).testTag(it))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertMainAxisSizeIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertMainAxisSizeIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("3")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(spacing + itemSize)
+ .assertMainAxisSizeIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("4")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(spacing + itemSize)
+ .assertMainAxisSizeIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun fixedLazyGridAppliesHorizontalSpacings() {
+ val items = (1..4).map { it.toString() }
+
+ val spacing = with(rule.density) { 15.toDp() }
+ val itemSize = with(rule.density) { 30.toDp() }
+
+ rule.setContent {
+ LazyGrid(
+ cells = 2,
+ modifier = Modifier.axisSize(itemSize * 2 + spacing, itemSize * 2),
+ crossAxisSpacedBy = spacing
+ ) {
+ items(items) {
+ Spacer(Modifier.size(itemSize).testTag(it))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+ .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertCrossAxisSizeIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+ .assertCrossAxisStartPositionInRootIsEqualTo(spacing + itemSize)
+ .assertCrossAxisSizeIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("3")
+ .assertIsDisplayed()
+ .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertCrossAxisSizeIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("4")
+ .assertIsDisplayed()
+ .assertCrossAxisStartPositionInRootIsEqualTo(spacing + itemSize)
+ .assertCrossAxisSizeIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun fixedLazyGridAppliesVerticalSpacingsWithContentPadding() {
+ val items = (1..4).map { it.toString() }
+
+ val spacing = with(rule.density) { 30.toDp() }
+ val itemSize = with(rule.density) { 77.toDp() }
+
+ rule.setContent {
+ LazyGrid(
+ cells = 2,
+ modifier = Modifier.axisSize(itemSize, itemSize * 2 + spacing),
+ mainAxisSpacedBy = spacing,
+ contentPadding = PaddingValues(mainAxis = spacing)
+ ) {
+ items(items) {
+ Spacer(Modifier.size(itemSize).testTag(it))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(spacing)
+ .assertMainAxisSizeIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(spacing)
+ .assertMainAxisSizeIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("3")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(spacing * 2 + itemSize)
+ .assertMainAxisSizeIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("4")
+ .assertIsDisplayed()
+ .assertMainAxisStartPositionInRootIsEqualTo(spacing * 2 + itemSize)
+ .assertMainAxisSizeIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun fixedLazyGridAppliesHorizontalSpacingsWithContentPadding() {
+ val items = (1..4).map { it.toString() }
+
+ val spacing = with(rule.density) { 22.toDp() }
+ val itemSize = with(rule.density) { 44.toDp() }
+
+ rule.setContent {
+ LazyGrid(
+ cells = 2,
+ modifier = Modifier.axisSize(itemSize * 2 + spacing * 3, itemSize * 2),
+ crossAxisSpacedBy = spacing,
+ contentPadding = PaddingValues(crossAxis = spacing)
+ ) {
+ items(items) {
+ Spacer(Modifier.size(itemSize).testTag(it))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+ .assertCrossAxisStartPositionInRootIsEqualTo(spacing)
+ .assertCrossAxisSizeIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+ .assertCrossAxisStartPositionInRootIsEqualTo(spacing * 2 + itemSize)
+ .assertCrossAxisSizeIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("3")
+ .assertIsDisplayed()
+ .assertCrossAxisStartPositionInRootIsEqualTo(spacing)
+ .assertCrossAxisSizeIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("4")
+ .assertIsDisplayed()
+ .assertCrossAxisStartPositionInRootIsEqualTo(spacing * 2 + itemSize)
+ .assertCrossAxisSizeIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun usedWithArray() {
+ val items = arrayOf("1", "2", "3", "4")
+
+ val itemSize = with(rule.density) { 15.toDp() }
+
+ rule.setContent {
+ LazyGrid(
+ cells = 2,
+ modifier = Modifier.crossAxisSize(itemSize * 2)
+ ) {
+ items(items) {
+ Spacer(Modifier.mainAxisSize(itemSize).testTag(it))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("2")
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertCrossAxisStartPositionInRootIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("3")
+ .assertMainAxisStartPositionInRootIsEqualTo(itemSize)
+ .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("4")
+ .assertMainAxisStartPositionInRootIsEqualTo(itemSize)
+ .assertCrossAxisStartPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun usedWithArrayIndexed() {
+ val items = arrayOf("1", "2", "3", "4")
+
+ val itemSize = with(rule.density) { 15.toDp() }
+
+ rule.setContent {
+ LazyGrid(
+ cells = 2,
+ Modifier.crossAxisSize(itemSize * 2)
+ ) {
+ itemsIndexed(items) { index, item ->
+ Spacer(Modifier.mainAxisSize(itemSize).testTag("$index*$item"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0*1")
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("1*2")
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertCrossAxisStartPositionInRootIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("2*3")
+ .assertMainAxisStartPositionInRootIsEqualTo(itemSize)
+ .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("3*4")
+ .assertMainAxisStartPositionInRootIsEqualTo(itemSize)
+ .assertCrossAxisStartPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun changeItemsCountAndScrollImmediately() {
+ lateinit var state: TvLazyGridState
+ var count by mutableStateOf(100)
+ val composedIndexes = mutableListOf<Int>()
+ rule.setContent {
+ state = rememberLazyGridState()
+ LazyGrid(
+ cells = 1,
+ modifier = Modifier.mainAxisSize(10.dp),
+ state = state
+ ) {
+ items(count) { index ->
+ composedIndexes.add(index)
+ Box(Modifier.size(20.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ composedIndexes.clear()
+ count = 10
+ runBlocking(AutoTestFrameClock()) {
+ // we try to scroll to the index after 10, but we expect that the component will
+ // already be aware there is a new count and not compose items with index > 10
+ state.scrollToItem(50)
+ }
+ composedIndexes.forEach {
+ Truth.assertThat(it).isLessThan(count)
+ }
+ Truth.assertThat(state.firstVisibleItemIndex).isEqualTo(9)
+ }
+ }
+
+ @Test
+ fun maxIntElements() {
+ val itemSize = with(rule.density) { 15.toDp() }
+
+ rule.setContent {
+ LazyGrid(
+ cells = 2,
+ modifier = Modifier.size(itemSize * 2).testTag(LazyGridTag),
+ state = TvLazyGridState(firstVisibleItemIndex = Int.MAX_VALUE - 3)
+ ) {
+ items(Int.MAX_VALUE) {
+ Box(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("${Int.MAX_VALUE - 3}")
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("${Int.MAX_VALUE - 2}")
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertCrossAxisStartPositionInRootIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("${Int.MAX_VALUE - 1}")
+ .assertMainAxisStartPositionInRootIsEqualTo(itemSize)
+ .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("${Int.MAX_VALUE}").assertDoesNotExist()
+ rule.onNodeWithTag("0").assertDoesNotExist()
+ }
+
+ @Test
+ fun pointerInputScrollingIsAllowedWhenUserScrollingIsEnabled() {
+ val itemSize = with(rule.density) { 30.toDp() }
+ rule.setContentWithTestViewConfiguration {
+ LazyGrid(
+ cells = 1,
+ modifier = Modifier.size(itemSize * 3).testTag(LazyGridTag),
+ userScrollEnabled = true
+ ) {
+ items(5) {
+ Spacer(Modifier.size(itemSize).testTag("$it").focusable())
+ }
+ }
+ }
+
+ rule.keyPress(3)
+
+ rule.onNodeWithTag("1")
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ }
+
+ @Test
+ fun pointerInputScrollingIsDisallowedWhenUserScrollingIsDisabled() {
+ val itemSize = with(rule.density) { 30.toDp() }
+ rule.setContentWithTestViewConfiguration {
+ LazyGrid(
+ cells = 1,
+ modifier = Modifier.size(itemSize * 3).testTag(LazyGridTag),
+ userScrollEnabled = false
+ ) {
+ items(5) {
+ Spacer(Modifier.size(itemSize).testTag("$it").focusable())
+ }
+ }
+ }
+
+ rule.keyPress(2)
+
+ rule.onNodeWithTag("1")
+ .assertMainAxisStartPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun programmaticScrollingIsAllowedWhenUserScrollingIsDisabled() {
+ val itemSizePx = 30f
+ val itemSize = with(rule.density) { itemSizePx.toDp() }
+ lateinit var state: TvLazyGridState
+ rule.setContentWithTestViewConfiguration {
+ LazyGrid(
+ cells = 1,
+ modifier = Modifier.size(itemSize * 3),
+ state = rememberLazyGridState().also { state = it },
+ userScrollEnabled = false,
+ ) {
+ items(5) {
+ Spacer(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(itemSizePx)
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ }
+
+ @Test
+ fun semanticScrollingIsDisallowedWhenUserScrollingIsDisabled() {
+ val itemSize = with(rule.density) { 30.toDp() }
+ rule.setContentWithTestViewConfiguration {
+ LazyGrid(
+ cells = 1,
+ modifier = Modifier.size(itemSize * 3).testTag(LazyGridTag),
+ userScrollEnabled = false,
+ ) {
+ items(5) {
+ Spacer(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyGridTag)
+ .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.ScrollBy))
+ .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.ScrollToIndex))
+ // but we still have a read only scroll range property
+ .assert(
+ keyIsDefined(
+ if (orientation == Orientation.Vertical) {
+ SemanticsProperties.VerticalScrollAxisRange
+ } else {
+ SemanticsProperties.HorizontalScrollAxisRange
+ }
+ )
+ )
+ }
+
+ @Test
+ fun rtl() {
+ val gridCrossAxisSize = 30
+ val gridCrossAxisSizeDp = with(rule.density) { gridCrossAxisSize.toDp() }
+ rule.setContent {
+ CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+ LazyGrid(
+ cells = 3,
+ modifier = Modifier.crossAxisSize(gridCrossAxisSizeDp)
+ ) {
+ items(3) {
+ Box(Modifier.mainAxisSize(1.dp).testTag("$it"))
+ }
+ }
+ }
+ }
+
+ val tags = if (orientation == Orientation.Vertical) {
+ listOf("0", "1", "2")
+ } else {
+ listOf("2", "1", "0")
+ }
+ rule.onNodeWithTag(tags[0])
+ .assertCrossAxisStartPositionInRootIsEqualTo(gridCrossAxisSizeDp * 2 / 3)
+ rule.onNodeWithTag(tags[1])
+ .assertCrossAxisStartPositionInRootIsEqualTo(gridCrossAxisSizeDp / 3)
+ rule.onNodeWithTag(tags[2]).assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+ }
+
+ @Test
+ fun withMissingItems() {
+ val itemMainAxisSize = with(rule.density) { 30.toDp() }
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ LazyGrid(
+ cells = 2,
+ modifier = Modifier.mainAxisSize(itemMainAxisSize + 1.dp),
+ state = state
+ ) {
+ items((0..8).map { it.toString() }) {
+ if (it != "3") {
+ Box(Modifier.mainAxisSize(itemMainAxisSize).testTag(it))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0").assertIsDisplayed()
+ rule.onNodeWithTag("1").assertIsDisplayed()
+ rule.onNodeWithTag("2").assertIsDisplayed()
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(3)
+ }
+ }
+
+ rule.onNodeWithTag("0").assertIsNotDisplayed()
+ rule.onNodeWithTag("1").assertIsNotDisplayed()
+ rule.onNodeWithTag("2").assertIsDisplayed()
+ rule.onNodeWithTag("4").assertIsDisplayed()
+ rule.onNodeWithTag("5").assertIsDisplayed()
+ rule.onNodeWithTag("6").assertDoesNotExist()
+ rule.onNodeWithTag("7").assertDoesNotExist()
+ }
+
+ @Test
+ fun passingNegativeItemsCountIsNotAllowed() {
+ var exception: Exception? = null
+ rule.setContentWithTestViewConfiguration {
+ LazyGrid(cells = 1) {
+ try {
+ items(-1) {
+ Box(Modifier)
+ }
+ } catch (e: Exception) {
+ exception = e
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(exception).isInstanceOf(IllegalArgumentException::class.java)
+ }
+ }
+
+ @Test
+ fun recomposingWithNewComposedModifierObjectIsNotCausingRemeasure() {
+ var remeasureCount = 0
+ val layoutModifier = Modifier.layout { measurable, constraints ->
+ remeasureCount++
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+ val counter = mutableStateOf(0)
+
+ rule.setContentWithTestViewConfiguration {
+ counter.value // just to trigger recomposition
+ LazyGrid(
+ cells = 1,
+ // this will return a new object everytime causing LazyGrid recomposition
+ // without causing remeasure
+ modifier = Modifier.composed { layoutModifier }
+ ) {
+ items(1) {
+ Spacer(Modifier.size(10.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(remeasureCount).isEqualTo(1)
+ counter.value++
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(remeasureCount).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun scrollingALotDoesntCauseLazyLayoutRecomposition() {
+ var recomposeCount = 0
+ lateinit var state: TvLazyGridState
+
+ rule.setContentWithTestViewConfiguration {
+ state = rememberLazyGridState()
+ LazyGrid(
+ cells = 1,
+ modifier = Modifier.composed {
+ recomposeCount++
+ Modifier
+ }.size(100.dp),
+ state
+ ) {
+ items(1000) {
+ Spacer(Modifier.size(100.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(recomposeCount).isEqualTo(1)
+
+ runBlocking {
+ state.scrollToItem(100)
+ }
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(recomposeCount).isEqualTo(1)
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ fun zIndexOnItemAffectsDrawingOrder() {
+ rule.setContentWithTestViewConfiguration {
+ LazyGrid(
+ cells = 1,
+ modifier = Modifier.size(6.dp).testTag(LazyGridTag)
+ ) {
+ items(listOf(Color.Blue, Color.Green, Color.Red)) { color ->
+ Box(
+ Modifier
+ .axisSize(6.dp, 2.dp)
+ .zIndex(if (color == Color.Green) 1f else 0f)
+ .drawBehind {
+ drawRect(
+ color,
+ topLeft = Offset(-10.dp.toPx(), -10.dp.toPx()),
+ size = Size(20.dp.toPx(), 20.dp.toPx())
+ )
+ })
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyGridTag)
+ .captureToImage()
+ .assertPixels { Color.Green }
+ }
+
+ @Test
+ fun customGridCells() {
+ val items = (1..5).map { it.toString() }
+
+ rule.setContent {
+ LazyGrid(
+ // Two columns in ratio 1:2
+ cells = object : TvGridCells {
+ override fun Density.calculateCrossAxisCellSizes(
+ availableSize: Int,
+ spacing: Int
+ ): List<Int> {
+ val availableCrossAxis = availableSize - spacing
+ val columnSize = availableCrossAxis / 3
+ return listOf(columnSize, columnSize * 2)
+ }
+ },
+ modifier = Modifier.axisSize(300.dp, 100.dp)
+ ) {
+ items(items) {
+ Spacer(Modifier.mainAxisSize(101.dp).testTag(it))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertCrossAxisSizeIsEqualTo(100.dp)
+
+ rule.onNodeWithTag("2")
+ .assertCrossAxisStartPositionInRootIsEqualTo(100.dp)
+ .assertCrossAxisSizeIsEqualTo(200.dp)
+
+ rule.onNodeWithTag("3")
+ .assertDoesNotExist()
+
+ rule.onNodeWithTag("4")
+ .assertDoesNotExist()
+
+ rule.onNodeWithTag("5")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun onlyOneInitialMeasurePass() {
+ val items by mutableStateOf((1..20).toList())
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ LazyGrid(
+ 1,
+ Modifier.requiredSize(100.dp).testTag(LazyGridTag),
+ state = state
+ ) {
+ items(items) {
+ Spacer(Modifier.requiredSize(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.numMeasurePasses).isEqualTo(1)
+ }
+ }
+}
+
+internal fun IntegerSubject.isEqualTo(expected: Int, tolerance: Int) {
+ isIn(Range.closed(expected - tolerance, expected + tolerance))
+}
+
+internal fun ComposeContentTestRule.keyPress(keyCode: Int, numberOfPresses: Int = 1) {
+ for (index in 0 until numberOfPresses)
+ InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keyCode)
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt
new file mode 100644
index 0000000..0f06a2b
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt
@@ -0,0 +1,1206 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import androidx.compose.animation.core.snap
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.tv.foundation.lazy.AutoTestFrameClock
+import androidx.tv.foundation.lazy.list.setContentWithTestViewConfiguration
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class LazyGridsContentPaddingTest {
+ private val LazyListTag = "LazyList"
+ private val ItemTag = "item"
+ private val ContainerTag = "container"
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ private var itemSize: Dp = Dp.Infinity
+ private var smallPaddingSize: Dp = Dp.Infinity
+ private var itemSizePx = 50f
+ private var smallPaddingSizePx = 12f
+
+ @Before
+ fun before() {
+ with(rule.density) {
+ itemSize = itemSizePx.toDp()
+ smallPaddingSize = smallPaddingSizePx.toDp()
+ }
+ }
+
+ @Test
+ fun verticalGrid_contentPaddingIsApplied() {
+ lateinit var state: TvLazyGridState
+ val containerSize = itemSize * 2
+ val largePaddingSize = itemSize
+ rule.setContent {
+ TvLazyVerticalGrid(
+ columns = TvGridCells.Fixed(1),
+ modifier = Modifier.requiredSize(containerSize)
+ .testTag(LazyListTag),
+ state = rememberLazyGridState().also { state = it },
+ contentPadding = PaddingValues(
+ start = smallPaddingSize,
+ top = largePaddingSize,
+ end = smallPaddingSize,
+ bottom = largePaddingSize
+ )
+ ) {
+ items(listOf(1)) {
+ Spacer(Modifier.height(itemSize).testTag(ItemTag))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(ItemTag)
+ .assertLeftPositionInRootIsEqualTo(smallPaddingSize)
+ .assertTopPositionInRootIsEqualTo(largePaddingSize)
+ .assertWidthIsEqualTo(containerSize - smallPaddingSize * 2)
+ .assertHeightIsEqualTo(itemSize)
+
+ state.scrollBy(largePaddingSize)
+
+ rule.onNodeWithTag(ItemTag)
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertHeightIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun verticalGrid_contentPaddingIsNotAffectingScrollPosition() {
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ TvLazyVerticalGrid(
+ columns = TvGridCells.Fixed(1),
+ modifier = Modifier.requiredSize(itemSize * 2)
+ .testTag(LazyListTag),
+ state = rememberLazyGridState().also { state = it },
+ contentPadding = PaddingValues(
+ top = itemSize,
+ bottom = itemSize
+ )
+ ) {
+ items(listOf(1)) {
+ Spacer(Modifier.height(itemSize).testTag(ItemTag))
+ }
+ }
+ }
+
+ state.assertScrollPosition(0, 0.dp)
+
+ state.scrollBy(itemSize)
+
+ state.assertScrollPosition(0, itemSize)
+ }
+
+ @Test
+ fun verticalGrid_scrollForwardItemWithinStartPaddingDisplayed() {
+ lateinit var state: TvLazyGridState
+ val padding = itemSize * 1.5f
+ rule.setContent {
+ TvLazyVerticalGrid(
+ columns = TvGridCells.Fixed(1),
+ modifier = Modifier.requiredSize(padding * 2 + itemSize)
+ .testTag(LazyListTag),
+ state = rememberLazyGridState().also { state = it },
+ contentPadding = PaddingValues(
+ top = padding,
+ bottom = padding
+ )
+ ) {
+ items((0..3).toList()) {
+ Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(padding)
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize + padding)
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemSize * 2 + padding)
+
+ state.scrollBy(padding)
+
+ state.assertScrollPosition(1, padding - itemSize)
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemSize * 2)
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(itemSize * 3)
+ }
+
+ @Test
+ fun verticalGrid_scrollBackwardItemWithinStartPaddingDisplayed() {
+ lateinit var state: TvLazyGridState
+ val padding = itemSize * 1.5f
+ rule.setContent {
+ TvLazyVerticalGrid(
+ columns = TvGridCells.Fixed(1),
+ modifier = Modifier.requiredSize(itemSize + padding * 2)
+ .testTag(LazyListTag),
+ state = rememberLazyGridState().also { state = it },
+ contentPadding = PaddingValues(
+ top = padding,
+ bottom = padding
+ )
+ ) {
+ items((0..3).toList()) {
+ Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
+ }
+ }
+ }
+
+ state.scrollBy(itemSize * 3)
+ state.scrollBy(-itemSize * 1.5f)
+
+ state.assertScrollPosition(1, itemSize * 0.5f)
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(itemSize * 1.5f - padding)
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize * 2.5f - padding)
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemSize * 3.5f - padding)
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(itemSize * 4.5f - padding)
+ }
+
+ @Test
+ fun verticalGrid_scrollForwardTillTheEnd() {
+ lateinit var state: TvLazyGridState
+ val padding = itemSize * 1.5f
+ rule.setContent {
+ TvLazyVerticalGrid(
+ columns = TvGridCells.Fixed(1),
+ modifier = Modifier.requiredSize(padding * 2 + itemSize)
+ .testTag(LazyListTag),
+ state = rememberLazyGridState().also { state = it },
+ contentPadding = PaddingValues(
+ top = padding,
+ bottom = padding
+ )
+ ) {
+ items((0..3).toList()) {
+ Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
+ }
+ }
+ }
+
+ state.scrollBy(itemSize * 3)
+
+ state.assertScrollPosition(3, 0.dp)
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize - padding)
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemSize * 2 - padding)
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(itemSize * 3 - padding)
+
+ // there are no space to scroll anymore, so it should change nothing
+ state.scrollBy(10.dp)
+
+ state.assertScrollPosition(3, 0.dp)
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize - padding)
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemSize * 2 - padding)
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(itemSize * 3 - padding)
+ }
+
+ @Test
+ fun verticalGrid_scrollForwardTillTheEndAndABitBack() {
+ lateinit var state: TvLazyGridState
+ val padding = itemSize * 1.5f
+ rule.setContent {
+ TvLazyVerticalGrid(
+ columns = TvGridCells.Fixed(1),
+ modifier = Modifier.requiredSize(padding * 2 + itemSize)
+ .testTag(LazyListTag),
+ state = rememberLazyGridState().also { state = it },
+ contentPadding = PaddingValues(
+ top = padding,
+ bottom = padding
+ )
+ ) {
+ items((0..3).toList()) {
+ Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
+ }
+ }
+ }
+
+ state.scrollBy(itemSize * 3)
+ state.scrollBy(-itemSize / 2)
+
+ state.assertScrollPosition(2, itemSize / 2)
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize * 1.5f - padding)
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemSize * 2.5f - padding)
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(itemSize * 3.5f - padding)
+ }
+
+ @Test
+ fun verticalGrid_contentPaddingFixedWidthContainer() {
+ rule.setContent {
+ Box(modifier = Modifier.testTag(ContainerTag).width(itemSize + 8.dp)) {
+ TvLazyVerticalGrid(
+ columns = TvGridCells.Fixed(1),
+ contentPadding = PaddingValues(
+ start = 2.dp,
+ top = 4.dp,
+ end = 6.dp,
+ bottom = 8.dp
+ )
+ ) {
+ items(listOf(1)) {
+ Spacer(Modifier.size(itemSize).testTag(ItemTag))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag(ItemTag)
+ .assertLeftPositionInRootIsEqualTo(2.dp)
+ .assertTopPositionInRootIsEqualTo(4.dp)
+ .assertWidthIsEqualTo(itemSize)
+ .assertHeightIsEqualTo(itemSize)
+
+ rule.onNodeWithTag(ContainerTag)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertWidthIsEqualTo(itemSize + 2.dp + 6.dp)
+ .assertHeightIsEqualTo(itemSize + 4.dp + 8.dp)
+ }
+
+ @Test
+ fun verticalGrid_contentPaddingAndNoContent() {
+ rule.setContent {
+ Box(modifier = Modifier.testTag(ContainerTag)) {
+ TvLazyVerticalGrid(
+ columns = TvGridCells.Fixed(1),
+ contentPadding = PaddingValues(
+ start = 2.dp,
+ top = 4.dp,
+ end = 6.dp,
+ bottom = 8.dp
+ )
+ ) { }
+ }
+ }
+
+ rule.onNodeWithTag(ContainerTag)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertWidthIsEqualTo(8.dp)
+ .assertHeightIsEqualTo(12.dp)
+ }
+
+ @Test
+ fun verticalGrid_contentPaddingAndZeroSizedItem() {
+ rule.setContent {
+ Box(modifier = Modifier.testTag(ContainerTag)) {
+ TvLazyVerticalGrid(
+ columns = TvGridCells.Fixed(1),
+ contentPadding = PaddingValues(
+ start = 2.dp,
+ top = 4.dp,
+ end = 6.dp,
+ bottom = 8.dp
+ )
+ ) {
+ items(0) { }
+ }
+ }
+ }
+
+ rule.onNodeWithTag(ContainerTag)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertWidthIsEqualTo(8.dp)
+ .assertHeightIsEqualTo(12.dp)
+ }
+
+ @Test
+ fun verticalGrid_contentPaddingAndReverseLayout() {
+ val topPadding = itemSize * 2
+ val bottomPadding = itemSize / 2
+ val listSize = itemSize * 3
+ lateinit var state: TvLazyGridState
+ rule.setContentWithTestViewConfiguration {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ reverseLayout = true,
+ state = rememberLazyGridState().also { state = it },
+ modifier = Modifier.size(listSize),
+ contentPadding = PaddingValues(top = topPadding, bottom = bottomPadding),
+ ) {
+ items(3) { index ->
+ Box(Modifier.size(itemSize).testTag("$index"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(listSize - bottomPadding - itemSize)
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(listSize - bottomPadding - itemSize * 2)
+ // Partially visible.
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(-itemSize / 2)
+
+ // Scroll to the top.
+ state.scrollBy(itemSize * 2.5f)
+
+ rule.onNodeWithTag("2").assertTopPositionInRootIsEqualTo(topPadding)
+ // Shouldn't be visible
+ rule.onNodeWithTag("1").assertIsNotDisplayed()
+ rule.onNodeWithTag("0").assertIsNotDisplayed()
+ }
+
+ @Test
+ fun column_overscrollWithContentPadding() {
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize + smallPaddingSize * 2)) {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ state = state,
+ contentPadding = PaddingValues(
+ vertical = smallPaddingSize
+ )
+ ) {
+ items(2) {
+ Box(Modifier.testTag("$it").height(itemSize))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(smallPaddingSize)
+ .assertHeightIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(smallPaddingSize + itemSize)
+ .assertHeightIsEqualTo(itemSize)
+
+ rule.runOnIdle {
+ runBlocking {
+ // itemSizePx is the maximum offset, plus if we overscroll the content padding
+ // the layout mechanism will decide the item 0 is not needed until we start
+ // filling the over scrolled gap.
+ state.scrollBy(value = itemSizePx + smallPaddingSizePx * 1.5f)
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(smallPaddingSize)
+ .assertHeightIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(smallPaddingSize - itemSize)
+ .assertHeightIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun totalPaddingLargerParentSize_initialState() {
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ state = state,
+ contentPadding = PaddingValues(vertical = itemSize)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("1")
+ .assertDoesNotExist()
+
+ rule.runOnIdle {
+ state.assertScrollPosition(0, 0.dp)
+ state.assertVisibleItems(0 to 0.dp)
+ state.assertLayoutInfoOffsetRange(-itemSize, itemSize * 0.5f)
+ }
+ }
+
+ @Test
+ fun totalPaddingLargerParentSize_scrollByPadding() {
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ state = state,
+ contentPadding = PaddingValues(vertical = itemSize)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ state.scrollBy(itemSize)
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("2")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ state.assertScrollPosition(1, 0.dp)
+ state.assertVisibleItems(0 to -itemSize, 1 to 0.dp)
+ }
+ }
+
+ @Test
+ fun totalPaddingLargerParentSize_scrollToLastItem() {
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ state = state,
+ contentPadding = PaddingValues(vertical = itemSize)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ state.scrollTo(3)
+
+ rule.onNodeWithTag("1")
+ .assertDoesNotExist()
+
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+
+ rule.runOnIdle {
+ state.assertScrollPosition(3, 0.dp)
+ state.assertVisibleItems(2 to -itemSize, 3 to 0.dp)
+ }
+ }
+
+ @Test
+ fun totalPaddingLargerParentSize_scrollToLastItemByDelta() {
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ state = state,
+ contentPadding = PaddingValues(vertical = itemSize)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ state.scrollBy(itemSize * 3)
+
+ rule.onNodeWithTag("1")
+ .assertIsNotDisplayed()
+
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+
+ rule.runOnIdle {
+ state.assertScrollPosition(3, 0.dp)
+ state.assertVisibleItems(2 to -itemSize, 3 to 0.dp)
+ }
+ }
+
+ @Test
+ fun totalPaddingLargerParentSize_scrollTillTheEnd() {
+ // the whole end content padding is displayed
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ state = state,
+ contentPadding = PaddingValues(vertical = itemSize)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ state.scrollBy(itemSize * 4.5f)
+
+ rule.onNodeWithTag("2")
+ .assertIsNotDisplayed()
+
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(-itemSize * 0.5f)
+
+ rule.runOnIdle {
+ state.assertScrollPosition(3, itemSize * 1.5f)
+ state.assertVisibleItems(3 to -itemSize * 1.5f)
+ }
+ }
+
+ @Test
+ fun eachPaddingLargerParentSize_initialState() {
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ state = state,
+ contentPadding = PaddingValues(vertical = itemSize * 2)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ state.assertScrollPosition(0, 0.dp)
+ state.assertVisibleItems(0 to 0.dp)
+ state.assertLayoutInfoOffsetRange(-itemSize * 2, -itemSize * 0.5f)
+ }
+ }
+
+ @Test
+ fun eachPaddingLargerParentSize_scrollByPadding() {
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ state = state,
+ contentPadding = PaddingValues(vertical = itemSize * 2)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ state.scrollBy(itemSize * 2)
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("2")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ state.assertScrollPosition(2, 0.dp)
+ state.assertVisibleItems(0 to -itemSize * 2, 1 to -itemSize, 2 to 0.dp)
+ }
+ }
+
+ @Test
+ fun eachPaddingLargerParentSize_scrollToLastItem() {
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ state = state,
+ contentPadding = PaddingValues(vertical = itemSize * 2)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ state.scrollTo(3)
+
+ rule.onNodeWithTag("0")
+ .assertIsNotDisplayed()
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("3")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ state.assertScrollPosition(3, 0.dp)
+ state.assertVisibleItems(1 to -itemSize * 2, 2 to -itemSize, 3 to 0.dp)
+ }
+ }
+
+ @Test
+ fun eachPaddingLargerParentSize_scrollToLastItemByDelta() {
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ state = state,
+ contentPadding = PaddingValues(vertical = itemSize * 2)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ state.scrollBy(itemSize * 3)
+
+ rule.onNodeWithTag("0")
+ .assertIsNotDisplayed()
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("3")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ state.assertScrollPosition(3, 0.dp)
+ state.assertVisibleItems(1 to -itemSize * 2, 2 to -itemSize, 3 to 0.dp)
+ }
+ }
+
+ @Test
+ fun eachPaddingLargerParentSize_scrollTillTheEnd() {
+ // only the end content padding is displayed
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ state = state,
+ contentPadding = PaddingValues(vertical = itemSize * 2)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ state.scrollBy(
+ itemSize * 1.5f + // container size
+ itemSize * 2 + // start padding
+ itemSize * 3 // all items
+ )
+
+ rule.onNodeWithTag("3")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ state.assertScrollPosition(3, itemSize * 3.5f)
+ state.assertVisibleItems(3 to -itemSize * 3.5f)
+ }
+ }
+
+ // @Test
+ // fun row_contentPaddingIsApplied() {
+ // lateinit var state: LazyGridState
+ // val containerSize = itemSize * 2
+ // val largePaddingSize = itemSize
+ // rule.setContent {
+ // LazyRow(
+ // modifier = Modifier.requiredSize(containerSize)
+ // .testTag(LazyListTag),
+ // state = rememberLazyGridState().also { state = it },
+ // contentPadding = PaddingValues(
+ // top = smallPaddingSize,
+ // start = largePaddingSize,
+ // bottom = smallPaddingSize,
+ // end = largePaddingSize
+ // )
+ // ) {
+ // items(listOf(1)) {
+ // Spacer(Modifier.fillParentMaxHeight().width(itemSize).testTag(ItemTag))
+ // }
+ // }
+ // }
+
+ // rule.onNodeWithTag(ItemTag)
+ // .assertTopPositionInRootIsEqualTo(smallPaddingSize)
+ // .assertLeftPositionInRootIsEqualTo(largePaddingSize)
+ // .assertHeightIsEqualTo(containerSize - smallPaddingSize * 2)
+ // .assertWidthIsEqualTo(itemSize)
+
+ // state.scrollBy(largePaddingSize)
+
+ // rule.onNodeWithTag(ItemTag)
+ // .assertLeftPositionInRootIsEqualTo(0.dp)
+ // .assertWidthIsEqualTo(itemSize)
+ // }
+
+ // @Test
+ // fun row_contentPaddingIsNotAffectingScrollPosition() {
+ // lateinit var state: LazyGridState
+ // val itemSize = with(rule.density) {
+ // 50.dp.roundToPx().toDp()
+ // }
+ // rule.setContent {
+ // LazyRow(
+ // modifier = Modifier.requiredSize(itemSize * 2)
+ // .testTag(LazyListTag),
+ // state = rememberLazyGridState().also { state = it },
+ // contentPadding = PaddingValues(
+ // start = itemSize,
+ // end = itemSize
+ // )
+ // ) {
+ // items(listOf(1)) {
+ // Spacer(Modifier.fillParentMaxHeight().width(itemSize).testTag(ItemTag))
+ // }
+ // }
+ // }
+
+ // state.assertScrollPosition(0, 0.dp)
+
+ // state.scrollBy(itemSize)
+
+ // state.assertScrollPosition(0, itemSize)
+ // }
+
+ // @Test
+ // fun row_scrollForwardItemWithinStartPaddingDisplayed() {
+ // lateinit var state: LazyGridState
+ // val padding = itemSize * 1.5f
+ // rule.setContent {
+ // LazyRow(
+ // modifier = Modifier.requiredSize(padding * 2 + itemSize)
+ // .testTag(LazyListTag),
+ // state = rememberLazyGridState().also { state = it },
+ // contentPadding = PaddingValues(
+ // start = padding,
+ // end = padding
+ // )
+ // ) {
+ // items((0..3).toList()) {
+ // Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
+ // }
+ // }
+ // }
+
+ // rule.onNodeWithTag("0")
+ // .assertLeftPositionInRootIsEqualTo(padding)
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(itemSize + padding)
+ // rule.onNodeWithTag("2")
+ // .assertLeftPositionInRootIsEqualTo(itemSize * 2 + padding)
+
+ // state.scrollBy(padding)
+
+ // state.assertScrollPosition(1, padding - itemSize)
+
+ // rule.onNodeWithTag("0")
+ // .assertLeftPositionInRootIsEqualTo(0.dp)
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(itemSize)
+ // rule.onNodeWithTag("2")
+ // .assertLeftPositionInRootIsEqualTo(itemSize * 2)
+ // rule.onNodeWithTag("3")
+ // .assertLeftPositionInRootIsEqualTo(itemSize * 3)
+ // }
+
+ // @Test
+ // fun row_scrollBackwardItemWithinStartPaddingDisplayed() {
+ // lateinit var state: LazyGridState
+ // val padding = itemSize * 1.5f
+ // rule.setContent {
+ // LazyRow(
+ // modifier = Modifier.requiredSize(itemSize + padding * 2)
+ // .testTag(LazyListTag),
+ // state = rememberLazyGridState().also { state = it },
+ // contentPadding = PaddingValues(
+ // start = padding,
+ // end = padding
+ // )
+ // ) {
+ // items((0..3).toList()) {
+ // Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
+ // }
+ // }
+ // }
+
+ // state.scrollBy(itemSize * 3)
+ // state.scrollBy(-itemSize * 1.5f)
+
+ // state.assertScrollPosition(1, itemSize * 0.5f)
+
+ // rule.onNodeWithTag("0")
+ // .assertLeftPositionInRootIsEqualTo(itemSize * 1.5f - padding)
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(itemSize * 2.5f - padding)
+ // rule.onNodeWithTag("2")
+ // .assertLeftPositionInRootIsEqualTo(itemSize * 3.5f - padding)
+ // rule.onNodeWithTag("3")
+ // .assertLeftPositionInRootIsEqualTo(itemSize * 4.5f - padding)
+ // }
+
+ // @Test
+ // fun row_scrollForwardTillTheEnd() {
+ // lateinit var state: LazyGridState
+ // val padding = itemSize * 1.5f
+ // rule.setContent {
+ // LazyRow(
+ // modifier = Modifier.requiredSize(padding * 2 + itemSize)
+ // .testTag(LazyListTag),
+ // state = rememberLazyGridState().also { state = it },
+ // contentPadding = PaddingValues(
+ // start = padding,
+ // end = padding
+ // )
+ // ) {
+ // items((0..3).toList()) {
+ // Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
+ // }
+ // }
+ // }
+
+ // state.scrollBy(itemSize * 3)
+
+ // state.assertScrollPosition(3, 0.dp)
+
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(itemSize - padding)
+ // rule.onNodeWithTag("2")
+ // .assertLeftPositionInRootIsEqualTo(itemSize * 2 - padding)
+ // rule.onNodeWithTag("3")
+ // .assertLeftPositionInRootIsEqualTo(itemSize * 3 - padding)
+
+ // // there are no space to scroll anymore, so it should change nothing
+ // state.scrollBy(10.dp)
+
+ // state.assertScrollPosition(3, 0.dp)
+
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(itemSize - padding)
+ // rule.onNodeWithTag("2")
+ // .assertLeftPositionInRootIsEqualTo(itemSize * 2 - padding)
+ // rule.onNodeWithTag("3")
+ // .assertLeftPositionInRootIsEqualTo(itemSize * 3 - padding)
+ // }
+
+ // @Test
+ // fun row_scrollForwardTillTheEndAndABitBack() {
+ // lateinit var state: LazyGridState
+ // val padding = itemSize * 1.5f
+ // rule.setContent {
+ // LazyRow(
+ // modifier = Modifier.requiredSize(padding * 2 + itemSize)
+ // .testTag(LazyListTag),
+ // state = rememberLazyGridState().also { state = it },
+ // contentPadding = PaddingValues(
+ // start = padding,
+ // end = padding
+ // )
+ // ) {
+ // items((0..3).toList()) {
+ // Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
+ // }
+ // }
+ // }
+
+ // state.scrollBy(itemSize * 3)
+ // state.scrollBy(-itemSize / 2)
+
+ // state.assertScrollPosition(2, itemSize / 2)
+
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(itemSize * 1.5f - padding)
+ // rule.onNodeWithTag("2")
+ // .assertLeftPositionInRootIsEqualTo(itemSize * 2.5f - padding)
+ // rule.onNodeWithTag("3")
+ // .assertLeftPositionInRootIsEqualTo(itemSize * 3.5f - padding)
+ // }
+
+ // @Test
+ // fun row_contentPaddingAndWrapContent() {
+ // rule.setContent {
+ // Box(modifier = Modifier.testTag(ContainerTag)) {
+ // LazyRow(
+ // contentPadding = PaddingValues(
+ // start = 2.dp,
+ // top = 4.dp,
+ // end = 6.dp,
+ // bottom = 8.dp
+ // )
+ // ) {
+ // items(listOf(1)) {
+ // Spacer(Modifier.requiredSize(itemSize).testTag(ItemTag))
+ // }
+ // }
+ // }
+ // }
+
+ // rule.onNodeWithTag(ItemTag)
+ // .assertLeftPositionInRootIsEqualTo(2.dp)
+ // .assertTopPositionInRootIsEqualTo(4.dp)
+ // .assertWidthIsEqualTo(itemSize)
+ // .assertHeightIsEqualTo(itemSize)
+
+ // rule.onNodeWithTag(ContainerTag)
+ // .assertLeftPositionInRootIsEqualTo(0.dp)
+ // .assertTopPositionInRootIsEqualTo(0.dp)
+ // .assertWidthIsEqualTo(itemSize + 2.dp + 6.dp)
+ // .assertHeightIsEqualTo(itemSize + 4.dp + 8.dp)
+ // }
+
+ // @Test
+ // fun row_contentPaddingAndNoContent() {
+ // rule.setContent {
+ // Box(modifier = Modifier.testTag(ContainerTag)) {
+ // LazyRow(
+ // contentPadding = PaddingValues(
+ // start = 2.dp,
+ // top = 4.dp,
+ // end = 6.dp,
+ // bottom = 8.dp
+ // )
+ // ) { }
+ // }
+ // }
+
+ // rule.onNodeWithTag(ContainerTag)
+ // .assertLeftPositionInRootIsEqualTo(0.dp)
+ // .assertTopPositionInRootIsEqualTo(0.dp)
+ // .assertWidthIsEqualTo(8.dp)
+ // .assertHeightIsEqualTo(12.dp)
+ // }
+
+ // @Test
+ // fun row_contentPaddingAndZeroSizedItem() {
+ // rule.setContent {
+ // Box(modifier = Modifier.testTag(ContainerTag)) {
+ // LazyRow(
+ // contentPadding = PaddingValues(
+ // start = 2.dp,
+ // top = 4.dp,
+ // end = 6.dp,
+ // bottom = 8.dp
+ // )
+ // ) {
+ // items(0) {}
+ // }
+ // }
+ // }
+
+ // rule.onNodeWithTag(ContainerTag)
+ // .assertLeftPositionInRootIsEqualTo(0.dp)
+ // .assertTopPositionInRootIsEqualTo(0.dp)
+ // .assertWidthIsEqualTo(8.dp)
+ // .assertHeightIsEqualTo(12.dp)
+ // }
+
+ // @Test
+ // fun row_contentPaddingAndReverseLayout() {
+ // val startPadding = itemSize * 2
+ // val endPadding = itemSize / 2
+ // val listSize = itemSize * 3
+ // lateinit var state: LazyGridState
+ // rule.setContentWithTestViewConfiguration {
+ // LazyRow(
+ // reverseLayout = true,
+ // state = rememberLazyGridState().also { state = it },
+ // modifier = Modifier.requiredSize(listSize),
+ // contentPadding = PaddingValues(start = startPadding, end = endPadding),
+ // ) {
+ // items(3) { index ->
+ // Box(Modifier.requiredSize(itemSize).testTag("$index"))
+ // }
+ // }
+ // }
+
+ // rule.onNodeWithTag("0")
+ // .assertLeftPositionInRootIsEqualTo(listSize - endPadding - itemSize)
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(listSize - endPadding - itemSize * 2)
+ // // Partially visible.
+ // rule.onNodeWithTag("2")
+ // .assertLeftPositionInRootIsEqualTo(-itemSize / 2)
+
+ // // Scroll to the top.
+ // state.scrollBy(itemSize * 2.5f)
+
+ // rule.onNodeWithTag("2").assertLeftPositionInRootIsEqualTo(startPadding)
+ // // Shouldn't be visible
+ // rule.onNodeWithTag("1").assertIsNotDisplayed()
+ // rule.onNodeWithTag("0").assertIsNotDisplayed()
+ // }
+
+ // @Test
+ // fun row_overscrollWithContentPadding() {
+ // lateinit var state: LazyListState
+ // rule.setContent {
+ // state = rememberLazyListState()
+ // Box(modifier = Modifier.testTag(ContainerTag).size(itemSize + smallPaddingSize * 2)) {
+ // LazyRow(
+ // state = state,
+ // contentPadding = PaddingValues(
+ // horizontal = smallPaddingSize
+ // )
+ // ) {
+ // items(2) {
+ // Box(Modifier.testTag("$it").fillParentMaxSize())
+ // }
+ // }
+ // }
+ // }
+
+ // rule.onNodeWithTag("0")
+ // .assertLeftPositionInRootIsEqualTo(smallPaddingSize)
+ // .assertWidthIsEqualTo(itemSize)
+
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(smallPaddingSize + itemSize)
+ // .assertWidthIsEqualTo(itemSize)
+
+ // rule.runOnIdle {
+ // runBlocking {
+ // // itemSizePx is the maximum offset, plus if we overscroll the content padding
+ // // the layout mechanism will decide the item 0 is not needed until we start
+ // // filling the over scrolled gap.
+ // state.scrollBy(value = itemSizePx + smallPaddingSizePx * 1.5f)
+ // }
+ // }
+
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(smallPaddingSize)
+ // .assertWidthIsEqualTo(itemSize)
+
+ // rule.onNodeWithTag("0")
+ // .assertLeftPositionInRootIsEqualTo(smallPaddingSize - itemSize)
+ // .assertWidthIsEqualTo(itemSize)
+ // }
+
+ private fun TvLazyGridState.scrollBy(offset: Dp) {
+ runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
+ animateScrollBy(with(rule.density) { offset.roundToPx().toFloat() }, snap())
+ }
+ }
+
+ private fun TvLazyGridState.assertScrollPosition(index: Int, offset: Dp) = with(rule.density) {
+ assertThat([email protected]).isEqualTo(index)
+ assertThat(firstVisibleItemScrollOffset.toDp().value).isWithin(0.5f).of(offset.value)
+ }
+
+ private fun TvLazyGridState.assertLayoutInfoOffsetRange(from: Dp, to: Dp) = with(rule.density) {
+ assertThat(layoutInfo.viewportStartOffset to layoutInfo.viewportEndOffset)
+ .isEqualTo(from.roundToPx() to to.roundToPx())
+ }
+
+ private fun TvLazyGridState.assertVisibleItems(vararg expected: Pair<Int, Dp>) =
+ with(rule.density) {
+ assertThat(layoutInfo.visibleItemsInfo.map { it.index to it.offset.y })
+ .isEqualTo(expected.map { it.first to it.second.roundToPx() })
+ }
+
+ fun TvLazyGridState.scrollTo(index: Int) {
+ runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
+ scrollToItem(index)
+ }
+ }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridsIndexedTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridsIndexedTest.kt
new file mode 100644
index 0000000..e99a386
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridsIndexedTest.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.unit.dp
+import org.junit.Rule
+import org.junit.Test
+
+class LazyGridsIndexedTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun lazyVerticalGridShowsIndexedItems() {
+ val items = (1..4).map { it.toString() }
+
+ rule.setContent {
+ TvLazyVerticalGrid(TvGridCells.Fixed(1), Modifier.height(200.dp)) {
+ itemsIndexed(items) { index, item ->
+ Spacer(
+ Modifier.height(101.dp).testTag("$index-$item")
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0-1")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("1-2")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("2-3")
+ .assertDoesNotExist()
+
+ rule.onNodeWithTag("3-4")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun verticalGridWithIndexesComposedWithCorrectIndexAndItem() {
+ val items = (0..1).map { it.toString() }
+
+ rule.setContent {
+ TvLazyVerticalGrid(TvGridCells.Fixed(1), Modifier.height(200.dp)) {
+ itemsIndexed(items) { index, item ->
+ BasicText(
+ "${index}x$item", Modifier.requiredHeight(100.dp)
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithText("0x0")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithText("1x1")
+ .assertTopPositionInRootIsEqualTo(100.dp)
+ }
+
+ // @Test
+ // fun lazyRowShowsIndexedItems() {
+ // val items = (1..4).map { it.toString() }
+
+ // rule.setContent {
+ // LazyRow(Modifier.width(200.dp)) {
+ // itemsIndexed(items) { index, item ->
+ // Spacer(
+ // Modifier.width(101.dp).fillParentMaxHeight()
+ // .testTag("$index-$item")
+ // )
+ // }
+ // }
+ // }
+
+ // rule.onNodeWithTag("0-1")
+ // .assertIsDisplayed()
+
+ // rule.onNodeWithTag("1-2")
+ // .assertIsDisplayed()
+
+ // rule.onNodeWithTag("2-3")
+ // .assertDoesNotExist()
+
+ // rule.onNodeWithTag("3-4")
+ // .assertDoesNotExist()
+ // }
+
+ // @Test
+ // fun rowWithIndexesComposedWithCorrectIndexAndItem() {
+ // val items = (0..1).map { it.toString() }
+
+ // rule.setContent {
+ // LazyRow(Modifier.width(200.dp)) {
+ // itemsIndexed(items) { index, item ->
+ // BasicText(
+ // "${index}x$item", Modifier.fillParentMaxHeight().requiredWidth(100.dp)
+ // )
+ // }
+ // }
+ // }
+
+ // rule.onNodeWithText("0x0")
+ // .assertLeftPositionInRootIsEqualTo(0.dp)
+
+ // rule.onNodeWithText("1x1")
+ // .assertLeftPositionInRootIsEqualTo(100.dp)
+ // }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt
new file mode 100644
index 0000000..c3cd0a9
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt
@@ -0,0 +1,520 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.tv.foundation.lazy.list.setContentWithTestViewConfiguration
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class LazyGridsReverseLayoutTest {
+
+ private val ContainerTag = "ContainerTag"
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ private var itemSize: Dp = Dp.Infinity
+
+ @Before
+ fun before() {
+ with(rule.density) {
+ itemSize = 50.toDp()
+ }
+ }
+
+ @Test
+ fun verticalGrid_reverseLayout() {
+ rule.setContentWithTestViewConfiguration {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(2),
+ Modifier.width(itemSize * 2),
+ reverseLayout = true
+ ) {
+ items(4) {
+ Box(Modifier.height(itemSize).testTag(it.toString()))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun column_emitTwoElementsAsOneItem() {
+ rule.setContentWithTestViewConfiguration {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(2),
+ Modifier.width(itemSize * 2),
+ reverseLayout = true
+ ) {
+ items(4) {
+ Box(Modifier.height(itemSize).testTag((it * 2).toString()))
+ Box(Modifier.height(itemSize).testTag((it * 2 + 1).toString()))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("4")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("5")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("6")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ rule.onNodeWithTag("7")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun verticalGrid_initialScrollPositionIs0() {
+ lateinit var state: TvLazyGridState
+ rule.setContentWithTestViewConfiguration {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(2),
+ reverseLayout = true,
+ state = rememberLazyGridState().also { state = it },
+ modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+ ) {
+ items((0..5).toList()) {
+ Box(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ }
+ }
+
+ @Test
+ fun verticalGrid_scrollInWrongDirectionDoesNothing() {
+ lateinit var state: TvLazyGridState
+ rule.setContentWithTestViewConfiguration {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ reverseLayout = true,
+ state = rememberLazyGridState().also { state = it },
+ modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+ ) {
+ items((0..2).toList()) {
+ Box(Modifier.size(itemSize).testTag("$it").focusable())
+ }
+ }
+ }
+
+ // we scroll down and as the scrolling is reversed it shouldn't affect anything
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 2)
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ }
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun verticalGrid_scrollForwardHalfWay() {
+ lateinit var state: TvLazyGridState
+ rule.setContentWithTestViewConfiguration {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ reverseLayout = true,
+ state = rememberLazyGridState().also { state = it },
+ modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
+ ) {
+ items((0..2).toList()) {
+ Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
+ }
+ }
+ }
+
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
+
+ val scrolled = rule.runOnIdle {
+ assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
+ }
+
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(-itemSize + scrolled)
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(scrolled)
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(itemSize + scrolled)
+ }
+
+ // @Test
+ // fun row_emitTwoElementsAsOneItem_positionedReversed() {
+ // rule.setContentWithTestViewConfiguration {
+ // LazyRow(
+ // reverseLayout = true
+ // ) {
+ // item {
+ // Box(Modifier.requiredSize(itemSize).testTag("0"))
+ // Box(Modifier.requiredSize(itemSize).testTag("1"))
+ // }
+ // }
+ // }
+
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(0.dp)
+ // rule.onNodeWithTag("0")
+ // .assertLeftPositionInRootIsEqualTo(itemSize)
+ // }
+
+ // @Test
+ // fun row_emitTwoItems_positionedReversed() {
+ // rule.setContentWithTestViewConfiguration {
+ // LazyRow(
+ // reverseLayout = true
+ // ) {
+ // item {
+ // Box(Modifier.requiredSize(itemSize).testTag("0"))
+ // }
+ // item {
+ // Box(Modifier.requiredSize(itemSize).testTag("1"))
+ // }
+ // }
+ // }
+
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(0.dp)
+ // rule.onNodeWithTag("0")
+ // .assertLeftPositionInRootIsEqualTo(itemSize)
+ // }
+
+ // @Test
+ // fun row_initialScrollPositionIs0() {
+ // lateinit var state: LazyListState
+ // rule.setContentWithTestViewConfiguration {
+ // LazyRow(
+ // reverseLayout = true,
+ // state = rememberLazyListState().also { state = it },
+ // modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
+ // ) {
+ // items((0..2).toList()) {
+ // Box(Modifier.requiredSize(itemSize).testTag("$it"))
+ // }
+ // }
+ // }
+
+ // rule.runOnIdle {
+ // assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ // assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ // }
+ // }
+
+ // @Test
+ // fun row_scrollInWrongDirectionDoesNothing() {
+ // lateinit var state: LazyListState
+ // rule.setContentWithTestViewConfiguration {
+ // LazyRow(
+ // reverseLayout = true,
+ // state = rememberLazyListState().also { state = it },
+ // modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
+ // ) {
+ // items((0..2).toList()) {
+ // Box(Modifier.requiredSize(itemSize).testTag("$it"))
+ // }
+ // }
+ // }
+
+ // // we scroll down and as the scrolling is reversed it shouldn't affect anything
+ // rule.onNodeWithTag(ContainerTag)
+ // .scrollBy(x = itemSize, density = rule.density)
+
+ // rule.runOnIdle {
+ // assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ // assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ // }
+
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(0.dp)
+ // rule.onNodeWithTag("0")
+ // .assertLeftPositionInRootIsEqualTo(itemSize)
+ // }
+
+ // @Test
+ // fun row_scrollForwardHalfWay() {
+ // lateinit var state: LazyListState
+ // rule.setContentWithTestViewConfiguration {
+ // LazyRow(
+ // reverseLayout = true,
+ // state = rememberLazyListState().also { state = it },
+ // modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
+ // ) {
+ // items((0..2).toList()) {
+ // Box(Modifier.requiredSize(itemSize).testTag("$it"))
+ // }
+ // }
+ // }
+
+ // rule.onNodeWithTag(ContainerTag)
+ // .scrollBy(x = -itemSize * 0.5f, density = rule.density)
+
+ // val scrolled = rule.runOnIdle {
+ // assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+ // assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ // with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
+ // }
+
+ // rule.onNodeWithTag("2")
+ // .assertLeftPositionInRootIsEqualTo(-itemSize + scrolled)
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(scrolled)
+ // rule.onNodeWithTag("0")
+ // .assertLeftPositionInRootIsEqualTo(itemSize + scrolled)
+ // }
+
+ // @Test
+ // fun row_scrollForwardTillTheEnd() {
+ // lateinit var state: LazyListState
+ // rule.setContentWithTestViewConfiguration {
+ // LazyRow(
+ // reverseLayout = true,
+ // state = rememberLazyListState().also { state = it },
+ // modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
+ // ) {
+ // items((0..3).toList()) {
+ // Box(Modifier.requiredSize(itemSize).testTag("$it"))
+ // }
+ // }
+ // }
+
+ // // we scroll a bit more than it is possible just to make sure we would stop correctly
+ // rule.onNodeWithTag(ContainerTag)
+ // .scrollBy(x = -itemSize * 2.2f, density = rule.density)
+
+ // rule.runOnIdle {
+ // with(rule.density) {
+ // val realOffset = state.firstVisibleItemScrollOffset.toDp() +
+ // itemSize * state.firstVisibleItemIndex
+ // assertThat(realOffset).isEqualTo(itemSize * 2)
+ // }
+ // }
+
+ // rule.onNodeWithTag("3")
+ // .assertLeftPositionInRootIsEqualTo(0.dp)
+ // rule.onNodeWithTag("2")
+ // .assertLeftPositionInRootIsEqualTo(itemSize)
+ // }
+
+ // @Test
+ // fun row_rtl_emitTwoElementsAsOneItem_positionedReversed() {
+ // rule.setContentWithTestViewConfiguration {
+ // CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+ // LazyRow(
+ // reverseLayout = true
+ // ) {
+ // item {
+ // Box(Modifier.requiredSize(itemSize).testTag("0"))
+ // Box(Modifier.requiredSize(itemSize).testTag("1"))
+ // }
+ // }
+ // }
+ // }
+
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(itemSize)
+ // rule.onNodeWithTag("0")
+ // .assertLeftPositionInRootIsEqualTo(0.dp)
+ // }
+
+ // @Test
+ // fun row_rtl_emitTwoItems_positionedReversed() {
+ // rule.setContentWithTestViewConfiguration {
+ // CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+ // LazyRow(
+ // reverseLayout = true
+ // ) {
+ // item {
+ // Box(Modifier.requiredSize(itemSize).testTag("0"))
+ // }
+ // item {
+ // Box(Modifier.requiredSize(itemSize).testTag("1"))
+ // }
+ // }
+ // }
+ // }
+
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(itemSize)
+ // rule.onNodeWithTag("0")
+ // .assertLeftPositionInRootIsEqualTo(0.dp)
+ // }
+
+ // @Test
+ // fun row_rtl_scrollForwardHalfWay() {
+ // lateinit var state: LazyListState
+ // rule.setContentWithTestViewConfiguration {
+ // CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+ // LazyRow(
+ // reverseLayout = true,
+ // state = rememberLazyListState().also { state = it },
+ // modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
+ // ) {
+ // items((0..2).toList()) {
+ // Box(Modifier.requiredSize(itemSize).testTag("$it"))
+ // }
+ // }
+ // }
+ // }
+
+ // rule.onNodeWithTag(ContainerTag)
+ // .scrollBy(x = itemSize * 0.5f, density = rule.density)
+
+ // val scrolled = rule.runOnIdle {
+ // assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+ // assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ // with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
+ // }
+
+ // rule.onNodeWithTag("0")
+ // .assertLeftPositionInRootIsEqualTo(-scrolled)
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(itemSize - scrolled)
+ // rule.onNodeWithTag("2")
+ // .assertLeftPositionInRootIsEqualTo(itemSize * 2 - scrolled)
+ // }
+
+ @Test
+ fun verticalGrid_whenParameterChanges() {
+ var reverse by mutableStateOf(true)
+ rule.setContentWithTestViewConfiguration {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(2),
+ Modifier.width(itemSize * 2),
+ reverseLayout = reverse
+ ) {
+ items(4) {
+ Box(Modifier.size(itemSize).testTag(it.toString()))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+
+ rule.runOnIdle {
+ reverse = false
+ }
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ }
+
+ // @Test
+ // fun row_whenParameterChanges() {
+ // var reverse by mutableStateOf(true)
+ // rule.setContentWithTestViewConfiguration {
+ // LazyRow(
+ // reverseLayout = reverse
+ // ) {
+ // item {
+ // Box(Modifier.requiredSize(itemSize).testTag("0"))
+ // Box(Modifier.requiredSize(itemSize).testTag("1"))
+ // }
+ // }
+ // }
+
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(0.dp)
+ // rule.onNodeWithTag("0")
+ // .assertLeftPositionInRootIsEqualTo(itemSize)
+
+ // rule.runOnIdle {
+ // reverse = false
+ // }
+
+ // rule.onNodeWithTag("0")
+ // .assertLeftPositionInRootIsEqualTo(0.dp)
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(itemSize)
+ // }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyItemStateRestoration.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyItemStateRestoration.kt
new file mode 100644
index 0000000..f4e519e
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyItemStateRestoration.kt
@@ -0,0 +1,319 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.StateRestorationTester
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.lazy.list.TvLazyRow
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+
+class LazyItemStateRestoration {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun visibleItemsStateRestored() {
+ val restorationTester = StateRestorationTester(rule)
+ var counter0 = 1
+ var counter1 = 10
+ var counter2 = 100
+ var realState = arrayOf(0, 0, 0)
+ restorationTester.setContent {
+ TvLazyVerticalGrid(TvGridCells.Fixed(1)) {
+ item {
+ realState[0] = rememberSaveable { counter0++ }
+ Box(Modifier.requiredSize(1.dp))
+ }
+ items((1..2).toList()) {
+ if (it == 1) {
+ realState[1] = rememberSaveable { counter1++ }
+ } else {
+ realState[2] = rememberSaveable { counter2++ }
+ }
+ Box(Modifier.requiredSize(1.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(realState[0]).isEqualTo(1)
+ assertThat(realState[1]).isEqualTo(10)
+ assertThat(realState[2]).isEqualTo(100)
+ realState = arrayOf(0, 0, 0)
+ }
+
+ restorationTester.emulateSavedInstanceStateRestore()
+
+ rule.runOnIdle {
+ assertThat(realState[0]).isEqualTo(1)
+ assertThat(realState[1]).isEqualTo(10)
+ assertThat(realState[2]).isEqualTo(100)
+ }
+ }
+
+ @Test
+ fun itemsStateRestoredWhenWeScrolledBackToIt() {
+ val restorationTester = StateRestorationTester(rule)
+ var counter0 = 1
+ lateinit var state: TvLazyGridState
+ var itemDisposed = false
+ var realState = 0
+ restorationTester.setContent {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ Modifier.requiredSize(20.dp),
+ state = rememberLazyGridState().also { state = it }
+ ) {
+ items((0..10).toList()) {
+ if (it == 0) {
+ realState = rememberSaveable { counter0++ }
+ DisposableEffect(Unit) {
+ onDispose {
+ itemDisposed = true
+ }
+ }
+ }
+ Box(Modifier.requiredSize(30.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(realState).isEqualTo(1)
+ runBlocking {
+ // we scroll through multiple items to make sure the 0th element is not kept in
+ // the reusable items buffer
+ state.scrollToItem(3)
+ state.scrollToItem(5)
+ state.scrollToItem(8)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(itemDisposed).isEqualTo(true)
+ realState = 0
+ runBlocking {
+ state.scrollToItem(0, 0)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(realState).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun itemsStateRestoredWhenWeScrolledRestoredAndScrolledBackTo() {
+ val restorationTester = StateRestorationTester(rule)
+ var counter0 = 1
+ var counter1 = 10
+ lateinit var state: TvLazyGridState
+ var realState = arrayOf(0, 0)
+ restorationTester.setContent {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ Modifier.requiredSize(20.dp),
+ state = rememberLazyGridState().also { state = it }
+ ) {
+ items((0..1).toList()) {
+ if (it == 0) {
+ realState[0] = rememberSaveable { counter0++ }
+ } else {
+ realState[1] = rememberSaveable { counter1++ }
+ }
+ Box(Modifier.requiredSize(30.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(realState[0]).isEqualTo(1)
+ runBlocking {
+ state.scrollToItem(1, 5)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(realState[1]).isEqualTo(10)
+ realState = arrayOf(0, 0)
+ }
+
+ restorationTester.emulateSavedInstanceStateRestore()
+
+ rule.runOnIdle {
+ assertThat(realState[1]).isEqualTo(10)
+ runBlocking {
+ state.scrollToItem(0, 0)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(realState[0]).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun nestedLazy_itemsStateRestoredWhenWeScrolledBackToIt() {
+ val restorationTester = StateRestorationTester(rule)
+ var counter0 = 1
+ lateinit var state: TvLazyGridState
+ var itemDisposed = false
+ var realState = 0
+ restorationTester.setContent {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ Modifier.requiredSize(20.dp),
+ state = rememberLazyGridState().also { state = it }
+ ) {
+ items((0..10).toList()) {
+ if (it == 0) {
+ TvLazyRow {
+ item {
+ realState = rememberSaveable { counter0++ }
+ DisposableEffect(Unit) {
+ onDispose {
+ itemDisposed = true
+ }
+ }
+ Box(Modifier.requiredSize(30.dp))
+ }
+ }
+ } else {
+ Box(Modifier.requiredSize(30.dp))
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(realState).isEqualTo(1)
+ runBlocking {
+ // we scroll through multiple items to make sure the 0th element is not kept in
+ // the reusable items buffer
+ state.scrollToItem(3)
+ state.scrollToItem(5)
+ state.scrollToItem(8)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(itemDisposed).isEqualTo(true)
+ realState = 0
+ runBlocking {
+ state.scrollToItem(0, 0)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(realState).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun stateRestoredWhenUsedWithCustomKeys() {
+ val restorationTester = StateRestorationTester(rule)
+ var counter0 = 1
+ var counter1 = 10
+ var counter2 = 100
+ var realState = arrayOf(0, 0, 0)
+ restorationTester.setContent {
+ TvLazyVerticalGrid(TvGridCells.Fixed(1)) {
+ items(3, key = { "$it" }) {
+ if (it == 0) {
+ realState[0] = rememberSaveable { counter0++ }
+ } else if (it == 1) {
+ realState[1] = rememberSaveable { counter1++ }
+ } else {
+ realState[2] = rememberSaveable { counter2++ }
+ }
+ Box(Modifier.requiredSize(1.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(realState[0]).isEqualTo(1)
+ assertThat(realState[1]).isEqualTo(10)
+ assertThat(realState[2]).isEqualTo(100)
+ realState = arrayOf(0, 0, 0)
+ }
+
+ restorationTester.emulateSavedInstanceStateRestore()
+
+ rule.runOnIdle {
+ assertThat(realState[0]).isEqualTo(1)
+ assertThat(realState[1]).isEqualTo(10)
+ assertThat(realState[2]).isEqualTo(100)
+ }
+ }
+
+ @Test
+ fun stateRestoredWhenUsedWithCustomKeysAfterReordering() {
+ val restorationTester = StateRestorationTester(rule)
+ var counter0 = 1
+ var counter1 = 10
+ var counter2 = 100
+ var realState = arrayOf(0, 0, 0)
+ var list by mutableStateOf(listOf(0, 1, 2))
+ restorationTester.setContent {
+ TvLazyVerticalGrid(TvGridCells.Fixed(1)) {
+ items(list, key = { "$it" }) {
+ if (it == 0) {
+ realState[0] = rememberSaveable { counter0++ }
+ } else if (it == 1) {
+ realState[1] = rememberSaveable { counter1++ }
+ } else {
+ realState[2] = rememberSaveable { counter2++ }
+ }
+ Box(Modifier.requiredSize(1.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(1, 2)
+ }
+
+ rule.runOnIdle {
+ assertThat(realState[0]).isEqualTo(1)
+ assertThat(realState[1]).isEqualTo(10)
+ assertThat(realState[2]).isEqualTo(100)
+ realState = arrayOf(0, 0, 0)
+ }
+
+ restorationTester.emulateSavedInstanceStateRestore()
+
+ rule.runOnIdle {
+ assertThat(realState[0]).isEqualTo(0)
+ assertThat(realState[1]).isEqualTo(10)
+ assertThat(realState[2]).isEqualTo(100)
+ }
+ }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyNestedScrollingTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyNestedScrollingTest.kt
new file mode 100644
index 0000000..404218a
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyNestedScrollingTest.kt
@@ -0,0 +1,382 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.gestures.scrollable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.tv.foundation.lazy.list.TestTouchSlop
+import androidx.tv.foundation.lazy.list.setContentWithTestViewConfiguration
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class LazyNestedScrollingTest {
+ private val LazyTag = "LazyTag"
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ private val expectedDragOffset = 20f
+ private val dragOffsetWithTouchSlop = expectedDragOffset + TestTouchSlop
+
+ @Test
+ fun verticalGrid_nestedScrollingBackwardInitially() = runBlocking {
+ val items = (1..3).toList()
+ var draggedOffset = 0f
+ val scrollable = ScrollableState {
+ draggedOffset += it
+ it
+ }
+ rule.setContentWithTestViewConfiguration {
+ Box(
+ Modifier.scrollable(
+ orientation = Orientation.Vertical,
+ state = scrollable
+ )
+ ) {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ Modifier.requiredSize(100.dp).testTag(LazyTag)
+ ) {
+ items(items) {
+ Spacer(Modifier.requiredSize(50.dp).testTag("$it"))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyTag)
+ .performTouchInput {
+ down(Offset(x = 10f, y = 10f))
+ moveBy(Offset(x = 0f, y = 100f + TestTouchSlop))
+ up()
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(draggedOffset).isEqualTo(100f)
+ }
+ }
+
+ @Test
+ fun verticalGrid_nestedScrollingBackwardOnceWeScrolledForwardPreviously() = runBlocking {
+ val items = (1..3).toList()
+ var draggedOffset = 0f
+ val scrollable = ScrollableState {
+ draggedOffset += it
+ it
+ }
+ rule.setContentWithTestViewConfiguration {
+ Box(
+ Modifier.scrollable(
+ orientation = Orientation.Vertical,
+ state = scrollable
+ )
+ ) {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ Modifier.requiredSize(100.dp).testTag(LazyTag),
+ ) {
+ items(items) {
+ Box(Modifier.requiredHeight(50.dp).testTag("$it").focusable())
+ }
+ }
+ }
+ }
+
+ // scroll forward
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
+
+ // scroll back so we again on 0 position
+ // we scroll one extra dp to prevent rounding issues
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 2)
+
+ rule.onNodeWithTag(LazyTag)
+ .performTouchInput {
+ draggedOffset = 0f
+ down(Offset(x = 100f, y = 100f))
+ moveBy(Offset(x = 0f, y = dragOffsetWithTouchSlop))
+ up()
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(draggedOffset).isEqualTo(expectedDragOffset)
+ }
+ }
+
+ @Test
+ fun verticalGrid_nestedScrollingForwardWhenTheFullContentIsInitiallyVisible() = runBlocking {
+ val items = (1..2).toList()
+ var draggedOffset = 0f
+ val scrollable = ScrollableState {
+ draggedOffset += it
+ it
+ }
+ rule.setContentWithTestViewConfiguration {
+ Box(
+ Modifier.scrollable(
+ orientation = Orientation.Vertical,
+ state = scrollable
+ )
+ ) {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ Modifier.requiredSize(100.dp).testTag(LazyTag)
+ ) {
+ items(items) {
+ Spacer(Modifier.requiredSize(40.dp).testTag("$it"))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyTag)
+ .performTouchInput {
+ down(Offset(x = 10f, y = 10f))
+ moveBy(Offset(x = 0f, y = -dragOffsetWithTouchSlop))
+ up()
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset)
+ }
+ }
+
+ @Test
+ fun verticalGrid_nestedScrollingForwardWhenScrolledToTheEnd() = runBlocking {
+ val items = (1..3).toList()
+ var draggedOffset = 0f
+ val scrollable = ScrollableState {
+ draggedOffset += it
+ it
+ }
+ rule.setContentWithTestViewConfiguration {
+ Box(
+ Modifier.scrollable(
+ orientation = Orientation.Vertical,
+ state = scrollable
+ )
+ ) {
+ TvLazyVerticalGrid(
+ TvGridCells.Fixed(1),
+ Modifier.requiredSize(100.dp).testTag(LazyTag)
+ ) {
+ items(items) {
+ Spacer(Modifier.requiredSize(50.dp).testTag("$it").focusable())
+ }
+ }
+ }
+ }
+
+ // scroll till the end
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
+
+ rule.onNodeWithTag(LazyTag)
+ .performTouchInput {
+ draggedOffset = 0f
+ down(Offset(x = 10f, y = 10f))
+ moveBy(Offset(x = 0f, y = -dragOffsetWithTouchSlop))
+ up()
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset)
+ }
+ }
+
+ // @Test
+ // fun row_nestedScrollingBackwardInitially() = runBlocking {
+ // val items = (1..3).toList()
+ // var draggedOffset = 0f
+ // val scrollable = ScrollableState {
+ // draggedOffset += it
+ // it
+ // }
+ // rule.setContentWithTestViewConfiguration {
+ // Box(
+ // Modifier.scrollable(
+ // orientation = Orientation.Horizontal,
+ // state = scrollable
+ // )
+ // ) {
+ // LazyRow(
+ // modifier = Modifier.requiredSize(100.dp).testTag(LazyTag)
+ // ) {
+ // items(items) {
+ // Spacer(Modifier.requiredSize(50.dp).testTag("$it"))
+ // }
+ // }
+ // }
+ // }
+
+ // rule.onNodeWithTag(LazyTag)
+ // .performTouchInput {
+ // down(Offset(x = 10f, y = 10f))
+ // moveBy(Offset(x = dragOffsetWithTouchSlop, y = 0f))
+ // up()
+ // }
+
+ // rule.runOnIdle {
+ // Truth.assertThat(draggedOffset).isEqualTo(expectedDragOffset)
+ // }
+ // }
+
+ // @Test
+ // fun row_nestedScrollingBackwardOnceWeScrolledForwardPreviously() = runBlocking {
+ // val items = (1..3).toList()
+ // var draggedOffset = 0f
+ // val scrollable = ScrollableState {
+ // draggedOffset += it
+ // it
+ // }
+ // rule.setContentWithTestViewConfiguration {
+ // Box(
+ // Modifier.scrollable(
+ // orientation = Orientation.Horizontal,
+ // state = scrollable
+ // )
+ // ) {
+ // LazyRow(
+ // modifier = Modifier.requiredSize(100.dp).testTag(LazyTag)
+ // ) {
+ // items(items) {
+ // Spacer(Modifier.requiredSize(50.dp).testTag("$it"))
+ // }
+ // }
+ // }
+ // }
+
+ // // scroll forward
+ // rule.onNodeWithTag(LazyTag)
+ // .scrollBy(x = 20.dp, density = rule.density)
+
+ // // scroll back so we again on 0 position
+ // // we scroll one extra dp to prevent rounding issues
+ // rule.onNodeWithTag(LazyTag)
+ // .scrollBy(x = -(21.dp), density = rule.density)
+
+ // rule.onNodeWithTag(LazyTag)
+ // .performTouchInput {
+ // draggedOffset = 0f
+ // down(Offset(x = 10f, y = 10f))
+ // moveBy(Offset(x = dragOffsetWithTouchSlop, y = 0f))
+ // up()
+ // }
+
+ // rule.runOnIdle {
+ // Truth.assertThat(draggedOffset).isEqualTo(expectedDragOffset)
+ // }
+ // }
+
+ // @Test
+ // fun row_nestedScrollingForwardWhenTheFullContentIsInitiallyVisible() = runBlocking {
+ // val items = (1..2).toList()
+ // var draggedOffset = 0f
+ // val scrollable = ScrollableState {
+ // draggedOffset += it
+ // it
+ // }
+ // rule.setContentWithTestViewConfiguration {
+ // Box(
+ // Modifier.scrollable(
+ // orientation = Orientation.Horizontal,
+ // state = scrollable
+ // )
+ // ) {
+ // LazyRow(
+ // modifier = Modifier.requiredSize(100.dp).testTag(LazyTag)
+ // ) {
+ // items(items) {
+ // Spacer(Modifier.requiredSize(40.dp).testTag("$it"))
+ // }
+ // }
+ // }
+ // }
+
+ // rule.onNodeWithTag(LazyTag)
+ // .performTouchInput {
+ // down(Offset(x = 10f, y = 10f))
+ // moveBy(Offset(x = -dragOffsetWithTouchSlop, y = 0f))
+ // up()
+ // }
+
+ // rule.runOnIdle {
+ // Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset)
+ // }
+ // }
+
+ // @Test
+ // fun row_nestedScrollingForwardWhenScrolledToTheEnd() = runBlocking {
+ // val items = (1..3).toList()
+ // var draggedOffset = 0f
+ // val scrollable = ScrollableState {
+ // draggedOffset += it
+ // it
+ // }
+ // rule.setContentWithTestViewConfiguration {
+ // Box(
+ // Modifier.scrollable(
+ // orientation = Orientation.Horizontal,
+ // state = scrollable
+ // )
+ // ) {
+ // LazyRow(
+ // modifier = Modifier.requiredSize(100.dp).testTag(LazyTag)
+ // ) {
+ // items(items) {
+ // Spacer(Modifier.requiredSize(50.dp).testTag("$it"))
+ // }
+ // }
+ // }
+ // }
+
+ // // scroll till the end
+ // rule.onNodeWithTag(LazyTag)
+ // .scrollBy(x = 55.dp, density = rule.density)
+
+ // rule.onNodeWithTag(LazyTag)
+ // .performTouchInput {
+ // draggedOffset = 0f
+ // down(Offset(x = 10f, y = 10f))
+ // moveBy(Offset(x = -dragOffsetWithTouchSlop, y = 0f))
+ // up()
+ // }
+
+ // rule.runOnIdle {
+ // Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset)
+ // }
+ // }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyScrollAccessibilityTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
new file mode 100644
index 0000000..8af2b8d
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
@@ -0,0 +1,344 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import android.R.id.accessibilityActionScrollDown
+import android.R.id.accessibilityActionScrollLeft
+import android.R.id.accessibilityActionScrollRight
+import android.R.id.accessibilityActionScrollUp
+import android.view.View
+import android.view.accessibility.AccessibilityNodeProvider
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsNode
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.core.view.ViewCompat
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD
+import androidx.test.filters.MediumTest
+import com.google.common.truth.IterableSubject
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@MediumTest
+@RunWith(Parameterized::class)
+class LazyScrollAccessibilityTest(
+ private val config: TestConfig
+) : BaseLazyGridTestWithOrientation(config.orientation) {
+
+ data class TestConfig(
+ val orientation: Orientation,
+ val rtl: Boolean,
+ val reversed: Boolean
+ ) {
+ val horizontal = orientation == Orientation.Horizontal
+ val vertical = !horizontal
+
+ override fun toString(): String {
+ return (if (orientation == Orientation.Horizontal) "horizontal" else "vertical") +
+ (if (rtl) ",rtl" else ",ltr") +
+ (if (reversed) ",reversed" else "")
+ }
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun params() =
+ listOf(Orientation.Horizontal, Orientation.Vertical).flatMap { horizontal ->
+ listOf(false, true).flatMap { rtl ->
+ listOf(false, true).map { reversed ->
+ TestConfig(horizontal, rtl, reversed)
+ }
+ }
+ }
+ }
+
+ private val scrollerTag = "ScrollerTest"
+ private var composeView: View? = null
+ private val accessibilityNodeProvider: AccessibilityNodeProvider
+ get() = checkNotNull(composeView) {
+ "composeView not initialized. Did `composeView = LocalView.current` not work?"
+ }.let { composeView ->
+ ViewCompat
+ .getAccessibilityDelegate(composeView)!!
+ .getAccessibilityNodeProvider(composeView)!!
+ .provider as AccessibilityNodeProvider
+ }
+
+ @Test
+ fun scrollForward() {
+ testRelativeDirection(58, ACTION_SCROLL_FORWARD)
+ }
+
+ @Test
+ fun scrollBackward() {
+ testRelativeDirection(41, ACTION_SCROLL_BACKWARD)
+ }
+
+ @Test
+ fun scrollRight() {
+ testAbsoluteDirection(58, accessibilityActionScrollRight, config.horizontal)
+ }
+
+ @Test
+ fun scrollLeft() {
+ testAbsoluteDirection(41, accessibilityActionScrollLeft, config.horizontal)
+ }
+
+ @Test
+ fun scrollDown() {
+ testAbsoluteDirection(58, accessibilityActionScrollDown, config.vertical)
+ }
+
+ @Test
+ fun scrollUp() {
+ testAbsoluteDirection(41, accessibilityActionScrollUp, config.vertical)
+ }
+
+ @Test
+ fun verifyScrollActionsAtStart() {
+ createScrollableContent_StartAtStart()
+ verifyNodeInfoScrollActions(
+ expectForward = !config.reversed,
+ expectBackward = config.reversed
+ )
+ }
+
+ @Test
+ fun verifyScrollActionsInMiddle() {
+ createScrollableContent_StartInMiddle()
+ verifyNodeInfoScrollActions(
+ expectForward = true,
+ expectBackward = true
+ )
+ }
+
+ @Test
+ fun verifyScrollActionsAtEnd() {
+ createScrollableContent_StartAtEnd()
+ verifyNodeInfoScrollActions(
+ expectForward = config.reversed,
+ expectBackward = !config.reversed
+ )
+ }
+
+ /**
+ * Setup the test, run the given [accessibilityAction], and check if the [canonicalTarget]
+ * has been reached. The canonical target is the item that we expect to see when moving
+ * forward in a non-reversed scrollable (e.g. down in LazyColumn or right in LazyRow in LTR).
+ * The actual target is either the canonical target or the target that is as far from the
+ * middle of the lazy list as the canonical target, but on the other side of the middle,
+ * depending on the [configuration][config].
+ */
+ private fun testRelativeDirection(canonicalTarget: Int, accessibilityAction: Int) {
+ val target = if (!config.reversed) canonicalTarget else 100 - canonicalTarget - 1
+ testScrollAction(target, accessibilityAction)
+ }
+
+ /**
+ * Setup the test, run the given [accessibilityAction], and check if the [canonicalTarget]
+ * has been reached (but only if we [expect][expectActionSuccess] the action to succeed).
+ * The canonical target is the item that we expect to see when moving forward in a
+ * non-reversed scrollable (e.g. down in LazyColumn or right in LazyRow in LTR). The actual
+ * target is either the canonical target or the target that is as far from the middle of the
+ * scrollable as the canonical target, but on the other side of the middle, depending on the
+ * [configuration][config].
+ */
+ private fun testAbsoluteDirection(
+ canonicalTarget: Int,
+ accessibilityAction: Int,
+ expectActionSuccess: Boolean
+ ) {
+ var target = canonicalTarget
+ if (config.horizontal && config.rtl) {
+ target = 100 - target - 1
+ }
+ if (config.reversed) {
+ target = 100 - target - 1
+ }
+ testScrollAction(target, accessibilityAction, expectActionSuccess)
+ }
+
+ /**
+ * Setup the test, run the given [accessibilityAction], and check if the [target] has been
+ * reached (but only if we [expect][expectActionSuccess] the action to succeed).
+ */
+ private fun testScrollAction(
+ target: Int,
+ accessibilityAction: Int,
+ expectActionSuccess: Boolean = true
+ ) {
+ createScrollableContent_StartInMiddle()
+ rule.onNodeWithText("$target").assertDoesNotExist()
+
+ val returnValue = rule.onNodeWithTag(scrollerTag).withSemanticsNode {
+ accessibilityNodeProvider.performAction(id, accessibilityAction, null)
+ }
+
+ assertThat(returnValue).isEqualTo(expectActionSuccess)
+ if (expectActionSuccess) {
+ rule.onNodeWithText("$target").assertIsDisplayed()
+ } else {
+ rule.onNodeWithText("$target").assertDoesNotExist()
+ }
+ }
+
+ /**
+ * Checks if all of the scroll actions are present or not according to what we expect based on
+ * [expectForward] and [expectBackward]. The scroll actions that are checked are forward,
+ * backward, left, right, up and down. The expectation parameters must already account for
+ * [reversing][TestConfig.reversed].
+ */
+ private fun verifyNodeInfoScrollActions(expectForward: Boolean, expectBackward: Boolean) {
+ val nodeInfo = rule.onNodeWithTag(scrollerTag).withSemanticsNode {
+ rule.runOnUiThread {
+ accessibilityNodeProvider.createAccessibilityNodeInfo(id)
+ }
+ }
+
+ val actions = nodeInfo.actionList.map { it.id }
+
+ assertThat(actions).contains(expectForward, ACTION_SCROLL_FORWARD)
+ assertThat(actions).contains(expectBackward, ACTION_SCROLL_BACKWARD)
+
+ if (config.horizontal) {
+ val expectLeft = if (config.rtl) expectForward else expectBackward
+ val expectRight = if (config.rtl) expectBackward else expectForward
+ assertThat(actions).contains(expectLeft, accessibilityActionScrollLeft)
+ assertThat(actions).contains(expectRight, accessibilityActionScrollRight)
+ assertThat(actions).contains(false, accessibilityActionScrollDown)
+ assertThat(actions).contains(false, accessibilityActionScrollUp)
+ } else {
+ assertThat(actions).contains(false, accessibilityActionScrollLeft)
+ assertThat(actions).contains(false, accessibilityActionScrollRight)
+ assertThat(actions).contains(expectForward, accessibilityActionScrollDown)
+ assertThat(actions).contains(expectBackward, accessibilityActionScrollUp)
+ }
+ }
+
+ private fun IterableSubject.contains(expectPresent: Boolean, element: Any) {
+ if (expectPresent) {
+ contains(element)
+ } else {
+ doesNotContain(element)
+ }
+ }
+
+ /**
+ * Creates a Row/Column that starts at the first item, according to [createScrollableContent]
+ */
+ private fun createScrollableContent_StartAtStart() {
+ createScrollableContent {
+ // Start at the start:
+ // -> pretty basic
+ rememberLazyGridState(0, 0)
+ }
+ }
+
+ /**
+ * Creates a Row/Column that starts in the middle, according to [createScrollableContent]
+ */
+ private fun createScrollableContent_StartInMiddle() {
+ createScrollableContent {
+ // Start at the middle:
+ // Content size: 100 items * 21dp per item = 2100dp
+ // Viewport size: 200dp rect - 50dp padding on both sides = 100dp
+ // Content outside viewport: 2100dp - 100dp = 2000dp
+ // -> centered when 1000dp on either side, which is 47 items + 13dp
+ rememberLazyGridState(
+ 47,
+ with(LocalDensity.current) { 13.dp.roundToPx() }
+ )
+ }
+ }
+
+ /**
+ * Creates a Row/Column that starts at the last item, according to [createScrollableContent]
+ */
+ private fun createScrollableContent_StartAtEnd() {
+ createScrollableContent {
+ // Start at the end:
+ // Content size: 100 items * 21dp per item = 2100dp
+ // Viewport size: 200dp rect - 50dp padding on both sides = 100dp
+ // Content outside viewport: 2100dp - 100dp = 2000dp
+ // -> at the end when offset at 2000dp, which is 95 items + 5dp
+ rememberLazyGridState(
+ 95,
+ with(LocalDensity.current) { 5.dp.roundToPx() }
+ )
+ }
+ }
+
+ /**
+ * Creates a grid with a viewport of 100.dp, containing 100 items each 17.dp in size.
+ * The items have a text with their index (ASC), and where the viewport starts is determined
+ * by the given [lambda][rememberTvLazyGridState]. All properties from [config] are applied.
+ * The viewport has padding around it to make sure scroll distance doesn't include padding.
+ */
+ private fun createScrollableContent(
+ rememberTvLazyGridState: @Composable () -> TvLazyGridState
+ ) {
+ rule.setContent {
+ composeView = LocalView.current
+
+ val state = rememberTvLazyGridState()
+
+ Box(Modifier.requiredSize(200.dp).background(Color.White)) {
+ val direction = if (config.rtl) LayoutDirection.Rtl else LayoutDirection.Ltr
+ CompositionLocalProvider(LocalLayoutDirection provides direction) {
+ LazyGrid(
+ cells = 1,
+ modifier = Modifier.testTag(scrollerTag).matchParentSize(),
+ state = state,
+ contentPadding = PaddingValues(50.dp),
+ reverseLayout = config.reversed
+ ) {
+ items(100) {
+ Box(Modifier.requiredSize(21.dp).background(Color.Yellow)) {
+ BasicText("$it", Modifier.align(Alignment.Center))
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun <T> SemanticsNodeInteraction.withSemanticsNode(block: SemanticsNode.() -> T): T {
+ return block.invoke(fetchSemanticsNode())
+ }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyScrollTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyScrollTest.kt
new file mode 100644
index 0000000..dc794c5
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazyScrollTest.kt
@@ -0,0 +1,336 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import androidx.compose.animation.core.FloatSpringSpec
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.test.filters.MediumTest
+import androidx.tv.foundation.lazy.AutoTestFrameClock
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import java.util.concurrent.TimeUnit
+import kotlin.math.roundToInt
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@MediumTest
+// @RunWith(Parameterized::class)
+class LazyScrollTest { // (private val orientation: Orientation)
+ @get:Rule
+ val rule = createComposeRule()
+
+ private val vertical: Boolean
+ get() = true // orientation == Orientation.Vertical
+
+ private val itemsCount = 40
+ private lateinit var state: TvLazyGridState
+
+ private val itemSizePx = 100
+ private var itemSizeDp = Dp.Unspecified
+ private var containerSizeDp = Dp.Unspecified
+
+ lateinit var scope: CoroutineScope
+
+ @Before
+ fun setup() {
+ with(rule.density) {
+ itemSizeDp = itemSizePx.toDp()
+ containerSizeDp = itemSizeDp * 3
+ }
+ rule.setContent {
+ state = rememberLazyGridState()
+ scope = rememberCoroutineScope()
+ TestContent()
+ }
+ }
+
+ @Test
+ fun setupWorks() {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ }
+
+ @Test
+ fun scrollToItem() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.scrollToItem(2)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(2)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.scrollToItem(0)
+ state.scrollToItem(3)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(2)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ }
+
+ @Test
+ fun scrollToItemWithOffset() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.scrollToItem(6, 10)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(6)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+ }
+
+ @Test
+ fun scrollToItemWithNegativeOffset() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.scrollToItem(6, -10)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(4)
+ val item6Offset = state.layoutInfo.visibleItemsInfo.first { it.index == 6 }.offset.y
+ assertThat(item6Offset).isEqualTo(10)
+ }
+
+ @Test
+ fun scrollToItemWithPositiveOffsetLargerThanAvailableSize() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.scrollToItem(itemsCount - 6, 10)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 6)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not 10
+ }
+
+ @Test
+ fun scrollToItemWithNegativeOffsetLargerThanAvailableSize() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.scrollToItem(1, -(itemSizePx + 10))
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not -10
+ }
+
+ @Test
+ fun scrollToItemWithIndexLargerThanItemsCount() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.scrollToItem(itemsCount + 4)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 6)
+ }
+
+ @Test
+ fun animateScrollBy() = runBlocking {
+ val scrollDistance = 320
+
+ val expectedLine = scrollDistance / itemSizePx // resolves to 3
+ val expectedItem = expectedLine * 2 // resolves to 6
+ val expectedOffset = scrollDistance % itemSizePx // resolves to 20px
+
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.animateScrollBy(scrollDistance.toFloat())
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(expectedItem)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(expectedOffset)
+ }
+
+ @Test
+ fun animateScrollToItem() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.animateScrollToItem(10, 10)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(10)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+ }
+
+ @Test
+ fun animateScrollToItemWithOffset() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.animateScrollToItem(6, 10)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(6)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+ }
+
+ @Test
+ fun animateScrollToItemWithNegativeOffset() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.animateScrollToItem(6, -10)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(4)
+ val item6Offset = state.layoutInfo.visibleItemsInfo.first { it.index == 6 }.offset.y
+ assertThat(item6Offset).isEqualTo(10)
+ }
+
+ @Test
+ fun animateScrollToItemWithPositiveOffsetLargerThanAvailableSize() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.animateScrollToItem(itemsCount - 6, 10)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 6)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not 10
+ }
+
+ @Test
+ fun animateScrollToItemWithNegativeOffsetLargerThanAvailableSize() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.animateScrollToItem(2, -(itemSizePx + 10))
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not -10
+ }
+
+ @Test
+ fun animateScrollToItemWithIndexLargerThanItemsCount() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.animateScrollToItem(itemsCount + 2)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 6)
+ }
+
+ @Test
+ fun animatePerFrameForwardToVisibleItem() {
+ assertSpringAnimation(toIndex = 4)
+ }
+
+ @Test
+ fun animatePerFrameForwardToVisibleItemWithOffset() {
+ assertSpringAnimation(toIndex = 4, toOffset = 35)
+ }
+
+ @Test
+ fun animatePerFrameForwardToNotVisibleItem() {
+ assertSpringAnimation(toIndex = 16)
+ }
+
+ @Test
+ fun animatePerFrameForwardToNotVisibleItemWithOffset() {
+ assertSpringAnimation(toIndex = 20, toOffset = 35)
+ }
+
+ @Test
+ fun animatePerFrameBackward() {
+ assertSpringAnimation(toIndex = 2, fromIndex = 12)
+ }
+
+ @Test
+ fun animatePerFrameBackwardWithOffset() {
+ assertSpringAnimation(toIndex = 2, fromIndex = 10, fromOffset = 58)
+ }
+
+ @Test
+ fun animatePerFrameBackwardWithInitialOffset() {
+ assertSpringAnimation(toIndex = 0, toOffset = 40, fromIndex = 8)
+ }
+
+ private fun assertSpringAnimation(
+ toIndex: Int,
+ toOffset: Int = 0,
+ fromIndex: Int = 0,
+ fromOffset: Int = 0
+ ) {
+ if (fromIndex != 0 || fromOffset != 0) {
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(fromIndex, fromOffset)
+ }
+ }
+ }
+ rule.waitForIdle()
+
+ assertThat(state.firstVisibleItemIndex).isEqualTo(fromIndex)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(fromOffset)
+
+ rule.mainClock.autoAdvance = false
+
+ scope.launch {
+ state.animateScrollToItem(toIndex, toOffset)
+ }
+
+ while (!state.isScrollInProgress) {
+ Thread.sleep(5)
+ }
+
+ val startOffset = (fromIndex / 2 * itemSizePx + fromOffset).toFloat()
+ val endOffset = (toIndex / 2 * itemSizePx + toOffset).toFloat()
+ val spec = FloatSpringSpec()
+
+ val duration =
+ TimeUnit.NANOSECONDS.toMillis(spec.getDurationNanos(startOffset, endOffset, 0f))
+ rule.mainClock.advanceTimeByFrame()
+ var expectedTime = rule.mainClock.currentTime
+ for (i in 0..duration step FrameDuration) {
+ val nanosTime = TimeUnit.MILLISECONDS.toNanos(i)
+ val expectedValue =
+ spec.getValueFromNanos(nanosTime, startOffset, endOffset, 0f)
+ val actualValue =
+ (state.firstVisibleItemIndex / 2 * itemSizePx + state.firstVisibleItemScrollOffset)
+ assertWithMessage(
+ "On animation frame at $i index=${state.firstVisibleItemIndex} " +
+ "offset=${state.firstVisibleItemScrollOffset} expectedValue=$expectedValue"
+ ).that(actualValue).isEqualTo(expectedValue.roundToInt(), tolerance = 1)
+
+ rule.mainClock.advanceTimeBy(FrameDuration)
+ expectedTime += FrameDuration
+ assertThat(expectedTime).isEqualTo(rule.mainClock.currentTime)
+ rule.waitForIdle()
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(toIndex)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(toOffset)
+ }
+
+ @Composable
+ private fun TestContent() {
+ if (vertical) {
+ TvLazyVerticalGrid(TvGridCells.Fixed(2), Modifier.height(containerSizeDp), state) {
+ items(itemsCount) {
+ ItemContent()
+ }
+ }
+ } else {
+ // LazyRow(Modifier.width(300.dp), state) {
+ // items(items) {
+ // ItemContent()
+ // }
+ // }
+ }
+ }
+
+ @Composable
+ private fun ItemContent() {
+ val modifier = if (vertical) {
+ Modifier.height(itemSizeDp)
+ } else {
+ Modifier.width(itemSizeDp)
+ }
+ Spacer(modifier)
+ }
+
+ // companion object {
+ // @JvmStatic
+ // @Parameterized.Parameters(name = "{0}")
+ // fun params() = arrayOf(Orientation.Vertical, Orientation.Horizontal)
+ // }
+}
+
+private val FrameDuration = 16L
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazySemanticsTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazySemanticsTest.kt
new file mode 100644
index 0000000..68c75ee
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/LazySemanticsTest.kt
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.requiredWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions.ScrollToIndex
+import androidx.compose.ui.semantics.SemanticsProperties.IndexForKey
+import androidx.compose.ui.semantics.getOrNull
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Tests the semantics properties defined on a LazyGrid:
+ * - GetIndexForKey
+ * - ScrollToIndex
+ *
+ * GetIndexForKey:
+ * Create a lazy grid, iterate over all indices, verify key of each of them
+ *
+ * ScrollToIndex:
+ * Create a lazy grid, scroll to a line off screen, verify shown items
+ *
+ * All tests performed in [runTest], scenarios set up in the test methods.
+ */
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class LazySemanticsTest {
+ private val N = 20
+ private val LazyGridTag = "lazy_grid"
+ private val LazyGridModifier = Modifier.testTag(LazyGridTag).requiredSize(100.dp)
+
+ private fun tag(index: Int): String = "tag_$index"
+ private fun key(index: Int): String = "key_$index"
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun itemSemantics_verticalGrid() {
+ rule.setContent {
+ TvLazyVerticalGrid(TvGridCells.Fixed(1), LazyGridModifier) {
+ repeat(N) {
+ item(key = key(it)) {
+ SpacerInColumn(it)
+ }
+ }
+ }
+ }
+ runTest()
+ }
+
+ @Test
+ fun itemsSemantics_verticalGrid() {
+ rule.setContent {
+ val state = rememberLazyGridState()
+ TvLazyVerticalGrid(TvGridCells.Fixed(1), LazyGridModifier, state) {
+ items(items = List(N) { it }, key = { key(it) }) {
+ SpacerInColumn(it)
+ }
+ }
+ }
+ runTest()
+ }
+
+ // @Test
+ // fun itemSemantics_row() {
+ // rule.setContent {
+ // LazyRow(LazyGridModifier) {
+ // repeat(N) {
+ // item(key = key(it)) {
+ // SpacerInRow(it)
+ // }
+ // }
+ // }
+ // }
+ // runTest()
+ // }
+
+ // @Test
+ // fun itemsSemantics_row() {
+ // rule.setContent {
+ // LazyRow(LazyGridModifier) {
+ // items(items = List(N) { it }, key = { key(it) }) {
+ // SpacerInRow(it)
+ // }
+ // }
+ // }
+ // runTest()
+ // }
+
+ private fun runTest() {
+ checkViewport(firstExpectedItem = 0, lastExpectedItem = 3)
+
+ // Verify IndexForKey
+ rule.onNodeWithTag(LazyGridTag).assert(
+ SemanticsMatcher.keyIsDefined(IndexForKey).and(
+ SemanticsMatcher("keys match") { node ->
+ val actualIndex = node.config.getOrNull(IndexForKey)!!
+ (0 until N).all { expectedIndex ->
+ expectedIndex == actualIndex.invoke(key(expectedIndex))
+ }
+ }
+ )
+ )
+
+ // Verify ScrollToIndex
+ rule.onNodeWithTag(LazyGridTag).assert(SemanticsMatcher.keyIsDefined(ScrollToIndex))
+
+ invokeScrollToIndex(targetIndex = 10)
+ checkViewport(firstExpectedItem = 10, lastExpectedItem = 13)
+
+ invokeScrollToIndex(targetIndex = N - 1)
+ checkViewport(firstExpectedItem = N - 4, lastExpectedItem = N - 1)
+ }
+
+ private fun invokeScrollToIndex(targetIndex: Int) {
+ val node = rule.onNodeWithTag(LazyGridTag)
+ .fetchSemanticsNode("Failed: invoke ScrollToIndex")
+ rule.runOnUiThread {
+ node.config[ScrollToIndex].action!!.invoke(targetIndex)
+ }
+ }
+
+ private fun checkViewport(firstExpectedItem: Int, lastExpectedItem: Int) {
+ if (firstExpectedItem > 0) {
+ rule.onNodeWithTag(tag(firstExpectedItem - 1)).assertDoesNotExist()
+ }
+ (firstExpectedItem..lastExpectedItem).forEach {
+ rule.onNodeWithTag(tag(it)).assertExists()
+ }
+ if (firstExpectedItem < N - 1) {
+ rule.onNodeWithTag(tag(lastExpectedItem + 1)).assertDoesNotExist()
+ }
+ }
+
+ @Composable
+ private fun SpacerInColumn(index: Int) {
+ Spacer(Modifier.testTag(tag(index)).requiredHeight(30.dp).fillMaxWidth())
+ }
+
+ @Composable
+ private fun SpacerInRow(index: Int) {
+ Spacer(Modifier.testTag(tag(index)).requiredWidth(30.dp).fillMaxHeight())
+ }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/TvLazyGridLayoutInfoTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/TvLazyGridLayoutInfoTest.kt
new file mode 100644
index 0000000..d2bee39
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/grid/TvLazyGridLayoutInfoTest.kt
@@ -0,0 +1,520 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.tv.foundation.lazy.list.LayoutInfoTestParam
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@MediumTest
+@RunWith(Parameterized::class)
+class TvLazyGridLayoutInfoTest(
+ param: LayoutInfoTestParam
+) : BaseLazyGridTestWithOrientation(param.orientation) {
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun initParameters(): Array<Any> = arrayOf(
+ LayoutInfoTestParam(Orientation.Vertical, false),
+ LayoutInfoTestParam(Orientation.Vertical, true),
+ LayoutInfoTestParam(Orientation.Horizontal, false),
+ LayoutInfoTestParam(Orientation.Horizontal, true),
+ )
+ }
+ private val isVertical = param.orientation == Orientation.Vertical
+ private val reverseLayout = param.reverseLayout
+
+ private var itemSizePx: Int = 50
+ private var itemSizeDp: Dp = Dp.Infinity
+ private var gridWidthPx: Int = itemSizePx * 2
+ private var gridWidthDp: Dp = Dp.Infinity
+
+ @Before
+ fun before() {
+ with(rule.density) {
+ itemSizeDp = itemSizePx.toDp()
+ gridWidthDp = gridWidthPx.toDp()
+ }
+ }
+
+ @Test
+ fun visibleItemsAreCorrect() {
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ LazyGrid(
+ cells = 2,
+ state = rememberLazyGridState().also { state = it },
+ reverseLayout = reverseLayout,
+ modifier = Modifier.axisSize(gridWidthDp, itemSizeDp * 3.5f),
+ ) {
+ items((0..11).toList()) {
+ Box(Modifier.mainAxisSize(itemSizeDp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ state.layoutInfo.assertVisibleItems(count = 8, cells = 2)
+ }
+ }
+
+ @Test
+ fun visibleItemsAreCorrectAfterScroll() {
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ LazyGrid(
+ cells = 2,
+ state = rememberLazyGridState().also { state = it },
+ reverseLayout = reverseLayout,
+ modifier = Modifier.axisSize(gridWidthDp, itemSizeDp * 3.5f),
+ ) {
+ items((0..11).toList()) {
+ Box(Modifier.mainAxisSize(itemSizeDp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(2, 10)
+ }
+ state.layoutInfo
+ .assertVisibleItems(count = 8, startIndex = 2, startOffset = -10, cells = 2)
+ }
+ }
+
+ @Test
+ fun visibleItemsAreCorrectWithSpacing() {
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ LazyGrid(
+ cells = 1,
+ state = rememberLazyGridState().also { state = it },
+ reverseLayout = reverseLayout,
+ mainAxisSpacedBy = itemSizeDp,
+ modifier = Modifier.axisSize(itemSizeDp, itemSizeDp * 3.5f),
+ ) {
+ items((0..11).toList()) {
+ Box(Modifier.mainAxisSize(itemSizeDp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ state.layoutInfo.assertVisibleItems(count = 2, spacing = itemSizePx, cells = 1)
+ }
+ }
+
+ @Composable
+ fun ObservingFun(state: TvLazyGridState, currentInfo: StableRef<TvLazyGridLayoutInfo?>) {
+ currentInfo.value = state.layoutInfo
+ }
+ @Test
+ fun visibleItemsAreObservableWhenWeScroll() {
+ lateinit var state: TvLazyGridState
+ val currentInfo = StableRef<TvLazyGridLayoutInfo?>(null)
+ rule.setContent {
+ LazyGrid(
+ cells = 2,
+ state = rememberLazyGridState().also { state = it },
+ reverseLayout = reverseLayout,
+ modifier = Modifier.axisSize(itemSizeDp * 2f, itemSizeDp * 3.5f),
+ ) {
+ items((0..11).toList()) {
+ Box(Modifier.mainAxisSize(itemSizeDp))
+ }
+ }
+ ObservingFun(state, currentInfo)
+ }
+
+ rule.runOnIdle {
+ // empty it here and scrolling should invoke observingFun again
+ currentInfo.value = null
+ runBlocking {
+ state.scrollToItem(2, 0)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(currentInfo.value).isNotNull()
+ currentInfo.value!!
+ .assertVisibleItems(count = 8, startIndex = 2, cells = 2)
+ }
+ }
+
+ @Test
+ fun visibleItemsAreObservableWhenResize() {
+ lateinit var state: TvLazyGridState
+ var size by mutableStateOf(itemSizeDp * 2)
+ var currentInfo: TvLazyGridLayoutInfo? = null
+ @Composable
+ fun observingFun() {
+ currentInfo = state.layoutInfo
+ }
+ rule.setContent {
+ LazyGrid(
+ cells = 1,
+ modifier = Modifier.crossAxisSize(itemSizeDp),
+ reverseLayout = reverseLayout,
+ state = rememberLazyGridState().also { state = it },
+ ) {
+ item {
+ Box(Modifier.size(size))
+ }
+ }
+ observingFun()
+ }
+
+ rule.runOnIdle {
+ assertThat(currentInfo).isNotNull()
+ currentInfo!!.assertVisibleItems(
+ count = 1,
+ expectedSize = if (isVertical) {
+ IntSize(itemSizePx, itemSizePx * 2)
+ } else {
+ IntSize(itemSizePx * 2, itemSizePx)
+ },
+ cells = 1
+ )
+ currentInfo = null
+ size = itemSizeDp
+ }
+
+ rule.runOnIdle {
+ assertThat(currentInfo).isNotNull()
+ currentInfo!!.assertVisibleItems(
+ count = 1,
+ expectedSize = IntSize(itemSizePx, itemSizePx),
+ cells = 1
+ )
+ }
+ }
+
+ @Test
+ fun totalCountIsCorrect() {
+ var count by mutableStateOf(10)
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ LazyGrid(
+ cells = 2,
+ reverseLayout = reverseLayout,
+ state = rememberLazyGridState().also { state = it },
+ ) {
+ items((0 until count).toList()) {
+ Box(Modifier.mainAxisSize(10.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.totalItemsCount).isEqualTo(10)
+ count = 20
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.totalItemsCount).isEqualTo(20)
+ }
+ }
+
+ @Test
+ fun viewportOffsetsAndSizeAreCorrect() {
+ val sizePx = 45
+ val sizeDp = with(rule.density) { sizePx.toDp() }
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ LazyGrid(
+ cells = 2,
+ modifier = Modifier.axisSize(sizeDp * 2, sizeDp),
+ reverseLayout = reverseLayout,
+ state = rememberLazyGridState().also { state = it },
+ ) {
+ items((0..7).toList()) {
+ Box(Modifier.mainAxisSize(sizeDp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(0)
+ assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx)
+ assertThat(state.layoutInfo.viewportSize).isEqualTo(
+ if (isVertical) {
+ IntSize(sizePx * 2, sizePx)
+ } else {
+ IntSize(sizePx, sizePx * 2)
+ }
+ )
+ }
+ }
+
+ @Test
+ fun viewportOffsetsAndSizeAreCorrectWithContentPadding() {
+ val sizePx = 45
+ val startPaddingPx = 10
+ val endPaddingPx = 15
+ val sizeDp = with(rule.density) { sizePx.toDp() }
+ val beforeContentPaddingDp = with(rule.density) {
+ if (!reverseLayout) startPaddingPx.toDp() else endPaddingPx.toDp()
+ }
+ val afterContentPaddingDp = with(rule.density) {
+ if (!reverseLayout) endPaddingPx.toDp() else startPaddingPx.toDp()
+ }
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ LazyGrid(
+ cells = 2,
+ modifier = Modifier.axisSize(sizeDp * 2, sizeDp),
+ contentPadding = PaddingValues(
+ beforeContent = beforeContentPaddingDp,
+ afterContent = afterContentPaddingDp,
+ beforeContentCrossAxis = 2.dp,
+ afterContentCrossAxis = 2.dp
+ ),
+ reverseLayout = reverseLayout,
+ state = rememberLazyGridState().also { state = it },
+ ) {
+ items((0..7).toList()) {
+ Box(Modifier.mainAxisSize(sizeDp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-startPaddingPx)
+ assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - startPaddingPx)
+ assertThat(state.layoutInfo.afterContentPadding).isEqualTo(endPaddingPx)
+ assertThat(state.layoutInfo.viewportSize).isEqualTo(
+ if (isVertical) {
+ IntSize(sizePx * 2, sizePx)
+ } else {
+ IntSize(sizePx, sizePx * 2)
+ }
+ )
+ }
+ }
+
+ @Test
+ fun emptyItemsInVisibleItemsInfo() {
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ LazyGrid(
+ cells = 2,
+ state = rememberLazyGridState().also { state = it }
+ ) {
+ item { Box(Modifier) }
+ item { }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.visibleItemsInfo.size).isEqualTo(2)
+ assertThat(state.layoutInfo.visibleItemsInfo.first().index).isEqualTo(0)
+ assertThat(state.layoutInfo.visibleItemsInfo.last().index).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun emptyContent() {
+ lateinit var state: TvLazyGridState
+ val sizePx = 45
+ val startPaddingPx = 10
+ val endPaddingPx = 15
+ val sizeDp = with(rule.density) { sizePx.toDp() }
+ val beforeContentPaddingDp = with(rule.density) {
+ if (!reverseLayout) startPaddingPx.toDp() else endPaddingPx.toDp()
+ }
+ val afterContentPaddingDp = with(rule.density) {
+ if (!reverseLayout) endPaddingPx.toDp() else startPaddingPx.toDp()
+ }
+ rule.setContent {
+ LazyGrid(
+ cells = 1,
+ modifier = Modifier.mainAxisSize(sizeDp).crossAxisSize(sizeDp * 2),
+ state = rememberLazyGridState().also { state = it },
+ reverseLayout = reverseLayout,
+ contentPadding = PaddingValues(
+ beforeContent = beforeContentPaddingDp,
+ afterContent = afterContentPaddingDp
+ )
+ ) {
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-startPaddingPx)
+ assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - startPaddingPx)
+ assertThat(state.layoutInfo.beforeContentPadding).isEqualTo(startPaddingPx)
+ assertThat(state.layoutInfo.afterContentPadding).isEqualTo(endPaddingPx)
+ assertThat(state.layoutInfo.viewportSize).isEqualTo(
+ if (vertical) IntSize(sizePx * 2, sizePx) else IntSize(sizePx, sizePx * 2)
+ )
+ }
+ }
+
+ @Test
+ fun viewportIsLargerThenTheContent() {
+ lateinit var state: TvLazyGridState
+ val sizePx = 45
+ val startPaddingPx = 10
+ val endPaddingPx = 15
+ val sizeDp = with(rule.density) { sizePx.toDp() }
+ val beforeContentPaddingDp = with(rule.density) {
+ if (!reverseLayout) startPaddingPx.toDp() else endPaddingPx.toDp()
+ }
+ val afterContentPaddingDp = with(rule.density) {
+ if (!reverseLayout) endPaddingPx.toDp() else startPaddingPx.toDp()
+ }
+ rule.setContent {
+ LazyGrid(
+ cells = 1,
+ modifier = Modifier.mainAxisSize(sizeDp).crossAxisSize(sizeDp * 2),
+ state = rememberLazyGridState().also { state = it },
+ reverseLayout = reverseLayout,
+ contentPadding = PaddingValues(
+ beforeContent = beforeContentPaddingDp,
+ afterContent = afterContentPaddingDp
+ )
+ ) {
+ item {
+ Box(Modifier.size(sizeDp / 2))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-startPaddingPx)
+ assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - startPaddingPx)
+ assertThat(state.layoutInfo.beforeContentPadding).isEqualTo(startPaddingPx)
+ assertThat(state.layoutInfo.afterContentPadding).isEqualTo(endPaddingPx)
+ assertThat(state.layoutInfo.viewportSize).isEqualTo(
+ if (vertical) IntSize(sizePx * 2, sizePx) else IntSize(sizePx, sizePx * 2)
+ )
+ }
+ }
+
+ @Test
+ fun reverseLayoutIsCorrect() {
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ LazyGrid(
+ cells = 2,
+ state = rememberLazyGridState().also { state = it },
+ reverseLayout = reverseLayout,
+ modifier = Modifier.width(gridWidthDp).height(itemSizeDp * 3.5f),
+ ) {
+ items((0..11).toList()) {
+ Box(Modifier.size(itemSizeDp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.reverseLayout).isEqualTo(reverseLayout)
+ }
+ }
+
+ @Test
+ fun orientationIsCorrect() {
+ lateinit var state: TvLazyGridState
+ rule.setContent {
+ LazyGrid(
+ cells = 2,
+ state = rememberLazyGridState().also { state = it },
+ reverseLayout = reverseLayout,
+ modifier = Modifier.axisSize(gridWidthDp, itemSizeDp * 3.5f),
+ ) {
+ items((0..11).toList()) {
+ Box(Modifier.mainAxisSize(itemSizeDp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.orientation == Orientation.Vertical).isEqualTo(isVertical)
+ }
+ }
+
+ fun TvLazyGridLayoutInfo.assertVisibleItems(
+ count: Int,
+ cells: Int,
+ startIndex: Int = 0,
+ startOffset: Int = 0,
+ expectedSize: IntSize = IntSize(itemSizePx, itemSizePx),
+ spacing: Int = 0
+ ) {
+ assertThat(visibleItemsInfo.size).isEqualTo(count)
+ if (count == 0) return
+
+ assertThat(startIndex % cells).isEqualTo(0)
+ assertThat(visibleItemsInfo.size % cells).isEqualTo(0)
+
+ var currentIndex = startIndex
+ var currentOffset = startOffset
+ var currentLine = startIndex / cells
+ var currentCell = 0
+ visibleItemsInfo.forEach {
+ assertThat(it.index).isEqualTo(currentIndex)
+ assertWithMessage("Offset of item $currentIndex")
+ .that(if (isVertical) it.offset.y else it.offset.x)
+ .isEqualTo(currentOffset)
+ assertThat(it.size).isEqualTo(expectedSize)
+ assertThat(if (isVertical) it.row else it.column)
+ .isEqualTo(currentLine)
+ assertThat(if (isVertical) it.column else it.row)
+ .isEqualTo(currentCell)
+ currentIndex++
+ currentCell++
+ if (currentCell == cells) {
+ currentCell = 0
+ ++currentLine
+ currentOffset += spacing + if (isVertical) it.size.height else it.size.width
+ }
+ }
+ }
+}
+
+class LayoutInfoTestParam(
+ val orientation: Orientation,
+ val reverseLayout: Boolean
+) {
+ override fun toString(): String {
+ return "orientation=$orientation;reverseLayout=$reverseLayout"
+ }
+}
+
+@Stable
+class StableRef<T>(var value: T)
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
new file mode 100644
index 0000000..9ab4802
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.list
+
+import androidx.compose.animation.core.snap
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.assertIsEqualTo
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.PivotOffsets
+import androidx.tv.foundation.lazy.AutoTestFrameClock
+import androidx.tv.foundation.lazy.grid.keyPress
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+
+open class BaseLazyListTestWithOrientation(private val orientation: Orientation) {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ val vertical: Boolean
+ get() = orientation == Orientation.Vertical
+
+ fun Modifier.mainAxisSize(size: Dp) =
+ if (vertical) {
+ this.height(size)
+ } else {
+ this.width(size)
+ }
+
+ fun Modifier.crossAxisSize(size: Dp) =
+ if (vertical) {
+ this.width(size)
+ } else {
+ this.height(size)
+ }
+
+ fun Modifier.fillMaxCrossAxis() =
+ if (vertical) {
+ this.fillMaxWidth()
+ } else {
+ this.fillMaxHeight()
+ }
+
+ fun LazyItemScope.fillParentMaxCrossAxis() =
+ if (vertical) {
+ Modifier.fillParentMaxWidth()
+ } else {
+ Modifier.fillParentMaxHeight()
+ }
+
+ fun SemanticsNodeInteraction.assertMainAxisSizeIsEqualTo(expectedSize: Dp) =
+ if (vertical) {
+ assertHeightIsEqualTo(expectedSize)
+ } else {
+ assertWidthIsEqualTo(expectedSize)
+ }
+
+ fun SemanticsNodeInteraction.assertCrossAxisSizeIsEqualTo(expectedSize: Dp) =
+ if (vertical) {
+ assertWidthIsEqualTo(expectedSize)
+ } else {
+ assertHeightIsEqualTo(expectedSize)
+ }
+
+ fun SemanticsNodeInteraction.assertStartPositionIsAlmost(expected: Dp) {
+ val position = if (vertical) {
+ getUnclippedBoundsInRoot().top
+ } else {
+ getUnclippedBoundsInRoot().left
+ }
+ position.assertIsEqualTo(expected, tolerance = 1.dp)
+ }
+
+ fun SemanticsNodeInteraction.assertStartPositionInRootIsEqualTo(expectedStart: Dp) =
+ if (vertical) {
+ assertTopPositionInRootIsEqualTo(expectedStart)
+ } else {
+ assertLeftPositionInRootIsEqualTo(expectedStart)
+ }
+
+ fun SemanticsNodeInteraction.assertCrossAxisStartPositionInRootIsEqualTo(expectedStart: Dp) =
+ if (vertical) {
+ assertLeftPositionInRootIsEqualTo(expectedStart)
+ } else {
+ assertTopPositionInRootIsEqualTo(expectedStart)
+ }
+
+ fun PaddingValues(
+ mainAxis: Dp = 0.dp,
+ crossAxis: Dp = 0.dp
+ ) = PaddingValues(
+ beforeContent = mainAxis,
+ afterContent = mainAxis,
+ beforeContentCrossAxis = crossAxis,
+ afterContentCrossAxis = crossAxis
+ )
+
+ fun PaddingValues(
+ beforeContent: Dp = 0.dp,
+ afterContent: Dp = 0.dp,
+ beforeContentCrossAxis: Dp = 0.dp,
+ afterContentCrossAxis: Dp = 0.dp,
+ ) = if (vertical) {
+ PaddingValues(
+ start = beforeContentCrossAxis,
+ top = beforeContent,
+ end = afterContentCrossAxis,
+ bottom = afterContent
+ )
+ } else {
+ PaddingValues(
+ start = beforeContent,
+ top = beforeContentCrossAxis,
+ end = afterContent,
+ bottom = afterContentCrossAxis
+ )
+ }
+
+ fun TvLazyListState.scrollBy(offset: Dp) {
+ runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
+ animateScrollBy(with(rule.density) { offset.roundToPx().toFloat() }, snap())
+ }
+ }
+
+ fun TvLazyListState.scrollTo(index: Int) {
+ runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
+ scrollToItem(index)
+ }
+ }
+
+ fun ComposeContentTestRule.keyPress(numberOfKeyPresses: Int, reverseScroll: Boolean = false) {
+ val keyCode: Int =
+ when {
+ vertical && reverseScroll -> NativeKeyEvent.KEYCODE_DPAD_UP
+ vertical && !reverseScroll -> NativeKeyEvent.KEYCODE_DPAD_DOWN
+ !vertical && reverseScroll -> NativeKeyEvent.KEYCODE_DPAD_LEFT
+ !vertical && !reverseScroll -> NativeKeyEvent.KEYCODE_DPAD_RIGHT
+ else -> NativeKeyEvent.KEYCODE_DPAD_RIGHT
+ }
+
+ keyPress(keyCode, numberOfKeyPresses)
+ }
+
+ @Composable
+ fun LazyColumnOrRow(
+ modifier: Modifier = Modifier,
+ state: TvLazyListState = rememberLazyListState(),
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+ reverseLayout: Boolean = false,
+ userScrollEnabled: Boolean = true,
+ spacedBy: Dp = 0.dp,
+ pivotOffsets: PivotOffsets =
+ PivotOffsets(parentFraction = 0f),
+ content: TvLazyListScope.() -> Unit
+ ) {
+ if (vertical) {
+ val verticalArrangement = when {
+ spacedBy != 0.dp -> Arrangement.spacedBy(spacedBy)
+ !reverseLayout -> Arrangement.Top
+ else -> Arrangement.Bottom
+ }
+ TvLazyColumn(
+ modifier = modifier,
+ state = state,
+ contentPadding = contentPadding,
+ reverseLayout = reverseLayout,
+ userScrollEnabled = userScrollEnabled,
+ verticalArrangement = verticalArrangement,
+ pivotOffsets = pivotOffsets,
+ content = content
+ )
+ } else {
+ val horizontalArrangement = when {
+ spacedBy != 0.dp -> Arrangement.spacedBy(spacedBy)
+ !reverseLayout -> Arrangement.Start
+ else -> Arrangement.End
+ }
+ TvLazyRow(
+ modifier = modifier,
+ state = state,
+ contentPadding = contentPadding,
+ reverseLayout = reverseLayout,
+ userScrollEnabled = userScrollEnabled,
+ horizontalArrangement = horizontalArrangement,
+ pivotOffsets = pivotOffsets,
+ content = content
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyArrangementsTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyArrangementsTest.kt
new file mode 100644
index 0000000..f960ffa
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyArrangementsTest.kt
@@ -0,0 +1,612 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.list
+
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.PivotOffsets
+import androidx.tv.foundation.lazy.grid.keyPress
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class LazyArrangementsTest {
+
+ private val ContainerTag = "ContainerTag"
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ private var itemSize: Dp = Dp.Infinity
+ private var smallerItemSize: Dp = Dp.Infinity
+ private var containerSize: Dp = Dp.Infinity
+
+ @Before
+ fun before() {
+ with(rule.density) {
+ itemSize = 50.toDp()
+ }
+ with(rule.density) {
+ smallerItemSize = 40.toDp()
+ }
+ containerSize = itemSize * 5
+ }
+
+ // cases when we have not enough items to fill min constraints:
+
+ @Test
+ fun column_defaultArrangementIsTop() {
+ rule.setContent {
+ TvLazyColumn(
+ modifier = Modifier.requiredSize(containerSize),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(2) {
+ Box(Modifier.requiredSize(itemSize).testTag(it.toString()).focusable())
+ }
+ }
+ }
+
+ assertArrangementForTwoItems(Arrangement.Top)
+ }
+
+ @Test
+ fun column_centerArrangement() {
+ composeColumnWith(Arrangement.Center)
+ assertArrangementForTwoItems(Arrangement.Center)
+ }
+
+ @Test
+ fun column_bottomArrangement() {
+ composeColumnWith(Arrangement.Bottom)
+ assertArrangementForTwoItems(Arrangement.Bottom)
+ }
+
+ @Test
+ fun column_spacedArrangementNotFillingViewport() {
+ val arrangement = Arrangement.spacedBy(10.dp)
+ composeColumnWith(arrangement)
+ assertArrangementForTwoItems(arrangement)
+ }
+
+ @Test
+ fun row_defaultArrangementIsStart() {
+ rule.setContent {
+ TvLazyRow(
+ modifier = Modifier.requiredSize(containerSize),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(2) {
+ Box(Modifier.requiredSize(itemSize).testTag(it.toString()).focusable())
+ }
+ }
+ }
+
+ assertArrangementForTwoItems(Arrangement.Start, LayoutDirection.Ltr)
+ }
+
+ @Test
+ fun row_centerArrangement() {
+ composeRowWith(Arrangement.Center, LayoutDirection.Ltr)
+ assertArrangementForTwoItems(Arrangement.Center, LayoutDirection.Ltr)
+ }
+
+ @Test
+ fun row_endArrangement() {
+ composeRowWith(Arrangement.End, LayoutDirection.Ltr)
+ assertArrangementForTwoItems(Arrangement.End, LayoutDirection.Ltr)
+ }
+
+ @Test
+ fun row_spacedArrangementNotFillingViewport() {
+ val arrangement = Arrangement.spacedBy(10.dp)
+ composeRowWith(arrangement, LayoutDirection.Ltr)
+ assertArrangementForTwoItems(arrangement, LayoutDirection.Ltr)
+ }
+
+ @Test
+ fun row_rtl_startArrangement() {
+ composeRowWith(Arrangement.Center, LayoutDirection.Rtl)
+ assertArrangementForTwoItems(Arrangement.Center, LayoutDirection.Rtl)
+ }
+
+ @Test
+ fun row_rtl_endArrangement() {
+ composeRowWith(Arrangement.End, LayoutDirection.Rtl)
+ assertArrangementForTwoItems(Arrangement.End, LayoutDirection.Rtl)
+ }
+
+ @Test
+ fun row_rtl_spacedArrangementNotFillingViewport() {
+ val arrangement = Arrangement.spacedBy(10.dp)
+ composeRowWith(arrangement, LayoutDirection.Rtl)
+ assertArrangementForTwoItems(arrangement, LayoutDirection.Rtl)
+ }
+
+ // wrap content and spacing
+
+ @Test
+ fun column_spacing_affects_wrap_content() {
+ rule.setContent {
+ TvLazyColumn(
+ verticalArrangement = Arrangement.spacedBy(itemSize),
+ modifier = Modifier.testTag(ContainerTag),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(2) {
+ Box(Modifier.requiredSize(itemSize).focusable())
+ }
+ }
+ }
+
+ rule.onNodeWithTag(ContainerTag)
+ .assertWidthIsEqualTo(itemSize)
+ .assertHeightIsEqualTo(itemSize * 3)
+ }
+
+ @Test
+ fun row_spacing_affects_wrap_content() {
+ rule.setContent {
+ TvLazyRow(
+ horizontalArrangement = Arrangement.spacedBy(itemSize),
+ modifier = Modifier.testTag(ContainerTag),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(2) {
+ Box(Modifier.requiredSize(itemSize).focusable())
+ }
+ }
+ }
+
+ rule.onNodeWithTag(ContainerTag)
+ .assertWidthIsEqualTo(itemSize * 3)
+ .assertHeightIsEqualTo(itemSize)
+ }
+
+ // spacing added when we have enough items to fill the viewport
+
+ @Test
+ fun column_spacing_scrolledToTheTop() {
+ rule.setContent {
+ TvLazyColumn(
+ verticalArrangement = Arrangement.spacedBy(itemSize),
+ modifier = Modifier.requiredSize(itemSize * 3.5f),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(3) {
+ Box(Modifier.requiredSize(itemSize).testTag(it.toString()).focusable())
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize * 2)
+ }
+
+ @Test
+ fun column_spacing_scrolledToTheBottom() {
+ rule.setContentWithTestViewConfiguration {
+ TvLazyColumn(
+ verticalArrangement = Arrangement.spacedBy(itemSize),
+ modifier = Modifier.requiredSize(itemSize * 3.5f).testTag(ContainerTag),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(3) {
+ Box(Modifier.requiredSize(itemSize).testTag(it.toString()).focusable())
+ }
+ }
+ }
+
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 3)
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize * 0.5f)
+
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemSize * 2.5f)
+ }
+
+ @Test
+ fun row_spacing_scrolledToTheStart() {
+ rule.setContent {
+ TvLazyRow(
+ horizontalArrangement = Arrangement.spacedBy(itemSize),
+ modifier = Modifier.requiredSize(itemSize * 3.5f),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(3) {
+ Box(Modifier.requiredSize(itemSize).testTag(it.toString()).focusable())
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(itemSize * 2)
+ }
+
+ @Test
+ fun row_spacing_scrolledToTheEnd() {
+ rule.setContentWithTestViewConfiguration {
+ TvLazyRow(
+ horizontalArrangement = Arrangement.spacedBy(itemSize),
+ modifier = Modifier.requiredSize(itemSize * 3.5f).testTag(ContainerTag),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(3) {
+ Box(Modifier.requiredSize(itemSize).testTag(it.toString()).focusable())
+ }
+ }
+ }
+
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT, 3)
+
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(itemSize * 0.5f)
+
+ rule.onNodeWithTag("2")
+ .assertLeftPositionInRootIsEqualTo(itemSize * 2.5f)
+ }
+
+ @Test
+ fun column_scrollingByExactlyTheItemSizePlusSpacer_switchesTheFirstVisibleItem() {
+ val itemSizePx = 30
+ val spacingSizePx = 4
+ val itemSize = with(rule.density) { itemSizePx.toDp() }
+ val spacingSize = with(rule.density) { spacingSizePx.toDp() }
+ lateinit var state: TvLazyListState
+ rule.setContentWithTestViewConfiguration {
+ TvLazyColumn(
+ Modifier.size(itemSize * 3),
+ state = rememberLazyListState().also { state = it },
+ verticalArrangement = Arrangement.spacedBy(spacingSize),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(5) {
+ Spacer(
+ Modifier.size(itemSize).testTag("$it").focusable()
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy((itemSizePx + spacingSizePx).toFloat())
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ }
+ }
+
+ @Test
+ fun column_scrollingByExactlyTheItemSizePlusHalfTheSpacer_staysOnTheSameItem() {
+ val itemSizePx = 30
+ val spacingSizePx = 4
+ val itemSize = with(rule.density) { itemSizePx.toDp() }
+ val spacingSize = with(rule.density) { spacingSizePx.toDp() }
+ lateinit var state: TvLazyListState
+ rule.setContentWithTestViewConfiguration {
+ TvLazyColumn(
+ Modifier.size(itemSize * 3),
+ state = rememberLazyListState().also { state = it },
+ verticalArrangement = Arrangement.spacedBy(spacingSize),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(5) {
+ Spacer(
+ Modifier.size(itemSize).testTag("$it").focusable()
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy((itemSizePx + spacingSizePx / 2).toFloat())
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ assertThat(state.firstVisibleItemScrollOffset)
+ .isEqualTo(itemSizePx + spacingSizePx / 2)
+ }
+ }
+
+ @Test
+ fun row_scrollingByExactlyTheItemSizePlusSpacer_switchesTheFirstVisibleItem() {
+ val itemSizePx = 30
+ val spacingSizePx = 4
+ val itemSize = with(rule.density) { itemSizePx.toDp() }
+ val spacingSize = with(rule.density) { spacingSizePx.toDp() }
+ lateinit var state: TvLazyListState
+ rule.setContentWithTestViewConfiguration {
+ TvLazyRow(
+ Modifier.size(itemSize * 3),
+ state = rememberLazyListState().also { state = it },
+ horizontalArrangement = Arrangement.spacedBy(spacingSize),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(5) {
+ Box(
+ Modifier.size(itemSize).testTag("$it").focusable()
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy((itemSizePx + spacingSizePx).toFloat())
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ }
+ }
+
+ @Test
+ fun row_scrollingByExactlyTheItemSizePlusHalfTheSpacer_staysOnTheSameItem() {
+ val itemSizePx = 30
+ val spacingSizePx = 4
+ val itemSize = with(rule.density) { itemSizePx.toDp() }
+ val spacingSize = with(rule.density) { spacingSizePx.toDp() }
+ lateinit var state: TvLazyListState
+ rule.setContentWithTestViewConfiguration {
+ TvLazyRow(
+ Modifier.size(itemSize * 3),
+ state = rememberLazyListState().also { state = it },
+ horizontalArrangement = Arrangement.spacedBy(spacingSize),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(5) {
+ Box(
+ Modifier.size(itemSize).testTag("$it").focusable()
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy((itemSizePx + spacingSizePx / 2).toFloat())
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ assertThat(state.firstVisibleItemScrollOffset)
+ .isEqualTo(itemSizePx + spacingSizePx / 2)
+ }
+ }
+
+ // with reverseLayout == true
+
+ @Test
+ fun column_defaultArrangementIsBottomWithReverseLayout() {
+ rule.setContent {
+ TvLazyColumn(
+ reverseLayout = true,
+ modifier = Modifier.requiredSize(containerSize),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(2) {
+ Item(it)
+ }
+ }
+ }
+
+ assertArrangementForTwoItems(Arrangement.Bottom, reverseLayout = true)
+ }
+
+ @Test
+ fun row_defaultArrangementIsEndWithReverseLayout() {
+ rule.setContent {
+ TvLazyRow(
+ reverseLayout = true,
+ modifier = Modifier.requiredSize(containerSize),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(2) {
+ Item(it)
+ }
+ }
+ }
+
+ assertArrangementForTwoItems(
+ Arrangement.End, LayoutDirection.Ltr, reverseLayout = true
+ )
+ }
+
+ @Test
+ fun column_whenArrangementChanges() {
+ var arrangement by mutableStateOf(Arrangement.Top)
+ rule.setContent {
+ TvLazyColumn(
+ modifier = Modifier.requiredSize(containerSize),
+ verticalArrangement = arrangement,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(2) {
+ Item(it)
+ }
+ }
+ }
+
+ assertArrangementForTwoItems(Arrangement.Top)
+
+ rule.runOnIdle {
+ arrangement = Arrangement.Bottom
+ }
+
+ assertArrangementForTwoItems(Arrangement.Bottom)
+ }
+
+ @Test
+ fun row_whenArrangementChanges() {
+ var arrangement by mutableStateOf(Arrangement.Start)
+ rule.setContent {
+ TvLazyRow(
+ modifier = Modifier.requiredSize(containerSize),
+ horizontalArrangement = arrangement,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(2) {
+ Item(it)
+ }
+ }
+ }
+
+ assertArrangementForTwoItems(Arrangement.Start, LayoutDirection.Ltr)
+
+ rule.runOnIdle {
+ arrangement = Arrangement.End
+ }
+
+ assertArrangementForTwoItems(Arrangement.End, LayoutDirection.Ltr)
+ }
+
+ fun composeColumnWith(arrangement: Arrangement.Vertical) {
+ rule.setContent {
+ TvLazyColumn(
+ verticalArrangement = arrangement,
+ modifier = Modifier.requiredSize(containerSize),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(2) {
+ Item(it)
+ }
+ }
+ }
+ }
+
+ fun composeRowWith(arrangement: Arrangement.Horizontal, layoutDirection: LayoutDirection) {
+ rule.setContent {
+ CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
+ TvLazyRow(
+ horizontalArrangement = arrangement,
+ modifier = Modifier.requiredSize(containerSize),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(2) {
+ Item(it)
+ }
+ }
+ }
+ }
+ }
+
+ @Composable
+ fun Item(index: Int) {
+ require(index < 2)
+ val size = if (index == 0) itemSize else smallerItemSize
+ Box(Modifier.requiredSize(size).testTag(index.toString()))
+ }
+
+ fun assertArrangementForTwoItems(
+ arrangement: Arrangement.Vertical,
+ reverseLayout: Boolean = false
+ ) {
+ with(rule.density) {
+ val sizes = IntArray(2) {
+ val index = if (reverseLayout) if (it == 0) 1 else 0 else it
+ if (index == 0) itemSize.roundToPx() else smallerItemSize.roundToPx()
+ }
+ val outPositions = IntArray(2) { 0 }
+ with(arrangement) { arrange(containerSize.roundToPx(), sizes, outPositions) }
+
+ outPositions.forEachIndexed { index, position ->
+ val realIndex = if (reverseLayout) if (index == 0) 1 else 0 else index
+ rule.onNodeWithTag("$realIndex")
+ .assertTopPositionInRootIsEqualTo(position.toDp())
+ }
+ }
+ }
+
+ fun assertArrangementForTwoItems(
+ arrangement: Arrangement.Horizontal,
+ layoutDirection: LayoutDirection,
+ reverseLayout: Boolean = false
+ ) {
+ with(rule.density) {
+ val sizes = IntArray(2) {
+ val index = if (reverseLayout) if (it == 0) 1 else 0 else it
+ if (index == 0) itemSize.roundToPx() else smallerItemSize.roundToPx()
+ }
+ val outPositions = IntArray(2) { 0 }
+ with(arrangement) {
+ arrange(containerSize.roundToPx(), sizes, layoutDirection, outPositions)
+ }
+
+ outPositions.forEachIndexed { index, position ->
+ val realIndex = if (reverseLayout) if (index == 0) 1 else 0 else index
+ val expectedPosition = position.toDp()
+ rule.onNodeWithTag("$realIndex")
+ .assertLeftPositionInRootIsEqualTo(expectedPosition)
+ }
+ }
+ }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyColumnTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyColumnTest.kt
new file mode 100644
index 0000000..8e8bc51
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyColumnTest.kt
@@ -0,0 +1,502 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.list
+
+import android.os.Build
+import androidx.tv.foundation.lazy.AutoTestFrameClock
+import androidx.compose.foundation.background
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.requiredWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertCountEquals
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onChildren
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.tv.foundation.PivotOffsets
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+/**
+ * This class contains all LazyColumn-specific tests, as well as (by convention) tests that don't
+ * need to be run in both orientations.
+ *
+ * To have a test run in both orientations (LazyRow and LazyColumn), add it to [LazyListTest]
+ */
+class LazyColumnTest {
+ private val LazyListTag = "LazyListTag"
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun compositionsAreDisposed_whenDataIsChanged() {
+ var composed = 0
+ var disposals = 0
+ val data1 = (1..3).toList()
+ val data2 = (4..5).toList() // smaller, to ensure removal is handled properly
+
+ var part2 by mutableStateOf(false)
+
+ rule.setContentWithTestViewConfiguration {
+ TvLazyColumn(
+ Modifier.testTag(LazyListTag).fillMaxSize(),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(if (!part2) data1 else data2) {
+ DisposableEffect(NeverEqualObject) {
+ composed++
+ onDispose {
+ disposals++
+ }
+ }
+
+ Box(Modifier.height(50.dp).focusable())
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertWithMessage("Not all items were composed")
+ .that(composed).isEqualTo(data1.size)
+ composed = 0
+
+ part2 = true
+ }
+
+ rule.runOnIdle {
+ assertWithMessage(
+ "No additional items were composed after data change, something didn't work"
+ ).that(composed).isEqualTo(data2.size)
+
+ // We may need to modify this test once we prefetch/cache items outside the viewport
+ assertWithMessage(
+ "Not enough compositions were disposed after scrolling, compositions were leaked"
+ ).that(disposals).isEqualTo(data1.size)
+ }
+ }
+
+ @Test
+ fun compositionsAreDisposed_whenLazyListIsDisposed() {
+ var emitLazyList by mutableStateOf(true)
+ var disposeCalledOnFirstItem = false
+ var disposeCalledOnSecondItem = false
+
+ rule.setContentWithTestViewConfiguration {
+ if (emitLazyList) {
+ TvLazyColumn(
+ Modifier.fillMaxSize(),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(2) {
+ Box(Modifier.requiredSize(100.dp).focusable())
+ DisposableEffect(Unit) {
+ onDispose {
+ if (it == 1) {
+ disposeCalledOnFirstItem = true
+ } else {
+ disposeCalledOnSecondItem = true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertWithMessage("First item was incorrectly immediately disposed")
+ .that(disposeCalledOnFirstItem).isFalse()
+ assertWithMessage("Second item was incorrectly immediately disposed")
+ .that(disposeCalledOnFirstItem).isFalse()
+ emitLazyList = false
+ }
+
+ rule.runOnIdle {
+ assertWithMessage("First item was not correctly disposed")
+ .that(disposeCalledOnFirstItem).isTrue()
+ assertWithMessage("Second item was not correctly disposed")
+ .that(disposeCalledOnSecondItem).isTrue()
+ }
+ }
+
+ @Test
+ fun removeItemsTest() {
+ val startingNumItems = 3
+ var numItems = startingNumItems
+ var numItemsModel by mutableStateOf(numItems)
+ val tag = "List"
+ rule.setContentWithTestViewConfiguration {
+ TvLazyColumn(
+ Modifier.testTag(tag),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items((1..numItemsModel).toList()) {
+ BasicText("$it")
+ }
+ }
+ }
+
+ while (numItems >= 0) {
+ // Confirm the number of children to ensure there are no extra items
+ rule.onNodeWithTag(tag)
+ .onChildren()
+ .assertCountEquals(numItems)
+
+ // Confirm the children's content
+ for (i in 1..3) {
+ rule.onNodeWithText("$i").apply {
+ if (i <= numItems) {
+ assertExists()
+ } else {
+ assertDoesNotExist()
+ }
+ }
+ }
+ numItems--
+ if (numItems >= 0) {
+ // Don't set the model to -1
+ rule.runOnIdle { numItemsModel = numItems }
+ }
+ }
+ }
+
+ @Test
+ fun changeItemsCountAndScrollImmediately() {
+ lateinit var state: TvLazyListState
+ var count by mutableStateOf(100)
+ val composedIndexes = mutableListOf<Int>()
+ rule.setContent {
+ state = rememberLazyListState()
+ TvLazyColumn(
+ Modifier.fillMaxWidth().height(10.dp),
+ state,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(count) { index ->
+ composedIndexes.add(index)
+ Box(Modifier.size(20.dp).focusable())
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ composedIndexes.clear()
+ count = 10
+ runBlocking(AutoTestFrameClock()) {
+ state.scrollToItem(50)
+ }
+ composedIndexes.forEach {
+ assertThat(it).isLessThan(count)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(9)
+ }
+ }
+
+ @Test
+ fun changingDataTest() {
+ val dataLists = listOf(
+ (1..3).toList(),
+ (4..8).toList(),
+ (3..4).toList()
+ )
+ var dataModel by mutableStateOf(dataLists[0])
+ val tag = "List"
+ rule.setContentWithTestViewConfiguration {
+ TvLazyColumn(
+ Modifier.testTag(tag),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(dataModel) {
+ BasicText("$it")
+ }
+ }
+ }
+
+ for (data in dataLists) {
+ rule.runOnIdle { dataModel = data }
+
+ // Confirm the number of children to ensure there are no extra items
+ val numItems = data.size
+ rule.onNodeWithTag(tag)
+ .onChildren()
+ .assertCountEquals(numItems)
+
+ // Confirm the children's content
+ for (item in data) {
+ rule.onNodeWithText("$item").assertExists()
+ }
+ }
+ }
+
+ private val firstItemTag = "firstItemTag"
+ private val secondItemTag = "secondItemTag"
+
+ private fun prepareLazyColumnsItemsAlignment(horizontalGravity: Alignment.Horizontal) {
+ rule.setContentWithTestViewConfiguration {
+ TvLazyColumn(
+ Modifier.testTag(LazyListTag).requiredWidth(100.dp),
+ horizontalAlignment = horizontalGravity,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(listOf(1, 2)) {
+ if (it == 1) {
+ Box(Modifier.size(50.dp).testTag(firstItemTag).focusable())
+ } else {
+ Box(Modifier.size(70.dp).testTag(secondItemTag).focusable())
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag(secondItemTag)
+ .assertIsDisplayed()
+
+ val lazyColumnBounds = rule.onNodeWithTag(LazyListTag)
+ .getUnclippedBoundsInRoot()
+
+ with(rule.density) {
+ // Verify the width of the column
+ assertThat(lazyColumnBounds.left.roundToPx()).isWithin1PixelFrom(0.dp.roundToPx())
+ assertThat(lazyColumnBounds.right.roundToPx()).isWithin1PixelFrom(100.dp.roundToPx())
+ }
+ }
+
+ @Test
+ fun lazyColumnAlignmentCenterHorizontally() {
+ prepareLazyColumnsItemsAlignment(Alignment.CenterHorizontally)
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertPositionInRootIsEqualTo(25.dp, 0.dp)
+
+ rule.onNodeWithTag(secondItemTag)
+ .assertPositionInRootIsEqualTo(15.dp, 50.dp)
+ }
+
+ @Test
+ fun lazyColumnAlignmentStart() {
+ prepareLazyColumnsItemsAlignment(Alignment.Start)
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+
+ rule.onNodeWithTag(secondItemTag)
+ .assertPositionInRootIsEqualTo(0.dp, 50.dp)
+ }
+
+ @Test
+ fun lazyColumnAlignmentEnd() {
+ prepareLazyColumnsItemsAlignment(Alignment.End)
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertPositionInRootIsEqualTo(50.dp, 0.dp)
+
+ rule.onNodeWithTag(secondItemTag)
+ .assertPositionInRootIsEqualTo(30.dp, 50.dp)
+ }
+
+ @Test
+ fun removalWithMutableStateListOf() {
+ val items = mutableStateListOf("1", "2", "3")
+
+ val itemSize = with(rule.density) { 15.toDp() }
+
+ rule.setContentWithTestViewConfiguration {
+ TvLazyColumn {
+ items(items) { item ->
+ Spacer(Modifier.size(itemSize).testTag(item))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ items.removeLast()
+ }
+
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("3")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun recompositionOrder() {
+ val outerState = mutableStateOf(0)
+ val innerState = mutableStateOf(0)
+ val recompositions = mutableListOf<Pair<Int, Int>>()
+
+ rule.setContent {
+ val localOuterState = outerState.value
+ TvLazyColumn {
+ items(count = 1) {
+ recompositions.add(localOuterState to innerState.value)
+ Box(Modifier.fillMaxSize())
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ innerState.value++
+ outerState.value++
+ }
+
+ rule.runOnIdle {
+ assertThat(recompositions).isEqualTo(
+ listOf(0 to 0, 1 to 1)
+ )
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun scrolledAwayItemIsNotDisplayedAnymore() {
+ lateinit var state: TvLazyListState
+ rule.setContentWithTestViewConfiguration {
+ state = rememberLazyListState()
+ TvLazyColumn(
+ Modifier
+ .requiredSize(10.dp)
+ .testTag(LazyListTag)
+ .graphicsLayer()
+ .background(Color.Blue),
+ state = state,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(2) {
+ val size = if (it == 0) 5.dp else 100.dp
+ val color = if (it == 0) Color.Red else Color.Transparent
+ Box(
+ Modifier
+ .fillMaxWidth()
+ .height(size)
+ .background(color)
+ .testTag("$it")
+ .focusable()
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ with(rule.density) {
+ runBlocking {
+ // we scroll enough to make the Red item not visible anymore
+ state.scrollBy(6.dp.toPx())
+ }
+ }
+ }
+
+ // and verify there is no Red item displayed
+ rule.onNodeWithTag(LazyListTag)
+ .captureToImage()
+ .assertPixels {
+ Color.Blue
+ }
+ }
+
+ @Test
+ fun wrappedNestedLazyRowDisplayCorrectContent() {
+ lateinit var state: TvLazyListState
+ rule.setContentWithTestViewConfiguration {
+ state = rememberLazyListState()
+ TvLazyColumn(
+ Modifier.size(20.dp),
+ state = state,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(100) {
+ LazyRowWrapped {
+ BasicText("$it", Modifier.size(21.dp))
+ }
+ }
+ }
+ }
+
+ (1..10).forEach { item ->
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(item)
+ }
+ }
+
+ rule.onNodeWithText("$item")
+ .assertIsDisplayed()
+ }
+ }
+
+ @Composable
+ private fun LazyRowWrapped(content: @Composable () -> Unit) {
+ TvLazyRow {
+ items(count = 1) {
+ content()
+ }
+ }
+ }
+}
+
+internal fun Modifier.drawOutsideOfBounds() = drawBehind {
+ val inflate = 20.dp.roundToPx().toFloat()
+ drawRect(
+ Color.Red,
+ Offset(-inflate, -inflate),
+ Size(size.width + inflate * 2, size.height + inflate * 2)
+ )
+}
\ No newline at end of file
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyCustomKeysTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyCustomKeysTest.kt
new file mode 100644
index 0000000..69d0123
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyCustomKeysTest.kt
@@ -0,0 +1,491 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.list
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.tv.foundation.PivotOffsets
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalFoundationApi::class)
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class LazyCustomKeysTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ val itemSize = with(rule.density) {
+ 100.toDp()
+ }
+
+ @Test
+ fun itemsWithKeysAreLaidOutCorrectly() {
+ val list = listOf(MyClass(0), MyClass(1), MyClass(2))
+
+ rule.setContent {
+ TvLazyColumn {
+ items(list, key = { it.id }) {
+ Item("${it.id}")
+ }
+ }
+ }
+
+ assertItems("0", "1", "2")
+ }
+
+ @Test
+ fun removing_statesAreMoved() {
+ var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
+
+ rule.setContent {
+ TvLazyColumn {
+ items(list, key = { it.id }) {
+ Item(remember { "${it.id}" })
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(list[0], list[2])
+ }
+
+ assertItems("0", "2")
+ }
+
+ @Test
+ fun reordering_statesAreMoved_list() {
+ testReordering { list ->
+ items(list, key = { it.id }) {
+ Item(remember { "${it.id}" })
+ }
+ }
+ }
+
+ @Test
+ fun reordering_statesAreMoved_list_indexed() {
+ testReordering { list ->
+ itemsIndexed(list, key = { _, item -> item.id }) { _, item ->
+ Item(remember { "${item.id}" })
+ }
+ }
+ }
+
+ @Test
+ fun reordering_statesAreMoved_array() {
+ testReordering { list ->
+ val array = list.toTypedArray()
+ items(array, key = { it.id }) {
+ Item(remember { "${it.id}" })
+ }
+ }
+ }
+
+ @Test
+ fun reordering_statesAreMoved_array_indexed() {
+ testReordering { list ->
+ val array = list.toTypedArray()
+ itemsIndexed(array, key = { _, item -> item.id }) { _, item ->
+ Item(remember { "${item.id}" })
+ }
+ }
+ }
+
+ @Test
+ fun reordering_statesAreMoved_itemsWithCount() {
+ testReordering { list ->
+ items(list.size, key = { list[it].id }) {
+ Item(remember { "${list[it].id}" })
+ }
+ }
+ }
+
+ @Test
+ fun fullyReplacingTheList() {
+ var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
+ var counter = 0
+
+ rule.setContent {
+ TvLazyColumn {
+ items(list, key = { it.id }) {
+ Item(remember { counter++ }.toString())
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(MyClass(3), MyClass(4), MyClass(5), MyClass(6))
+ }
+
+ assertItems("3", "4", "5", "6")
+ }
+
+ @Test
+ fun keepingOneItem() {
+ var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
+ var counter = 0
+
+ rule.setContent {
+ TvLazyColumn {
+ items(list, key = { it.id }) {
+ Item(remember { counter++ }.toString())
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(MyClass(1))
+ }
+
+ assertItems("1")
+ }
+
+ @Test
+ fun keepingOneItemAndAddingMore() {
+ var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
+ var counter = 0
+
+ rule.setContent {
+ TvLazyColumn {
+ items(list, key = { it.id }) {
+ Item(remember { counter++ }.toString())
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(MyClass(1), MyClass(3))
+ }
+
+ assertItems("1", "3")
+ }
+
+ @Test
+ fun mixingKeyedItemsAndNot() {
+ testReordering { list ->
+ item {
+ Item("${list.first().id}")
+ }
+ items(list.subList(fromIndex = 1, toIndex = list.size), key = { it.id }) {
+ Item(remember { "${it.id}" })
+ }
+ }
+ }
+
+ @Test
+ fun updatingTheDataSetIsCorrectlyApplied() {
+ val state = mutableStateOf(emptyList<Int>())
+
+ rule.setContent {
+ LaunchedEffect(Unit) {
+ state.value = listOf(4, 1, 3)
+ }
+
+ val list = state.value
+
+ TvLazyColumn(
+ Modifier.fillMaxSize(),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(list, key = { it }) {
+ Item(it.toString())
+ }
+ }
+ }
+
+ assertItems("4", "1", "3")
+
+ rule.runOnIdle {
+ state.value = listOf(2, 4, 6, 1, 3, 5)
+ }
+
+ assertItems("2", "4", "6", "1", "3", "5")
+ }
+
+ @Test
+ fun reordering_usingMutableStateListOf() {
+ val list = mutableStateListOf(MyClass(0), MyClass(1), MyClass(2))
+
+ rule.setContent {
+ TvLazyColumn {
+ items(list, key = { it.id }) {
+ Item(remember { "${it.id}" })
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list.add(list.removeAt(1))
+ }
+
+ assertItems("0", "2", "1")
+ }
+
+ @Test
+ fun keysInLazyListItemInfoAreCorrect() {
+ val list = listOf(MyClass(0), MyClass(1), MyClass(2))
+ lateinit var state: TvLazyListState
+
+ rule.setContent {
+ state = rememberLazyListState()
+ TvLazyColumn(
+ state = state,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)) {
+ items(list, key = { it.id }) {
+ Item(remember { "${it.id}" })
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(
+ state.visibleKeys
+ ).isEqualTo(listOf(0, 1, 2))
+ }
+ }
+
+ @Test
+ fun keysInLazyListItemInfoAreCorrectAfterReordering() {
+ var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
+ lateinit var state: TvLazyListState
+
+ rule.setContent {
+ state = rememberLazyListState()
+ TvLazyColumn(
+ state = state,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(list, key = { it.id }) {
+ Item(remember { "${it.id}" })
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(list[0], list[2], list[1])
+ }
+
+ rule.runOnIdle {
+ assertThat(
+ state.visibleKeys
+ ).isEqualTo(listOf(0, 2, 1))
+ }
+ }
+
+ @Test
+ fun addingItemsBeforeWithoutKeysIsMaintainingTheIndex() {
+ var list by mutableStateOf((10..15).toList())
+ lateinit var state: TvLazyListState
+
+ rule.setContent {
+ state = rememberLazyListState()
+ TvLazyColumn(
+ Modifier.size(itemSize * 2.5f),
+ state,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(list) {
+ Item(remember { "$it" })
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = (0..15).toList()
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ }
+ }
+
+ @Test
+ fun addingItemsBeforeKeepingThisItemFirst() {
+ var list by mutableStateOf((10..15).toList())
+ lateinit var state: TvLazyListState
+
+ rule.setContent {
+ state = rememberLazyListState()
+ TvLazyColumn(
+ Modifier.size(itemSize * 2.5f),
+ state,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(list, key = { it }) {
+ Item(remember { "$it" })
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = (0..15).toList()
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(10)
+ assertThat(
+ state.visibleKeys
+ ).isEqualTo(listOf(10, 11, 12))
+ }
+ }
+
+ @Test
+ fun addingItemsRightAfterKeepingThisItemFirst() {
+ var list by mutableStateOf((0..5).toList() + (10..15).toList())
+ lateinit var state: TvLazyListState
+
+ rule.setContent {
+ state = rememberLazyListState(5)
+ TvLazyColumn(
+ Modifier.size(itemSize * 2.5f),
+ state,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(list, key = { it }) {
+ Item(remember { "$it" })
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = (0..15).toList()
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(5)
+ assertThat(
+ state.visibleKeys
+ ).isEqualTo(listOf(5, 6, 7))
+ }
+ }
+
+ @Test
+ fun addingItemsBeforeWhileCurrentItemIsNotInTheBeginning() {
+ var list by mutableStateOf((10..30).toList())
+ lateinit var state: TvLazyListState
+
+ rule.setContent {
+ state = rememberLazyListState(10) // key 20 is the first item
+ TvLazyColumn(
+ Modifier.size(itemSize * 2.5f),
+ state,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(list, key = { it }) {
+ Item(remember { "$it" })
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = (0..30).toList()
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(20)
+ assertThat(
+ state.visibleKeys
+ ).isEqualTo(listOf(20, 21, 22))
+ }
+ }
+
+ @Test
+ fun removingTheCurrentItemMaintainsTheIndex() {
+ var list by mutableStateOf((0..20).toList())
+ lateinit var state: TvLazyListState
+
+ rule.setContent {
+ state = rememberLazyListState(5)
+ TvLazyColumn(
+ Modifier.size(itemSize * 2.5f),
+ state,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(list, key = { it }) {
+ Item(remember { "$it" })
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = (0..20) - 5
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(5)
+ assertThat(state.visibleKeys).isEqualTo(listOf(6, 7, 8))
+ }
+ }
+
+ private fun testReordering(content: TvLazyListScope.(List<MyClass>) -> Unit) {
+ var list by mutableStateOf(listOf(MyClass(0), MyClass(1), MyClass(2)))
+
+ rule.setContent {
+ TvLazyColumn {
+ content(list)
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(list[0], list[2], list[1])
+ }
+
+ assertItems("0", "2", "1")
+ }
+
+ private fun assertItems(vararg tags: String) {
+ var currentTop = 0.dp
+ tags.forEach {
+ rule.onNodeWithTag(it)
+ .assertTopPositionInRootIsEqualTo(currentTop)
+ .assertHeightIsEqualTo(itemSize)
+ currentTop += itemSize
+ }
+ }
+
+ @Composable
+ private fun Item(tag: String) {
+ Spacer(
+ Modifier.testTag(tag).size(itemSize)
+ )
+ }
+
+ private class MyClass(val id: Int)
+}
+
+val TvLazyListState.visibleKeys: List<Any> get() = layoutInfo.visibleItemsInfo.map { it.key }
\ No newline at end of file
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyItemStateRestoration.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyItemStateRestoration.kt
new file mode 100644
index 0000000..6a037fb
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyItemStateRestoration.kt
@@ -0,0 +1,320 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.list
+
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.StateRestorationTester
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.PivotOffsets
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+
+class LazyItemStateRestoration {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun visibleItemsStateRestored() {
+ val restorationTester = StateRestorationTester(rule)
+ var counter0 = 1
+ var counter1 = 10
+ var counter2 = 100
+ var realState = arrayOf(0, 0, 0)
+ restorationTester.setContent {
+ TvLazyColumn {
+ item {
+ realState[0] = rememberSaveable { counter0++ }
+ Box(Modifier.requiredSize(1.dp))
+ }
+ items((1..2).toList()) {
+ if (it == 1) {
+ realState[1] = rememberSaveable { counter1++ }
+ } else {
+ realState[2] = rememberSaveable { counter2++ }
+ }
+ Box(Modifier.requiredSize(1.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(realState[0]).isEqualTo(1)
+ assertThat(realState[1]).isEqualTo(10)
+ assertThat(realState[2]).isEqualTo(100)
+ realState = arrayOf(0, 0, 0)
+ }
+
+ restorationTester.emulateSavedInstanceStateRestore()
+
+ rule.runOnIdle {
+ assertThat(realState[0]).isEqualTo(1)
+ assertThat(realState[1]).isEqualTo(10)
+ assertThat(realState[2]).isEqualTo(100)
+ }
+ }
+
+ @Test
+ fun itemsStateRestoredWhenWeScrolledBackToIt() {
+ val restorationTester = StateRestorationTester(rule)
+ var counter0 = 1
+ lateinit var state: TvLazyListState
+ var itemDisposed = false
+ var realState = 0
+ restorationTester.setContent {
+ TvLazyColumn(
+ Modifier.requiredSize(20.dp),
+ state = rememberLazyListState().also { state = it },
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items((0..10).toList()) {
+ if (it == 0) {
+ realState = rememberSaveable { counter0++ }
+ DisposableEffect(Unit) {
+ onDispose {
+ itemDisposed = true
+ }
+ }
+ }
+ Box(Modifier.requiredSize(30.dp).focusable())
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(realState).isEqualTo(1)
+ runBlocking {
+ // we scroll through multiple items to make sure the 0th element is not kept in
+ // the reusable items buffer
+ state.scrollToItem(3)
+ state.scrollToItem(5)
+ state.scrollToItem(8)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(itemDisposed).isEqualTo(true)
+ realState = 0
+ runBlocking {
+ state.scrollToItem(0, 0)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(realState).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun itemsStateRestoredWhenWeScrolledRestoredAndScrolledBackTo() {
+ val restorationTester = StateRestorationTester(rule)
+ var counter0 = 1
+ var counter1 = 10
+ lateinit var state: TvLazyListState
+ var realState = arrayOf(0, 0)
+ restorationTester.setContent {
+ TvLazyColumn(
+ Modifier.requiredSize(20.dp),
+ state = rememberLazyListState().also { state = it },
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items((0..1).toList()) {
+ if (it == 0) {
+ realState[0] = rememberSaveable { counter0++ }
+ } else {
+ realState[1] = rememberSaveable { counter1++ }
+ }
+ Box(Modifier.requiredSize(30.dp).focusable())
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(realState[0]).isEqualTo(1)
+ runBlocking {
+ state.scrollToItem(1, 5)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(realState[1]).isEqualTo(10)
+ realState = arrayOf(0, 0)
+ }
+
+ restorationTester.emulateSavedInstanceStateRestore()
+
+ rule.runOnIdle {
+ assertThat(realState[1]).isEqualTo(10)
+ runBlocking {
+ state.scrollToItem(0, 0)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(realState[0]).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun nestedLazy_itemsStateRestoredWhenWeScrolledBackToIt() {
+ val restorationTester = StateRestorationTester(rule)
+ var counter0 = 1
+ lateinit var state: TvLazyListState
+ var itemDisposed = false
+ var realState = 0
+ restorationTester.setContent {
+ TvLazyColumn(
+ Modifier.requiredSize(20.dp),
+ state = rememberLazyListState().also { state = it },
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items((0..10).toList()) {
+ if (it == 0) {
+ TvLazyRow {
+ item {
+ realState = rememberSaveable { counter0++ }
+ DisposableEffect(Unit) {
+ onDispose {
+ itemDisposed = true
+ }
+ }
+ Box(Modifier.requiredSize(30.dp).focusable())
+ }
+ }
+ } else {
+ Box(Modifier.requiredSize(30.dp).focusable())
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(realState).isEqualTo(1)
+ runBlocking {
+ // we scroll through multiple items to make sure the 0th element is not kept in
+ // the reusable items buffer
+ state.scrollToItem(3)
+ state.scrollToItem(5)
+ state.scrollToItem(8)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(itemDisposed).isEqualTo(true)
+ realState = 0
+ runBlocking {
+ state.scrollToItem(0, 0)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(realState).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun stateRestoredWhenUsedWithCustomKeys() {
+ val restorationTester = StateRestorationTester(rule)
+ var counter0 = 1
+ var counter1 = 10
+ var counter2 = 100
+ var realState = arrayOf(0, 0, 0)
+ restorationTester.setContent {
+ TvLazyColumn {
+ items(3, key = { "$it" }) {
+ if (it == 0) {
+ realState[0] = rememberSaveable { counter0++ }
+ } else if (it == 1) {
+ realState[1] = rememberSaveable { counter1++ }
+ } else {
+ realState[2] = rememberSaveable { counter2++ }
+ }
+ Box(Modifier.requiredSize(1.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(realState[0]).isEqualTo(1)
+ assertThat(realState[1]).isEqualTo(10)
+ assertThat(realState[2]).isEqualTo(100)
+ realState = arrayOf(0, 0, 0)
+ }
+
+ restorationTester.emulateSavedInstanceStateRestore()
+
+ rule.runOnIdle {
+ assertThat(realState[0]).isEqualTo(1)
+ assertThat(realState[1]).isEqualTo(10)
+ assertThat(realState[2]).isEqualTo(100)
+ }
+ }
+
+ @Test
+ fun stateRestoredWhenUsedWithCustomKeysAfterReordering() {
+ val restorationTester = StateRestorationTester(rule)
+ var counter0 = 1
+ var counter1 = 10
+ var counter2 = 100
+ var realState = arrayOf(0, 0, 0)
+ var list by mutableStateOf(listOf(0, 1, 2))
+ restorationTester.setContent {
+ TvLazyColumn {
+ items(list, key = { "$it" }) {
+ if (it == 0) {
+ realState[0] = rememberSaveable { counter0++ }
+ } else if (it == 1) {
+ realState[1] = rememberSaveable { counter1++ }
+ } else {
+ realState[2] = rememberSaveable { counter2++ }
+ }
+ Box(Modifier.requiredSize(1.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(1, 2)
+ }
+
+ rule.runOnIdle {
+ assertThat(realState[0]).isEqualTo(1)
+ assertThat(realState[1]).isEqualTo(10)
+ assertThat(realState[2]).isEqualTo(100)
+ realState = arrayOf(0, 0, 0)
+ }
+
+ restorationTester.emulateSavedInstanceStateRestore()
+
+ rule.runOnIdle {
+ assertThat(realState[0]).isEqualTo(0)
+ assertThat(realState[1]).isEqualTo(10)
+ assertThat(realState[2]).isEqualTo(100)
+ }
+ }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt
new file mode 100644
index 0000000..351bf6f
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt
@@ -0,0 +1,1226 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.list
+
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.requiredHeightIn
+import androidx.compose.foundation.layout.requiredWidth
+import androidx.compose.foundation.layout.requiredWidthIn
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.getBoundsInRoot
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.height
+import androidx.compose.ui.unit.width
+import androidx.test.filters.LargeTest
+import androidx.tv.foundation.PivotOffsets
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.runBlocking
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import kotlin.math.roundToInt
+
+@LargeTest
+@RunWith(Parameterized::class)
+@OptIn(ExperimentalFoundationApi::class)
+class LazyListAnimateItemPlacementTest(private val config: Config) {
+
+ private val isVertical: Boolean get() = config.isVertical
+ private val reverseLayout: Boolean get() = config.reverseLayout
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ private val itemSize: Int = 50
+ private var itemSizeDp: Dp = Dp.Infinity
+ private val itemSize2: Int = 30
+ private var itemSize2Dp: Dp = Dp.Infinity
+ private val itemSize3: Int = 20
+ private var itemSize3Dp: Dp = Dp.Infinity
+ private val containerSize: Int = itemSize * 5
+ private var containerSizeDp: Dp = Dp.Infinity
+ private val spacing: Int = 10
+ private var spacingDp: Dp = Dp.Infinity
+ private val itemSizePlusSpacing = itemSize + spacing
+ private var itemSizePlusSpacingDp = Dp.Infinity
+ private lateinit var state: TvLazyListState
+
+ @Before
+ fun before() {
+ rule.mainClock.autoAdvance = false
+ with(rule.density) {
+ itemSizeDp = itemSize.toDp()
+ itemSize2Dp = itemSize2.toDp()
+ itemSize3Dp = itemSize3.toDp()
+ containerSizeDp = containerSize.toDp()
+ spacingDp = spacing.toDp()
+ itemSizePlusSpacingDp = itemSizePlusSpacing.toDp()
+ }
+ }
+
+ @Test
+ fun reorderTwoItems() {
+ var list by mutableStateOf(listOf(0, 1))
+ rule.setContent {
+ LazyList {
+ items(list, key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ assertPositions(0 to 0, 1 to itemSize)
+
+ rule.runOnIdle {
+ list = listOf(1, 0)
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 0 to 0 + (itemSize * fraction).roundToInt(),
+ 1 to itemSize - (itemSize * fraction).roundToInt(),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun reorderTwoItems_layoutInfoHasFinalPositions() {
+ var list by mutableStateOf(listOf(0, 1))
+ rule.setContent {
+ LazyList {
+ items(list, key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ assertLayoutInfoPositions(0 to 0, 1 to itemSize)
+
+ rule.runOnIdle {
+ list = listOf(1, 0)
+ }
+
+ onAnimationFrame {
+ // fraction doesn't affect the offsets in layout info
+ assertLayoutInfoPositions(1 to 0, 0 to itemSize)
+ }
+ }
+
+ @Test
+ fun reorderFirstAndLastItems() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+ rule.setContent {
+ LazyList {
+ items(list, key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ assertPositions(
+ 0 to 0,
+ 1 to itemSize,
+ 2 to itemSize * 2,
+ 3 to itemSize * 3,
+ 4 to itemSize * 4,
+ )
+
+ rule.runOnIdle {
+ list = listOf(4, 1, 2, 3, 0)
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 0 to 0 + (itemSize * 4 * fraction).roundToInt(),
+ 1 to itemSize,
+ 2 to itemSize * 2,
+ 3 to itemSize * 3,
+ 4 to itemSize * 4 - (itemSize * 4 * fraction).roundToInt(),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun moveFirstItemToEndCausingAllItemsToAnimate() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+ rule.setContent {
+ LazyList {
+ items(list, key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ assertPositions(
+ 0 to 0,
+ 1 to itemSize,
+ 2 to itemSize * 2,
+ 3 to itemSize * 3,
+ 4 to itemSize * 4,
+ )
+
+ rule.runOnIdle {
+ list = listOf(1, 2, 3, 4, 0)
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 0 to 0 + (itemSize * 4 * fraction).roundToInt(),
+ 1 to itemSize - (itemSize * fraction).roundToInt(),
+ 2 to itemSize * 2 - (itemSize * fraction).roundToInt(),
+ 3 to itemSize * 3 - (itemSize * fraction).roundToInt(),
+ 4 to itemSize * 4 - (itemSize * fraction).roundToInt(),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun itemSizeChangeAnimatesNextItems() {
+ var size by mutableStateOf(itemSizeDp)
+ rule.setContent {
+ LazyList(
+ minSize = itemSizeDp * 5,
+ maxSize = itemSizeDp * 5
+ ) {
+ items(listOf(0, 1, 2, 3), key = { it }) {
+ Item(it, size = if (it == 1) size else itemSizeDp)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ size = itemSizeDp * 2
+ }
+ rule.mainClock.advanceTimeByFrame()
+
+ rule.onNodeWithTag("1")
+ .assertMainAxisSizeIsEqualTo(size)
+
+ onAnimationFrame { fraction ->
+ if (!reverseLayout) {
+ assertPositions(
+ 0 to 0,
+ 1 to itemSize,
+ 2 to itemSize * 2 + (itemSize * fraction).roundToInt(),
+ 3 to itemSize * 3 + (itemSize * fraction).roundToInt(),
+ fraction = fraction,
+ autoReverse = false
+ )
+ } else {
+ assertPositions(
+ 3 to itemSize - (itemSize * fraction).roundToInt(),
+ 2 to itemSize * 2 - (itemSize * fraction).roundToInt(),
+ 1 to itemSize * 3 - (itemSize * fraction).roundToInt(),
+ 0 to itemSize * 4,
+ fraction = fraction,
+ autoReverse = false
+ )
+ }
+ }
+ }
+
+ @Test
+ fun onlyItemsWithModifierAnimates() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+ rule.setContent {
+ LazyList {
+ items(list, key = { it }) {
+ Item(it, animSpec = if (it == 1 || it == 3) AnimSpec else null)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(1, 2, 3, 4, 0)
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 0 to itemSize * 4,
+ 1 to itemSize - (itemSize * fraction).roundToInt(),
+ 2 to itemSize,
+ 3 to itemSize * 3 - (itemSize * fraction).roundToInt(),
+ 4 to itemSize * 3,
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun animationsWithDifferentDurations() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+ rule.setContent {
+ LazyList {
+ items(list, key = { it }) {
+ val duration = if (it == 1 || it == 3) Duration * 2 else Duration
+ Item(it, animSpec = tween(duration.toInt(), easing = LinearEasing))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(1, 2, 3, 4, 0)
+ }
+
+ onAnimationFrame(duration = Duration * 2) { fraction ->
+ val shorterAnimFraction = (fraction * 2).coerceAtMost(1f)
+ assertPositions(
+ 0 to 0 + (itemSize * 4 * shorterAnimFraction).roundToInt(),
+ 1 to itemSize - (itemSize * fraction).roundToInt(),
+ 2 to itemSize * 2 - (itemSize * shorterAnimFraction).roundToInt(),
+ 3 to itemSize * 3 - (itemSize * fraction).roundToInt(),
+ 4 to itemSize * 4 - (itemSize * shorterAnimFraction).roundToInt(),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun multipleChildrenPerItem() {
+ var list by mutableStateOf(listOf(0, 2))
+ rule.setContent {
+ LazyList {
+ items(list, key = { it }) {
+ Item(it)
+ Item(it + 1)
+ }
+ }
+ }
+
+ assertPositions(
+ 0 to 0,
+ 1 to itemSize,
+ 2 to itemSize * 2,
+ 3 to itemSize * 3,
+ )
+
+ rule.runOnIdle {
+ list = listOf(2, 0)
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 0 to 0 + (itemSize * 2 * fraction).roundToInt(),
+ 1 to itemSize + (itemSize * 2 * fraction).roundToInt(),
+ 2 to itemSize * 2 - (itemSize * 2 * fraction).roundToInt(),
+ 3 to itemSize * 3 - (itemSize * 2 * fraction).roundToInt(),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun multipleChildrenPerItemSomeDoNotAnimate() {
+ var list by mutableStateOf(listOf(0, 2))
+ rule.setContent {
+ LazyList {
+ items(list, key = { it }) {
+ Item(it)
+ Item(it + 1, animSpec = null)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(2, 0)
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 0 to 0 + (itemSize * 2 * fraction).roundToInt(),
+ 1 to itemSize * 3,
+ 2 to itemSize * 2 - (itemSize * 2 * fraction).roundToInt(),
+ 3 to itemSize,
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun animateArrangementChange() {
+ var arrangement by mutableStateOf(Arrangement.Center)
+ rule.setContent {
+ LazyList(
+ arrangement = arrangement,
+ minSize = itemSizeDp * 5,
+ maxSize = itemSizeDp * 5
+ ) {
+ items(listOf(1, 2, 3), key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ assertPositions(
+ 1 to itemSize,
+ 2 to itemSize * 2,
+ 3 to itemSize * 3,
+ )
+
+ rule.runOnIdle {
+ arrangement = Arrangement.SpaceBetween
+ }
+ rule.mainClock.advanceTimeByFrame()
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 1 to itemSize - (itemSize * fraction).roundToInt(),
+ 2 to itemSize * 2,
+ 3 to itemSize * 3 + (itemSize * fraction).roundToInt(),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun moveItemToTheBottomOutsideOfBounds() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5))
+ rule.setContent {
+ LazyList(maxSize = itemSizeDp * 3) {
+ items(list, key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ assertPositions(
+ 0 to 0,
+ 1 to itemSize,
+ 2 to itemSize * 2
+ )
+
+ rule.runOnIdle {
+ list = listOf(0, 4, 2, 3, 1, 5)
+ }
+
+ onAnimationFrame { fraction ->
+ val item1Offset = itemSize + (itemSize * 3 * fraction).roundToInt()
+ val item4Offset = itemSize * 4 - (itemSize * 3 * fraction).roundToInt()
+ val expected = mutableListOf<Pair<Any, Int>>().apply {
+ add(0 to 0)
+ if (item1Offset < itemSize * 3) {
+ add(1 to item1Offset)
+ } else {
+ rule.onNodeWithTag("1").assertIsNotDisplayed()
+ }
+ add(2 to itemSize * 2)
+ if (item4Offset < itemSize * 3) {
+ add(4 to item4Offset)
+ } else {
+ rule.onNodeWithTag("4").assertIsNotDisplayed()
+ }
+ }
+ assertPositions(
+ expected = expected.toTypedArray(),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun moveItemToTheTopOutsideOfBounds() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5))
+ rule.setContent {
+ LazyList(maxSize = itemSizeDp * 3f, startIndex = 3) {
+ items(list, key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ assertPositions(
+ 3 to 0,
+ 4 to itemSize,
+ 5 to itemSize * 2
+ )
+
+ rule.runOnIdle {
+ list = listOf(2, 4, 0, 3, 1, 5)
+ }
+
+ onAnimationFrame { fraction ->
+ val item1Offset = itemSize * -2 + (itemSize * 3 * fraction).roundToInt()
+ val item4Offset = itemSize - (itemSize * 3 * fraction).roundToInt()
+ val expected = mutableListOf<Pair<Any, Int>>().apply {
+ if (item4Offset > -itemSize) {
+ add(4 to item4Offset)
+ } else {
+ rule.onNodeWithTag("4").assertIsNotDisplayed()
+ }
+ add(3 to 0)
+ if (item1Offset > -itemSize) {
+ add(1 to item1Offset)
+ } else {
+ rule.onNodeWithTag("1").assertIsNotDisplayed()
+ }
+ add(5 to itemSize * 2)
+ }
+ assertPositions(
+ expected = expected.toTypedArray(),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun moveFirstItemToEndCausingAllItemsToAnimate_withSpacing() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3))
+ rule.setContent {
+ LazyList(arrangement = Arrangement.spacedBy(spacingDp)) {
+ items(list, key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(1, 2, 3, 0)
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 0 to 0 + (itemSizePlusSpacing * 3 * fraction).roundToInt(),
+ 1 to itemSizePlusSpacing - (itemSizePlusSpacing * fraction).roundToInt(),
+ 2 to itemSizePlusSpacing * 2 - (itemSizePlusSpacing * fraction).roundToInt(),
+ 3 to itemSizePlusSpacing * 3 - (itemSizePlusSpacing * fraction).roundToInt(),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun moveItemToTheBottomOutsideOfBounds_withSpacing() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5))
+ rule.setContent {
+ LazyList(
+ maxSize = itemSizeDp * 3 + spacingDp * 2,
+ arrangement = Arrangement.spacedBy(spacingDp)
+ ) {
+ items(list, key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ assertPositions(
+ 0 to 0,
+ 1 to itemSizePlusSpacing,
+ 2 to itemSizePlusSpacing * 2
+ )
+
+ rule.runOnIdle {
+ list = listOf(0, 4, 2, 3, 1, 5)
+ }
+
+ onAnimationFrame { fraction ->
+ val item1Offset =
+ itemSizePlusSpacing + (itemSizePlusSpacing * 3 * fraction).roundToInt()
+ val item4Offset =
+ itemSizePlusSpacing * 4 - (itemSizePlusSpacing * 3 * fraction).roundToInt()
+ val screenSize = itemSize * 3 + spacing * 2
+ val expected = mutableListOf<Pair<Any, Int>>().apply {
+ add(0 to 0)
+ if (item1Offset < screenSize) {
+ add(1 to item1Offset)
+ } else {
+ rule.onNodeWithTag("1").assertIsNotDisplayed()
+ }
+ add(2 to itemSizePlusSpacing * 2)
+ if (item4Offset < screenSize) {
+ add(4 to item4Offset)
+ } else {
+ rule.onNodeWithTag("4").assertIsNotDisplayed()
+ }
+ }
+ assertPositions(
+ expected = expected.toTypedArray(),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun moveItemToTheTopOutsideOfBounds_withSpacing() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7))
+ rule.setContent {
+ LazyList(
+ maxSize = itemSizeDp * 3 + spacingDp * 2,
+ startIndex = 3,
+ arrangement = Arrangement.spacedBy(spacingDp)
+ ) {
+ items(list, key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ assertPositions(
+ 3 to 0,
+ 4 to itemSizePlusSpacing,
+ 5 to itemSizePlusSpacing * 2
+ )
+
+ rule.runOnIdle {
+ list = listOf(2, 4, 0, 3, 1, 5, 6, 7)
+ }
+
+ onAnimationFrame { fraction ->
+ val item1Offset =
+ itemSizePlusSpacing * -2 + (itemSizePlusSpacing * 3 * fraction).roundToInt()
+ val item4Offset =
+ (itemSizePlusSpacing - itemSizePlusSpacing * 3 * fraction).roundToInt()
+ val expected = mutableListOf<Pair<Any, Int>>().apply {
+ if (item4Offset > -itemSize) {
+ add(4 to item4Offset)
+ } else {
+ rule.onNodeWithTag("4").assertIsNotDisplayed()
+ }
+ add(3 to 0)
+ if (item1Offset > -itemSize) {
+ add(1 to item1Offset)
+ } else {
+ rule.onNodeWithTag("1").assertIsNotDisplayed()
+ }
+ add(5 to itemSizePlusSpacing * 2)
+ }
+ assertPositions(
+ expected = expected.toTypedArray(),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun moveItemToTheTopOutsideOfBounds_differentSizes() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5))
+ rule.setContent {
+ LazyList(maxSize = itemSize2Dp + itemSize3Dp + itemSizeDp, startIndex = 3) {
+ items(list, key = { it }) {
+ val size =
+ if (it == 3) itemSize2Dp else if (it == 1) itemSize3Dp else itemSizeDp
+ Item(it, size = size)
+ }
+ }
+ }
+
+ val item3Size = itemSize2
+ val item4Size = itemSize
+ assertPositions(
+ 3 to 0,
+ 4 to item3Size,
+ 5 to item3Size + item4Size
+ )
+
+ rule.runOnIdle {
+ // swap 4 and 1
+ list = listOf(0, 4, 2, 3, 1, 5)
+ }
+
+ onAnimationFrame { fraction ->
+ rule.onNodeWithTag("2").assertDoesNotExist()
+ // item 2 was between 1 and 3 but we don't compose it and don't know the real size,
+ // so we use an average size.
+ val item2Size = (itemSize + itemSize2 + itemSize3) / 3
+ val item1Size = itemSize3 /* the real size of the item 1 */
+ val startItem1Offset = -item1Size - item2Size
+ val item1Offset =
+ startItem1Offset + ((itemSize2 - startItem1Offset) * fraction).roundToInt()
+ val endItem4Offset = -item4Size - item2Size
+ val item4Offset = item3Size - ((item3Size - endItem4Offset) * fraction).roundToInt()
+ val expected = mutableListOf<Pair<Any, Int>>().apply {
+ if (item4Offset > -item4Size) {
+ add(4 to item4Offset)
+ } else {
+ rule.onNodeWithTag("4").assertIsNotDisplayed()
+ }
+ add(3 to 0)
+ if (item1Offset > -item1Size) {
+ add(1 to item1Offset)
+ } else {
+ rule.onNodeWithTag("1").assertIsNotDisplayed()
+ }
+ add(5 to item3Size + item4Size - ((item4Size - item1Size) * fraction).roundToInt())
+ }
+ assertPositions(
+ expected = expected.toTypedArray(),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun moveItemToTheBottomOutsideOfBounds_differentSizes() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5))
+ val listSize = itemSize2 + itemSize3 + itemSize - 1
+ val listSizeDp = with(rule.density) { listSize.toDp() }
+ rule.setContent {
+ LazyList(maxSize = listSizeDp) {
+ items(list, key = { it }) {
+ val size =
+ if (it == 0) itemSize2Dp else if (it == 4) itemSize3Dp else itemSizeDp
+ Item(it, size = size)
+ }
+ }
+ }
+
+ val item0Size = itemSize2
+ val item1Size = itemSize
+ assertPositions(
+ 0 to 0,
+ 1 to item0Size,
+ 2 to item0Size + item1Size
+ )
+
+ rule.runOnIdle {
+ list = listOf(0, 4, 2, 3, 1, 5)
+ }
+
+ onAnimationFrame { fraction ->
+ val item2Size = itemSize
+ val item4Size = itemSize3
+ // item 3 was between 2 and 4 but we don't compose it and don't know the real size,
+ // so we use an average size.
+ val item3Size = (itemSize + itemSize2 + itemSize3) / 3
+ val startItem4Offset = item0Size + item1Size + item2Size + item3Size
+ val endItem1Offset = item0Size + item4Size + item2Size + item3Size
+ val item1Offset =
+ item0Size + ((endItem1Offset - item0Size) * fraction).roundToInt()
+ val item4Offset =
+ startItem4Offset - ((startItem4Offset - item0Size) * fraction).roundToInt()
+ val expected = mutableListOf<Pair<Any, Int>>().apply {
+ add(0 to 0)
+ if (item1Offset < listSize) {
+ add(1 to item1Offset)
+ } else {
+ rule.onNodeWithTag("1").assertIsNotDisplayed()
+ }
+ add(2 to item0Size + item1Size - ((item1Size - item4Size) * fraction).roundToInt())
+ if (item4Offset < listSize) {
+ add(4 to item4Offset)
+ } else {
+ rule.onNodeWithTag("4").assertIsNotDisplayed()
+ }
+ }
+ assertPositions(
+ expected = expected.toTypedArray(),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun animateAlignmentChange() {
+ var alignment by mutableStateOf(CrossAxisAlignment.End)
+ rule.setContent {
+ LazyList(
+ crossAxisAlignment = alignment,
+ crossAxisSize = itemSizeDp
+ ) {
+ items(listOf(1, 2, 3), key = { it }) {
+ val crossAxisSize =
+ if (it == 1) itemSizeDp else if (it == 2) itemSize2Dp else itemSize3Dp
+ Item(it, crossAxisSize = crossAxisSize)
+ }
+ }
+ }
+
+ val item2Start = itemSize - itemSize2
+ val item3Start = itemSize - itemSize3
+ assertPositions(
+ 1 to 0,
+ 2 to itemSize,
+ 3 to itemSize * 2,
+ crossAxis = listOf(
+ 1 to 0,
+ 2 to item2Start,
+ 3 to item3Start,
+ )
+ )
+
+ rule.runOnIdle {
+ alignment = CrossAxisAlignment.Center
+ }
+ rule.mainClock.advanceTimeByFrame()
+
+ val item2End = itemSize / 2 - itemSize2 / 2
+ val item3End = itemSize / 2 - itemSize3 / 2
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 1 to 0,
+ 2 to itemSize,
+ 3 to itemSize * 2,
+ crossAxis = listOf(
+ 1 to 0,
+ 2 to item2Start + ((item2End - item2Start) * fraction).roundToInt(),
+ 3 to item3Start + ((item3End - item3Start) * fraction).roundToInt(),
+ ),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun animateAlignmentChange_multipleChildrenPerItem() {
+ var alignment by mutableStateOf(CrossAxisAlignment.Start)
+ rule.setContent {
+ LazyList(
+ crossAxisAlignment = alignment,
+ crossAxisSize = itemSizeDp * 2
+ ) {
+ items(1) {
+ listOf(1, 2, 3).forEach {
+ val crossAxisSize =
+ if (it == 1) itemSizeDp else if (it == 2) itemSize2Dp else itemSize3Dp
+ Item(it, crossAxisSize = crossAxisSize)
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ alignment = CrossAxisAlignment.End
+ }
+ rule.mainClock.advanceTimeByFrame()
+
+ val containerSize = itemSize * 2
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 1 to 0,
+ 2 to itemSize,
+ 3 to itemSize * 2,
+ crossAxis = listOf(
+ 1 to ((containerSize - itemSize) * fraction).roundToInt(),
+ 2 to ((containerSize - itemSize2) * fraction).roundToInt(),
+ 3 to ((containerSize - itemSize3) * fraction).roundToInt()
+ ),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun animateAlignmentChange_rtl() {
+ // this test is not applicable to LazyRow
+ assumeTrue(isVertical)
+
+ var alignment by mutableStateOf(CrossAxisAlignment.End)
+ rule.setContent {
+ CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+ LazyList(
+ crossAxisAlignment = alignment,
+ crossAxisSize = itemSizeDp
+ ) {
+ items(listOf(1, 2, 3), key = { it }) {
+ val crossAxisSize =
+ if (it == 1) itemSizeDp else if (it == 2) itemSize2Dp else itemSize3Dp
+ Item(it, crossAxisSize = crossAxisSize)
+ }
+ }
+ }
+ }
+
+ assertPositions(
+ 1 to 0,
+ 2 to itemSize,
+ 3 to itemSize * 2,
+ crossAxis = listOf(
+ 1 to 0,
+ 2 to 0,
+ 3 to 0,
+ )
+ )
+
+ rule.runOnIdle {
+ alignment = CrossAxisAlignment.Center
+ }
+ rule.mainClock.advanceTimeByFrame()
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 1 to 0,
+ 2 to itemSize,
+ 3 to itemSize * 2,
+ crossAxis = listOf(
+ 1 to 0,
+ 2 to ((itemSize / 2 - itemSize2 / 2) * fraction).roundToInt(),
+ 3 to ((itemSize / 2 - itemSize3 / 2) * fraction).roundToInt(),
+ ),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun moveItemToEndCausingNextItemsToAnimate_withContentPadding() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+ val rawStartPadding = 8
+ val rawEndPadding = 12
+ val (startPaddingDp, endPaddingDp) = with(rule.density) {
+ rawStartPadding.toDp() to rawEndPadding.toDp()
+ }
+ rule.setContent {
+ LazyList(startPadding = startPaddingDp, endPadding = endPaddingDp) {
+ items(list, key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ val startPadding = if (reverseLayout) rawEndPadding else rawStartPadding
+ assertPositions(
+ 0 to startPadding,
+ 1 to startPadding + itemSize,
+ 2 to startPadding + itemSize * 2,
+ 3 to startPadding + itemSize * 3,
+ 4 to startPadding + itemSize * 4,
+ )
+
+ rule.runOnIdle {
+ list = listOf(0, 2, 3, 4, 1)
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 0 to startPadding,
+ 1 to startPadding + itemSize + (itemSize * 3 * fraction).roundToInt(),
+ 2 to startPadding + itemSize * 2 - (itemSize * fraction).roundToInt(),
+ 3 to startPadding + itemSize * 3 - (itemSize * fraction).roundToInt(),
+ 4 to startPadding + itemSize * 4 - (itemSize * fraction).roundToInt(),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun reorderFirstAndLastItems_noNewLayoutInfoProduced() {
+ var list by mutableStateOf(listOf(0, 1, 2, 3, 4))
+
+ var measurePasses = 0
+ rule.setContent {
+ LazyList {
+ items(list, key = { it }) {
+ Item(it)
+ }
+ }
+ LaunchedEffect(Unit) {
+ snapshotFlow { state.layoutInfo }
+ .collect {
+ measurePasses++
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ list = listOf(4, 1, 2, 3, 0)
+ }
+
+ var startMeasurePasses = Int.MIN_VALUE
+ onAnimationFrame { fraction ->
+ if (fraction == 0f) {
+ startMeasurePasses = measurePasses
+ }
+ }
+ rule.mainClock.advanceTimeByFrame()
+ // new layoutInfo is produced on every remeasure of Lazy lists.
+ // but we want to avoid remeasuring and only do relayout on each animation frame.
+ // two extra measures are possible as we switch inProgress flag.
+ assertThat(measurePasses).isAtMost(startMeasurePasses + 2)
+ }
+
+ @Test
+ fun noAnimationWhenScrollOtherPosition() {
+ rule.setContent {
+ LazyList(maxSize = itemSizeDp * 3) {
+ items(listOf(0, 1, 2, 3, 4, 5, 6, 7), key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(0, itemSize / 2)
+ }
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 0 to -itemSize / 2,
+ 1 to itemSize / 2,
+ 2 to itemSize * 3 / 2,
+ 3 to itemSize * 5 / 2,
+ fraction = fraction
+ )
+ }
+ }
+
+ private fun assertPositions(
+ vararg expected: Pair<Any, Int>,
+ crossAxis: List<Pair<Any, Int>>? = null,
+ fraction: Float? = null,
+ autoReverse: Boolean = reverseLayout
+ ) {
+ with(rule.density) {
+ val actual = expected.map {
+ val actualOffset = rule.onNodeWithTag(it.first.toString())
+ .getUnclippedBoundsInRoot().let { bounds ->
+ val offset = if (isVertical) bounds.top else bounds.left
+ if (offset == Dp.Unspecified) Int.MIN_VALUE else offset.roundToPx()
+ }
+ it.first to actualOffset
+ }
+ val subject = if (fraction == null) {
+ assertThat(actual)
+ } else {
+ assertWithMessage("Fraction=$fraction").that(actual)
+ }
+ subject.isEqualTo(
+ listOf(*expected).let { list ->
+ if (!autoReverse) {
+ list
+ } else {
+ val containerBounds = rule.onNodeWithTag(ContainerTag).getBoundsInRoot()
+ val mainAxisSize =
+ if (isVertical) containerBounds.height else containerBounds.width
+ val mainAxisSizePx = with(rule.density) { mainAxisSize.roundToPx() }
+ list.map {
+ val itemSize = rule.onNodeWithTag(it.first.toString())
+ .getUnclippedBoundsInRoot().let { bounds ->
+ (if (isVertical) bounds.height else bounds.width).roundToPx()
+ }
+ it.first to (mainAxisSizePx - itemSize - it.second)
+ }
+ }
+ }
+ )
+ if (crossAxis != null) {
+ val actualCross = expected.map {
+ val actualOffset = rule.onNodeWithTag(it.first.toString())
+ .getUnclippedBoundsInRoot().let { bounds ->
+ val offset = if (isVertical) bounds.left else bounds.top
+ if (offset == Dp.Unspecified) Int.MIN_VALUE else offset.roundToPx()
+ }
+ it.first to actualOffset
+ }
+ assertWithMessage(
+ "CrossAxis" + if (fraction != null) "for fraction=$fraction" else ""
+ )
+ .that(actualCross)
+ .isEqualTo(crossAxis)
+ }
+ }
+ }
+
+ private fun assertLayoutInfoPositions(vararg offsets: Pair<Any, Int>) {
+ rule.runOnIdle {
+ assertThat(visibleItemsOffsets).isEqualTo(listOf(*offsets))
+ }
+ }
+
+ private val visibleItemsOffsets: List<Pair<Any, Int>>
+ get() = state.layoutInfo.visibleItemsInfo.map {
+ it.key to it.offset
+ }
+
+ private fun onAnimationFrame(duration: Long = Duration, onFrame: (fraction: Float) -> Unit) {
+ require(duration.mod(FrameDuration) == 0L)
+ rule.waitForIdle()
+ rule.mainClock.advanceTimeByFrame()
+ var expectedTime = rule.mainClock.currentTime
+ for (i in 0..duration step FrameDuration) {
+ onFrame(i / duration.toFloat())
+ rule.mainClock.advanceTimeBy(FrameDuration)
+ expectedTime += FrameDuration
+ assertThat(expectedTime).isEqualTo(rule.mainClock.currentTime)
+ rule.waitForIdle()
+ }
+ }
+
+ @Composable
+ private fun LazyList(
+ arrangement: Arrangement.HorizontalOrVertical? = null,
+ minSize: Dp = 0.dp,
+ maxSize: Dp = containerSizeDp,
+ startIndex: Int = 0,
+ crossAxisSize: Dp = Dp.Unspecified,
+ crossAxisAlignment: CrossAxisAlignment = CrossAxisAlignment.Start,
+ startPadding: Dp = 0.dp,
+ endPadding: Dp = 0.dp,
+ content: TvLazyListScope.() -> Unit
+ ) {
+ state = rememberLazyListState(startIndex)
+ if (isVertical) {
+ val verticalArrangement =
+ arrangement ?: if (!reverseLayout) Arrangement.Top else Arrangement.Bottom
+ val horizontalAlignment = if (crossAxisAlignment == CrossAxisAlignment.Start) {
+ Alignment.Start
+ } else if (crossAxisAlignment == CrossAxisAlignment.Center) {
+ Alignment.CenterHorizontally
+ } else {
+ Alignment.End
+ }
+ TvLazyColumn(
+ state = state,
+ modifier = Modifier
+ .requiredHeightIn(min = minSize, max = maxSize)
+ .then(
+ if (crossAxisSize != Dp.Unspecified) {
+ Modifier.requiredWidth(crossAxisSize)
+ } else {
+ Modifier.fillMaxWidth()
+ }
+ )
+ .testTag(ContainerTag),
+ verticalArrangement = verticalArrangement,
+ horizontalAlignment = horizontalAlignment,
+ reverseLayout = reverseLayout,
+ contentPadding = PaddingValues(top = startPadding, bottom = endPadding),
+ pivotOffsets = PivotOffsets(parentFraction = 0f),
+ content = content
+ )
+ } else {
+ val horizontalArrangement =
+ arrangement ?: if (!reverseLayout) Arrangement.Start else Arrangement.End
+ val verticalAlignment = if (crossAxisAlignment == CrossAxisAlignment.Start) {
+ Alignment.Top
+ } else if (crossAxisAlignment == CrossAxisAlignment.Center) {
+ Alignment.CenterVertically
+ } else {
+ Alignment.Bottom
+ }
+ TvLazyRow(
+ state = state,
+ modifier = Modifier
+ .requiredWidthIn(min = minSize, max = maxSize)
+ .then(
+ if (crossAxisSize != Dp.Unspecified) {
+ Modifier.requiredHeight(crossAxisSize)
+ } else {
+ Modifier.fillMaxHeight()
+ }
+ )
+ .testTag(ContainerTag),
+ horizontalArrangement = horizontalArrangement,
+ verticalAlignment = verticalAlignment,
+ reverseLayout = reverseLayout,
+ contentPadding = PaddingValues(start = startPadding, end = endPadding),
+ pivotOffsets = PivotOffsets(parentFraction = 0f),
+ content = content
+ )
+ }
+ }
+
+ @Composable
+ private fun LazyItemScope.Item(
+ tag: Int,
+ size: Dp = itemSizeDp,
+ crossAxisSize: Dp = size,
+ animSpec: FiniteAnimationSpec<IntOffset>? = AnimSpec
+ ) {
+ Box(
+ Modifier
+ .then(
+ if (isVertical) {
+ Modifier.requiredHeight(size).requiredWidth(crossAxisSize)
+ } else {
+ Modifier.requiredWidth(size).requiredHeight(crossAxisSize)
+ }
+ )
+ .testTag(tag.toString())
+ .then(
+ if (animSpec != null) {
+ Modifier.animateItemPlacement(animSpec)
+ } else {
+ Modifier
+ }
+ )
+ )
+ }
+
+ private fun SemanticsNodeInteraction.assertMainAxisSizeIsEqualTo(
+ expected: Dp
+ ): SemanticsNodeInteraction {
+ return if (isVertical) assertHeightIsEqualTo(expected) else assertWidthIsEqualTo(expected)
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun params() = arrayOf(
+ Config(isVertical = true, reverseLayout = false),
+ Config(isVertical = false, reverseLayout = false),
+ Config(isVertical = true, reverseLayout = true),
+ Config(isVertical = false, reverseLayout = true),
+ )
+
+ class Config(
+ val isVertical: Boolean,
+ val reverseLayout: Boolean
+ ) {
+ override fun toString() =
+ (if (isVertical) "LazyColumn" else "LazyRow") +
+ (if (reverseLayout) "(reverse)" else "")
+ }
+ }
+}
+
+private val FrameDuration = 16L
+private val Duration = 400L
+private val AnimSpec = tween<IntOffset>(Duration.toInt(), easing = LinearEasing)
+private val ContainerTag = "container"
+
+private enum class CrossAxisAlignment {
+ Start,
+ End,
+ Center
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListLayoutInfoTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListLayoutInfoTest.kt
new file mode 100644
index 0000000..5e39177
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListLayoutInfoTest.kt
@@ -0,0 +1,461 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.list
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@MediumTest
+@RunWith(Parameterized::class)
+class LazyListLayoutInfoTest(
+ param: LayoutInfoTestParam
+) : BaseLazyListTestWithOrientation(param.orientation) {
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun initParameters(): Array<Any> = arrayOf(
+ LayoutInfoTestParam(Orientation.Vertical, false),
+ LayoutInfoTestParam(Orientation.Vertical, true),
+ LayoutInfoTestParam(Orientation.Horizontal, false),
+ LayoutInfoTestParam(Orientation.Horizontal, true),
+ )
+ }
+
+ private val reverseLayout = param.reverseLayout
+
+ private var itemSizePx: Int = 50
+ private var itemSizeDp: Dp = Dp.Infinity
+
+ @Before
+ fun before() {
+ with(rule.density) {
+ itemSizeDp = itemSizePx.toDp()
+ }
+ }
+
+ @Test
+ fun visibleItemsAreCorrect() {
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ LazyColumnOrRow(
+ state = rememberLazyListState().also { state = it },
+ reverseLayout = reverseLayout,
+ modifier = Modifier.requiredSize(itemSizeDp * 3.5f)
+ ) {
+ items((0..5).toList()) {
+ Box(Modifier.requiredSize(itemSizeDp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ state.layoutInfo.assertVisibleItems(count = 4)
+ }
+ }
+
+ @Test
+ fun visibleItemsAreCorrectAfterScroll() {
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ LazyColumnOrRow(
+ state = rememberLazyListState().also { state = it },
+ reverseLayout = reverseLayout,
+ modifier = Modifier.requiredSize(itemSizeDp * 3.5f)
+ ) {
+ items((0..5).toList()) {
+ Box(Modifier.requiredSize(itemSizeDp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(1, 10)
+ }
+ state.layoutInfo.assertVisibleItems(count = 4, startIndex = 1, startOffset = -10)
+ }
+ }
+
+ @Test
+ fun visibleItemsAreCorrectWithSpacing() {
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ LazyColumnOrRow(
+ state = rememberLazyListState().also { state = it },
+ reverseLayout = reverseLayout,
+ spacedBy = itemSizeDp,
+ modifier = Modifier.requiredSize(itemSizeDp * 3.5f)
+ ) {
+ items((0..5).toList()) {
+ Box(Modifier.requiredSize(itemSizeDp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ state.layoutInfo.assertVisibleItems(count = 2, spacing = itemSizePx)
+ }
+ }
+
+ @Composable
+ fun ObservingFun(state: TvLazyListState, currentInfo: StableRef<LazyListLayoutInfo?>) {
+ currentInfo.value = state.layoutInfo
+ }
+ @Test
+ fun visibleItemsAreObservableWhenWeScroll() {
+ lateinit var state: TvLazyListState
+ val currentInfo = StableRef<LazyListLayoutInfo?>(null)
+ rule.setContent {
+ LazyColumnOrRow(
+ state = rememberLazyListState().also { state = it },
+ reverseLayout = reverseLayout,
+ modifier = Modifier.requiredSize(itemSizeDp * 3.5f)
+ ) {
+ items((0..5).toList()) {
+ Box(Modifier.requiredSize(itemSizeDp))
+ }
+ }
+ ObservingFun(state, currentInfo)
+ }
+
+ rule.runOnIdle {
+ // empty it here and scrolling should invoke observingFun again
+ currentInfo.value = null
+ runBlocking {
+ state.scrollToItem(1, 0)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(currentInfo.value).isNotNull()
+ currentInfo.value!!.assertVisibleItems(count = 4, startIndex = 1)
+ }
+ }
+
+ @Test
+ fun visibleItemsAreObservableWhenResize() {
+ lateinit var state: TvLazyListState
+ var size by mutableStateOf(itemSizeDp * 2)
+ var currentInfo: LazyListLayoutInfo? = null
+ @Composable
+ fun observingFun() {
+ currentInfo = state.layoutInfo
+ }
+ rule.setContent {
+ LazyColumnOrRow(
+ reverseLayout = reverseLayout,
+ state = rememberLazyListState().also { state = it }
+ ) {
+ item {
+ Box(Modifier.requiredSize(size))
+ }
+ }
+ observingFun()
+ }
+
+ rule.runOnIdle {
+ assertThat(currentInfo).isNotNull()
+ currentInfo!!.assertVisibleItems(count = 1, expectedSize = itemSizePx * 2)
+ currentInfo = null
+ size = itemSizeDp
+ }
+
+ rule.runOnIdle {
+ assertThat(currentInfo).isNotNull()
+ currentInfo!!.assertVisibleItems(count = 1, expectedSize = itemSizePx)
+ }
+ }
+
+ @Test
+ fun totalCountIsCorrect() {
+ var count by mutableStateOf(10)
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ LazyColumnOrRow(
+ reverseLayout = reverseLayout,
+ state = rememberLazyListState().also { state = it }
+ ) {
+ items((0 until count).toList()) {
+ Box(Modifier.requiredSize(10.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.totalItemsCount).isEqualTo(10)
+ count = 20
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.totalItemsCount).isEqualTo(20)
+ }
+ }
+
+ @Test
+ fun viewportOffsetsAndSizeAreCorrect() {
+ val sizePx = 45
+ val sizeDp = with(rule.density) { sizePx.toDp() }
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ LazyColumnOrRow(
+ Modifier.mainAxisSize(sizeDp).crossAxisSize(sizeDp * 2),
+ reverseLayout = reverseLayout,
+ state = rememberLazyListState().also { state = it }
+ ) {
+ items((0..3).toList()) {
+ Box(Modifier.requiredSize(sizeDp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(0)
+ assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx)
+ assertThat(state.layoutInfo.viewportSize).isEqualTo(
+ if (vertical) IntSize(sizePx * 2, sizePx) else IntSize(sizePx, sizePx * 2)
+ )
+ }
+ }
+
+ @Test
+ fun viewportOffsetsAndSizeAreCorrectWithContentPadding() {
+ val sizePx = 45
+ val startPaddingPx = 10
+ val endPaddingPx = 15
+ val sizeDp = with(rule.density) { sizePx.toDp() }
+ val beforeContentPaddingDp = with(rule.density) {
+ if (!reverseLayout) startPaddingPx.toDp() else endPaddingPx.toDp()
+ }
+ val afterContentPaddingDp = with(rule.density) {
+ if (!reverseLayout) endPaddingPx.toDp() else startPaddingPx.toDp()
+ }
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ LazyColumnOrRow(
+ Modifier.mainAxisSize(sizeDp).crossAxisSize(sizeDp * 2),
+ contentPadding = PaddingValues(
+ beforeContent = beforeContentPaddingDp,
+ afterContent = afterContentPaddingDp,
+ beforeContentCrossAxis = 2.dp,
+ afterContentCrossAxis = 2.dp
+ ),
+ reverseLayout = reverseLayout,
+ state = rememberLazyListState().also { state = it }
+ ) {
+ items((0..3).toList()) {
+ Box(Modifier.requiredSize(sizeDp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-startPaddingPx)
+ assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - startPaddingPx)
+ assertThat(state.layoutInfo.afterContentPadding).isEqualTo(endPaddingPx)
+ assertThat(state.layoutInfo.viewportSize).isEqualTo(
+ if (vertical) IntSize(sizePx * 2, sizePx) else IntSize(sizePx, sizePx * 2)
+ )
+ }
+ }
+
+ @Test
+ fun emptyItemsInVisibleItemsInfo() {
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ LazyColumnOrRow(
+ state = rememberLazyListState().also { state = it }
+ ) {
+ item { Box(Modifier) }
+ item { }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.visibleItemsInfo.size).isEqualTo(2)
+ assertThat(state.layoutInfo.visibleItemsInfo.first().index).isEqualTo(0)
+ assertThat(state.layoutInfo.visibleItemsInfo.last().index).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun emptyContent() {
+ lateinit var state: TvLazyListState
+ val sizePx = 45
+ val startPaddingPx = 10
+ val endPaddingPx = 15
+ val sizeDp = with(rule.density) { sizePx.toDp() }
+ val beforeContentPaddingDp = with(rule.density) {
+ if (!reverseLayout) startPaddingPx.toDp() else endPaddingPx.toDp()
+ }
+ val afterContentPaddingDp = with(rule.density) {
+ if (!reverseLayout) endPaddingPx.toDp() else startPaddingPx.toDp()
+ }
+ rule.setContent {
+ LazyColumnOrRow(
+ Modifier.mainAxisSize(sizeDp).crossAxisSize(sizeDp * 2),
+ state = rememberLazyListState().also { state = it },
+ reverseLayout = reverseLayout,
+ contentPadding = PaddingValues(
+ beforeContent = beforeContentPaddingDp,
+ afterContent = afterContentPaddingDp
+ )
+ ) {
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-startPaddingPx)
+ assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - startPaddingPx)
+ assertThat(state.layoutInfo.beforeContentPadding).isEqualTo(startPaddingPx)
+ assertThat(state.layoutInfo.afterContentPadding).isEqualTo(endPaddingPx)
+ assertThat(state.layoutInfo.viewportSize).isEqualTo(
+ if (vertical) IntSize(sizePx * 2, sizePx) else IntSize(sizePx, sizePx * 2)
+ )
+ }
+ }
+
+ @Test
+ fun viewportIsLargerThenTheContent() {
+ lateinit var state: TvLazyListState
+ val sizePx = 45
+ val startPaddingPx = 10
+ val endPaddingPx = 15
+ val sizeDp = with(rule.density) { sizePx.toDp() }
+ val beforeContentPaddingDp = with(rule.density) {
+ if (!reverseLayout) startPaddingPx.toDp() else endPaddingPx.toDp()
+ }
+ val afterContentPaddingDp = with(rule.density) {
+ if (!reverseLayout) endPaddingPx.toDp() else startPaddingPx.toDp()
+ }
+ rule.setContent {
+ LazyColumnOrRow(
+ Modifier.mainAxisSize(sizeDp).crossAxisSize(sizeDp * 2),
+ state = rememberLazyListState().also { state = it },
+ reverseLayout = reverseLayout,
+ contentPadding = PaddingValues(
+ beforeContent = beforeContentPaddingDp,
+ afterContent = afterContentPaddingDp
+ )
+ ) {
+ item {
+ Box(Modifier.size(sizeDp / 2))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-startPaddingPx)
+ assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - startPaddingPx)
+ assertThat(state.layoutInfo.beforeContentPadding).isEqualTo(startPaddingPx)
+ assertThat(state.layoutInfo.afterContentPadding).isEqualTo(endPaddingPx)
+ assertThat(state.layoutInfo.viewportSize).isEqualTo(
+ if (vertical) IntSize(sizePx * 2, sizePx) else IntSize(sizePx, sizePx * 2)
+ )
+ }
+ }
+
+ @Test
+ fun reverseLayoutIsCorrect() {
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ LazyColumnOrRow(
+ state = rememberLazyListState().also { state = it },
+ reverseLayout = reverseLayout,
+ modifier = Modifier.requiredSize(itemSizeDp * 3.5f)
+ ) {
+ items((0..5).toList()) {
+ Box(Modifier.requiredSize(itemSizeDp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.reverseLayout).isEqualTo(reverseLayout)
+ }
+ }
+
+ @Test
+ fun orientationIsCorrect() {
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ LazyColumnOrRow(
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.requiredSize(itemSizeDp * 3.5f)
+ ) {
+ items((0..5).toList()) {
+ Box(Modifier.requiredSize(itemSizeDp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.orientation)
+ .isEqualTo(if (vertical) Orientation.Vertical else Orientation.Horizontal)
+ }
+ }
+
+ fun LazyListLayoutInfo.assertVisibleItems(
+ count: Int,
+ startIndex: Int = 0,
+ startOffset: Int = 0,
+ expectedSize: Int = itemSizePx,
+ spacing: Int = 0
+ ) {
+ assertThat(visibleItemsInfo.size).isEqualTo(count)
+ var currentIndex = startIndex
+ var currentOffset = startOffset
+ visibleItemsInfo.forEach {
+ assertThat(it.index).isEqualTo(currentIndex)
+ assertWithMessage("Offset of item $currentIndex").that(it.offset)
+ .isEqualTo(currentOffset)
+ assertThat(it.size).isEqualTo(expectedSize)
+ currentIndex++
+ currentOffset += it.size + spacing
+ }
+ }
+}
+
+class LayoutInfoTestParam(
+ val orientation: Orientation,
+ val reverseLayout: Boolean
+) {
+ override fun toString(): String {
+ return "orientation=$orientation;reverseLayout=$reverseLayout"
+ }
+}
+
+@Stable
+class StableRef<T>(var value: T)
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListPrefetcherTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListPrefetcherTest.kt
new file mode 100644
index 0000000..db1d248
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListPrefetcherTest.kt
@@ -0,0 +1,380 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.list
+
+import androidx.tv.foundation.lazy.AutoTestFrameClock
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Remeasurement
+import androidx.compose.ui.layout.RemeasurementModifier
+import androidx.compose.ui.layout.SubcomposeLayout
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.LargeTest
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class LazyListPrefetcherTest(
+ orientation: Orientation
+) : BaseLazyListTestWithOrientation(orientation) {
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun initParameters(): Array<Any> = arrayOf(
+ Orientation.Vertical,
+ Orientation.Horizontal,
+ )
+ }
+
+ val itemsSizePx = 30
+ val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
+
+ lateinit var state: TvLazyListState
+
+ @Test
+ fun notPrefetchingForwardInitially() {
+ composeList()
+
+ rule.onNodeWithTag("2")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun notPrefetchingBackwardInitially() {
+ composeList(firstItem = 2)
+
+ rule.onNodeWithTag("0")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun prefetchingForwardAfterSmallScroll() {
+ composeList()
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(5f)
+ }
+ }
+
+ waitForPrefetch(2)
+
+ rule.onNodeWithTag("2")
+ .assertExists()
+ rule.onNodeWithTag("3")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun prefetchingBackwardAfterSmallScroll() {
+ composeList(firstItem = 2, itemOffset = 10)
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(-5f)
+ }
+ }
+
+ waitForPrefetch(1)
+
+ rule.onNodeWithTag("1")
+ .assertExists()
+ rule.onNodeWithTag("0")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun prefetchingForwardAndBackward() {
+ composeList(firstItem = 1)
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(5f)
+ }
+ }
+
+ waitForPrefetch(3)
+
+ rule.onNodeWithTag("3")
+ .assertExists()
+ rule.onNodeWithTag("0")
+ .assertDoesNotExist()
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(-2f)
+ state.scrollBy(-1f)
+ }
+ }
+
+ waitForPrefetch(0)
+
+ rule.onNodeWithTag("0")
+ .assertExists()
+ rule.onNodeWithTag("3")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun prefetchingForwardTwice() {
+ composeList()
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(5f)
+ }
+ }
+
+ waitForPrefetch(2)
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(itemsSizePx / 2f)
+ state.scrollBy(itemsSizePx / 2f)
+ }
+ }
+
+ waitForPrefetch(3)
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("3")
+ .assertExists()
+ rule.onNodeWithTag("4")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun prefetchingBackwardTwice() {
+ composeList(firstItem = 4)
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(-5f)
+ }
+ }
+
+ waitForPrefetch(2)
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(-itemsSizePx / 2f)
+ state.scrollBy(-itemsSizePx / 2f)
+ }
+ }
+
+ waitForPrefetch(1)
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("1")
+ .assertExists()
+ rule.onNodeWithTag("0")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun prefetchingForwardAndBackwardReverseLayout() {
+ composeList(firstItem = 1, reverseLayout = true)
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(5f)
+ }
+ }
+
+ waitForPrefetch(3)
+
+ rule.onNodeWithTag("3")
+ .assertExists()
+ rule.onNodeWithTag("0")
+ .assertDoesNotExist()
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(-2f)
+ state.scrollBy(-1f)
+ }
+ }
+
+ waitForPrefetch(0)
+
+ rule.onNodeWithTag("0")
+ .assertExists()
+ rule.onNodeWithTag("3")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun prefetchingForwardAndBackwardWithContentPadding() {
+ val halfItemSize = itemsSizeDp / 2f
+ composeList(
+ firstItem = 2,
+ itemOffset = 5,
+ contentPadding = PaddingValues(mainAxis = halfItemSize)
+ )
+
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("3")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("0")
+ .assertDoesNotExist()
+ rule.onNodeWithTag("4")
+ .assertDoesNotExist()
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(5f)
+ }
+ }
+
+ waitForPrefetch(3)
+
+ rule.onNodeWithTag("4")
+ .assertExists()
+ rule.onNodeWithTag("0")
+ .assertDoesNotExist()
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(-2f)
+ }
+ }
+
+ waitForPrefetch(0)
+
+ rule.onNodeWithTag("0")
+ .assertExists()
+ }
+
+ @Test
+ fun disposingWhilePrefetchingScheduled() {
+ var emit = true
+ lateinit var remeasure: Remeasurement
+ rule.setContent {
+ SubcomposeLayout(
+ modifier = object : RemeasurementModifier {
+ override fun onRemeasurementAvailable(remeasurement: Remeasurement) {
+ remeasure = remeasurement
+ }
+ }
+ ) { constraints ->
+ val placeable = if (emit) {
+ subcompose(Unit) {
+ state = rememberLazyListState()
+ LazyColumnOrRow(
+ Modifier.mainAxisSize(itemsSizeDp * 1.5f),
+ state,
+ ) {
+ items(1000) {
+ Spacer(
+ Modifier
+ .mainAxisSize(itemsSizeDp)
+ .then(fillParentMaxCrossAxis())
+ )
+ }
+ }
+ }.first().measure(constraints)
+ } else {
+ null
+ }
+ layout(constraints.maxWidth, constraints.maxHeight) {
+ placeable?.place(0, 0)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ // this will schedule the prefetching
+ runBlocking(AutoTestFrameClock()) {
+ state.scrollBy(itemsSizePx.toFloat())
+ }
+ // then we synchronously dispose LazyColumn
+ emit = false
+ remeasure.forceRemeasure()
+ }
+
+ rule.runOnIdle { }
+ }
+
+ private fun waitForPrefetch(index: Int) {
+ rule.waitUntil {
+ activeNodes.contains(index) && activeMeasuredNodes.contains(index)
+ }
+ }
+
+ private val activeNodes = mutableSetOf<Int>()
+ private val activeMeasuredNodes = mutableSetOf<Int>()
+
+ private fun composeList(
+ firstItem: Int = 0,
+ itemOffset: Int = 0,
+ reverseLayout: Boolean = false,
+ contentPadding: PaddingValues = PaddingValues(0.dp)
+ ) {
+ rule.setContent {
+ state = rememberLazyListState(
+ initialFirstVisibleItemIndex = firstItem,
+ initialFirstVisibleItemScrollOffset = itemOffset
+ )
+ LazyColumnOrRow(
+ Modifier.mainAxisSize(itemsSizeDp * 1.5f),
+ state,
+ reverseLayout = reverseLayout,
+ contentPadding = contentPadding
+ ) {
+ items(100) {
+ DisposableEffect(it) {
+ activeNodes.add(it)
+ onDispose {
+ activeNodes.remove(it)
+ activeMeasuredNodes.remove(it)
+ }
+ }
+ Spacer(
+ Modifier
+ .mainAxisSize(itemsSizeDp)
+ .fillMaxCrossAxis()
+ .testTag("$it")
+ .layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ activeMeasuredNodes.add(it)
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListSlotsReuseTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListSlotsReuseTest.kt
new file mode 100644
index 0000000..7e0f810
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListSlotsReuseTest.kt
@@ -0,0 +1,506 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.list
+
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.tv.foundation.PivotOffsets
+import com.google.common.truth.Truth
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class LazyListSlotsReuseTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ val itemsSizePx = 30f
+ val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
+
+ @Test
+ fun scroll1ItemScrolledOffItemIsKeptForReuse() {
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ TvLazyColumn(
+ Modifier.height(itemsSizeDp * 1.5f),
+ state,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(100) {
+ Box(
+ Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it")
+ .focusable())
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertIsDisplayed()
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(1)
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun scroll2ItemsScrolledOffItemsAreKeptForReuse() {
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ TvLazyColumn(
+ Modifier.height(itemsSizeDp * 1.5f),
+ state,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(100) {
+ Box(
+ Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it")
+ .focusable())
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(2)
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule.onNodeWithTag("1")
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun checkMaxItemsKeptForReuse() {
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ TvLazyColumn(
+ Modifier.height(itemsSizeDp * (DefaultMaxItemsToRetain + 0.5f)),
+ state,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(100) {
+ Box(
+ Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it")
+ .focusable())
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(DefaultMaxItemsToRetain + 1)
+ }
+ }
+
+ repeat(DefaultMaxItemsToRetain) {
+ rule.onNodeWithTag("$it")
+ .assertExists()
+ .assertIsNotDisplayed()
+ }
+ rule.onNodeWithTag("$DefaultMaxItemsToRetain")
+ .assertDoesNotExist()
+ rule.onNodeWithTag("${DefaultMaxItemsToRetain + 1}")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun scroll3Items2OfScrolledOffItemsAreKeptForReuse() {
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ TvLazyColumn(
+ Modifier.height(itemsSizeDp * 1.5f),
+ state,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(100) {
+ Box(
+ Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it")
+ .focusable())
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+
+ rule.runOnIdle {
+ runBlocking {
+ // after this step 0 and 1 are in reusable buffer
+ state.scrollToItem(2)
+
+ // this step requires one item and will take the last item from the buffer - item
+ // 1 plus will put 2 in the buffer. so expected buffer is items 2 and 0
+ state.scrollToItem(3)
+ }
+ }
+
+ // recycled
+ rule.onNodeWithTag("1")
+ .assertDoesNotExist()
+
+ // in buffer
+ rule.onNodeWithTag("0")
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule.onNodeWithTag("2")
+ .assertExists()
+ .assertIsNotDisplayed()
+
+ // visible
+ rule.onNodeWithTag("3")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("4")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun doMultipleScrollsOneByOne() {
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ TvLazyColumn(
+ Modifier.height(itemsSizeDp * 1.5f),
+ state,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(100) {
+ Box(
+ Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it")
+ .focusable())
+ }
+ }
+ }
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(1) // buffer is [0]
+ state.scrollToItem(2) // 0 used, buffer is [1]
+ state.scrollToItem(3) // 1 used, buffer is [2]
+ state.scrollToItem(4) // 2 used, buffer is [3]
+ }
+ }
+
+ // recycled
+ rule.onNodeWithTag("0")
+ .assertDoesNotExist()
+ rule.onNodeWithTag("1")
+ .assertDoesNotExist()
+ rule.onNodeWithTag("2")
+ .assertDoesNotExist()
+
+ // in buffer
+ rule.onNodeWithTag("3")
+ .assertExists()
+ .assertIsNotDisplayed()
+
+ // visible
+ rule.onNodeWithTag("4")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("5")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun scrollBackwardOnce() {
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ state = rememberLazyListState(10)
+ TvLazyColumn(
+ Modifier.height(itemsSizeDp * 1.5f),
+ state,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(100) {
+ Box(
+ Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it")
+ .focusable())
+ }
+ }
+ }
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(8) // buffer is [10, 11]
+ }
+ }
+
+ // in buffer
+ rule.onNodeWithTag("10")
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule.onNodeWithTag("11")
+ .assertExists()
+ .assertIsNotDisplayed()
+
+ // visible
+ rule.onNodeWithTag("8")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("9")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun scrollBackwardOneByOne() {
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ state = rememberLazyListState(10)
+ TvLazyColumn(
+ Modifier.height(itemsSizeDp * 1.5f),
+ state,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(100) {
+ Box(
+ Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it")
+ .focusable())
+ }
+ }
+ }
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(9) // buffer is [11]
+ state.scrollToItem(7) // 11 reused, buffer is [9]
+ state.scrollToItem(6) // 9 reused, buffer is [8]
+ }
+ }
+
+ // in buffer
+ rule.onNodeWithTag("8")
+ .assertExists()
+ .assertIsNotDisplayed()
+
+ // visible
+ rule.onNodeWithTag("6")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("7")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun scrollingBackReusesTheSameSlot() {
+ lateinit var state: TvLazyListState
+ var counter0 = 0
+ var counter1 = 10
+ var rememberedValue0 = -1
+ var rememberedValue1 = -1
+ rule.setContent {
+ state = rememberLazyListState()
+ TvLazyColumn(
+ Modifier.height(itemsSizeDp * 1.5f),
+ state,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(100) {
+ if (it == 0) {
+ rememberedValue0 = remember { counter0++ }
+ }
+ if (it == 1) {
+ rememberedValue1 = remember { counter1++ }
+ }
+ Box(
+ Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it")
+ .focusable())
+ }
+ }
+ }
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(2) // buffer is [0, 1]
+ state.scrollToItem(0) // scrolled back, 0 and 1 are reused back. buffer: [2, 3]
+ }
+ }
+
+ rule.runOnIdle {
+ Truth.assertWithMessage("Item 0 restored remembered value is $rememberedValue0")
+ .that(rememberedValue0).isEqualTo(0)
+ Truth.assertWithMessage("Item 1 restored remembered value is $rememberedValue1")
+ .that(rememberedValue1).isEqualTo(10)
+ }
+
+ rule.onNodeWithTag("0")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("2")
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule.onNodeWithTag("3")
+ .assertExists()
+ .assertIsNotDisplayed()
+ }
+
+ @Test
+ fun differentContentTypes() {
+ lateinit var state: TvLazyListState
+ val visibleItemsCount = (DefaultMaxItemsToRetain + 1) * 2
+ val startOfType1 = DefaultMaxItemsToRetain + 1
+ rule.setContent {
+ state = rememberLazyListState()
+ TvLazyColumn(
+ Modifier.height(itemsSizeDp * (visibleItemsCount - 0.5f)),
+ state,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(
+ 100,
+ contentType = { if (it >= startOfType1) 1 else 0 }
+ ) {
+ Box(
+ Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it").focusable())
+ }
+ }
+ }
+
+ for (i in 0 until visibleItemsCount) {
+ rule.onNodeWithTag("$i")
+ .assertIsDisplayed()
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(visibleItemsCount)
+ }
+ }
+
+ rule.onNodeWithTag("$visibleItemsCount")
+ .assertIsDisplayed()
+
+ // [DefaultMaxItemsToRetain] items of type 0 are left for reuse
+ for (i in 0 until DefaultMaxItemsToRetain) {
+ rule.onNodeWithTag("$i")
+ .assertExists()
+ .assertIsNotDisplayed()
+ }
+ rule.onNodeWithTag("$DefaultMaxItemsToRetain")
+ .assertDoesNotExist()
+
+ // and 7 items of type 1
+ for (i in startOfType1 until startOfType1 + DefaultMaxItemsToRetain) {
+ rule.onNodeWithTag("$i")
+ .assertExists()
+ .assertIsNotDisplayed()
+ }
+ rule.onNodeWithTag("${startOfType1 + DefaultMaxItemsToRetain}")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun differentTypesFromDifferentItemCalls() {
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ TvLazyColumn(
+ Modifier.height(itemsSizeDp * 2.5f),
+ state,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ val content = @Composable { tag: String ->
+ Spacer(Modifier.height(itemsSizeDp).width(10.dp).testTag(tag).focusable())
+ }
+ item(contentType = "not-to-reuse-0") {
+ content("0")
+ }
+ item(contentType = "reuse") {
+ content("1")
+ }
+ items(
+ List(100) { it + 2 },
+ contentType = { if (it == 10) "reuse" else "not-to-reuse-$it" }) {
+ content("$it")
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(2)
+ // now items 0 and 1 are put into reusables
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule.onNodeWithTag("1")
+ .assertExists()
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(9)
+ // item 10 should reuse slot 1
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertExists()
+ .assertIsNotDisplayed()
+ rule.onNodeWithTag("1")
+ .assertDoesNotExist()
+ rule.onNodeWithTag("9")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("10")
+ .assertIsDisplayed()
+ rule.onNodeWithTag("11")
+ .assertIsDisplayed()
+ }
+}
+
+private val DefaultMaxItemsToRetain = 7
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListTest.kt
new file mode 100644
index 0000000..6b61bf4
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListTest.kt
@@ -0,0 +1,1733 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.list
+
+import android.os.Build
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.requiredSizeIn
+import androidx.compose.foundation.layout.requiredWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.WithTouchSlop
+import androidx.compose.testutils.assertPixels
+import androidx.compose.testutils.assertShape
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher.Companion.keyIsDefined
+import androidx.compose.ui.test.SemanticsMatcher.Companion.keyNotDefined
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.StateRestorationTester
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.zIndex
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.tv.foundation.PivotOffsets
+import androidx.tv.foundation.lazy.AutoTestFrameClock
+import androidx.tv.foundation.lazy.grid.keyPress
+import com.google.common.collect.Range
+import com.google.common.truth.IntegerSubject
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import java.util.concurrent.CountDownLatch
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class LazyListTest(orientation: Orientation) : BaseLazyListTestWithOrientation(orientation) {
+ private val LazyListTag = "LazyListTag"
+ private val firstItemTag = "firstItemTag"
+
+ @Test
+ fun lazyListShowsCombinedItems() {
+ val itemTestTag = "itemTestTag"
+ val items = listOf(1, 2).map { it.toString() }
+ val indexedItems = listOf(3, 4, 5)
+
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(Modifier.mainAxisSize(200.dp)) {
+ item {
+ Spacer(
+ Modifier.mainAxisSize(40.dp)
+ .then(fillParentMaxCrossAxis())
+ .testTag(itemTestTag)
+ )
+ }
+ items(items) {
+ Spacer(Modifier.mainAxisSize(40.dp).then(fillParentMaxCrossAxis()).testTag(it))
+ }
+ itemsIndexed(indexedItems) { index, item ->
+ Spacer(
+ Modifier.mainAxisSize(41.dp).then(fillParentMaxCrossAxis())
+ .testTag("$index-$item")
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag(itemTestTag)
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("0-3")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("1-4")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("2-5")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun lazyListAllowEmptyListItems() {
+ val itemTag = "itemTag"
+
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow {
+ items(emptyList<Any>()) { }
+ item {
+ Spacer(Modifier.size(10.dp).testTag(itemTag))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(itemTag)
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun lazyListAllowsNullableItems() {
+ val items = listOf("1", null, "3")
+ val nullTestTag = "nullTestTag"
+
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(Modifier.mainAxisSize(200.dp)) {
+ items(items) {
+ if (it != null) {
+ Spacer(
+ Modifier.mainAxisSize(101.dp)
+ .then(fillParentMaxCrossAxis())
+ .testTag(it)
+ )
+ } else {
+ Spacer(
+ Modifier.mainAxisSize(101.dp).then(fillParentMaxCrossAxis())
+ .testTag(nullTestTag)
+ )
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag(nullTestTag)
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("3")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun lazyListOnlyVisibleItemsAdded() {
+ val items = (1..4).map { it.toString() }
+
+ rule.setContentWithTestViewConfiguration {
+ Box(Modifier.mainAxisSize(200.dp)) {
+ LazyColumnOrRow(pivotOffsets = PivotOffsets(parentFraction = 0.4f)) {
+ items(items) {
+ Spacer(
+ Modifier.mainAxisSize(101.dp).then(fillParentMaxCrossAxis()).testTag(it)
+ )
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("3")
+ .assertDoesNotExist()
+
+ rule.onNodeWithTag("4")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun lazyListScrollToShowItems123() {
+ val items = (1..4).map { it.toString() }
+ rule.setContentWithTestViewConfiguration {
+ Box(Modifier.mainAxisSize(200.dp)) {
+ LazyColumnOrRow(
+ modifier = Modifier.testTag(LazyListTag),
+ pivotOffsets = PivotOffsets(parentFraction = 0.3f)
+ ) {
+ items(items) {
+ Box(
+ Modifier.mainAxisSize(101.dp).then(fillParentMaxCrossAxis())
+ .testTag(it).focusable().border(3.dp, Color.Red)
+ ) {
+ BasicText(it)
+ }
+ }
+ }
+ }
+ }
+
+ rule.keyPress(3)
+
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("3")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("4")
+ .assertIsNotDisplayed()
+ }
+
+ @Test
+ fun lazyListScrollToHideFirstItem() {
+ val items = (1..4).map { it.toString() }
+ rule.setContentWithTestViewConfiguration {
+ Box(Modifier.mainAxisSize(200.dp)) {
+ LazyColumnOrRow(modifier = Modifier.testTag(LazyListTag)) {
+ items(items) {
+ Box(
+ Modifier.mainAxisSize(101.dp).then(fillParentMaxCrossAxis())
+ .testTag(it).focusable()
+ )
+ }
+ }
+ }
+ }
+
+ rule.keyPress(3)
+
+ rule.onNodeWithTag("1")
+ .assertIsNotDisplayed()
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("3")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun lazyListScrollToShowItems234() {
+ val items = (1..4).map { it.toString() }
+
+ rule.setContentWithTestViewConfiguration {
+ Box(Modifier.mainAxisSize(200.dp)) {
+ LazyColumnOrRow(
+ modifier = Modifier.testTag(LazyListTag),
+ pivotOffsets = PivotOffsets(parentFraction = 0.3f)
+ ) {
+ items(items) {
+ Box(
+ Modifier.mainAxisSize(101.dp).then(fillParentMaxCrossAxis())
+ .testTag(it).focusable()
+ )
+ }
+ }
+ }
+ }
+
+ rule.keyPress(4)
+
+ rule.onNodeWithTag("1")
+ .assertIsNotDisplayed()
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("3")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("4")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun lazyListWrapsContent() = with(rule.density) {
+ val itemInsideLazyList = "itemInsideLazyList"
+ val itemOutsideLazyList = "itemOutsideLazyList"
+ var sameSizeItems by mutableStateOf(true)
+
+ rule.setContentWithTestViewConfiguration {
+ Column {
+ LazyColumnOrRow(Modifier.testTag(LazyListTag)) {
+ items(listOf(1, 2)) {
+ if (it == 1) {
+ Spacer(Modifier.size(50.dp).testTag(itemInsideLazyList))
+ } else {
+ Spacer(Modifier.size(if (sameSizeItems) 50.dp else 70.dp))
+ }
+ }
+ }
+ Spacer(Modifier.size(50.dp).testTag(itemOutsideLazyList))
+ }
+ }
+
+ rule.onNodeWithTag(itemInsideLazyList)
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag(itemOutsideLazyList)
+ .assertIsDisplayed()
+
+ var lazyListBounds = rule.onNodeWithTag(LazyListTag).getUnclippedBoundsInRoot()
+ var mainAxisEndBound = if (vertical) lazyListBounds.bottom else lazyListBounds.right
+ var crossAxisEndBound = if (vertical) lazyListBounds.right else lazyListBounds.bottom
+
+ assertThat(lazyListBounds.left.roundToPx()).isWithin1PixelFrom(0.dp.roundToPx())
+ assertThat(mainAxisEndBound.roundToPx()).isWithin1PixelFrom(100.dp.roundToPx())
+ assertThat(lazyListBounds.top.roundToPx()).isWithin1PixelFrom(0.dp.roundToPx())
+ assertThat(crossAxisEndBound.roundToPx()).isWithin1PixelFrom(50.dp.roundToPx())
+
+ rule.runOnIdle {
+ sameSizeItems = false
+ }
+
+ rule.waitForIdle()
+
+ rule.onNodeWithTag(itemInsideLazyList)
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag(itemOutsideLazyList)
+ .assertIsDisplayed()
+
+ lazyListBounds = rule.onNodeWithTag(LazyListTag).getUnclippedBoundsInRoot()
+ mainAxisEndBound = if (vertical) lazyListBounds.bottom else lazyListBounds.right
+ crossAxisEndBound = if (vertical) lazyListBounds.right else lazyListBounds.bottom
+
+ assertThat(lazyListBounds.left.roundToPx()).isWithin1PixelFrom(0.dp.roundToPx())
+ assertThat(mainAxisEndBound.roundToPx()).isWithin1PixelFrom(120.dp.roundToPx())
+ assertThat(lazyListBounds.top.roundToPx()).isWithin1PixelFrom(0.dp.roundToPx())
+ assertThat(crossAxisEndBound.roundToPx()).isWithin1PixelFrom(70.dp.roundToPx())
+ }
+
+ @Test
+ fun compositionsAreDisposed_whenNodesAreScrolledOff() {
+ var composed: Boolean
+ var disposed = false
+ // Ten 31dp spacers in a 300dp list
+ val latch = CountDownLatch(10)
+
+ rule.setContentWithTestViewConfiguration {
+ // Fixed size to eliminate device size as a factor
+ Box(Modifier.testTag(LazyListTag).mainAxisSize(300.dp)) {
+ LazyColumnOrRow(Modifier.fillMaxSize()) {
+ items(50) {
+ DisposableEffect(NeverEqualObject) {
+ composed = true
+ // Signal when everything is done composing
+ latch.countDown()
+ onDispose {
+ disposed = true
+ }
+ }
+
+ // There will be 10 of these in the 300dp box
+ Box(Modifier.mainAxisSize(31.dp).focusable()) {
+ BasicText(it.toString())
+ }
+ }
+ }
+ }
+ }
+
+ latch.await()
+ composed = false
+
+ assertWithMessage("Compositions were disposed before we did any scrolling")
+ .that(disposed).isFalse()
+
+ // Mostly a validity check, this is not part of the behavior under test
+ assertWithMessage("Additional composition occurred for no apparent reason")
+ .that(composed).isFalse()
+
+ Thread.sleep(5000L)
+ rule.keyPress(
+ if (vertical) NativeKeyEvent.KEYCODE_DPAD_DOWN else NativeKeyEvent.KEYCODE_DPAD_RIGHT,
+ 13
+ )
+ Thread.sleep(5000L)
+
+ rule.waitForIdle()
+
+ assertWithMessage("No additional items were composed after scroll, scroll didn't work")
+ .that(composed).isTrue()
+
+ // We may need to modify this test once we prefetch/cache items outside the viewport
+ assertWithMessage(
+ "No compositions were disposed after scrolling, compositions were leaked"
+ ).that(disposed).isTrue()
+ }
+
+ @Test
+ fun whenItemsAreInitiallyCreatedWith0SizeWeCanScrollWhenTheyExpanded() {
+ val thirdTag = "third"
+ val items = (1..3).toList()
+ var thirdHasSize by mutableStateOf(false)
+
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(
+ Modifier.fillMaxCrossAxis()
+ .mainAxisSize(100.dp)
+ .testTag(LazyListTag)
+ ) {
+ items(items) {
+ if (it == 3) {
+ Box(
+ Modifier.testTag(thirdTag)
+ .then(fillParentMaxCrossAxis())
+ .mainAxisSize(if (thirdHasSize) 60.dp else 0.dp).focusable()
+ )
+ } else {
+ Box(Modifier.then(fillParentMaxCrossAxis()).mainAxisSize(60.dp).focusable())
+ }
+ }
+ }
+ }
+
+ rule.keyPress(3)
+
+ rule.onNodeWithTag(thirdTag)
+ .assertExists()
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ thirdHasSize = true
+ }
+
+ rule.waitForIdle()
+
+ rule.keyPress(2)
+
+ rule.onNodeWithTag(thirdTag)
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun itemFillingParentWidth() {
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(Modifier.requiredSize(width = 100.dp, height = 150.dp)) {
+ items(listOf(0)) {
+ Spacer(
+ Modifier.fillParentMaxWidth().requiredHeight(50.dp).testTag(firstItemTag)
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertWidthIsEqualTo(100.dp)
+ .assertHeightIsEqualTo(50.dp)
+ }
+
+ @Test
+ fun itemFillingParentHeight() {
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(Modifier.requiredSize(width = 100.dp, height = 150.dp)) {
+ items(listOf(0)) {
+ Spacer(
+ Modifier.requiredWidth(50.dp).fillParentMaxHeight().testTag(firstItemTag)
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertWidthIsEqualTo(50.dp)
+ .assertHeightIsEqualTo(150.dp)
+ }
+
+ @Test
+ fun itemFillingParentSize() {
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(Modifier.requiredSize(width = 100.dp, height = 150.dp)) {
+ items(listOf(0)) {
+ Spacer(Modifier.fillParentMaxSize().testTag(firstItemTag))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertWidthIsEqualTo(100.dp)
+ .assertHeightIsEqualTo(150.dp)
+ }
+
+ @Test
+ fun itemFillingParentWidthFraction() {
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(Modifier.requiredSize(width = 100.dp, height = 150.dp)) {
+ items(listOf(0)) {
+ Spacer(
+ Modifier.fillParentMaxWidth(0.7f)
+ .requiredHeight(50.dp)
+ .testTag(firstItemTag)
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertWidthIsEqualTo(70.dp)
+ .assertHeightIsEqualTo(50.dp)
+ }
+
+ @Test
+ fun itemFillingParentHeightFraction() {
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(Modifier.requiredSize(width = 100.dp, height = 150.dp)) {
+ items(listOf(0)) {
+ Spacer(
+ Modifier.requiredWidth(50.dp)
+ .fillParentMaxHeight(0.3f)
+ .testTag(firstItemTag)
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertWidthIsEqualTo(50.dp)
+ .assertHeightIsEqualTo(45.dp)
+ }
+
+ @Test
+ fun itemFillingParentSizeFraction() {
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(Modifier.requiredSize(width = 100.dp, height = 150.dp)) {
+ items(listOf(0)) {
+ Spacer(Modifier.fillParentMaxSize(0.5f).testTag(firstItemTag))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertWidthIsEqualTo(50.dp)
+ .assertHeightIsEqualTo(75.dp)
+ }
+
+ @Test
+ fun itemFillingParentSizeParentResized() {
+ var parentSize by mutableStateOf(100.dp)
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(Modifier.requiredSize(parentSize)) {
+ items(listOf(0)) {
+ Spacer(Modifier.fillParentMaxSize().testTag(firstItemTag))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ parentSize = 150.dp
+ }
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertWidthIsEqualTo(150.dp)
+ .assertHeightIsEqualTo(150.dp)
+ }
+
+ @Test
+ fun whenNotAnymoreAvailableItemWasDisplayed() {
+ var items by mutableStateOf((1..30).toList())
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(modifier = Modifier.requiredSize(100.dp).testTag(LazyListTag)) {
+ items(items) {
+ Box(Modifier.requiredSize(20.dp).testTag("$it").focusable())
+ }
+ }
+ }
+
+ // after scroll we will display items 16-20
+ rule.keyPress(17)
+
+ rule.runOnIdle {
+ items = (1..10).toList()
+ }
+
+ // there is no item 16 anymore so we will just display the last items 6-10
+ rule.onNodeWithTag("6")
+ .assertStartPositionIsAlmost(0.dp)
+ }
+
+ @Test
+ fun whenFewDisplayedItemsWereRemoved() {
+ var items by mutableStateOf((1..10).toList())
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(modifier = Modifier.requiredSize(100.dp).testTag(LazyListTag)) {
+ items(items) {
+ Spacer(Modifier.requiredSize(20.dp).testTag("$it").focusable())
+ }
+ }
+ }
+
+ // after scroll we will display items 6-10
+ rule.keyPress(5)
+ rule.runOnIdle {
+ items = (1..8).toList()
+ }
+
+ // there are no more items 9 and 10, so we have to scroll back
+ rule.onNodeWithTag("4")
+ .assertStartPositionIsAlmost(0.dp)
+ }
+
+ @Test
+ fun whenItemsBecameEmpty() {
+ var items by mutableStateOf((1..10).toList())
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(
+ modifier = Modifier.requiredSizeIn(maxHeight = 100.dp, maxWidth = 100.dp)
+ .testTag(LazyListTag)
+ ) {
+ items(items) {
+ Spacer(Modifier.requiredSize(20.dp).testTag("$it").focusable())
+ }
+ }
+ }
+
+ // after scroll we will display items 2-6
+ rule.keyPress(2)
+
+ rule.runOnIdle {
+ items = emptyList()
+ }
+
+ // there are no more items so the lazy list is zero sized
+ rule.onNodeWithTag(LazyListTag)
+ .assertWidthIsEqualTo(0.dp)
+ .assertHeightIsEqualTo(0.dp)
+
+ // and has no children
+ rule.onNodeWithTag("1")
+ .assertDoesNotExist()
+ rule.onNodeWithTag("2")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun scrollBackAndForth() {
+ val items by mutableStateOf((1..20).toList())
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(
+ modifier = Modifier.requiredSize(100.dp).testTag(LazyListTag)
+ ) {
+ items(items) {
+ Spacer(Modifier.requiredSize(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ // after scroll we will display items 6-10
+ rule.keyPress(5)
+
+ // and scroll back
+ rule.keyPress(5, reverseScroll = true)
+
+ rule.onNodeWithTag("1")
+ .assertStartPositionIsAlmost(0.dp)
+ }
+
+ @Test
+ fun tryToScrollBackwardWhenAlreadyOnTop() {
+ val items by mutableStateOf((1..20).toList())
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(modifier = Modifier.requiredSize(100.dp).testTag(LazyListTag)) {
+ items(items) {
+ Box(Modifier.requiredSize(20.dp).testTag("$it").focusable())
+ }
+ }
+ }
+
+ // getting focus to the first element
+ rule.keyPress(2)
+ // we already displaying the first item, so this should do nothing
+ rule.keyPress(4, reverseScroll = true)
+
+ rule.onNodeWithTag("1")
+ .assertStartPositionIsAlmost(0.dp)
+ rule.onNodeWithTag("2")
+ .assertStartPositionIsAlmost(20.dp)
+ rule.onNodeWithTag("3")
+ .assertStartPositionIsAlmost(40.dp)
+ rule.onNodeWithTag("4")
+ .assertStartPositionIsAlmost(60.dp)
+ rule.onNodeWithTag("5")
+ .assertStartPositionIsAlmost(80.dp)
+ }
+
+ @Test
+ fun contentOfNotStableItemsIsNotRecomposedDuringScroll() {
+ val items = listOf(NotStable(1), NotStable(2))
+ var firstItemRecomposed = 0
+ var secondItemRecomposed = 0
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(modifier = Modifier.requiredSize(100.dp).testTag(LazyListTag)) {
+ items(items) {
+ if (it.count == 1) {
+ firstItemRecomposed++
+ } else {
+ secondItemRecomposed++
+ }
+ Box(Modifier.requiredSize(75.dp).focusable())
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(firstItemRecomposed).isEqualTo(1)
+ assertThat(secondItemRecomposed).isEqualTo(1)
+ }
+
+ rule.keyPress(2)
+
+ rule.runOnIdle {
+ assertThat(firstItemRecomposed).isEqualTo(1)
+ assertThat(secondItemRecomposed).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun onlyOneMeasurePassForScrollEvent() {
+ val items by mutableStateOf((1..20).toList())
+ lateinit var state: TvLazyListState
+ rule.setContentWithTestViewConfiguration {
+ state = rememberLazyListState()
+ state.prefetchingEnabled = false
+ LazyColumnOrRow(
+ Modifier.requiredSize(100.dp).testTag(LazyListTag),
+ state = state
+ ) {
+ items(items) {
+ Spacer(Modifier.requiredSize(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ val initialMeasurePasses = state.numMeasurePasses
+
+ rule.runOnIdle {
+ with(rule.density) {
+ state.onScroll(-110.dp.toPx())
+ }
+ }
+
+ rule.waitForIdle()
+
+ assertThat(state.numMeasurePasses).isEqualTo(initialMeasurePasses + 1)
+ }
+
+ @Test
+ fun onlyOneInitialMeasurePass() {
+ val items by mutableStateOf((1..20).toList())
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ LazyColumnOrRow(
+ Modifier.requiredSize(100.dp).testTag(LazyListTag),
+ state = state
+ ) {
+ items(items) {
+ Spacer(Modifier.requiredSize(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.numMeasurePasses).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun scroll_makeListSmaller_scroll() {
+ var items by mutableStateOf((1..100).toList())
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(modifier = Modifier.requiredSize(100.dp).testTag(LazyListTag)) {
+ items(items) {
+ Box(Modifier.requiredSize(10.dp).testTag("$it").focusable())
+ }
+ }
+ }
+
+ rule.keyPress(30)
+ rule.runOnIdle {
+ items = (1..11).toList()
+ }
+
+ rule.waitForIdle()
+ // try to scroll after the data set has been updated. this was causing a crash previously
+ rule.keyPress(1, reverseScroll = true)
+ rule.onNodeWithTag("11")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun initialScrollIsApplied() {
+ val items by mutableStateOf((0..20).toList())
+ lateinit var state: TvLazyListState
+ val expectedOffset = with(rule.density) { 10.dp.roundToPx() }
+ rule.setContentWithTestViewConfiguration {
+ state = rememberLazyListState(2, expectedOffset)
+ LazyColumnOrRow(
+ Modifier.requiredSize(100.dp).testTag(LazyListTag),
+ state = state
+ ) {
+ items(items) {
+ Spacer(Modifier.requiredSize(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(2)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(expectedOffset)
+ }
+
+ rule.onNodeWithTag("2")
+ .assertStartPositionInRootIsEqualTo((-10).dp)
+ }
+
+ @Test
+ fun stateIsRestored() {
+ val restorationTester = StateRestorationTester(rule)
+ var state: TvLazyListState? = null
+ restorationTester.setContent {
+ state = rememberLazyListState()
+ LazyColumnOrRow(
+ Modifier.requiredSize(100.dp).testTag(LazyListTag),
+ state = state!!
+ ) {
+ items(20) {
+ Spacer(Modifier.requiredSize(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ rule.keyPress(3)
+
+ val (index, scrollOffset) = rule.runOnIdle {
+ state!!.firstVisibleItemIndex to state!!.firstVisibleItemScrollOffset
+ }
+
+ state = null
+
+ restorationTester.emulateSavedInstanceStateRestore()
+
+ rule.runOnIdle {
+ assertThat(state!!.firstVisibleItemIndex).isEqualTo(index)
+ assertThat(state!!.firstVisibleItemScrollOffset).isEqualTo(scrollOffset)
+ }
+ }
+
+ @Test
+ fun snapToItemIndex() {
+ lateinit var state: TvLazyListState
+ rule.setContentWithTestViewConfiguration {
+ state = rememberLazyListState()
+ LazyColumnOrRow(
+ Modifier.requiredSize(100.dp).testTag(LazyListTag),
+ state = state
+ ) {
+ items(20) {
+ Spacer(Modifier.requiredSize(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(3, 10)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(3)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+ }
+ }
+
+ // TODO: Needs to be debugged and fixed for TV surfaces.
+ /*@Test
+ fun itemsAreNotRedrawnDuringScroll() {
+ val items = (0..20).toList()
+ val redrawCount = Array(6) { 0 }
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(
+ modifier = Modifier.requiredSize(100.dp).testTag(LazyListTag),
+ pivotOffsetConfig = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(items) {
+ Box(
+ Modifier.requiredSize(20.dp)
+ .testTag(it.toString())
+ .drawBehind {
+ redrawCount[it]++
+ if (redrawCount[it] != 1) {
+ Log.i("REMOVE_ME", Exception("Redrawn").stackTraceToString())
+ }
+ }
+ .focusable()
+ ) {
+ BasicText(it.toString())
+ }
+ }
+ }
+ }
+
+ rule.keyPress(3)
+ rule.onNodeWithTag("0").assertIsNotDisplayed()
+ rule.runOnIdle {
+ redrawCount.forEachIndexed { index, i ->
+ assertWithMessage("Item with index $index was redrawn $i times")
+ .that(i).isEqualTo(1)
+ }
+ }
+ }*/
+
+ @Test
+ fun itemInvalidationIsNotCausingAnotherItemToRedraw() {
+ val redrawCount = Array(2) { 0 }
+ var stateUsedInDrawScope by mutableStateOf(false)
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(Modifier.requiredSize(100.dp).testTag(LazyListTag)) {
+ items(2) {
+ Spacer(
+ Modifier.requiredSize(50.dp)
+ .drawBehind {
+ redrawCount[it]++
+ if (it == 1) {
+ stateUsedInDrawScope.hashCode()
+ }
+ }
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ stateUsedInDrawScope = true
+ }
+
+ rule.runOnIdle {
+ assertWithMessage("First items is not expected to be redrawn")
+ .that(redrawCount[0]).isEqualTo(1)
+ assertWithMessage("Second items is expected to be redrawn")
+ .that(redrawCount[1]).isEqualTo(2)
+ }
+ }
+
+ @Test
+ fun notVisibleAnymoreItemNotAffectingCrossAxisSize() {
+ val itemSize = with(rule.density) { 30.toDp() }
+ val itemSizeMinusOne = with(rule.density) { 29.toDp() }
+ lateinit var state: TvLazyListState
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(
+ Modifier.mainAxisSize(itemSizeMinusOne).testTag(LazyListTag),
+ state = rememberLazyListState().also { state = it }
+ ) {
+ items(2) {
+ Spacer(
+ if (it == 0) {
+ Modifier.crossAxisSize(30.dp).mainAxisSize(itemSizeMinusOne)
+ } else {
+ Modifier.crossAxisSize(20.dp).mainAxisSize(itemSize)
+ }
+ )
+ }
+ }
+ }
+
+ state.scrollBy(itemSize)
+
+ rule.onNodeWithTag(LazyListTag)
+ .assertCrossAxisSizeIsEqualTo(20.dp)
+ }
+
+ @Test
+ fun itemStillVisibleAfterOverscrollIsAffectingCrossAxisSize() {
+ val items = (0..2).toList()
+ val itemSize = with(rule.density) { 30.toDp() }
+ lateinit var state: TvLazyListState
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(
+ Modifier.mainAxisSize(itemSize * 1.75f).testTag(LazyListTag),
+ state = rememberLazyListState().also { state = it }
+ ) {
+ items(items) {
+ Spacer(
+ if (it == 0) {
+ Modifier.crossAxisSize(30.dp).mainAxisSize(itemSize / 2)
+ } else if (it == 1) {
+ Modifier.crossAxisSize(20.dp).mainAxisSize(itemSize / 2)
+ } else {
+ Modifier.crossAxisSize(20.dp).mainAxisSize(itemSize)
+ }
+ )
+ }
+ }
+ }
+
+ state.scrollBy(itemSize)
+
+ rule.onNodeWithTag(LazyListTag)
+ .assertCrossAxisSizeIsEqualTo(30.dp)
+ }
+
+ @Test
+ fun usedWithArray() {
+ val items = arrayOf("1", "2", "3")
+
+ val itemSize = with(rule.density) { 15.toDp() }
+
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow {
+ items(items) {
+ Spacer(Modifier.requiredSize(itemSize).testTag(it))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertStartPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("2")
+ .assertStartPositionInRootIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("3")
+ .assertStartPositionInRootIsEqualTo(itemSize * 2)
+ }
+
+ @Test
+ fun usedWithArrayIndexed() {
+ val items = arrayOf("1", "2", "3")
+
+ val itemSize = with(rule.density) { 15.toDp() }
+
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow {
+ itemsIndexed(items) { index, item ->
+ Spacer(Modifier.requiredSize(itemSize).testTag("$index*$item"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0*1")
+ .assertStartPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("1*2")
+ .assertStartPositionInRootIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("2*3")
+ .assertStartPositionInRootIsEqualTo(itemSize * 2)
+ }
+
+ @Test
+ fun changeItemsCountAndScrollImmediately() {
+ lateinit var state: TvLazyListState
+ var count by mutableStateOf(100)
+ val composedIndexes = mutableListOf<Int>()
+ rule.setContent {
+ state = rememberLazyListState()
+ LazyColumnOrRow(Modifier.fillMaxCrossAxis().mainAxisSize(10.dp), state) {
+ items(count) { index ->
+ composedIndexes.add(index)
+ Box(Modifier.size(20.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ composedIndexes.clear()
+ count = 10
+ runBlocking(AutoTestFrameClock()) {
+ state.scrollToItem(50)
+ }
+ composedIndexes.forEach {
+ assertThat(it).isLessThan(count)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(9)
+ }
+ }
+
+ @Test
+ fun overscrollingBackwardFromNotTheFirstPosition() {
+ val containerTag = "container"
+ val itemSizePx = 10
+ val itemSizeDp = with(rule.density) { itemSizePx.toDp() }
+ val containerSize = itemSizeDp * 5
+ rule.setContentWithTestViewConfiguration {
+ Box(
+ Modifier
+ .testTag(containerTag)
+ .size(containerSize)
+ ) {
+ LazyColumnOrRow(
+ Modifier
+ .testTag(LazyListTag)
+ .background(Color.Blue),
+ state = rememberLazyListState(2, 5)
+ ) {
+ items(100) {
+ Box(
+ Modifier
+ .fillMaxCrossAxis()
+ .mainAxisSize(itemSizeDp)
+ .testTag("$it")
+ .focusable()
+ )
+ }
+ }
+ }
+ }
+
+ rule.keyPress(
+ if (vertical) NativeKeyEvent.KEYCODE_DPAD_UP else NativeKeyEvent.KEYCODE_DPAD_LEFT,
+ 15
+ )
+
+ rule.onNodeWithTag(LazyListTag)
+ .assertMainAxisSizeIsEqualTo(containerSize)
+
+ rule.onNodeWithTag("0")
+ .assertStartPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("4")
+ .assertStartPositionInRootIsEqualTo(containerSize - itemSizeDp)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun doesNotClipHorizontalOverdraw() {
+ rule.setContent {
+ Box(Modifier.size(60.dp).testTag("container").background(Color.Gray)) {
+ LazyColumnOrRow(
+ Modifier
+ .padding(20.dp)
+ .fillMaxSize(),
+ rememberLazyListState(1)
+ ) {
+ items(4) {
+ Box(Modifier.size(20.dp).drawOutsideOfBounds())
+ }
+ }
+ }
+ }
+
+ val horizontalPadding = if (vertical) 0.dp else 20.dp
+ val verticalPadding = if (vertical) 20.dp else 0.dp
+
+ rule.onNodeWithTag("container")
+ .captureToImage()
+ .assertShape(
+ density = rule.density,
+ shape = RectangleShape,
+ shapeColor = Color.Red,
+ backgroundColor = Color.Gray,
+ horizontalPadding = horizontalPadding,
+ verticalPadding = verticalPadding
+ )
+ }
+
+ @Test
+ fun initialScrollPositionIsCorrectWhenItemsAreLoadedAsynchronously() {
+ lateinit var state: TvLazyListState
+ var itemsCount by mutableStateOf(0)
+ rule.setContent {
+ state = rememberLazyListState(2, 10)
+ LazyColumnOrRow(Modifier.fillMaxSize(), state) {
+ items(itemsCount) {
+ Box(Modifier.size(20.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ itemsCount = 100
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(2)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+ }
+ }
+
+ @Test
+ fun restoredScrollPositionIsCorrectWhenItemsAreLoadedAsynchronously() {
+ lateinit var state: TvLazyListState
+ var itemsCount = 100
+ val recomposeCounter = mutableStateOf(0)
+ val tester = StateRestorationTester(rule)
+ tester.setContent {
+ state = rememberLazyListState()
+ LazyColumnOrRow(Modifier.fillMaxSize(), state) {
+ recomposeCounter.value
+ items(itemsCount) {
+ Box(Modifier.size(20.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(2, 10)
+ }
+ itemsCount = 0
+ }
+
+ tester.emulateSavedInstanceStateRestore()
+
+ rule.runOnIdle {
+ itemsCount = 100
+ recomposeCounter.value = 1
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(2)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+ }
+ }
+
+ @Test
+ fun animateScrollToItemDoesNotScrollPastItem() {
+ lateinit var state: TvLazyListState
+ var target = 0
+ var reverse = false
+ rule.setContent {
+ val listState = rememberLazyListState()
+ SideEffect {
+ state = listState
+ }
+ LazyColumnOrRow(Modifier.fillMaxSize(), listState) {
+ items(2500) { _ ->
+ Box(Modifier.size(100.dp))
+ }
+ }
+
+ if (reverse) {
+ assertThat(listState.firstVisibleItemIndex).isAtLeast(target)
+ } else {
+ assertThat(listState.firstVisibleItemIndex).isAtMost(target)
+ }
+ }
+
+ // Try a bunch of different targets with varying spacing
+ listOf(500, 800, 1500, 1600, 1800).forEach {
+ target = it
+ rule.runOnIdle {
+ runBlocking(AutoTestFrameClock()) {
+ state.animateScrollToItem(target)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(target)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ }
+ }
+
+ reverse = true
+
+ listOf(1600, 1500, 800, 500, 0).forEach {
+ target = it
+ rule.runOnIdle {
+ runBlocking(AutoTestFrameClock()) {
+ state.animateScrollToItem(target)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(target)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ }
+ }
+ }
+
+ @Test
+ fun animateScrollToTheLastItemWhenItemsAreLargerThenTheScreen() {
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ LazyColumnOrRow(Modifier.crossAxisSize(150.dp).mainAxisSize(100.dp), state) {
+ items(20) {
+ Box(Modifier.size(150.dp))
+ }
+ }
+ }
+
+ // Try a bunch of different start indexes
+ listOf(0, 5, 12).forEach {
+ val startIndex = it
+ rule.runOnIdle {
+ runBlocking(AutoTestFrameClock()) {
+ state.scrollToItem(startIndex)
+ state.animateScrollToItem(19)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(19)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ }
+ }
+ }
+
+ @Test
+ fun recreatingContentLambdaTriggersItemRecomposition() {
+ val countState = mutableStateOf(0)
+ rule.setContent {
+ val count = countState.value
+ LazyColumnOrRow {
+ item {
+ BasicText(text = "Count $count")
+ }
+ }
+ }
+
+ rule.onNodeWithText("Count 0")
+ .assertIsDisplayed()
+
+ rule.runOnIdle {
+ countState.value++
+ }
+
+ rule.onNodeWithText("Count 1")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun semanticsScroll_isAnimated() {
+ rule.mainClock.autoAdvance = false
+ val state = TvLazyListState()
+
+ rule.setContent {
+ LazyColumnOrRow(Modifier.testTag(LazyListTag), state = state) {
+ items(50) {
+ Box(Modifier.mainAxisSize(200.dp))
+ }
+ }
+ }
+
+ rule.waitForIdle()
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+
+ rule.onNodeWithTag(LazyListTag).performSemanticsAction(SemanticsActions.ScrollBy) {
+ if (vertical) {
+ it(0f, 100f)
+ } else {
+ it(100f, 0f)
+ }
+ }
+
+ // We haven't advanced time yet, make sure it's still zero
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+
+ // Advance and make sure we're partway through
+ // Note that we need two frames for the animation to actually happen
+ rule.mainClock.advanceTimeByFrame()
+ rule.mainClock.advanceTimeByFrame()
+
+ // The items are 200dp each, so still the first one, but offset
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+ assertThat(state.firstVisibleItemScrollOffset).isLessThan(100)
+
+ // Finish the scroll, make sure we're at the target
+ rule.mainClock.advanceTimeBy(5000)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(100)
+ }
+
+ @Test
+ fun maxIntElements() {
+ val itemSize = with(rule.density) { 15.toDp() }
+
+ rule.setContent {
+ LazyColumnOrRow(
+ modifier = Modifier.requiredSize(itemSize * 3),
+ state = TvLazyListState(firstVisibleItemIndex = Int.MAX_VALUE - 3)
+ ) {
+ items(Int.MAX_VALUE) {
+ Box(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("${Int.MAX_VALUE - 3}").assertStartPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("${Int.MAX_VALUE - 2}").assertStartPositionInRootIsEqualTo(itemSize)
+ rule.onNodeWithTag("${Int.MAX_VALUE - 1}").assertStartPositionInRootIsEqualTo(itemSize * 2)
+
+ rule.onNodeWithTag("${Int.MAX_VALUE}").assertDoesNotExist()
+ rule.onNodeWithTag("0").assertDoesNotExist()
+ }
+
+ @Test
+ fun scrollingByExactlyTheItemSize_switchesTheFirstVisibleItem() {
+ val itemSize = with(rule.density) { 30.toDp() }
+ lateinit var state: TvLazyListState
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(
+ Modifier.mainAxisSize(itemSize * 3),
+ state = rememberLazyListState().also { state = it },
+ ) {
+ items(5) {
+ Spacer(
+ Modifier.size(itemSize).testTag("$it")
+ )
+ }
+ }
+ }
+
+ state.scrollBy(itemSize)
+
+ rule.onNodeWithTag("0")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ }
+ }
+
+ @Test
+ fun pointerInputScrollingIsAllowedWhenUserScrollingIsEnabled() {
+ val itemSize = with(rule.density) { 30.toDp() }
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(
+ Modifier.mainAxisSize(itemSize * 3).testTag(LazyListTag),
+ userScrollEnabled = true,
+ ) {
+ items(5) {
+ Spacer(Modifier.size(itemSize).testTag("$it").focusable())
+ }
+ }
+ }
+
+ rule.keyPress(3)
+
+ rule.onNodeWithTag("1")
+ .assertStartPositionInRootIsEqualTo(0.dp)
+ }
+
+ @Test
+ fun pointerInputScrollingIsDisallowedWhenUserScrollingIsDisabled() {
+ val itemSize = with(rule.density) { 30.toDp() }
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(
+ Modifier.mainAxisSize(itemSize * 3).testTag(LazyListTag),
+ userScrollEnabled = false,
+ ) {
+ items(5) {
+ Spacer(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ rule.keyPress(1)
+
+ rule.onNodeWithTag("1")
+ .assertStartPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun programmaticScrollingIsAllowedWhenUserScrollingIsDisabled() {
+ val itemSize = with(rule.density) { 30.toDp() }
+ lateinit var state: TvLazyListState
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(
+ Modifier.mainAxisSize(itemSize * 3),
+ state = rememberLazyListState().also { state = it },
+ userScrollEnabled = false,
+ ) {
+ items(5) {
+ Spacer(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ state.scrollBy(itemSize)
+
+ rule.onNodeWithTag("1")
+ .assertStartPositionInRootIsEqualTo(0.dp)
+ }
+
+ @Test
+ fun semanticScrollingIsDisallowedWhenUserScrollingIsDisabled() {
+ val itemSize = with(rule.density) { 30.toDp() }
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(
+ Modifier.mainAxisSize(itemSize * 3).testTag(LazyListTag),
+ userScrollEnabled = false,
+ ) {
+ items(5) {
+ Spacer(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyListTag)
+ .assert(keyNotDefined(SemanticsActions.ScrollBy))
+ .assert(keyNotDefined(SemanticsActions.ScrollToIndex))
+ // but we still have a read only scroll range property
+ .assert(
+ keyIsDefined(
+ if (vertical) {
+ SemanticsProperties.VerticalScrollAxisRange
+ } else {
+ SemanticsProperties.HorizontalScrollAxisRange
+ }
+ )
+ )
+ }
+
+ @Test
+ fun withMissingItems() {
+ val itemSize = with(rule.density) { 30.toDp() }
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ LazyColumnOrRow(
+ modifier = Modifier.mainAxisSize(itemSize + 1.dp),
+ state = state
+ ) {
+ items(4) {
+ if (it != 1) {
+ Box(Modifier.size(itemSize).testTag(it.toString()).focusable())
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0").assertIsDisplayed()
+ rule.onNodeWithTag("2").assertIsDisplayed()
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(1)
+ }
+ }
+
+ rule.onNodeWithTag("0").assertIsNotDisplayed()
+ rule.onNodeWithTag("2").assertIsDisplayed()
+ rule.onNodeWithTag("3").assertIsDisplayed()
+ }
+
+ @Test
+ fun recomposingWithNewComposedModifierObjectIsNotCausingRemeasure() {
+ var remeasureCount = 0
+ val layoutModifier = Modifier.layout { measurable, constraints ->
+ remeasureCount++
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+ val counter = mutableStateOf(0)
+
+ rule.setContentWithTestViewConfiguration {
+ counter.value // just to trigger recomposition
+ LazyColumnOrRow(
+ // this will return a new object everytime causing Lazy list recomposition
+ // without causing remeasure
+ Modifier.composed { layoutModifier }
+ ) {
+ items(1) {
+ Spacer(Modifier.size(10.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(remeasureCount).isEqualTo(1)
+ counter.value++
+ }
+
+ rule.runOnIdle {
+ assertThat(remeasureCount).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun passingNegativeItemsCountIsNotAllowed() {
+ var exception: Exception? = null
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow {
+ try {
+ items(-1) {
+ Box(Modifier)
+ }
+ } catch (e: Exception) {
+ exception = e
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(exception).isInstanceOf(IllegalArgumentException::class.java)
+ }
+ }
+
+ @Test
+ fun scrollingALotDoesntCauseLazyLayoutRecomposition() {
+ var recomposeCount = 0
+ lateinit var state: TvLazyListState
+
+ rule.setContentWithTestViewConfiguration {
+ state = rememberLazyListState()
+ LazyColumnOrRow(
+ Modifier.composed {
+ recomposeCount++
+ Modifier
+ },
+ state
+ ) {
+ items(1000) {
+ Spacer(Modifier.size(10.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(recomposeCount).isEqualTo(1)
+
+ runBlocking {
+ state.scrollToItem(100)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(recomposeCount).isEqualTo(1)
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ fun zIndexOnItemAffectsDrawingOrder() {
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(
+ Modifier.size(6.dp).testTag(LazyListTag)
+ ) {
+ items(listOf(Color.Blue, Color.Green, Color.Red)) { color ->
+ Box(
+ Modifier
+ .mainAxisSize(2.dp)
+ .crossAxisSize(6.dp)
+ .zIndex(if (color == Color.Green) 1f else 0f)
+ .drawBehind {
+ drawRect(
+ color,
+ topLeft = Offset(-10.dp.toPx(), -10.dp.toPx()),
+ size = Size(20.dp.toPx(), 20.dp.toPx())
+ )
+ })
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyListTag)
+ .captureToImage()
+ .assertPixels { Color.Green }
+ }
+
+ // ********************* END OF TESTS *********************
+ // Helper functions, etc. live below here
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun params() = arrayOf(Orientation.Vertical, Orientation.Horizontal)
+ }
+}
+
+internal val NeverEqualObject = object {
+ override fun equals(other: Any?): Boolean {
+ return false
+ }
+}
+
+private data class NotStable(val count: Int)
+
+internal const val TestTouchSlop = 18f
+
+internal fun IntegerSubject.isWithin1PixelFrom(expected: Int) {
+ isEqualTo(expected, 1)
+}
+
+internal fun IntegerSubject.isEqualTo(expected: Int, tolerance: Int) {
+ isIn(Range.closed(expected - tolerance, expected + tolerance))
+}
+
+internal fun ComposeContentTestRule.setContentWithTestViewConfiguration(
+ composable: @Composable () -> Unit
+) {
+ this.setContent {
+ WithTouchSlop(TestTouchSlop, composable)
+ }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListsContentPaddingTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListsContentPaddingTest.kt
new file mode 100644
index 0000000..eccaaed
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListsContentPaddingTest.kt
@@ -0,0 +1,781 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.list
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class LazyListsContentPaddingTest(orientation: Orientation) :
+ BaseLazyListTestWithOrientation(orientation) {
+
+ private val LazyListTag = "LazyList"
+ private val ItemTag = "item"
+ private val ContainerTag = "container"
+
+ private var itemSize: Dp = Dp.Infinity
+ private var smallPaddingSize: Dp = Dp.Infinity
+ private var itemSizePx = 50f
+ private var smallPaddingSizePx = 12f
+
+ @Before
+ fun before() {
+ with(rule.density) {
+ itemSize = itemSizePx.toDp()
+ smallPaddingSize = smallPaddingSizePx.toDp()
+ }
+ }
+
+ @Test
+ fun contentPaddingIsApplied() {
+ lateinit var state: TvLazyListState
+ val containerSize = itemSize * 2
+ val largePaddingSize = itemSize
+ rule.setContent {
+ LazyColumnOrRow(
+ modifier = Modifier.requiredSize(containerSize)
+ .testTag(LazyListTag),
+ state = rememberLazyListState().also { state = it },
+ contentPadding = PaddingValues(
+ mainAxis = largePaddingSize,
+ crossAxis = smallPaddingSize
+ )
+ ) {
+ items(listOf(1)) {
+ Spacer(
+ Modifier
+ .then(fillParentMaxCrossAxis())
+ .mainAxisSize(itemSize)
+ .testTag(ItemTag)
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag(ItemTag)
+ .assertCrossAxisStartPositionInRootIsEqualTo(smallPaddingSize)
+ .assertStartPositionInRootIsEqualTo(largePaddingSize)
+ .assertCrossAxisSizeIsEqualTo(containerSize - smallPaddingSize * 2)
+ .assertMainAxisSizeIsEqualTo(itemSize)
+
+ state.scrollBy(largePaddingSize)
+
+ rule.onNodeWithTag(ItemTag)
+ .assertStartPositionInRootIsEqualTo(0.dp)
+ .assertMainAxisSizeIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun contentPaddingIsNotAffectingScrollPosition() {
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ LazyColumnOrRow(
+ modifier = Modifier.requiredSize(itemSize * 2)
+ .testTag(LazyListTag),
+ state = rememberLazyListState().also { state = it },
+ contentPadding = PaddingValues(mainAxis = itemSize)
+ ) {
+ items(listOf(1)) {
+ Spacer(
+ Modifier
+ .then(fillParentMaxCrossAxis())
+ .mainAxisSize(itemSize)
+ .testTag(ItemTag))
+ }
+ }
+ }
+
+ state.assertScrollPosition(0, 0.dp)
+
+ state.scrollBy(itemSize)
+
+ state.assertScrollPosition(0, itemSize)
+ }
+
+ @Test
+ fun scrollForwardItemWithinStartPaddingDisplayed() {
+ lateinit var state: TvLazyListState
+ val padding = itemSize * 1.5f
+ rule.setContent {
+ LazyColumnOrRow(
+ modifier = Modifier.requiredSize(padding * 2 + itemSize)
+ .testTag(LazyListTag),
+ state = rememberLazyListState().also { state = it },
+ contentPadding = PaddingValues(mainAxis = padding)
+ ) {
+ items((0..3).toList()) {
+ Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertStartPositionInRootIsEqualTo(padding)
+ rule.onNodeWithTag("1")
+ .assertStartPositionInRootIsEqualTo(itemSize + padding)
+ rule.onNodeWithTag("2")
+ .assertStartPositionInRootIsEqualTo(itemSize * 2 + padding)
+
+ state.scrollBy(padding)
+
+ state.assertScrollPosition(1, padding - itemSize)
+
+ rule.onNodeWithTag("0")
+ .assertStartPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("1")
+ .assertStartPositionInRootIsEqualTo(itemSize)
+ rule.onNodeWithTag("2")
+ .assertStartPositionInRootIsEqualTo(itemSize * 2)
+ rule.onNodeWithTag("3")
+ .assertStartPositionInRootIsEqualTo(itemSize * 3)
+ }
+
+ @Test
+ fun scrollBackwardItemWithinStartPaddingDisplayed() {
+ lateinit var state: TvLazyListState
+ val padding = itemSize * 1.5f
+ rule.setContent {
+ LazyColumnOrRow(
+ modifier = Modifier.requiredSize(itemSize + padding * 2)
+ .testTag(LazyListTag),
+ state = rememberLazyListState().also { state = it },
+ contentPadding = PaddingValues(mainAxis = padding)
+ ) {
+ items((0..3).toList()) {
+ Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
+ }
+ }
+ }
+
+ state.scrollBy(itemSize * 3)
+ state.scrollBy(-itemSize * 1.5f)
+
+ state.assertScrollPosition(1, itemSize * 0.5f)
+
+ rule.onNodeWithTag("0")
+ .assertStartPositionInRootIsEqualTo(itemSize * 1.5f - padding)
+ rule.onNodeWithTag("1")
+ .assertStartPositionInRootIsEqualTo(itemSize * 2.5f - padding)
+ rule.onNodeWithTag("2")
+ .assertStartPositionInRootIsEqualTo(itemSize * 3.5f - padding)
+ rule.onNodeWithTag("3")
+ .assertStartPositionInRootIsEqualTo(itemSize * 4.5f - padding)
+ }
+
+ @Test
+ fun scrollForwardTillTheEnd() {
+ lateinit var state: TvLazyListState
+ val padding = itemSize * 1.5f
+ rule.setContent {
+ LazyColumnOrRow(
+ modifier = Modifier.requiredSize(padding * 2 + itemSize)
+ .testTag(LazyListTag),
+ state = rememberLazyListState().also { state = it },
+ contentPadding = PaddingValues(mainAxis = padding)
+ ) {
+ items((0..3).toList()) {
+ Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
+ }
+ }
+ }
+
+ state.scrollBy(itemSize * 3)
+
+ state.assertScrollPosition(3, 0.dp)
+
+ rule.onNodeWithTag("1")
+ .assertStartPositionInRootIsEqualTo(itemSize - padding)
+ rule.onNodeWithTag("2")
+ .assertStartPositionInRootIsEqualTo(itemSize * 2 - padding)
+ rule.onNodeWithTag("3")
+ .assertStartPositionInRootIsEqualTo(itemSize * 3 - padding)
+
+ // there are no space to scroll anymore, so it should change nothing
+ state.scrollBy(10.dp)
+
+ state.assertScrollPosition(3, 0.dp)
+
+ rule.onNodeWithTag("1")
+ .assertStartPositionInRootIsEqualTo(itemSize - padding)
+ rule.onNodeWithTag("2")
+ .assertStartPositionInRootIsEqualTo(itemSize * 2 - padding)
+ rule.onNodeWithTag("3")
+ .assertStartPositionInRootIsEqualTo(itemSize * 3 - padding)
+ }
+
+ @Test
+ fun scrollForwardTillTheEndAndABitBack() {
+ lateinit var state: TvLazyListState
+ val padding = itemSize * 1.5f
+ rule.setContent {
+ LazyColumnOrRow(
+ modifier = Modifier.requiredSize(padding * 2 + itemSize)
+ .testTag(LazyListTag),
+ state = rememberLazyListState().also { state = it },
+ contentPadding = PaddingValues(mainAxis = padding)
+ ) {
+ items((0..3).toList()) {
+ Spacer(Modifier.requiredSize(itemSize).testTag(it.toString()))
+ }
+ }
+ }
+
+ state.scrollBy(itemSize * 3)
+ state.scrollBy(-itemSize / 2)
+
+ state.assertScrollPosition(2, itemSize / 2)
+
+ rule.onNodeWithTag("1")
+ .assertStartPositionInRootIsEqualTo(itemSize * 1.5f - padding)
+ rule.onNodeWithTag("2")
+ .assertStartPositionInRootIsEqualTo(itemSize * 2.5f - padding)
+ rule.onNodeWithTag("3")
+ .assertStartPositionInRootIsEqualTo(itemSize * 3.5f - padding)
+ }
+
+ @Test
+ fun contentPaddingAndWrapContent() {
+ rule.setContent {
+ Box(modifier = Modifier.testTag(ContainerTag)) {
+ LazyColumnOrRow(
+ contentPadding = PaddingValues(
+ beforeContentCrossAxis = 2.dp,
+ beforeContent = 4.dp,
+ afterContentCrossAxis = 6.dp,
+ afterContent = 8.dp
+ )
+ ) {
+ items(listOf(1)) {
+ Spacer(Modifier.requiredSize(itemSize).testTag(ItemTag))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag(ItemTag)
+ .assertCrossAxisStartPositionInRootIsEqualTo(2.dp)
+ .assertStartPositionInRootIsEqualTo(4.dp)
+ .assertCrossAxisSizeIsEqualTo(itemSize)
+ .assertMainAxisSizeIsEqualTo(itemSize)
+
+ rule.onNodeWithTag(ContainerTag)
+ .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertStartPositionInRootIsEqualTo(0.dp)
+ .assertCrossAxisSizeIsEqualTo(itemSize + 2.dp + 6.dp)
+ .assertMainAxisSizeIsEqualTo(itemSize + 4.dp + 8.dp)
+ }
+
+ @Test
+ fun contentPaddingAndNoContent() {
+ rule.setContent {
+ Box(modifier = Modifier.testTag(ContainerTag)) {
+ LazyColumnOrRow(
+ contentPadding = PaddingValues(
+ beforeContentCrossAxis = 2.dp,
+ beforeContent = 4.dp,
+ afterContentCrossAxis = 6.dp,
+ afterContent = 8.dp
+ )
+ ) { }
+ }
+ }
+
+ rule.onNodeWithTag(ContainerTag)
+ .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertStartPositionInRootIsEqualTo(0.dp)
+ .assertCrossAxisSizeIsEqualTo(8.dp)
+ .assertMainAxisSizeIsEqualTo(12.dp)
+ }
+
+ @Test
+ fun contentPaddingAndZeroSizedItem() {
+ rule.setContent {
+ Box(modifier = Modifier.testTag(ContainerTag)) {
+ LazyColumnOrRow(
+ contentPadding = PaddingValues(
+ beforeContentCrossAxis = 2.dp,
+ beforeContent = 4.dp,
+ afterContentCrossAxis = 6.dp,
+ afterContent = 8.dp
+ )
+ ) {
+ items(0) { }
+ }
+ }
+ }
+
+ rule.onNodeWithTag(ContainerTag)
+ .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertStartPositionInRootIsEqualTo(0.dp)
+ .assertCrossAxisSizeIsEqualTo(8.dp)
+ .assertMainAxisSizeIsEqualTo(12.dp)
+ }
+
+ @Test
+ fun contentPaddingAndReverseLayout() {
+ val topPadding = itemSize * 2
+ val bottomPadding = itemSize / 2
+ val listSize = itemSize * 3
+ lateinit var state: TvLazyListState
+ rule.setContentWithTestViewConfiguration {
+ LazyColumnOrRow(
+ reverseLayout = true,
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.requiredSize(listSize),
+ contentPadding = PaddingValues(
+ beforeContent = topPadding,
+ afterContent = bottomPadding
+ ),
+ ) {
+ items(3) { index ->
+ Box(Modifier.requiredSize(itemSize).testTag("$index"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertStartPositionInRootIsEqualTo(listSize - bottomPadding - itemSize)
+ rule.onNodeWithTag("1")
+ .assertStartPositionInRootIsEqualTo(listSize - bottomPadding - itemSize * 2)
+ // Partially visible.
+ rule.onNodeWithTag("2")
+ .assertStartPositionInRootIsEqualTo(-itemSize / 2)
+
+ // Scroll to the top.
+ state.scrollBy(itemSize * 2.5f)
+
+ rule.onNodeWithTag("2").assertStartPositionInRootIsEqualTo(topPadding)
+ // Shouldn't be visible
+ rule.onNodeWithTag("1").assertIsNotDisplayed()
+ rule.onNodeWithTag("0").assertIsNotDisplayed()
+ }
+
+ @Test
+ fun overscrollWithContentPadding() {
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize + smallPaddingSize * 2)) {
+ LazyColumnOrRow(
+ state = state,
+ contentPadding = PaddingValues(mainAxis = smallPaddingSize)
+ ) {
+ items(2) {
+ Box(Modifier.testTag("$it").fillParentMaxSize())
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertStartPositionInRootIsEqualTo(smallPaddingSize)
+ .assertMainAxisSizeIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("1")
+ .assertStartPositionInRootIsEqualTo(smallPaddingSize + itemSize)
+ .assertMainAxisSizeIsEqualTo(itemSize)
+
+ rule.runOnIdle {
+ runBlocking {
+ // itemSizePx is the maximum offset, plus if we overscroll the content padding
+ // the layout mechanism will decide the item 0 is not needed until we start
+ // filling the over scrolled gap.
+ state.scrollBy(value = itemSizePx + smallPaddingSizePx * 1.5f)
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertStartPositionInRootIsEqualTo(smallPaddingSize)
+ .assertMainAxisSizeIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("0")
+ .assertStartPositionInRootIsEqualTo(smallPaddingSize - itemSize)
+ .assertMainAxisSizeIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun totalPaddingLargerParentSize_initialState() {
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ LazyColumnOrRow(
+ state = state,
+ contentPadding = PaddingValues(mainAxis = itemSize)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertStartPositionInRootIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("1")
+ .assertDoesNotExist()
+
+ rule.runOnIdle {
+ state.assertScrollPosition(0, 0.dp)
+ state.assertVisibleItems(0 to 0.dp)
+ state.assertLayoutInfoOffsetRange(-itemSize, itemSize * 0.5f)
+ }
+ }
+
+ @Test
+ fun totalPaddingLargerParentSize_scrollByPadding() {
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ LazyColumnOrRow(
+ state = state,
+ contentPadding = PaddingValues(mainAxis = itemSize)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ state.scrollBy(itemSize)
+
+ rule.onNodeWithTag("0")
+ .assertStartPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("1")
+ .assertStartPositionInRootIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("2")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ state.assertScrollPosition(1, 0.dp)
+ state.assertVisibleItems(0 to -itemSize, 1 to 0.dp)
+ }
+ }
+
+ @Test
+ fun totalPaddingLargerParentSize_scrollToLastItem() {
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ LazyColumnOrRow(
+ state = state,
+ contentPadding = PaddingValues(mainAxis = itemSize)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ state.scrollTo(3)
+
+ rule.onNodeWithTag("1")
+ .assertDoesNotExist()
+
+ rule.onNodeWithTag("2")
+ .assertStartPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("3")
+ .assertStartPositionInRootIsEqualTo(itemSize)
+
+ rule.runOnIdle {
+ state.assertScrollPosition(3, 0.dp)
+ state.assertVisibleItems(2 to -itemSize, 3 to 0.dp)
+ }
+ }
+
+ @Test
+ fun totalPaddingLargerParentSize_scrollToLastItemByDelta() {
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ LazyColumnOrRow(
+ state = state,
+ contentPadding = PaddingValues(mainAxis = itemSize)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ state.scrollBy(itemSize * 3)
+
+ rule.onNodeWithTag("1")
+ .assertIsNotDisplayed()
+
+ rule.onNodeWithTag("2")
+ .assertStartPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("3")
+ .assertStartPositionInRootIsEqualTo(itemSize)
+
+ rule.runOnIdle {
+ state.assertScrollPosition(3, 0.dp)
+ state.assertVisibleItems(2 to -itemSize, 3 to 0.dp)
+ }
+ }
+
+ @Test
+ fun totalPaddingLargerParentSize_scrollTillTheEnd() {
+ // the whole end content padding is displayed
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ LazyColumnOrRow(
+ state = state,
+ contentPadding = PaddingValues(mainAxis = itemSize)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ state.scrollBy(itemSize * 4.5f)
+
+ rule.onNodeWithTag("2")
+ .assertIsNotDisplayed()
+
+ rule.onNodeWithTag("3")
+ .assertStartPositionInRootIsEqualTo(-itemSize * 0.5f)
+
+ rule.runOnIdle {
+ state.assertScrollPosition(3, itemSize * 1.5f)
+ state.assertVisibleItems(3 to -itemSize * 1.5f)
+ }
+ }
+
+ @Test
+ fun eachPaddingLargerParentSize_initialState() {
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ LazyColumnOrRow(
+ state = state,
+ contentPadding = PaddingValues(mainAxis = itemSize * 2)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ state.assertScrollPosition(0, 0.dp)
+ state.assertVisibleItems(0 to 0.dp)
+ state.assertLayoutInfoOffsetRange(-itemSize * 2, -itemSize * 0.5f)
+ }
+ }
+
+ @Test
+ fun eachPaddingLargerParentSize_scrollByPadding() {
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ LazyColumnOrRow(
+ state = state,
+ contentPadding = PaddingValues(mainAxis = itemSize * 2)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ state.scrollBy(itemSize * 2)
+
+ rule.onNodeWithTag("0")
+ .assertStartPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("1")
+ .assertStartPositionInRootIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("2")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ state.assertScrollPosition(2, 0.dp)
+ state.assertVisibleItems(0 to -itemSize * 2, 1 to -itemSize, 2 to 0.dp)
+ }
+ }
+
+ @Test
+ fun eachPaddingLargerParentSize_scrollToLastItem() {
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ LazyColumnOrRow(
+ state = state,
+ contentPadding = PaddingValues(mainAxis = itemSize * 2)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ state.scrollTo(3)
+
+ rule.onNodeWithTag("0")
+ .assertIsNotDisplayed()
+
+ rule.onNodeWithTag("1")
+ .assertStartPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("2")
+ .assertStartPositionInRootIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("3")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ state.assertScrollPosition(3, 0.dp)
+ state.assertVisibleItems(1 to -itemSize * 2, 2 to -itemSize, 3 to 0.dp)
+ }
+ }
+
+ @Test
+ fun eachPaddingLargerParentSize_scrollToLastItemByDelta() {
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ LazyColumnOrRow(
+ state = state,
+ contentPadding = PaddingValues(mainAxis = itemSize * 2)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ state.scrollBy(itemSize * 3)
+
+ rule.onNodeWithTag("0")
+ .assertIsNotDisplayed()
+
+ rule.onNodeWithTag("1")
+ .assertStartPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("2")
+ .assertStartPositionInRootIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("3")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ state.assertScrollPosition(3, 0.dp)
+ state.assertVisibleItems(1 to -itemSize * 2, 2 to -itemSize, 3 to 0.dp)
+ }
+ }
+
+ @Test
+ fun eachPaddingLargerParentSize_scrollTillTheEnd() {
+ // only the end content padding is displayed
+ lateinit var state: TvLazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ LazyColumnOrRow(
+ state = state,
+ contentPadding = PaddingValues(mainAxis = itemSize * 2)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ state.scrollBy(
+ itemSize * 1.5f + // container size
+ itemSize * 2 + // start padding
+ itemSize * 3 // all items
+ )
+
+ rule.onNodeWithTag("3")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ state.assertScrollPosition(3, itemSize * 3.5f)
+ state.assertVisibleItems(3 to -itemSize * 3.5f)
+ }
+ }
+
+ private fun TvLazyListState.assertScrollPosition(index: Int, offset: Dp) = with(rule.density) {
+ assertThat(firstVisibleItemIndex).isEqualTo(index)
+ assertThat(firstVisibleItemScrollOffset.toDp().value).isWithin(0.5f).of(offset.value)
+ }
+
+ private fun TvLazyListState.assertLayoutInfoOffsetRange(from: Dp, to: Dp) = with(rule.density) {
+ assertThat(layoutInfo.viewportStartOffset to layoutInfo.viewportEndOffset)
+ .isEqualTo(from.roundToPx() to to.roundToPx())
+ }
+
+ private fun TvLazyListState.assertVisibleItems(vararg expected: Pair<Int, Dp>) =
+ with(rule.density) {
+ assertThat(layoutInfo.visibleItemsInfo.map { it.index to it.offset })
+ .isEqualTo(expected.map { it.first to it.second.roundToPx() })
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun params() = arrayOf(Orientation.Vertical, Orientation.Horizontal)
+ }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListsIndexedTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListsIndexedTest.kt
new file mode 100644
index 0000000..a868e08
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListsIndexedTest.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.list
+
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.requiredWidth
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.PivotOffsets
+import org.junit.Rule
+import org.junit.Test
+
+class LazyListsIndexedTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun lazyColumnShowsIndexedItems() {
+ val items = (1..4).map { it.toString() }
+
+ rule.setContent {
+ TvLazyColumn(
+ Modifier.height(200.dp),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ itemsIndexed(items) { index, item ->
+ Spacer(
+ Modifier.height(101.dp).fillParentMaxWidth()
+ .testTag("$index-$item").focusable()
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0-1")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("1-2")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("2-3")
+ .assertDoesNotExist()
+
+ rule.onNodeWithTag("3-4")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun columnWithIndexesComposedWithCorrectIndexAndItem() {
+ val items = (0..1).map { it.toString() }
+
+ rule.setContent {
+ TvLazyColumn(
+ Modifier.height(200.dp),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ itemsIndexed(items) { index, item ->
+ BasicText(
+ "${index}x$item", Modifier.fillParentMaxWidth().requiredHeight(100.dp)
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithText("0x0")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithText("1x1")
+ .assertTopPositionInRootIsEqualTo(100.dp)
+ }
+
+ @Test
+ fun lazyRowShowsIndexedItems() {
+ val items = (1..4).map { it.toString() }
+
+ rule.setContent {
+ TvLazyRow(
+ Modifier.width(200.dp),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ itemsIndexed(items) { index, item ->
+ Spacer(
+ Modifier.width(101.dp).fillParentMaxHeight()
+ .testTag("$index-$item").focusable()
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0-1")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("1-2")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("2-3")
+ .assertDoesNotExist()
+
+ rule.onNodeWithTag("3-4")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun rowWithIndexesComposedWithCorrectIndexAndItem() {
+ val items = (0..1).map { it.toString() }
+
+ rule.setContent {
+ TvLazyRow(
+ Modifier.width(200.dp),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ itemsIndexed(items) { index, item ->
+ BasicText(
+ "${index}x$item",
+ Modifier.fillParentMaxHeight().requiredWidth(100.dp).focusable()
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithText("0x0")
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithText("1x1")
+ .assertLeftPositionInRootIsEqualTo(100.dp)
+ }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListsReverseLayoutTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListsReverseLayoutTest.kt
new file mode 100644
index 0000000..1798212
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyListsReverseLayoutTest.kt
@@ -0,0 +1,516 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.list
+
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.PivotOffsets
+import androidx.tv.foundation.lazy.grid.keyPress
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class LazyListsReverseLayoutTest {
+
+ private val ContainerTag = "ContainerTag"
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ private var itemSize: Dp = Dp.Infinity
+
+ @Before
+ fun before() {
+ with(rule.density) {
+ itemSize = 50.toDp()
+ }
+ }
+
+ @Test
+ fun column_emitTwoElementsAsOneItem_positionedReversed() {
+ rule.setContentWithTestViewConfiguration {
+ TvLazyColumn(
+ reverseLayout = true,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ item {
+ Box(Modifier.requiredSize(itemSize).testTag("0").focusable())
+ Box(Modifier.requiredSize(itemSize).testTag("1").focusable())
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun column_emitTwoItems_positionedReversed() {
+ rule.setContentWithTestViewConfiguration {
+ TvLazyColumn(
+ reverseLayout = true,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ item {
+ Box(Modifier.requiredSize(itemSize).testTag("0").focusable())
+ }
+ item {
+ Box(Modifier.requiredSize(itemSize).testTag("1").focusable())
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun column_initialScrollPositionIs0() {
+ lateinit var state: TvLazyListState
+ rule.setContentWithTestViewConfiguration {
+ TvLazyColumn(
+ reverseLayout = true,
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items((0..2).toList()) {
+ Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ }
+ }
+
+ @Test
+ fun column_scrollInWrongDirectionDoesNothing() {
+ lateinit var state: TvLazyListState
+ rule.setContentWithTestViewConfiguration {
+ TvLazyColumn(
+ reverseLayout = true,
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items((0..2).toList()) {
+ Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
+ }
+ }
+ }
+
+ // we scroll down and as the scrolling is reversed it shouldn't affect anything
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ }
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun column_scrollForwardHalfWay() {
+ lateinit var state: TvLazyListState
+ rule.setContentWithTestViewConfiguration {
+ TvLazyColumn(
+ reverseLayout = true,
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
+ pivotOffsets = PivotOffsets(parentFraction = 0.3f)
+ ) {
+ items((0..2).toList()) {
+ Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
+ }
+ }
+ }
+
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 3)
+
+ val scrolled = rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+ with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
+ }
+
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(-itemSize + scrolled)
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(scrolled)
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(itemSize + scrolled)
+ }
+
+ @Test
+ fun column_scrollForwardTillTheEnd() {
+ lateinit var state: TvLazyListState
+ rule.setContentWithTestViewConfiguration {
+ TvLazyColumn(
+ reverseLayout = true,
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items((0..3).toList()) {
+ Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
+ }
+ }
+ }
+
+ // we scroll a bit more than it is possible just to make sure we would stop correctly
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 6)
+
+ rule.runOnIdle {
+ with(rule.density) {
+ val realOffset = state.firstVisibleItemScrollOffset.toDp() +
+ itemSize * state.firstVisibleItemIndex
+ assertThat(realOffset).isEqualTo(itemSize * 2)
+ }
+ }
+
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun row_emitTwoElementsAsOneItem_positionedReversed() {
+ rule.setContentWithTestViewConfiguration {
+ TvLazyRow(
+ reverseLayout = true,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ item {
+ Box(Modifier.requiredSize(itemSize).testTag("0").focusable())
+ Box(Modifier.requiredSize(itemSize).testTag("1").focusable())
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("0")
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun row_emitTwoItems_positionedReversed() {
+ rule.setContentWithTestViewConfiguration {
+ TvLazyRow(
+ reverseLayout = true,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ item {
+ Box(Modifier.requiredSize(itemSize).testTag("0").focusable())
+ }
+ item {
+ Box(Modifier.requiredSize(itemSize).testTag("1"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("0")
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun row_initialScrollPositionIs0() {
+ lateinit var state: TvLazyListState
+ rule.setContentWithTestViewConfiguration {
+ TvLazyRow(
+ reverseLayout = true,
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items((0..2).toList()) {
+ Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ }
+ }
+
+ @Test
+ fun row_scrollInWrongDirectionDoesNothing() {
+ lateinit var state: TvLazyListState
+ rule.setContentWithTestViewConfiguration {
+ TvLazyRow(
+ reverseLayout = true,
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items((0..2).toList()) {
+ Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
+ }
+ }
+ }
+
+ // we scroll down and as the scrolling is reversed it shouldn't affect anything
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT, 2)
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ }
+
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("0")
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun row_scrollForwardHalfWay() {
+ lateinit var state: TvLazyListState
+ rule.setContentWithTestViewConfiguration {
+ TvLazyRow(
+ reverseLayout = true,
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
+ pivotOffsets = PivotOffsets(parentFraction = 0.3f)
+ ) {
+ items((0..2).toList()) {
+ Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
+ }
+ }
+ }
+
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT, 3)
+
+ val scrolled = rule.runOnIdle {
+ assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
+ }
+
+ rule.onNodeWithTag("2")
+ .assertLeftPositionInRootIsEqualTo(-itemSize + scrolled)
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(scrolled)
+ rule.onNodeWithTag("0")
+ .assertLeftPositionInRootIsEqualTo(itemSize + scrolled)
+ }
+
+ @Test
+ fun row_scrollForwardTillTheEnd() {
+ lateinit var state: TvLazyListState
+ rule.setContentWithTestViewConfiguration {
+ TvLazyRow(
+ reverseLayout = true,
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items((0..3).toList()) {
+ Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
+ }
+ }
+ }
+
+ // we scroll a bit more than it is possible just to make sure we would stop correctly
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT, 6)
+ rule.runOnIdle {
+ with(rule.density) {
+ val realOffset = state.firstVisibleItemScrollOffset.toDp() +
+ itemSize * state.firstVisibleItemIndex
+ assertThat(realOffset).isEqualTo(itemSize * 2)
+ }
+ }
+
+ rule.onNodeWithTag("3")
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("2")
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun row_rtl_emitTwoElementsAsOneItem_positionedReversed() {
+ rule.setContentWithTestViewConfiguration {
+ CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+ TvLazyRow(
+ reverseLayout = true,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ item {
+ Box(Modifier.requiredSize(itemSize).testTag("0").focusable())
+ Box(Modifier.requiredSize(itemSize).testTag("1").focusable())
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ rule.onNodeWithTag("0")
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ }
+
+ @Test
+ fun row_rtl_emitTwoItems_positionedReversed() {
+ rule.setContentWithTestViewConfiguration {
+ CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+ TvLazyRow(
+ reverseLayout = true,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ item {
+ Box(Modifier.requiredSize(itemSize).testTag("0").focusable())
+ }
+ item {
+ Box(Modifier.requiredSize(itemSize).testTag("1").focusable())
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ rule.onNodeWithTag("0")
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ }
+
+ @Test
+ fun row_rtl_scrollForwardHalfWay() {
+ lateinit var state: TvLazyListState
+ rule.setContentWithTestViewConfiguration {
+ CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+ TvLazyRow(
+ reverseLayout = true,
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag),
+ pivotOffsets = PivotOffsets(parentFraction = 0.3f)
+ ) {
+ items((0..2).toList()) {
+ Box(Modifier.requiredSize(itemSize).testTag("$it").focusable())
+ }
+ }
+ }
+ }
+
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT, 3)
+
+ val scrolled = rule.runOnIdle {
+ assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertLeftPositionInRootIsEqualTo(-scrolled)
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(itemSize - scrolled)
+ rule.onNodeWithTag("2")
+ .assertLeftPositionInRootIsEqualTo(itemSize * 2 - scrolled)
+ }
+
+ @Test
+ fun column_whenParameterChanges() {
+ var reverse by mutableStateOf(true)
+ rule.setContentWithTestViewConfiguration {
+ TvLazyColumn(
+ reverseLayout = reverse,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ item {
+ Box(Modifier.requiredSize(itemSize).testTag("0").focusable())
+ Box(Modifier.requiredSize(itemSize).testTag("1").focusable())
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+
+ rule.runOnIdle {
+ reverse = false
+ }
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun row_whenParameterChanges() {
+ var reverse by mutableStateOf(true)
+ rule.setContentWithTestViewConfiguration {
+ TvLazyRow(
+ reverseLayout = reverse,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ item {
+ Box(Modifier.requiredSize(itemSize).testTag("0").focusable())
+ Box(Modifier.requiredSize(itemSize).testTag("1").focusable())
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("0")
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+
+ rule.runOnIdle {
+ reverse = false
+ }
+
+ rule.onNodeWithTag("0")
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyNestedScrollingTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyNestedScrollingTest.kt
new file mode 100644
index 0000000..c79c0f8
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyNestedScrollingTest.kt
@@ -0,0 +1,382 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.list
+
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.gestures.scrollable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.tv.foundation.PivotOffsets
+import androidx.tv.foundation.lazy.grid.keyPress
+import com.google.common.truth.Truth
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class LazyNestedScrollingTest {
+ private val LazyTag = "LazyTag"
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ private val expectedDragOffset = 20f
+ private val dragOffsetWithTouchSlop = expectedDragOffset + TestTouchSlop
+
+ @Test
+ fun column_nestedScrollingBackwardInitially() = runBlocking {
+ val items = (1..3).toList()
+ var draggedOffset = 0f
+ val scrollable = ScrollableState {
+ draggedOffset += it
+ it
+ }
+ rule.setContentWithTestViewConfiguration {
+ Box(
+ Modifier.scrollable(
+ orientation = Orientation.Vertical,
+ state = scrollable
+ )
+ ) {
+ TvLazyColumn(
+ Modifier.requiredSize(100.dp).testTag(LazyTag),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(items) {
+ Box(Modifier.requiredSize(50.dp).testTag("$it").focusable())
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyTag)
+ .performTouchInput {
+ down(Offset(x = 10f, y = 10f))
+ moveBy(Offset(x = 0f, y = 100f + TestTouchSlop))
+ up()
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(draggedOffset).isEqualTo(100f)
+ }
+ }
+
+ @Test
+ fun column_nestedScrollingBackwardOnceWeScrolledForwardPreviously() = runBlocking {
+ val items = (1..3).toList()
+ var draggedOffset = 0f
+ val scrollable = ScrollableState {
+ draggedOffset += it
+ it
+ }
+ rule.setContentWithTestViewConfiguration {
+ Box(
+ Modifier.scrollable(
+ orientation = Orientation.Vertical,
+ state = scrollable
+ )
+ ) {
+ TvLazyColumn(
+ modifier = Modifier.requiredSize(100.dp).testTag(LazyTag),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(items) {
+ Box(Modifier.requiredSize(50.dp).testTag("$it").focusable())
+ }
+ }
+ }
+ }
+
+ // scroll forward
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 3)
+
+ // scroll back so we again on 0 position
+ // we scroll one extra dp to prevent rounding issues
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 2)
+
+ rule.onNodeWithTag(LazyTag)
+ .performTouchInput {
+ draggedOffset = 0f
+ down(Offset(x = 10f, y = 10f))
+ moveBy(Offset(x = 0f, y = dragOffsetWithTouchSlop))
+ up()
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(draggedOffset).isEqualTo(expectedDragOffset)
+ }
+ }
+
+ @Test
+ fun column_nestedScrollingForwardWhenTheFullContentIsInitiallyVisible() = runBlocking {
+ val items = (1..2).toList()
+ var draggedOffset = 0f
+ val scrollable = ScrollableState {
+ draggedOffset += it
+ it
+ }
+ rule.setContentWithTestViewConfiguration {
+ Box(
+ Modifier.scrollable(
+ orientation = Orientation.Vertical,
+ state = scrollable
+ )
+ ) {
+ TvLazyColumn(
+ Modifier.requiredSize(100.dp).testTag(LazyTag),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(items) {
+ Box(Modifier.requiredSize(40.dp).testTag("$it").focusable())
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyTag)
+ .performTouchInput {
+ down(Offset(x = 10f, y = 10f))
+ moveBy(Offset(x = 0f, y = -dragOffsetWithTouchSlop))
+ up()
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset)
+ }
+ }
+
+ @Test
+ fun column_nestedScrollingForwardWhenScrolledToTheEnd() = runBlocking {
+ val items = (1..3).toList()
+ var draggedOffset = 0f
+ val scrollable = ScrollableState {
+ draggedOffset += it
+ it
+ }
+ rule.setContentWithTestViewConfiguration {
+ Box(
+ Modifier.scrollable(
+ orientation = Orientation.Vertical,
+ state = scrollable
+ )
+ ) {
+ TvLazyColumn(
+ modifier = Modifier.requiredSize(100.dp).testTag(LazyTag),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(items) {
+ Box(Modifier.requiredSize(50.dp).testTag("$it").focusable())
+ }
+ }
+ }
+ }
+
+ // scroll till the end
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 3)
+
+ rule.onNodeWithTag(LazyTag)
+ .performTouchInput {
+ draggedOffset = 0f
+ down(Offset(x = 10f, y = 10f))
+ moveBy(Offset(x = 0f, y = -dragOffsetWithTouchSlop))
+ up()
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset)
+ }
+ }
+
+ @Test
+ fun row_nestedScrollingBackwardInitially() = runBlocking {
+ val items = (1..3).toList()
+ var draggedOffset = 0f
+ val scrollable = ScrollableState {
+ draggedOffset += it
+ it
+ }
+ rule.setContentWithTestViewConfiguration {
+ Box(
+ Modifier.scrollable(
+ orientation = Orientation.Horizontal,
+ state = scrollable
+ )
+ ) {
+ TvLazyRow(
+ modifier = Modifier.requiredSize(100.dp).testTag(LazyTag),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(items) {
+ Spacer(Modifier.requiredSize(50.dp).testTag("$it").focusable())
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyTag)
+ .performTouchInput {
+ down(Offset(x = 10f, y = 10f))
+ moveBy(Offset(x = dragOffsetWithTouchSlop, y = 0f))
+ up()
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(draggedOffset).isEqualTo(expectedDragOffset)
+ }
+ }
+
+ @Test
+ fun row_nestedScrollingBackwardOnceWeScrolledForwardPreviously() = runBlocking {
+ val items = (1..3).toList()
+ var draggedOffset = 0f
+ val scrollable = ScrollableState {
+ draggedOffset += it
+ it
+ }
+ rule.setContentWithTestViewConfiguration {
+ Box(
+ Modifier.scrollable(
+ orientation = Orientation.Horizontal,
+ state = scrollable
+ )
+ ) {
+ TvLazyRow(
+ modifier = Modifier.requiredSize(100.dp).testTag(LazyTag),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(items) {
+ Spacer(Modifier.requiredSize(50.dp).testTag("$it").focusable())
+ }
+ }
+ }
+ }
+
+ // scroll forward
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT, 2)
+
+ // scroll back so we again on 0 position
+ // we scroll one extra dp to prevent rounding issues
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT, 2)
+
+ rule.onNodeWithTag(LazyTag)
+ .performTouchInput {
+ draggedOffset = 0f
+ down(Offset(x = 10f, y = 10f))
+ moveBy(Offset(x = dragOffsetWithTouchSlop, y = 0f))
+ up()
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(draggedOffset).isEqualTo(expectedDragOffset)
+ }
+ }
+
+ @Test
+ fun row_nestedScrollingForwardWhenTheFullContentIsInitiallyVisible() = runBlocking {
+ val items = (1..2).toList()
+ var draggedOffset = 0f
+ val scrollable = ScrollableState {
+ draggedOffset += it
+ it
+ }
+ rule.setContentWithTestViewConfiguration {
+ Box(
+ Modifier.scrollable(
+ orientation = Orientation.Horizontal,
+ state = scrollable
+ )
+ ) {
+ TvLazyRow(
+ modifier = Modifier.requiredSize(100.dp).testTag(LazyTag),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(items) {
+ Spacer(Modifier.requiredSize(40.dp).testTag("$it").focusable())
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyTag)
+ .performTouchInput {
+ down(Offset(x = 10f, y = 10f))
+ moveBy(Offset(x = -dragOffsetWithTouchSlop, y = 0f))
+ up()
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset)
+ }
+ }
+
+ @Test
+ fun row_nestedScrollingForwardWhenScrolledToTheEnd() = runBlocking {
+ val items = (1..3).toList()
+ var draggedOffset = 0f
+ val scrollable = ScrollableState {
+ draggedOffset += it
+ it
+ }
+ rule.setContentWithTestViewConfiguration {
+ Box(
+ Modifier.scrollable(
+ orientation = Orientation.Horizontal,
+ state = scrollable
+ )
+ ) {
+ TvLazyRow(
+ modifier = Modifier.requiredSize(100.dp).testTag(LazyTag),
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(items) {
+ Spacer(Modifier.requiredSize(50.dp).testTag("$it").focusable())
+ }
+ }
+ }
+ }
+
+ // scroll till the end
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT, 3)
+
+ rule.onNodeWithTag(LazyTag)
+ .performTouchInput {
+ draggedOffset = 0f
+ down(Offset(x = 10f, y = 10f))
+ moveBy(Offset(x = -dragOffsetWithTouchSlop, y = 0f))
+ up()
+ }
+
+ rule.runOnIdle {
+ Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset)
+ }
+ }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyRowTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyRowTest.kt
new file mode 100644
index 0000000..13bfd51
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyRowTest.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.list
+
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.tv.foundation.PivotOffsets
+import androidx.tv.foundation.lazy.grid.keyPress
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class LazyRowTest {
+ private val LazyListTag = "LazyListTag"
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ private val firstItemTag = "firstItemTag"
+ private val secondItemTag = "secondItemTag"
+
+ private fun prepareLazyRowForAlignment(verticalGravity: Alignment.Vertical) {
+ rule.setContentWithTestViewConfiguration {
+ TvLazyRow(
+ Modifier.testTag(LazyListTag).requiredHeight(100.dp),
+ verticalAlignment = verticalGravity,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(listOf(1, 2)) {
+ if (it == 1) {
+ Box(Modifier.size(50.dp).testTag(firstItemTag).focusable())
+ } else {
+ Box(Modifier.size(70.dp).testTag(secondItemTag).focusable())
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag(secondItemTag)
+ .assertIsDisplayed()
+
+ val lazyRowBounds = rule.onNodeWithTag(LazyListTag)
+ .getUnclippedBoundsInRoot()
+
+ with(rule.density) {
+ // Verify the height of the row
+ assertThat(lazyRowBounds.top.roundToPx()).isWithin1PixelFrom(0.dp.roundToPx())
+ assertThat(lazyRowBounds.bottom.roundToPx()).isWithin1PixelFrom(100.dp.roundToPx())
+ }
+ }
+
+ @Test
+ fun lazyRowAlignmentCenterVertically() {
+ prepareLazyRowForAlignment(Alignment.CenterVertically)
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertPositionInRootIsEqualTo(0.dp, 25.dp)
+
+ rule.onNodeWithTag(secondItemTag)
+ .assertPositionInRootIsEqualTo(50.dp, 15.dp)
+ }
+
+ @Test
+ fun lazyRowAlignmentTop() {
+ prepareLazyRowForAlignment(Alignment.Top)
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+
+ rule.onNodeWithTag(secondItemTag)
+ .assertPositionInRootIsEqualTo(50.dp, 0.dp)
+ }
+
+ @Test
+ fun lazyRowAlignmentBottom() {
+ prepareLazyRowForAlignment(Alignment.Bottom)
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertPositionInRootIsEqualTo(0.dp, 50.dp)
+
+ rule.onNodeWithTag(secondItemTag)
+ .assertPositionInRootIsEqualTo(50.dp, 30.dp)
+ }
+
+ @Test
+ fun scrollsLeftInRtl() {
+ lateinit var state: TvLazyListState
+ rule.setContentWithTestViewConfiguration {
+ CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+ Box(Modifier.width(100.dp)) {
+ state = rememberLazyListState()
+ TvLazyRow(
+ Modifier.testTag(LazyListTag),
+ state,
+ pivotOffsets =
+ PivotOffsets(parentFraction = 0f)
+ ) {
+ items(4) {
+ Box(
+ Modifier.width(101.dp).fillParentMaxHeight().testTag("$it")
+ .focusable()
+ )
+ }
+ }
+ }
+ }
+ }
+
+ rule.keyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT, 3)
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+ assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+ }
+ }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyScrollAccessibilityTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyScrollAccessibilityTest.kt
new file mode 100644
index 0000000..53c9775
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyScrollAccessibilityTest.kt
@@ -0,0 +1,364 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.list
+
+import android.R.id.accessibilityActionScrollDown
+import android.R.id.accessibilityActionScrollLeft
+import android.R.id.accessibilityActionScrollRight
+import android.R.id.accessibilityActionScrollUp
+import android.view.View
+import android.view.accessibility.AccessibilityNodeProvider
+import androidx.activity.ComponentActivity
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsNode
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.core.view.ViewCompat
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD
+import androidx.test.filters.MediumTest
+import androidx.tv.foundation.PivotOffsets
+import com.google.common.truth.IterableSubject
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@MediumTest
+@RunWith(Parameterized::class)
+class LazyScrollAccessibilityTest(private val config: TestConfig) {
+ data class TestConfig(
+ val horizontal: Boolean,
+ val rtl: Boolean,
+ val reversed: Boolean
+ ) {
+ val vertical = !horizontal
+
+ override fun toString(): String {
+ return (if (horizontal) "horizontal" else "vertical") +
+ (if (rtl) ",rtl" else ",ltr") +
+ (if (reversed) ",reversed" else "")
+ }
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun params() =
+ listOf(true, false).flatMap { horizontal ->
+ listOf(false, true).flatMap { rtl ->
+ listOf(false, true).map { reversed ->
+ TestConfig(horizontal, rtl, reversed)
+ }
+ }
+ }
+ }
+
+ @get:Rule
+ val rule = createAndroidComposeRule<ComponentActivity>()
+
+ private val scrollerTag = "ScrollerTest"
+ private var composeView: View? = null
+ private val accessibilityNodeProvider: AccessibilityNodeProvider
+ get() = checkNotNull(composeView) {
+ "composeView not initialized. Did `composeView = LocalView.current` not work?"
+ }.let { composeView ->
+ ViewCompat
+ .getAccessibilityDelegate(composeView)!!
+ .getAccessibilityNodeProvider(composeView)!!
+ .provider as AccessibilityNodeProvider
+ }
+
+ @Test
+ fun scrollForward() {
+ testRelativeDirection(58, ACTION_SCROLL_FORWARD)
+ }
+
+ @Test
+ fun scrollBackward() {
+ testRelativeDirection(41, ACTION_SCROLL_BACKWARD)
+ }
+
+ @Test
+ fun scrollRight() {
+ testAbsoluteDirection(58, accessibilityActionScrollRight, config.horizontal)
+ }
+
+ @Test
+ fun scrollLeft() {
+ testAbsoluteDirection(41, accessibilityActionScrollLeft, config.horizontal)
+ }
+
+ @Test
+ fun scrollDown() {
+ testAbsoluteDirection(58, accessibilityActionScrollDown, config.vertical)
+ }
+
+ @Test
+ fun scrollUp() {
+ testAbsoluteDirection(41, accessibilityActionScrollUp, config.vertical)
+ }
+
+ @Test
+ fun verifyScrollActionsAtStart() {
+ createScrollableContent_StartAtStart()
+ verifyNodeInfoScrollActions(
+ expectForward = !config.reversed,
+ expectBackward = config.reversed
+ )
+ }
+
+ @Test
+ fun verifyScrollActionsInMiddle() {
+ createScrollableContent_StartInMiddle()
+ verifyNodeInfoScrollActions(
+ expectForward = true,
+ expectBackward = true
+ )
+ }
+
+ @Test
+ fun verifyScrollActionsAtEnd() {
+ createScrollableContent_StartAtEnd()
+ verifyNodeInfoScrollActions(
+ expectForward = config.reversed,
+ expectBackward = !config.reversed
+ )
+ }
+
+ /**
+ * Setup the test, run the given [accessibilityAction], and check if the [canonicalTarget]
+ * has been reached. The canonical target is the item that we expect to see when moving
+ * forward in a non-reversed scrollable (e.g. down in LazyColumn or right in LazyRow in LTR).
+ * The actual target is either the canonical target or the target that is as far from the
+ * middle of the lazy list as the canonical target, but on the other side of the middle,
+ * depending on the [configuration][config].
+ */
+ private fun testRelativeDirection(canonicalTarget: Int, accessibilityAction: Int) {
+ val target = if (!config.reversed) canonicalTarget else 100 - canonicalTarget - 1
+ testScrollAction(target, accessibilityAction)
+ }
+
+ /**
+ * Setup the test, run the given [accessibilityAction], and check if the [canonicalTarget]
+ * has been reached (but only if we [expect][expectActionSuccess] the action to succeed).
+ * The canonical target is the item that we expect to see when moving forward in a
+ * non-reversed scrollable (e.g. down in LazyColumn or right in LazyRow in LTR). The actual
+ * target is either the canonical target or the target that is as far from the middle of the
+ * scrollable as the canonical target, but on the other side of the middle, depending on the
+ * [configuration][config].
+ */
+ private fun testAbsoluteDirection(
+ canonicalTarget: Int,
+ accessibilityAction: Int,
+ expectActionSuccess: Boolean
+ ) {
+ var target = canonicalTarget
+ if (config.horizontal && config.rtl) {
+ target = 100 - target - 1
+ }
+ if (config.reversed) {
+ target = 100 - target - 1
+ }
+ testScrollAction(target, accessibilityAction, expectActionSuccess)
+ }
+
+ /**
+ * Setup the test, run the given [accessibilityAction], and check if the [target] has been
+ * reached (but only if we [expect][expectActionSuccess] the action to succeed).
+ */
+ private fun testScrollAction(
+ target: Int,
+ accessibilityAction: Int,
+ expectActionSuccess: Boolean = true
+ ) {
+ createScrollableContent_StartInMiddle()
+ rule.onNodeWithText("$target").assertDoesNotExist()
+
+ val returnValue = rule.onNodeWithTag(scrollerTag).withSemanticsNode {
+ accessibilityNodeProvider.performAction(id, accessibilityAction, null)
+ }
+
+ assertThat(returnValue).isEqualTo(expectActionSuccess)
+ if (expectActionSuccess) {
+ rule.onNodeWithText("$target").assertIsDisplayed()
+ } else {
+ rule.onNodeWithText("$target").assertDoesNotExist()
+ }
+ }
+
+ /**
+ * Checks if all of the scroll actions are present or not according to what we expect based on
+ * [expectForward] and [expectBackward]. The scroll actions that are checked are forward,
+ * backward, left, right, up and down. The expectation parameters must already account for
+ * [reversing][TestConfig.reversed].
+ */
+ private fun verifyNodeInfoScrollActions(expectForward: Boolean, expectBackward: Boolean) {
+ val nodeInfo = rule.onNodeWithTag(scrollerTag).withSemanticsNode {
+ rule.runOnUiThread {
+ accessibilityNodeProvider.createAccessibilityNodeInfo(id)
+ }
+ }
+
+ val actions = nodeInfo.actionList.map { it.id }
+
+ assertThat(actions).contains(expectForward, ACTION_SCROLL_FORWARD)
+ assertThat(actions).contains(expectBackward, ACTION_SCROLL_BACKWARD)
+
+ if (config.horizontal) {
+ val expectLeft = if (config.rtl) expectForward else expectBackward
+ val expectRight = if (config.rtl) expectBackward else expectForward
+ assertThat(actions).contains(expectLeft, accessibilityActionScrollLeft)
+ assertThat(actions).contains(expectRight, accessibilityActionScrollRight)
+ assertThat(actions).contains(false, accessibilityActionScrollDown)
+ assertThat(actions).contains(false, accessibilityActionScrollUp)
+ } else {
+ assertThat(actions).contains(false, accessibilityActionScrollLeft)
+ assertThat(actions).contains(false, accessibilityActionScrollRight)
+ assertThat(actions).contains(expectForward, accessibilityActionScrollDown)
+ assertThat(actions).contains(expectBackward, accessibilityActionScrollUp)
+ }
+ }
+
+ private fun IterableSubject.contains(expectPresent: Boolean, element: Any) {
+ if (expectPresent) {
+ contains(element)
+ } else {
+ doesNotContain(element)
+ }
+ }
+
+ /**
+ * Creates a Row/Column that starts at the first item, according to [createScrollableContent]
+ */
+ private fun createScrollableContent_StartAtStart() {
+ createScrollableContent {
+ // Start at the start:
+ // -> pretty basic
+ rememberLazyListState(0, 0)
+ }
+ }
+
+ /**
+ * Creates a Row/Column that starts in the middle, according to [createScrollableContent]
+ */
+ private fun createScrollableContent_StartInMiddle() {
+ createScrollableContent {
+ // Start at the middle:
+ // Content size: 100 items * 21dp per item = 2100dp
+ // Viewport size: 200dp rect - 50dp padding on both sides = 100dp
+ // Content outside viewport: 2100dp - 100dp = 2000dp
+ // -> centered when 1000dp on either side, which is 47 items + 13dp
+ rememberLazyListState(
+ 47,
+ with(LocalDensity.current) { 13.dp.roundToPx() }
+ )
+ }
+ }
+
+ /**
+ * Creates a Row/Column that starts at the last item, according to [createScrollableContent]
+ */
+ private fun createScrollableContent_StartAtEnd() {
+ createScrollableContent {
+ // Start at the end:
+ // Content size: 100 items * 21dp per item = 2100dp
+ // Viewport size: 200dp rect - 50dp padding on both sides = 100dp
+ // Content outside viewport: 2100dp - 100dp = 2000dp
+ // -> at the end when offset at 2000dp, which is 95 items + 5dp
+ rememberLazyListState(
+ 95,
+ with(LocalDensity.current) { 5.dp.roundToPx() }
+ )
+ }
+ }
+
+ /**
+ * Creates a Row/Column with a viewport of 100.dp, containing 100 items each 17.dp in size.
+ * The items have a text with their index (ASC), and where the viewport starts is determined
+ * by the given [lambda][rememberLazyListState]. All properties from [config] are applied.
+ * The viewport has padding around it to make sure scroll distance doesn't include padding.
+ */
+ private fun createScrollableContent(rememberLazyListState: @Composable () -> TvLazyListState) {
+ rule.setContent {
+ composeView = LocalView.current
+ val lazyContent: TvLazyListScope.() -> Unit = {
+ items(100) {
+ Box(Modifier.requiredSize(21.dp).background(Color.Yellow)) {
+ BasicText("$it", Modifier.align(Alignment.Center))
+ }
+ }
+ }
+
+ val state = rememberLazyListState()
+
+ Box(Modifier.requiredSize(200.dp).background(Color.White)) {
+ val direction = if (config.rtl) LayoutDirection.Rtl else LayoutDirection.Ltr
+ CompositionLocalProvider(LocalLayoutDirection provides direction) {
+ if (config.horizontal) {
+ TvLazyRow(
+ Modifier.testTag(scrollerTag).matchParentSize(),
+ state = state,
+ contentPadding = PaddingValues(50.dp),
+ reverseLayout = config.reversed,
+ verticalAlignment = Alignment.CenterVertically,
+ pivotOffsets =
+ PivotOffsets(parentFraction = 0f)
+ ) {
+ lazyContent()
+ }
+ } else {
+ TvLazyColumn(
+ Modifier.testTag(scrollerTag).matchParentSize(),
+ state = state,
+ contentPadding = PaddingValues(50.dp),
+ reverseLayout = config.reversed,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ pivotOffsets =
+ PivotOffsets(parentFraction = 0f)
+ ) {
+ lazyContent()
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun <T> SemanticsNodeInteraction.withSemanticsNode(block: SemanticsNode.() -> T): T {
+ return block.invoke(fetchSemanticsNode())
+ }
+}
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyScrollTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyScrollTest.kt
new file mode 100644
index 0000000..e8416f6
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazyScrollTest.kt
@@ -0,0 +1,340 @@
+/*
+ * Copyright 2020 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.tv.compose.foundation.lazy.list
+
+import androidx.compose.animation.core.FloatSpringSpec
+import androidx.tv.foundation.lazy.AutoTestFrameClock
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.test.filters.MediumTest
+import androidx.tv.foundation.PivotOffsets
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import java.util.concurrent.TimeUnit
+import kotlin.math.roundToInt
+
+@MediumTest
+@RunWith(Parameterized::class)
+class LazyScrollTest(private val orientation: Orientation) {
+ @get:Rule
+ val rule = createComposeRule()
+
+ private val vertical: Boolean
+ get() = orientation == Orientation.Vertical
+
+ private val itemsCount = 20
+ private lateinit var state: TvLazyListState
+
+ private val itemSizePx = 100
+ private var itemSizeDp = Dp.Unspecified
+ private var containerSizeDp = Dp.Unspecified
+
+ lateinit var scope: CoroutineScope
+
+ @Before
+ fun setup() {
+ with(rule.density) {
+ itemSizeDp = itemSizePx.toDp()
+ containerSizeDp = itemSizeDp * 3
+ }
+ rule.setContent {
+ state = rememberLazyListState()
+ scope = rememberCoroutineScope()
+ TestContent()
+ }
+ }
+
+ @Test
+ fun setupWorks() {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ }
+
+ @Test
+ fun scrollToItem() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.scrollToItem(3)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(3)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ }
+
+ @Test
+ fun scrollToItemWithOffset() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.scrollToItem(3, 10)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(3)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+ }
+
+ @Test
+ fun scrollToItemWithNegativeOffset() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.scrollToItem(3, -10)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(2)
+ val item3Offset = state.layoutInfo.visibleItemsInfo.first { it.index == 3 }.offset
+ assertThat(item3Offset).isEqualTo(10)
+ }
+
+ @Test
+ fun scrollToItemWithPositiveOffsetLargerThanAvailableSize() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.scrollToItem(itemsCount - 3, 10)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 3)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not 10
+ }
+
+ @Test
+ fun scrollToItemWithNegativeOffsetLargerThanAvailableSize() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.scrollToItem(1, -(itemSizePx + 10))
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not -10
+ }
+
+ @Test
+ fun scrollToItemWithIndexLargerThanItemsCount() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.scrollToItem(itemsCount + 2)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 3)
+ }
+
+ @Test
+ fun animateScrollBy() = runBlocking {
+ val scrollDistance = 320
+
+ val expectedIndex = scrollDistance / itemSizePx // resolves to 3
+ val expectedOffset = scrollDistance % itemSizePx // resolves to 20px
+
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.animateScrollBy(scrollDistance.toFloat())
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(expectedIndex)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(expectedOffset)
+ }
+
+ @Test
+ fun animateScrollToItem() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.animateScrollToItem(5, 10)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(5)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+ }
+
+ @Test
+ fun animateScrollToItemWithOffset() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.animateScrollToItem(3, 10)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(3)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+ }
+
+ @Test
+ fun animateScrollToItemWithNegativeOffset() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.animateScrollToItem(3, -10)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(2)
+ val item3Offset = state.layoutInfo.visibleItemsInfo.first { it.index == 3 }.offset
+ assertThat(item3Offset).isEqualTo(10)
+ }
+
+ @Test
+ fun animateScrollToItemWithPositiveOffsetLargerThanAvailableSize() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.animateScrollToItem(itemsCount - 3, 10)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 3)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not 10
+ }
+
+ @Test
+ fun animateScrollToItemWithNegativeOffsetLargerThanAvailableSize() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.animateScrollToItem(1, -(itemSizePx + 10))
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) // not -10
+ }
+
+ @Test
+ fun animateScrollToItemWithIndexLargerThanItemsCount() = runBlocking {
+ withContext(Dispatchers.Main + AutoTestFrameClock()) {
+ state.animateScrollToItem(itemsCount + 2)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 3)
+ }
+
+ @Test
+ fun animatePerFrameForwardToVisibleItem() {
+ assertSpringAnimation(toIndex = 2)
+ }
+
+ @Test
+ fun animatePerFrameForwardToVisibleItemWithOffset() {
+ assertSpringAnimation(toIndex = 2, toOffset = 35)
+ }
+
+ @Test
+ fun animatePerFrameForwardToNotVisibleItem() {
+ assertSpringAnimation(toIndex = 8)
+ }
+
+ @Test
+ fun animatePerFrameForwardToNotVisibleItemWithOffset() {
+ assertSpringAnimation(toIndex = 10, toOffset = 35)
+ }
+
+ @Test
+ fun animatePerFrameBackward() {
+ assertSpringAnimation(toIndex = 1, fromIndex = 6)
+ }
+
+ @Test
+ fun animatePerFrameBackwardWithOffset() {
+ assertSpringAnimation(toIndex = 1, fromIndex = 5, fromOffset = 58)
+ }
+
+ @Test
+ fun animatePerFrameBackwardWithInitialOffset() {
+ assertSpringAnimation(toIndex = 0, toOffset = 20, fromIndex = 8)
+ }
+
+ private fun assertSpringAnimation(
+ toIndex: Int,
+ toOffset: Int = 0,
+ fromIndex: Int = 0,
+ fromOffset: Int = 0
+ ) {
+ if (fromIndex != 0 || fromOffset != 0) {
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollToItem(fromIndex, fromOffset)
+ }
+ }
+ }
+ rule.waitForIdle()
+
+ assertThat(state.firstVisibleItemIndex).isEqualTo(fromIndex)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(fromOffset)
+
+ rule.mainClock.autoAdvance = false
+
+ scope.launch {
+ state.animateScrollToItem(toIndex, toOffset)
+ }
+
+ while (!state.isScrollInProgress) {
+ Thread.sleep(5)
+ }
+
+ val startOffset = (fromIndex * itemSizePx + fromOffset).toFloat()
+ val endOffset = (toIndex * itemSizePx + toOffset).toFloat()
+ val spec = FloatSpringSpec()
+
+ val duration =
+ TimeUnit.NANOSECONDS.toMillis(spec.getDurationNanos(startOffset, endOffset, 0f))
+ rule.mainClock.advanceTimeByFrame()
+ var expectedTime = rule.mainClock.currentTime
+ for (i in 0..duration step FrameDuration) {
+ val nanosTime = TimeUnit.MILLISECONDS.toNanos(i)
+ val expectedValue =
+ spec.getValueFromNanos(nanosTime, startOffset, endOffset, 0f)
+ val actualValue =
+ (state.firstVisibleItemIndex * itemSizePx + state.firstVisibleItemScrollOffset)
+ assertWithMessage(
+ "On animation frame at $i index=${state.firstVisibleItemIndex} " +
+ "offset=${state.firstVisibleItemScrollOffset} expectedValue=$expectedValue"
+ ).that(actualValue).isEqualTo(expectedValue.roundToInt(), tolerance = 1)
+
+ rule.mainClock.advanceTimeBy(FrameDuration)
+ expectedTime += FrameDuration
+ assertThat(expectedTime).isEqualTo(rule.mainClock.currentTime)
+ rule.waitForIdle()
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(toIndex)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(toOffset)
+ }
+
+ @Composable
+ private fun TestContent() {
+ if (vertical) {
+ TvLazyColumn(
+ Modifier.height(containerSizeDp),
+ state,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(itemsCount) {
+ ItemContent()
+ }
+ }
+ } else {
+ TvLazyRow(
+ Modifier.width(containerSizeDp),
+ state,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(itemsCount) {
+ ItemContent()
+ }
+ }
+ }
+ }
+
+ @Composable
+ private fun ItemContent() {
+ val modifier = if (vertical) {
+ Modifier.height(itemSizeDp)
+ } else {
+ Modifier.width(itemSizeDp)
+ }
+ Spacer(modifier)
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun params() = arrayOf(Orientation.Vertical, Orientation.Horizontal)
+ }
+}
+
+private val FrameDuration = 16L
diff --git a/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazySemanticsTest.kt b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazySemanticsTest.kt
new file mode 100644
index 0000000..2ac1492
--- /dev/null
+++ b/tv/tv-foundation/src/androidAndroidTest/kotlin/androidx/tv/compose/foundation/lazy/list/LazySemanticsTest.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.compose.foundation.lazy.list
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.requiredWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions.ScrollToIndex
+import androidx.compose.ui.semantics.SemanticsProperties.IndexForKey
+import androidx.compose.ui.semantics.getOrNull
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.tv.foundation.PivotOffsets
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Tests the semantics properties defined on a LazyList:
+ * - GetIndexForKey
+ * - ScrollToIndex
+ *
+ * GetIndexForKey:
+ * Create a lazy list, iterate over all indices, verify key of each of them
+ *
+ * ScrollToIndex:
+ * Create a lazy list, scroll to an item off screen, verify shown items
+ *
+ * All tests performed in [runTest], scenarios set up in the test methods.
+ */
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class LazySemanticsTest {
+ private val N = 20
+ private val LazyListTag = "lazy_list"
+ private val LazyListModifier = Modifier.testTag(LazyListTag).requiredSize(100.dp)
+
+ private fun tag(index: Int): String = "tag_$index"
+ private fun key(index: Int): String = "key_$index"
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun itemSemantics_column() {
+ rule.setContent {
+ TvLazyColumn(
+ LazyListModifier,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ repeat(N) {
+ item(key = key(it)) {
+ SpacerInColumn(it)
+ }
+ }
+ }
+ }
+ runTest()
+ }
+
+ @Test
+ fun itemsSemantics_column() {
+ rule.setContent {
+ TvLazyColumn(
+ LazyListModifier,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(items = List(N) { it }, key = { key(it) }) {
+ SpacerInColumn(it)
+ }
+ }
+ }
+ runTest()
+ }
+
+ @Test
+ fun itemSemantics_row() {
+ rule.setContent {
+ TvLazyRow(
+ LazyListModifier,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ repeat(N) {
+ item(key = key(it)) {
+ SpacerInRow(it)
+ }
+ }
+ }
+ }
+ runTest()
+ }
+
+ @Test
+ fun itemsSemantics_row() {
+ rule.setContent {
+ TvLazyRow(
+ LazyListModifier,
+ pivotOffsets = PivotOffsets(parentFraction = 0f)
+ ) {
+ items(items = List(N) { it }, key = { key(it) }) {
+ SpacerInRow(it)
+ }
+ }
+ }
+ runTest()
+ }
+
+ private fun runTest() {
+ checkViewport(firstExpectedItem = 0, lastExpectedItem = 3)
+
+ // Verify IndexForKey
+ rule.onNodeWithTag(LazyListTag).assert(
+ SemanticsMatcher.keyIsDefined(IndexForKey).and(
+ SemanticsMatcher("keys match") { node ->
+ val actualIndex = node.config.getOrNull(IndexForKey)!!
+ (0 until N).all { expectedIndex ->
+ expectedIndex == actualIndex.invoke(key(expectedIndex))
+ }
+ }
+ )
+ )
+
+ // Verify ScrollToIndex
+ rule.onNodeWithTag(LazyListTag).assert(SemanticsMatcher.keyIsDefined(ScrollToIndex))
+
+ invokeScrollToIndex(targetIndex = 10)
+ checkViewport(firstExpectedItem = 10, lastExpectedItem = 13)
+
+ invokeScrollToIndex(targetIndex = N - 1)
+ checkViewport(firstExpectedItem = N - 4, lastExpectedItem = N - 1)
+ }
+
+ private fun invokeScrollToIndex(targetIndex: Int) {
+ val node = rule.onNodeWithTag(LazyListTag)
+ .fetchSemanticsNode("Failed: invoke ScrollToIndex")
+ rule.runOnUiThread {
+ node.config[ScrollToIndex].action!!.invoke(targetIndex)
+ }
+ }
+
+ private fun checkViewport(firstExpectedItem: Int, lastExpectedItem: Int) {
+ if (firstExpectedItem > 0) {
+ rule.onNodeWithTag(tag(firstExpectedItem - 1)).assertDoesNotExist()
+ }
+ (firstExpectedItem..lastExpectedItem).forEach {
+ rule.onNodeWithTag(tag(it)).assertExists()
+ }
+ if (firstExpectedItem < N - 1) {
+ rule.onNodeWithTag(tag(lastExpectedItem + 1)).assertDoesNotExist()
+ }
+ }
+
+ @Composable
+ private fun SpacerInColumn(index: Int) {
+ Spacer(Modifier.testTag(tag(index)).requiredHeight(30.dp).fillMaxWidth())
+ }
+
+ @Composable
+ private fun SpacerInRow(index: Int) {
+ Spacer(Modifier.testTag(tag(index)).requiredWidth(30.dp).fillMaxHeight())
+ }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/MarioScrollable.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/MarioScrollable.kt
new file mode 100644
index 0000000..00904a5
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/MarioScrollable.kt
@@ -0,0 +1,323 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.focusGroup
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.Orientation.Horizontal
+import androidx.compose.foundation.gestures.Orientation.Vertical
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.onFocusedBoundsChanged
+import androidx.compose.foundation.relocation.BringIntoViewResponder
+import androidx.compose.foundation.relocation.bringIntoViewResponder
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.rememberUpdatedState
+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.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.OnPlacedModifier
+import androidx.compose.ui.layout.OnRemeasuredModifier
+import androidx.compose.ui.modifier.ModifierLocalProvider
+import androidx.compose.ui.modifier.modifierLocalOf
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.toSize
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/* Copied from
+ compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/
+ Scrollable.kt and modified */
+
+/**
+ * Configure touch scrolling and flinging for the UI element in a single [Orientation].
+ *
+ * Users should update their state themselves using default [ScrollableState] and its
+ * `consumeScrollDelta` callback or by implementing [ScrollableState] interface manually and reflect
+ * their own state in UI when using this component.
+ *
+ * @param state [ScrollableState] state of the scrollable. Defines how scroll events will be
+ * interpreted by the user land logic and contains useful information about on-going events.
+ * @param orientation orientation of the scrolling
+ * @param pivotOffsets offsets of child element within the parent and starting edge of the child
+ * from the pivot defined by the parentOffset.
+ * @param enabled whether or not scrolling in enabled
+ * @param reverseDirection reverse the direction of the scroll, so top to bottom scroll will
+ * behave like bottom to top and left to right will behave like right to left.
+ * drag events when this scrollable is being dragged.
+ */
+
+@OptIn(ExperimentalFoundationApi::class)
+fun Modifier.marioScrollable(
+ state: ScrollableState,
+ orientation: Orientation,
+ pivotOffsets: PivotOffsets,
+ enabled: Boolean = true,
+ reverseDirection: Boolean = false
+): Modifier = composed(
+ inspectorInfo = debugInspectorInfo {
+ name = "marioScrollable"
+ properties["orientation"] = orientation
+ properties["state"] = state
+ properties["enabled"] = enabled
+ properties["reverseDirection"] = reverseDirection
+ properties["pivotOffsets"] = pivotOffsets
+ },
+ factory = {
+ val coroutineScope = rememberCoroutineScope()
+ val keepFocusedChildInViewModifier =
+ remember(coroutineScope, orientation, state, reverseDirection) {
+ ContentInViewModifier(
+ coroutineScope, orientation, state, reverseDirection, pivotOffsets)
+ }
+
+ Modifier
+ .focusGroup()
+ .then(keepFocusedChildInViewModifier.modifier)
+ .pointerScrollable(
+ orientation,
+ reverseDirection,
+ state,
+ enabled
+ )
+ .then(if (enabled) ModifierLocalScrollableContainerProvider else Modifier)
+ }
+)
+
+@Suppress("ComposableModifierFactory")
+@Composable
+private fun Modifier.pointerScrollable(
+ orientation: Orientation,
+ reverseDirection: Boolean,
+ controller: ScrollableState,
+ enabled: Boolean
+): Modifier {
+ val nestedScrollDispatcher = remember { mutableStateOf(NestedScrollDispatcher()) }
+ val scrollLogic = rememberUpdatedState(
+ ScrollingLogic(
+ orientation,
+ reverseDirection,
+ controller
+ )
+ )
+ val nestedScrollConnection = remember(enabled) {
+ scrollableNestedScrollConnection(scrollLogic, enabled)
+ }
+
+ return this.nestedScroll(nestedScrollConnection, nestedScrollDispatcher.value)
+}
+
+private class ScrollingLogic(
+ val orientation: Orientation,
+ val reverseDirection: Boolean,
+ val scrollableState: ScrollableState,
+) {
+ private fun Float.toOffset(): Offset = when {
+ this == 0f -> Offset.Zero
+ orientation == Horizontal -> Offset(this, 0f)
+ else -> Offset(0f, this)
+ }
+
+ private fun Offset.toFloat(): Float =
+ if (orientation == Horizontal) this.x else this.y
+ private fun Float.reverseIfNeeded(): Float = if (reverseDirection) this * -1 else this
+
+ fun performRawScroll(scroll: Offset): Offset {
+ return if (scrollableState.isScrollInProgress) {
+ Offset.Zero
+ } else {
+ scrollableState.dispatchRawDelta(scroll.toFloat().reverseIfNeeded())
+ .reverseIfNeeded().toOffset()
+ }
+ }
+}
+
+private fun scrollableNestedScrollConnection(
+ scrollLogic: State<ScrollingLogic>,
+ enabled: Boolean
+): NestedScrollConnection = object : NestedScrollConnection {
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset = if (enabled) {
+ scrollLogic.value.performRawScroll(available)
+ } else {
+ Offset.Zero
+ }
+}
+
+/**
+ * Handles any logic related to bringing or keeping content in view, including
+ * [BringIntoViewResponder] and ensuring the focused child stays in view when the scrollable area
+ * is shrunk.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+private class ContentInViewModifier(
+ private val scope: CoroutineScope,
+ private val orientation: Orientation,
+ private val scrollableState: ScrollableState,
+ private val reverseDirection: Boolean,
+ private val pivotOffsets: PivotOffsets
+) : BringIntoViewResponder, OnRemeasuredModifier, OnPlacedModifier {
+ private var focusedChild: LayoutCoordinates? = null
+ private var coordinates: LayoutCoordinates? = null
+ private var oldSize: IntSize? = null
+
+ val modifier: Modifier = this
+ .onFocusedBoundsChanged { focusedChild = it }
+ .bringIntoViewResponder(this)
+
+ override fun onRemeasured(size: IntSize) {
+ val coordinates = coordinates
+ val oldSize = oldSize
+ // We only care when this node becomes smaller than it previously was, so don't care about
+ // the initial measurement.
+ if (oldSize != null && oldSize != size && coordinates?.isAttached == true) {
+ onSizeChanged(coordinates, oldSize)
+ }
+ this.oldSize = size
+ }
+
+ override fun onPlaced(coordinates: LayoutCoordinates) {
+ this.coordinates = coordinates
+ }
+
+ override fun calculateRectForParent(localRect: Rect): Rect {
+ val oldSize = checkNotNull(oldSize) {
+ "Expected BringIntoViewRequester to not be used before parents are placed."
+ }
+ // oldSize will only be null before the initial measurement.
+ return computeDestination(localRect, oldSize, pivotOffsets)
+ }
+
+ override suspend fun bringChildIntoView(localRect: Rect) {
+ performBringIntoView(localRect, calculateRectForParent(localRect))
+ }
+
+ private fun onSizeChanged(coordinates: LayoutCoordinates, oldSize: IntSize) {
+ val containerShrunk = if (orientation == Horizontal) {
+ coordinates.size.width < oldSize.width
+ } else {
+ coordinates.size.height < oldSize.height
+ }
+ // If the container is growing, then if the focused child is only partially visible it will
+ // soon be _more_ visible, so don't scroll.
+ if (!containerShrunk) return
+
+ val focusedBounds = focusedChild
+ ?.let { coordinates.localBoundingBoxOf(it, clipBounds = false) }
+ ?: return
+ val myOldBounds = Rect(Offset.Zero, oldSize.toSize())
+ val adjustedBounds = computeDestination(focusedBounds, coordinates.size, pivotOffsets)
+ val wasVisible = myOldBounds.overlaps(focusedBounds)
+ val isFocusedChildClipped = adjustedBounds != focusedBounds
+
+ if (wasVisible && isFocusedChildClipped) {
+ scope.launch {
+ performBringIntoView(focusedBounds, adjustedBounds)
+ }
+ }
+ }
+
+ /**
+ * Compute the destination given the source rectangle and current bounds.
+ *
+ * @param source The bounding box of the item that sent the request to be brought into view.
+ * @param pivotOffsets offsets of child element within the parent and starting edge of the child
+ * from the pivot defined by the parentOffset.
+ * @return the destination rectangle.
+ */
+ private fun computeDestination(
+ source: Rect,
+ intSize: IntSize,
+ pivotOffsets: PivotOffsets
+ ): Rect {
+ val size = intSize.toSize()
+ return when (orientation) {
+ Vertical ->
+ source.translate(
+ 0f,
+ relocationDistance(source.top, source.bottom, size.height, pivotOffsets))
+ Horizontal ->
+ source.translate(
+ relocationDistance(source.left, source.right, size.width, pivotOffsets),
+ 0f)
+ }
+ }
+
+ /**
+ * Using the source and destination bounds, perform an animated scroll.
+ */
+ private suspend fun performBringIntoView(source: Rect, destination: Rect) {
+ val offset = when (orientation) {
+ Vertical -> source.top - destination.top
+ Horizontal -> source.left - destination.left
+ }
+ val scrollDelta = if (reverseDirection) -offset else offset
+
+ // Note that this results in weird behavior if called before the previous
+ // performBringIntoView finishes due to b/220119990.
+ scrollableState.animateScrollBy(scrollDelta)
+ }
+
+ /**
+ * Calculate the offset needed to bring one of the edges into view. The leadingEdge is the side
+ * closest to the origin (For the x-axis this is 'left', for the y-axis this is 'top').
+ * The trailing edge is the other side (For the x-axis this is 'right', for the y-axis this is
+ * 'bottom').
+ */
+ private fun relocationDistance(
+ leadingEdgeOfItemRequestingFocus: Float,
+ trailingEdgeOfItemRequestingFocus: Float,
+ parentSize: Float,
+ pivotOffsets: PivotOffsets
+ ): Float {
+ val totalWidthOfItemRequestingFocus =
+ trailingEdgeOfItemRequestingFocus - leadingEdgeOfItemRequestingFocus
+ val pivotOfItemRequestingFocus =
+ pivotOffsets.childFraction * totalWidthOfItemRequestingFocus
+ val intendedLocationOfItemRequestingFocus = parentSize * pivotOffsets.parentFraction
+
+ return leadingEdgeOfItemRequestingFocus - intendedLocationOfItemRequestingFocus +
+ pivotOfItemRequestingFocus
+ }
+}
+
+// TODO: b/203141462 - make this public and move it to ui
+/**
+ * Whether this modifier is inside a scrollable container, provided by [Modifier.marioScrollable].
+ * Defaults to false.
+ */
+internal val ModifierLocalScrollableContainer = modifierLocalOf { false }
+
+private object ModifierLocalScrollableContainerProvider : ModifierLocalProvider<Boolean> {
+ override val key = ModifierLocalScrollableContainer
+ override val value = true
+}
\ No newline at end of file
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/PivotOffsets.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/PivotOffsets.kt
new file mode 100644
index 0000000..2700311
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/PivotOffsets.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation
+
+/**
+ * Holds the offsets needed for mario-scrolling.
+ *
+ * {@property parentFraction} defines the offset of the starting edge of the child
+ * element from the starting edge of the parent element. This value should be between 0 and 1.
+ *
+ * {@property childFraction} defines the offset of the starting edge of the child from
+ * the pivot defined by parentFraction. This value should be between 0 and 1.
+ */
+class PivotOffsets constructor(
+ val parentFraction: Float = 0.3f,
+ val childFraction: Float = 0f
+) {
+ init {
+ validateFraction(parentFraction)
+ validateFraction(childFraction)
+ }
+
+ /* Verify that the fraction passed in lies between 0 and 1 */
+ private fun validateFraction(fraction: Float): Float {
+ if (fraction in 0.0..1.0)
+ return fraction
+ else
+ throw IllegalArgumentException(
+ "OffsetFractions should be between 0 and 1. $fraction is not between 0 and 1.")
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is PivotOffsets) return false
+
+ if (parentFraction != other.parentFraction) return false
+ if (childFraction != other.childFraction) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = parentFraction.hashCode()
+ result = 31 * result + childFraction.hashCode()
+ return result
+ }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/LazyBeyondBoundsModifier.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/LazyBeyondBoundsModifier.kt
new file mode 100644
index 0000000..d0611207
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/LazyBeyondBoundsModifier.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy
+
+import androidx.tv.foundation.lazy.LazyListBeyondBoundsInfo.Interval
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.BeyondBoundsLayout
+import androidx.compose.ui.layout.BeyondBoundsLayout.BeyondBoundsScope
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Above
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.After
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Before
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Below
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Left
+import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Right
+import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
+import androidx.compose.ui.modifier.ModifierLocalProvider
+import androidx.compose.ui.modifier.ProvidableModifierLocal
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.LayoutDirection.Ltr
+import androidx.compose.ui.unit.LayoutDirection.Rtl
+import androidx.tv.foundation.lazy.list.TvLazyListState
+
+/**
+ * This modifier is used to measure and place additional items when the lazyList receives a
+ * request to layout items beyond the visible bounds.
+ */
+@Suppress("ComposableModifierFactory")
+@Composable
+internal fun Modifier.lazyListBeyondBoundsModifier(
+ state: TvLazyListState,
+ beyondBoundsInfo: LazyListBeyondBoundsInfo,
+ reverseLayout: Boolean,
+): Modifier {
+ val layoutDirection = LocalLayoutDirection.current
+ return this then remember(state, beyondBoundsInfo, reverseLayout, layoutDirection) {
+ LazyListBeyondBoundsModifierLocal(state, beyondBoundsInfo, reverseLayout, layoutDirection)
+ }
+}
+
+private class LazyListBeyondBoundsModifierLocal(
+ private val state: TvLazyListState,
+ private val beyondBoundsInfo: LazyListBeyondBoundsInfo,
+ private val reverseLayout: Boolean,
+ private val layoutDirection: LayoutDirection
+) : ModifierLocalProvider<BeyondBoundsLayout?>, BeyondBoundsLayout {
+ override val key: ProvidableModifierLocal<BeyondBoundsLayout?>
+ get() = ModifierLocalBeyondBoundsLayout
+ override val value: BeyondBoundsLayout
+ get() = this
+
+ override fun <T> layout(
+ direction: BeyondBoundsLayout.LayoutDirection,
+ block: BeyondBoundsScope.() -> T?
+ ): T? {
+ // We use a new interval each time because this function is re-entrant.
+ var interval = beyondBoundsInfo.addInterval(
+ state.firstVisibleItemIndex,
+ state.layoutInfo.visibleItemsInfo.last().index
+ )
+
+ var found: T? = null
+ while (found == null && interval.hasMoreContent(direction)) {
+
+ // Add one extra beyond bounds item.
+ interval = addNextInterval(interval, direction).also {
+ beyondBoundsInfo.removeInterval(interval)
+ }
+ state.remeasurement?.forceRemeasure()
+
+ // When we invoke this block, the beyond bounds items are present.
+ found = block.invoke(
+ object : BeyondBoundsScope {
+ override val hasMoreContent: Boolean
+ get() = interval.hasMoreContent(direction)
+ }
+ )
+ }
+
+ // Dispose the items that are beyond the visible bounds.
+ beyondBoundsInfo.removeInterval(interval)
+ state.remeasurement?.forceRemeasure()
+ return found
+ }
+
+ private fun addNextInterval(
+ currentInterval: Interval,
+ direction: BeyondBoundsLayout.LayoutDirection
+ ): Interval {
+ var start = currentInterval.start
+ var end = currentInterval.end
+ when (direction) {
+ Before -> start--
+ After -> end++
+ Above -> if (reverseLayout) end++ else start--
+ Below -> if (reverseLayout) start-- else end++
+ Left -> when (layoutDirection) {
+ Ltr -> if (reverseLayout) end++ else start--
+ Rtl -> if (reverseLayout) start-- else end++
+ }
+ Right -> when (layoutDirection) {
+ Ltr -> if (reverseLayout) start-- else end++
+ Rtl -> if (reverseLayout) end++ else start--
+ }
+ else -> unsupportedDirection()
+ }
+ return beyondBoundsInfo.addInterval(start, end)
+ }
+
+ private fun Interval.hasMoreContent(direction: BeyondBoundsLayout.LayoutDirection): Boolean {
+ fun hasMoreItemsBefore() = start > 0
+ fun hasMoreItemsAfter() = end < state.layoutInfo.totalItemsCount - 1
+ return when (direction) {
+ Before -> hasMoreItemsBefore()
+ After -> hasMoreItemsAfter()
+ Above -> if (reverseLayout) hasMoreItemsAfter() else hasMoreItemsBefore()
+ Below -> if (reverseLayout) hasMoreItemsBefore() else hasMoreItemsAfter()
+ Left -> when (layoutDirection) {
+ Ltr -> if (reverseLayout) hasMoreItemsAfter() else hasMoreItemsBefore()
+ Rtl -> if (reverseLayout) hasMoreItemsBefore() else hasMoreItemsAfter()
+ }
+ Right -> when (layoutDirection) {
+ Ltr -> if (reverseLayout) hasMoreItemsBefore() else hasMoreItemsAfter()
+ Rtl -> if (reverseLayout) hasMoreItemsAfter() else hasMoreItemsBefore()
+ }
+ else -> unsupportedDirection()
+ }
+ }
+}
+
+private fun unsupportedDirection(): Nothing = error(
+ "Lazy list does not support beyond bounds layout for the specified direction"
+)
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/LazyListBeyondBoundsInfo.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/LazyListBeyondBoundsInfo.kt
new file mode 100644
index 0000000..05005c8
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/LazyListBeyondBoundsInfo.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy
+
+import androidx.compose.runtime.collection.mutableVectorOf
+
+/**
+ * This data structure is used to save information about the number of "beyond bounds items"
+ * that we want to compose. These items are not within the visible bounds of the lazylist,
+ * but we compose them because they are explicitly requested through the
+ * [beyond bounds layout API][androidx.compose.ui.layout.BeyondBoundsLayout].
+ *
+ * When the LazyList receives a
+ * [searchBeyondBounds][androidx.compose.ui.layout.BeyondBoundsLayout.searchBeyondBounds] request to
+ * layout items beyond visible bounds, it creates an instance of [LazyListBeyondBoundsInfo] by using
+ * the [addInterval] function. This returns the interval of items that are currently composed,
+ * and we can edit this interval to control the number of beyond bounds items.
+ *
+ * There can be multiple intervals created at the same time, and LazyList merges all the
+ * intervals to calculate the effective beyond bounds items.
+ *
+ * The [beyond bounds layout API][androidx.compose.ui.layout.BeyondBoundsLayout] is designed to be
+ * synchronous, so once you are done using the items, call [removeInterval] to remove
+ * the extra items you had requested.
+ *
+ * Note that when you clear an interval, the items in that interval might not be cleared right
+ * away if another interval was created that has the same items. This is done to support two use
+ * cases:
+ *
+ * 1. To allow items to be pinned while they are being scrolled into view.
+ *
+ * 2. To allow users to call
+ * [searchBeyondBounds][androidx.compose.ui.layout.BeyondBoundsLayout.searchBeyondBounds]
+ * from within the completion block of another searchBeyondBounds call.
+ */
+internal class LazyListBeyondBoundsInfo {
+ private val beyondBoundsItems = mutableVectorOf<Interval>()
+
+ /**
+ * Create a beyond bounds interval. This can be used to specify which composed items we want to
+ * retain. For instance, it can be used to force the measuring of items that are beyond the
+ * visible bounds of a lazy list.
+ *
+ * @param start The starting index (inclusive) for this interval.
+ * @param end The ending index (inclusive) for this interval.
+ *
+ * @return An interval that specifies which items we want to retain.
+ */
+ fun addInterval(start: Int, end: Int): Interval {
+ return Interval(start, end).apply {
+ beyondBoundsItems.add(this)
+ }
+ }
+
+ /**
+ * Clears the specified interval. Use this to remove the interval created by [addInterval].
+ */
+ fun removeInterval(interval: Interval) {
+ beyondBoundsItems.remove(interval)
+ }
+
+ /**
+ * Returns true if there are beyond bounds intervals.
+ */
+ fun hasIntervals(): Boolean = beyondBoundsItems.isNotEmpty()
+
+ /**
+ * The effective start index after merging all the current intervals.
+ */
+ val start: Int
+ get() {
+ var minIndex = beyondBoundsItems.first().start
+ beyondBoundsItems.forEach {
+ if (it.start < minIndex) {
+ minIndex = it.start
+ }
+ }
+ require(minIndex >= 0)
+ return minIndex
+ }
+
+ /**
+ * The effective end index after merging all the current intervals.
+ */
+ val end: Int
+ get() {
+ var maxIndex = beyondBoundsItems.first().end
+ beyondBoundsItems.forEach {
+ if (it.end > maxIndex) {
+ maxIndex = it.end
+ }
+ }
+ return maxIndex
+ }
+
+ /**
+ * The Interval used to implement [LazyListBeyondBoundsInfo].
+ */
+ internal data class Interval(
+ /** The start index for the interval. */
+ val start: Int,
+
+ /** The end index for the interval. */
+ val end: Int
+ ) {
+ init {
+ require(start >= 0)
+ require(end >= start)
+ }
+ }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/LazyListPinningModifier.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/LazyListPinningModifier.kt
new file mode 100644
index 0000000..a726ffb
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/LazyListPinningModifier.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.ModifierLocalPinnableParent
+import androidx.compose.foundation.lazy.layout.PinnableParent
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.modifier.ModifierLocalConsumer
+import androidx.compose.ui.modifier.ModifierLocalProvider
+import androidx.compose.ui.modifier.ModifierLocalReadScope
+import androidx.compose.ui.modifier.ProvidableModifierLocal
+import androidx.tv.foundation.lazy.list.TvLazyListState
+
+/**
+ * This is a temporary placeholder implementation of pinning until we implement b/195049010.
+ */
+@Suppress("ComposableModifierFactory")
+@Composable
+internal fun Modifier.lazyListPinningModifier(
+ state: TvLazyListState,
+ beyondBoundsInfo: LazyListBeyondBoundsInfo
+): Modifier {
+ return this then remember(state, beyondBoundsInfo) {
+ LazyListPinningModifier(state, beyondBoundsInfo)
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private class LazyListPinningModifier(
+ private val state: TvLazyListState,
+ private val beyondBoundsInfo: LazyListBeyondBoundsInfo,
+) : ModifierLocalProvider<PinnableParent?>, ModifierLocalConsumer, PinnableParent {
+ var pinnableGrandParent: PinnableParent? = null
+
+ override val key: ProvidableModifierLocal<PinnableParent?>
+ get() = ModifierLocalPinnableParent
+
+ override val value: PinnableParent
+ get() = this
+
+ override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
+ pinnableGrandParent = with(scope) { ModifierLocalPinnableParent.current }
+ }
+
+ override fun pinItems(): PinnableParent.PinnedItemsHandle = with(beyondBoundsInfo) {
+ if (hasIntervals()) {
+ object : PinnableParent.PinnedItemsHandle {
+ val parentPinnedItemsHandle = pinnableGrandParent?.pinItems()
+ val interval = addInterval(start, end)
+ override fun unpin() {
+ removeInterval(interval)
+ parentPinnedItemsHandle?.unpin()
+ state.remeasurement?.forceRemeasure()
+ }
+ }
+ } else {
+ pinnableGrandParent?.pinItems() ?: EmptyPinnedItemsHandle
+ }
+ }
+
+ companion object {
+ private val EmptyPinnedItemsHandle = object : PinnableParent.PinnedItemsHandle {
+ override fun unpin() {}
+ }
+ }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/ItemIndex.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/ItemIndex.kt
new file mode 100644
index 0000000..8ae2a25
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/ItemIndex.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+/**
+ * Represents a line index in the lazy grid.
+ */
+@Suppress("NOTHING_TO_INLINE")
[email protected]
+internal value class LineIndex(val value: Int) {
+ inline operator fun inc(): LineIndex = LineIndex(value + 1)
+ inline operator fun dec(): LineIndex = LineIndex(value - 1)
+ inline operator fun plus(i: Int): LineIndex = LineIndex(value + i)
+ inline operator fun minus(i: Int): LineIndex = LineIndex(value - i)
+ inline operator fun minus(i: LineIndex): LineIndex = LineIndex(value - i.value)
+ inline operator fun compareTo(other: LineIndex): Int = value - other.value
+}
+
+/**
+ * Represents an item index in the lazy grid.
+ */
+@Suppress("NOTHING_TO_INLINE")
[email protected]
+internal value class ItemIndex(val value: Int) {
+ inline operator fun inc(): ItemIndex = ItemIndex(value + 1)
+ inline operator fun dec(): ItemIndex = ItemIndex(value - 1)
+ inline operator fun plus(i: Int): ItemIndex = ItemIndex(value + i)
+ inline operator fun minus(i: Int): ItemIndex = ItemIndex(value - i)
+ inline operator fun minus(i: ItemIndex): ItemIndex = ItemIndex(value - i.value)
+ inline operator fun compareTo(other: ItemIndex): Int = value - other.value
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGrid.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGrid.kt
new file mode 100644
index 0000000..e84fadf
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGrid.kt
@@ -0,0 +1,357 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.checkScrollableContainerConstraints
+import androidx.compose.foundation.clipScrollableContainer
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.calculateEndPadding
+import androidx.compose.foundation.layout.calculateStartPadding
+import androidx.compose.foundation.lazy.layout.LazyLayout
+import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.offset
+import androidx.compose.ui.util.fastForEach
+import androidx.tv.foundation.PivotOffsets
+import androidx.tv.foundation.marioScrollable
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+internal fun LazyGrid(
+ /** Modifier to be applied for the inner layout */
+ modifier: Modifier = Modifier,
+ /** State controlling the scroll position */
+ state: TvLazyGridState,
+ /** Prefix sums of cross axis sizes of slots per line, e.g. the columns for vertical grid. */
+ slotSizesSums: Density.(Constraints) -> List<Int>,
+ /** The inner padding to be added for the whole content (not for each individual item) */
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+ /** reverse the direction of scrolling and layout */
+ reverseLayout: Boolean = false,
+ /** The layout orientation of the grid */
+ isVertical: Boolean,
+ /** Whether scrolling via the user gestures is allowed. */
+ userScrollEnabled: Boolean,
+ /** The vertical arrangement for items/lines. */
+ verticalArrangement: Arrangement.Vertical,
+ /** The horizontal arrangement for items/lines. */
+ horizontalArrangement: Arrangement.Horizontal,
+ /** offsets of child element within the parent and starting edge of the child from the pivot
+ * defined by the parentOffset */
+ pivotOffsets: PivotOffsets,
+ /** The content of the grid */
+ content: TvLazyGridScope.() -> Unit
+) {
+ val itemProvider = rememberItemProvider(state, content)
+
+ val scope = rememberCoroutineScope()
+ val placementAnimator = remember(state, isVertical) {
+ LazyGridItemPlacementAnimator(scope, isVertical)
+ }
+ state.placementAnimator = placementAnimator
+
+ val measurePolicy = rememberLazyGridMeasurePolicy(
+ itemProvider,
+ state,
+ slotSizesSums,
+ contentPadding,
+ reverseLayout,
+ isVertical,
+ horizontalArrangement,
+ verticalArrangement,
+ placementAnimator
+ )
+
+ state.isVertical = isVertical
+
+ ScrollPositionUpdater(itemProvider, state)
+
+ val orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal
+ LazyLayout(
+ modifier = modifier
+ .then(state.remeasurementModifier)
+ .then(state.awaitLayoutModifier)
+ .lazyGridSemantics(
+ itemProvider = itemProvider,
+ state = state,
+ coroutineScope = scope,
+ isVertical = isVertical,
+ reverseScrolling = reverseLayout,
+ userScrollEnabled = userScrollEnabled
+ )
+ .clipScrollableContainer(orientation)
+ .marioScrollable(
+ orientation = orientation,
+ reverseDirection = run {
+ // A finger moves with the content, not with the viewport. Therefore,
+ // always reverse once to have "natural" gesture that goes reversed to layout
+ var reverseDirection = !reverseLayout
+ // But if rtl and horizontal, things move the other way around
+ val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
+ if (isRtl && !isVertical) {
+ reverseDirection = !reverseDirection
+ }
+ reverseDirection
+ },
+ state = state,
+ enabled = userScrollEnabled,
+ pivotOffsets = pivotOffsets
+ ),
+ prefetchState = state.prefetchState,
+ measurePolicy = measurePolicy,
+ itemProvider = itemProvider
+ )
+}
+
+/** Extracted to minimize the recomposition scope */
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun ScrollPositionUpdater(
+ itemProvider: LazyGridItemProvider,
+ state: TvLazyGridState
+) {
+ if (itemProvider.itemCount > 0) {
+ state.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun rememberLazyGridMeasurePolicy(
+ /** Items provider of the list. */
+ itemProvider: LazyGridItemProvider,
+ /** The state of the list. */
+ state: TvLazyGridState,
+ /** Prefix sums of cross axis sizes of slots of the grid. */
+ slotSizesSums: Density.(Constraints) -> List<Int>,
+ /** The inner padding to be added for the whole content(nor for each individual item) */
+ contentPadding: PaddingValues,
+ /** reverse the direction of scrolling and layout */
+ reverseLayout: Boolean,
+ /** The layout orientation of the list */
+ isVertical: Boolean,
+ /** The horizontal arrangement for items. Required when isVertical is false */
+ horizontalArrangement: Arrangement.Horizontal? = null,
+ /** The vertical arrangement for items. Required when isVertical is true */
+ verticalArrangement: Arrangement.Vertical? = null,
+ /** Item placement animator. Should be notified with the measuring result */
+ placementAnimator: LazyGridItemPlacementAnimator
+) = remember<LazyLayoutMeasureScope.(Constraints) -> MeasureResult>(
+ state,
+ slotSizesSums,
+ contentPadding,
+ reverseLayout,
+ isVertical,
+ horizontalArrangement,
+ verticalArrangement,
+ placementAnimator
+) {
+ { containerConstraints ->
+ checkScrollableContainerConstraints(
+ containerConstraints,
+ if (isVertical) Orientation.Vertical else Orientation.Horizontal
+ )
+
+ // resolve content paddings
+ val startPadding =
+ if (isVertical) {
+ contentPadding.calculateLeftPadding(layoutDirection).roundToPx()
+ } else {
+ // in horizontal configuration, padding is reversed by placeRelative
+ contentPadding.calculateStartPadding(layoutDirection).roundToPx()
+ }
+
+ val endPadding =
+ if (isVertical) {
+ contentPadding.calculateRightPadding(layoutDirection).roundToPx()
+ } else {
+ // in horizontal configuration, padding is reversed by placeRelative
+ contentPadding.calculateEndPadding(layoutDirection).roundToPx()
+ }
+ val topPadding = contentPadding.calculateTopPadding().roundToPx()
+ val bottomPadding = contentPadding.calculateBottomPadding().roundToPx()
+ val totalVerticalPadding = topPadding + bottomPadding
+ val totalHorizontalPadding = startPadding + endPadding
+ val totalMainAxisPadding = if (isVertical) totalVerticalPadding else totalHorizontalPadding
+ val beforeContentPadding = when {
+ isVertical && !reverseLayout -> topPadding
+ isVertical && reverseLayout -> bottomPadding
+ !isVertical && !reverseLayout -> startPadding
+ else -> endPadding // !isVertical && reverseLayout
+ }
+ val afterContentPadding = totalMainAxisPadding - beforeContentPadding
+ val contentConstraints =
+ containerConstraints.offset(-totalHorizontalPadding, -totalVerticalPadding)
+
+ state.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
+
+ val spanLayoutProvider = itemProvider.spanLayoutProvider
+ val resolvedSlotSizesSums = slotSizesSums(containerConstraints)
+ spanLayoutProvider.slotsPerLine = resolvedSlotSizesSums.size
+
+ // Update the state's cached Density and slotsPerLine
+ state.density = this
+ state.slotsPerLine = resolvedSlotSizesSums.size
+
+ val spaceBetweenLinesDp = if (isVertical) {
+ requireNotNull(verticalArrangement).spacing
+ } else {
+ requireNotNull(horizontalArrangement).spacing
+ }
+ val spaceBetweenLines = spaceBetweenLinesDp.roundToPx()
+ val spaceBetweenSlotsDp = if (isVertical) {
+ horizontalArrangement?.spacing ?: 0.dp
+ } else {
+ verticalArrangement?.spacing ?: 0.dp
+ }
+ val spaceBetweenSlots = spaceBetweenSlotsDp.roundToPx()
+
+ val itemsCount = itemProvider.itemCount
+
+ // can be negative if the content padding is larger than the max size from constraints
+ val mainAxisAvailableSize = if (isVertical) {
+ containerConstraints.maxHeight - totalVerticalPadding
+ } else {
+ containerConstraints.maxWidth - totalHorizontalPadding
+ }
+ val visualItemOffset = if (!reverseLayout || mainAxisAvailableSize > 0) {
+ IntOffset(startPadding, topPadding)
+ } else {
+ // When layout is reversed and paddings together take >100% of the available space,
+ // layout size is coerced to 0 when positioning. To take that space into account,
+ // we offset start padding by negative space between paddings.
+ IntOffset(
+ if (isVertical) startPadding else startPadding + mainAxisAvailableSize,
+ if (isVertical) topPadding + mainAxisAvailableSize else topPadding
+ )
+ }
+
+ val measuredItemProvider = LazyMeasuredItemProvider(
+ itemProvider,
+ this,
+ spaceBetweenLines
+ ) { index, key, crossAxisSize, mainAxisSpacing, placeables ->
+ LazyMeasuredItem(
+ index = index,
+ key = key,
+ isVertical = isVertical,
+ crossAxisSize = crossAxisSize,
+ mainAxisSpacing = mainAxisSpacing,
+ reverseLayout = reverseLayout,
+ layoutDirection = layoutDirection,
+ beforeContentPadding = beforeContentPadding,
+ afterContentPadding = afterContentPadding,
+ visualOffset = visualItemOffset,
+ placeables = placeables,
+ placementAnimator = placementAnimator
+ )
+ }
+ val measuredLineProvider = LazyMeasuredLineProvider(
+ isVertical,
+ resolvedSlotSizesSums,
+ spaceBetweenSlots,
+ itemsCount,
+ spaceBetweenLines,
+ measuredItemProvider,
+ spanLayoutProvider
+ ) { index, items, spans, mainAxisSpacing ->
+ LazyMeasuredLine(
+ index = index,
+ items = items,
+ spans = spans,
+ isVertical = isVertical,
+ slotsPerLine = resolvedSlotSizesSums.size,
+ layoutDirection = layoutDirection,
+ mainAxisSpacing = mainAxisSpacing,
+ crossAxisSpacing = spaceBetweenSlots
+ )
+ }
+ state.prefetchInfoRetriever = { line ->
+ val lineConfiguration = spanLayoutProvider.getLineConfiguration(line.value)
+ var index = ItemIndex(lineConfiguration.firstItemIndex)
+ var slot = 0
+ val result = ArrayList<Pair<Int, Constraints>>(lineConfiguration.spans.size)
+ lineConfiguration.spans.fastForEach {
+ val span = it.currentLineSpan
+ result.add(index.value to measuredLineProvider.childConstraints(slot, span))
+ ++index
+ slot += span
+ }
+ result
+ }
+
+ val firstVisibleLineIndex: LineIndex
+ val firstVisibleLineScrollOffset: Int
+ Snapshot.withoutReadObservation {
+ if (state.firstVisibleItemIndex < itemsCount || itemsCount <= 0) {
+ firstVisibleLineIndex = spanLayoutProvider.getLineIndexOfItem(
+ state.firstVisibleItemIndex
+ )
+ firstVisibleLineScrollOffset = state.firstVisibleItemScrollOffset
+ } else {
+ // the data set has been updated and now we have less items that we were
+ // scrolled to before
+ firstVisibleLineIndex = spanLayoutProvider.getLineIndexOfItem(itemsCount - 1)
+ firstVisibleLineScrollOffset = 0
+ }
+ }
+ measureLazyGrid(
+ itemsCount = itemsCount,
+ measuredLineProvider = measuredLineProvider,
+ measuredItemProvider = measuredItemProvider,
+ mainAxisAvailableSize = mainAxisAvailableSize,
+ slotsPerLine = resolvedSlotSizesSums.size,
+ beforeContentPadding = beforeContentPadding,
+ afterContentPadding = afterContentPadding,
+ firstVisibleLineIndex = firstVisibleLineIndex,
+ firstVisibleLineScrollOffset = firstVisibleLineScrollOffset,
+ scrollToBeConsumed = state.scrollToBeConsumed,
+ constraints = contentConstraints,
+ isVertical = isVertical,
+ verticalArrangement = verticalArrangement,
+ horizontalArrangement = horizontalArrangement,
+ reverseLayout = reverseLayout,
+ density = this,
+ placementAnimator = placementAnimator,
+ layout = { width, height, placement ->
+ layout(
+ containerConstraints.constrainWidth(width + totalHorizontalPadding),
+ containerConstraints.constrainHeight(height + totalVerticalPadding),
+ emptyMap(),
+ placement
+ )
+ }
+ ).also { state.applyMeasureResult(it) }
+ }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridDsl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridDsl.kt
new file mode 100644
index 0000000..9321d6d
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridDsl.kt
@@ -0,0 +1,481 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.calculateEndPadding
+import androidx.compose.foundation.layout.calculateStartPadding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.PivotOffsets
+
+/**
+ * A lazy vertical grid layout. It composes only visible rows of the grid.
+ *
+ * @param columns describes the count and the size of the grid's columns,
+ * see [TvGridCells] doc for more information
+ * @param modifier the modifier to apply to this layout
+ * @param state the state object to be used to control or observe the list's state
+ * @param contentPadding specify a padding around the whole content
+ * @param reverseLayout reverse the direction of scrolling and layout. When `true`, items will be
+ * laid out in the reverse order and [TvLazyGridState.firstVisibleItemIndex] == 0 means
+ * that grid is scrolled to the bottom. Note that [reverseLayout] does not change the behavior of
+ * [verticalArrangement],
+ * e.g. with [Arrangement.Top] (top) 123### (bottom) becomes (top) 321### (bottom).
+ * @param verticalArrangement The vertical arrangement of the layout's children
+ * @param horizontalArrangement The horizontal arrangement of the layout's children
+ * @param pivotOffsets offsets that are used when implementing Mario Scrolling
+ * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions
+ * is allowed. You can still scroll programmatically using the state even when it is disabled.
+ * @param pivotOffsets offsets of child element within the parent and starting edge of the child
+ * from the pivot defined by the parentOffset.
+ * @param content the [TvLazyGridScope] which describes the content
+ */
+@Composable
+fun TvLazyVerticalGrid(
+ columns: TvGridCells,
+ modifier: Modifier = Modifier,
+ state: TvLazyGridState = rememberTvLazyGridState(),
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+ reverseLayout: Boolean = false,
+ verticalArrangement: Arrangement.Vertical =
+ if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
+ horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
+ userScrollEnabled: Boolean = true,
+ pivotOffsets: PivotOffsets = PivotOffsets(),
+ content: TvLazyGridScope.() -> Unit
+) {
+ val slotSizesSums = rememberColumnWidthSums(columns, horizontalArrangement, contentPadding)
+ LazyGrid(
+ slotSizesSums = slotSizesSums,
+ modifier = modifier,
+ state = state,
+ contentPadding = contentPadding,
+ reverseLayout = reverseLayout,
+ isVertical = true,
+ horizontalArrangement = horizontalArrangement,
+ verticalArrangement = verticalArrangement,
+ userScrollEnabled = userScrollEnabled,
+ content = content,
+ pivotOffsets = pivotOffsets
+ )
+}
+
+/**
+ * A lazy horizontal grid layout. It composes only visible columns of the grid.
+ *
+ * @param rows a class describing how cells form rows, see [TvGridCells] doc for more information
+ * @param modifier the modifier to apply to this layout
+ * @param state the state object to be used to control or observe the list's state
+ * @param contentPadding specify a padding around the whole content
+ * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
+ * composed from the end to the start and [TvLazyGridState.firstVisibleItemIndex] == 0 will mean
+ * the first item is located at the end.
+ * @param verticalArrangement The vertical arrangement of the layout's children
+ * @param horizontalArrangement The horizontal arrangement of the layout's children
+ * @param pivotOffsets offsets that are used when implementing Mario Scrolling
+ * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions
+ * is allowed. You can still scroll programmatically using the state even when it is disabled.
+ * @param pivotOffsets offsets of child element within the parent and starting edge of the child
+ * from the pivot defined by the parentOffset.
+ * @param content the [TvLazyGridScope] which describes the content
+ */
+@Composable
+fun TvLazyHorizontalGrid(
+ rows: TvGridCells,
+ modifier: Modifier = Modifier,
+ state: TvLazyGridState = rememberTvLazyGridState(),
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+ reverseLayout: Boolean = false,
+ horizontalArrangement: Arrangement.Horizontal =
+ if (!reverseLayout) Arrangement.Start else Arrangement.End,
+ verticalArrangement: Arrangement.Vertical = Arrangement.Top,
+ userScrollEnabled: Boolean = true,
+ pivotOffsets: PivotOffsets = PivotOffsets(),
+ content: TvLazyGridScope.() -> Unit
+) {
+ val slotSizesSums = rememberRowHeightSums(rows, verticalArrangement, contentPadding)
+ LazyGrid(
+ slotSizesSums = slotSizesSums,
+ modifier = modifier,
+ state = state,
+ contentPadding = contentPadding,
+ reverseLayout = reverseLayout,
+ isVertical = false,
+ horizontalArrangement = horizontalArrangement,
+ verticalArrangement = verticalArrangement,
+ userScrollEnabled = userScrollEnabled,
+ pivotOffsets = pivotOffsets,
+ content = content
+ )
+}
+
+/** Returns prefix sums of column widths. */
+@Composable
+private fun rememberColumnWidthSums(
+ columns: TvGridCells,
+ horizontalArrangement: Arrangement.Horizontal,
+ contentPadding: PaddingValues
+) = remember<Density.(Constraints) -> List<Int>>(
+ columns,
+ horizontalArrangement,
+ contentPadding,
+) {
+ { constraints ->
+ require(constraints.maxWidth != Constraints.Infinity) {
+ "LazyVerticalGrid's width should be bound by parent."
+ }
+ val horizontalPadding = contentPadding.calculateStartPadding(LayoutDirection.Ltr) +
+ contentPadding.calculateEndPadding(LayoutDirection.Ltr)
+ val gridWidth = constraints.maxWidth - horizontalPadding.roundToPx()
+ with(columns) {
+ calculateCrossAxisCellSizes(
+ gridWidth,
+ horizontalArrangement.spacing.roundToPx()
+ ).toMutableList().apply {
+ for (i in 1 until size) {
+ this[i] += this[i - 1]
+ }
+ }
+ }
+ }
+}
+
+/** Returns prefix sums of row heights. */
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun rememberRowHeightSums(
+ rows: TvGridCells,
+ verticalArrangement: Arrangement.Vertical,
+ contentPadding: PaddingValues
+) = remember<Density.(Constraints) -> List<Int>>(
+ rows,
+ verticalArrangement,
+ contentPadding,
+) {
+ { constraints ->
+ require(constraints.maxHeight != Constraints.Infinity) {
+ "LazyHorizontalGrid's height should be bound by parent."
+ }
+ val verticalPadding = contentPadding.calculateTopPadding() +
+ contentPadding.calculateBottomPadding()
+ val gridHeight = constraints.maxHeight - verticalPadding.roundToPx()
+ with(rows) {
+ calculateCrossAxisCellSizes(
+ gridHeight,
+ verticalArrangement.spacing.roundToPx()
+ ).toMutableList().apply {
+ for (i in 1 until size) {
+ this[i] += this[i - 1]
+ }
+ }
+ }
+ }
+}
+
+/**
+ * This class describes the count and the sizes of columns in vertical grids,
+ * or rows in horizontal grids.
+ */
+@Stable
+interface TvGridCells {
+ /**
+ * Calculates the number of cells and their cross axis size based on
+ * [availableSize] and [spacing].
+ *
+ * For example, in vertical grids, [spacing] is passed from the grid's [Arrangement.Horizontal].
+ * The [Arrangement.Horizontal] will also be used to arrange items in a row if the grid is wider
+ * than the calculated sum of columns.
+ *
+ * Note that the calculated cross axis sizes will be considered in an RTL-aware manner --
+ * if the grid is vertical and the layout direction is RTL, the first width in the returned
+ * list will correspond to the rightmost column.
+ *
+ * @param availableSize available size on cross axis, e.g. width of [TvLazyVerticalGrid].
+ * @param spacing cross axis spacing, e.g. horizontal spacing for [TvLazyVerticalGrid].
+ * The spacing is passed from the corresponding [Arrangement] param of the lazy grid.
+ */
+ fun Density.calculateCrossAxisCellSizes(availableSize: Int, spacing: Int): List<Int>
+
+ /**
+ * Defines a grid with fixed number of rows or columns.
+ *
+ * For example, for the vertical [TvLazyVerticalGrid] Fixed(3) would mean that
+ * there are 3 columns 1/3 of the parent width.
+ */
+ class Fixed(private val count: Int) : TvGridCells {
+ init {
+ require(count > 0)
+ }
+
+ override fun Density.calculateCrossAxisCellSizes(
+ availableSize: Int,
+ spacing: Int
+ ): List<Int> {
+ return calculateCellsCrossAxisSizeImpl(availableSize, count, spacing)
+ }
+
+ override fun hashCode(): Int {
+ return -count // Different sign from Adaptive.
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other is Fixed && count == other.count
+ }
+ }
+
+ /**
+ * Defines a grid with as many rows or columns as possible on the condition that
+ * every cell has at least [minSize] space and all extra space distributed evenly.
+ *
+ * For example, for the vertical [TvLazyVerticalGrid] Adaptive(20.dp) would mean that
+ * there will be as many columns as possible and every column will be at least 20.dp
+ * and all the columns will have equal width. If the screen is 88.dp wide then
+ * there will be 4 columns 22.dp each.
+ */
+ class Adaptive(private val minSize: Dp) : TvGridCells {
+ init {
+ require(minSize > 0.dp)
+ }
+
+ override fun Density.calculateCrossAxisCellSizes(
+ availableSize: Int,
+ spacing: Int
+ ): List<Int> {
+ val count = maxOf((availableSize + spacing) / (minSize.roundToPx() + spacing), 1)
+ return calculateCellsCrossAxisSizeImpl(availableSize, count, spacing)
+ }
+
+ override fun hashCode(): Int {
+ return minSize.hashCode()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other is Adaptive && minSize == other.minSize
+ }
+ }
+}
+
+private fun calculateCellsCrossAxisSizeImpl(
+ gridSize: Int,
+ slotCount: Int,
+ spacing: Int
+): List<Int> {
+ val gridSizeWithoutSpacing = gridSize - spacing * (slotCount - 1)
+ val slotSize = gridSizeWithoutSpacing / slotCount
+ val remainingPixels = gridSizeWithoutSpacing % slotCount
+ return List(slotCount) {
+ slotSize + if (it < remainingPixels) 1 else 0
+ }
+}
+
+/**
+ * Receiver scope which is used by [TvLazyVerticalGrid].
+ */
+@TvLazyGridScopeMarker
+sealed interface TvLazyGridScope {
+ /**
+ * Adds a single item to the scope.
+ *
+ * @param key a stable and unique key representing the item. Using the same key
+ * for multiple items in the grid is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the grid will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
+ * @param span the span of the item. Default is 1x1. It is good practice to leave it `null`
+ * when this matches the intended behavior, as providing a custom implementation impacts
+ * performance
+ * @param contentType the type of the content of this item. The item compositions of the same
+ * type could be reused more efficiently. Note that null is a valid type and items of such
+ * type will be considered compatible.
+ * @param content the content of the item
+ */
+ fun item(
+ key: Any? = null,
+ span: (TvLazyGridItemSpanScope.() -> TvGridItemSpan)? = null,
+ contentType: Any? = null,
+ content: @Composable TvLazyGridItemScope.() -> Unit
+ )
+
+ /**
+ * Adds a [count] of items.
+ *
+ * @param count the items count
+ * @param key a factory of stable and unique keys representing the item. Using the same key
+ * for multiple items in the grid is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the grid will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
+ * @param span define custom spans for the items. Default is 1x1. It is good practice to
+ * leave it `null` when this matches the intended behavior, as providing a custom
+ * implementation impacts performance
+ * @param contentType a factory of the content types for the item. The item compositions of
+ * the same type could be reused more efficiently. Note that null is a valid type and items
+ * of such type will be considered compatible.
+ * @param itemContent the content displayed by a single item
+ */
+ fun items(
+ count: Int,
+ key: ((index: Int) -> Any)? = null,
+ span: (TvLazyGridItemSpanScope.(index: Int) -> TvGridItemSpan)? = null,
+ contentType: (index: Int) -> Any? = { null },
+ itemContent: @Composable TvLazyGridItemScope.(index: Int) -> Unit
+ )
+}
+
+/**
+ * Adds a list of items.
+ *
+ * @param items the data list
+ * @param key a factory of stable and unique keys representing the item. Using the same key
+ * for multiple items in the grid is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the grid will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
+ * @param span define custom spans for the items. Default is 1x1. It is good practice to
+ * leave it `null` when this matches the intended behavior, as providing a custom implementation
+ * impacts performance
+ * @param contentType a factory of the content types for the item. The item compositions of
+ * the same type could be reused more efficiently. Note that null is a valid type and items of such
+ * type will be considered compatible.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> TvLazyGridScope.items(
+ items: List<T>,
+ noinline key: ((item: T) -> Any)? = null,
+ noinline span: (TvLazyGridItemSpanScope.(item: T) -> TvGridItemSpan)? = null,
+ noinline contentType: (item: T) -> Any? = { null },
+ crossinline itemContent: @Composable TvLazyGridItemScope.(item: T) -> Unit
+) = items(
+ count = items.size,
+ key = if (key != null) { index: Int -> key(items[index]) } else null,
+ span = if (span != null) { { span(items[it]) } } else null,
+ contentType = { index: Int -> contentType(items[index]) }
+) {
+ itemContent(items[it])
+}
+
+/**
+ * Adds a list of items where the content of an item is aware of its index.
+ *
+ * @param items the data list
+ * @param key a factory of stable and unique keys representing the item. Using the same key
+ * for multiple items in the grid is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the grid will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
+ * @param span define custom spans for the items. Default is 1x1. It is good practice to leave
+ * it `null` when this matches the intended behavior, as providing a custom implementation
+ * impacts performance
+ * @param contentType a factory of the content types for the item. The item compositions of
+ * the same type could be reused more efficiently. Note that null is a valid type and items of such
+ * type will be considered compatible.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> TvLazyGridScope.itemsIndexed(
+ items: List<T>,
+ noinline key: ((index: Int, item: T) -> Any)? = null,
+ noinline span: (TvLazyGridItemSpanScope.(index: Int, item: T) -> TvGridItemSpan)? = null,
+ crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null },
+ crossinline itemContent: @Composable TvLazyGridItemScope.(index: Int, item: T) -> Unit
+) = items(
+ count = items.size,
+ key = if (key != null) { index: Int -> key(index, items[index]) } else null,
+ span = if (span != null) { { span(it, items[it]) } } else null,
+ contentType = { index -> contentType(index, items[index]) }
+) {
+ itemContent(it, items[it])
+}
+
+/**
+ * Adds an array of items.
+ *
+ * @param items the data array
+ * @param key a factory of stable and unique keys representing the item. Using the same key
+ * for multiple items in the grid is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the grid will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
+ * @param span define custom spans for the items. Default is 1x1. It is good practice to leave
+ * it `null` when this matches the intended behavior, as providing a custom implementation
+ * impacts performance
+ * @param contentType a factory of the content types for the item. The item compositions of
+ * the same type could be reused more efficiently. Note that null is a valid type and items of such
+ * type will be considered compatible.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> TvLazyGridScope.items(
+ items: Array<T>,
+ noinline key: ((item: T) -> Any)? = null,
+ noinline span: (TvLazyGridItemSpanScope.(item: T) -> TvGridItemSpan)? = null,
+ noinline contentType: (item: T) -> Any? = { null },
+ crossinline itemContent: @Composable TvLazyGridItemScope.(item: T) -> Unit
+) = items(
+ count = items.size,
+ key = if (key != null) { index: Int -> key(items[index]) } else null,
+ span = if (span != null) { { span(items[it]) } } else null,
+ contentType = { index: Int -> contentType(items[index]) }
+) {
+ itemContent(items[it])
+}
+
+/**
+ * Adds an array of items where the content of an item is aware of its index.
+ *
+ * @param items the data array
+ * @param key a factory of stable and unique keys representing the item. Using the same key
+ * for multiple items in the grid is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the grid will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
+ * @param span define custom spans for the items. Default is 1x1. It is good practice to leave
+ * it `null` when this matches the intended behavior, as providing a custom implementation
+ * impacts performance
+ * @param contentType a factory of the content types for the item. The item compositions of
+ * the same type could be reused more efficiently. Note that null is a valid type and items of such
+ * type will be considered compatible.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> TvLazyGridScope.itemsIndexed(
+ items: Array<T>,
+ noinline key: ((index: Int, item: T) -> Any)? = null,
+ noinline span: (TvLazyGridItemSpanScope.(index: Int, item: T) -> TvGridItemSpan)? = null,
+ crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null },
+ crossinline itemContent: @Composable TvLazyGridItemScope.(index: Int, item: T) -> Unit
+) = items(
+ count = items.size,
+ key = if (key != null) { index: Int -> key(index, items[index]) } else null,
+ span = if (span != null) { { span(it, items[it]) } } else null,
+ contentType = { index -> contentType(index, items[index]) }
+) {
+ itemContent(it, items[it])
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
new file mode 100644
index 0000000..223640a
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
@@ -0,0 +1,463 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.SpringSpec
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.VisibilityThreshold
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.util.fastAny
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastForEachIndexed
+import kotlin.math.absoluteValue
+import kotlin.math.max
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * Handles the item placement animations when it is set via
+ * [TvLazyGridItemScope.animateItemPlacement].
+ *
+ * This class is responsible for detecting when item position changed, figuring our start/end
+ * offsets and starting the animations.
+ */
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@OptIn(ExperimentalFoundationApi::class)
+internal class LazyGridItemPlacementAnimator(
+ private val scope: CoroutineScope,
+ private val isVertical: Boolean
+) {
+ private var slotsPerLine = 0
+
+ // state containing an animation and all relevant info for each item.
+ private val keyToItemInfoMap = mutableMapOf<Any, ItemInfo>()
+
+ // snapshot of the key to index map used for the last measuring.
+ private var keyToIndexMap: Map<Any, Int> = emptyMap()
+
+ // keeps the first and the last items positioned in the viewport and their visible part sizes.
+ private var viewportStartItemIndex = -1
+ private var viewportStartItemNotVisiblePartSize = 0
+ private var viewportEndItemIndex = -1
+ private var viewportEndItemNotVisiblePartSize = 0
+
+ // stored to not allocate it every pass.
+ private val positionedKeys = mutableSetOf<Any>()
+
+ /**
+ * Should be called after the measuring so we can detect position changes and start animations.
+ *
+ * Note that this method can compose new item and add it into the [positionedItems] list.
+ */
+ fun onMeasured(
+ consumedScroll: Int,
+ layoutWidth: Int,
+ layoutHeight: Int,
+ slotsPerLine: Int,
+ reverseLayout: Boolean,
+ positionedItems: MutableList<TvLazyGridPositionedItem>,
+ measuredItemProvider: LazyMeasuredItemProvider,
+ ) {
+ if (!positionedItems.fastAny { it.hasAnimations }) {
+ // no animations specified - no work needed
+ reset()
+ return
+ }
+
+ this.slotsPerLine = slotsPerLine
+
+ val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
+
+ // the consumed scroll is considered as a delta we don't need to animate
+ val notAnimatableDelta = (if (reverseLayout) -consumedScroll else consumedScroll).toOffset()
+
+ val newFirstItem = positionedItems.first()
+ val newLastItem = positionedItems.last()
+
+ positionedItems.fastForEach { item ->
+ val itemInfo = keyToItemInfoMap[item.key] ?: return@fastForEach
+ itemInfo.index = item.index
+ itemInfo.crossAxisSize = item.getCrossAxisSize()
+ itemInfo.crossAxisOffset = item.getCrossAxisOffset()
+ }
+
+ val averageLineMainAxisSize = run {
+ val lineOf: (Int) -> Int = {
+ if (isVertical) positionedItems[it].row else positionedItems[it].column
+ }
+
+ var totalLinesMainAxisSize = 0
+ var linesCount = 0
+
+ var lineStartIndex = 0
+ while (lineStartIndex < positionedItems.size) {
+ val currentLine = lineOf(lineStartIndex)
+ if (currentLine == -1) {
+ // Filter out exiting items.
+ ++lineStartIndex
+ continue
+ }
+
+ var lineMainAxisSize = 0
+ var lineEndIndex = lineStartIndex
+ while (lineEndIndex < positionedItems.size && lineOf(lineEndIndex) == currentLine) {
+ lineMainAxisSize = max(
+ lineMainAxisSize,
+ positionedItems[lineEndIndex].mainAxisSizeWithSpacings
+ )
+ ++lineEndIndex
+ }
+
+ totalLinesMainAxisSize += lineMainAxisSize
+ ++linesCount
+
+ lineStartIndex = lineEndIndex
+ }
+
+ totalLinesMainAxisSize / linesCount
+ }
+
+ positionedKeys.clear()
+ // iterate through the items which are visible (without animated offsets)
+ positionedItems.fastForEach { item ->
+ positionedKeys.add(item.key)
+ val itemInfo = keyToItemInfoMap[item.key]
+ if (itemInfo == null) {
+ // there is no state associated with this item yet
+ if (item.hasAnimations) {
+ val newItemInfo = ItemInfo(
+ item.index,
+ item.getCrossAxisSize(),
+ item.getCrossAxisOffset()
+ )
+ val previousIndex = keyToIndexMap[item.key]
+ val offset = item.placeableOffset
+
+ val targetPlaceableOffsetMainAxis = if (previousIndex == null) {
+ // it is a completely new item. no animation is needed
+ offset.mainAxis
+ } else {
+ val fallback = if (!reverseLayout) {
+ offset.mainAxis
+ } else {
+ offset.mainAxis - item.mainAxisSizeWithSpacings
+ }
+ calculateExpectedOffset(
+ index = previousIndex,
+ mainAxisSizeWithSpacings = item.mainAxisSizeWithSpacings,
+ averageLineMainAxisSize = averageLineMainAxisSize,
+ scrolledBy = notAnimatableDelta,
+ fallback = fallback,
+ reverseLayout = reverseLayout,
+ mainAxisLayoutSize = mainAxisLayoutSize
+ )
+ }
+ val targetPlaceableOffset = if (isVertical) {
+ offset.copy(y = targetPlaceableOffsetMainAxis)
+ } else {
+ offset.copy(x = targetPlaceableOffsetMainAxis)
+ }
+
+ // populate placeable info list
+ repeat(item.placeablesCount) { placeableIndex ->
+ newItemInfo.placeables.add(
+ PlaceableInfo(
+ targetPlaceableOffset,
+ item.getMainAxisSize(placeableIndex)
+ )
+ )
+ }
+ keyToItemInfoMap[item.key] = newItemInfo
+ startAnimationsIfNeeded(item, newItemInfo)
+ }
+ } else {
+ if (item.hasAnimations) {
+ // apply new not animatable offset
+ itemInfo.notAnimatableDelta += notAnimatableDelta
+ startAnimationsIfNeeded(item, itemInfo)
+ } else {
+ // no animation, clean up if needed
+ keyToItemInfoMap.remove(item.key)
+ }
+ }
+ }
+
+ // previously we were animating items which are visible in the end state so we had to
+ // compare the current state with the state used for the previous measuring.
+ // now we will animate disappearing items so the current state is their starting state
+ // so we can update current viewport start/end items
+
+ if (!reverseLayout) {
+ viewportStartItemIndex = newFirstItem.index
+ viewportStartItemNotVisiblePartSize = newFirstItem.offset.mainAxis
+ viewportEndItemIndex = newLastItem.index
+ viewportEndItemNotVisiblePartSize = newLastItem.offset.mainAxis +
+ newLastItem.lineMainAxisSizeWithSpacings - mainAxisLayoutSize
+ } else {
+ viewportStartItemIndex = newLastItem.index
+ viewportStartItemNotVisiblePartSize = mainAxisLayoutSize -
+ newLastItem.offset.mainAxis - newLastItem.lineMainAxisSize
+ viewportEndItemIndex = newFirstItem.index
+ viewportEndItemNotVisiblePartSize = -newFirstItem.offset.mainAxis +
+ (newFirstItem.lineMainAxisSizeWithSpacings -
+ if (isVertical) newFirstItem.size.height else newFirstItem.size.width)
+ }
+
+ val iterator = keyToItemInfoMap.iterator()
+ while (iterator.hasNext()) {
+ val entry = iterator.next()
+ if (!positionedKeys.contains(entry.key)) {
+ // found an item which was in our map previously but is not a part of the
+ // positionedItems now
+ val itemInfo = entry.value
+ // apply new not animatable delta for this item
+ itemInfo.notAnimatableDelta += notAnimatableDelta
+
+ val index = measuredItemProvider.keyToIndexMap[entry.key]
+
+ // whether at least one placeable is within the viewport bounds.
+ // this usually means that we will start animation for it right now
+ val withinBounds = itemInfo.placeables.fastAny {
+ val currentTarget = it.targetOffset + itemInfo.notAnimatableDelta
+ currentTarget.mainAxis + it.mainAxisSize > 0 &&
+ currentTarget.mainAxis < mainAxisLayoutSize
+ }
+
+ // whether the animation associated with the item has been finished
+ val isFinished = !itemInfo.placeables.fastAny { it.inProgress }
+
+ if ((!withinBounds && isFinished) ||
+ index == null ||
+ itemInfo.placeables.isEmpty()
+ ) {
+ iterator.remove()
+ } else {
+ // not sure if this item will end up on the last line or not. assume not,
+ // therefore leave the mainAxisSpacing to be the default one
+ val measuredItem = measuredItemProvider.getAndMeasure(
+ index = ItemIndex(index),
+ constraints = if (isVertical) {
+ Constraints.fixedWidth(itemInfo.crossAxisSize)
+ } else {
+ Constraints.fixedHeight(itemInfo.crossAxisSize)
+ }
+ )
+
+ // calculate the target offset for the animation.
+ val absoluteTargetOffset = calculateExpectedOffset(
+ index = index,
+ mainAxisSizeWithSpacings = measuredItem.mainAxisSizeWithSpacings,
+ averageLineMainAxisSize = averageLineMainAxisSize,
+ scrolledBy = notAnimatableDelta,
+ fallback = mainAxisLayoutSize,
+ reverseLayout = reverseLayout,
+ mainAxisLayoutSize = mainAxisLayoutSize
+ )
+ val targetOffset = if (reverseLayout) {
+ mainAxisLayoutSize - absoluteTargetOffset - measuredItem.mainAxisSize
+ } else {
+ absoluteTargetOffset
+ }
+
+ val item = measuredItem.position(
+ targetOffset,
+ itemInfo.crossAxisOffset,
+ layoutWidth,
+ layoutHeight,
+ TvLazyGridItemInfo.UnknownRow,
+ TvLazyGridItemInfo.UnknownColumn,
+ measuredItem.mainAxisSize
+ )
+ positionedItems.add(item)
+ startAnimationsIfNeeded(item, itemInfo)
+ }
+ }
+ }
+
+ keyToIndexMap = measuredItemProvider.keyToIndexMap
+ }
+
+ /**
+ * Returns the current animated item placement offset. By calling it only during the layout
+ * phase we can skip doing remeasure on every animation frame.
+ */
+ fun getAnimatedOffset(
+ key: Any,
+ placeableIndex: Int,
+ minOffset: Int,
+ maxOffset: Int,
+ rawOffset: IntOffset
+ ): IntOffset {
+ val itemInfo = keyToItemInfoMap[key] ?: return rawOffset
+ val item = itemInfo.placeables[placeableIndex]
+ val currentValue = item.animatedOffset.value + itemInfo.notAnimatableDelta
+ val currentTarget = item.targetOffset + itemInfo.notAnimatableDelta
+
+ // cancel the animation if it is fully out of the bounds.
+ if (item.inProgress &&
+ ((currentTarget.mainAxis < minOffset && currentValue.mainAxis < minOffset) ||
+ (currentTarget.mainAxis > maxOffset && currentValue.mainAxis > maxOffset))
+ ) {
+ scope.launch {
+ item.animatedOffset.snapTo(item.targetOffset)
+ item.inProgress = false
+ }
+ }
+
+ return currentValue
+ }
+
+ /**
+ * Should be called when the animations are not needed for the next positions change,
+ * for example when we snap to a new position.
+ */
+ fun reset() {
+ keyToItemInfoMap.clear()
+ keyToIndexMap = emptyMap()
+ viewportStartItemIndex = -1
+ viewportStartItemNotVisiblePartSize = 0
+ viewportEndItemIndex = -1
+ viewportEndItemNotVisiblePartSize = 0
+ }
+
+ /**
+ * Estimates the outside of the viewport offset for the item. Used to understand from
+ * where to start animation for the item which wasn't visible previously or where it should
+ * end for the item which is not going to be visible in the end.
+ */
+ private fun calculateExpectedOffset(
+ index: Int,
+ mainAxisSizeWithSpacings: Int,
+ averageLineMainAxisSize: Int,
+ scrolledBy: IntOffset,
+ reverseLayout: Boolean,
+ mainAxisLayoutSize: Int,
+ fallback: Int
+ ): Int {
+ require(slotsPerLine != 0)
+ val beforeViewportStart =
+ if (!reverseLayout) viewportEndItemIndex < index else viewportEndItemIndex > index
+ val afterViewportEnd =
+ if (!reverseLayout) viewportStartItemIndex > index else viewportStartItemIndex < index
+ return when {
+ beforeViewportStart -> {
+ val diff = ((index - viewportEndItemIndex).absoluteValue + slotsPerLine - 1) /
+ slotsPerLine
+ mainAxisLayoutSize + viewportEndItemNotVisiblePartSize +
+ averageLineMainAxisSize * (diff - 1) +
+ scrolledBy.mainAxis
+ }
+ afterViewportEnd -> {
+ val diff = ((viewportStartItemIndex - index).absoluteValue + slotsPerLine - 1) /
+ slotsPerLine
+ viewportStartItemNotVisiblePartSize - mainAxisSizeWithSpacings -
+ averageLineMainAxisSize * (diff - 1) +
+ scrolledBy.mainAxis
+ }
+ else -> {
+ fallback
+ }
+ }
+ }
+
+ private fun startAnimationsIfNeeded(item: TvLazyGridPositionedItem, itemInfo: ItemInfo) {
+ // first we make sure our item info is up to date (has the item placeables count)
+ while (itemInfo.placeables.size > item.placeablesCount) {
+ itemInfo.placeables.removeLast()
+ }
+ while (itemInfo.placeables.size < item.placeablesCount) {
+ val newPlaceableInfoIndex = itemInfo.placeables.size
+ val rawOffset = item.offset
+ itemInfo.placeables.add(
+ PlaceableInfo(
+ rawOffset - itemInfo.notAnimatableDelta,
+ item.getMainAxisSize(newPlaceableInfoIndex)
+ )
+ )
+ }
+
+ itemInfo.placeables.fastForEachIndexed { index, placeableInfo ->
+ val currentTarget = placeableInfo.targetOffset + itemInfo.notAnimatableDelta
+ val currentOffset = item.placeableOffset
+ placeableInfo.mainAxisSize = item.getMainAxisSize(index)
+ val animationSpec = item.getAnimationSpec(index)
+ if (currentTarget != currentOffset) {
+ placeableInfo.targetOffset = currentOffset - itemInfo.notAnimatableDelta
+ if (animationSpec != null) {
+ placeableInfo.inProgress = true
+ scope.launch {
+ val finalSpec = if (placeableInfo.animatedOffset.isRunning) {
+ // when interrupted, use the default spring, unless the spec is a spring.
+ if (animationSpec is SpringSpec<IntOffset>) animationSpec else
+ InterruptionSpec
+ } else {
+ animationSpec
+ }
+
+ try {
+ placeableInfo.animatedOffset.animateTo(
+ placeableInfo.targetOffset,
+ finalSpec
+ )
+ placeableInfo.inProgress = false
+ } catch (_: CancellationException) {
+ // we don't reset inProgress in case of cancellation as it means
+ // there is a new animation started which would reset it later
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun Int.toOffset() =
+ IntOffset(if (isVertical) 0 else this, if (!isVertical) 0 else this)
+
+ private val IntOffset.mainAxis get() = if (isVertical) y else x
+}
+
+private class ItemInfo(
+ var index: Int,
+ var crossAxisSize: Int,
+ var crossAxisOffset: Int
+) {
+ var notAnimatableDelta: IntOffset = IntOffset.Zero
+ val placeables = mutableListOf<PlaceableInfo>()
+}
+
+private class PlaceableInfo(initialOffset: IntOffset, var mainAxisSize: Int) {
+ val animatedOffset = Animatable(initialOffset, IntOffset.VectorConverter)
+ var targetOffset: IntOffset = initialOffset
+ var inProgress by mutableStateOf(false)
+}
+
+/**
+ * We switch to this spec when a duration based animation is being interrupted.
+ */
+private val InterruptionSpec = spring(
+ stiffness = Spring.StiffnessMediumLow,
+ visibilityThreshold = IntOffset.VisibilityThreshold
+)
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemProvider.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemProvider.kt
new file mode 100644
index 0000000..bfab189
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemProvider.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@ExperimentalFoundationApi
+internal interface LazyGridItemProvider : LazyLayoutItemProvider {
+ val spanLayoutProvider: LazyGridSpanLayoutProvider
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProviderImpl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemsProviderImpl.kt
similarity index 65%
rename from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProviderImpl.kt
rename to tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemsProviderImpl.kt
index 9c2d01e..9ee4e5e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProviderImpl.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemsProviderImpl.kt
@@ -14,40 +14,49 @@
* limitations under the License.
*/
-package androidx.compose.foundation.lazy.grid
+package androidx.tv.foundation.lazy.grid
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.layout.IntervalList
-import androidx.compose.foundation.lazy.layout.calculateNearestItemsRange
import androidx.compose.foundation.lazy.layout.getDefaultLazyLayoutKey
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.structuralEqualityPolicy
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.runtime.snapshots.Snapshot
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
@ExperimentalFoundationApi
@Composable
internal fun rememberItemProvider(
- state: LazyGridState,
- content: LazyGridScope.() -> Unit,
+ state: TvLazyGridState,
+ content: TvLazyGridScope.() -> Unit,
): LazyGridItemProvider {
val latestContent = rememberUpdatedState(content)
+ // mutableState + LaunchedEffect below are used instead of derivedStateOf to ensure that update
+ // of derivedState in return expr will only happen after the state value has been changed.
val nearestItemsRangeState = remember(state) {
- derivedStateOf(structuralEqualityPolicy()) {
- calculateNearestItemsRange(
- slidingWindowSize = NearestItemsSlidingWindowSize,
- extraItemCount = NearestItemsExtraItemCount,
- firstVisibleItem = state.firstVisibleItemIndex
- )
- }
+ mutableStateOf(
+ Snapshot.withoutReadObservation {
+ // State read is observed in composition, causing it to recompose 1 additional time.
+ calculateNearestItemsRange(state.firstVisibleItemIndex)
+ }
+ )
}
-
+ LaunchedEffect(nearestItemsRangeState) {
+ snapshotFlow { calculateNearestItemsRange(state.firstVisibleItemIndex) }
+ // MutableState's SnapshotMutationPolicy will make sure the provider is only
+ // recreated when the state is updated with a new range.
+ .collect { nearestItemsRangeState.value = it }
+ }
return remember(nearestItemsRangeState) {
LazyGridItemProviderImpl(
derivedStateOf {
- val listScope = LazyGridScopeImpl().apply(latestContent.value)
+ val listScope = TvLazyGridScopeImpl().apply(latestContent.value)
LazyGridItemsSnapshot(
listScope.intervals,
listScope.hasCustomSpans,
@@ -58,6 +67,7 @@
}
}
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
@ExperimentalFoundationApi
internal class LazyGridItemsSnapshot(
private val intervals: IntervalList<LazyGridIntervalContent>,
@@ -75,7 +85,7 @@
return key ?: getDefaultLazyLayoutKey(index)
}
- fun LazyGridItemSpanScope.getSpan(index: Int): GridItemSpan {
+ fun TvLazyGridItemSpanScope.getSpan(index: Int): TvGridItemSpan {
val interval = intervals[index]
val localIntervalIndex = index - interval.startIndex
return interval.value.span.invoke(this, localIntervalIndex)
@@ -85,7 +95,7 @@
fun Item(index: Int) {
val interval = intervals[index]
val localIntervalIndex = index - interval.startIndex
- interval.value.item.invoke(LazyGridItemScopeImpl, localIntervalIndex)
+ interval.value.item.invoke(TvLazyGridItemScopeImpl, localIntervalIndex)
}
val keyToIndexMap: Map<Any, Int> = generateKeyToIndexMap(nearestItemsRange, intervals)
@@ -97,10 +107,12 @@
}
}
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
@ExperimentalFoundationApi
internal class LazyGridItemProviderImpl(
private val itemsSnapshot: State<LazyGridItemsSnapshot>
) : LazyGridItemProvider {
+
override val itemCount get() = itemsSnapshot.value.itemsCount
override fun getKey(index: Int) = itemsSnapshot.value.getKey(index)
@@ -123,6 +135,7 @@
* the indexes in the passed [range].
* The returned map will not contain the values for intervals with no key mapping provided.
*/
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
@ExperimentalFoundationApi
internal fun generateKeyToIndexMap(
range: IntRange,
@@ -153,12 +166,26 @@
}
/**
+ * Returns a range of indexes which contains at least [ExtraItemsNearTheSlidingWindow] items near
+ * the first visible item. It is optimized to return the same range for small changes in the
+ * firstVisibleItem value so we do not regenerate the map on each scroll.
+ */
+private fun calculateNearestItemsRange(firstVisibleItem: Int): IntRange {
+ val slidingWindowStart = VisibleItemsSlidingWindowSize *
+ (firstVisibleItem / VisibleItemsSlidingWindowSize)
+
+ val start = maxOf(slidingWindowStart - ExtraItemsNearTheSlidingWindow, 0)
+ val end = slidingWindowStart + VisibleItemsSlidingWindowSize + ExtraItemsNearTheSlidingWindow
+ return start until end
+}
+
+/**
* We use the idea of sliding window as an optimization, so user can scroll up to this number of
* items until we have to regenerate the key to index map.
*/
-private const val NearestItemsSlidingWindowSize = 90
+private val VisibleItemsSlidingWindowSize = 90
/**
* The minimum amount of items near the current first visible item we want to have mapping for.
*/
-private const val NearestItemsExtraItemCount = 200
+private val ExtraItemsNearTheSlidingWindow = 200
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
new file mode 100644
index 0000000..13569c4
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
@@ -0,0 +1,334 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastSumBy
+import kotlin.math.abs
+import kotlin.math.min
+import kotlin.math.roundToInt
+import kotlin.math.sign
+
+/**
+ * Measures and calculates the positions for the currently visible items. The result is produced
+ * as a [TvLazyGridMeasureResult] which contains all the calculations.
+ */
+internal fun measureLazyGrid(
+ itemsCount: Int,
+ measuredLineProvider: LazyMeasuredLineProvider,
+ measuredItemProvider: LazyMeasuredItemProvider,
+ mainAxisAvailableSize: Int,
+ slotsPerLine: Int,
+ beforeContentPadding: Int,
+ afterContentPadding: Int,
+ firstVisibleLineIndex: LineIndex,
+ firstVisibleLineScrollOffset: Int,
+ scrollToBeConsumed: Float,
+ constraints: Constraints,
+ isVertical: Boolean,
+ verticalArrangement: Arrangement.Vertical?,
+ horizontalArrangement: Arrangement.Horizontal?,
+ reverseLayout: Boolean,
+ density: Density,
+ placementAnimator: LazyGridItemPlacementAnimator,
+ layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
+): TvLazyGridMeasureResult {
+ require(beforeContentPadding >= 0)
+ require(afterContentPadding >= 0)
+ if (itemsCount <= 0) {
+ // empty data set. reset the current scroll and report zero size
+ return TvLazyGridMeasureResult(
+ firstVisibleLine = null,
+ firstVisibleLineScrollOffset = 0,
+ canScrollForward = false,
+ consumedScroll = 0f,
+ measureResult = layout(constraints.minWidth, constraints.minHeight) {},
+ visibleItemsInfo = emptyList(),
+ viewportStartOffset = -beforeContentPadding,
+ viewportEndOffset = mainAxisAvailableSize + afterContentPadding,
+ totalItemsCount = 0,
+ reverseLayout = reverseLayout,
+ orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
+ afterContentPadding = afterContentPadding
+ )
+ } else {
+ var currentFirstLineIndex = firstVisibleLineIndex
+ var currentFirstLineScrollOffset = firstVisibleLineScrollOffset
+
+ // represents the real amount of scroll we applied as a result of this measure pass.
+ var scrollDelta = scrollToBeConsumed.roundToInt()
+
+ // applying the whole requested scroll offset. we will figure out if we can't consume
+ // all of it later
+ currentFirstLineScrollOffset -= scrollDelta
+
+ // if the current scroll offset is less than minimally possible
+ if (currentFirstLineIndex == LineIndex(0) && currentFirstLineScrollOffset < 0) {
+ scrollDelta += currentFirstLineScrollOffset
+ currentFirstLineScrollOffset = 0
+ }
+
+ // this will contain all the MeasuredItems representing the visible lines
+ val visibleLines = mutableListOf<LazyMeasuredLine>()
+
+ // include the start padding so we compose items in the padding area. before starting
+ // scrolling forward we would remove it back
+ currentFirstLineScrollOffset -= beforeContentPadding
+
+ // define min and max offsets (min offset currently includes beforeContentPadding)
+ val minOffset = -beforeContentPadding
+ val maxOffset = mainAxisAvailableSize
+
+ // we had scrolled backward or we compose items in the start padding area, which means
+ // items before current firstLineScrollOffset should be visible. compose them and update
+ // firstLineScrollOffset
+ while (currentFirstLineScrollOffset < 0 && currentFirstLineIndex > LineIndex(0)) {
+ val previous = LineIndex(currentFirstLineIndex.value - 1)
+ val measuredLine = measuredLineProvider.getAndMeasure(previous)
+ visibleLines.add(0, measuredLine)
+ currentFirstLineScrollOffset += measuredLine.mainAxisSizeWithSpacings
+ currentFirstLineIndex = previous
+ }
+ // if we were scrolled backward, but there were not enough lines before. this means
+ // not the whole scroll was consumed
+ if (currentFirstLineScrollOffset < minOffset) {
+ scrollDelta += currentFirstLineScrollOffset
+ currentFirstLineScrollOffset = minOffset
+ }
+
+ // neutralize previously added start padding as we stopped filling the before content padding
+ currentFirstLineScrollOffset += beforeContentPadding
+
+ var index = currentFirstLineIndex
+ val maxMainAxis = (maxOffset + afterContentPadding).coerceAtLeast(0)
+ var currentMainAxisOffset = -currentFirstLineScrollOffset
+
+ // first we need to skip lines we already composed while composing backward
+ visibleLines.fastForEach {
+ index++
+ currentMainAxisOffset += it.mainAxisSizeWithSpacings
+ }
+
+ // then composing visible lines forward until we fill the whole viewport.
+ // we want to have at least one line in visibleItems even if in fact all the items are
+ // offscreen, this can happen if the content padding is larger than the available size.
+ while (currentMainAxisOffset <= maxMainAxis || visibleLines.isEmpty()) {
+ val measuredLine = measuredLineProvider.getAndMeasure(index)
+ if (measuredLine.isEmpty()) {
+ --index
+ break
+ }
+
+ currentMainAxisOffset += measuredLine.mainAxisSizeWithSpacings
+ if (currentMainAxisOffset <= minOffset &&
+ measuredLine.items.last().index.value != itemsCount - 1) {
+ // this line is offscreen and will not be placed. advance firstVisibleLineIndex
+ currentFirstLineIndex = index + 1
+ currentFirstLineScrollOffset -= measuredLine.mainAxisSizeWithSpacings
+ } else {
+ visibleLines.add(measuredLine)
+ }
+ index++
+ }
+
+ // we didn't fill the whole viewport with lines starting from firstVisibleLineIndex.
+ // lets try to scroll back if we have enough lines before firstVisibleLineIndex.
+ if (currentMainAxisOffset < maxOffset) {
+ val toScrollBack = maxOffset - currentMainAxisOffset
+ currentFirstLineScrollOffset -= toScrollBack
+ currentMainAxisOffset += toScrollBack
+ while (currentFirstLineScrollOffset < beforeContentPadding &&
+ currentFirstLineIndex > LineIndex(0)
+ ) {
+ val previousIndex = LineIndex(currentFirstLineIndex.value - 1)
+ val measuredLine = measuredLineProvider.getAndMeasure(previousIndex)
+ visibleLines.add(0, measuredLine)
+ currentFirstLineScrollOffset += measuredLine.mainAxisSizeWithSpacings
+ currentFirstLineIndex = previousIndex
+ }
+ scrollDelta += toScrollBack
+ if (currentFirstLineScrollOffset < 0) {
+ scrollDelta += currentFirstLineScrollOffset
+ currentMainAxisOffset += currentFirstLineScrollOffset
+ currentFirstLineScrollOffset = 0
+ }
+ }
+
+ // report the amount of pixels we consumed. scrollDelta can be smaller than
+ // scrollToBeConsumed if there were not enough lines to fill the offered space or it
+ // can be larger if lines were resized, or if, for example, we were previously
+ // displaying the line 15, but now we have only 10 lines in total in the data set.
+ val consumedScroll = if (scrollToBeConsumed.roundToInt().sign == scrollDelta.sign &&
+ abs(scrollToBeConsumed.roundToInt()) >= abs(scrollDelta)
+ ) {
+ scrollDelta.toFloat()
+ } else {
+ scrollToBeConsumed
+ }
+
+ // the initial offset for lines from visibleLines list
+ val visibleLinesScrollOffset = -currentFirstLineScrollOffset
+ var firstLine = visibleLines.first()
+
+ // even if we compose lines to fill before content padding we should ignore lines fully
+ // located there for the state's scroll position calculation (first line + first offset)
+ if (beforeContentPadding > 0) {
+ for (i in visibleLines.indices) {
+ val size = visibleLines[i].mainAxisSizeWithSpacings
+ if (currentFirstLineScrollOffset != 0 && size <= currentFirstLineScrollOffset &&
+ i != visibleLines.lastIndex) {
+ currentFirstLineScrollOffset -= size
+ firstLine = visibleLines[i + 1]
+ } else {
+ break
+ }
+ }
+ }
+
+ val layoutWidth = if (isVertical) {
+ constraints.maxWidth
+ } else {
+ constraints.constrainWidth(currentMainAxisOffset)
+ }
+ val layoutHeight = if (isVertical) {
+ constraints.constrainHeight(currentMainAxisOffset)
+ } else {
+ constraints.maxHeight
+ }
+
+ val positionedItems = calculateItemsOffsets(
+ lines = visibleLines,
+ layoutWidth = layoutWidth,
+ layoutHeight = layoutHeight,
+ finalMainAxisOffset = currentMainAxisOffset,
+ maxOffset = maxOffset,
+ firstLineScrollOffset = visibleLinesScrollOffset,
+ isVertical = isVertical,
+ verticalArrangement = verticalArrangement,
+ horizontalArrangement = horizontalArrangement,
+ reverseLayout = reverseLayout,
+ density = density
+ )
+
+ placementAnimator.onMeasured(
+ consumedScroll = consumedScroll.toInt(),
+ layoutWidth = layoutWidth,
+ layoutHeight = layoutHeight,
+ slotsPerLine = slotsPerLine,
+ reverseLayout = reverseLayout,
+ positionedItems = positionedItems,
+ measuredItemProvider = measuredItemProvider
+ )
+
+ return TvLazyGridMeasureResult(
+ firstVisibleLine = firstLine,
+ firstVisibleLineScrollOffset = currentFirstLineScrollOffset,
+ canScrollForward = currentMainAxisOffset > maxOffset,
+ consumedScroll = consumedScroll,
+ measureResult = layout(layoutWidth, layoutHeight) {
+ positionedItems.fastForEach { it.place(this) }
+ },
+ viewportStartOffset = -beforeContentPadding,
+ viewportEndOffset = mainAxisAvailableSize + afterContentPadding,
+ visibleItemsInfo = positionedItems,
+ totalItemsCount = itemsCount,
+ reverseLayout = reverseLayout,
+ orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
+ afterContentPadding = afterContentPadding
+ )
+ }
+}
+
+/**
+ * Calculates [LazyMeasuredLine]s offsets.
+ */
+private fun calculateItemsOffsets(
+ lines: List<LazyMeasuredLine>,
+ layoutWidth: Int,
+ layoutHeight: Int,
+ finalMainAxisOffset: Int,
+ maxOffset: Int,
+ firstLineScrollOffset: Int,
+ isVertical: Boolean,
+ verticalArrangement: Arrangement.Vertical?,
+ horizontalArrangement: Arrangement.Horizontal?,
+ reverseLayout: Boolean,
+ density: Density,
+): MutableList<TvLazyGridPositionedItem> {
+ val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
+ val hasSpareSpace = finalMainAxisOffset < min(mainAxisLayoutSize, maxOffset)
+ if (hasSpareSpace) {
+ check(firstLineScrollOffset == 0)
+ }
+
+ val positionedItems = ArrayList<TvLazyGridPositionedItem>(lines.fastSumBy { it.items.size })
+
+ if (hasSpareSpace) {
+ val linesCount = lines.size
+ fun Int.reverseAware() =
+ if (!reverseLayout) this else linesCount - this - 1
+
+ val sizes = IntArray(linesCount) { index ->
+ lines[index.reverseAware()].mainAxisSize
+ }
+ val offsets = IntArray(linesCount) { 0 }
+ if (isVertical) {
+ with(requireNotNull(verticalArrangement)) {
+ density.arrange(mainAxisLayoutSize, sizes, offsets)
+ }
+ } else {
+ with(requireNotNull(horizontalArrangement)) {
+ // Enforces Ltr layout direction as it is mirrored with placeRelative later.
+ density.arrange(mainAxisLayoutSize, sizes, LayoutDirection.Ltr, offsets)
+ }
+ }
+
+ val reverseAwareOffsetIndices =
+ if (reverseLayout) offsets.indices.reversed() else offsets.indices
+
+ for (index in reverseAwareOffsetIndices) {
+ val absoluteOffset = offsets[index]
+ // when reverseLayout == true, offsets are stored in the reversed order to items
+ val line = lines[index.reverseAware()]
+ val relativeOffset = if (reverseLayout) {
+ // inverse offset to align with scroll direction for positioning
+ mainAxisLayoutSize - absoluteOffset - line.mainAxisSize
+ } else {
+ absoluteOffset
+ }
+ positionedItems.addAll(
+ line.position(relativeOffset, layoutWidth, layoutHeight)
+ )
+ }
+ } else {
+ var currentMainAxis = firstLineScrollOffset
+ lines.fastForEach {
+ positionedItems.addAll(it.position(currentMainAxis, layoutWidth, layoutHeight))
+ currentMainAxis += it.mainAxisSizeWithSpacings
+ }
+ }
+ return positionedItems
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrollPosition.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrollPosition.kt
new file mode 100644
index 0000000..c7a6165
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrollPosition.kt
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.Snapshot
+
+/**
+ * Contains the current scroll position represented by the first visible item index and the first
+ * visible item scroll offset.
+ */
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@OptIn(ExperimentalFoundationApi::class)
+internal class LazyGridScrollPosition(
+ initialIndex: Int = 0,
+ initialScrollOffset: Int = 0
+) {
+ var index by mutableStateOf(ItemIndex(initialIndex))
+ private set
+
+ var scrollOffset by mutableStateOf(initialScrollOffset)
+ private set
+
+ private var hadFirstNotEmptyLayout = false
+
+ /** The last known key of the first item at [index] line. */
+ private var lastKnownFirstItemKey: Any? = null
+
+ /**
+ * Updates the current scroll position based on the results of the last measurement.
+ */
+ fun updateFromMeasureResult(measureResult: TvLazyGridMeasureResult) {
+ lastKnownFirstItemKey = measureResult.firstVisibleLine?.items?.firstOrNull()?.key
+ // we ignore the index and offset from measureResult until we get at least one
+ // measurement with real items. otherwise the initial index and scroll passed to the
+ // state would be lost and overridden with zeros.
+ if (hadFirstNotEmptyLayout || measureResult.totalItemsCount > 0) {
+ hadFirstNotEmptyLayout = true
+ val scrollOffset = measureResult.firstVisibleLineScrollOffset
+ check(scrollOffset >= 0f) { "scrollOffset should be non-negative ($scrollOffset)" }
+ Snapshot.withoutReadObservation {
+ update(
+ ItemIndex(
+ measureResult.firstVisibleLine?.items?.firstOrNull()?.index?.value ?: 0
+ ),
+ scrollOffset
+ )
+ }
+ }
+ }
+
+ /**
+ * Updates the scroll position - the passed values will be used as a start position for
+ * composing the items during the next measure pass and will be updated by the real
+ * position calculated during the measurement. This means that there is guarantee that
+ * exactly this index and offset will be applied as it is possible that:
+ * a) there will be no item at this index in reality
+ * b) item at this index will be smaller than the asked scrollOffset, which means we would
+ * switch to the next item
+ * c) there will be not enough items to fill the viewport after the requested index, so we
+ * would have to compose few elements before the asked index, changing the first visible item.
+ */
+ fun requestPosition(index: ItemIndex, scrollOffset: Int) {
+ update(index, scrollOffset)
+ // clear the stored key as we have a direct request to scroll to [index] position and the
+ // next [checkIfFirstVisibleItemWasMoved] shouldn't override this.
+ lastKnownFirstItemKey = null
+ }
+
+ /**
+ * In addition to keeping the first visible item index we also store the key of this item.
+ * When the user provided custom keys for the items this mechanism allows us to detect when
+ * there were items added or removed before our current first visible item and keep this item
+ * as the first visible one even given that its index has been changed.
+ */
+ fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyGridItemProvider) {
+ Snapshot.withoutReadObservation {
+ update(findLazyGridIndexByKey(lastKnownFirstItemKey, index, itemProvider), scrollOffset)
+ }
+ }
+
+ private fun update(index: ItemIndex, scrollOffset: Int) {
+ require(index.value >= 0f) { "Index should be non-negative (${index.value})" }
+ if (index != this.index) {
+ this.index = index
+ }
+ if (scrollOffset != this.scrollOffset) {
+ this.scrollOffset = scrollOffset
+ }
+ }
+
+ private companion object {
+ /**
+ * Finds a position of the item with the given key in the grid. This logic allows us to
+ * detect when there were items added or removed before our current first item.
+ */
+ private fun findLazyGridIndexByKey(
+ key: Any?,
+ lastKnownIndex: ItemIndex,
+ itemProvider: LazyGridItemProvider
+ ): ItemIndex {
+ if (key == null) {
+ // there were no real item during the previous measure
+ return lastKnownIndex
+ }
+ if (lastKnownIndex.value < itemProvider.itemCount &&
+ key == itemProvider.getKey(lastKnownIndex.value)
+ ) {
+ // this item is still at the same index
+ return lastKnownIndex
+ }
+ val newIndex = itemProvider.keyToIndexMap[key]
+ if (newIndex != null) {
+ return ItemIndex(newIndex)
+ }
+ // fallback to the previous index if we don't know the new index of the item
+ return lastKnownIndex
+ }
+ }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrolling.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrolling.kt
new file mode 100644
index 0000000..20bbd68
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridScrolling.kt
@@ -0,0 +1,299 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.animation.core.AnimationState
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.animateTo
+import androidx.compose.animation.core.copy
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastFirstOrNull
+import kotlin.coroutines.cancellation.CancellationException
+import kotlin.math.abs
+import kotlin.math.max
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@OptIn(ExperimentalFoundationApi::class)
+private class ItemFoundInScroll(
+ val item: TvLazyGridItemInfo,
+ val previousAnimation: AnimationState<Float, AnimationVector1D>
+) : CancellationException()
+
+private val TargetDistance = 2500.dp
+private val BoundDistance = 1500.dp
+
+private const val DEBUG = false
+private inline fun debugLog(generateMsg: () -> String) {
+ if (DEBUG) {
+ println("LazyGridScrolling: ${generateMsg()}")
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+internal suspend fun TvLazyGridState.doSmoothScrollToItem(
+ index: Int,
+ scrollOffset: Int,
+ slotsPerLine: Int
+) {
+ require(index >= 0f) { "Index should be non-negative ($index)" }
+ fun getTargetItem() = layoutInfo.visibleItemsInfo.fastFirstOrNull {
+ it.index == index
+ }
+ scroll {
+ try {
+ val targetDistancePx = with(density) { TargetDistance.toPx() }
+ val boundDistancePx = with(density) { BoundDistance.toPx() }
+ var loop = true
+ var anim = AnimationState(0f)
+ val targetItemInitialInfo = getTargetItem()
+ if (targetItemInitialInfo != null) {
+ // It's already visible, just animate directly
+ throw ItemFoundInScroll(
+ targetItemInitialInfo,
+ anim
+ )
+ }
+ val forward = index > firstVisibleItemIndex
+
+ fun isOvershot(): Boolean {
+ // Did we scroll past the item?
+ @Suppress("RedundantIf") // It's way easier to understand the logic this way
+ return if (forward) {
+ if (firstVisibleItemIndex > index) {
+ true
+ } else if (
+ firstVisibleItemIndex == index &&
+ firstVisibleItemScrollOffset > scrollOffset
+ ) {
+ true
+ } else {
+ false
+ }
+ } else { // backward
+ if (firstVisibleItemIndex < index) {
+ true
+ } else if (
+ firstVisibleItemIndex == index &&
+ firstVisibleItemScrollOffset < scrollOffset
+ ) {
+ true
+ } else {
+ false
+ }
+ }
+ }
+
+ var loops = 1
+ while (loop && layoutInfo.totalItemsCount > 0) {
+ val visibleItems = layoutInfo.visibleItemsInfo
+ val averageLineMainAxisSize = calculateLineAverageMainAxisSize(
+ visibleItems,
+ true // TODO(b/191238807)
+ )
+ val before = index < firstVisibleItemIndex
+ val linesDiff =
+ (index - firstVisibleItemIndex + (slotsPerLine - 1) * if (before) -1 else 1) /
+ slotsPerLine
+
+ val expectedDistance = (averageLineMainAxisSize * linesDiff).toFloat() +
+ scrollOffset - firstVisibleItemScrollOffset
+ val target = if (abs(expectedDistance) < targetDistancePx) {
+ expectedDistance
+ } else {
+ if (forward) targetDistancePx else -targetDistancePx
+ }
+
+ debugLog {
+ "Scrolling to index=$index offset=$scrollOffset from " +
+ "index=$firstVisibleItemIndex offset=$firstVisibleItemScrollOffset with " +
+ "averageSize=$averageLineMainAxisSize and calculated target=$target"
+ }
+
+ anim = anim.copy(value = 0f)
+ var prevValue = 0f
+ anim.animateTo(
+ target,
+ sequentialAnimation = (anim.velocity != 0f)
+ ) {
+ // If we haven't found the item yet, check if it's visible.
+ var targetItem = getTargetItem()
+
+ if (targetItem == null) {
+ // Springs can overshoot their target, clamp to the desired range
+ val coercedValue = if (target > 0) {
+ value.coerceAtMost(target)
+ } else {
+ value.coerceAtLeast(target)
+ }
+ val delta = coercedValue - prevValue
+ debugLog {
+ "Scrolling by $delta (target: $target, coercedValue: $coercedValue)"
+ }
+
+ val consumed = scrollBy(delta)
+ targetItem = getTargetItem()
+ if (targetItem != null) {
+ debugLog { "Found the item after performing scrollBy()" }
+ } else if (!isOvershot()) {
+ if (delta != consumed) {
+ debugLog { "Hit end without finding the item" }
+ cancelAnimation()
+ loop = false
+ return@animateTo
+ }
+ prevValue += delta
+ if (forward) {
+ if (value > boundDistancePx) {
+ debugLog { "Struck bound going forward" }
+ cancelAnimation()
+ }
+ } else {
+ if (value < -boundDistancePx) {
+ debugLog { "Struck bound going backward" }
+ cancelAnimation()
+ }
+ }
+
+ // Magic constants for teleportation chosen arbitrarily by experiment
+ if (forward) {
+ if (
+ loops >= 2 &&
+ index - layoutInfo.visibleItemsInfo.last().index > 200
+ ) {
+ // Teleport
+ debugLog { "Teleport forward" }
+ snapToItemIndexInternal(index = index - 200, scrollOffset = 0)
+ }
+ } else {
+ if (
+ loops >= 2 &&
+ layoutInfo.visibleItemsInfo.first().index - index > 100
+ ) {
+ // Teleport
+ debugLog { "Teleport backward" }
+ snapToItemIndexInternal(index = index + 200, scrollOffset = 0)
+ }
+ }
+ }
+ }
+
+ // We don't throw ItemFoundInScroll when we snap, because once we've snapped to
+ // the final position, there's no need to animate to it.
+ if (isOvershot()) {
+ debugLog { "Overshot" }
+ snapToItemIndexInternal(index = index, scrollOffset = scrollOffset)
+ loop = false
+ cancelAnimation()
+ return@animateTo
+ } else if (targetItem != null) {
+ debugLog { "Found item" }
+ throw ItemFoundInScroll(
+ targetItem,
+ anim
+ )
+ }
+ }
+
+ loops++
+ }
+ } catch (itemFound: ItemFoundInScroll) {
+ // We found it, animate to it
+ // Bring to the requested position - will be automatically stopped if not possible
+ val anim = itemFound.previousAnimation.copy(value = 0f)
+ // TODO(b/191238807)
+ val target = (itemFound.item.offset.y + scrollOffset).toFloat()
+ var prevValue = 0f
+ debugLog {
+ "Seeking by $target at velocity ${itemFound.previousAnimation.velocity}"
+ }
+ anim.animateTo(target, sequentialAnimation = (anim.velocity != 0f)) {
+ // Springs can overshoot their target, clamp to the desired range
+ val coercedValue = when {
+ target > 0 -> {
+ value.coerceAtMost(target)
+ }
+ target < 0 -> {
+ value.coerceAtLeast(target)
+ }
+ else -> {
+ debugLog { "WARNING: somehow ended up seeking 0px, this shouldn't happen" }
+ 0f
+ }
+ }
+ val delta = coercedValue - prevValue
+ debugLog { "Seeking by $delta (coercedValue = $coercedValue)" }
+ val consumed = scrollBy(delta)
+ if (delta != consumed /* hit the end, stop */ ||
+ coercedValue != value /* would have overshot, stop */
+ ) {
+ cancelAnimation()
+ }
+ prevValue += delta
+ }
+ // Once we're finished the animation, snap to the exact position to account for
+ // rounding error (otherwise we tend to end up with the previous item scrolled the
+ // tiniest bit onscreen)
+ // TODO: prevent temporarily scrolling *past* the item
+ snapToItemIndexInternal(index = index, scrollOffset = scrollOffset)
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun calculateLineAverageMainAxisSize(
+ visibleItems: List<TvLazyGridItemInfo>,
+ isVertical: Boolean
+): Int {
+ val lineOf: (Int) -> Int = {
+ if (isVertical) visibleItems[it].row else visibleItems[it].column
+ }
+
+ var totalLinesMainAxisSize = 0
+ var linesCount = 0
+
+ var lineStartIndex = 0
+ while (lineStartIndex < visibleItems.size) {
+ val currentLine = lineOf(lineStartIndex)
+ if (currentLine == -1) {
+ // Filter out exiting items.
+ ++lineStartIndex
+ continue
+ }
+
+ var lineMainAxisSize = 0
+ var lineEndIndex = lineStartIndex
+ while (lineEndIndex < visibleItems.size && lineOf(lineEndIndex) == currentLine) {
+ lineMainAxisSize = max(
+ lineMainAxisSize,
+ if (isVertical) {
+ visibleItems[lineEndIndex].size.height
+ } else {
+ visibleItems[lineEndIndex].size.width
+ }
+ )
+ ++lineEndIndex
+ }
+
+ totalLinesMainAxisSize += lineMainAxisSize
+ ++linesCount
+
+ lineStartIndex = lineEndIndex
+ }
+
+ return totalLinesMainAxisSize / linesCount
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpan.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpan.kt
new file mode 100644
index 0000000..73e06cc
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpan.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.Immutable
+
+/**
+ * Represents the span of an item in a [TvLazyVerticalGrid].
+ */
+@Immutable
[email protected]
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+value class TvGridItemSpan internal constructor(private val packedValue: Long) {
+ /**
+ * The span of the item on the current line. This will be the horizontal span for items of
+ * [TvLazyVerticalGrid].
+ */
+ @ExperimentalFoundationApi
+ val currentLineSpan: Int get() = packedValue.toInt()
+}
+
+/**
+ * Creates a [TvGridItemSpan] with a specified [currentLineSpan]. This will be the horizontal span
+ * for an item of a [TvLazyVerticalGrid].
+ */
+fun TvGridItemSpan(currentLineSpan: Int) = TvGridItemSpan(currentLineSpan.toLong())
+
+/**
+ * Scope of lambdas used to calculate the spans of items in lazy grids.
+ */
+@TvLazyGridScopeMarker
+sealed interface TvLazyGridItemSpanScope {
+ /**
+ * The max current line (horizontal for vertical grids) the item can occupy, such that
+ * it will be positioned on the current line.
+ *
+ * For example if [TvLazyVerticalGrid] has 3 columns this value will be 3 for the first cell in
+ * the line, 2 for the second cell, and 1 for the last one. If you return a span count larger
+ * than [maxCurrentLineSpan] this means we can't fit this cell into the current line, so the
+ * cell will be positioned on the next line.
+ */
+ val maxCurrentLineSpan: Int
+
+ /**
+ * The max line span (horizontal for vertical grids) an item can occupy. This will be the
+ * number of columns in vertical grids or the number of rows in horizontal grids.
+ *
+ * For example if [TvLazyVerticalGrid] has 3 columns this value will be 3 for each cell.
+ */
+ val maxLineSpan: Int
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
new file mode 100644
index 0000000..e72ee1e
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import kotlin.math.min
+import kotlin.math.sqrt
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@OptIn(ExperimentalFoundationApi::class)
+internal class LazyGridSpanLayoutProvider(private val itemsSnapshot: LazyGridItemsSnapshot) {
+ class LineConfiguration(val firstItemIndex: Int, val spans: List<TvGridItemSpan>)
+
+ /** Caches the bucket info on lines 0, [bucketSize], 2 * [bucketSize], etc. */
+ private val buckets = ArrayList<Bucket>().apply { add(Bucket(0)) }
+ /**
+ * The interval at each we will store the starting element of lines. These will be then
+ * used to calculate the layout of arbitrary lines, by starting from the closest
+ * known "bucket start". The smaller the bucketSize, the smaller cost for calculating layout
+ * of arbitrary lines but the higher memory usage for [buckets].
+ */
+ private val bucketSize get() = sqrt(1.0 * totalSize / slotsPerLine).toInt() + 1
+ /** Caches the last calculated line index, useful when scrolling in main axis direction. */
+ private var lastLineIndex = 0
+ /** Caches the starting item index on [lastLineIndex]. */
+ private var lastLineStartItemIndex = 0
+ /** Caches the span of [lastLineStartItemIndex], if this was already calculated. */
+ private var lastLineStartKnownSpan = 0
+ /**
+ * Caches a calculated bucket, this is useful when scrolling in reverse main axis
+ * direction. We cannot only keep the last element, as we would not know previous max span.
+ */
+ private var cachedBucketIndex = -1
+ /**
+ * Caches layout of [cachedBucketIndex], this is useful when scrolling in reverse main axis
+ * direction. We cannot only keep the last element, as we would not know previous max span.
+ */
+ private val cachedBucket = mutableListOf<Int>()
+ /**
+ * List of 1x1 spans if we do not have custom spans.
+ */
+ private var previousDefaultSpans = emptyList<TvGridItemSpan>()
+ private fun getDefaultSpans(currentSlotsPerLine: Int) =
+ if (currentSlotsPerLine == previousDefaultSpans.size) {
+ previousDefaultSpans
+ } else {
+ List(currentSlotsPerLine) { TvGridItemSpan(1) }.also { previousDefaultSpans = it }
+ }
+
+ val totalSize get() = itemsSnapshot.itemsCount
+
+ /** The number of slots on one grid line e.g. the number of columns of a vertical grid. */
+ var slotsPerLine = 0
+ set(value) {
+ if (value != field) {
+ field = value
+ invalidateCache()
+ }
+ }
+
+ fun getLineConfiguration(lineIndex: Int): LineConfiguration {
+ if (!itemsSnapshot.hasCustomSpans) {
+ // Quick return when all spans are 1x1 - in this case we can easily calculate positions.
+ val firstItemIndex = lineIndex * slotsPerLine
+ return LineConfiguration(
+ firstItemIndex,
+ getDefaultSpans(slotsPerLine.coerceAtMost(totalSize - firstItemIndex)
+ .coerceAtLeast(0))
+ )
+ }
+
+ val bucketIndex = min(lineIndex / bucketSize, buckets.size - 1)
+ // We can calculate the items on the line from the closest cached bucket start item.
+ var currentLine = bucketIndex * bucketSize
+ var currentItemIndex = buckets[bucketIndex].firstItemIndex
+ var knownCurrentItemSpan = buckets[bucketIndex].firstItemKnownSpan
+ // ... but try using the more localised cached values.
+ if (lastLineIndex in currentLine..lineIndex) {
+ // The last calculated value is a better start point. Common when scrolling main axis.
+ currentLine = lastLineIndex
+ currentItemIndex = lastLineStartItemIndex
+ knownCurrentItemSpan = lastLineStartKnownSpan
+ } else if (bucketIndex == cachedBucketIndex &&
+ lineIndex - currentLine < cachedBucket.size
+ ) {
+ // It happens that the needed line start is fully cached. Common when scrolling in
+ // reverse main axis, as we decided to cacheThisBucket previously.
+ currentItemIndex = cachedBucket[lineIndex - currentLine]
+ currentLine = lineIndex
+ knownCurrentItemSpan = 0
+ }
+
+ val cacheThisBucket = currentLine % bucketSize == 0 &&
+ lineIndex - currentLine in 2 until bucketSize
+ if (cacheThisBucket) {
+ cachedBucketIndex = bucketIndex
+ cachedBucket.clear()
+ }
+
+ check(currentLine <= lineIndex)
+
+ while (currentLine < lineIndex && currentItemIndex < totalSize) {
+ if (cacheThisBucket) {
+ cachedBucket.add(currentItemIndex)
+ }
+
+ var spansUsed = 0
+ while (spansUsed < slotsPerLine && currentItemIndex < totalSize) {
+ val span = if (knownCurrentItemSpan == 0) {
+ spanOf(currentItemIndex, slotsPerLine - spansUsed)
+ } else {
+ knownCurrentItemSpan.also { knownCurrentItemSpan = 0 }
+ }
+ if (spansUsed + span > slotsPerLine) {
+ knownCurrentItemSpan = span
+ break
+ }
+
+ currentItemIndex++
+ spansUsed += span
+ }
+ ++currentLine
+ if (currentLine % bucketSize == 0 && currentItemIndex < totalSize) {
+ val currentLineBucket = currentLine / bucketSize
+ // This should happen, as otherwise this should have been used as starting point.
+ check(buckets.size == currentLineBucket)
+ buckets.add(Bucket(currentItemIndex, knownCurrentItemSpan))
+ }
+ }
+
+ lastLineIndex = lineIndex
+ lastLineStartItemIndex = currentItemIndex
+ lastLineStartKnownSpan = knownCurrentItemSpan
+
+ val firstItemIndex = currentItemIndex
+ val spans = mutableListOf<TvGridItemSpan>()
+
+ var spansUsed = 0
+ while (spansUsed < slotsPerLine && currentItemIndex < totalSize) {
+ val span = if (knownCurrentItemSpan == 0) {
+ spanOf(currentItemIndex, slotsPerLine - spansUsed)
+ } else {
+ knownCurrentItemSpan.also { knownCurrentItemSpan = 0 }
+ }
+ if (spansUsed + span > slotsPerLine) break
+
+ currentItemIndex++
+ spans.add(TvGridItemSpan(span))
+ spansUsed += span
+ }
+ return LineConfiguration(firstItemIndex, spans)
+ }
+
+ /**
+ * Calculate the line of index [itemIndex].
+ */
+ fun getLineIndexOfItem(itemIndex: Int): LineIndex {
+ if (totalSize <= 0) {
+ return LineIndex(0)
+ }
+ require(itemIndex < totalSize)
+ if (!itemsSnapshot.hasCustomSpans) {
+ return LineIndex(itemIndex / slotsPerLine)
+ }
+
+ val lowerBoundBucket = buckets.binarySearch { it.firstItemIndex - itemIndex }.let {
+ if (it >= 0) it else -it - 2
+ }
+ var currentLine = lowerBoundBucket * bucketSize
+ var currentItemIndex = buckets[lowerBoundBucket].firstItemIndex
+
+ require(currentItemIndex <= itemIndex)
+ var spansUsed = 0
+ while (currentItemIndex < itemIndex) {
+ val span = spanOf(currentItemIndex++, slotsPerLine - spansUsed)
+ if (spansUsed + span < slotsPerLine) {
+ spansUsed += span
+ } else if (spansUsed + span == slotsPerLine) {
+ ++currentLine
+ spansUsed = 0
+ } else {
+ // spansUsed + span > slotsPerLine
+ ++currentLine
+ spansUsed = span
+ }
+ if (currentLine % bucketSize == 0) {
+ val currentLineBucket = currentLine / bucketSize
+ if (currentLineBucket >= buckets.size) {
+ buckets.add(Bucket(currentItemIndex - if (spansUsed > 0) 1 else 0))
+ }
+ }
+ }
+ if (spansUsed + spanOf(itemIndex, slotsPerLine - spansUsed) > slotsPerLine) {
+ ++currentLine
+ }
+
+ return LineIndex(currentLine)
+ }
+
+ private fun spanOf(itemIndex: Int, maxSpan: Int) = with(itemsSnapshot) {
+ with(TvLazyGridItemSpanScopeImpl) {
+ maxCurrentLineSpan = maxSpan
+ maxLineSpan = slotsPerLine
+
+ getSpan(itemIndex).currentLineSpan.coerceIn(1, slotsPerLine)
+ }
+ }
+
+ private fun invalidateCache() {
+ buckets.clear()
+ buckets.add(Bucket(0))
+ lastLineIndex = 0
+ lastLineStartItemIndex = 0
+ cachedBucketIndex = -1
+ cachedBucket.clear()
+ }
+
+ private class Bucket(
+ /** Index of the first item in the bucket */
+ val firstItemIndex: Int,
+ /** Known span of the first item. Not zero only if this item caused "line break". */
+ val firstItemKnownSpan: Int = 0
+ )
+
+ private object TvLazyGridItemSpanScopeImpl : TvLazyGridItemSpanScope {
+ override var maxCurrentLineSpan = 0
+ override var maxLineSpan = 0
+ }
+}
\ No newline at end of file
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredItem.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredItem.kt
new file mode 100644
index 0000000..9041d51e
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredItem.kt
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+
+/**
+ * Represents one measured item of the lazy grid. It can in fact consist of multiple placeables
+ * if the user emit multiple layout nodes in the item callback.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal class LazyMeasuredItem(
+ val index: ItemIndex,
+ val key: Any,
+ private val isVertical: Boolean,
+ /**
+ * Cross axis size is the same for all [placeables]. Take it as parameter for the case when
+ * [placeables] is empty.
+ */
+ val crossAxisSize: Int,
+ val mainAxisSpacing: Int,
+ private val reverseLayout: Boolean,
+ private val layoutDirection: LayoutDirection,
+ private val beforeContentPadding: Int,
+ private val afterContentPadding: Int,
+ val placeables: Array<Placeable>,
+ private val placementAnimator: LazyGridItemPlacementAnimator,
+ /**
+ * The offset which shouldn't affect any calculations but needs to be applied for the final
+ * value passed into the place() call.
+ */
+ private val visualOffset: IntOffset
+) {
+ /**
+ * Main axis size of the item - the max main axis size of the placeables.
+ */
+ val mainAxisSize: Int
+
+ /**
+ * The max main axis size of the placeables plus mainAxisSpacing.
+ */
+ val mainAxisSizeWithSpacings: Int
+
+ init {
+ var maxMainAxis = 0
+ placeables.forEach {
+ maxMainAxis = maxOf(maxMainAxis, if (isVertical) it.height else it.width)
+ }
+ mainAxisSize = maxMainAxis
+ mainAxisSizeWithSpacings = maxMainAxis + mainAxisSpacing
+ }
+
+ /**
+ * Calculates positions for the inner placeables at [rawCrossAxisOffset], [rawCrossAxisOffset].
+ * [layoutWidth] and [layoutHeight] should be provided to not place placeables which are ended
+ * up outside of the viewport (for example one item consist of 2 placeables, and the first one
+ * is not going to be visible, so we don't place it as an optimization, but place the second
+ * one). If [reverseOrder] is true the inner placeables would be placed in the inverted order.
+ */
+ fun position(
+ rawMainAxisOffset: Int,
+ rawCrossAxisOffset: Int,
+ layoutWidth: Int,
+ layoutHeight: Int,
+ row: Int,
+ column: Int,
+ lineMainAxisSize: Int
+ ): TvLazyGridPositionedItem {
+ val wrappers = mutableListOf<LazyGridPlaceableWrapper>()
+
+ val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
+ val mainAxisOffset = if (reverseLayout) {
+ mainAxisLayoutSize - rawMainAxisOffset - mainAxisSize
+ } else {
+ rawMainAxisOffset
+ }
+ val crossAxisLayoutSize = if (isVertical) layoutWidth else layoutHeight
+ val crossAxisOffset = if (isVertical && layoutDirection == LayoutDirection.Rtl) {
+ crossAxisLayoutSize - rawCrossAxisOffset - crossAxisSize
+ } else {
+ rawCrossAxisOffset
+ }
+ val placeableOffset = if (isVertical) {
+ IntOffset(crossAxisOffset, mainAxisOffset)
+ } else {
+ IntOffset(mainAxisOffset, crossAxisOffset)
+ }
+
+ var placeableIndex = if (reverseLayout) placeables.lastIndex else 0
+ while (if (reverseLayout) placeableIndex >= 0 else placeableIndex < placeables.size) {
+ val it = placeables[placeableIndex]
+ val addIndex = if (reverseLayout) 0 else wrappers.size
+ wrappers.add(
+ addIndex,
+ LazyGridPlaceableWrapper(placeableOffset, it, placeables[placeableIndex].parentData)
+ )
+ if (reverseLayout) placeableIndex-- else placeableIndex++
+ }
+
+ return TvLazyGridPositionedItem(
+ offset = if (isVertical) {
+ IntOffset(rawCrossAxisOffset, rawMainAxisOffset)
+ } else {
+ IntOffset(rawMainAxisOffset, rawCrossAxisOffset)
+ },
+ placeableOffset = placeableOffset,
+ index = index.value,
+ key = key,
+ row = row,
+ column = column,
+ size = if (isVertical) {
+ IntSize(crossAxisSize, mainAxisSize)
+ } else {
+ IntSize(mainAxisSize, crossAxisSize)
+ },
+ lineMainAxisSize = lineMainAxisSize,
+ mainAxisSpacing = mainAxisSpacing,
+ minMainAxisOffset = -if (!reverseLayout) {
+ beforeContentPadding
+ } else {
+ afterContentPadding
+ },
+ maxMainAxisOffset = mainAxisLayoutSize +
+ if (!reverseLayout) afterContentPadding else beforeContentPadding,
+ isVertical = isVertical,
+ wrappers = wrappers,
+ placementAnimator = placementAnimator,
+ visualOffset = visualOffset
+ )
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+internal class TvLazyGridPositionedItem(
+ override val offset: IntOffset,
+ val placeableOffset: IntOffset,
+ override val index: Int,
+ override val key: Any,
+ override val row: Int,
+ override val column: Int,
+ override val size: IntSize,
+ val lineMainAxisSize: Int,
+ private val mainAxisSpacing: Int,
+ private val minMainAxisOffset: Int,
+ private val maxMainAxisOffset: Int,
+ private val isVertical: Boolean,
+ private val wrappers: List<LazyGridPlaceableWrapper>,
+ private val placementAnimator: LazyGridItemPlacementAnimator,
+ private val visualOffset: IntOffset
+) : TvLazyGridItemInfo {
+ val placeablesCount: Int get() = wrappers.size
+
+ val mainAxisSizeWithSpacings: Int get() =
+ mainAxisSpacing + if (isVertical) size.height else size.width
+
+ val lineMainAxisSizeWithSpacings: Int get() = mainAxisSpacing + lineMainAxisSize
+
+ fun getMainAxisSize(index: Int) = wrappers[index].placeable.mainAxisSize
+
+ fun getCrossAxisSize() = if (isVertical) size.width else size.height
+
+ fun getCrossAxisOffset() = if (isVertical) offset.x else offset.y
+
+ @Suppress("UNCHECKED_CAST")
+ fun getAnimationSpec(index: Int) =
+ wrappers[index].parentData as? FiniteAnimationSpec<IntOffset>?
+
+ val hasAnimations = run {
+ repeat(placeablesCount) { index ->
+ if (getAnimationSpec(index) != null) {
+ return@run true
+ }
+ }
+ false
+ }
+
+ fun place(
+ scope: Placeable.PlacementScope,
+ ) = with(scope) {
+ repeat(placeablesCount) { index ->
+ val placeable = wrappers[index].placeable
+ val minOffset = minMainAxisOffset - placeable.mainAxisSize
+ val maxOffset = maxMainAxisOffset
+ val offset = if (getAnimationSpec(index) != null) {
+ placementAnimator.getAnimatedOffset(
+ key, index, minOffset, maxOffset, placeableOffset
+ )
+ } else {
+ placeableOffset
+ }
+ if (offset.mainAxis > minOffset && offset.mainAxis < maxOffset) {
+ if (isVertical) {
+ placeable.placeWithLayer(offset + visualOffset)
+ } else {
+ placeable.placeRelativeWithLayer(offset + visualOffset)
+ }
+ }
+ }
+ }
+
+ private val IntOffset.mainAxis get() = if (isVertical) y else x
+ private val Placeable.mainAxisSize get() = if (isVertical) height else width
+}
+
+internal class LazyGridPlaceableWrapper(
+ val offset: IntOffset,
+ val placeable: Placeable,
+ val parentData: Any?
+)
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredItemProvider.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredItemProvider.kt
new file mode 100644
index 0000000..5ba2016
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredItemProvider.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.Constraints
+
+/**
+ * Abstracts away the subcomposition from the measuring logic.
+ */
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@OptIn(ExperimentalFoundationApi::class)
+internal class LazyMeasuredItemProvider @ExperimentalFoundationApi constructor(
+ private val itemProvider: LazyGridItemProvider,
+ private val measureScope: LazyLayoutMeasureScope,
+ private val defaultMainAxisSpacing: Int,
+ private val measuredItemFactory: MeasuredItemFactory
+) {
+ /**
+ * Used to subcompose individual items of lazy grids. Composed placeables will be measured
+ * with the provided [constraints] and wrapped into [LazyMeasuredItem].
+ */
+ fun getAndMeasure(
+ index: ItemIndex,
+ mainAxisSpacing: Int = defaultMainAxisSpacing,
+ constraints: Constraints
+ ): LazyMeasuredItem {
+ val key = itemProvider.getKey(index.value)
+ val placeables = measureScope.measure(index.value, constraints)
+ val crossAxisSize = if (constraints.hasFixedWidth) {
+ constraints.minWidth
+ } else {
+ require(constraints.hasFixedHeight)
+ constraints.minHeight
+ }
+ return measuredItemFactory.createItem(
+ index,
+ key,
+ crossAxisSize,
+ mainAxisSpacing,
+ placeables
+ )
+ }
+
+ /**
+ * Contains the mapping between the key and the index. It could contain not all the items of
+ * the list as an optimization.
+ **/
+ val keyToIndexMap: Map<Any, Int> get() = itemProvider.keyToIndexMap
+}
+
+// This interface allows to avoid autoboxing on index param
+internal fun interface MeasuredItemFactory {
+ fun createItem(
+ index: ItemIndex,
+ key: Any,
+ crossAxisSize: Int,
+ mainAxisSpacing: Int,
+ placeables: Array<Placeable>
+ ): LazyMeasuredItem
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredLine.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredLine.kt
new file mode 100644
index 0000000..5310da9
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredLine.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.ui.unit.LayoutDirection
+
+/**
+ * Represents one measured line of the lazy list. Each item on the line can in fact consist of
+ * multiple placeables if the user emit multiple layout nodes in the item callback.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal class LazyMeasuredLine constructor(
+ val index: LineIndex,
+ val items: Array<LazyMeasuredItem>,
+ private val spans: List<TvGridItemSpan>,
+ private val isVertical: Boolean,
+ private val slotsPerLine: Int,
+ private val layoutDirection: LayoutDirection,
+ /**
+ * Spacing to be added after [mainAxisSize], in the main axis direction.
+ */
+ private val mainAxisSpacing: Int,
+ private val crossAxisSpacing: Int
+) {
+ /**
+ * Main axis size of the line - the max main axis size of the items on the line.
+ */
+ val mainAxisSize: Int
+
+ /**
+ * Sum of [mainAxisSpacing] and the max of the main axis sizes of the placeables on the line.
+ */
+ val mainAxisSizeWithSpacings: Int
+
+ init {
+ var maxMainAxis = 0
+ items.forEach { item ->
+ maxMainAxis = maxOf(maxMainAxis, item.mainAxisSize)
+ }
+ mainAxisSize = maxMainAxis
+ mainAxisSizeWithSpacings = mainAxisSize + mainAxisSpacing
+ }
+
+ /**
+ * Whether this line contains any items.
+ */
+ fun isEmpty() = items.isEmpty()
+
+ /**
+ * Calculates positions for the [items] at [offset] main axis position.
+ * If [reverseOrder] is true the [items] would be placed in the inverted order.
+ */
+ fun position(
+ offset: Int,
+ layoutWidth: Int,
+ layoutHeight: Int
+ ): List<TvLazyGridPositionedItem> {
+ var usedCrossAxis = 0
+ var usedSpan = 0
+ return items.mapIndexed { itemIndex, item ->
+ val span = spans[itemIndex].currentLineSpan
+ val startSlot = if (layoutDirection == LayoutDirection.Rtl) {
+ slotsPerLine - usedSpan - span
+ } else {
+ usedSpan
+ }
+
+ item.position(
+ rawMainAxisOffset = offset,
+ rawCrossAxisOffset = usedCrossAxis,
+ layoutWidth = layoutWidth,
+ layoutHeight = layoutHeight,
+ row = if (isVertical) index.value else startSlot,
+ column = if (isVertical) startSlot else index.value,
+ lineMainAxisSize = mainAxisSize
+ ).also {
+ usedCrossAxis += item.crossAxisSize + crossAxisSpacing
+ usedSpan += span
+ }
+ }
+ }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredLineProvider.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredLineProvider.kt
new file mode 100644
index 0000000..5cf56f4
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyMeasuredLineProvider.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.ui.unit.Constraints
+
+/**
+ * Abstracts away subcomposition and span calculation from the measuring logic of entire lines.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal class LazyMeasuredLineProvider(
+ private val isVertical: Boolean,
+ slotSizesSums: List<Int>,
+ crossAxisSpacing: Int,
+ private val gridItemsCount: Int,
+ private val spaceBetweenLines: Int,
+ private val measuredItemProvider: LazyMeasuredItemProvider,
+ private val spanLayoutProvider: LazyGridSpanLayoutProvider,
+ private val measuredLineFactory: MeasuredLineFactory
+) {
+ // The constraints for cross axis size. The main axis is not restricted.
+ internal val childConstraints: (startSlot: Int, span: Int) -> Constraints = { startSlot, span ->
+ val lastSlotSum = slotSizesSums[startSlot + span - 1]
+ val prevSlotSum = if (startSlot == 0) 0 else slotSizesSums[startSlot - 1]
+ val slotsSize = lastSlotSum - prevSlotSum
+ val crossAxisSize = slotsSize + crossAxisSpacing * (span - 1)
+ if (isVertical) {
+ Constraints.fixedWidth(crossAxisSize)
+ } else {
+ Constraints.fixedHeight(crossAxisSize)
+ }
+ }
+
+ /**
+ * Used to subcompose items on lines of lazy grids. Composed placeables will be measured
+ * with the correct constraints and wrapped into [LazyMeasuredLine].
+ */
+ fun getAndMeasure(lineIndex: LineIndex): LazyMeasuredLine {
+ val lineConfiguration = spanLayoutProvider.getLineConfiguration(lineIndex.value)
+ val lineItemsCount = lineConfiguration.spans.size
+
+ // we add space between lines as an extra spacing for all lines apart from the last one
+ // so the lazy grid measuring logic will take it into account.
+ val mainAxisSpacing = if (lineItemsCount == 0 ||
+ lineConfiguration.firstItemIndex + lineItemsCount == gridItemsCount) {
+ 0
+ } else {
+ spaceBetweenLines
+ }
+
+ var startSlot = 0
+ val items = Array(lineItemsCount) {
+ val span = lineConfiguration.spans[it].currentLineSpan
+ val constraints = childConstraints(startSlot, span)
+ measuredItemProvider.getAndMeasure(
+ ItemIndex(lineConfiguration.firstItemIndex + it),
+ mainAxisSpacing,
+ constraints
+ ).also { startSlot += span }
+ }
+ return measuredLineFactory.createLine(
+ lineIndex,
+ items,
+ lineConfiguration.spans,
+ mainAxisSpacing
+ )
+ }
+
+ /**
+ * Contains the mapping between the key and the index. It could contain not all the items of
+ * the list as an optimization.
+ **/
+ val keyToIndexMap: Map<Any, Int> get() = measuredItemProvider.keyToIndexMap
+}
+
+// This interface allows to avoid autoboxing on index param
+@OptIn(ExperimentalFoundationApi::class)
+internal fun interface MeasuredLineFactory {
+ fun createLine(
+ index: LineIndex,
+ items: Array<LazyMeasuredItem>,
+ spans: List<TvGridItemSpan>,
+ mainAxisSpacing: Int
+ ): LazyMeasuredLine
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazySemantics.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazySemantics.kt
new file mode 100644
index 0000000..ea19b91
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazySemantics.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.CollectionInfo
+import androidx.compose.ui.semantics.ScrollAxisRange
+import androidx.compose.ui.semantics.collectionInfo
+import androidx.compose.ui.semantics.horizontalScrollAxisRange
+import androidx.compose.ui.semantics.indexForKey
+import androidx.compose.ui.semantics.scrollBy
+import androidx.compose.ui.semantics.scrollToIndex
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.verticalScrollAxisRange
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalFoundationApi::class)
+@Suppress("ComposableModifierFactory", "ModifierInspectorInfo")
+@Composable
+internal fun Modifier.lazyGridSemantics(
+ itemProvider: LazyGridItemProvider,
+ state: TvLazyGridState,
+ coroutineScope: CoroutineScope,
+ isVertical: Boolean,
+ reverseScrolling: Boolean,
+ userScrollEnabled: Boolean
+) = this.then(
+ remember(
+ itemProvider,
+ state,
+ isVertical,
+ reverseScrolling,
+ userScrollEnabled
+ ) {
+ val indexForKeyMapping: (Any) -> Int = { needle ->
+ val key = itemProvider::getKey
+ var result = -1
+ for (index in 0 until itemProvider.itemCount) {
+ if (key(index) == needle) {
+ result = index
+ break
+ }
+ }
+ result
+ }
+
+ val accessibilityScrollState = ScrollAxisRange(
+ value = {
+ // This is a simple way of representing the current position without
+ // needing any lazy items to be measured. It's good enough so far, because
+ // screen-readers care mostly about whether scroll position changed or not
+ // rather than the actual offset in pixels.
+ state.firstVisibleItemIndex + state.firstVisibleItemScrollOffset / 100_000f
+ },
+ maxValue = {
+ if (state.canScrollForward) {
+ // If we can scroll further, we don't know the end yet,
+ // but it's upper bounded by #items + 1
+ itemProvider.itemCount + 1f
+ } else {
+ // If we can't scroll further, the current value is the max
+ state.firstVisibleItemIndex + state.firstVisibleItemScrollOffset / 100_000f
+ }
+ },
+ reverseScrolling = reverseScrolling
+ )
+
+ val scrollByAction: ((x: Float, y: Float) -> Boolean)? = if (userScrollEnabled) {
+ { x, y ->
+ val delta = if (isVertical) {
+ y
+ } else {
+ x
+ }
+ coroutineScope.launch {
+ (state as ScrollableState).animateScrollBy(delta)
+ }
+ // TODO(aelias): is it important to return false if we know in advance we cannot scroll?
+ true
+ }
+ } else {
+ null
+ }
+
+ val scrollToIndexAction: ((Int) -> Boolean)? = if (userScrollEnabled) {
+ { index ->
+ require(index >= 0 && index < state.layoutInfo.totalItemsCount) {
+ "Can't scroll to index $index, it is out of " +
+ "bounds [0, ${state.layoutInfo.totalItemsCount})"
+ }
+ coroutineScope.launch {
+ state.scrollToItem(index)
+ }
+ true
+ }
+ } else {
+ null
+ }
+
+ // TODO(popam): check if this is correct - it would be nice to provide correct columns here
+ val collectionInfo = CollectionInfo(rowCount = -1, columnCount = -1)
+
+ Modifier.semantics {
+ indexForKey(indexForKeyMapping)
+
+ if (isVertical) {
+ verticalScrollAxisRange = accessibilityScrollState
+ } else {
+ horizontalScrollAxisRange = accessibilityScrollState
+ }
+
+ if (scrollByAction != null) {
+ scrollBy(action = scrollByAction)
+ }
+
+ if (scrollToIndexAction != null) {
+ scrollToIndex(action = scrollToIndexAction)
+ }
+
+ this.collectionInfo = collectionInfo
+ }
+ }
+)
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemInfo.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemInfo.kt
new file mode 100644
index 0000000..2bcedda
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemInfo.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+
+/**
+ * Contains useful information about an individual item in lazy grids like [TvLazyVerticalGrid].
+ *
+ * @see TvLazyGridLayoutInfo
+ */
+sealed interface TvLazyGridItemInfo {
+ /**
+ * The index of the item in the grid.
+ */
+ val index: Int
+
+ /**
+ * The key of the item which was passed to the item() or items() function.
+ */
+ val key: Any
+
+ /**
+ * The offset of the item in pixels. It is relative to the top start of the lazy grid container.
+ */
+ val offset: IntOffset
+
+ /**
+ * The row occupied by the top start point of the item.
+ * If this is unknown, for example while this item is animating to exit the viewport and is
+ * still visible, the value will be [UnknownRow].
+ */
+ val row: Int
+
+ /**
+ * The column occupied by the top start point of the item.
+ * If this is unknown, for example while this item is animating to exit the viewport and is
+ * still visible, the value will be [UnknownColumn].
+ */
+ val column: Int
+
+ /**
+ * The pixel size of the item. Note that if you emit multiple layouts in the composable
+ * slot for the item then this size will be calculated as the max of their sizes.
+ */
+ val size: IntSize
+
+ companion object {
+ /**
+ * Possible value for [row], when they are unknown. This can happen when the item is
+ * visible while animating to exit the viewport.
+ */
+ const val UnknownRow = -1
+ /**
+ * Possible value for [column], when they are unknown. This can happen when the item is
+ * visible while animating to exit the viewport.
+ */
+ const val UnknownColumn = -1
+ }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemScope.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemScope.kt
new file mode 100644
index 0000000..5fae2451
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemScope.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.VisibilityThreshold
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.IntOffset
+
+/**
+ * Receiver scope being used by the item content parameter of [TvLazyVerticalGrid].
+ */
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@Stable
+@TvLazyGridScopeMarker
+sealed interface TvLazyGridItemScope {
+ /**
+ * This modifier animates the item placement within the Lazy grid.
+ *
+ * When you provide a key via [TvLazyGridScope.item]/[TvLazyGridScope.items] this modifier will
+ * enable item reordering animations. Aside from item reordering all other position changes
+ * caused by events like arrangement or alignment changes will also be animated.
+ *
+ * @param animationSpec a finite animation that will be used to animate the item placement.
+ */
+ @ExperimentalFoundationApi
+ fun Modifier.animateItemPlacement(
+ animationSpec: FiniteAnimationSpec<IntOffset> = spring(
+ stiffness = Spring.StiffnessMediumLow,
+ visibilityThreshold = IntOffset.VisibilityThreshold
+ )
+ ): Modifier
+}
\ No newline at end of file
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemScopeImpl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemScopeImpl.kt
new file mode 100644
index 0000000..35d063f
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridItemScopeImpl.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ParentDataModifier
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.InspectorValueInfo
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@OptIn(ExperimentalFoundationApi::class)
+internal object TvLazyGridItemScopeImpl : TvLazyGridItemScope {
+ @ExperimentalFoundationApi
+ override fun Modifier.animateItemPlacement(animationSpec: FiniteAnimationSpec<IntOffset>) =
+ this.then(AnimateItemPlacementModifier(animationSpec, debugInspectorInfo {
+ name = "animateItemPlacement"
+ value = animationSpec
+ }))
+}
+
+private class AnimateItemPlacementModifier(
+ val animationSpec: FiniteAnimationSpec<IntOffset>,
+ inspectorInfo: InspectorInfo.() -> Unit,
+) : ParentDataModifier, InspectorValueInfo(inspectorInfo) {
+ override fun Density.modifyParentData(parentData: Any?): Any = animationSpec
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is AnimateItemPlacementModifier) return false
+ return animationSpec != other.animationSpec
+ }
+
+ override fun hashCode(): Int {
+ return animationSpec.hashCode()
+ }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridLayoutInfo.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridLayoutInfo.kt
new file mode 100644
index 0000000..6fc30dc
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridLayoutInfo.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.IntSize
+
+/**
+ * Contains useful information about the currently displayed layout state of lazy grids like
+ * [TvLazyVerticalGrid]. For example you can get the list of currently displayed items.
+ *
+ * Use [TvLazyGridState.layoutInfo] to retrieve this
+ */
+sealed interface TvLazyGridLayoutInfo {
+ /**
+ * The list of [TvLazyGridItemInfo] representing all the currently visible items.
+ */
+ val visibleItemsInfo: List<TvLazyGridItemInfo>
+
+ /**
+ * The start offset of the layout's viewport in pixels. You can think of it as a minimum offset
+ * which would be visible. Usually it is 0, but it can be negative if non-zero [beforeContentPadding]
+ * was applied as the content displayed in the content padding area is still visible.
+ *
+ * You can use it to understand what items from [visibleItemsInfo] are fully visible.
+ */
+ val viewportStartOffset: Int
+
+ /**
+ * The end offset of the layout's viewport in pixels. You can think of it as a maximum offset
+ * which would be visible. It is the size of the lazy grid layout minus [beforeContentPadding].
+ *
+ * You can use it to understand what items from [visibleItemsInfo] are fully visible.
+ */
+ val viewportEndOffset: Int
+
+ /**
+ * The total count of items passed to [TvLazyVerticalGrid].
+ */
+ val totalItemsCount: Int
+
+ /**
+ * The size of the viewport in pixels. It is the lazy grid layout size including all the
+ * content paddings.
+ */
+ val viewportSize: IntSize
+
+ /**
+ * The orientation of the lazy grid.
+ */
+ val orientation: Orientation
+
+ /**
+ * True if the direction of scrolling and layout is reversed.
+ */
+ val reverseLayout: Boolean
+
+ /**
+ * The content padding in pixels applied before the first row/column in the direction of scrolling.
+ * For example it is a top content padding for LazyVerticalGrid with reverseLayout set to false.
+ */
+ val beforeContentPadding: Int
+
+ /**
+ * The content padding in pixels applied after the last row/column in the direction of scrolling.
+ * For example it is a bottom content padding for LazyVerticalGrid with reverseLayout set to false.
+ */
+ val afterContentPadding: Int
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridMeasureResult.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridMeasureResult.kt
new file mode 100644
index 0000000..51bee04
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridMeasureResult.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.unit.IntSize
+
+/**
+ * The result of the measure pass for lazy list layout.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal class TvLazyGridMeasureResult(
+ // properties defining the scroll position:
+ /** The new first visible line of items.*/
+ val firstVisibleLine: LazyMeasuredLine?,
+ /** The new value for [TvLazyGridState.firstVisibleItemScrollOffset].*/
+ val firstVisibleLineScrollOffset: Int,
+ /** True if there is some space available to continue scrolling in the forward direction.*/
+ val canScrollForward: Boolean,
+ /** The amount of scroll consumed during the measure pass.*/
+ val consumedScroll: Float,
+ /** MeasureResult defining the layout.*/
+ measureResult: MeasureResult,
+ // properties representing the info needed for LazyListLayoutInfo:
+ /** see [TvLazyGridLayoutInfo.visibleItemsInfo] */
+ override val visibleItemsInfo: List<TvLazyGridItemInfo>,
+ /** see [TvLazyGridLayoutInfo.viewportStartOffset] */
+ override val viewportStartOffset: Int,
+ /** see [TvLazyGridLayoutInfo.viewportEndOffset] */
+ override val viewportEndOffset: Int,
+ /** see [TvLazyGridLayoutInfo.totalItemsCount] */
+ override val totalItemsCount: Int,
+ /** see [TvLazyGridLayoutInfo.reverseLayout] */
+ override val reverseLayout: Boolean,
+ /** see [TvLazyGridLayoutInfo.orientation] */
+ override val orientation: Orientation,
+ /** see [TvLazyGridLayoutInfo.afterContentPadding] */
+ override val afterContentPadding: Int
+) : TvLazyGridLayoutInfo, MeasureResult by measureResult {
+ override val viewportSize: IntSize
+ get() = IntSize(width, height)
+ override val beforeContentPadding: Int get() = -viewportStartOffset
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeImpl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeImpl.kt
new file mode 100644
index 0000000..bfd2510
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeImpl.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.MutableIntervalList
+import androidx.compose.runtime.Composable
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@OptIn(ExperimentalFoundationApi::class)
+internal class TvLazyGridScopeImpl : TvLazyGridScope {
+ internal val intervals = MutableIntervalList<LazyGridIntervalContent>()
+ internal var hasCustomSpans = false
+
+ private val DefaultSpan: TvLazyGridItemSpanScope.(Int) -> TvGridItemSpan = { TvGridItemSpan(1) }
+
+ override fun item(
+ key: Any?,
+ span: (TvLazyGridItemSpanScope.() -> TvGridItemSpan)?,
+ contentType: Any?,
+ content: @Composable TvLazyGridItemScope.() -> Unit
+ ) {
+ intervals.addInterval(
+ 1,
+ LazyGridIntervalContent(
+ key = key?.let { { key } },
+ span = span?.let { { span() } } ?: DefaultSpan,
+ type = { contentType },
+ item = { content() }
+ )
+ )
+ if (span != null) hasCustomSpans = true
+ }
+
+ override fun items(
+ count: Int,
+ key: ((index: Int) -> Any)?,
+ span: (TvLazyGridItemSpanScope.(Int) -> TvGridItemSpan)?,
+ contentType: (index: Int) -> Any?,
+ itemContent: @Composable TvLazyGridItemScope.(index: Int) -> Unit
+ ) {
+ intervals.addInterval(
+ count,
+ LazyGridIntervalContent(
+ key = key,
+ span = span ?: DefaultSpan,
+ type = contentType,
+ item = itemContent
+ )
+ )
+ if (span != null) hasCustomSpans = true
+ }
+}
+
+internal class LazyGridIntervalContent(
+ val key: ((index: Int) -> Any)?,
+ val span: TvLazyGridItemSpanScope.(Int) -> TvGridItemSpan,
+ val type: ((index: Int) -> Any?),
+ val item: @Composable TvLazyGridItemScope.(Int) -> Unit
+)
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/ExperimentalWindowExtensionsApi.java b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeMarker.kt
similarity index 73%
rename from window/extensions/extensions/src/main/java/androidx/window/extensions/ExperimentalWindowExtensionsApi.java
rename to tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeMarker.kt
index 0da12db..ed5b2bc 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/ExperimentalWindowExtensionsApi.java
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridScopeMarker.kt
@@ -14,13 +14,10 @@
* limitations under the License.
*/
-package androidx.window.extensions;
-
-import androidx.annotation.RequiresOptIn;
+package androidx.tv.foundation.lazy.grid
/**
- * Denotes that the API uses experimental WindowManager extension APIs.
+ * DSL marker used to distinguish between lazy grid dsl scope and the item content scope.
*/
-@RequiresOptIn
-public @interface ExperimentalWindowExtensionsApi {
-}
+@DslMarker
+annotation class TvLazyGridScopeMarker
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridState.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridState.kt
new file mode 100644
index 0000000..df4e4c2
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridState.kt
@@ -0,0 +1,414 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.interaction.InteractionSource
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.collection.mutableVectorOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.layout.Remeasurement
+import androidx.compose.ui.layout.RemeasurementModifier
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.util.fastForEach
+import androidx.tv.foundation.lazy.list.AwaitFirstLayoutModifier
+import kotlin.math.abs
+
+/**
+ * Creates a [TvLazyGridState] that is remembered across compositions.
+ *
+ * Changes to the provided initial values will **not** result in the state being recreated or
+ * changed in any way if it has already been created.
+ *
+ * @param initialFirstVisibleItemIndex the initial value for [TvLazyGridState.firstVisibleItemIndex]
+ * @param initialFirstVisibleItemScrollOffset the initial value for
+ * [TvLazyGridState.firstVisibleItemScrollOffset]
+ */
+@Composable
+fun rememberTvLazyGridState(
+ initialFirstVisibleItemIndex: Int = 0,
+ initialFirstVisibleItemScrollOffset: Int = 0
+): TvLazyGridState {
+ return rememberSaveable(saver = TvLazyGridState.Saver) {
+ TvLazyGridState(
+ initialFirstVisibleItemIndex,
+ initialFirstVisibleItemScrollOffset
+ )
+ }
+}
+
+/**
+ * A state object that can be hoisted to control and observe scrolling.
+ *
+ * In most cases, this will be created via [rememberTvLazyGridState].
+ *
+ * @param firstVisibleItemIndex the initial value for [TvLazyGridState.firstVisibleItemIndex]
+ * @param firstVisibleItemScrollOffset the initial value for
+ * [TvLazyGridState.firstVisibleItemScrollOffset]
+ */
+@OptIn(ExperimentalFoundationApi::class)
+@Stable
+class TvLazyGridState constructor(
+ firstVisibleItemIndex: Int = 0,
+ firstVisibleItemScrollOffset: Int = 0
+) : ScrollableState {
+ /**
+ * The holder class for the current scroll position.
+ */
+ private val scrollPosition =
+ LazyGridScrollPosition(firstVisibleItemIndex, firstVisibleItemScrollOffset)
+
+ /**
+ * The index of the first item that is visible.
+ *
+ * Note that this property is observable and if you use it in the composable function it will
+ * be recomposed on every change causing potential performance issues.
+ */
+ val firstVisibleItemIndex: Int get() = scrollPosition.index.value
+
+ /**
+ * The scroll offset of the first visible item. Scrolling forward is positive - i.e., the
+ * amount that the item is offset backwards
+ */
+ val firstVisibleItemScrollOffset: Int get() = scrollPosition.scrollOffset
+
+ /** Backing state for [layoutInfo] */
+ private val layoutInfoState = mutableStateOf<TvLazyGridLayoutInfo>(EmptyTvLazyGridLayoutInfo)
+
+ /**
+ * The object of [TvLazyGridLayoutInfo] calculated during the last layout pass. For example,
+ * you can use it to calculate what items are currently visible.
+ *
+ * Note that this property is observable and is updated after every scroll or remeasure.
+ * If you use it in the composable function it will be recomposed on every change causing
+ * potential performance issues including infinity recomposition loop.
+ * Therefore, avoid using it in the composition.
+ */
+ val layoutInfo: TvLazyGridLayoutInfo get() = layoutInfoState.value
+
+ /**
+ * [InteractionSource] that will be used to dispatch drag events when this
+ * grid is being dragged. If you want to know whether the fling (or animated scroll) is in
+ * progress, use [isScrollInProgress].
+ */
+ val interactionSource: InteractionSource get() = internalInteractionSource
+
+ internal val internalInteractionSource: MutableInteractionSource = MutableInteractionSource()
+
+ /**
+ * The amount of scroll to be consumed in the next layout pass. Scrolling forward is negative
+ * - that is, it is the amount that the items are offset in y
+ */
+ internal var scrollToBeConsumed = 0f
+ private set
+
+ /**
+ * Needed for [animateScrollToItem]. Updated on every measure.
+ */
+ internal var slotsPerLine: Int by mutableStateOf(0)
+
+ /**
+ * Needed for [animateScrollToItem]. Updated on every measure.
+ */
+ internal var density: Density by mutableStateOf(Density(1f, 1f))
+
+ /**
+ * Needed for [notifyPrefetch].
+ */
+ internal var isVertical: Boolean by mutableStateOf(true)
+
+ /**
+ * The ScrollableController instance. We keep it as we need to call stopAnimation on it once
+ * we reached the end of the grid.
+ */
+ private val scrollableState = ScrollableState { -onScroll(-it) }
+
+ /**
+ * Only used for testing to confirm that we're not making too many measure passes
+ */
+ /*@VisibleForTesting*/
+ internal var numMeasurePasses: Int = 0
+ private set
+
+ /**
+ * Only used for testing to disable prefetching when needed to test the main logic.
+ */
+ /*@VisibleForTesting*/
+ internal var prefetchingEnabled: Boolean = true
+
+ /**
+ * The index scheduled to be prefetched (or the last prefetched index if the prefetch is done).
+ */
+ private var lineToPrefetch = -1
+
+ /**
+ * The list of handles associated with the items from the [lineToPrefetch] line.
+ */
+ private var currentLinePrefetchHandles =
+ mutableVectorOf<LazyLayoutPrefetchState.PrefetchHandle>()
+
+ /**
+ * Keeps the scrolling direction during the previous calculation in order to be able to
+ * detect the scrolling direction change.
+ */
+ private var wasScrollingForward = false
+
+ /**
+ * The [Remeasurement] object associated with our layout. It allows us to remeasure
+ * synchronously during scroll.
+ */
+ private var remeasurement: Remeasurement? by mutableStateOf(null)
+
+ /**
+ * The modifier which provides [remeasurement].
+ */
+ internal val remeasurementModifier = object : RemeasurementModifier {
+ override fun onRemeasurementAvailable(remeasurement: Remeasurement) {
+ [email protected] = remeasurement
+ }
+ }
+
+ /**
+ * Provides a modifier which allows to delay some interactions (e.g. scroll)
+ * until layout is ready.
+ */
+ internal val awaitLayoutModifier = AwaitFirstLayoutModifier()
+
+ /**
+ * Finds items on a line and their measurement constraints. Used for prefetching.
+ */
+ internal var prefetchInfoRetriever: (line: LineIndex) -> List<Pair<Int, Constraints>> by
+ mutableStateOf({ emptyList() })
+
+ internal var placementAnimator by mutableStateOf<LazyGridItemPlacementAnimator?>(null)
+
+ /**
+ * Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset]
+ * pixels.
+ *
+ * @param index the index to which to scroll. Must be non-negative.
+ * @param scrollOffset the offset that the item should end up after the scroll. Note that
+ * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will
+ * scroll the item further upward (taking it partly offscreen).
+ */
+ suspend fun scrollToItem(
+ /*@IntRange(from = 0)*/
+ index: Int,
+ scrollOffset: Int = 0
+ ) {
+ scroll {
+ snapToItemIndexInternal(index, scrollOffset)
+ }
+ }
+
+ internal fun snapToItemIndexInternal(index: Int, scrollOffset: Int) {
+ scrollPosition.requestPosition(ItemIndex(index), scrollOffset)
+ // placement animation is not needed because we snap into a new position.
+ placementAnimator?.reset()
+ remeasurement?.forceRemeasure()
+ }
+
+ /**
+ * Call this function to take control of scrolling and gain the ability to send scroll events
+ * via [ScrollScope.scrollBy]. All actions that change the logical scroll position must be
+ * performed within a [scroll] block (even if they don't call any other methods on this
+ * object) in order to guarantee that mutual exclusion is enforced.
+ *
+ * If [scroll] is called from elsewhere, this will be canceled.
+ */
+ override suspend fun scroll(
+ scrollPriority: MutatePriority,
+ block: suspend ScrollScope.() -> Unit
+ ) {
+ awaitLayoutModifier.waitForFirstLayout()
+ scrollableState.scroll(scrollPriority, block)
+ }
+
+ override fun dispatchRawDelta(delta: Float): Float =
+ scrollableState.dispatchRawDelta(delta)
+
+ override val isScrollInProgress: Boolean
+ get() = scrollableState.isScrollInProgress
+
+ private var canScrollBackward: Boolean = false
+ internal var canScrollForward: Boolean = false
+ private set
+
+ // TODO: Coroutine scrolling APIs will allow this to be private again once we have more
+ // fine-grained control over scrolling
+ /*@VisibleForTesting*/
+ internal fun onScroll(distance: Float): Float {
+ if (distance < 0 && !canScrollForward || distance > 0 && !canScrollBackward) {
+ return 0f
+ }
+ check(abs(scrollToBeConsumed) <= 0.5f) {
+ "entered drag with non-zero pending scroll: $scrollToBeConsumed"
+ }
+ scrollToBeConsumed += distance
+
+ // scrollToBeConsumed will be consumed synchronously during the forceRemeasure invocation
+ // inside measuring we do scrollToBeConsumed.roundToInt() so there will be no scroll if
+ // we have less than 0.5 pixels
+ if (abs(scrollToBeConsumed) > 0.5f) {
+ val preScrollToBeConsumed = scrollToBeConsumed
+ remeasurement?.forceRemeasure()
+ if (prefetchingEnabled) {
+ notifyPrefetch(preScrollToBeConsumed - scrollToBeConsumed)
+ }
+ }
+
+ // here scrollToBeConsumed is already consumed during the forceRemeasure invocation
+ if (abs(scrollToBeConsumed) <= 0.5f) {
+ // We consumed all of it - we'll hold onto the fractional scroll for later, so report
+ // that we consumed the whole thing
+ return distance
+ } else {
+ val scrollConsumed = distance - scrollToBeConsumed
+ // We did not consume all of it - return the rest to be consumed elsewhere (e.g.,
+ // nested scrolling)
+ scrollToBeConsumed = 0f // We're not consuming the rest, give it back
+ return scrollConsumed
+ }
+ }
+
+ private fun notifyPrefetch(delta: Float) {
+ val prefetchState = prefetchState
+ if (!prefetchingEnabled) {
+ return
+ }
+ val info = layoutInfo
+ if (info.visibleItemsInfo.isNotEmpty()) {
+ // check(isActive)
+ val scrollingForward = delta < 0
+ val lineToPrefetch: Int
+ val closestNextItemToPrefetch: Int
+ if (scrollingForward) {
+ lineToPrefetch = 1 + info.visibleItemsInfo.last().let {
+ if (isVertical) it.row else it.column
+ }
+ closestNextItemToPrefetch = info.visibleItemsInfo.last().index + 1
+ } else {
+ lineToPrefetch = -1 + info.visibleItemsInfo.first().let {
+ if (isVertical) it.row else it.column
+ }
+ closestNextItemToPrefetch = info.visibleItemsInfo.first().index - 1
+ }
+ if (lineToPrefetch != this.lineToPrefetch &&
+ closestNextItemToPrefetch in 0 until info.totalItemsCount
+ ) {
+ if (wasScrollingForward != scrollingForward) {
+ // the scrolling direction has been changed which means the last prefetched
+ // is not going to be reached anytime soon so it is safer to dispose it.
+ // if this line is already visible it is safe to call the method anyway
+ // as it will be no-op
+ currentLinePrefetchHandles.forEach { it.cancel() }
+ }
+ this.wasScrollingForward = scrollingForward
+ this.lineToPrefetch = lineToPrefetch
+ currentLinePrefetchHandles.clear()
+ prefetchInfoRetriever(LineIndex(lineToPrefetch)).fastForEach {
+ currentLinePrefetchHandles.add(
+ prefetchState.schedulePrefetch(it.first, it.second)
+ )
+ }
+ }
+ }
+ }
+
+ internal val prefetchState = LazyLayoutPrefetchState()
+
+ /**
+ * Animate (smooth scroll) to the given item.
+ *
+ * @param index the index to which to scroll. Must be non-negative.
+ * @param scrollOffset the offset that the item should end up after the scroll. Note that
+ * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will
+ * scroll the item further upward (taking it partly offscreen).
+ */
+ suspend fun animateScrollToItem(
+ /*@IntRange(from = 0)*/
+ index: Int,
+ scrollOffset: Int = 0
+ ) {
+ doSmoothScrollToItem(index, scrollOffset, slotsPerLine)
+ }
+
+ /**
+ * Updates the state with the new calculated scroll position and consumed scroll.
+ */
+ internal fun applyMeasureResult(result: TvLazyGridMeasureResult) {
+ scrollPosition.updateFromMeasureResult(result)
+ scrollToBeConsumed -= result.consumedScroll
+ layoutInfoState.value = result
+
+ canScrollForward = result.canScrollForward
+ canScrollBackward = (result.firstVisibleLine?.index?.value ?: 0) != 0 ||
+ result.firstVisibleLineScrollOffset != 0
+
+ numMeasurePasses++
+ }
+
+ /**
+ * When the user provided custom keys for the items we can try to detect when there were
+ * items added or removed before our current first visible item and keep this item
+ * as the first visible one even given that its index has been changed.
+ */
+ internal fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyGridItemProvider) {
+ scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
+ }
+
+ companion object {
+ /**
+ * The default [Saver] implementation for [TvLazyGridState].
+ */
+ val Saver: Saver<TvLazyGridState, *> = listSaver(
+ save = { listOf(it.firstVisibleItemIndex, it.firstVisibleItemScrollOffset) },
+ restore = {
+ TvLazyGridState(
+ firstVisibleItemIndex = it[0],
+ firstVisibleItemScrollOffset = it[1]
+ )
+ }
+ )
+ }
+}
+
+private object EmptyTvLazyGridLayoutInfo : TvLazyGridLayoutInfo {
+ override val visibleItemsInfo = emptyList<TvLazyGridItemInfo>()
+ override val viewportStartOffset = 0
+ override val viewportEndOffset = 0
+ override val totalItemsCount = 0
+ override val viewportSize = IntSize.Zero
+ override val orientation = Orientation.Vertical
+ override val reverseLayout = false
+ override val beforeContentPadding: Int = 0
+ override val afterContentPadding: Int = 0
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/DataIndex.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/DataIndex.kt
new file mode 100644
index 0000000..7480db2
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/DataIndex.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+/**
+ * Represents an index in the list of items of lazy layout.
+ */
+@Suppress("NOTHING_TO_INLINE")
[email protected]
+internal value class DataIndex(val value: Int) {
+ inline operator fun inc(): DataIndex = DataIndex(value + 1)
+ inline operator fun dec(): DataIndex = DataIndex(value - 1)
+ inline operator fun plus(i: Int): DataIndex = DataIndex(value + i)
+ inline operator fun minus(i: Int): DataIndex = DataIndex(value - i)
+ inline operator fun minus(i: DataIndex): DataIndex = DataIndex(value - i.value)
+ inline operator fun compareTo(other: DataIndex): Int = value - other.value
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyDsl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyDsl.kt
new file mode 100644
index 0000000..64c697a
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyDsl.kt
@@ -0,0 +1,298 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.PivotOffsets
+
+/* Copied from
+ compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt
+ and modified */
+
+/**
+ * Receiver scope which is used by [TvLazyColumn] and [TvLazyRow].
+ */
+@TvLazyListScopeMarker
+sealed interface TvLazyListScope {
+ /**
+ * Adds a single item.
+ *
+ * @param key a stable and unique key representing the item. Using the same key
+ * for multiple items in the list is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the list will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
+ * @param contentType the type of the content of this item. The item compositions of the same
+ * type could be reused more efficiently. Note that null is a valid type and items of such
+ * type will be considered compatible.
+ * @param content the content of the item
+ */
+ fun item(
+ key: Any? = null,
+ contentType: Any? = null,
+ content: @Composable TvLazyListItemScope.() -> Unit
+ )
+
+ /**
+ * Adds a [count] of items.
+ *
+ * @param count the items count
+ * @param key a factory of stable and unique keys representing the item. Using the same key
+ * for multiple items in the list is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the list will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
+ * @param contentType a factory of the content types for the item. The item compositions of
+ * the same type could be reused more efficiently. Note that null is a valid type and items of such
+ * type will be considered compatible.
+ * @param itemContent the content displayed by a single item
+ */
+ fun items(
+ count: Int,
+ key: ((index: Int) -> Any)? = null,
+ contentType: (index: Int) -> Any? = { null },
+ itemContent: @Composable TvLazyListItemScope.(index: Int) -> Unit
+ )
+}
+
+/**
+ * Adds a list of items.
+ *
+ * @param items the data list
+ * @param key a factory of stable and unique keys representing the item. Using the same key
+ * for multiple items in the list is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the list will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
+ * @param contentType a factory of the content types for the item. The item compositions of
+ * the same type could be reused more efficiently. Note that null is a valid type and items of such
+ * type will be considered compatible.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> TvLazyListScope.items(
+ items: List<T>,
+ noinline key: ((item: T) -> Any)? = null,
+ noinline contentType: (item: T) -> Any? = { null },
+ crossinline itemContent: @Composable TvLazyListItemScope.(item: T) -> Unit
+) = items(
+ count = items.size,
+ key = if (key != null) { index: Int -> key(items[index]) } else null,
+ contentType = { index: Int -> contentType(items[index]) }
+) {
+ itemContent(items[it])
+}
+
+/**
+ * Adds a list of items where the content of an item is aware of its index.
+ *
+ * @param items the data list
+ * @param key a factory of stable and unique keys representing the item. Using the same key
+ * for multiple items in the list is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the list will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
+ * @param contentType a factory of the content types for the item. The item compositions of
+ * the same type could be reused more efficiently. Note that null is a valid type and items of such
+ * type will be considered compatible.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> TvLazyListScope.itemsIndexed(
+ items: List<T>,
+ noinline key: ((index: Int, item: T) -> Any)? = null,
+ crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null },
+ crossinline itemContent: @Composable TvLazyListItemScope.(index: Int, item: T) -> Unit
+) = items(
+ count = items.size,
+ key = if (key != null) { index: Int -> key(index, items[index]) } else null,
+ contentType = { index -> contentType(index, items[index]) }
+) {
+ itemContent(it, items[it])
+}
+
+/**
+ * Adds an array of items.
+ *
+ * @param items the data array
+ * @param key a factory of stable and unique keys representing the item. Using the same key
+ * for multiple items in the list is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the list will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
+ * @param contentType a factory of the content types for the item. The item compositions of
+ * the same type could be reused more efficiently. Note that null is a valid type and items of such
+ * type will be considered compatible.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> TvLazyListScope.items(
+ items: Array<T>,
+ noinline key: ((item: T) -> Any)? = null,
+ noinline contentType: (item: T) -> Any? = { null },
+ crossinline itemContent: @Composable TvLazyListItemScope.(item: T) -> Unit
+) = items(
+ count = items.size,
+ key = if (key != null) { index: Int -> key(items[index]) } else null,
+ contentType = { index: Int -> contentType(items[index]) }
+) {
+ itemContent(items[it])
+}
+
+/**
+ * Adds an array of items where the content of an item is aware of its index.
+ *
+ * @param items the data array
+ * @param key a factory of stable and unique keys representing the item. Using the same key
+ * for multiple items in the list is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the list will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
+ * @param contentType a factory of the content types for the item. The item compositions of
+ * the same type could be reused more efficiently. Note that null is a valid type and items of such
+ * type will be considered compatible.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> TvLazyListScope.itemsIndexed(
+ items: Array<T>,
+ noinline key: ((index: Int, item: T) -> Any)? = null,
+ crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null },
+ crossinline itemContent: @Composable TvLazyListItemScope.(index: Int, item: T) -> Unit
+) = items(
+ count = items.size,
+ key = if (key != null) { index: Int -> key(index, items[index]) } else null,
+ contentType = { index -> contentType(index, items[index]) }
+) {
+ itemContent(it, items[it])
+}
+
+/**
+ * The horizontally scrolling list that only composes and lays out the currently visible items.
+ * The [content] block defines a DSL which allows you to emit items of different types. For
+ * example you can use [TvLazyListScope.item] to add a single item and [TvLazyListScope.items] to add
+ * a list of items.
+ *
+ * @param modifier the modifier to apply to this layout
+ * @param state the state object to be used to control or observe the list's state
+ * @param contentPadding a padding around the whole content. This will add padding for the
+ * content after it has been clipped, which is not possible via [modifier] param. You can use it
+ * to add a padding before the first item or after the last one. If you want to add a spacing
+ * between each item use [horizontalArrangement].
+ * @param reverseLayout reverse the direction of scrolling and layout. When `true`, items are
+ * laid out in the reverse order and [TvLazyListState.firstVisibleItemIndex] == 0 means
+ * that row is scrolled to the end. Note that [reverseLayout] does not change the behavior of
+ * [horizontalArrangement], e.g. with [Arrangement.Start] [123###] becomes [321###].
+ * @param horizontalArrangement The horizontal arrangement of the layout's children. This allows
+ * to add a spacing between items and specify the arrangement of the items when we have not enough
+ * of them to fill the whole minimum size.
+ * @param verticalAlignment the vertical alignment applied to the items
+ * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions
+ * is allowed. You can still scroll programmatically using the state even when it is disabled.
+ * @param pivotOffsets offsets of child element within the parent and starting edge of the child
+ * from the pivot defined by the parentOffset.
+ * @param content a block which describes the content. Inside this block you can use methods like
+ * [TvLazyListScope.item] to add a single item or [TvLazyListScope.items] to add a list of items.
+ */
+@Composable
+fun TvLazyRow(
+ modifier: Modifier = Modifier,
+ state: TvLazyListState = rememberTvLazyListState(),
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+ reverseLayout: Boolean = false,
+ horizontalArrangement: Arrangement.Horizontal =
+ if (!reverseLayout) Arrangement.Start else Arrangement.End,
+ verticalAlignment: Alignment.Vertical = Alignment.Top,
+ userScrollEnabled: Boolean = true,
+ pivotOffsets: PivotOffsets = PivotOffsets(),
+ content: TvLazyListScope.() -> Unit
+) {
+ LazyList(
+ modifier = modifier,
+ state = state,
+ contentPadding = contentPadding,
+ verticalAlignment = verticalAlignment,
+ horizontalArrangement = horizontalArrangement,
+ isVertical = false,
+ reverseLayout = reverseLayout,
+ userScrollEnabled = userScrollEnabled,
+ content = content,
+ pivotOffsets = pivotOffsets
+ )
+}
+
+/**
+ * The vertically scrolling list that only composes and lays out the currently visible items.
+ * The [content] block defines a DSL which allows you to emit items of different types. For
+ * example you can use [TvLazyListScope.item] to add a single item and [TvLazyListScope.items] to add
+ * a list of items.
+ *
+ * @param modifier the modifier to apply to this layout.
+ * @param state the state object to be used to control or observe the list's state.
+ * @param contentPadding a padding around the whole content. This will add padding for the.
+ * content after it has been clipped, which is not possible via [modifier] param. You can use it
+ * to add a padding before the first item or after the last one. If you want to add a spacing
+ * between each item use [verticalArrangement].
+ * @param reverseLayout reverse the direction of scrolling and layout. When `true`, items are
+ * laid out in the reverse order and [TvLazyListState.firstVisibleItemIndex] == 0 means
+ * that column is scrolled to the bottom. Note that [reverseLayout] does not change the behavior of
+ * [verticalArrangement],
+ * e.g. with [Arrangement.Top] (top) 123### (bottom) becomes (top) 321### (bottom).
+ * @param verticalArrangement The vertical arrangement of the layout's children. This allows
+ * to add a spacing between items and specify the arrangement of the items when we have not enough
+ * of them to fill the whole minimum size.
+ * @param horizontalAlignment the horizontal alignment applied to the items.
+ * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions
+ * is allowed. You can still scroll programmatically using the state even when it is disabled.
+ * @param content a block which describes the content. Inside this block you can use methods like
+ * @param pivotOffsets offsets of child element within the parent and starting edge of the child
+ * from the pivot defined by the parentOffset.
+ * [TvLazyListScope.item] to add a single item or [TvLazyListScope.items] to add a list of items.
+ */
+@Composable
+fun TvLazyColumn(
+ modifier: Modifier = Modifier,
+ state: TvLazyListState = rememberTvLazyListState(),
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+ reverseLayout: Boolean = false,
+ verticalArrangement: Arrangement.Vertical =
+ if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
+ horizontalAlignment: Alignment.Horizontal = Alignment.Start,
+ userScrollEnabled: Boolean = true,
+ pivotOffsets: PivotOffsets = PivotOffsets(),
+ content: TvLazyListScope.() -> Unit
+) {
+ LazyList(
+ modifier = modifier,
+ state = state,
+ contentPadding = contentPadding,
+ horizontalAlignment = horizontalAlignment,
+ verticalArrangement = verticalArrangement,
+ isVertical = true,
+ reverseLayout = reverseLayout,
+ userScrollEnabled = userScrollEnabled,
+ content = content,
+ pivotOffsets = pivotOffsets
+ )
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyList.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyList.kt
new file mode 100644
index 0000000..6ee45b7
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyList.kt
@@ -0,0 +1,329 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.checkScrollableContainerConstraints
+import androidx.compose.foundation.clipScrollableContainer
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.calculateEndPadding
+import androidx.compose.foundation.layout.calculateStartPadding
+import androidx.compose.foundation.lazy.layout.LazyLayout
+import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
+import androidx.compose.ui.unit.offset
+import androidx.tv.foundation.PivotOffsets
+import androidx.tv.foundation.lazy.LazyListBeyondBoundsInfo
+import androidx.tv.foundation.lazy.lazyListBeyondBoundsModifier
+import androidx.tv.foundation.lazy.lazyListPinningModifier
+import androidx.tv.foundation.marioScrollable
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+internal fun LazyList(
+ /** Modifier to be applied for the inner layout */
+ modifier: Modifier,
+ /** State controlling the scroll position */
+ state: TvLazyListState,
+ /** The inner padding to be added for the whole content(not for each individual item) */
+ contentPadding: PaddingValues,
+ /** reverse the direction of scrolling and layout */
+ reverseLayout: Boolean,
+ /** The layout orientation of the list */
+ isVertical: Boolean,
+ /** Whether scrolling via the user gestures is allowed. */
+ userScrollEnabled: Boolean,
+ /** offsets of child element within the parent and starting edge of the child from the pivot
+ * defined by the parentOffset. */
+ pivotOffsets: PivotOffsets,
+ /** The alignment to align items horizontally. Required when isVertical is true */
+ horizontalAlignment: Alignment.Horizontal? = null,
+ /** The vertical arrangement for items. Required when isVertical is true */
+ verticalArrangement: Arrangement.Vertical? = null,
+ /** The alignment to align items vertically. Required when isVertical is false */
+ verticalAlignment: Alignment.Vertical? = null,
+ /** The horizontal arrangement for items. Required when isVertical is false */
+ horizontalArrangement: Arrangement.Horizontal? = null,
+ /** The content of the list */
+ content: TvLazyListScope.() -> Unit
+) {
+ val itemProvider = rememberItemProvider(state, content)
+ val beyondBoundsInfo = remember { LazyListBeyondBoundsInfo() }
+ val scope = rememberCoroutineScope()
+ val placementAnimator = remember(state, isVertical) {
+ LazyListItemPlacementAnimator(scope, isVertical)
+ }
+ state.placementAnimator = placementAnimator
+
+ val measurePolicy = rememberLazyListMeasurePolicy(
+ itemProvider,
+ state,
+ beyondBoundsInfo,
+ contentPadding,
+ reverseLayout,
+ isVertical,
+ horizontalAlignment,
+ verticalAlignment,
+ horizontalArrangement,
+ verticalArrangement,
+ placementAnimator
+ )
+
+ ScrollPositionUpdater(itemProvider, state)
+
+ val orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal
+
+ LazyLayout(
+ modifier = modifier
+ .then(state.remeasurementModifier)
+ .then(state.awaitLayoutModifier)
+ .lazyListSemantics(
+ itemProvider = itemProvider,
+ state = state,
+ coroutineScope = scope,
+ isVertical = isVertical,
+ reverseScrolling = reverseLayout,
+ userScrollEnabled = userScrollEnabled
+ )
+ .clipScrollableContainer(orientation)
+ .lazyListBeyondBoundsModifier(state, beyondBoundsInfo, reverseLayout)
+ .lazyListPinningModifier(state, beyondBoundsInfo)
+ .marioScrollable(
+ orientation = orientation,
+ reverseDirection = run {
+ // A finger moves with the content, not with the viewport. Therefore,
+ // always reverse once to have "natural" gesture that goes reversed to layout
+ var reverseDirection = !reverseLayout
+ // But if rtl and horizontal, things move the other way around
+ val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
+ if (isRtl && !isVertical) {
+ reverseDirection = !reverseDirection
+ }
+ reverseDirection
+ },
+ state = state,
+ enabled = userScrollEnabled,
+ pivotOffsets = pivotOffsets
+ ),
+ prefetchState = state.prefetchState,
+ measurePolicy = measurePolicy,
+ itemProvider = itemProvider
+ )
+}
+
+/** Extracted to minimize the recomposition scope */
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@ExperimentalFoundationApi
+@Composable
+private fun ScrollPositionUpdater(
+ itemProvider: LazyListItemProvider,
+ state: TvLazyListState
+) {
+ if (itemProvider.itemCount > 0) {
+ state.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
+ }
+}
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@ExperimentalFoundationApi
+@Composable
+private fun rememberLazyListMeasurePolicy(
+ /** Items provider of the list. */
+ itemProvider: LazyListItemProvider,
+ /** The state of the list. */
+ state: TvLazyListState,
+ /** Keeps track of the number of items we measure and place that are beyond visible bounds. */
+ beyondBoundsInfo: LazyListBeyondBoundsInfo,
+ /** The inner padding to be added for the whole content(nor for each individual item) */
+ contentPadding: PaddingValues,
+ /** reverse the direction of scrolling and layout */
+ reverseLayout: Boolean,
+ /** The layout orientation of the list */
+ isVertical: Boolean,
+ /** The alignment to align items horizontally. Required when isVertical is true */
+ horizontalAlignment: Alignment.Horizontal? = null,
+ /** The alignment to align items vertically. Required when isVertical is false */
+ verticalAlignment: Alignment.Vertical? = null,
+ /** The horizontal arrangement for items. Required when isVertical is false */
+ horizontalArrangement: Arrangement.Horizontal? = null,
+ /** The vertical arrangement for items. Required when isVertical is true */
+ verticalArrangement: Arrangement.Vertical? = null,
+ /** Item placement animator. Should be notified with the measuring result */
+ placementAnimator: LazyListItemPlacementAnimator
+) = remember<LazyLayoutMeasureScope.(Constraints) -> MeasureResult>(
+ state,
+ beyondBoundsInfo,
+ contentPadding,
+ reverseLayout,
+ isVertical,
+ horizontalAlignment,
+ verticalAlignment,
+ horizontalArrangement,
+ verticalArrangement,
+ placementAnimator
+) {
+ { containerConstraints ->
+ checkScrollableContainerConstraints(
+ containerConstraints,
+ if (isVertical) Orientation.Vertical else Orientation.Horizontal
+ )
+
+ // resolve content paddings
+ val startPadding =
+ if (isVertical) {
+ contentPadding.calculateLeftPadding(layoutDirection).roundToPx()
+ } else {
+ // in horizontal configuration, padding is reversed by placeRelative
+ contentPadding.calculateStartPadding(layoutDirection).roundToPx()
+ }
+
+ val endPadding =
+ if (isVertical) {
+ contentPadding.calculateRightPadding(layoutDirection).roundToPx()
+ } else {
+ // in horizontal configuration, padding is reversed by placeRelative
+ contentPadding.calculateEndPadding(layoutDirection).roundToPx()
+ }
+ val topPadding = contentPadding.calculateTopPadding().roundToPx()
+ val bottomPadding = contentPadding.calculateBottomPadding().roundToPx()
+ val totalVerticalPadding = topPadding + bottomPadding
+ val totalHorizontalPadding = startPadding + endPadding
+ val totalMainAxisPadding = if (isVertical) totalVerticalPadding else totalHorizontalPadding
+ val beforeContentPadding = when {
+ isVertical && !reverseLayout -> topPadding
+ isVertical && reverseLayout -> bottomPadding
+ !isVertical && !reverseLayout -> startPadding
+ else -> endPadding // !isVertical && reverseLayout
+ }
+ val afterContentPadding = totalMainAxisPadding - beforeContentPadding
+ val contentConstraints =
+ containerConstraints.offset(-totalHorizontalPadding, -totalVerticalPadding)
+
+ state.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
+
+ // Update the state's cached Density
+ state.density = this
+
+ // this will update the scope used by the item composables
+ itemProvider.itemScope.maxWidth = contentConstraints.maxWidth.toDp()
+ itemProvider.itemScope.maxHeight = contentConstraints.maxHeight.toDp()
+
+ val spaceBetweenItemsDp = if (isVertical) {
+ requireNotNull(verticalArrangement).spacing
+ } else {
+ requireNotNull(horizontalArrangement).spacing
+ }
+ val spaceBetweenItems = spaceBetweenItemsDp.roundToPx()
+
+ val itemsCount = itemProvider.itemCount
+
+ // can be negative if the content padding is larger than the max size from constraints
+ val mainAxisAvailableSize = if (isVertical) {
+ containerConstraints.maxHeight - totalVerticalPadding
+ } else {
+ containerConstraints.maxWidth - totalHorizontalPadding
+ }
+ val visualItemOffset = if (!reverseLayout || mainAxisAvailableSize > 0) {
+ IntOffset(startPadding, topPadding)
+ } else {
+ // When layout is reversed and paddings together take >100% of the available space,
+ // layout size is coerced to 0 when positioning. To take that space into account,
+ // we offset start padding by negative space between paddings.
+ IntOffset(
+ if (isVertical) startPadding else startPadding + mainAxisAvailableSize,
+ if (isVertical) topPadding + mainAxisAvailableSize else topPadding
+ )
+ }
+
+ val measuredItemProvider = LazyMeasuredItemProvider(
+ contentConstraints,
+ isVertical,
+ itemProvider,
+ this
+ ) { index, key, placeables ->
+ // we add spaceBetweenItems as an extra spacing for all items apart from the last one so
+ // the lazy list measuring logic will take it into account.
+ val spacing = if (index.value == itemsCount - 1) 0 else spaceBetweenItems
+ LazyMeasuredItem(
+ index = index.value,
+ placeables = placeables,
+ isVertical = isVertical,
+ horizontalAlignment = horizontalAlignment,
+ verticalAlignment = verticalAlignment,
+ layoutDirection = layoutDirection,
+ reverseLayout = reverseLayout,
+ beforeContentPadding = beforeContentPadding,
+ afterContentPadding = afterContentPadding,
+ spacing = spacing,
+ visualOffset = visualItemOffset,
+ key = key,
+ placementAnimator = placementAnimator
+ )
+ }
+ state.premeasureConstraints = measuredItemProvider.childConstraints
+
+ val firstVisibleItemIndex: DataIndex
+ val firstVisibleScrollOffset: Int
+ Snapshot.withoutReadObservation {
+ firstVisibleItemIndex = DataIndex(state.firstVisibleItemIndex)
+ firstVisibleScrollOffset = state.firstVisibleItemScrollOffset
+ }
+
+ measureLazyList(
+ itemsCount = itemsCount,
+ itemProvider = measuredItemProvider,
+ mainAxisAvailableSize = mainAxisAvailableSize,
+ beforeContentPadding = beforeContentPadding,
+ afterContentPadding = afterContentPadding,
+ firstVisibleItemIndex = firstVisibleItemIndex,
+ firstVisibleItemScrollOffset = firstVisibleScrollOffset,
+ scrollToBeConsumed = state.scrollToBeConsumed,
+ constraints = contentConstraints,
+ isVertical = isVertical,
+ headerIndexes = itemProvider.headerIndexes,
+ verticalArrangement = verticalArrangement,
+ horizontalArrangement = horizontalArrangement,
+ reverseLayout = reverseLayout,
+ density = this,
+ placementAnimator = placementAnimator,
+ beyondBoundsInfo = beyondBoundsInfo,
+ layout = { width, height, placement ->
+ layout(
+ containerConstraints.constrainWidth(width + totalHorizontalPadding),
+ containerConstraints.constrainHeight(height + totalVerticalPadding),
+ emptyMap(),
+ placement
+ )
+ }
+ ).also { state.applyMeasureResult(it) }
+ }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListHeaders.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListHeaders.kt
new file mode 100644
index 0000000..d20d6f5
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListHeaders.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.ui.util.fastForEachIndexed
+
+/**
+ * This method finds the sticky header in composedItems list or composes the header item if needed.
+ *
+ * @param composedVisibleItems list of items already composed and expected to be visible. if the
+ * header wasn't in this list but is needed the header will be added as the first item in this list.
+ * @param itemProvider the provider so we can compose a header if it wasn't composed already
+ * @param headerIndexes list of indexes of headers. Must be sorted.
+ * @param beforeContentPadding the padding before the first item in the list
+ */
+internal fun findOrComposeLazyListHeader(
+ composedVisibleItems: MutableList<LazyListPositionedItem>,
+ itemProvider: LazyMeasuredItemProvider,
+ headerIndexes: List<Int>,
+ beforeContentPadding: Int,
+ layoutWidth: Int,
+ layoutHeight: Int,
+): LazyListPositionedItem? {
+ var currentHeaderOffset: Int = Int.MIN_VALUE
+ var nextHeaderOffset: Int = Int.MIN_VALUE
+
+ var currentHeaderListPosition = -1
+ var nextHeaderListPosition = -1
+ // we use visibleItemsInfo and not firstVisibleItemIndex as visibleItemsInfo list also
+ // contains all the items which are visible in the start content padding area
+ val firstVisible = composedVisibleItems.first().index
+ // find the header which can be displayed
+ for (index in headerIndexes.indices) {
+ if (headerIndexes[index] <= firstVisible) {
+ currentHeaderListPosition = headerIndexes[index]
+ nextHeaderListPosition = headerIndexes.getOrElse(index + 1) { -1 }
+ } else {
+ break
+ }
+ }
+
+ var indexInComposedVisibleItems = -1
+ composedVisibleItems.fastForEachIndexed { index, item ->
+ if (item.index == currentHeaderListPosition) {
+ indexInComposedVisibleItems = index
+ currentHeaderOffset = item.offset
+ } else {
+ if (item.index == nextHeaderListPosition) {
+ nextHeaderOffset = item.offset
+ }
+ }
+ }
+
+ if (currentHeaderListPosition == -1) {
+ // we have no headers needing special handling
+ return null
+ }
+
+ val measuredHeaderItem = itemProvider.getAndMeasure(DataIndex(currentHeaderListPosition))
+
+ var headerOffset = if (currentHeaderOffset != Int.MIN_VALUE) {
+ maxOf(-beforeContentPadding, currentHeaderOffset)
+ } else {
+ -beforeContentPadding
+ }
+ // if we have a next header overlapping with the current header, the next one will be
+ // pushing the current one away from the viewport.
+ if (nextHeaderOffset != Int.MIN_VALUE) {
+ headerOffset = minOf(headerOffset, nextHeaderOffset - measuredHeaderItem.size)
+ }
+
+ return measuredHeaderItem.position(headerOffset, layoutWidth, layoutHeight).also {
+ if (indexInComposedVisibleItems != -1) {
+ composedVisibleItems[indexInComposedVisibleItems] = it
+ } else {
+ composedVisibleItems.add(0, it)
+ }
+ }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemPlacementAnimator.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemPlacementAnimator.kt
new file mode 100644
index 0000000..317b454
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemPlacementAnimator.kt
@@ -0,0 +1,441 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.SpringSpec
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.VisibilityThreshold
+import androidx.compose.animation.core.spring
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.util.fastAny
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastForEachIndexed
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * Handles the item placement animations when it is set via [LazyItemScope.animateItemPlacement].
+ *
+ * This class is responsible for detecting when item position changed, figuring our start/end
+ * offsets and starting the animations.
+ */
+internal class LazyListItemPlacementAnimator(
+ private val scope: CoroutineScope,
+ private val isVertical: Boolean
+) {
+ // state containing an animation and all relevant info for each item.
+ private val keyToItemInfoMap = mutableMapOf<Any, ItemInfo>()
+
+ // snapshot of the key to index map used for the last measuring.
+ private var keyToIndexMap: Map<Any, Int> = emptyMap()
+
+ // keeps the first and the last items positioned in the viewport and their visible part sizes.
+ private var viewportStartItemIndex = -1
+ private var viewportStartItemNotVisiblePartSize = 0
+ private var viewportEndItemIndex = -1
+ private var viewportEndItemNotVisiblePartSize = 0
+
+ // stored to not allocate it every pass.
+ private val positionedKeys = mutableSetOf<Any>()
+
+ /**
+ * Should be called after the measuring so we can detect position changes and start animations.
+ *
+ * Note that this method can compose new item and add it into the [positionedItems] list.
+ */
+ fun onMeasured(
+ consumedScroll: Int,
+ layoutWidth: Int,
+ layoutHeight: Int,
+ reverseLayout: Boolean,
+ positionedItems: MutableList<LazyListPositionedItem>,
+ itemProvider: LazyMeasuredItemProvider,
+ ) {
+ if (!positionedItems.fastAny { it.hasAnimations }) {
+ // no animations specified - no work needed
+ reset()
+ return
+ }
+
+ val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
+
+ // the consumed scroll is considered as a delta we don't need to animate
+ val notAnimatableDelta = (if (reverseLayout) -consumedScroll else consumedScroll).toOffset()
+
+ val newFirstItem = positionedItems.first()
+ val newLastItem = positionedItems.last()
+
+ var totalItemsSize = 0
+ // update known indexes and calculate the average size
+ positionedItems.fastForEach { item ->
+ keyToItemInfoMap[item.key]?.index = item.index
+ totalItemsSize += item.sizeWithSpacings
+ }
+ val averageItemSize = totalItemsSize / positionedItems.size
+
+ positionedKeys.clear()
+ // iterate through the items which are visible (without animated offsets)
+ positionedItems.fastForEach { item ->
+ positionedKeys.add(item.key)
+ val itemInfo = keyToItemInfoMap[item.key]
+ if (itemInfo == null) {
+ // there is no state associated with this item yet
+ if (item.hasAnimations) {
+ val newItemInfo = ItemInfo(item.index)
+ val previousIndex = keyToIndexMap[item.key]
+ val firstPlaceableOffset = item.getOffset(0)
+ val firstPlaceableSize = item.getMainAxisSize(0)
+
+ val targetFirstPlaceableOffsetMainAxis = if (previousIndex == null) {
+ // it is a completely new item. no animation is needed
+ firstPlaceableOffset.mainAxis
+ } else {
+ val fallback = if (!reverseLayout) {
+ firstPlaceableOffset.mainAxis
+ } else {
+ firstPlaceableOffset.mainAxis - item.sizeWithSpacings +
+ firstPlaceableSize
+ }
+ calculateExpectedOffset(
+ index = previousIndex,
+ sizeWithSpacings = item.sizeWithSpacings,
+ averageItemsSize = averageItemSize,
+ scrolledBy = notAnimatableDelta,
+ fallback = fallback,
+ reverseLayout = reverseLayout,
+ mainAxisLayoutSize = mainAxisLayoutSize,
+ visibleItems = positionedItems
+ ) + if (reverseLayout) {
+ item.size - firstPlaceableSize
+ } else {
+ 0
+ }
+ }
+ val targetFirstPlaceableOffset = if (isVertical) {
+ firstPlaceableOffset.copy(y = targetFirstPlaceableOffsetMainAxis)
+ } else {
+ firstPlaceableOffset.copy(x = targetFirstPlaceableOffsetMainAxis)
+ }
+
+ // populate placeable info list
+ repeat(item.placeablesCount) { placeableIndex ->
+ val diffToFirstPlaceableOffset =
+ item.getOffset(placeableIndex) - firstPlaceableOffset
+ newItemInfo.placeables.add(
+ PlaceableInfo(
+ targetFirstPlaceableOffset + diffToFirstPlaceableOffset,
+ item.getMainAxisSize(placeableIndex)
+ )
+ )
+ }
+ keyToItemInfoMap[item.key] = newItemInfo
+ startAnimationsIfNeeded(item, newItemInfo)
+ }
+ } else {
+ if (item.hasAnimations) {
+ // apply new not animatable offset
+ itemInfo.notAnimatableDelta += notAnimatableDelta
+ startAnimationsIfNeeded(item, itemInfo)
+ } else {
+ // no animation, clean up if needed
+ keyToItemInfoMap.remove(item.key)
+ }
+ }
+ }
+
+ // previously we were animating items which are visible in the end state so we had to
+ // compare the current state with the state used for the previous measuring.
+ // now we will animate disappearing items so the current state is their starting state
+ // so we can update current viewport start/end items
+ if (!reverseLayout) {
+ viewportStartItemIndex = newFirstItem.index
+ viewportStartItemNotVisiblePartSize = newFirstItem.offset
+ viewportEndItemIndex = newLastItem.index
+ viewportEndItemNotVisiblePartSize =
+ newLastItem.offset + newLastItem.sizeWithSpacings - mainAxisLayoutSize
+ } else {
+ viewportStartItemIndex = newLastItem.index
+ viewportStartItemNotVisiblePartSize =
+ mainAxisLayoutSize - newLastItem.offset - newLastItem.size
+ viewportEndItemIndex = newFirstItem.index
+ viewportEndItemNotVisiblePartSize =
+ -newFirstItem.offset + (newFirstItem.sizeWithSpacings - newFirstItem.size)
+ }
+
+ val iterator = keyToItemInfoMap.iterator()
+ while (iterator.hasNext()) {
+ val entry = iterator.next()
+ if (!positionedKeys.contains(entry.key)) {
+ // found an item which was in our map previously but is not a part of the
+ // positionedItems now
+ val itemInfo = entry.value
+ // apply new not animatable delta for this item
+ itemInfo.notAnimatableDelta += notAnimatableDelta
+
+ val index = itemProvider.keyToIndexMap[entry.key]
+
+ // whether at least one placeable is within the viewport bounds.
+ // this usually means that we will start animation for it right now
+ val withinBounds = itemInfo.placeables.fastAny {
+ val currentTarget = it.targetOffset + itemInfo.notAnimatableDelta
+ currentTarget.mainAxis + it.size > 0 &&
+ currentTarget.mainAxis < mainAxisLayoutSize
+ }
+
+ // whether the animation associated with the item has been finished
+ val isFinished = !itemInfo.placeables.fastAny { it.inProgress }
+
+ if ((!withinBounds && isFinished) ||
+ index == null ||
+ itemInfo.placeables.isEmpty()
+ ) {
+ iterator.remove()
+ } else {
+
+ val measuredItem = itemProvider.getAndMeasure(DataIndex(index))
+
+ // calculate the target offset for the animation.
+ val absoluteTargetOffset = calculateExpectedOffset(
+ index = index,
+ sizeWithSpacings = measuredItem.sizeWithSpacings,
+ averageItemsSize = averageItemSize,
+ scrolledBy = notAnimatableDelta,
+ fallback = mainAxisLayoutSize,
+ reverseLayout = reverseLayout,
+ mainAxisLayoutSize = mainAxisLayoutSize,
+ visibleItems = positionedItems
+ )
+ val targetOffset = if (reverseLayout) {
+ mainAxisLayoutSize - absoluteTargetOffset - measuredItem.size
+ } else {
+ absoluteTargetOffset
+ }
+
+ val item = measuredItem.position(targetOffset, layoutWidth, layoutHeight)
+ positionedItems.add(item)
+ startAnimationsIfNeeded(item, itemInfo)
+ }
+ }
+ }
+
+ keyToIndexMap = itemProvider.keyToIndexMap
+ }
+
+ /**
+ * Returns the current animated item placement offset. By calling it only during the layout
+ * phase we can skip doing remeasure on every animation frame.
+ */
+ fun getAnimatedOffset(
+ key: Any,
+ placeableIndex: Int,
+ minOffset: Int,
+ maxOffset: Int,
+ rawOffset: IntOffset
+ ): IntOffset {
+ val itemInfo = keyToItemInfoMap[key] ?: return rawOffset
+ val item = itemInfo.placeables[placeableIndex]
+ val currentValue = item.animatedOffset.value + itemInfo.notAnimatableDelta
+ val currentTarget = item.targetOffset + itemInfo.notAnimatableDelta
+
+ // cancel the animation if it is fully out of the bounds.
+ if (item.inProgress &&
+ ((currentTarget.mainAxis < minOffset && currentValue.mainAxis < minOffset) ||
+ (currentTarget.mainAxis > maxOffset && currentValue.mainAxis > maxOffset))
+ ) {
+ scope.launch {
+ item.animatedOffset.snapTo(item.targetOffset)
+ item.inProgress = false
+ }
+ }
+
+ return currentValue
+ }
+
+ /**
+ * Should be called when the animations are not needed for the next positions change,
+ * for example when we snap to a new position.
+ */
+ fun reset() {
+ keyToItemInfoMap.clear()
+ keyToIndexMap = emptyMap()
+ viewportStartItemIndex = -1
+ viewportStartItemNotVisiblePartSize = 0
+ viewportEndItemIndex = -1
+ viewportEndItemNotVisiblePartSize = 0
+ }
+
+ /**
+ * Estimates the outside of the viewport offset for the item. Used to understand from
+ * where to start animation for the item which wasn't visible previously or where it should
+ * end for the item which is not going to be visible in the end.
+ */
+ private fun calculateExpectedOffset(
+ index: Int,
+ sizeWithSpacings: Int,
+ averageItemsSize: Int,
+ scrolledBy: IntOffset,
+ reverseLayout: Boolean,
+ mainAxisLayoutSize: Int,
+ fallback: Int,
+ visibleItems: List<LazyListPositionedItem>
+ ): Int {
+ val afterViewportEnd =
+ if (!reverseLayout) viewportEndItemIndex < index else viewportEndItemIndex > index
+ val beforeViewportStart =
+ if (!reverseLayout) viewportStartItemIndex > index else viewportStartItemIndex < index
+ return when {
+ afterViewportEnd -> {
+ var itemsSizes = 0
+ // add sizes of the items between the last visible one and this one.
+ val range = if (!reverseLayout) {
+ viewportEndItemIndex + 1 until index
+ } else {
+ index + 1 until viewportEndItemIndex
+ }
+ for (i in range) {
+ itemsSizes += visibleItems.getItemSize(
+ itemIndex = i,
+ fallback = averageItemsSize
+ )
+ }
+ mainAxisLayoutSize + viewportEndItemNotVisiblePartSize + itemsSizes +
+ scrolledBy.mainAxis
+ }
+ beforeViewportStart -> {
+ // add the size of this item as we need the start offset of this item.
+ var itemsSizes = sizeWithSpacings
+ // add sizes of the items between the first visible one and this one.
+ val range = if (!reverseLayout) {
+ index + 1 until viewportStartItemIndex
+ } else {
+ viewportStartItemIndex + 1 until index
+ }
+ for (i in range) {
+ itemsSizes += visibleItems.getItemSize(
+ itemIndex = i,
+ fallback = averageItemsSize
+ )
+ }
+ viewportStartItemNotVisiblePartSize - itemsSizes + scrolledBy.mainAxis
+ }
+ else -> {
+ fallback
+ }
+ }
+ }
+
+ private fun List<LazyListPositionedItem>.getItemSize(itemIndex: Int, fallback: Int): Int {
+ if (isEmpty() || itemIndex < first().index || itemIndex > last().index) return fallback
+ if ((itemIndex - first().index) < (last().index - itemIndex)) {
+ for (index in indices) {
+ val item = get(index)
+ if (item.index == itemIndex) return item.sizeWithSpacings
+ if (item.index > itemIndex) break
+ }
+ } else {
+ for (index in lastIndex downTo 0) {
+ val item = get(index)
+ if (item.index == itemIndex) return item.sizeWithSpacings
+ if (item.index < itemIndex) break
+ }
+ }
+ return fallback
+ }
+
+ private fun startAnimationsIfNeeded(item: LazyListPositionedItem, itemInfo: ItemInfo) {
+ // first we make sure our item info is up to date (has the item placeables count)
+ while (itemInfo.placeables.size > item.placeablesCount) {
+ itemInfo.placeables.removeLast()
+ }
+ while (itemInfo.placeables.size < item.placeablesCount) {
+ val newPlaceableInfoIndex = itemInfo.placeables.size
+ val rawOffset = item.getOffset(newPlaceableInfoIndex)
+ itemInfo.placeables.add(
+ PlaceableInfo(
+ rawOffset - itemInfo.notAnimatableDelta,
+ item.getMainAxisSize(newPlaceableInfoIndex)
+ )
+ )
+ }
+
+ itemInfo.placeables.fastForEachIndexed { index, placeableInfo ->
+ val currentTarget = placeableInfo.targetOffset + itemInfo.notAnimatableDelta
+ val currentOffset = item.getOffset(index)
+ placeableInfo.size = item.getMainAxisSize(index)
+ val animationSpec = item.getAnimationSpec(index)
+ if (currentTarget != currentOffset) {
+ placeableInfo.targetOffset = currentOffset - itemInfo.notAnimatableDelta
+ if (animationSpec != null) {
+ placeableInfo.inProgress = true
+ scope.launch {
+ val finalSpec = if (placeableInfo.animatedOffset.isRunning) {
+ // when interrupted, use the default spring, unless the spec is a spring.
+ if (animationSpec is SpringSpec<IntOffset>) animationSpec else
+ InterruptionSpec
+ } else {
+ animationSpec
+ }
+
+ try {
+ placeableInfo.animatedOffset.animateTo(
+ placeableInfo.targetOffset,
+ finalSpec
+ )
+ placeableInfo.inProgress = false
+ } catch (_: CancellationException) {
+ // we don't reset inProgress in case of cancellation as it means
+ // there is a new animation started which would reset it later
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun Int.toOffset() =
+ IntOffset(if (isVertical) 0 else this, if (!isVertical) 0 else this)
+
+ private val IntOffset.mainAxis get() = if (isVertical) y else x
+}
+
+private class ItemInfo(var index: Int) {
+ var notAnimatableDelta: IntOffset = IntOffset.Zero
+ val placeables = mutableListOf<PlaceableInfo>()
+}
+
+private class PlaceableInfo(
+ initialOffset: IntOffset,
+ var size: Int
+) {
+ val animatedOffset = Animatable(initialOffset, IntOffset.VectorConverter)
+ var targetOffset: IntOffset = initialOffset
+ var inProgress by mutableStateOf(false)
+}
+
+/**
+ * We switch to this spec when a duration based animation is being interrupted.
+ */
+private val InterruptionSpec = spring(
+ stiffness = Spring.StiffnessMediumLow,
+ visibilityThreshold = IntOffset.VisibilityThreshold
+)
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemProvider.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemProvider.kt
new file mode 100644
index 0000000..7d4137f
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemProvider.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@ExperimentalFoundationApi
+internal sealed interface LazyListItemProvider : LazyLayoutItemProvider {
+ /** The list of indexes of the sticky header items */
+ val headerIndexes: List<Int>
+ /** The scope used by the item content lambdas */
+ val itemScope: TvLazyListItemScopeImpl
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProviderImpl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemsProviderImpl.kt
similarity index 63%
rename from compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProviderImpl.kt
rename to tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemsProviderImpl.kt
index 2229a03..1e9966d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProviderImpl.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemsProviderImpl.kt
@@ -14,41 +14,50 @@
* limitations under the License.
*/
-package androidx.compose.foundation.lazy
+package androidx.tv.foundation.lazy.list
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.layout.IntervalList
-import androidx.compose.foundation.lazy.layout.calculateNearestItemsRange
import androidx.compose.foundation.lazy.layout.getDefaultLazyLayoutKey
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.structuralEqualityPolicy
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.runtime.snapshots.Snapshot
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
@ExperimentalFoundationApi
@Composable
internal fun rememberItemProvider(
- state: LazyListState,
- content: LazyListScope.() -> Unit
+ state: TvLazyListState,
+ content: TvLazyListScope.() -> Unit
): LazyListItemProvider {
val latestContent = rememberUpdatedState(content)
-
+ // mutableState + LaunchedEffect below are used instead of derivedStateOf to ensure that update
+ // of derivedState in return expr will only happen after the state value has been changed.
val nearestItemsRangeState = remember(state) {
- derivedStateOf(structuralEqualityPolicy()) {
- calculateNearestItemsRange(
- slidingWindowSize = NearestItemsSlidingWindowSize,
- extraItemCount = NearestItemsExtraItemCount,
- firstVisibleItem = state.firstVisibleItemIndex
- )
- }
+ mutableStateOf(
+ Snapshot.withoutReadObservation {
+ // State read is observed in composition, causing it to recompose 1 additional time.
+ calculateNearestItemsRange(state.firstVisibleItemIndex)
+ }
+ )
}
+ LaunchedEffect(nearestItemsRangeState) {
+ snapshotFlow { calculateNearestItemsRange(state.firstVisibleItemIndex) }
+ // MutableState's SnapshotMutationPolicy will make sure the provider is only
+ // recreated when the state is updated with a new range.
+ .collect { nearestItemsRangeState.value = it }
+ }
return remember(nearestItemsRangeState) {
LazyListItemProviderImpl(
derivedStateOf {
- val listScope = LazyListScopeImpl().apply(latestContent.value)
+ val listScope = TvLazyListScopeImpl().apply(latestContent.value)
LazyListItemsSnapshot(
listScope.intervals,
listScope.headerIndexes,
@@ -59,6 +68,7 @@
}
}
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
@ExperimentalFoundationApi
internal class LazyListItemsSnapshot(
private val intervals: IntervalList<LazyListIntervalContent>,
@@ -75,7 +85,7 @@
}
@Composable
- fun Item(scope: LazyItemScope, index: Int) {
+ fun Item(scope: TvLazyListItemScope, index: Int) {
val interval = intervals[index]
val localIntervalIndex = index - interval.startIndex
interval.value.item.invoke(scope, localIntervalIndex)
@@ -90,16 +100,17 @@
}
}
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
@ExperimentalFoundationApi
internal class LazyListItemProviderImpl(
private val itemsSnapshot: State<LazyListItemsSnapshot>
) : LazyListItemProvider {
- override val itemScope = LazyItemScopeImpl()
+ override val itemScope = TvLazyListItemScopeImpl()
override val headerIndexes: List<Int> get() = itemsSnapshot.value.headerIndexes
- override val itemCount get() = itemsSnapshot.value.itemsCount
+ override val itemCount get() = itemsSnapshot.value.itemsCount
override fun getKey(index: Int) = itemsSnapshot.value.getKey(index)
@@ -113,11 +124,6 @@
override fun getContentType(index: Int) = itemsSnapshot.value.getContentType(index)
}
-/**
- * Traverses the interval [list] in order to create a mapping from the key to the index for all
- * the indexes in the passed [range].
- * The returned map will not contain the values for intervals with no key mapping provided.
- */
@ExperimentalFoundationApi
internal fun generateKeyToIndexMap(
range: IntRange,
@@ -148,12 +154,26 @@
}
/**
+ * Returns a range of indexes which contains at least [ExtraItemsNearTheSlidingWindow] items near
+ * the first visible item. It is optimized to return the same range for small changes in the
+ * firstVisibleItem value so we do not regenerate the map on each scroll.
+ */
+private fun calculateNearestItemsRange(firstVisibleItem: Int): IntRange {
+ val slidingWindowStart = VisibleItemsSlidingWindowSize *
+ (firstVisibleItem / VisibleItemsSlidingWindowSize)
+
+ val start = maxOf(slidingWindowStart - ExtraItemsNearTheSlidingWindow, 0)
+ val end = slidingWindowStart + VisibleItemsSlidingWindowSize + ExtraItemsNearTheSlidingWindow
+ return start until end
+}
+
+/**
* We use the idea of sliding window as an optimization, so user can scroll up to this number of
* items until we have to regenerate the key to index map.
*/
-private const val NearestItemsSlidingWindowSize = 30
+private val VisibleItemsSlidingWindowSize = 30
/**
* The minimum amount of items near the current first visible item we want to have mapping for.
*/
-private const val NearestItemsExtraItemCount = 100
+private val ExtraItemsNearTheSlidingWindow = 100
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt
new file mode 100644
index 0000000..01fbb22
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt
@@ -0,0 +1,424 @@
+/*
+ * Copyright 2020 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.tv.foundation.lazy.list
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
+import androidx.compose.ui.util.fastForEach
+import androidx.tv.foundation.lazy.LazyListBeyondBoundsInfo
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+import kotlin.math.abs
+import kotlin.math.min
+import kotlin.math.roundToInt
+import kotlin.math.sign
+
+/**
+ * Measures and calculates the positions for the requested items. The result is produced
+ * as a [LazyListMeasureResult] which contains all the calculations.
+ */
+internal fun measureLazyList(
+ itemsCount: Int,
+ itemProvider: LazyMeasuredItemProvider,
+ mainAxisAvailableSize: Int,
+ beforeContentPadding: Int,
+ afterContentPadding: Int,
+ firstVisibleItemIndex: DataIndex,
+ firstVisibleItemScrollOffset: Int,
+ scrollToBeConsumed: Float,
+ constraints: Constraints,
+ isVertical: Boolean,
+ headerIndexes: List<Int>,
+ verticalArrangement: Arrangement.Vertical?,
+ horizontalArrangement: Arrangement.Horizontal?,
+ reverseLayout: Boolean,
+ density: Density,
+ placementAnimator: LazyListItemPlacementAnimator,
+ beyondBoundsInfo: LazyListBeyondBoundsInfo,
+ layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
+): LazyListMeasureResult {
+ require(beforeContentPadding >= 0)
+ require(afterContentPadding >= 0)
+ if (itemsCount <= 0) {
+ // empty data set. reset the current scroll and report zero size
+ return LazyListMeasureResult(
+ firstVisibleItem = null,
+ firstVisibleItemScrollOffset = 0,
+ canScrollForward = false,
+ consumedScroll = 0f,
+ measureResult = layout(constraints.minWidth, constraints.minHeight) {},
+ visibleItemsInfo = emptyList(),
+ viewportStartOffset = -beforeContentPadding,
+ viewportEndOffset = mainAxisAvailableSize + afterContentPadding,
+ totalItemsCount = 0,
+ reverseLayout = reverseLayout,
+ orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
+ afterContentPadding = afterContentPadding
+ )
+ } else {
+ var currentFirstItemIndex = firstVisibleItemIndex
+ var currentFirstItemScrollOffset = firstVisibleItemScrollOffset
+ if (currentFirstItemIndex.value >= itemsCount) {
+ // the data set has been updated and now we have less items that we were
+ // scrolled to before
+ currentFirstItemIndex = DataIndex(itemsCount - 1)
+ currentFirstItemScrollOffset = 0
+ }
+
+ // represents the real amount of scroll we applied as a result of this measure pass.
+ var scrollDelta = scrollToBeConsumed.roundToInt()
+
+ // applying the whole requested scroll offset. we will figure out if we can't consume
+ // all of it later
+ currentFirstItemScrollOffset -= scrollDelta
+
+ // if the current scroll offset is less than minimally possible
+ if (currentFirstItemIndex == DataIndex(0) && currentFirstItemScrollOffset < 0) {
+ scrollDelta += currentFirstItemScrollOffset
+ currentFirstItemScrollOffset = 0
+ }
+
+ // this will contain all the MeasuredItems representing the visible items
+ val visibleItems = mutableListOf<LazyMeasuredItem>()
+
+ // include the start padding so we compose items in the padding area. before starting
+ // scrolling forward we would remove it back
+ currentFirstItemScrollOffset -= beforeContentPadding
+
+ // define min and max offsets (min offset currently includes beforeContentPadding)
+ val minOffset = -beforeContentPadding
+ val maxOffset = mainAxisAvailableSize
+
+ // max of cross axis sizes of all visible items
+ var maxCrossAxis = 0
+
+ // we had scrolled backward or we compose items in the start padding area, which means
+ // items before current firstItemScrollOffset should be visible. compose them and update
+ // firstItemScrollOffset
+ while (currentFirstItemScrollOffset < 0 && currentFirstItemIndex > DataIndex(0)) {
+ val previous = DataIndex(currentFirstItemIndex.value - 1)
+ val measuredItem = itemProvider.getAndMeasure(previous)
+ visibleItems.add(0, measuredItem)
+ maxCrossAxis = maxOf(maxCrossAxis, measuredItem.crossAxisSize)
+ currentFirstItemScrollOffset += measuredItem.sizeWithSpacings
+ currentFirstItemIndex = previous
+ }
+ // if we were scrolled backward, but there were not enough items before. this means
+ // not the whole scroll was consumed
+ if (currentFirstItemScrollOffset < minOffset) {
+ scrollDelta += currentFirstItemScrollOffset
+ currentFirstItemScrollOffset = minOffset
+ }
+
+ // neutralize previously added start padding as we stopped filling the before content padding
+ currentFirstItemScrollOffset += beforeContentPadding
+
+ var index = currentFirstItemIndex
+ val maxMainAxis = (maxOffset + afterContentPadding).coerceAtLeast(0)
+ var currentMainAxisOffset = -currentFirstItemScrollOffset
+
+ // first we need to skip items we already composed while composing backward
+ visibleItems.fastForEach {
+ index++
+ currentMainAxisOffset += it.sizeWithSpacings
+ }
+
+ // then composing visible items forward until we fill the whole viewport.
+ // we want to have at least one item in visibleItems even if in fact all the items are
+ // offscreen, this can happen if the content padding is larger than the available size.
+ while ((currentMainAxisOffset <= maxMainAxis || visibleItems.isEmpty()) &&
+ index.value < itemsCount
+ ) {
+ val measuredItem = itemProvider.getAndMeasure(index)
+ currentMainAxisOffset += measuredItem.sizeWithSpacings
+
+ if (currentMainAxisOffset <= minOffset && index.value != itemsCount - 1) {
+ // this item is offscreen and will not be placed. advance firstVisibleItemIndex
+ currentFirstItemIndex = index + 1
+ currentFirstItemScrollOffset -= measuredItem.sizeWithSpacings
+ } else {
+ maxCrossAxis = maxOf(maxCrossAxis, measuredItem.crossAxisSize)
+ visibleItems.add(measuredItem)
+ }
+
+ index++
+ }
+
+ // we didn't fill the whole viewport with items starting from firstVisibleItemIndex.
+ // lets try to scroll back if we have enough items before firstVisibleItemIndex.
+ if (currentMainAxisOffset < maxOffset) {
+ val toScrollBack = maxOffset - currentMainAxisOffset
+ currentFirstItemScrollOffset -= toScrollBack
+ currentMainAxisOffset += toScrollBack
+ while (currentFirstItemScrollOffset < beforeContentPadding &&
+ currentFirstItemIndex > DataIndex(0)
+ ) {
+ val previousIndex = DataIndex(currentFirstItemIndex.value - 1)
+ val measuredItem = itemProvider.getAndMeasure(previousIndex)
+ visibleItems.add(0, measuredItem)
+ maxCrossAxis = maxOf(maxCrossAxis, measuredItem.crossAxisSize)
+ currentFirstItemScrollOffset += measuredItem.sizeWithSpacings
+ currentFirstItemIndex = previousIndex
+ }
+ scrollDelta += toScrollBack
+ if (currentFirstItemScrollOffset < 0) {
+ scrollDelta += currentFirstItemScrollOffset
+ currentMainAxisOffset += currentFirstItemScrollOffset
+ currentFirstItemScrollOffset = 0
+ }
+ }
+
+ // report the amount of pixels we consumed. scrollDelta can be smaller than
+ // scrollToBeConsumed if there were not enough items to fill the offered space or it
+ // can be larger if items were resized, or if, for example, we were previously
+ // displaying the item 15, but now we have only 10 items in total in the data set.
+ val consumedScroll = if (scrollToBeConsumed.roundToInt().sign == scrollDelta.sign &&
+ abs(scrollToBeConsumed.roundToInt()) >= abs(scrollDelta)
+ ) {
+ scrollDelta.toFloat()
+ } else {
+ scrollToBeConsumed
+ }
+
+ // the initial offset for items from visibleItems list
+ val visibleItemsScrollOffset = -currentFirstItemScrollOffset
+ var firstItem = visibleItems.first()
+
+ // even if we compose items to fill before content padding we should ignore items fully
+ // located there for the state's scroll position calculation (first item + first offset)
+ if (beforeContentPadding > 0) {
+ for (i in visibleItems.indices) {
+ val size = visibleItems[i].sizeWithSpacings
+ if (currentFirstItemScrollOffset != 0 && size <= currentFirstItemScrollOffset &&
+ i != visibleItems.lastIndex
+ ) {
+ currentFirstItemScrollOffset -= size
+ firstItem = visibleItems[i + 1]
+ } else {
+ break
+ }
+ }
+ }
+
+ // Compose extra items before or after the visible items.
+ fun LazyListBeyondBoundsInfo.startIndex() = min(start, itemsCount - 1)
+ fun LazyListBeyondBoundsInfo.endIndex() = min(end, itemsCount - 1)
+ val extraItemsBefore =
+ if (beyondBoundsInfo.hasIntervals() &&
+ visibleItems.first().index > beyondBoundsInfo.startIndex()) {
+ mutableListOf<LazyMeasuredItem>().apply {
+ for (i in visibleItems.first().index - 1 downTo beyondBoundsInfo.startIndex()) {
+ add(itemProvider.getAndMeasure(DataIndex(i)))
+ }
+ }
+ } else {
+ emptyList()
+ }
+ val extraItemsAfter =
+ if (beyondBoundsInfo.hasIntervals() &&
+ visibleItems.last().index < beyondBoundsInfo.endIndex()) {
+ mutableListOf<LazyMeasuredItem>().apply {
+ for (i in visibleItems.last().index until beyondBoundsInfo.endIndex()) {
+ add(itemProvider.getAndMeasure(DataIndex(i + 1)))
+ }
+ }
+ } else {
+ emptyList()
+ }
+
+ val noExtraItems = firstItem == visibleItems.first() &&
+ extraItemsBefore.isEmpty() &&
+ extraItemsAfter.isEmpty()
+
+ val layoutWidth =
+ constraints.constrainWidth(if (isVertical) maxCrossAxis else currentMainAxisOffset)
+ val layoutHeight =
+ constraints.constrainHeight(if (isVertical) currentMainAxisOffset else maxCrossAxis)
+
+ val positionedItems = calculateItemsOffsets(
+ items = visibleItems,
+ extraItemsBefore = extraItemsBefore,
+ extraItemsAfter = extraItemsAfter,
+ layoutWidth = layoutWidth,
+ layoutHeight = layoutHeight,
+ finalMainAxisOffset = currentMainAxisOffset,
+ maxOffset = maxOffset,
+ itemsScrollOffset = visibleItemsScrollOffset,
+ isVertical = isVertical,
+ verticalArrangement = verticalArrangement,
+ horizontalArrangement = horizontalArrangement,
+ reverseLayout = reverseLayout,
+ density = density,
+ )
+
+ val headerItem = if (headerIndexes.isNotEmpty()) {
+ findOrComposeLazyListHeader(
+ composedVisibleItems = positionedItems,
+ itemProvider = itemProvider,
+ headerIndexes = headerIndexes,
+ beforeContentPadding = beforeContentPadding,
+ layoutWidth = layoutWidth,
+ layoutHeight = layoutHeight
+ )
+ } else {
+ null
+ }
+
+ placementAnimator.onMeasured(
+ consumedScroll = consumedScroll.toInt(),
+ layoutWidth = layoutWidth,
+ layoutHeight = layoutHeight,
+ reverseLayout = reverseLayout,
+ positionedItems = positionedItems,
+ itemProvider = itemProvider
+ )
+
+ return LazyListMeasureResult(
+ firstVisibleItem = firstItem,
+ firstVisibleItemScrollOffset = currentFirstItemScrollOffset,
+ canScrollForward = currentMainAxisOffset > maxOffset,
+ consumedScroll = consumedScroll,
+ measureResult = layout(layoutWidth, layoutHeight) {
+ positionedItems.fastForEach {
+ if (it !== headerItem) {
+ it.place(this)
+ }
+ }
+ // the header item should be placed (drawn) after all other items
+ headerItem?.place(this)
+ },
+ viewportStartOffset = -beforeContentPadding,
+ viewportEndOffset = maxOffset + afterContentPadding,
+ visibleItemsInfo = if (noExtraItems) positionedItems else positionedItems.fastFilter {
+ (it.index >= visibleItems.first().index && it.index <= visibleItems.last().index) ||
+ it === headerItem
+ },
+ totalItemsCount = itemsCount,
+ reverseLayout = reverseLayout,
+ orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
+ afterContentPadding = afterContentPadding
+ )
+ }
+}
+
+/**
+ * Calculates [LazyMeasuredItem]s offsets.
+ */
+private fun calculateItemsOffsets(
+ items: List<LazyMeasuredItem>,
+ extraItemsBefore: List<LazyMeasuredItem>,
+ extraItemsAfter: List<LazyMeasuredItem>,
+ layoutWidth: Int,
+ layoutHeight: Int,
+ finalMainAxisOffset: Int,
+ maxOffset: Int,
+ itemsScrollOffset: Int,
+ isVertical: Boolean,
+ verticalArrangement: Arrangement.Vertical?,
+ horizontalArrangement: Arrangement.Horizontal?,
+ reverseLayout: Boolean,
+ density: Density,
+): MutableList<LazyListPositionedItem> {
+ val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
+ val hasSpareSpace = finalMainAxisOffset < minOf(mainAxisLayoutSize, maxOffset)
+ if (hasSpareSpace) {
+ check(itemsScrollOffset == 0)
+ }
+
+ val positionedItems =
+ ArrayList<LazyListPositionedItem>(items.size + extraItemsBefore.size + extraItemsAfter.size)
+
+ if (hasSpareSpace) {
+ require(extraItemsBefore.isEmpty() && extraItemsAfter.isEmpty())
+
+ val itemsCount = items.size
+ fun Int.reverseAware() =
+ if (!reverseLayout) this else itemsCount - this - 1
+
+ val sizes = IntArray(itemsCount) { index ->
+ items[index.reverseAware()].size
+ }
+ val offsets = IntArray(itemsCount) { 0 }
+ if (isVertical) {
+ with(requireNotNull(verticalArrangement)) {
+ density.arrange(mainAxisLayoutSize, sizes, offsets)
+ }
+ } else {
+ with(requireNotNull(horizontalArrangement)) {
+ // Enforces Ltr layout direction as it is mirrored with placeRelative later.
+ density.arrange(mainAxisLayoutSize, sizes, LayoutDirection.Ltr, offsets)
+ }
+ }
+
+ val reverseAwareOffsetIndices =
+ if (!reverseLayout) offsets.indices else offsets.indices.reversed()
+ for (index in reverseAwareOffsetIndices) {
+ val absoluteOffset = offsets[index]
+ // when reverseLayout == true, offsets are stored in the reversed order to items
+ val item = items[index.reverseAware()]
+ val relativeOffset = if (reverseLayout) {
+ // inverse offset to align with scroll direction for positioning
+ mainAxisLayoutSize - absoluteOffset - item.size
+ } else {
+ absoluteOffset
+ }
+ positionedItems.add(item.position(relativeOffset, layoutWidth, layoutHeight))
+ }
+ } else {
+ var currentMainAxis = itemsScrollOffset
+ extraItemsBefore.fastForEach {
+ currentMainAxis -= it.sizeWithSpacings
+ positionedItems.add(it.position(currentMainAxis, layoutWidth, layoutHeight))
+ }
+
+ currentMainAxis = itemsScrollOffset
+ items.fastForEach {
+ positionedItems.add(it.position(currentMainAxis, layoutWidth, layoutHeight))
+ currentMainAxis += it.sizeWithSpacings
+ }
+
+ extraItemsAfter.fastForEach {
+ positionedItems.add(it.position(currentMainAxis, layoutWidth, layoutHeight))
+ currentMainAxis += it.sizeWithSpacings
+ }
+ }
+ return positionedItems
+}
+
+/**
+ * Returns a list containing only elements matching the given [predicate].
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
+ */
+@OptIn(ExperimentalContracts::class)
+internal fun <T> List<T>.fastFilter(predicate: (T) -> Boolean): List<T> {
+ contract { callsInPlace(predicate) }
+ val target = ArrayList<T>(size)
+ fastForEach {
+ if (predicate(it)) target += (it)
+ }
+ return target
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasureResult.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasureResult.kt
new file mode 100644
index 0000000..16902ec
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasureResult.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.unit.IntSize
+
+/**
+ * The result of the measure pass for lazy list layout.
+ */
+internal class LazyListMeasureResult(
+ // properties defining the scroll position:
+ /** The new first visible item.*/
+ val firstVisibleItem: LazyMeasuredItem?,
+ /** The new value for [TvLazyListState.firstVisibleItemScrollOffset].*/
+ val firstVisibleItemScrollOffset: Int,
+ /** True if there is some space available to continue scrolling in the forward direction.*/
+ val canScrollForward: Boolean,
+ /** The amount of scroll consumed during the measure pass.*/
+ val consumedScroll: Float,
+ /** MeasureResult defining the layout.*/
+ measureResult: MeasureResult,
+ // properties representing the info needed for LazyListLayoutInfo:
+ /** see [TvLazyListLayoutInfo.visibleItemsInfo] */
+ override val visibleItemsInfo: List<TvLazyListItemInfo>,
+ /** see [TvLazyListLayoutInfo.viewportStartOffset] */
+ override val viewportStartOffset: Int,
+ /** see [TvLazyListLayoutInfo.viewportEndOffset] */
+ override val viewportEndOffset: Int,
+ /** see [TvLazyListLayoutInfo.totalItemsCount] */
+ override val totalItemsCount: Int,
+ /** see [TvLazyListLayoutInfo.reverseLayout] */
+ override val reverseLayout: Boolean,
+ /** see [TvLazyListLayoutInfo.orientation] */
+ override val orientation: Orientation,
+ /** see [TvLazyListLayoutInfo.afterContentPadding] */
+ override val afterContentPadding: Int
+) : TvLazyListLayoutInfo, MeasureResult by measureResult {
+ override val viewportSize: IntSize
+ get() = IntSize(width, height)
+ override val beforeContentPadding: Int get() = -viewportStartOffset
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrollPosition.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrollPosition.kt
new file mode 100644
index 0000000..8242e4a
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrollPosition.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.Snapshot
+
+/**
+ * Contains the current scroll position represented by the first visible item index and the first
+ * visible item scroll offset.
+ */
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+internal class LazyListScrollPosition(
+ initialIndex: Int = 0,
+ initialScrollOffset: Int = 0
+) {
+ var index by mutableStateOf(DataIndex(initialIndex))
+
+ var scrollOffset by mutableStateOf(initialScrollOffset)
+ private set
+
+ private var hadFirstNotEmptyLayout = false
+
+ /** The last know key of the item at [index] position. */
+ private var lastKnownFirstItemKey: Any? = null
+
+ /**
+ * Updates the current scroll position based on the results of the last measurement.
+ */
+ fun updateFromMeasureResult(measureResult: LazyListMeasureResult) {
+ lastKnownFirstItemKey = measureResult.firstVisibleItem?.key
+ // we ignore the index and offset from measureResult until we get at least one
+ // measurement with real items. otherwise the initial index and scroll passed to the
+ // state would be lost and overridden with zeros.
+ if (hadFirstNotEmptyLayout || measureResult.totalItemsCount > 0) {
+ hadFirstNotEmptyLayout = true
+ val scrollOffset = measureResult.firstVisibleItemScrollOffset
+ check(scrollOffset >= 0f) { "scrollOffset should be non-negative ($scrollOffset)" }
+ Snapshot.withoutReadObservation {
+ update(
+ DataIndex(measureResult.firstVisibleItem?.index ?: 0),
+ scrollOffset
+ )
+ }
+ }
+ }
+
+ /**
+ * Updates the scroll position - the passed values will be used as a start position for
+ * composing the items during the next measure pass and will be updated by the real
+ * position calculated during the measurement. This means that there is no guarantee that
+ * exactly this index and offset will be applied as it is possible that:
+ * a) there will be no item at this index in reality
+ * b) item at this index will be smaller than the asked scrollOffset, which means we would
+ * switch to the next item
+ * c) there will be not enough items to fill the viewport after the requested index, so we
+ * would have to compose few elements before the asked index, changing the first visible item.
+ */
+ fun requestPosition(index: DataIndex, scrollOffset: Int) {
+ update(index, scrollOffset)
+ // clear the stored key as we have a direct request to scroll to [index] position and the
+ // next [checkIfFirstVisibleItemWasMoved] shouldn't override this.
+ lastKnownFirstItemKey = null
+ }
+
+ /**
+ * In addition to keeping the first visible item index we also store the key of this item.
+ * When the user provided custom keys for the items this mechanism allows us to detect when
+ * there were items added or removed before our current first visible item and keep this item
+ * as the first visible one even given that its index has been changed.
+ */
+ @Suppress("IllegalExperimentalApiUsage") // TODO(b/233188423): Address before moving to beta
+ @ExperimentalFoundationApi
+ fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyListItemProvider) {
+ Snapshot.withoutReadObservation {
+ update(findLazyListIndexByKey(lastKnownFirstItemKey, index, itemProvider), scrollOffset)
+ }
+ }
+
+ private fun update(index: DataIndex, scrollOffset: Int) {
+ require(index.value >= 0f) { "Index should be non-negative (${index.value})" }
+ if (index != this.index) {
+ this.index = index
+ }
+ if (scrollOffset != this.scrollOffset) {
+ this.scrollOffset = scrollOffset
+ }
+ }
+
+ private companion object {
+ /**
+ * Finds a position of the item with the given key in the lists. This logic allows us to
+ * detect when there were items added or removed before our current first item.
+ */
+ @ExperimentalFoundationApi
+ private fun findLazyListIndexByKey(
+ key: Any?,
+ lastKnownIndex: DataIndex,
+ itemProvider: LazyListItemProvider
+ ): DataIndex {
+ if (key == null) {
+ // there were no real item during the previous measure
+ return lastKnownIndex
+ }
+ if (lastKnownIndex.value < itemProvider.itemCount &&
+ key == itemProvider.getKey(lastKnownIndex.value)
+ ) {
+ // this item is still at the same index
+ return lastKnownIndex
+ }
+ val newIndex = itemProvider.keyToIndexMap[key]
+ if (newIndex != null) {
+ return DataIndex(newIndex)
+ }
+ // fallback to the previous index if we don't know the new index of the item
+ return lastKnownIndex
+ }
+ }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrolling.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrolling.kt
new file mode 100644
index 0000000..b78decf
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListScrolling.kt
@@ -0,0 +1,236 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.animation.core.AnimationState
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.animateTo
+import androidx.compose.animation.core.copy
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastFirstOrNull
+import androidx.compose.ui.util.fastSumBy
+import kotlin.coroutines.cancellation.CancellationException
+import kotlin.math.abs
+
+private class ItemFoundInScroll(
+ val item: TvLazyListItemInfo,
+ val previousAnimation: AnimationState<Float, AnimationVector1D>
+) : CancellationException()
+
+private val TargetDistance = 2500.dp
+private val BoundDistance = 1500.dp
+
+private const val DEBUG = false
+private inline fun debugLog(generateMsg: () -> String) {
+ if (DEBUG) {
+ println("LazyListScrolling: ${generateMsg()}")
+ }
+}
+
+internal suspend fun TvLazyListState.doSmoothScrollToItem(
+ index: Int,
+ scrollOffset: Int
+) {
+ require(index >= 0f) { "Index should be non-negative ($index)" }
+ fun getTargetItem() = layoutInfo.visibleItemsInfo.fastFirstOrNull {
+ it.index == index
+ }
+ scroll {
+ try {
+ val targetDistancePx = with(density) { TargetDistance.toPx() }
+ val boundDistancePx = with(density) { BoundDistance.toPx() }
+ var loop = true
+ var anim = AnimationState(0f)
+ val targetItemInitialInfo = getTargetItem()
+ if (targetItemInitialInfo != null) {
+ // It's already visible, just animate directly
+ throw ItemFoundInScroll(targetItemInitialInfo, anim)
+ }
+ val forward = index > firstVisibleItemIndex
+
+ fun isOvershot(): Boolean {
+ // Did we scroll past the item?
+ @Suppress("RedundantIf") // It's way easier to understand the logic this way
+ return if (forward) {
+ if (firstVisibleItemIndex > index) {
+ true
+ } else if (
+ firstVisibleItemIndex == index &&
+ firstVisibleItemScrollOffset > scrollOffset
+ ) {
+ true
+ } else {
+ false
+ }
+ } else { // backward
+ if (firstVisibleItemIndex < index) {
+ true
+ } else if (
+ firstVisibleItemIndex == index &&
+ firstVisibleItemScrollOffset < scrollOffset
+ ) {
+ true
+ } else {
+ false
+ }
+ }
+ }
+
+ var loops = 1
+ while (loop && layoutInfo.totalItemsCount > 0) {
+ val visibleItems = layoutInfo.visibleItemsInfo
+ val averageSize = visibleItems.fastSumBy { it.size } / visibleItems.size
+ val indexesDiff = index - firstVisibleItemIndex
+ val expectedDistance = (averageSize * indexesDiff).toFloat() +
+ scrollOffset - firstVisibleItemScrollOffset
+ val target = if (abs(expectedDistance) < targetDistancePx) {
+ expectedDistance
+ } else {
+ if (forward) targetDistancePx else -targetDistancePx
+ }
+
+ debugLog {
+ "Scrolling to index=$index offset=$scrollOffset from " +
+ "index=$firstVisibleItemIndex offset=$firstVisibleItemScrollOffset with " +
+ "averageSize=$averageSize and calculated target=$target"
+ }
+
+ anim = anim.copy(value = 0f)
+ var prevValue = 0f
+ anim.animateTo(
+ target,
+ sequentialAnimation = (anim.velocity != 0f)
+ ) {
+ // If we haven't found the item yet, check if it's visible.
+ var targetItem = getTargetItem()
+
+ if (targetItem == null) {
+ // Springs can overshoot their target, clamp to the desired range
+ val coercedValue = if (target > 0) {
+ value.coerceAtMost(target)
+ } else {
+ value.coerceAtLeast(target)
+ }
+ val delta = coercedValue - prevValue
+ debugLog {
+ "Scrolling by $delta (target: $target, coercedValue: $coercedValue)"
+ }
+
+ val consumed = scrollBy(delta)
+ targetItem = getTargetItem()
+ if (targetItem != null) {
+ debugLog { "Found the item after performing scrollBy()" }
+ } else if (!isOvershot()) {
+ if (delta != consumed) {
+ debugLog { "Hit end without finding the item" }
+ cancelAnimation()
+ loop = false
+ return@animateTo
+ }
+ prevValue += delta
+ if (forward) {
+ if (value > boundDistancePx) {
+ debugLog { "Struck bound going forward" }
+ cancelAnimation()
+ }
+ } else {
+ if (value < -boundDistancePx) {
+ debugLog { "Struck bound going backward" }
+ cancelAnimation()
+ }
+ }
+
+ // Magic constants for teleportation chosen arbitrarily by experiment
+ if (forward) {
+ if (
+ loops >= 2 &&
+ index - layoutInfo.visibleItemsInfo.last().index > 100
+ ) {
+ // Teleport
+ debugLog { "Teleport forward" }
+ snapToItemIndexInternal(index = index - 100, scrollOffset = 0)
+ }
+ } else {
+ if (
+ loops >= 2 &&
+ layoutInfo.visibleItemsInfo.first().index - index > 100
+ ) {
+ // Teleport
+ debugLog { "Teleport backward" }
+ snapToItemIndexInternal(index = index + 100, scrollOffset = 0)
+ }
+ }
+ }
+ }
+
+ // We don't throw ItemFoundInScroll when we snap, because once we've snapped to
+ // the final position, there's no need to animate to it.
+ if (isOvershot()) {
+ debugLog { "Overshot" }
+ snapToItemIndexInternal(index = index, scrollOffset = scrollOffset)
+ loop = false
+ cancelAnimation()
+ return@animateTo
+ } else if (targetItem != null) {
+ debugLog { "Found item" }
+ throw ItemFoundInScroll(targetItem, anim)
+ }
+ }
+
+ loops++
+ }
+ } catch (itemFound: ItemFoundInScroll) {
+ // We found it, animate to it
+ // Bring to the requested position - will be automatically stopped if not possible
+ val anim = itemFound.previousAnimation.copy(value = 0f)
+ val target = (itemFound.item.offset + scrollOffset).toFloat()
+ var prevValue = 0f
+ debugLog {
+ "Seeking by $target at velocity ${itemFound.previousAnimation.velocity}"
+ }
+ anim.animateTo(target, sequentialAnimation = (anim.velocity != 0f)) {
+ // Springs can overshoot their target, clamp to the desired range
+ val coercedValue = when {
+ target > 0 -> {
+ value.coerceAtMost(target)
+ }
+ target < 0 -> {
+ value.coerceAtLeast(target)
+ }
+ else -> {
+ debugLog { "WARNING: somehow ended up seeking 0px, this shouldn't happen" }
+ 0f
+ }
+ }
+ val delta = coercedValue - prevValue
+ debugLog { "Seeking by $delta (coercedValue = $coercedValue)" }
+ val consumed = scrollBy(delta)
+ if (delta != consumed /* hit the end, stop */ ||
+ coercedValue != value /* would have overshot, stop */
+ ) {
+ cancelAnimation()
+ }
+ prevValue += delta
+ }
+ // Once we're finished the animation, snap to the exact position to account for
+ // rounding error (otherwise we tend to end up with the previous item scrolled the
+ // tiniest bit onscreen)
+ // TODO: prevent temporarily scrolling *past* the item
+ snapToItemIndexInternal(index = index, scrollOffset = scrollOffset)
+ }
+ }
+}
\ No newline at end of file
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt
new file mode 100644
index 0000000..37862be
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt
@@ -0,0 +1,429 @@
+/*
+ * Copyright 2020 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.tv.foundation.lazy.list
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.interaction.InteractionSource
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.OnGloballyPositionedModifier
+import androidx.compose.ui.layout.Remeasurement
+import androidx.compose.ui.layout.RemeasurementModifier
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+import kotlin.math.abs
+
+/**
+ * Creates a [TvLazyListState] that is remembered across compositions.
+ *
+ * Changes to the provided initial values will **not** result in the state being recreated or
+ * changed in any way if it has already been created.
+ *
+ * @param initialFirstVisibleItemIndex the initial value for [TvLazyListState.firstVisibleItemIndex]
+ * @param initialFirstVisibleItemScrollOffset the initial value for
+ * [TvLazyListState.firstVisibleItemScrollOffset]
+ */
+@Composable
+fun rememberTvLazyListState(
+ initialFirstVisibleItemIndex: Int = 0,
+ initialFirstVisibleItemScrollOffset: Int = 0
+): TvLazyListState {
+ return rememberSaveable(saver = TvLazyListState.Saver) {
+ TvLazyListState(
+ initialFirstVisibleItemIndex,
+ initialFirstVisibleItemScrollOffset
+ )
+ }
+}
+
+/**
+ * A state object that can be hoisted to control and observe scrolling.
+ *
+ * In most cases, this will be created via [rememberTvLazyListState].
+ *
+ * @param firstVisibleItemIndex the initial value for [TvLazyListState.firstVisibleItemIndex]
+ * @param firstVisibleItemScrollOffset the initial value for
+ * [TvLazyListState.firstVisibleItemScrollOffset]
+ */
+@OptIn(ExperimentalFoundationApi::class)
+@Stable
+class TvLazyListState constructor(
+ firstVisibleItemIndex: Int = 0,
+ firstVisibleItemScrollOffset: Int = 0
+) : ScrollableState {
+ /**
+ * The holder class for the current scroll position.
+ */
+ private val scrollPosition =
+ LazyListScrollPosition(firstVisibleItemIndex, firstVisibleItemScrollOffset)
+
+ /**
+ * The index of the first item that is visible.
+ *
+ * Note that this property is observable and if you use it in the composable function it will
+ * be recomposed on every change causing potential performance issues.
+ *
+ * If you want to run some side effects like sending an analytics event or updating a state
+ * based on this value consider using "snapshotFlow":
+ * @sample androidx.compose.foundation.samples.UsingListScrollPositionForSideEffectSample
+ *
+ * If you need to use it in the composition then consider wrapping the calculation into a
+ * derived state in order to only have recompositions when the derived value changes:
+ * @sample androidx.compose.foundation.samples.UsingListScrollPositionInCompositionSample
+ */
+ val firstVisibleItemIndex: Int get() = scrollPosition.index.value
+
+ /**
+ * The scroll offset of the first visible item. Scrolling forward is positive - i.e., the
+ * amount that the item is offset backwards.
+ *
+ * Note that this property is observable and if you use it in the composable function it will
+ * be recomposed on every scroll causing potential performance issues.
+ * @see firstVisibleItemIndex for samples with the recommended usage patterns.
+ */
+ val firstVisibleItemScrollOffset: Int get() = scrollPosition.scrollOffset
+
+ /** Backing state for [layoutInfo] */
+ private val layoutInfoState = mutableStateOf<TvLazyListLayoutInfo>(EmptyLazyListLayoutInfo)
+
+ /**
+ * The object of [TvLazyListLayoutInfo] calculated during the last layout pass. For example,
+ * you can use it to calculate what items are currently visible.
+ *
+ * Note that this property is observable and is updated after every scroll or remeasure.
+ * If you use it in the composable function it will be recomposed on every change causing
+ * potential performance issues including infinity recomposition loop.
+ * Therefore, avoid using it in the composition.
+ *
+ * If you want to run some side effects like sending an analytics event or updating a state
+ * based on this value consider using "snapshotFlow":
+ * @sample androidx.compose.foundation.samples.UsingListLayoutInfoForSideEffectSample
+ */
+ val layoutInfo: TvLazyListLayoutInfo get() = layoutInfoState.value
+
+ /**
+ * [InteractionSource] that will be used to dispatch drag events when this
+ * list is being dragged. If you want to know whether the fling (or animated scroll) is in
+ * progress, use [isScrollInProgress].
+ */
+ val interactionSource: InteractionSource get() = internalInteractionSource
+
+ internal val internalInteractionSource: MutableInteractionSource = MutableInteractionSource()
+
+ /**
+ * The amount of scroll to be consumed in the next layout pass. Scrolling forward is negative
+ * - that is, it is the amount that the items are offset in y
+ */
+ internal var scrollToBeConsumed = 0f
+ private set
+
+ /**
+ * Needed for [animateScrollToItem]. Updated on every measure.
+ */
+ internal var density: Density by mutableStateOf(Density(1f, 1f))
+
+ /**
+ * The ScrollableController instance. We keep it as we need to call stopAnimation on it once
+ * we reached the end of the list.
+ */
+ private val scrollableState = ScrollableState { -onScroll(-it) }
+
+ /**
+ * Only used for testing to confirm that we're not making too many measure passes
+ */
+ /*@VisibleForTesting*/
+ internal var numMeasurePasses: Int = 0
+ private set
+
+ /**
+ * Only used for testing to disable prefetching when needed to test the main logic.
+ */
+ /*@VisibleForTesting*/
+ internal var prefetchingEnabled: Boolean = true
+
+ /**
+ * The index scheduled to be prefetched (or the last prefetched index if the prefetch is done).
+ */
+ private var indexToPrefetch = -1
+
+ /**
+ * The handle associated with the current index from [indexToPrefetch].
+ */
+ private var currentPrefetchHandle: LazyLayoutPrefetchState.PrefetchHandle? = null
+
+ /**
+ * Keeps the scrolling direction during the previous calculation in order to be able to
+ * detect the scrolling direction change.
+ */
+ private var wasScrollingForward = false
+
+ /**
+ * The [Remeasurement] object associated with our layout. It allows us to remeasure
+ * synchronously during scroll.
+ */
+ internal var remeasurement: Remeasurement? by mutableStateOf(null)
+ private set
+ /**
+ * The modifier which provides [remeasurement].
+ */
+ internal val remeasurementModifier = object : RemeasurementModifier {
+ override fun onRemeasurementAvailable(remeasurement: Remeasurement) {
+ [email protected] = remeasurement
+ }
+ }
+
+ /**
+ * Provides a modifier which allows to delay some interactions (e.g. scroll)
+ * until layout is ready.
+ */
+ internal val awaitLayoutModifier = AwaitFirstLayoutModifier()
+
+ internal var placementAnimator by mutableStateOf<LazyListItemPlacementAnimator?>(null)
+
+ /**
+ * Constraints passed to the prefetcher for premeasuring the prefetched items.
+ */
+ internal var premeasureConstraints by mutableStateOf(Constraints())
+
+ /**
+ * Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset]
+ * pixels.
+ *
+ * @param index the index to which to scroll. Must be non-negative.
+ * @param scrollOffset the offset that the item should end up after the scroll. Note that
+ * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will
+ * scroll the item further upward (taking it partly offscreen).
+ */
+ suspend fun scrollToItem(
+ /*@IntRange(from = 0)*/
+ index: Int,
+ scrollOffset: Int = 0
+ ) {
+ scroll {
+ snapToItemIndexInternal(index, scrollOffset)
+ }
+ }
+
+ internal fun snapToItemIndexInternal(index: Int, scrollOffset: Int) {
+ scrollPosition.requestPosition(DataIndex(index), scrollOffset)
+ // placement animation is not needed because we snap into a new position.
+ placementAnimator?.reset()
+ remeasurement?.forceRemeasure()
+ }
+
+ /**
+ * Call this function to take control of scrolling and gain the ability to send scroll events
+ * via [ScrollScope.scrollBy]. All actions that change the logical scroll position must be
+ * performed within a [scroll] block (even if they don't call any other methods on this
+ * object) in order to guarantee that mutual exclusion is enforced.
+ *
+ * If [scroll] is called from elsewhere, this will be canceled.
+ */
+ override suspend fun scroll(
+ scrollPriority: MutatePriority,
+ block: suspend ScrollScope.() -> Unit
+ ) {
+ awaitLayoutModifier.waitForFirstLayout()
+ scrollableState.scroll(scrollPriority, block)
+ }
+
+ override fun dispatchRawDelta(delta: Float): Float =
+ scrollableState.dispatchRawDelta(delta)
+
+ override val isScrollInProgress: Boolean
+ get() = scrollableState.isScrollInProgress
+
+ private var canScrollBackward: Boolean = false
+ internal var canScrollForward: Boolean = false
+ private set
+
+ // TODO: Coroutine scrolling APIs will allow this to be private again once we have more
+ // fine-grained control over scrolling
+ /*@VisibleForTesting*/
+ internal fun onScroll(distance: Float): Float {
+ if (distance < 0 && !canScrollForward || distance > 0 && !canScrollBackward) {
+ return 0f
+ }
+ check(abs(scrollToBeConsumed) <= 0.5f) {
+ "entered drag with non-zero pending scroll: $scrollToBeConsumed"
+ }
+ scrollToBeConsumed += distance
+
+ // scrollToBeConsumed will be consumed synchronously during the forceRemeasure invocation
+ // inside measuring we do scrollToBeConsumed.roundToInt() so there will be no scroll if
+ // we have less than 0.5 pixels
+ if (abs(scrollToBeConsumed) > 0.5f) {
+ val preScrollToBeConsumed = scrollToBeConsumed
+ remeasurement?.forceRemeasure()
+ if (prefetchingEnabled) {
+ notifyPrefetch(preScrollToBeConsumed - scrollToBeConsumed)
+ }
+ }
+
+ // here scrollToBeConsumed is already consumed during the forceRemeasure invocation
+ if (abs(scrollToBeConsumed) <= 0.5f) {
+ // We consumed all of it - we'll hold onto the fractional scroll for later, so report
+ // that we consumed the whole thing
+ return distance
+ } else {
+ val scrollConsumed = distance - scrollToBeConsumed
+ // We did not consume all of it - return the rest to be consumed elsewhere (e.g.,
+ // nested scrolling)
+ scrollToBeConsumed = 0f // We're not consuming the rest, give it back
+ return scrollConsumed
+ }
+ }
+
+ private fun notifyPrefetch(delta: Float) {
+ if (!prefetchingEnabled) {
+ return
+ }
+ val info = layoutInfo
+ if (info.visibleItemsInfo.isNotEmpty()) {
+ // check(isActive)
+ val scrollingForward = delta < 0
+ val indexToPrefetch = if (scrollingForward) {
+ info.visibleItemsInfo.last().index + 1
+ } else {
+ info.visibleItemsInfo.first().index - 1
+ }
+ if (indexToPrefetch != this.indexToPrefetch &&
+ indexToPrefetch in 0 until info.totalItemsCount
+ ) {
+ if (wasScrollingForward != scrollingForward) {
+ // the scrolling direction has been changed which means the last prefetched
+ // is not going to be reached anytime soon so it is safer to dispose it.
+ // if this item is already visible it is safe to call the method anyway
+ // as it will be no-op
+ currentPrefetchHandle?.cancel()
+ }
+ this.wasScrollingForward = scrollingForward
+ this.indexToPrefetch = indexToPrefetch
+ currentPrefetchHandle = prefetchState.schedulePrefetch(
+ indexToPrefetch, premeasureConstraints
+ )
+ }
+ }
+ }
+
+ internal val prefetchState = LazyLayoutPrefetchState()
+
+ /**
+ * Animate (smooth scroll) to the given item.
+ *
+ * @param index the index to which to scroll. Must be non-negative.
+ * @param scrollOffset the offset that the item should end up after the scroll. Note that
+ * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will
+ * scroll the item further upward (taking it partly offscreen).
+ */
+ suspend fun animateScrollToItem(
+ /*@IntRange(from = 0)*/
+ index: Int,
+ scrollOffset: Int = 0
+ ) {
+ doSmoothScrollToItem(index, scrollOffset)
+ }
+
+ /**
+ * Updates the state with the new calculated scroll position and consumed scroll.
+ */
+ internal fun applyMeasureResult(result: LazyListMeasureResult) {
+ scrollPosition.updateFromMeasureResult(result)
+ scrollToBeConsumed -= result.consumedScroll
+ layoutInfoState.value = result
+
+ canScrollForward = result.canScrollForward
+ canScrollBackward = (result.firstVisibleItem?.index ?: 0) != 0 ||
+ result.firstVisibleItemScrollOffset != 0
+
+ numMeasurePasses++
+ }
+
+ /**
+ * When the user provided custom keys for the items we can try to detect when there were
+ * items added or removed before our current first visible item and keep this item
+ * as the first visible one even given that its index has been changed.
+ */
+ internal fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyListItemProvider) {
+ scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
+ }
+
+ companion object {
+ /**
+ * The default [Saver] implementation for [TvLazyListState].
+ */
+ val Saver: Saver<TvLazyListState, *> = listSaver(
+ save = { listOf(it.firstVisibleItemIndex, it.firstVisibleItemScrollOffset) },
+ restore = {
+ TvLazyListState(
+ firstVisibleItemIndex = it[0],
+ firstVisibleItemScrollOffset = it[1]
+ )
+ }
+ )
+ }
+}
+
+private object EmptyLazyListLayoutInfo : TvLazyListLayoutInfo {
+ override val visibleItemsInfo = emptyList<TvLazyListItemInfo>()
+ override val viewportStartOffset = 0
+ override val viewportEndOffset = 0
+ override val totalItemsCount = 0
+ override val viewportSize = IntSize.Zero
+ override val orientation = Orientation.Vertical
+ override val reverseLayout = false
+ override val beforeContentPadding = 0
+ override val afterContentPadding = 0
+}
+
+internal class AwaitFirstLayoutModifier : OnGloballyPositionedModifier {
+ private var wasPositioned = false
+ private var continuation: Continuation<Unit>? = null
+
+ suspend fun waitForFirstLayout() {
+ if (!wasPositioned) {
+ val oldContinuation = continuation
+ suspendCoroutine<Unit> { continuation = it }
+ oldContinuation?.resume(Unit)
+ }
+ }
+
+ override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
+ if (!wasPositioned) {
+ wasPositioned = true
+ continuation?.resume(Unit)
+ continuation = null
+ }
+ }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyMeasuredItem.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyMeasuredItem.kt
new file mode 100644
index 0000000..9698c79
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyMeasuredItem.kt
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2020 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.tv.foundation.lazy.list
+
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.LayoutDirection
+
+/**
+ * Represents one measured item of the lazy list. It can in fact consist of multiple placeables
+ * if the user emit multiple layout nodes in the item callback.
+ */
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+internal class LazyMeasuredItem @ExperimentalFoundationApi constructor(
+ val index: Int,
+ private val placeables: Array<Placeable>,
+ private val isVertical: Boolean,
+ private val horizontalAlignment: Alignment.Horizontal?,
+ private val verticalAlignment: Alignment.Vertical?,
+ private val layoutDirection: LayoutDirection,
+ private val reverseLayout: Boolean,
+ private val beforeContentPadding: Int,
+ private val afterContentPadding: Int,
+ private val placementAnimator: LazyListItemPlacementAnimator,
+ /**
+ * Extra spacing to be added to [size] aside from the sum of the [placeables] size. It
+ * is usually representing the spacing after the item.
+ */
+ private val spacing: Int,
+ /**
+ * The offset which shouldn't affect any calculations but needs to be applied for the final
+ * value passed into the place() call.
+ */
+ private val visualOffset: IntOffset,
+ val key: Any,
+) {
+ /**
+ * Sum of the main axis sizes of all the inner placeables.
+ */
+ val size: Int
+
+ /**
+ * Sum of the main axis sizes of all the inner placeables and [spacing].
+ */
+ val sizeWithSpacings: Int
+
+ /**
+ * Max of the cross axis sizes of all the inner placeables.
+ */
+ val crossAxisSize: Int
+
+ init {
+ var mainAxisSize = 0
+ var maxCrossAxis = 0
+ placeables.forEach {
+ mainAxisSize += if (isVertical) it.height else it.width
+ maxCrossAxis = maxOf(maxCrossAxis, if (!isVertical) it.height else it.width)
+ }
+ size = mainAxisSize
+ sizeWithSpacings = size + spacing
+ crossAxisSize = maxCrossAxis
+ }
+
+ /**
+ * Calculates positions for the inner placeables at [offset] main axis position.
+ * If [reverseOrder] is true the inner placeables would be placed in the inverted order.
+ */
+ fun position(
+ offset: Int,
+ layoutWidth: Int,
+ layoutHeight: Int
+ ): LazyListPositionedItem {
+ val wrappers = mutableListOf<LazyListPlaceableWrapper>()
+ val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
+ var mainAxisOffset = if (reverseLayout) {
+ mainAxisLayoutSize - offset - size
+ } else {
+ offset
+ }
+ var index = if (reverseLayout) placeables.lastIndex else 0
+ while (if (reverseLayout) index >= 0 else index < placeables.size) {
+ val it = placeables[index]
+ val addIndex = if (reverseLayout) 0 else wrappers.size
+ val placeableOffset = if (isVertical) {
+ val x = requireNotNull(horizontalAlignment)
+ .align(it.width, layoutWidth, layoutDirection)
+ IntOffset(x, mainAxisOffset)
+ } else {
+ val y = requireNotNull(verticalAlignment).align(it.height, layoutHeight)
+ IntOffset(mainAxisOffset, y)
+ }
+ mainAxisOffset += if (isVertical) it.height else it.width
+ wrappers.add(
+ addIndex,
+ LazyListPlaceableWrapper(placeableOffset, it, placeables[index].parentData)
+ )
+ if (reverseLayout) index-- else index++
+ }
+ return LazyListPositionedItem(
+ offset = offset,
+ index = this.index,
+ key = key,
+ size = size,
+ sizeWithSpacings = sizeWithSpacings,
+ minMainAxisOffset = -if (!reverseLayout) beforeContentPadding else afterContentPadding,
+ maxMainAxisOffset = mainAxisLayoutSize +
+ if (!reverseLayout) afterContentPadding else beforeContentPadding,
+ isVertical = isVertical,
+ wrappers = wrappers,
+ placementAnimator = placementAnimator,
+ visualOffset = visualOffset
+ )
+ }
+}
+
+internal class LazyListPositionedItem(
+ override val offset: Int,
+ override val index: Int,
+ override val key: Any,
+ override val size: Int,
+ val sizeWithSpacings: Int,
+ private val minMainAxisOffset: Int,
+ private val maxMainAxisOffset: Int,
+ private val isVertical: Boolean,
+ private val wrappers: List<LazyListPlaceableWrapper>,
+ private val placementAnimator: LazyListItemPlacementAnimator,
+ private val visualOffset: IntOffset
+) : TvLazyListItemInfo {
+ val placeablesCount: Int get() = wrappers.size
+
+ fun getOffset(index: Int) = wrappers[index].offset
+
+ fun getMainAxisSize(index: Int) = wrappers[index].placeable.mainAxisSize
+
+ @Suppress("UNCHECKED_CAST")
+ fun getAnimationSpec(index: Int) =
+ wrappers[index].parentData as? FiniteAnimationSpec<IntOffset>?
+
+ val hasAnimations = run {
+ repeat(placeablesCount) { index ->
+ if (getAnimationSpec(index) != null) {
+ return@run true
+ }
+ }
+ false
+ }
+
+ fun place(
+ scope: Placeable.PlacementScope,
+ ) = with(scope) {
+ repeat(placeablesCount) { index ->
+ val placeable = wrappers[index].placeable
+ val minOffset = minMainAxisOffset - placeable.mainAxisSize
+ val maxOffset = maxMainAxisOffset
+ val offset = if (getAnimationSpec(index) != null) {
+ placementAnimator.getAnimatedOffset(
+ key, index, minOffset, maxOffset, getOffset(index)
+ )
+ } else {
+ getOffset(index)
+ }
+ if (isVertical) {
+ placeable.placeWithLayer(offset + visualOffset)
+ } else {
+ placeable.placeRelativeWithLayer(offset + visualOffset)
+ }
+ }
+ }
+
+ private val Placeable.mainAxisSize get() = if (isVertical) height else width
+}
+
+internal class LazyListPlaceableWrapper(
+ val offset: IntOffset,
+ val placeable: Placeable,
+ val parentData: Any?
+)
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyMeasuredItemProvider.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyMeasuredItemProvider.kt
new file mode 100644
index 0000000..2cd0037
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyMeasuredItemProvider.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2020 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.tv.foundation.lazy.list
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.Constraints
+
+/**
+ * Abstracts away the subcomposition from the measuring logic.
+ */
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@OptIn(ExperimentalFoundationApi::class)
+internal class LazyMeasuredItemProvider @ExperimentalFoundationApi constructor(
+ constraints: Constraints,
+ isVertical: Boolean,
+ private val itemProvider: LazyListItemProvider,
+ private val measureScope: LazyLayoutMeasureScope,
+ private val measuredItemFactory: MeasuredItemFactory
+) {
+ // the constraints we will measure child with. the main axis is not restricted
+ val childConstraints = Constraints(
+ maxWidth = if (isVertical) constraints.maxWidth else Constraints.Infinity,
+ maxHeight = if (!isVertical) constraints.maxHeight else Constraints.Infinity
+ )
+
+ /**
+ * Used to subcompose items of lazy lists. Composed placeables will be measured with the
+ * correct constraints and wrapped into [LazyMeasuredItem].
+ */
+ fun getAndMeasure(index: DataIndex): LazyMeasuredItem {
+ val key = itemProvider.getKey(index.value)
+ val placeables = measureScope.measure(index.value, childConstraints)
+ return measuredItemFactory.createItem(index, key, placeables)
+ }
+
+ /**
+ * Contains the mapping between the key and the index. It could contain not all the items of
+ * the list as an optimization.
+ **/
+ val keyToIndexMap: Map<Any, Int> get() = itemProvider.keyToIndexMap
+}
+
+// This interface allows to avoid autoboxing on index param
+internal fun interface MeasuredItemFactory {
+ fun createItem(
+ index: DataIndex,
+ key: Any,
+ placeables: Array<Placeable>
+ ): LazyMeasuredItem
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazySemantics.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazySemantics.kt
new file mode 100644
index 0000000..1e3bb60
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazySemantics.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.CollectionInfo
+import androidx.compose.ui.semantics.ScrollAxisRange
+import androidx.compose.ui.semantics.collectionInfo
+import androidx.compose.ui.semantics.horizontalScrollAxisRange
+import androidx.compose.ui.semantics.indexForKey
+import androidx.compose.ui.semantics.scrollBy
+import androidx.compose.ui.semantics.scrollToIndex
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.verticalScrollAxisRange
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+// TODO (b/233188423): Address IllegalExperimentalApiUsage before moving to beta
+@Suppress("ComposableModifierFactory", "ModifierInspectorInfo", "IllegalExperimentalApiUsage")
+@ExperimentalFoundationApi
+@Composable
+internal fun Modifier.lazyListSemantics(
+ itemProvider: LazyListItemProvider,
+ state: TvLazyListState,
+ coroutineScope: CoroutineScope,
+ isVertical: Boolean,
+ reverseScrolling: Boolean,
+ userScrollEnabled: Boolean
+) = this.then(
+ remember(
+ itemProvider,
+ state,
+ isVertical,
+ reverseScrolling,
+ userScrollEnabled
+ ) {
+ val indexForKeyMapping: (Any) -> Int = { needle ->
+ val key = itemProvider::getKey
+ var result = -1
+ for (index in 0 until itemProvider.itemCount) {
+ if (key(index) == needle) {
+ result = index
+ break
+ }
+ }
+ result
+ }
+
+ val accessibilityScrollState = ScrollAxisRange(
+ value = {
+ // This is a simple way of representing the current position without
+ // needing any lazy items to be measured. It's good enough so far, because
+ // screen-readers care mostly about whether scroll position changed or not
+ // rather than the actual offset in pixels.
+ state.firstVisibleItemIndex + state.firstVisibleItemScrollOffset / 100_000f
+ },
+ maxValue = {
+ if (state.canScrollForward) {
+ // If we can scroll further, we don't know the end yet,
+ // but it's upper bounded by #items + 1
+ itemProvider.itemCount + 1f
+ } else {
+ // If we can't scroll further, the current value is the max
+ state.firstVisibleItemIndex + state.firstVisibleItemScrollOffset / 100_000f
+ }
+ },
+ reverseScrolling = reverseScrolling
+ )
+ val scrollByAction: ((x: Float, y: Float) -> Boolean)? = if (userScrollEnabled) {
+ { x, y ->
+ val delta = if (isVertical) {
+ y
+ } else {
+ x
+ }
+ coroutineScope.launch {
+ (state as ScrollableState).animateScrollBy(delta)
+ }
+ // TODO(aelias): is it important to return false if we know in advance we cannot scroll?
+ true
+ }
+ } else {
+ null
+ }
+
+ val scrollToIndexAction: ((Int) -> Boolean)? = if (userScrollEnabled) {
+ { index ->
+ require(index >= 0 && index < state.layoutInfo.totalItemsCount) {
+ "Can't scroll to index $index, it is out of " +
+ "bounds [0, ${state.layoutInfo.totalItemsCount})"
+ }
+ coroutineScope.launch {
+ state.scrollToItem(index)
+ }
+ true
+ }
+ } else {
+ null
+ }
+
+ val collectionInfo = CollectionInfo(
+ rowCount = if (isVertical) -1 else 1,
+ columnCount = if (isVertical) 1 else -1
+ )
+
+ Modifier.semantics {
+ indexForKey(indexForKeyMapping)
+
+ if (isVertical) {
+ verticalScrollAxisRange = accessibilityScrollState
+ } else {
+ horizontalScrollAxisRange = accessibilityScrollState
+ }
+
+ if (scrollByAction != null) {
+ scrollBy(action = scrollByAction)
+ }
+
+ if (scrollToIndexAction != null) {
+ scrollToIndex(action = scrollToIndexAction)
+ }
+
+ this.collectionInfo = collectionInfo
+ }
+ }
+)
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemInfo.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemInfo.kt
new file mode 100644
index 0000000..55274f8
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemInfo.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+/**
+ * Contains useful information about an individual item in lazy lists like [TvLazyColumn]
+ * or [TvLazyRow].
+ *
+ * @see TvLazyListLayoutInfo
+ */
+interface TvLazyListItemInfo {
+ /**
+ * The index of the item in the list.
+ */
+ val index: Int
+
+ /**
+ * The key of the item which was passed to the item() or items() function.
+ */
+ val key: Any
+
+ /**
+ * The main axis offset of the item in pixels. It is relative to the start of the lazy list container.
+ */
+ val offset: Int
+
+ /**
+ * The main axis size of the item in pixels. Note that if you emit multiple layouts in the composable
+ * slot for the item then this size will be calculated as the sum of their sizes.
+ */
+ val size: Int
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemScope.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemScope.kt
new file mode 100644
index 0000000..441895d
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemScope.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.annotation.FloatRange
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.VisibilityThreshold
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntOffset
+
+@Stable
+@TvLazyListScopeMarker
+sealed interface TvLazyListItemScope {
+ /**
+ * Have the content fill the [Constraints.maxWidth] and [Constraints.maxHeight] of the parent
+ * measurement constraints by setting the [minimum width][Constraints.minWidth] to be equal to
+ * the [maximum width][Constraints.maxWidth] multiplied by [fraction] and the [minimum
+ * height][Constraints.minHeight] to be equal to the [maximum height][Constraints.maxHeight]
+ * multiplied by [fraction]. Note that, by default, the [fraction] is 1, so the modifier will
+ * make the content fill the whole available space. [fraction] must be between `0` and `1`.
+ *
+ * Regular [Modifier.fillMaxSize] can't work inside the scrolling layouts as the items are
+ * measured with [Constraints.Infinity] as the constraints for the main axis.
+ */
+ fun Modifier.fillParentMaxSize(
+ @FloatRange(from = 0.0, to = 1.0)
+ fraction: Float = 1f
+ ): Modifier
+
+ /**
+ * Have the content fill the [Constraints.maxWidth] of the parent measurement constraints
+ * by setting the [minimum width][Constraints.minWidth] to be equal to the
+ * [maximum width][Constraints.maxWidth] multiplied by [fraction]. Note that, by default, the
+ * [fraction] is 1, so the modifier will make the content fill the whole parent width.
+ * [fraction] must be between `0` and `1`.
+ *
+ * Regular [Modifier.fillMaxWidth] can't work inside the scrolling horizontally layouts as the
+ * items are measured with [Constraints.Infinity] as the constraints for the main axis.
+ */
+ fun Modifier.fillParentMaxWidth(
+ @FloatRange(from = 0.0, to = 1.0)
+ fraction: Float = 1f
+ ): Modifier
+
+ /**
+ * Have the content fill the [Constraints.maxHeight] of the incoming measurement constraints
+ * by setting the [minimum height][Constraints.minHeight] to be equal to the
+ * [maximum height][Constraints.maxHeight] multiplied by [fraction]. Note that, by default, the
+ * [fraction] is 1, so the modifier will make the content fill the whole parent height.
+ * [fraction] must be between `0` and `1`.
+ *
+ * Regular [Modifier.fillMaxHeight] can't work inside the scrolling vertically layouts as the
+ * items are measured with [Constraints.Infinity] as the constraints for the main axis.
+ */
+ fun Modifier.fillParentMaxHeight(
+ @FloatRange(from = 0.0, to = 1.0)
+ fraction: Float = 1f
+ ): Modifier
+
+ /**
+ * This modifier animates the item placement within the Lazy list.
+ *
+ * When you provide a key via [TvLazyListScope.item]/[TvLazyListScope.items] this modifier will
+ * enable item reordering animations. Aside from item reordering all other position changes
+ * caused by events like arrangement or alignment changes will also be animated.
+ *
+ * @sample androidx.compose.foundation.samples.ItemPlacementAnimationSample
+ *
+ * @param animationSpec a finite animation that will be used to animate the item placement.
+ */
+ @ExperimentalFoundationApi
+ fun Modifier.animateItemPlacement(
+ animationSpec: FiniteAnimationSpec<IntOffset> = spring(
+ stiffness = Spring.StiffnessMediumLow,
+ visibilityThreshold = IntOffset.VisibilityThreshold
+ )
+ ): Modifier
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemScopeImpl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemScopeImpl.kt
new file mode 100644
index 0000000..64bd81c
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemScopeImpl.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ParentDataModifier
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.InspectorValueInfo
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+
+internal class TvLazyListItemScopeImpl : TvLazyListItemScope {
+
+ var maxWidth: Dp by mutableStateOf(Dp.Unspecified)
+ var maxHeight: Dp by mutableStateOf(Dp.Unspecified)
+
+ override fun Modifier.fillParentMaxSize(fraction: Float) = size(
+ maxWidth * fraction,
+ maxHeight * fraction
+ )
+
+ override fun Modifier.fillParentMaxWidth(fraction: Float) =
+ width(maxWidth * fraction)
+
+ override fun Modifier.fillParentMaxHeight(fraction: Float) =
+ height(maxHeight * fraction)
+
+ @Suppress("IllegalExperimentalApiUsage") // TODO(b/233188423): Address before moving to beta
+ @ExperimentalFoundationApi
+ override fun Modifier.animateItemPlacement(animationSpec: FiniteAnimationSpec<IntOffset>) =
+ this.then(AnimateItemPlacementModifier(animationSpec, debugInspectorInfo {
+ name = "animateItemPlacement"
+ value = animationSpec
+ }))
+}
+
+private class AnimateItemPlacementModifier(
+ val animationSpec: FiniteAnimationSpec<IntOffset>,
+ inspectorInfo: InspectorInfo.() -> Unit,
+) : ParentDataModifier, InspectorValueInfo(inspectorInfo) {
+ override fun Density.modifyParentData(parentData: Any?): Any = animationSpec
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is AnimateItemPlacementModifier) return false
+ return animationSpec != other.animationSpec
+ }
+
+ override fun hashCode(): Int {
+ return animationSpec.hashCode()
+ }
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListLayoutInfo.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListLayoutInfo.kt
new file mode 100644
index 0000000..cc31459
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListLayoutInfo.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.IntSize
+
+/**
+ * Contains useful information about the currently displayed layout state of lazy lists like
+ * [TvLazyColumn] or [TvLazyRow]. For example you can get the list of currently displayed item.
+ *
+ * Use [TvLazyListState.layoutInfo] to retrieve this
+ */
+sealed interface TvLazyListLayoutInfo {
+ /**
+ * The list of [TvLazyListItemInfo] representing all the currently visible items.
+ */
+ val visibleItemsInfo: List<TvLazyListItemInfo>
+
+ /**
+ * The start offset of the layout's viewport in pixels. You can think of it as a minimum offset
+ * which would be visible. Usually it is 0, but it can be negative if non-zero [beforeContentPadding]
+ * was applied as the content displayed in the content padding area is still visible.
+ *
+ * You can use it to understand what items from [visibleItemsInfo] are fully visible.
+ */
+ val viewportStartOffset: Int
+
+ /**
+ * The end offset of the layout's viewport in pixels. You can think of it as a maximum offset
+ * which would be visible. It is the size of the lazy list layout minus [beforeContentPadding].
+ *
+ * You can use it to understand what items from [visibleItemsInfo] are fully visible.
+ */
+ val viewportEndOffset: Int
+
+ /**
+ * The total count of items passed to [TvLazyColumn] or [TvLazyRow].
+ */
+ val totalItemsCount: Int
+
+ /**
+ * The size of the viewport in pixels. It is the lazy list layout size including all the
+ * content paddings.
+ */
+ val viewportSize: IntSize
+
+ /**
+ * The orientation of the lazy list.
+ */
+ val orientation: Orientation
+
+ /**
+ * True if the direction of scrolling and layout is reversed.
+ */
+ val reverseLayout: Boolean
+
+ /**
+ * The content padding in pixels applied before the first item in the direction of scrolling.
+ * For example it is a top content padding for LazyColumn with reverseLayout set to false.
+ */
+ val beforeContentPadding: Int
+
+ /**
+ * The content padding in pixels applied after the last item in the direction of scrolling.
+ * For example it is a bottom content padding for LazyColumn with reverseLayout set to false.
+ */
+ val afterContentPadding: Int
+}
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeImpl.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeImpl.kt
new file mode 100644
index 0000000..e91c249
--- /dev/null
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeImpl.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.foundation.lazy.list
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.IntervalList
+import androidx.compose.foundation.lazy.layout.MutableIntervalList
+import androidx.compose.runtime.Composable
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@OptIn(ExperimentalFoundationApi::class)
+internal class TvLazyListScopeImpl : TvLazyListScope {
+
+ private val _intervals = MutableIntervalList<LazyListIntervalContent>()
+ val intervals: IntervalList<LazyListIntervalContent> = _intervals
+
+ private var _headerIndexes: MutableList<Int>? = null
+ val headerIndexes: List<Int> get() = _headerIndexes ?: emptyList()
+
+ override fun items(
+ count: Int,
+ key: ((index: Int) -> Any)?,
+ contentType: (index: Int) -> Any?,
+ itemContent: @Composable TvLazyListItemScope.(index: Int) -> Unit
+ ) {
+ _intervals.addInterval(
+ count,
+ LazyListIntervalContent(
+ key = key,
+ type = contentType,
+ item = itemContent
+ )
+ )
+ }
+
+ override fun item(
+ key: Any?,
+ contentType: Any?,
+ content: @Composable TvLazyListItemScope.() -> Unit
+ ) {
+ _intervals.addInterval(
+ 1,
+ LazyListIntervalContent(
+ key = if (key != null) { _: Int -> key } else null,
+ type = { contentType },
+ item = { content() }
+ )
+ )
+ }
+}
+
+internal class LazyListIntervalContent(
+ val key: ((index: Int) -> Any)?,
+ val type: ((index: Int) -> Any?),
+ val item: @Composable TvLazyListItemScope.(index: Int) -> Unit
+)
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/gallery/GalleryScreen.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeMarker.kt
similarity index 73%
rename from camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/gallery/GalleryScreen.kt
rename to tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeMarker.kt
index fdc9e2d..4931977 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/gallery/GalleryScreen.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListScopeMarker.kt
@@ -14,12 +14,10 @@
* limitations under the License.
*/
-package androidx.camera.integration.uiwidgets.compose.ui.screen.gallery
+package androidx.tv.foundation.lazy.list
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-
-@Composable
-fun GalleryScreen() {
- Text("Gallery Screen")
-}
\ No newline at end of file
+/**
+ * DSL marker used to distinguish between lazy layout scope and the item scope.
+ */
+@DslMarker
+annotation class TvLazyListScopeMarker
\ No newline at end of file
diff --git a/wear/compose/compose-foundation/build.gradle b/wear/compose/compose-foundation/build.gradle
index ef5258f..6c20d7c 100644
--- a/wear/compose/compose-foundation/build.gradle
+++ b/wear/compose/compose-foundation/build.gradle
@@ -35,7 +35,7 @@
implementation(libs.kotlinStdlib)
implementation(project(":compose:foundation:foundation-layout"))
- implementation(project(":profileinstaller:profileinstaller"))
+ implementation("androidx.profileinstaller:profileinstaller:1.2.0")
testImplementation(libs.testRules)
testImplementation(libs.testRunner)
diff --git a/wear/compose/compose-material/build.gradle b/wear/compose/compose-material/build.gradle
index 76fdfda..e67002e 100644
--- a/wear/compose/compose-material/build.gradle
+++ b/wear/compose/compose-material/build.gradle
@@ -40,7 +40,7 @@
implementation(project(":compose:material:material-ripple"))
implementation(project(":compose:ui:ui-util"))
implementation(project(":wear:compose:compose-foundation"))
- implementation(project(":profileinstaller:profileinstaller"))
+ implementation("androidx.profileinstaller:profileinstaller:1.2.0")
androidTestImplementation(project(":compose:ui:ui-test"))
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
diff --git a/wear/compose/compose-navigation/build.gradle b/wear/compose/compose-navigation/build.gradle
index 1581111..bc7a4e8 100644
--- a/wear/compose/compose-navigation/build.gradle
+++ b/wear/compose/compose-navigation/build.gradle
@@ -32,7 +32,7 @@
implementation(libs.kotlinStdlib)
implementation("androidx.navigation:navigation-compose:2.4.0")
- implementation(project(":profileinstaller:profileinstaller"))
+ implementation("androidx.profileinstaller:profileinstaller:1.2.0")
androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
diff --git a/wear/tiles/tiles-material/api/1.1.0-beta01.txt b/wear/tiles/tiles-material/api/1.1.0-beta01.txt
new file mode 100644
index 0000000..aecb80e
--- /dev/null
+++ b/wear/tiles/tiles-material/api/1.1.0-beta01.txt
@@ -0,0 +1,298 @@
+// Signature format: 4.0
+package androidx.wear.tiles.material {
+
+ public class Button implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.Button? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.ButtonColors getButtonColors();
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable getClickable();
+ method public CharSequence? getContentDescription();
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getCustomContent();
+ method public String? getIconContent();
+ method public String? getImageContent();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension getSize();
+ method public String? getTextContent();
+ }
+
+ public static final class Button.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public Button.Builder(android.content.Context, androidx.wear.tiles.ModifiersBuilders.Clickable);
+ method public androidx.wear.tiles.material.Button build();
+ method public androidx.wear.tiles.material.Button.Builder setButtonColors(androidx.wear.tiles.material.ButtonColors);
+ method public androidx.wear.tiles.material.Button.Builder setContentDescription(CharSequence);
+ method public androidx.wear.tiles.material.Button.Builder setCustomContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.Button.Builder setIconContent(String, androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public androidx.wear.tiles.material.Button.Builder setIconContent(String);
+ method public androidx.wear.tiles.material.Button.Builder setImageContent(String);
+ method public androidx.wear.tiles.material.Button.Builder setSize(androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public androidx.wear.tiles.material.Button.Builder setSize(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ method public androidx.wear.tiles.material.Button.Builder setTextContent(String);
+ method public androidx.wear.tiles.material.Button.Builder setTextContent(String, int);
+ }
+
+ public class ButtonColors {
+ ctor public ButtonColors(@ColorInt int, @ColorInt int);
+ ctor public ButtonColors(androidx.wear.tiles.ColorBuilders.ColorProp, androidx.wear.tiles.ColorBuilders.ColorProp);
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getBackgroundColor();
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getContentColor();
+ method public static androidx.wear.tiles.material.ButtonColors primaryButtonColors(androidx.wear.tiles.material.Colors);
+ method public static androidx.wear.tiles.material.ButtonColors secondaryButtonColors(androidx.wear.tiles.material.Colors);
+ }
+
+ public class ButtonDefaults {
+ method public static androidx.wear.tiles.DimensionBuilders.DpProp recommendedIconSize(androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public static androidx.wear.tiles.DimensionBuilders.DpProp recommendedIconSize(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ field public static final androidx.wear.tiles.DimensionBuilders.DpProp DEFAULT_SIZE;
+ field public static final androidx.wear.tiles.DimensionBuilders.DpProp EXTRA_LARGE_SIZE;
+ field public static final androidx.wear.tiles.DimensionBuilders.DpProp LARGE_SIZE;
+ field public static final androidx.wear.tiles.material.ButtonColors PRIMARY_COLORS;
+ field public static final androidx.wear.tiles.material.ButtonColors SECONDARY_COLORS;
+ }
+
+ public class Chip implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.Chip? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.ChipColors getChipColors();
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable getClickable();
+ method public CharSequence? getContentDescription();
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getCustomContent();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension getHeight();
+ method public int getHorizontalAlignment();
+ method public String? getIconContent();
+ method public String? getPrimaryLabelContent();
+ method public String? getSecondaryLabelContent();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension getWidth();
+ }
+
+ public static final class Chip.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public Chip.Builder(android.content.Context, androidx.wear.tiles.ModifiersBuilders.Clickable, androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public androidx.wear.tiles.material.Chip build();
+ method public androidx.wear.tiles.material.Chip.Builder setChipColors(androidx.wear.tiles.material.ChipColors);
+ method public androidx.wear.tiles.material.Chip.Builder setContentDescription(CharSequence);
+ method public androidx.wear.tiles.material.Chip.Builder setCustomContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.Chip.Builder setHorizontalAlignment(int);
+ method public androidx.wear.tiles.material.Chip.Builder setIconContent(String);
+ method public androidx.wear.tiles.material.Chip.Builder setPrimaryLabelContent(String);
+ method public androidx.wear.tiles.material.Chip.Builder setSecondaryLabelContent(String);
+ method public androidx.wear.tiles.material.Chip.Builder setWidth(androidx.wear.tiles.DimensionBuilders.ContainerDimension);
+ method public androidx.wear.tiles.material.Chip.Builder setWidth(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ }
+
+ public class ChipColors {
+ ctor public ChipColors(@ColorInt int, @ColorInt int, @ColorInt int, @ColorInt int);
+ ctor public ChipColors(@ColorInt int, @ColorInt int);
+ ctor public ChipColors(androidx.wear.tiles.ColorBuilders.ColorProp, androidx.wear.tiles.ColorBuilders.ColorProp, androidx.wear.tiles.ColorBuilders.ColorProp, androidx.wear.tiles.ColorBuilders.ColorProp);
+ ctor public ChipColors(androidx.wear.tiles.ColorBuilders.ColorProp, androidx.wear.tiles.ColorBuilders.ColorProp);
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getBackgroundColor();
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getContentColor();
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getIconColor();
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getSecondaryContentColor();
+ method public static androidx.wear.tiles.material.ChipColors primaryChipColors(androidx.wear.tiles.material.Colors);
+ method public static androidx.wear.tiles.material.ChipColors secondaryChipColors(androidx.wear.tiles.material.Colors);
+ }
+
+ public class ChipDefaults {
+ field public static final androidx.wear.tiles.material.ChipColors COMPACT_PRIMARY_COLORS;
+ field public static final androidx.wear.tiles.material.ChipColors COMPACT_SECONDARY_COLORS;
+ field public static final androidx.wear.tiles.material.ChipColors PRIMARY_COLORS;
+ field public static final androidx.wear.tiles.material.ChipColors SECONDARY_COLORS;
+ field public static final androidx.wear.tiles.material.ChipColors TITLE_PRIMARY_COLORS;
+ field public static final androidx.wear.tiles.material.ChipColors TITLE_SECONDARY_COLORS;
+ }
+
+ public class CircularProgressIndicator implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.CircularProgressIndicator? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.ProgressIndicatorColors getCircularProgressIndicatorColors();
+ method public CharSequence? getContentDescription();
+ method public androidx.wear.tiles.DimensionBuilders.DegreesProp getEndAngle();
+ method public androidx.wear.tiles.DimensionBuilders.DegreesProp getProgress();
+ method public androidx.wear.tiles.DimensionBuilders.DegreesProp getStartAngle();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp getStrokeWidth();
+ }
+
+ public static final class CircularProgressIndicator.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public CircularProgressIndicator.Builder();
+ method public androidx.wear.tiles.material.CircularProgressIndicator build();
+ method public androidx.wear.tiles.material.CircularProgressIndicator.Builder setCircularProgressIndicatorColors(androidx.wear.tiles.material.ProgressIndicatorColors);
+ method public androidx.wear.tiles.material.CircularProgressIndicator.Builder setContentDescription(CharSequence);
+ method public androidx.wear.tiles.material.CircularProgressIndicator.Builder setEndAngle(float);
+ method public androidx.wear.tiles.material.CircularProgressIndicator.Builder setProgress(@FloatRange(from=0, to=1) float);
+ method public androidx.wear.tiles.material.CircularProgressIndicator.Builder setStartAngle(float);
+ method public androidx.wear.tiles.material.CircularProgressIndicator.Builder setStrokeWidth(androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public androidx.wear.tiles.material.CircularProgressIndicator.Builder setStrokeWidth(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ }
+
+ public class Colors {
+ ctor public Colors(@ColorInt int, @ColorInt int, @ColorInt int, @ColorInt int);
+ method @ColorInt public int getOnPrimary();
+ method @ColorInt public int getOnSurface();
+ method @ColorInt public int getPrimary();
+ method @ColorInt public int getSurface();
+ field public static final androidx.wear.tiles.material.Colors DEFAULT;
+ }
+
+ public class CompactChip implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.CompactChip? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.ChipColors getChipColors();
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable getClickable();
+ method public String getText();
+ }
+
+ public static final class CompactChip.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public CompactChip.Builder(android.content.Context, String, androidx.wear.tiles.ModifiersBuilders.Clickable, androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public androidx.wear.tiles.material.CompactChip build();
+ method public androidx.wear.tiles.material.CompactChip.Builder setChipColors(androidx.wear.tiles.material.ChipColors);
+ }
+
+ public class ProgressIndicatorColors {
+ ctor public ProgressIndicatorColors(androidx.wear.tiles.ColorBuilders.ColorProp, androidx.wear.tiles.ColorBuilders.ColorProp);
+ ctor public ProgressIndicatorColors(@ColorInt int, @ColorInt int);
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getIndicatorColor();
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getTrackColor();
+ method public static androidx.wear.tiles.material.ProgressIndicatorColors progressIndicatorColors(androidx.wear.tiles.material.Colors);
+ }
+
+ public class ProgressIndicatorDefaults {
+ field public static final androidx.wear.tiles.material.ProgressIndicatorColors DEFAULT_COLORS;
+ field public static final androidx.wear.tiles.DimensionBuilders.DpProp DEFAULT_STROKE_WIDTH;
+ field public static final float GAP_END_ANGLE = 156.1f;
+ field public static final float GAP_START_ANGLE = -156.1f;
+ }
+
+ public class Text implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.Text? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getColor();
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle getFontStyle();
+ method public float getLineHeight();
+ method public int getMaxLines();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers getModifiers();
+ method public int getMultilineAlignment();
+ method public int getOverflow();
+ method public String getText();
+ method public int getWeight();
+ method public boolean isItalic();
+ method public boolean isUnderline();
+ }
+
+ public static final class Text.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public Text.Builder(android.content.Context, String);
+ method public androidx.wear.tiles.material.Text build();
+ method public androidx.wear.tiles.material.Text.Builder setColor(androidx.wear.tiles.ColorBuilders.ColorProp);
+ method public androidx.wear.tiles.material.Text.Builder setItalic(boolean);
+ method public androidx.wear.tiles.material.Text.Builder setMaxLines(@IntRange(from=1) int);
+ method public androidx.wear.tiles.material.Text.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.material.Text.Builder setMultilineAlignment(int);
+ method public androidx.wear.tiles.material.Text.Builder setOverflow(int);
+ method public androidx.wear.tiles.material.Text.Builder setTypography(int);
+ method public androidx.wear.tiles.material.Text.Builder setUnderline(boolean);
+ method public androidx.wear.tiles.material.Text.Builder setWeight(int);
+ }
+
+ public class TitleChip implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.TitleChip? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.ChipColors getChipColors();
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable getClickable();
+ method public int getHorizontalAlignment();
+ method public String getText();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension getWidth();
+ }
+
+ public static final class TitleChip.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public TitleChip.Builder(android.content.Context, String, androidx.wear.tiles.ModifiersBuilders.Clickable, androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public androidx.wear.tiles.material.TitleChip build();
+ method public androidx.wear.tiles.material.TitleChip.Builder setChipColors(androidx.wear.tiles.material.ChipColors);
+ method public androidx.wear.tiles.material.TitleChip.Builder setHorizontalAlignment(int);
+ method public androidx.wear.tiles.material.TitleChip.Builder setWidth(androidx.wear.tiles.DimensionBuilders.ContainerDimension);
+ method public androidx.wear.tiles.material.TitleChip.Builder setWidth(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ }
+
+ public class Typography {
+ field public static final int TYPOGRAPHY_BODY1 = 7; // 0x7
+ field public static final int TYPOGRAPHY_BODY2 = 8; // 0x8
+ field public static final int TYPOGRAPHY_BUTTON = 9; // 0x9
+ field public static final int TYPOGRAPHY_CAPTION1 = 10; // 0xa
+ field public static final int TYPOGRAPHY_CAPTION2 = 11; // 0xb
+ field public static final int TYPOGRAPHY_CAPTION3 = 12; // 0xc
+ field public static final int TYPOGRAPHY_DISPLAY1 = 1; // 0x1
+ field public static final int TYPOGRAPHY_DISPLAY2 = 2; // 0x2
+ field public static final int TYPOGRAPHY_DISPLAY3 = 3; // 0x3
+ field public static final int TYPOGRAPHY_TITLE1 = 4; // 0x4
+ field public static final int TYPOGRAPHY_TITLE2 = 5; // 0x5
+ field public static final int TYPOGRAPHY_TITLE3 = 6; // 0x6
+ }
+
+}
+
+package androidx.wear.tiles.material.layouts {
+
+ public class EdgeContentLayout implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.layouts.EdgeContentLayout? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getContent();
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getEdgeContent();
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getPrimaryLabelTextContent();
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getSecondaryLabelTextContent();
+ }
+
+ public static final class EdgeContentLayout.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public EdgeContentLayout.Builder(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public androidx.wear.tiles.material.layouts.EdgeContentLayout build();
+ method public androidx.wear.tiles.material.layouts.EdgeContentLayout.Builder setContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.EdgeContentLayout.Builder setEdgeContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.EdgeContentLayout.Builder setPrimaryLabelTextContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.EdgeContentLayout.Builder setSecondaryLabelTextContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ }
+
+ public class LayoutDefaults {
+ field public static final androidx.wear.tiles.DimensionBuilders.DpProp DEFAULT_VERTICAL_SPACER_HEIGHT;
+ field public static final float EDGE_CONTENT_LAYOUT_PADDING_ABOVE_MAIN_CONTENT_DP = 6.0f;
+ field public static final float EDGE_CONTENT_LAYOUT_PADDING_BELOW_MAIN_CONTENT_DP = 8.0f;
+ field public static final int MULTI_BUTTON_MAX_NUMBER = 7; // 0x7
+ field public static final androidx.wear.tiles.DimensionBuilders.DpProp MULTI_SLOT_LAYOUT_HORIZONTAL_SPACER_WIDTH;
+ }
+
+ public class MultiButtonLayout implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.layouts.MultiButtonLayout? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public java.util.List<androidx.wear.tiles.LayoutElementBuilders.LayoutElement!> getButtonContents();
+ method public int getFiveButtonDistribution();
+ field public static final int FIVE_BUTTON_DISTRIBUTION_BOTTOM_HEAVY = 2; // 0x2
+ field public static final int FIVE_BUTTON_DISTRIBUTION_TOP_HEAVY = 1; // 0x1
+ }
+
+ public static final class MultiButtonLayout.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public MultiButtonLayout.Builder();
+ method public androidx.wear.tiles.material.layouts.MultiButtonLayout.Builder addButtonContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.MultiButtonLayout build();
+ method public androidx.wear.tiles.material.layouts.MultiButtonLayout.Builder setFiveButtonDistribution(int);
+ }
+
+ public class MultiSlotLayout implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.layouts.MultiSlotLayout? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method @Dimension(unit=androidx.annotation.Dimension.DP) public float getHorizontalSpacerWidth();
+ method public java.util.List<androidx.wear.tiles.LayoutElementBuilders.LayoutElement!> getSlotContents();
+ }
+
+ public static final class MultiSlotLayout.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public MultiSlotLayout.Builder();
+ method public androidx.wear.tiles.material.layouts.MultiSlotLayout.Builder addSlotContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.MultiSlotLayout build();
+ method public androidx.wear.tiles.material.layouts.MultiSlotLayout.Builder setHorizontalSpacerWidth(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ }
+
+ public class PrimaryLayout implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.layouts.PrimaryLayout? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getContent();
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getPrimaryChipContent();
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getPrimaryLabelTextContent();
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getSecondaryLabelTextContent();
+ method @Dimension(unit=androidx.annotation.Dimension.DP) public float getVerticalSpacerHeight();
+ }
+
+ public static final class PrimaryLayout.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public PrimaryLayout.Builder(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public androidx.wear.tiles.material.layouts.PrimaryLayout build();
+ method public androidx.wear.tiles.material.layouts.PrimaryLayout.Builder setContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.PrimaryLayout.Builder setPrimaryChipContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.PrimaryLayout.Builder setPrimaryLabelTextContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.PrimaryLayout.Builder setSecondaryLabelTextContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.PrimaryLayout.Builder setVerticalSpacerHeight(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ }
+
+}
+
diff --git a/wear/tiles/tiles-material/api/public_plus_experimental_1.1.0-beta01.txt b/wear/tiles/tiles-material/api/public_plus_experimental_1.1.0-beta01.txt
new file mode 100644
index 0000000..aecb80e
--- /dev/null
+++ b/wear/tiles/tiles-material/api/public_plus_experimental_1.1.0-beta01.txt
@@ -0,0 +1,298 @@
+// Signature format: 4.0
+package androidx.wear.tiles.material {
+
+ public class Button implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.Button? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.ButtonColors getButtonColors();
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable getClickable();
+ method public CharSequence? getContentDescription();
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getCustomContent();
+ method public String? getIconContent();
+ method public String? getImageContent();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension getSize();
+ method public String? getTextContent();
+ }
+
+ public static final class Button.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public Button.Builder(android.content.Context, androidx.wear.tiles.ModifiersBuilders.Clickable);
+ method public androidx.wear.tiles.material.Button build();
+ method public androidx.wear.tiles.material.Button.Builder setButtonColors(androidx.wear.tiles.material.ButtonColors);
+ method public androidx.wear.tiles.material.Button.Builder setContentDescription(CharSequence);
+ method public androidx.wear.tiles.material.Button.Builder setCustomContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.Button.Builder setIconContent(String, androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public androidx.wear.tiles.material.Button.Builder setIconContent(String);
+ method public androidx.wear.tiles.material.Button.Builder setImageContent(String);
+ method public androidx.wear.tiles.material.Button.Builder setSize(androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public androidx.wear.tiles.material.Button.Builder setSize(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ method public androidx.wear.tiles.material.Button.Builder setTextContent(String);
+ method public androidx.wear.tiles.material.Button.Builder setTextContent(String, int);
+ }
+
+ public class ButtonColors {
+ ctor public ButtonColors(@ColorInt int, @ColorInt int);
+ ctor public ButtonColors(androidx.wear.tiles.ColorBuilders.ColorProp, androidx.wear.tiles.ColorBuilders.ColorProp);
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getBackgroundColor();
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getContentColor();
+ method public static androidx.wear.tiles.material.ButtonColors primaryButtonColors(androidx.wear.tiles.material.Colors);
+ method public static androidx.wear.tiles.material.ButtonColors secondaryButtonColors(androidx.wear.tiles.material.Colors);
+ }
+
+ public class ButtonDefaults {
+ method public static androidx.wear.tiles.DimensionBuilders.DpProp recommendedIconSize(androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public static androidx.wear.tiles.DimensionBuilders.DpProp recommendedIconSize(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ field public static final androidx.wear.tiles.DimensionBuilders.DpProp DEFAULT_SIZE;
+ field public static final androidx.wear.tiles.DimensionBuilders.DpProp EXTRA_LARGE_SIZE;
+ field public static final androidx.wear.tiles.DimensionBuilders.DpProp LARGE_SIZE;
+ field public static final androidx.wear.tiles.material.ButtonColors PRIMARY_COLORS;
+ field public static final androidx.wear.tiles.material.ButtonColors SECONDARY_COLORS;
+ }
+
+ public class Chip implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.Chip? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.ChipColors getChipColors();
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable getClickable();
+ method public CharSequence? getContentDescription();
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getCustomContent();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension getHeight();
+ method public int getHorizontalAlignment();
+ method public String? getIconContent();
+ method public String? getPrimaryLabelContent();
+ method public String? getSecondaryLabelContent();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension getWidth();
+ }
+
+ public static final class Chip.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public Chip.Builder(android.content.Context, androidx.wear.tiles.ModifiersBuilders.Clickable, androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public androidx.wear.tiles.material.Chip build();
+ method public androidx.wear.tiles.material.Chip.Builder setChipColors(androidx.wear.tiles.material.ChipColors);
+ method public androidx.wear.tiles.material.Chip.Builder setContentDescription(CharSequence);
+ method public androidx.wear.tiles.material.Chip.Builder setCustomContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.Chip.Builder setHorizontalAlignment(int);
+ method public androidx.wear.tiles.material.Chip.Builder setIconContent(String);
+ method public androidx.wear.tiles.material.Chip.Builder setPrimaryLabelContent(String);
+ method public androidx.wear.tiles.material.Chip.Builder setSecondaryLabelContent(String);
+ method public androidx.wear.tiles.material.Chip.Builder setWidth(androidx.wear.tiles.DimensionBuilders.ContainerDimension);
+ method public androidx.wear.tiles.material.Chip.Builder setWidth(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ }
+
+ public class ChipColors {
+ ctor public ChipColors(@ColorInt int, @ColorInt int, @ColorInt int, @ColorInt int);
+ ctor public ChipColors(@ColorInt int, @ColorInt int);
+ ctor public ChipColors(androidx.wear.tiles.ColorBuilders.ColorProp, androidx.wear.tiles.ColorBuilders.ColorProp, androidx.wear.tiles.ColorBuilders.ColorProp, androidx.wear.tiles.ColorBuilders.ColorProp);
+ ctor public ChipColors(androidx.wear.tiles.ColorBuilders.ColorProp, androidx.wear.tiles.ColorBuilders.ColorProp);
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getBackgroundColor();
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getContentColor();
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getIconColor();
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getSecondaryContentColor();
+ method public static androidx.wear.tiles.material.ChipColors primaryChipColors(androidx.wear.tiles.material.Colors);
+ method public static androidx.wear.tiles.material.ChipColors secondaryChipColors(androidx.wear.tiles.material.Colors);
+ }
+
+ public class ChipDefaults {
+ field public static final androidx.wear.tiles.material.ChipColors COMPACT_PRIMARY_COLORS;
+ field public static final androidx.wear.tiles.material.ChipColors COMPACT_SECONDARY_COLORS;
+ field public static final androidx.wear.tiles.material.ChipColors PRIMARY_COLORS;
+ field public static final androidx.wear.tiles.material.ChipColors SECONDARY_COLORS;
+ field public static final androidx.wear.tiles.material.ChipColors TITLE_PRIMARY_COLORS;
+ field public static final androidx.wear.tiles.material.ChipColors TITLE_SECONDARY_COLORS;
+ }
+
+ public class CircularProgressIndicator implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.CircularProgressIndicator? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.ProgressIndicatorColors getCircularProgressIndicatorColors();
+ method public CharSequence? getContentDescription();
+ method public androidx.wear.tiles.DimensionBuilders.DegreesProp getEndAngle();
+ method public androidx.wear.tiles.DimensionBuilders.DegreesProp getProgress();
+ method public androidx.wear.tiles.DimensionBuilders.DegreesProp getStartAngle();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp getStrokeWidth();
+ }
+
+ public static final class CircularProgressIndicator.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public CircularProgressIndicator.Builder();
+ method public androidx.wear.tiles.material.CircularProgressIndicator build();
+ method public androidx.wear.tiles.material.CircularProgressIndicator.Builder setCircularProgressIndicatorColors(androidx.wear.tiles.material.ProgressIndicatorColors);
+ method public androidx.wear.tiles.material.CircularProgressIndicator.Builder setContentDescription(CharSequence);
+ method public androidx.wear.tiles.material.CircularProgressIndicator.Builder setEndAngle(float);
+ method public androidx.wear.tiles.material.CircularProgressIndicator.Builder setProgress(@FloatRange(from=0, to=1) float);
+ method public androidx.wear.tiles.material.CircularProgressIndicator.Builder setStartAngle(float);
+ method public androidx.wear.tiles.material.CircularProgressIndicator.Builder setStrokeWidth(androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public androidx.wear.tiles.material.CircularProgressIndicator.Builder setStrokeWidth(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ }
+
+ public class Colors {
+ ctor public Colors(@ColorInt int, @ColorInt int, @ColorInt int, @ColorInt int);
+ method @ColorInt public int getOnPrimary();
+ method @ColorInt public int getOnSurface();
+ method @ColorInt public int getPrimary();
+ method @ColorInt public int getSurface();
+ field public static final androidx.wear.tiles.material.Colors DEFAULT;
+ }
+
+ public class CompactChip implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.CompactChip? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.ChipColors getChipColors();
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable getClickable();
+ method public String getText();
+ }
+
+ public static final class CompactChip.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public CompactChip.Builder(android.content.Context, String, androidx.wear.tiles.ModifiersBuilders.Clickable, androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public androidx.wear.tiles.material.CompactChip build();
+ method public androidx.wear.tiles.material.CompactChip.Builder setChipColors(androidx.wear.tiles.material.ChipColors);
+ }
+
+ public class ProgressIndicatorColors {
+ ctor public ProgressIndicatorColors(androidx.wear.tiles.ColorBuilders.ColorProp, androidx.wear.tiles.ColorBuilders.ColorProp);
+ ctor public ProgressIndicatorColors(@ColorInt int, @ColorInt int);
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getIndicatorColor();
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getTrackColor();
+ method public static androidx.wear.tiles.material.ProgressIndicatorColors progressIndicatorColors(androidx.wear.tiles.material.Colors);
+ }
+
+ public class ProgressIndicatorDefaults {
+ field public static final androidx.wear.tiles.material.ProgressIndicatorColors DEFAULT_COLORS;
+ field public static final androidx.wear.tiles.DimensionBuilders.DpProp DEFAULT_STROKE_WIDTH;
+ field public static final float GAP_END_ANGLE = 156.1f;
+ field public static final float GAP_START_ANGLE = -156.1f;
+ }
+
+ public class Text implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.Text? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getColor();
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle getFontStyle();
+ method public float getLineHeight();
+ method public int getMaxLines();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers getModifiers();
+ method public int getMultilineAlignment();
+ method public int getOverflow();
+ method public String getText();
+ method public int getWeight();
+ method public boolean isItalic();
+ method public boolean isUnderline();
+ }
+
+ public static final class Text.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public Text.Builder(android.content.Context, String);
+ method public androidx.wear.tiles.material.Text build();
+ method public androidx.wear.tiles.material.Text.Builder setColor(androidx.wear.tiles.ColorBuilders.ColorProp);
+ method public androidx.wear.tiles.material.Text.Builder setItalic(boolean);
+ method public androidx.wear.tiles.material.Text.Builder setMaxLines(@IntRange(from=1) int);
+ method public androidx.wear.tiles.material.Text.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.material.Text.Builder setMultilineAlignment(int);
+ method public androidx.wear.tiles.material.Text.Builder setOverflow(int);
+ method public androidx.wear.tiles.material.Text.Builder setTypography(int);
+ method public androidx.wear.tiles.material.Text.Builder setUnderline(boolean);
+ method public androidx.wear.tiles.material.Text.Builder setWeight(int);
+ }
+
+ public class TitleChip implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.TitleChip? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.ChipColors getChipColors();
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable getClickable();
+ method public int getHorizontalAlignment();
+ method public String getText();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension getWidth();
+ }
+
+ public static final class TitleChip.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public TitleChip.Builder(android.content.Context, String, androidx.wear.tiles.ModifiersBuilders.Clickable, androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public androidx.wear.tiles.material.TitleChip build();
+ method public androidx.wear.tiles.material.TitleChip.Builder setChipColors(androidx.wear.tiles.material.ChipColors);
+ method public androidx.wear.tiles.material.TitleChip.Builder setHorizontalAlignment(int);
+ method public androidx.wear.tiles.material.TitleChip.Builder setWidth(androidx.wear.tiles.DimensionBuilders.ContainerDimension);
+ method public androidx.wear.tiles.material.TitleChip.Builder setWidth(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ }
+
+ public class Typography {
+ field public static final int TYPOGRAPHY_BODY1 = 7; // 0x7
+ field public static final int TYPOGRAPHY_BODY2 = 8; // 0x8
+ field public static final int TYPOGRAPHY_BUTTON = 9; // 0x9
+ field public static final int TYPOGRAPHY_CAPTION1 = 10; // 0xa
+ field public static final int TYPOGRAPHY_CAPTION2 = 11; // 0xb
+ field public static final int TYPOGRAPHY_CAPTION3 = 12; // 0xc
+ field public static final int TYPOGRAPHY_DISPLAY1 = 1; // 0x1
+ field public static final int TYPOGRAPHY_DISPLAY2 = 2; // 0x2
+ field public static final int TYPOGRAPHY_DISPLAY3 = 3; // 0x3
+ field public static final int TYPOGRAPHY_TITLE1 = 4; // 0x4
+ field public static final int TYPOGRAPHY_TITLE2 = 5; // 0x5
+ field public static final int TYPOGRAPHY_TITLE3 = 6; // 0x6
+ }
+
+}
+
+package androidx.wear.tiles.material.layouts {
+
+ public class EdgeContentLayout implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.layouts.EdgeContentLayout? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getContent();
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getEdgeContent();
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getPrimaryLabelTextContent();
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getSecondaryLabelTextContent();
+ }
+
+ public static final class EdgeContentLayout.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public EdgeContentLayout.Builder(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public androidx.wear.tiles.material.layouts.EdgeContentLayout build();
+ method public androidx.wear.tiles.material.layouts.EdgeContentLayout.Builder setContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.EdgeContentLayout.Builder setEdgeContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.EdgeContentLayout.Builder setPrimaryLabelTextContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.EdgeContentLayout.Builder setSecondaryLabelTextContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ }
+
+ public class LayoutDefaults {
+ field public static final androidx.wear.tiles.DimensionBuilders.DpProp DEFAULT_VERTICAL_SPACER_HEIGHT;
+ field public static final float EDGE_CONTENT_LAYOUT_PADDING_ABOVE_MAIN_CONTENT_DP = 6.0f;
+ field public static final float EDGE_CONTENT_LAYOUT_PADDING_BELOW_MAIN_CONTENT_DP = 8.0f;
+ field public static final int MULTI_BUTTON_MAX_NUMBER = 7; // 0x7
+ field public static final androidx.wear.tiles.DimensionBuilders.DpProp MULTI_SLOT_LAYOUT_HORIZONTAL_SPACER_WIDTH;
+ }
+
+ public class MultiButtonLayout implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.layouts.MultiButtonLayout? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public java.util.List<androidx.wear.tiles.LayoutElementBuilders.LayoutElement!> getButtonContents();
+ method public int getFiveButtonDistribution();
+ field public static final int FIVE_BUTTON_DISTRIBUTION_BOTTOM_HEAVY = 2; // 0x2
+ field public static final int FIVE_BUTTON_DISTRIBUTION_TOP_HEAVY = 1; // 0x1
+ }
+
+ public static final class MultiButtonLayout.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public MultiButtonLayout.Builder();
+ method public androidx.wear.tiles.material.layouts.MultiButtonLayout.Builder addButtonContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.MultiButtonLayout build();
+ method public androidx.wear.tiles.material.layouts.MultiButtonLayout.Builder setFiveButtonDistribution(int);
+ }
+
+ public class MultiSlotLayout implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.layouts.MultiSlotLayout? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method @Dimension(unit=androidx.annotation.Dimension.DP) public float getHorizontalSpacerWidth();
+ method public java.util.List<androidx.wear.tiles.LayoutElementBuilders.LayoutElement!> getSlotContents();
+ }
+
+ public static final class MultiSlotLayout.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public MultiSlotLayout.Builder();
+ method public androidx.wear.tiles.material.layouts.MultiSlotLayout.Builder addSlotContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.MultiSlotLayout build();
+ method public androidx.wear.tiles.material.layouts.MultiSlotLayout.Builder setHorizontalSpacerWidth(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ }
+
+ public class PrimaryLayout implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.layouts.PrimaryLayout? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getContent();
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getPrimaryChipContent();
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getPrimaryLabelTextContent();
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getSecondaryLabelTextContent();
+ method @Dimension(unit=androidx.annotation.Dimension.DP) public float getVerticalSpacerHeight();
+ }
+
+ public static final class PrimaryLayout.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public PrimaryLayout.Builder(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public androidx.wear.tiles.material.layouts.PrimaryLayout build();
+ method public androidx.wear.tiles.material.layouts.PrimaryLayout.Builder setContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.PrimaryLayout.Builder setPrimaryChipContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.PrimaryLayout.Builder setPrimaryLabelTextContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.PrimaryLayout.Builder setSecondaryLabelTextContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.PrimaryLayout.Builder setVerticalSpacerHeight(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ }
+
+}
+
diff --git a/wear/tiles/tiles-material/api/res-1.1.0-beta01.txt b/wear/tiles/tiles-material/api/res-1.1.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/tiles/tiles-material/api/res-1.1.0-beta01.txt
diff --git a/wear/tiles/tiles-material/api/restricted_1.1.0-beta01.txt b/wear/tiles/tiles-material/api/restricted_1.1.0-beta01.txt
new file mode 100644
index 0000000..aecb80e
--- /dev/null
+++ b/wear/tiles/tiles-material/api/restricted_1.1.0-beta01.txt
@@ -0,0 +1,298 @@
+// Signature format: 4.0
+package androidx.wear.tiles.material {
+
+ public class Button implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.Button? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.ButtonColors getButtonColors();
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable getClickable();
+ method public CharSequence? getContentDescription();
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getCustomContent();
+ method public String? getIconContent();
+ method public String? getImageContent();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension getSize();
+ method public String? getTextContent();
+ }
+
+ public static final class Button.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public Button.Builder(android.content.Context, androidx.wear.tiles.ModifiersBuilders.Clickable);
+ method public androidx.wear.tiles.material.Button build();
+ method public androidx.wear.tiles.material.Button.Builder setButtonColors(androidx.wear.tiles.material.ButtonColors);
+ method public androidx.wear.tiles.material.Button.Builder setContentDescription(CharSequence);
+ method public androidx.wear.tiles.material.Button.Builder setCustomContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.Button.Builder setIconContent(String, androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public androidx.wear.tiles.material.Button.Builder setIconContent(String);
+ method public androidx.wear.tiles.material.Button.Builder setImageContent(String);
+ method public androidx.wear.tiles.material.Button.Builder setSize(androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public androidx.wear.tiles.material.Button.Builder setSize(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ method public androidx.wear.tiles.material.Button.Builder setTextContent(String);
+ method public androidx.wear.tiles.material.Button.Builder setTextContent(String, int);
+ }
+
+ public class ButtonColors {
+ ctor public ButtonColors(@ColorInt int, @ColorInt int);
+ ctor public ButtonColors(androidx.wear.tiles.ColorBuilders.ColorProp, androidx.wear.tiles.ColorBuilders.ColorProp);
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getBackgroundColor();
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getContentColor();
+ method public static androidx.wear.tiles.material.ButtonColors primaryButtonColors(androidx.wear.tiles.material.Colors);
+ method public static androidx.wear.tiles.material.ButtonColors secondaryButtonColors(androidx.wear.tiles.material.Colors);
+ }
+
+ public class ButtonDefaults {
+ method public static androidx.wear.tiles.DimensionBuilders.DpProp recommendedIconSize(androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public static androidx.wear.tiles.DimensionBuilders.DpProp recommendedIconSize(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ field public static final androidx.wear.tiles.DimensionBuilders.DpProp DEFAULT_SIZE;
+ field public static final androidx.wear.tiles.DimensionBuilders.DpProp EXTRA_LARGE_SIZE;
+ field public static final androidx.wear.tiles.DimensionBuilders.DpProp LARGE_SIZE;
+ field public static final androidx.wear.tiles.material.ButtonColors PRIMARY_COLORS;
+ field public static final androidx.wear.tiles.material.ButtonColors SECONDARY_COLORS;
+ }
+
+ public class Chip implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.Chip? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.ChipColors getChipColors();
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable getClickable();
+ method public CharSequence? getContentDescription();
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getCustomContent();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension getHeight();
+ method public int getHorizontalAlignment();
+ method public String? getIconContent();
+ method public String? getPrimaryLabelContent();
+ method public String? getSecondaryLabelContent();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension getWidth();
+ }
+
+ public static final class Chip.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public Chip.Builder(android.content.Context, androidx.wear.tiles.ModifiersBuilders.Clickable, androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public androidx.wear.tiles.material.Chip build();
+ method public androidx.wear.tiles.material.Chip.Builder setChipColors(androidx.wear.tiles.material.ChipColors);
+ method public androidx.wear.tiles.material.Chip.Builder setContentDescription(CharSequence);
+ method public androidx.wear.tiles.material.Chip.Builder setCustomContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.Chip.Builder setHorizontalAlignment(int);
+ method public androidx.wear.tiles.material.Chip.Builder setIconContent(String);
+ method public androidx.wear.tiles.material.Chip.Builder setPrimaryLabelContent(String);
+ method public androidx.wear.tiles.material.Chip.Builder setSecondaryLabelContent(String);
+ method public androidx.wear.tiles.material.Chip.Builder setWidth(androidx.wear.tiles.DimensionBuilders.ContainerDimension);
+ method public androidx.wear.tiles.material.Chip.Builder setWidth(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ }
+
+ public class ChipColors {
+ ctor public ChipColors(@ColorInt int, @ColorInt int, @ColorInt int, @ColorInt int);
+ ctor public ChipColors(@ColorInt int, @ColorInt int);
+ ctor public ChipColors(androidx.wear.tiles.ColorBuilders.ColorProp, androidx.wear.tiles.ColorBuilders.ColorProp, androidx.wear.tiles.ColorBuilders.ColorProp, androidx.wear.tiles.ColorBuilders.ColorProp);
+ ctor public ChipColors(androidx.wear.tiles.ColorBuilders.ColorProp, androidx.wear.tiles.ColorBuilders.ColorProp);
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getBackgroundColor();
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getContentColor();
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getIconColor();
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getSecondaryContentColor();
+ method public static androidx.wear.tiles.material.ChipColors primaryChipColors(androidx.wear.tiles.material.Colors);
+ method public static androidx.wear.tiles.material.ChipColors secondaryChipColors(androidx.wear.tiles.material.Colors);
+ }
+
+ public class ChipDefaults {
+ field public static final androidx.wear.tiles.material.ChipColors COMPACT_PRIMARY_COLORS;
+ field public static final androidx.wear.tiles.material.ChipColors COMPACT_SECONDARY_COLORS;
+ field public static final androidx.wear.tiles.material.ChipColors PRIMARY_COLORS;
+ field public static final androidx.wear.tiles.material.ChipColors SECONDARY_COLORS;
+ field public static final androidx.wear.tiles.material.ChipColors TITLE_PRIMARY_COLORS;
+ field public static final androidx.wear.tiles.material.ChipColors TITLE_SECONDARY_COLORS;
+ }
+
+ public class CircularProgressIndicator implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.CircularProgressIndicator? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.ProgressIndicatorColors getCircularProgressIndicatorColors();
+ method public CharSequence? getContentDescription();
+ method public androidx.wear.tiles.DimensionBuilders.DegreesProp getEndAngle();
+ method public androidx.wear.tiles.DimensionBuilders.DegreesProp getProgress();
+ method public androidx.wear.tiles.DimensionBuilders.DegreesProp getStartAngle();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp getStrokeWidth();
+ }
+
+ public static final class CircularProgressIndicator.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public CircularProgressIndicator.Builder();
+ method public androidx.wear.tiles.material.CircularProgressIndicator build();
+ method public androidx.wear.tiles.material.CircularProgressIndicator.Builder setCircularProgressIndicatorColors(androidx.wear.tiles.material.ProgressIndicatorColors);
+ method public androidx.wear.tiles.material.CircularProgressIndicator.Builder setContentDescription(CharSequence);
+ method public androidx.wear.tiles.material.CircularProgressIndicator.Builder setEndAngle(float);
+ method public androidx.wear.tiles.material.CircularProgressIndicator.Builder setProgress(@FloatRange(from=0, to=1) float);
+ method public androidx.wear.tiles.material.CircularProgressIndicator.Builder setStartAngle(float);
+ method public androidx.wear.tiles.material.CircularProgressIndicator.Builder setStrokeWidth(androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public androidx.wear.tiles.material.CircularProgressIndicator.Builder setStrokeWidth(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ }
+
+ public class Colors {
+ ctor public Colors(@ColorInt int, @ColorInt int, @ColorInt int, @ColorInt int);
+ method @ColorInt public int getOnPrimary();
+ method @ColorInt public int getOnSurface();
+ method @ColorInt public int getPrimary();
+ method @ColorInt public int getSurface();
+ field public static final androidx.wear.tiles.material.Colors DEFAULT;
+ }
+
+ public class CompactChip implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.CompactChip? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.ChipColors getChipColors();
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable getClickable();
+ method public String getText();
+ }
+
+ public static final class CompactChip.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public CompactChip.Builder(android.content.Context, String, androidx.wear.tiles.ModifiersBuilders.Clickable, androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public androidx.wear.tiles.material.CompactChip build();
+ method public androidx.wear.tiles.material.CompactChip.Builder setChipColors(androidx.wear.tiles.material.ChipColors);
+ }
+
+ public class ProgressIndicatorColors {
+ ctor public ProgressIndicatorColors(androidx.wear.tiles.ColorBuilders.ColorProp, androidx.wear.tiles.ColorBuilders.ColorProp);
+ ctor public ProgressIndicatorColors(@ColorInt int, @ColorInt int);
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getIndicatorColor();
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getTrackColor();
+ method public static androidx.wear.tiles.material.ProgressIndicatorColors progressIndicatorColors(androidx.wear.tiles.material.Colors);
+ }
+
+ public class ProgressIndicatorDefaults {
+ field public static final androidx.wear.tiles.material.ProgressIndicatorColors DEFAULT_COLORS;
+ field public static final androidx.wear.tiles.DimensionBuilders.DpProp DEFAULT_STROKE_WIDTH;
+ field public static final float GAP_END_ANGLE = 156.1f;
+ field public static final float GAP_START_ANGLE = -156.1f;
+ }
+
+ public class Text implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.Text? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.ColorBuilders.ColorProp getColor();
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle getFontStyle();
+ method public float getLineHeight();
+ method public int getMaxLines();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers getModifiers();
+ method public int getMultilineAlignment();
+ method public int getOverflow();
+ method public String getText();
+ method public int getWeight();
+ method public boolean isItalic();
+ method public boolean isUnderline();
+ }
+
+ public static final class Text.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public Text.Builder(android.content.Context, String);
+ method public androidx.wear.tiles.material.Text build();
+ method public androidx.wear.tiles.material.Text.Builder setColor(androidx.wear.tiles.ColorBuilders.ColorProp);
+ method public androidx.wear.tiles.material.Text.Builder setItalic(boolean);
+ method public androidx.wear.tiles.material.Text.Builder setMaxLines(@IntRange(from=1) int);
+ method public androidx.wear.tiles.material.Text.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.material.Text.Builder setMultilineAlignment(int);
+ method public androidx.wear.tiles.material.Text.Builder setOverflow(int);
+ method public androidx.wear.tiles.material.Text.Builder setTypography(int);
+ method public androidx.wear.tiles.material.Text.Builder setUnderline(boolean);
+ method public androidx.wear.tiles.material.Text.Builder setWeight(int);
+ }
+
+ public class TitleChip implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.TitleChip? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.ChipColors getChipColors();
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable getClickable();
+ method public int getHorizontalAlignment();
+ method public String getText();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension getWidth();
+ }
+
+ public static final class TitleChip.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public TitleChip.Builder(android.content.Context, String, androidx.wear.tiles.ModifiersBuilders.Clickable, androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public androidx.wear.tiles.material.TitleChip build();
+ method public androidx.wear.tiles.material.TitleChip.Builder setChipColors(androidx.wear.tiles.material.ChipColors);
+ method public androidx.wear.tiles.material.TitleChip.Builder setHorizontalAlignment(int);
+ method public androidx.wear.tiles.material.TitleChip.Builder setWidth(androidx.wear.tiles.DimensionBuilders.ContainerDimension);
+ method public androidx.wear.tiles.material.TitleChip.Builder setWidth(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ }
+
+ public class Typography {
+ field public static final int TYPOGRAPHY_BODY1 = 7; // 0x7
+ field public static final int TYPOGRAPHY_BODY2 = 8; // 0x8
+ field public static final int TYPOGRAPHY_BUTTON = 9; // 0x9
+ field public static final int TYPOGRAPHY_CAPTION1 = 10; // 0xa
+ field public static final int TYPOGRAPHY_CAPTION2 = 11; // 0xb
+ field public static final int TYPOGRAPHY_CAPTION3 = 12; // 0xc
+ field public static final int TYPOGRAPHY_DISPLAY1 = 1; // 0x1
+ field public static final int TYPOGRAPHY_DISPLAY2 = 2; // 0x2
+ field public static final int TYPOGRAPHY_DISPLAY3 = 3; // 0x3
+ field public static final int TYPOGRAPHY_TITLE1 = 4; // 0x4
+ field public static final int TYPOGRAPHY_TITLE2 = 5; // 0x5
+ field public static final int TYPOGRAPHY_TITLE3 = 6; // 0x6
+ }
+
+}
+
+package androidx.wear.tiles.material.layouts {
+
+ public class EdgeContentLayout implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.layouts.EdgeContentLayout? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getContent();
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getEdgeContent();
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getPrimaryLabelTextContent();
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getSecondaryLabelTextContent();
+ }
+
+ public static final class EdgeContentLayout.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public EdgeContentLayout.Builder(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public androidx.wear.tiles.material.layouts.EdgeContentLayout build();
+ method public androidx.wear.tiles.material.layouts.EdgeContentLayout.Builder setContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.EdgeContentLayout.Builder setEdgeContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.EdgeContentLayout.Builder setPrimaryLabelTextContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.EdgeContentLayout.Builder setSecondaryLabelTextContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ }
+
+ public class LayoutDefaults {
+ field public static final androidx.wear.tiles.DimensionBuilders.DpProp DEFAULT_VERTICAL_SPACER_HEIGHT;
+ field public static final float EDGE_CONTENT_LAYOUT_PADDING_ABOVE_MAIN_CONTENT_DP = 6.0f;
+ field public static final float EDGE_CONTENT_LAYOUT_PADDING_BELOW_MAIN_CONTENT_DP = 8.0f;
+ field public static final int MULTI_BUTTON_MAX_NUMBER = 7; // 0x7
+ field public static final androidx.wear.tiles.DimensionBuilders.DpProp MULTI_SLOT_LAYOUT_HORIZONTAL_SPACER_WIDTH;
+ }
+
+ public class MultiButtonLayout implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.layouts.MultiButtonLayout? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public java.util.List<androidx.wear.tiles.LayoutElementBuilders.LayoutElement!> getButtonContents();
+ method public int getFiveButtonDistribution();
+ field public static final int FIVE_BUTTON_DISTRIBUTION_BOTTOM_HEAVY = 2; // 0x2
+ field public static final int FIVE_BUTTON_DISTRIBUTION_TOP_HEAVY = 1; // 0x1
+ }
+
+ public static final class MultiButtonLayout.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public MultiButtonLayout.Builder();
+ method public androidx.wear.tiles.material.layouts.MultiButtonLayout.Builder addButtonContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.MultiButtonLayout build();
+ method public androidx.wear.tiles.material.layouts.MultiButtonLayout.Builder setFiveButtonDistribution(int);
+ }
+
+ public class MultiSlotLayout implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.layouts.MultiSlotLayout? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method @Dimension(unit=androidx.annotation.Dimension.DP) public float getHorizontalSpacerWidth();
+ method public java.util.List<androidx.wear.tiles.LayoutElementBuilders.LayoutElement!> getSlotContents();
+ }
+
+ public static final class MultiSlotLayout.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public MultiSlotLayout.Builder();
+ method public androidx.wear.tiles.material.layouts.MultiSlotLayout.Builder addSlotContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.MultiSlotLayout build();
+ method public androidx.wear.tiles.material.layouts.MultiSlotLayout.Builder setHorizontalSpacerWidth(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ }
+
+ public class PrimaryLayout implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public static androidx.wear.tiles.material.layouts.PrimaryLayout? fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getContent();
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getPrimaryChipContent();
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getPrimaryLabelTextContent();
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getSecondaryLabelTextContent();
+ method @Dimension(unit=androidx.annotation.Dimension.DP) public float getVerticalSpacerHeight();
+ }
+
+ public static final class PrimaryLayout.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public PrimaryLayout.Builder(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public androidx.wear.tiles.material.layouts.PrimaryLayout build();
+ method public androidx.wear.tiles.material.layouts.PrimaryLayout.Builder setContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.PrimaryLayout.Builder setPrimaryChipContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.PrimaryLayout.Builder setPrimaryLabelTextContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.PrimaryLayout.Builder setSecondaryLabelTextContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.material.layouts.PrimaryLayout.Builder setVerticalSpacerHeight(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ }
+
+}
+
diff --git a/wear/tiles/tiles-material/build.gradle b/wear/tiles/tiles-material/build.gradle
index 014dcf7..a7156b4c 100644
--- a/wear/tiles/tiles-material/build.gradle
+++ b/wear/tiles/tiles-material/build.gradle
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+import androidx.build.Publish
import androidx.build.LibraryType
plugins {
@@ -71,6 +72,7 @@
androidx {
name = "Android Wear Tiles Material"
type = LibraryType.PUBLISHED_LIBRARY
+ publish = Publish.SNAPSHOT_AND_RELEASE
mavenGroup = LibraryGroups.WEAR_TILES
inceptionYear = "2021"
description = "Material components library for Android Wear Tiles."
diff --git a/wear/tiles/tiles-renderer/api/1.1.0-beta01.txt b/wear/tiles/tiles-renderer/api/1.1.0-beta01.txt
new file mode 100644
index 0000000..a162ceb
--- /dev/null
+++ b/wear/tiles/tiles-renderer/api/1.1.0-beta01.txt
@@ -0,0 +1,80 @@
+// Signature format: 4.0
+package androidx.wear.tiles.client {
+
+ public interface TileClient {
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> requestApiVersion();
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.ResourceBuilders.Resources!> requestResources(androidx.wear.tiles.RequestBuilders.ResourcesRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.TileBuilders.Tile!> requestTile(androidx.wear.tiles.RequestBuilders.TileRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> sendOnTileAddedEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> sendOnTileEnterEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> sendOnTileLeaveEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> sendOnTileRemovedEvent();
+ }
+
+}
+
+package androidx.wear.tiles.connection {
+
+ public final class DefaultTileClient implements androidx.wear.tiles.client.TileClient {
+ ctor public DefaultTileClient(android.content.Context context, android.content.ComponentName componentName, kotlinx.coroutines.CoroutineScope coroutineScope, kotlinx.coroutines.CoroutineDispatcher coroutineDispatcher);
+ ctor public DefaultTileClient(android.content.Context context, android.content.ComponentName componentName, java.util.concurrent.Executor executor);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer> requestApiVersion();
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.ResourceBuilders.Resources> requestResources(androidx.wear.tiles.RequestBuilders.ResourcesRequest requestParams);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.TileBuilders.Tile> requestTile(androidx.wear.tiles.RequestBuilders.TileRequest requestParams);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> sendOnTileAddedEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> sendOnTileEnterEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> sendOnTileLeaveEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> sendOnTileRemovedEvent();
+ }
+
+}
+
+package androidx.wear.tiles.manager {
+
+ public final class TileUiClient implements java.lang.AutoCloseable {
+ ctor public TileUiClient(android.content.Context context, android.content.ComponentName component, android.view.ViewGroup parentView);
+ method @MainThread public void close();
+ method @MainThread public void connect();
+ }
+
+}
+
+package androidx.wear.tiles.renderer {
+
+ public final class TileRenderer {
+ ctor public TileRenderer(android.content.Context, androidx.wear.tiles.LayoutElementBuilders.Layout, androidx.wear.tiles.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
+ ctor public TileRenderer(android.content.Context, androidx.wear.tiles.LayoutElementBuilders.Layout, @StyleRes int, androidx.wear.tiles.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
+ method public android.view.View? inflate(android.view.ViewGroup);
+ }
+
+ public static interface TileRenderer.LoadActionListener {
+ method public void onClick(androidx.wear.tiles.StateBuilders.State);
+ }
+
+}
+
+package androidx.wear.tiles.timeline {
+
+ public final class TilesTimelineCache {
+ ctor public TilesTimelineCache(androidx.wear.tiles.TimelineBuilders.Timeline);
+ method @MainThread public androidx.wear.tiles.TimelineBuilders.TimelineEntry? findClosestTimelineEntry(long);
+ method @MainThread public long findCurrentTimelineEntryExpiry(androidx.wear.tiles.TimelineBuilders.TimelineEntry, long);
+ method @MainThread public androidx.wear.tiles.TimelineBuilders.TimelineEntry? findTimelineEntryForTime(long);
+ }
+
+ public class TilesTimelineManager implements java.lang.AutoCloseable {
+ ctor public TilesTimelineManager(android.app.AlarmManager, androidx.wear.tiles.timeline.TilesTimelineManager.Clock, androidx.wear.tiles.TimelineBuilders.Timeline, int, java.util.concurrent.Executor, androidx.wear.tiles.timeline.TilesTimelineManager.Listener);
+ method public void close();
+ method public void init();
+ }
+
+ public static interface TilesTimelineManager.Clock {
+ method public long getCurrentTimeMillis();
+ }
+
+ public static interface TilesTimelineManager.Listener {
+ method public void onLayoutUpdate(int, androidx.wear.tiles.LayoutElementBuilders.Layout);
+ }
+
+}
+
diff --git a/wear/tiles/tiles-renderer/api/public_plus_experimental_1.1.0-beta01.txt b/wear/tiles/tiles-renderer/api/public_plus_experimental_1.1.0-beta01.txt
new file mode 100644
index 0000000..a162ceb
--- /dev/null
+++ b/wear/tiles/tiles-renderer/api/public_plus_experimental_1.1.0-beta01.txt
@@ -0,0 +1,80 @@
+// Signature format: 4.0
+package androidx.wear.tiles.client {
+
+ public interface TileClient {
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> requestApiVersion();
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.ResourceBuilders.Resources!> requestResources(androidx.wear.tiles.RequestBuilders.ResourcesRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.TileBuilders.Tile!> requestTile(androidx.wear.tiles.RequestBuilders.TileRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> sendOnTileAddedEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> sendOnTileEnterEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> sendOnTileLeaveEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> sendOnTileRemovedEvent();
+ }
+
+}
+
+package androidx.wear.tiles.connection {
+
+ public final class DefaultTileClient implements androidx.wear.tiles.client.TileClient {
+ ctor public DefaultTileClient(android.content.Context context, android.content.ComponentName componentName, kotlinx.coroutines.CoroutineScope coroutineScope, kotlinx.coroutines.CoroutineDispatcher coroutineDispatcher);
+ ctor public DefaultTileClient(android.content.Context context, android.content.ComponentName componentName, java.util.concurrent.Executor executor);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer> requestApiVersion();
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.ResourceBuilders.Resources> requestResources(androidx.wear.tiles.RequestBuilders.ResourcesRequest requestParams);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.TileBuilders.Tile> requestTile(androidx.wear.tiles.RequestBuilders.TileRequest requestParams);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> sendOnTileAddedEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> sendOnTileEnterEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> sendOnTileLeaveEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> sendOnTileRemovedEvent();
+ }
+
+}
+
+package androidx.wear.tiles.manager {
+
+ public final class TileUiClient implements java.lang.AutoCloseable {
+ ctor public TileUiClient(android.content.Context context, android.content.ComponentName component, android.view.ViewGroup parentView);
+ method @MainThread public void close();
+ method @MainThread public void connect();
+ }
+
+}
+
+package androidx.wear.tiles.renderer {
+
+ public final class TileRenderer {
+ ctor public TileRenderer(android.content.Context, androidx.wear.tiles.LayoutElementBuilders.Layout, androidx.wear.tiles.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
+ ctor public TileRenderer(android.content.Context, androidx.wear.tiles.LayoutElementBuilders.Layout, @StyleRes int, androidx.wear.tiles.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
+ method public android.view.View? inflate(android.view.ViewGroup);
+ }
+
+ public static interface TileRenderer.LoadActionListener {
+ method public void onClick(androidx.wear.tiles.StateBuilders.State);
+ }
+
+}
+
+package androidx.wear.tiles.timeline {
+
+ public final class TilesTimelineCache {
+ ctor public TilesTimelineCache(androidx.wear.tiles.TimelineBuilders.Timeline);
+ method @MainThread public androidx.wear.tiles.TimelineBuilders.TimelineEntry? findClosestTimelineEntry(long);
+ method @MainThread public long findCurrentTimelineEntryExpiry(androidx.wear.tiles.TimelineBuilders.TimelineEntry, long);
+ method @MainThread public androidx.wear.tiles.TimelineBuilders.TimelineEntry? findTimelineEntryForTime(long);
+ }
+
+ public class TilesTimelineManager implements java.lang.AutoCloseable {
+ ctor public TilesTimelineManager(android.app.AlarmManager, androidx.wear.tiles.timeline.TilesTimelineManager.Clock, androidx.wear.tiles.TimelineBuilders.Timeline, int, java.util.concurrent.Executor, androidx.wear.tiles.timeline.TilesTimelineManager.Listener);
+ method public void close();
+ method public void init();
+ }
+
+ public static interface TilesTimelineManager.Clock {
+ method public long getCurrentTimeMillis();
+ }
+
+ public static interface TilesTimelineManager.Listener {
+ method public void onLayoutUpdate(int, androidx.wear.tiles.LayoutElementBuilders.Layout);
+ }
+
+}
+
diff --git a/wear/tiles/tiles-renderer/api/res-1.1.0-beta01.txt b/wear/tiles/tiles-renderer/api/res-1.1.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/tiles/tiles-renderer/api/res-1.1.0-beta01.txt
diff --git a/wear/tiles/tiles-renderer/api/restricted_1.1.0-beta01.txt b/wear/tiles/tiles-renderer/api/restricted_1.1.0-beta01.txt
new file mode 100644
index 0000000..a162ceb
--- /dev/null
+++ b/wear/tiles/tiles-renderer/api/restricted_1.1.0-beta01.txt
@@ -0,0 +1,80 @@
+// Signature format: 4.0
+package androidx.wear.tiles.client {
+
+ public interface TileClient {
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> requestApiVersion();
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.ResourceBuilders.Resources!> requestResources(androidx.wear.tiles.RequestBuilders.ResourcesRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.TileBuilders.Tile!> requestTile(androidx.wear.tiles.RequestBuilders.TileRequest);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> sendOnTileAddedEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> sendOnTileEnterEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> sendOnTileLeaveEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> sendOnTileRemovedEvent();
+ }
+
+}
+
+package androidx.wear.tiles.connection {
+
+ public final class DefaultTileClient implements androidx.wear.tiles.client.TileClient {
+ ctor public DefaultTileClient(android.content.Context context, android.content.ComponentName componentName, kotlinx.coroutines.CoroutineScope coroutineScope, kotlinx.coroutines.CoroutineDispatcher coroutineDispatcher);
+ ctor public DefaultTileClient(android.content.Context context, android.content.ComponentName componentName, java.util.concurrent.Executor executor);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer> requestApiVersion();
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.ResourceBuilders.Resources> requestResources(androidx.wear.tiles.RequestBuilders.ResourcesRequest requestParams);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.TileBuilders.Tile> requestTile(androidx.wear.tiles.RequestBuilders.TileRequest requestParams);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> sendOnTileAddedEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> sendOnTileEnterEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> sendOnTileLeaveEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> sendOnTileRemovedEvent();
+ }
+
+}
+
+package androidx.wear.tiles.manager {
+
+ public final class TileUiClient implements java.lang.AutoCloseable {
+ ctor public TileUiClient(android.content.Context context, android.content.ComponentName component, android.view.ViewGroup parentView);
+ method @MainThread public void close();
+ method @MainThread public void connect();
+ }
+
+}
+
+package androidx.wear.tiles.renderer {
+
+ public final class TileRenderer {
+ ctor public TileRenderer(android.content.Context, androidx.wear.tiles.LayoutElementBuilders.Layout, androidx.wear.tiles.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
+ ctor public TileRenderer(android.content.Context, androidx.wear.tiles.LayoutElementBuilders.Layout, @StyleRes int, androidx.wear.tiles.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
+ method public android.view.View? inflate(android.view.ViewGroup);
+ }
+
+ public static interface TileRenderer.LoadActionListener {
+ method public void onClick(androidx.wear.tiles.StateBuilders.State);
+ }
+
+}
+
+package androidx.wear.tiles.timeline {
+
+ public final class TilesTimelineCache {
+ ctor public TilesTimelineCache(androidx.wear.tiles.TimelineBuilders.Timeline);
+ method @MainThread public androidx.wear.tiles.TimelineBuilders.TimelineEntry? findClosestTimelineEntry(long);
+ method @MainThread public long findCurrentTimelineEntryExpiry(androidx.wear.tiles.TimelineBuilders.TimelineEntry, long);
+ method @MainThread public androidx.wear.tiles.TimelineBuilders.TimelineEntry? findTimelineEntryForTime(long);
+ }
+
+ public class TilesTimelineManager implements java.lang.AutoCloseable {
+ ctor public TilesTimelineManager(android.app.AlarmManager, androidx.wear.tiles.timeline.TilesTimelineManager.Clock, androidx.wear.tiles.TimelineBuilders.Timeline, int, java.util.concurrent.Executor, androidx.wear.tiles.timeline.TilesTimelineManager.Listener);
+ method public void close();
+ method public void init();
+ }
+
+ public static interface TilesTimelineManager.Clock {
+ method public long getCurrentTimeMillis();
+ }
+
+ public static interface TilesTimelineManager.Listener {
+ method public void onLayoutUpdate(int, androidx.wear.tiles.LayoutElementBuilders.Layout);
+ }
+
+}
+
diff --git a/wear/tiles/tiles-renderer/build.gradle b/wear/tiles/tiles-renderer/build.gradle
index f7c230b..1f8a366 100644
--- a/wear/tiles/tiles-renderer/build.gradle
+++ b/wear/tiles/tiles-renderer/build.gradle
@@ -16,6 +16,7 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+import androidx.build.Publish
import androidx.build.LibraryType
plugins {
@@ -111,6 +112,7 @@
androidx {
name = "Android Wear Tiles Renderer"
type = LibraryType.PUBLISHED_LIBRARY
+ publish = Publish.SNAPSHOT_AND_RELEASE
mavenGroup = LibraryGroups.WEAR_TILES
inceptionYear = "2021"
description = "Android Wear Tiles Renderer components. These components can be used to parse " +
diff --git a/wear/tiles/tiles-testing/api/1.1.0-beta01.txt b/wear/tiles/tiles-testing/api/1.1.0-beta01.txt
new file mode 100644
index 0000000..353b0c6
--- /dev/null
+++ b/wear/tiles/tiles-testing/api/1.1.0-beta01.txt
@@ -0,0 +1,17 @@
+// Signature format: 4.0
+package androidx.wear.tiles.testing {
+
+ public final class TestTileClient<T extends androidx.wear.tiles.TileService> implements androidx.wear.tiles.client.TileClient {
+ ctor public TestTileClient(T service, kotlinx.coroutines.CoroutineScope coroutineScope, kotlinx.coroutines.CoroutineDispatcher coroutineDispatcher);
+ ctor public TestTileClient(T service, java.util.concurrent.Executor executor);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer> requestApiVersion();
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.ResourceBuilders.Resources> requestResources(androidx.wear.tiles.RequestBuilders.ResourcesRequest requestParams);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.TileBuilders.Tile> requestTile(androidx.wear.tiles.RequestBuilders.TileRequest requestParams);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> sendOnTileAddedEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> sendOnTileEnterEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> sendOnTileLeaveEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> sendOnTileRemovedEvent();
+ }
+
+}
+
diff --git a/wear/tiles/tiles-testing/api/public_plus_experimental_1.1.0-beta01.txt b/wear/tiles/tiles-testing/api/public_plus_experimental_1.1.0-beta01.txt
new file mode 100644
index 0000000..353b0c6
--- /dev/null
+++ b/wear/tiles/tiles-testing/api/public_plus_experimental_1.1.0-beta01.txt
@@ -0,0 +1,17 @@
+// Signature format: 4.0
+package androidx.wear.tiles.testing {
+
+ public final class TestTileClient<T extends androidx.wear.tiles.TileService> implements androidx.wear.tiles.client.TileClient {
+ ctor public TestTileClient(T service, kotlinx.coroutines.CoroutineScope coroutineScope, kotlinx.coroutines.CoroutineDispatcher coroutineDispatcher);
+ ctor public TestTileClient(T service, java.util.concurrent.Executor executor);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer> requestApiVersion();
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.ResourceBuilders.Resources> requestResources(androidx.wear.tiles.RequestBuilders.ResourcesRequest requestParams);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.TileBuilders.Tile> requestTile(androidx.wear.tiles.RequestBuilders.TileRequest requestParams);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> sendOnTileAddedEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> sendOnTileEnterEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> sendOnTileLeaveEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> sendOnTileRemovedEvent();
+ }
+
+}
+
diff --git a/wear/tiles/tiles-testing/api/res-1.1.0-beta01.txt b/wear/tiles/tiles-testing/api/res-1.1.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/tiles/tiles-testing/api/res-1.1.0-beta01.txt
diff --git a/wear/tiles/tiles-testing/api/restricted_1.1.0-beta01.txt b/wear/tiles/tiles-testing/api/restricted_1.1.0-beta01.txt
new file mode 100644
index 0000000..353b0c6
--- /dev/null
+++ b/wear/tiles/tiles-testing/api/restricted_1.1.0-beta01.txt
@@ -0,0 +1,17 @@
+// Signature format: 4.0
+package androidx.wear.tiles.testing {
+
+ public final class TestTileClient<T extends androidx.wear.tiles.TileService> implements androidx.wear.tiles.client.TileClient {
+ ctor public TestTileClient(T service, kotlinx.coroutines.CoroutineScope coroutineScope, kotlinx.coroutines.CoroutineDispatcher coroutineDispatcher);
+ ctor public TestTileClient(T service, java.util.concurrent.Executor executor);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer> requestApiVersion();
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.ResourceBuilders.Resources> requestResources(androidx.wear.tiles.RequestBuilders.ResourcesRequest requestParams);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.TileBuilders.Tile> requestTile(androidx.wear.tiles.RequestBuilders.TileRequest requestParams);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> sendOnTileAddedEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> sendOnTileEnterEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> sendOnTileLeaveEvent();
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> sendOnTileRemovedEvent();
+ }
+
+}
+
diff --git a/wear/tiles/tiles-testing/build.gradle b/wear/tiles/tiles-testing/build.gradle
index 7d23776..ddc83b8 100644
--- a/wear/tiles/tiles-testing/build.gradle
+++ b/wear/tiles/tiles-testing/build.gradle
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+import androidx.build.Publish
import androidx.build.LibraryType
plugins {
@@ -61,6 +62,7 @@
androidx {
name = "Android Wear Tiles Testing Utilities"
type = LibraryType.PUBLISHED_TEST_LIBRARY
+ publish = Publish.SNAPSHOT_AND_RELEASE
mavenGroup = LibraryGroups.WEAR_TILES
inceptionYear = "2021"
description = "Testing utilities for Android Wear Tiles."
diff --git a/wear/tiles/tiles/api/1.1.0-beta01.txt b/wear/tiles/tiles/api/1.1.0-beta01.txt
new file mode 100644
index 0000000..1f4b5140
--- /dev/null
+++ b/wear/tiles/tiles/api/1.1.0-beta01.txt
@@ -0,0 +1,1084 @@
+// Signature format: 4.0
+package androidx.wear.tiles {
+
+ public final class ActionBuilders {
+ method public static androidx.wear.tiles.ActionBuilders.AndroidBooleanExtra booleanExtra(boolean);
+ method public static androidx.wear.tiles.ActionBuilders.AndroidDoubleExtra doubleExtra(double);
+ method public static androidx.wear.tiles.ActionBuilders.AndroidIntExtra intExtra(int);
+ method public static androidx.wear.tiles.ActionBuilders.AndroidLongExtra longExtra(long);
+ method public static androidx.wear.tiles.ActionBuilders.AndroidStringExtra stringExtra(String);
+ }
+
+ public static interface ActionBuilders.Action {
+ }
+
+ public static interface ActionBuilders.Action.Builder {
+ method public androidx.wear.tiles.ActionBuilders.Action build();
+ }
+
+ public static final class ActionBuilders.AndroidActivity {
+ method public String getClassName();
+ method public java.util.Map<java.lang.String!,androidx.wear.tiles.ActionBuilders.AndroidExtra!> getKeyToExtraMapping();
+ method public String getPackageName();
+ }
+
+ public static final class ActionBuilders.AndroidActivity.Builder {
+ ctor public ActionBuilders.AndroidActivity.Builder();
+ method public androidx.wear.tiles.ActionBuilders.AndroidActivity.Builder addKeyToExtraMapping(String, androidx.wear.tiles.ActionBuilders.AndroidExtra);
+ method public androidx.wear.tiles.ActionBuilders.AndroidActivity build();
+ method public androidx.wear.tiles.ActionBuilders.AndroidActivity.Builder setClassName(String);
+ method public androidx.wear.tiles.ActionBuilders.AndroidActivity.Builder setPackageName(String);
+ }
+
+ public static final class ActionBuilders.AndroidBooleanExtra implements androidx.wear.tiles.ActionBuilders.AndroidExtra {
+ method public boolean getValue();
+ }
+
+ public static final class ActionBuilders.AndroidBooleanExtra.Builder implements androidx.wear.tiles.ActionBuilders.AndroidExtra.Builder {
+ ctor public ActionBuilders.AndroidBooleanExtra.Builder();
+ method public androidx.wear.tiles.ActionBuilders.AndroidBooleanExtra build();
+ method public androidx.wear.tiles.ActionBuilders.AndroidBooleanExtra.Builder setValue(boolean);
+ }
+
+ public static final class ActionBuilders.AndroidDoubleExtra implements androidx.wear.tiles.ActionBuilders.AndroidExtra {
+ method public double getValue();
+ }
+
+ public static final class ActionBuilders.AndroidDoubleExtra.Builder implements androidx.wear.tiles.ActionBuilders.AndroidExtra.Builder {
+ ctor public ActionBuilders.AndroidDoubleExtra.Builder();
+ method public androidx.wear.tiles.ActionBuilders.AndroidDoubleExtra build();
+ method public androidx.wear.tiles.ActionBuilders.AndroidDoubleExtra.Builder setValue(double);
+ }
+
+ public static interface ActionBuilders.AndroidExtra {
+ }
+
+ public static interface ActionBuilders.AndroidExtra.Builder {
+ method public androidx.wear.tiles.ActionBuilders.AndroidExtra build();
+ }
+
+ public static final class ActionBuilders.AndroidIntExtra implements androidx.wear.tiles.ActionBuilders.AndroidExtra {
+ method public int getValue();
+ }
+
+ public static final class ActionBuilders.AndroidIntExtra.Builder implements androidx.wear.tiles.ActionBuilders.AndroidExtra.Builder {
+ ctor public ActionBuilders.AndroidIntExtra.Builder();
+ method public androidx.wear.tiles.ActionBuilders.AndroidIntExtra build();
+ method public androidx.wear.tiles.ActionBuilders.AndroidIntExtra.Builder setValue(int);
+ }
+
+ public static final class ActionBuilders.AndroidLongExtra implements androidx.wear.tiles.ActionBuilders.AndroidExtra {
+ method public long getValue();
+ }
+
+ public static final class ActionBuilders.AndroidLongExtra.Builder implements androidx.wear.tiles.ActionBuilders.AndroidExtra.Builder {
+ ctor public ActionBuilders.AndroidLongExtra.Builder();
+ method public androidx.wear.tiles.ActionBuilders.AndroidLongExtra build();
+ method public androidx.wear.tiles.ActionBuilders.AndroidLongExtra.Builder setValue(long);
+ }
+
+ public static final class ActionBuilders.AndroidStringExtra implements androidx.wear.tiles.ActionBuilders.AndroidExtra {
+ method public String getValue();
+ }
+
+ public static final class ActionBuilders.AndroidStringExtra.Builder implements androidx.wear.tiles.ActionBuilders.AndroidExtra.Builder {
+ ctor public ActionBuilders.AndroidStringExtra.Builder();
+ method public androidx.wear.tiles.ActionBuilders.AndroidStringExtra build();
+ method public androidx.wear.tiles.ActionBuilders.AndroidStringExtra.Builder setValue(String);
+ }
+
+ public static final class ActionBuilders.LaunchAction implements androidx.wear.tiles.ActionBuilders.Action {
+ method public androidx.wear.tiles.ActionBuilders.AndroidActivity? getAndroidActivity();
+ }
+
+ public static final class ActionBuilders.LaunchAction.Builder implements androidx.wear.tiles.ActionBuilders.Action.Builder {
+ ctor public ActionBuilders.LaunchAction.Builder();
+ method public androidx.wear.tiles.ActionBuilders.LaunchAction build();
+ method public androidx.wear.tiles.ActionBuilders.LaunchAction.Builder setAndroidActivity(androidx.wear.tiles.ActionBuilders.AndroidActivity);
+ }
+
+ public static final class ActionBuilders.LoadAction implements androidx.wear.tiles.ActionBuilders.Action {
+ method public androidx.wear.tiles.StateBuilders.State? getRequestState();
+ }
+
+ public static final class ActionBuilders.LoadAction.Builder implements androidx.wear.tiles.ActionBuilders.Action.Builder {
+ ctor public ActionBuilders.LoadAction.Builder();
+ method public androidx.wear.tiles.ActionBuilders.LoadAction build();
+ method public androidx.wear.tiles.ActionBuilders.LoadAction.Builder setRequestState(androidx.wear.tiles.StateBuilders.State);
+ }
+
+ public final class ColorBuilders {
+ method public static androidx.wear.tiles.ColorBuilders.ColorProp argb(@ColorInt int);
+ }
+
+ public static final class ColorBuilders.ColorProp {
+ method @ColorInt public int getArgb();
+ }
+
+ public static final class ColorBuilders.ColorProp.Builder {
+ ctor public ColorBuilders.ColorProp.Builder();
+ method public androidx.wear.tiles.ColorBuilders.ColorProp build();
+ method public androidx.wear.tiles.ColorBuilders.ColorProp.Builder setArgb(@ColorInt int);
+ }
+
+ public final class DeviceParametersBuilders {
+ field public static final int DEVICE_PLATFORM_UNDEFINED = 0; // 0x0
+ field public static final int DEVICE_PLATFORM_WEAR_OS = 1; // 0x1
+ field public static final int SCREEN_SHAPE_RECT = 2; // 0x2
+ field public static final int SCREEN_SHAPE_ROUND = 1; // 0x1
+ field public static final int SCREEN_SHAPE_UNDEFINED = 0; // 0x0
+ }
+
+ public static final class DeviceParametersBuilders.DeviceParameters {
+ method public int getDevicePlatform();
+ method @FloatRange(from=0.0, fromInclusive=false, toInclusive=false) public float getScreenDensity();
+ method @Dimension(unit=androidx.annotation.Dimension.DP) public int getScreenHeightDp();
+ method public int getScreenShape();
+ method @Dimension(unit=androidx.annotation.Dimension.DP) public int getScreenWidthDp();
+ }
+
+ public static final class DeviceParametersBuilders.DeviceParameters.Builder {
+ ctor public DeviceParametersBuilders.DeviceParameters.Builder();
+ method public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters build();
+ method public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters.Builder setDevicePlatform(int);
+ method public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters.Builder setScreenDensity(@FloatRange(from=0.0, fromInclusive=false, toInclusive=false) float);
+ method public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters.Builder setScreenHeightDp(@Dimension(unit=androidx.annotation.Dimension.DP) int);
+ method public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters.Builder setScreenShape(int);
+ method public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters.Builder setScreenWidthDp(@Dimension(unit=androidx.annotation.Dimension.DP) int);
+ }
+
+ public final class DimensionBuilders {
+ method public static androidx.wear.tiles.DimensionBuilders.DegreesProp degrees(float);
+ method public static androidx.wear.tiles.DimensionBuilders.DpProp dp(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ method public static androidx.wear.tiles.DimensionBuilders.EmProp em(int);
+ method public static androidx.wear.tiles.DimensionBuilders.EmProp em(float);
+ method public static androidx.wear.tiles.DimensionBuilders.ExpandedDimensionProp expand();
+ method public static androidx.wear.tiles.DimensionBuilders.SpProp sp(@Dimension(unit=androidx.annotation.Dimension.SP) float);
+ method public static androidx.wear.tiles.DimensionBuilders.WrappedDimensionProp wrap();
+ }
+
+ public static interface DimensionBuilders.ContainerDimension {
+ }
+
+ public static interface DimensionBuilders.ContainerDimension.Builder {
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension build();
+ }
+
+ public static final class DimensionBuilders.DegreesProp {
+ method public float getValue();
+ }
+
+ public static final class DimensionBuilders.DegreesProp.Builder {
+ ctor public DimensionBuilders.DegreesProp.Builder();
+ method public androidx.wear.tiles.DimensionBuilders.DegreesProp build();
+ method public androidx.wear.tiles.DimensionBuilders.DegreesProp.Builder setValue(float);
+ }
+
+ public static final class DimensionBuilders.DpProp implements androidx.wear.tiles.DimensionBuilders.ContainerDimension androidx.wear.tiles.DimensionBuilders.ImageDimension androidx.wear.tiles.DimensionBuilders.SpacerDimension {
+ method @Dimension(unit=androidx.annotation.Dimension.DP) public float getValue();
+ }
+
+ public static final class DimensionBuilders.DpProp.Builder implements androidx.wear.tiles.DimensionBuilders.ContainerDimension.Builder androidx.wear.tiles.DimensionBuilders.ImageDimension.Builder androidx.wear.tiles.DimensionBuilders.SpacerDimension.Builder {
+ ctor public DimensionBuilders.DpProp.Builder();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp build();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp.Builder setValue(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ }
+
+ public static final class DimensionBuilders.EmProp {
+ method public float getValue();
+ }
+
+ public static final class DimensionBuilders.EmProp.Builder {
+ ctor public DimensionBuilders.EmProp.Builder();
+ method public androidx.wear.tiles.DimensionBuilders.EmProp build();
+ method public androidx.wear.tiles.DimensionBuilders.EmProp.Builder setValue(float);
+ }
+
+ public static final class DimensionBuilders.ExpandedDimensionProp implements androidx.wear.tiles.DimensionBuilders.ContainerDimension androidx.wear.tiles.DimensionBuilders.ImageDimension {
+ }
+
+ public static final class DimensionBuilders.ExpandedDimensionProp.Builder implements androidx.wear.tiles.DimensionBuilders.ContainerDimension.Builder androidx.wear.tiles.DimensionBuilders.ImageDimension.Builder {
+ ctor public DimensionBuilders.ExpandedDimensionProp.Builder();
+ method public androidx.wear.tiles.DimensionBuilders.ExpandedDimensionProp build();
+ }
+
+ public static interface DimensionBuilders.ImageDimension {
+ }
+
+ public static interface DimensionBuilders.ImageDimension.Builder {
+ method public androidx.wear.tiles.DimensionBuilders.ImageDimension build();
+ }
+
+ public static final class DimensionBuilders.ProportionalDimensionProp implements androidx.wear.tiles.DimensionBuilders.ImageDimension {
+ method @IntRange(from=0) public int getAspectRatioHeight();
+ method @IntRange(from=0) public int getAspectRatioWidth();
+ }
+
+ public static final class DimensionBuilders.ProportionalDimensionProp.Builder implements androidx.wear.tiles.DimensionBuilders.ImageDimension.Builder {
+ ctor public DimensionBuilders.ProportionalDimensionProp.Builder();
+ method public androidx.wear.tiles.DimensionBuilders.ProportionalDimensionProp build();
+ method public androidx.wear.tiles.DimensionBuilders.ProportionalDimensionProp.Builder setAspectRatioHeight(@IntRange(from=0) int);
+ method public androidx.wear.tiles.DimensionBuilders.ProportionalDimensionProp.Builder setAspectRatioWidth(@IntRange(from=0) int);
+ }
+
+ public static final class DimensionBuilders.SpProp {
+ method @Dimension(unit=androidx.annotation.Dimension.SP) public float getValue();
+ }
+
+ public static final class DimensionBuilders.SpProp.Builder {
+ ctor public DimensionBuilders.SpProp.Builder();
+ method public androidx.wear.tiles.DimensionBuilders.SpProp build();
+ method public androidx.wear.tiles.DimensionBuilders.SpProp.Builder setValue(@Dimension(unit=androidx.annotation.Dimension.SP) float);
+ }
+
+ public static interface DimensionBuilders.SpacerDimension {
+ }
+
+ public static interface DimensionBuilders.SpacerDimension.Builder {
+ method public androidx.wear.tiles.DimensionBuilders.SpacerDimension build();
+ }
+
+ public static final class DimensionBuilders.WrappedDimensionProp implements androidx.wear.tiles.DimensionBuilders.ContainerDimension {
+ }
+
+ public static final class DimensionBuilders.WrappedDimensionProp.Builder implements androidx.wear.tiles.DimensionBuilders.ContainerDimension.Builder {
+ ctor public DimensionBuilders.WrappedDimensionProp.Builder();
+ method public androidx.wear.tiles.DimensionBuilders.WrappedDimensionProp build();
+ }
+
+ public final class EventBuilders {
+ }
+
+ public static final class EventBuilders.TileAddEvent {
+ }
+
+ public static final class EventBuilders.TileAddEvent.Builder {
+ ctor public EventBuilders.TileAddEvent.Builder();
+ method public androidx.wear.tiles.EventBuilders.TileAddEvent build();
+ }
+
+ public static final class EventBuilders.TileEnterEvent {
+ }
+
+ public static final class EventBuilders.TileEnterEvent.Builder {
+ ctor public EventBuilders.TileEnterEvent.Builder();
+ method public androidx.wear.tiles.EventBuilders.TileEnterEvent build();
+ }
+
+ public static final class EventBuilders.TileLeaveEvent {
+ }
+
+ public static final class EventBuilders.TileLeaveEvent.Builder {
+ ctor public EventBuilders.TileLeaveEvent.Builder();
+ method public androidx.wear.tiles.EventBuilders.TileLeaveEvent build();
+ }
+
+ public static final class EventBuilders.TileRemoveEvent {
+ }
+
+ public static final class EventBuilders.TileRemoveEvent.Builder {
+ ctor public EventBuilders.TileRemoveEvent.Builder();
+ method public androidx.wear.tiles.EventBuilders.TileRemoveEvent build();
+ }
+
+ public final class LayoutElementBuilders {
+ field public static final int ARC_ANCHOR_CENTER = 2; // 0x2
+ field public static final int ARC_ANCHOR_END = 3; // 0x3
+ field public static final int ARC_ANCHOR_START = 1; // 0x1
+ field public static final int ARC_ANCHOR_UNDEFINED = 0; // 0x0
+ field public static final int CONTENT_SCALE_MODE_CROP = 2; // 0x2
+ field public static final int CONTENT_SCALE_MODE_FILL_BOUNDS = 3; // 0x3
+ field public static final int CONTENT_SCALE_MODE_FIT = 1; // 0x1
+ field public static final int CONTENT_SCALE_MODE_UNDEFINED = 0; // 0x0
+ field public static final int FONT_VARIANT_BODY = 2; // 0x2
+ field public static final int FONT_VARIANT_TITLE = 1; // 0x1
+ field public static final int FONT_VARIANT_UNDEFINED = 0; // 0x0
+ field public static final int FONT_WEIGHT_BOLD = 700; // 0x2bc
+ field public static final int FONT_WEIGHT_NORMAL = 400; // 0x190
+ field public static final int FONT_WEIGHT_UNDEFINED = 0; // 0x0
+ field public static final int HORIZONTAL_ALIGN_CENTER = 2; // 0x2
+ field public static final int HORIZONTAL_ALIGN_END = 5; // 0x5
+ field public static final int HORIZONTAL_ALIGN_LEFT = 1; // 0x1
+ field public static final int HORIZONTAL_ALIGN_RIGHT = 3; // 0x3
+ field public static final int HORIZONTAL_ALIGN_START = 4; // 0x4
+ field public static final int HORIZONTAL_ALIGN_UNDEFINED = 0; // 0x0
+ field public static final int SPAN_VERTICAL_ALIGN_BOTTOM = 1; // 0x1
+ field public static final int SPAN_VERTICAL_ALIGN_TEXT_BASELINE = 2; // 0x2
+ field public static final int SPAN_VERTICAL_ALIGN_UNDEFINED = 0; // 0x0
+ field public static final int TEXT_ALIGN_CENTER = 2; // 0x2
+ field public static final int TEXT_ALIGN_END = 3; // 0x3
+ field public static final int TEXT_ALIGN_START = 1; // 0x1
+ field public static final int TEXT_ALIGN_UNDEFINED = 0; // 0x0
+ field public static final int TEXT_OVERFLOW_ELLIPSIZE_END = 2; // 0x2
+ field public static final int TEXT_OVERFLOW_TRUNCATE = 1; // 0x1
+ field public static final int TEXT_OVERFLOW_UNDEFINED = 0; // 0x0
+ field public static final int VERTICAL_ALIGN_BOTTOM = 3; // 0x3
+ field public static final int VERTICAL_ALIGN_CENTER = 2; // 0x2
+ field public static final int VERTICAL_ALIGN_TOP = 1; // 0x1
+ field public static final int VERTICAL_ALIGN_UNDEFINED = 0; // 0x0
+ }
+
+ public static final class LayoutElementBuilders.Arc implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public androidx.wear.tiles.DimensionBuilders.DegreesProp? getAnchorAngle();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcAnchorTypeProp? getAnchorType();
+ method public java.util.List<androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement!> getContents();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers? getModifiers();
+ method public androidx.wear.tiles.LayoutElementBuilders.VerticalAlignmentProp? getVerticalAlign();
+ }
+
+ public static final class LayoutElementBuilders.Arc.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public LayoutElementBuilders.Arc.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Arc.Builder addContent(androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.Arc build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Arc.Builder setAnchorAngle(androidx.wear.tiles.DimensionBuilders.DegreesProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Arc.Builder setAnchorType(androidx.wear.tiles.LayoutElementBuilders.ArcAnchorTypeProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Arc.Builder setAnchorType(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Arc.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.Arc.Builder setVerticalAlign(androidx.wear.tiles.LayoutElementBuilders.VerticalAlignmentProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Arc.Builder setVerticalAlign(int);
+ }
+
+ public static final class LayoutElementBuilders.ArcAdapter implements androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement {
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getContent();
+ method public androidx.wear.tiles.TypeBuilders.BoolProp? getRotateContents();
+ }
+
+ public static final class LayoutElementBuilders.ArcAdapter.Builder implements androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement.Builder {
+ ctor public LayoutElementBuilders.ArcAdapter.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcAdapter build();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcAdapter.Builder setContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcAdapter.Builder setRotateContents(androidx.wear.tiles.TypeBuilders.BoolProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcAdapter.Builder setRotateContents(boolean);
+ }
+
+ public static final class LayoutElementBuilders.ArcAnchorTypeProp {
+ method public int getValue();
+ }
+
+ public static final class LayoutElementBuilders.ArcAnchorTypeProp.Builder {
+ ctor public LayoutElementBuilders.ArcAnchorTypeProp.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcAnchorTypeProp build();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcAnchorTypeProp.Builder setValue(int);
+ }
+
+ public static interface LayoutElementBuilders.ArcLayoutElement {
+ }
+
+ public static interface LayoutElementBuilders.ArcLayoutElement.Builder {
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement build();
+ }
+
+ public static final class LayoutElementBuilders.ArcLine implements androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement {
+ method public androidx.wear.tiles.ColorBuilders.ColorProp? getColor();
+ method public androidx.wear.tiles.DimensionBuilders.DegreesProp? getLength();
+ method public androidx.wear.tiles.ModifiersBuilders.ArcModifiers? getModifiers();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getThickness();
+ }
+
+ public static final class LayoutElementBuilders.ArcLine.Builder implements androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement.Builder {
+ ctor public LayoutElementBuilders.ArcLine.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcLine build();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcLine.Builder setColor(androidx.wear.tiles.ColorBuilders.ColorProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcLine.Builder setLength(androidx.wear.tiles.DimensionBuilders.DegreesProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcLine.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.ArcModifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcLine.Builder setThickness(androidx.wear.tiles.DimensionBuilders.DpProp);
+ }
+
+ public static final class LayoutElementBuilders.ArcSpacer implements androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement {
+ method public androidx.wear.tiles.DimensionBuilders.DegreesProp? getLength();
+ method public androidx.wear.tiles.ModifiersBuilders.ArcModifiers? getModifiers();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getThickness();
+ }
+
+ public static final class LayoutElementBuilders.ArcSpacer.Builder implements androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement.Builder {
+ ctor public LayoutElementBuilders.ArcSpacer.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcSpacer build();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcSpacer.Builder setLength(androidx.wear.tiles.DimensionBuilders.DegreesProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcSpacer.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.ArcModifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcSpacer.Builder setThickness(androidx.wear.tiles.DimensionBuilders.DpProp);
+ }
+
+ public static final class LayoutElementBuilders.ArcText implements androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement {
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle? getFontStyle();
+ method public androidx.wear.tiles.ModifiersBuilders.ArcModifiers? getModifiers();
+ method public androidx.wear.tiles.TypeBuilders.StringProp? getText();
+ }
+
+ public static final class LayoutElementBuilders.ArcText.Builder implements androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement.Builder {
+ ctor public LayoutElementBuilders.ArcText.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcText build();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcText.Builder setFontStyle(androidx.wear.tiles.LayoutElementBuilders.FontStyle);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcText.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.ArcModifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcText.Builder setText(androidx.wear.tiles.TypeBuilders.StringProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcText.Builder setText(String);
+ }
+
+ public static final class LayoutElementBuilders.Box implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public java.util.List<androidx.wear.tiles.LayoutElementBuilders.LayoutElement!> getContents();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension? getHeight();
+ method public androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignmentProp? getHorizontalAlignment();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers? getModifiers();
+ method public androidx.wear.tiles.LayoutElementBuilders.VerticalAlignmentProp? getVerticalAlignment();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension? getWidth();
+ }
+
+ public static final class LayoutElementBuilders.Box.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public LayoutElementBuilders.Box.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Box.Builder addContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.Box build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Box.Builder setHeight(androidx.wear.tiles.DimensionBuilders.ContainerDimension);
+ method public androidx.wear.tiles.LayoutElementBuilders.Box.Builder setHorizontalAlignment(androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignmentProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Box.Builder setHorizontalAlignment(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Box.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.Box.Builder setVerticalAlignment(androidx.wear.tiles.LayoutElementBuilders.VerticalAlignmentProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Box.Builder setVerticalAlignment(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Box.Builder setWidth(androidx.wear.tiles.DimensionBuilders.ContainerDimension);
+ }
+
+ public static final class LayoutElementBuilders.ColorFilter {
+ method public androidx.wear.tiles.ColorBuilders.ColorProp? getTint();
+ }
+
+ public static final class LayoutElementBuilders.ColorFilter.Builder {
+ ctor public LayoutElementBuilders.ColorFilter.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.ColorFilter build();
+ method public androidx.wear.tiles.LayoutElementBuilders.ColorFilter.Builder setTint(androidx.wear.tiles.ColorBuilders.ColorProp);
+ }
+
+ public static final class LayoutElementBuilders.Column implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public java.util.List<androidx.wear.tiles.LayoutElementBuilders.LayoutElement!> getContents();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension? getHeight();
+ method public androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignmentProp? getHorizontalAlignment();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers? getModifiers();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension? getWidth();
+ }
+
+ public static final class LayoutElementBuilders.Column.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public LayoutElementBuilders.Column.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Column.Builder addContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.Column build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Column.Builder setHeight(androidx.wear.tiles.DimensionBuilders.ContainerDimension);
+ method public androidx.wear.tiles.LayoutElementBuilders.Column.Builder setHorizontalAlignment(androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignmentProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Column.Builder setHorizontalAlignment(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Column.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.Column.Builder setWidth(androidx.wear.tiles.DimensionBuilders.ContainerDimension);
+ }
+
+ public static final class LayoutElementBuilders.ContentScaleModeProp {
+ method public int getValue();
+ }
+
+ public static final class LayoutElementBuilders.ContentScaleModeProp.Builder {
+ ctor public LayoutElementBuilders.ContentScaleModeProp.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.ContentScaleModeProp build();
+ method public androidx.wear.tiles.LayoutElementBuilders.ContentScaleModeProp.Builder setValue(int);
+ }
+
+ public static final class LayoutElementBuilders.FontStyle {
+ method public androidx.wear.tiles.ColorBuilders.ColorProp? getColor();
+ method public androidx.wear.tiles.TypeBuilders.BoolProp? getItalic();
+ method public androidx.wear.tiles.DimensionBuilders.EmProp? getLetterSpacing();
+ method public androidx.wear.tiles.DimensionBuilders.SpProp? getSize();
+ method public androidx.wear.tiles.TypeBuilders.BoolProp? getUnderline();
+ method public androidx.wear.tiles.LayoutElementBuilders.FontWeightProp? getWeight();
+ }
+
+ public static final class LayoutElementBuilders.FontStyle.Builder {
+ ctor public LayoutElementBuilders.FontStyle.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle build();
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setColor(androidx.wear.tiles.ColorBuilders.ColorProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setItalic(androidx.wear.tiles.TypeBuilders.BoolProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setItalic(boolean);
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setLetterSpacing(androidx.wear.tiles.DimensionBuilders.EmProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setSize(androidx.wear.tiles.DimensionBuilders.SpProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setUnderline(androidx.wear.tiles.TypeBuilders.BoolProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setUnderline(boolean);
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setWeight(androidx.wear.tiles.LayoutElementBuilders.FontWeightProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setWeight(int);
+ }
+
+ public static class LayoutElementBuilders.FontStyles {
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder body1(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder body2(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder button(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder caption1(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder caption2(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder display1(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder display2(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder display3(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder title1(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder title2(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder title3(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ }
+
+ public static final class LayoutElementBuilders.FontWeightProp {
+ method public int getValue();
+ }
+
+ public static final class LayoutElementBuilders.FontWeightProp.Builder {
+ ctor public LayoutElementBuilders.FontWeightProp.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.FontWeightProp build();
+ method public androidx.wear.tiles.LayoutElementBuilders.FontWeightProp.Builder setValue(int);
+ }
+
+ public static final class LayoutElementBuilders.HorizontalAlignmentProp {
+ method public int getValue();
+ }
+
+ public static final class LayoutElementBuilders.HorizontalAlignmentProp.Builder {
+ ctor public LayoutElementBuilders.HorizontalAlignmentProp.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignmentProp build();
+ method public androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignmentProp.Builder setValue(int);
+ }
+
+ public static final class LayoutElementBuilders.Image implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public androidx.wear.tiles.LayoutElementBuilders.ColorFilter? getColorFilter();
+ method public androidx.wear.tiles.LayoutElementBuilders.ContentScaleModeProp? getContentScaleMode();
+ method public androidx.wear.tiles.DimensionBuilders.ImageDimension? getHeight();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers? getModifiers();
+ method public androidx.wear.tiles.TypeBuilders.StringProp? getResourceId();
+ method public androidx.wear.tiles.DimensionBuilders.ImageDimension? getWidth();
+ }
+
+ public static final class LayoutElementBuilders.Image.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public LayoutElementBuilders.Image.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Image build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Image.Builder setColorFilter(androidx.wear.tiles.LayoutElementBuilders.ColorFilter);
+ method public androidx.wear.tiles.LayoutElementBuilders.Image.Builder setContentScaleMode(androidx.wear.tiles.LayoutElementBuilders.ContentScaleModeProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Image.Builder setContentScaleMode(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Image.Builder setHeight(androidx.wear.tiles.DimensionBuilders.ImageDimension);
+ method public androidx.wear.tiles.LayoutElementBuilders.Image.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.Image.Builder setResourceId(androidx.wear.tiles.TypeBuilders.StringProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Image.Builder setResourceId(String);
+ method public androidx.wear.tiles.LayoutElementBuilders.Image.Builder setWidth(androidx.wear.tiles.DimensionBuilders.ImageDimension);
+ }
+
+ public static final class LayoutElementBuilders.Layout {
+ method public static androidx.wear.tiles.LayoutElementBuilders.Layout fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getRoot();
+ }
+
+ public static final class LayoutElementBuilders.Layout.Builder {
+ ctor public LayoutElementBuilders.Layout.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Layout build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Layout.Builder setRoot(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ }
+
+ public static interface LayoutElementBuilders.LayoutElement {
+ }
+
+ public static interface LayoutElementBuilders.LayoutElement.Builder {
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement build();
+ }
+
+ public static final class LayoutElementBuilders.Row implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public java.util.List<androidx.wear.tiles.LayoutElementBuilders.LayoutElement!> getContents();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension? getHeight();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers? getModifiers();
+ method public androidx.wear.tiles.LayoutElementBuilders.VerticalAlignmentProp? getVerticalAlignment();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension? getWidth();
+ }
+
+ public static final class LayoutElementBuilders.Row.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public LayoutElementBuilders.Row.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Row.Builder addContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.Row build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Row.Builder setHeight(androidx.wear.tiles.DimensionBuilders.ContainerDimension);
+ method public androidx.wear.tiles.LayoutElementBuilders.Row.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.Row.Builder setVerticalAlignment(androidx.wear.tiles.LayoutElementBuilders.VerticalAlignmentProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Row.Builder setVerticalAlignment(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Row.Builder setWidth(androidx.wear.tiles.DimensionBuilders.ContainerDimension);
+ }
+
+ public static final class LayoutElementBuilders.Spacer implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public androidx.wear.tiles.DimensionBuilders.SpacerDimension? getHeight();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers? getModifiers();
+ method public androidx.wear.tiles.DimensionBuilders.SpacerDimension? getWidth();
+ }
+
+ public static final class LayoutElementBuilders.Spacer.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public LayoutElementBuilders.Spacer.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Spacer build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Spacer.Builder setHeight(androidx.wear.tiles.DimensionBuilders.SpacerDimension);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spacer.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spacer.Builder setWidth(androidx.wear.tiles.DimensionBuilders.SpacerDimension);
+ }
+
+ public static interface LayoutElementBuilders.Span {
+ }
+
+ public static interface LayoutElementBuilders.Span.Builder {
+ method public androidx.wear.tiles.LayoutElementBuilders.Span build();
+ }
+
+ public static final class LayoutElementBuilders.SpanImage implements androidx.wear.tiles.LayoutElementBuilders.Span {
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanVerticalAlignmentProp? getAlignment();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getHeight();
+ method public androidx.wear.tiles.ModifiersBuilders.SpanModifiers? getModifiers();
+ method public androidx.wear.tiles.TypeBuilders.StringProp? getResourceId();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getWidth();
+ }
+
+ public static final class LayoutElementBuilders.SpanImage.Builder implements androidx.wear.tiles.LayoutElementBuilders.Span.Builder {
+ ctor public LayoutElementBuilders.SpanImage.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanImage build();
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanImage.Builder setAlignment(androidx.wear.tiles.LayoutElementBuilders.SpanVerticalAlignmentProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanImage.Builder setAlignment(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanImage.Builder setHeight(androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanImage.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.SpanModifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanImage.Builder setResourceId(androidx.wear.tiles.TypeBuilders.StringProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanImage.Builder setResourceId(String);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanImage.Builder setWidth(androidx.wear.tiles.DimensionBuilders.DpProp);
+ }
+
+ public static final class LayoutElementBuilders.SpanText implements androidx.wear.tiles.LayoutElementBuilders.Span {
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle? getFontStyle();
+ method public androidx.wear.tiles.ModifiersBuilders.SpanModifiers? getModifiers();
+ method public androidx.wear.tiles.TypeBuilders.StringProp? getText();
+ }
+
+ public static final class LayoutElementBuilders.SpanText.Builder implements androidx.wear.tiles.LayoutElementBuilders.Span.Builder {
+ ctor public LayoutElementBuilders.SpanText.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanText build();
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanText.Builder setFontStyle(androidx.wear.tiles.LayoutElementBuilders.FontStyle);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanText.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.SpanModifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanText.Builder setText(androidx.wear.tiles.TypeBuilders.StringProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanText.Builder setText(String);
+ }
+
+ public static final class LayoutElementBuilders.SpanVerticalAlignmentProp {
+ method public int getValue();
+ }
+
+ public static final class LayoutElementBuilders.SpanVerticalAlignmentProp.Builder {
+ ctor public LayoutElementBuilders.SpanVerticalAlignmentProp.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanVerticalAlignmentProp build();
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanVerticalAlignmentProp.Builder setValue(int);
+ }
+
+ public static final class LayoutElementBuilders.Spannable implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public androidx.wear.tiles.DimensionBuilders.SpProp? getLineHeight();
+ method public androidx.wear.tiles.TypeBuilders.Int32Prop? getMaxLines();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers? getModifiers();
+ method public androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignmentProp? getMultilineAlignment();
+ method public androidx.wear.tiles.LayoutElementBuilders.TextOverflowProp? getOverflow();
+ method public java.util.List<androidx.wear.tiles.LayoutElementBuilders.Span!> getSpans();
+ }
+
+ public static final class LayoutElementBuilders.Spannable.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public LayoutElementBuilders.Spannable.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder addSpan(androidx.wear.tiles.LayoutElementBuilders.Span);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder setLineHeight(androidx.wear.tiles.DimensionBuilders.SpProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder setMaxLines(androidx.wear.tiles.TypeBuilders.Int32Prop);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder setMaxLines(@IntRange(from=1) int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder setMultilineAlignment(androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignmentProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder setMultilineAlignment(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder setOverflow(androidx.wear.tiles.LayoutElementBuilders.TextOverflowProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder setOverflow(int);
+ }
+
+ public static final class LayoutElementBuilders.Text implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle? getFontStyle();
+ method public androidx.wear.tiles.DimensionBuilders.SpProp? getLineHeight();
+ method public androidx.wear.tiles.TypeBuilders.Int32Prop? getMaxLines();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers? getModifiers();
+ method public androidx.wear.tiles.LayoutElementBuilders.TextAlignmentProp? getMultilineAlignment();
+ method public androidx.wear.tiles.LayoutElementBuilders.TextOverflowProp? getOverflow();
+ method public androidx.wear.tiles.TypeBuilders.StringProp? getText();
+ }
+
+ public static final class LayoutElementBuilders.Text.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public LayoutElementBuilders.Text.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Text build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setFontStyle(androidx.wear.tiles.LayoutElementBuilders.FontStyle);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setLineHeight(androidx.wear.tiles.DimensionBuilders.SpProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setMaxLines(androidx.wear.tiles.TypeBuilders.Int32Prop);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setMaxLines(@IntRange(from=1) int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setMultilineAlignment(androidx.wear.tiles.LayoutElementBuilders.TextAlignmentProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setMultilineAlignment(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setOverflow(androidx.wear.tiles.LayoutElementBuilders.TextOverflowProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setOverflow(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setText(androidx.wear.tiles.TypeBuilders.StringProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setText(String);
+ }
+
+ public static final class LayoutElementBuilders.TextAlignmentProp {
+ method public int getValue();
+ }
+
+ public static final class LayoutElementBuilders.TextAlignmentProp.Builder {
+ ctor public LayoutElementBuilders.TextAlignmentProp.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.TextAlignmentProp build();
+ method public androidx.wear.tiles.LayoutElementBuilders.TextAlignmentProp.Builder setValue(int);
+ }
+
+ public static final class LayoutElementBuilders.TextOverflowProp {
+ method public int getValue();
+ }
+
+ public static final class LayoutElementBuilders.TextOverflowProp.Builder {
+ ctor public LayoutElementBuilders.TextOverflowProp.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.TextOverflowProp build();
+ method public androidx.wear.tiles.LayoutElementBuilders.TextOverflowProp.Builder setValue(int);
+ }
+
+ public static final class LayoutElementBuilders.VerticalAlignmentProp {
+ method public int getValue();
+ }
+
+ public static final class LayoutElementBuilders.VerticalAlignmentProp.Builder {
+ ctor public LayoutElementBuilders.VerticalAlignmentProp.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.VerticalAlignmentProp build();
+ method public androidx.wear.tiles.LayoutElementBuilders.VerticalAlignmentProp.Builder setValue(int);
+ }
+
+ public final class ModifiersBuilders {
+ }
+
+ public static final class ModifiersBuilders.ArcModifiers {
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable? getClickable();
+ method public androidx.wear.tiles.ModifiersBuilders.Semantics? getSemantics();
+ }
+
+ public static final class ModifiersBuilders.ArcModifiers.Builder {
+ ctor public ModifiersBuilders.ArcModifiers.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.ArcModifiers build();
+ method public androidx.wear.tiles.ModifiersBuilders.ArcModifiers.Builder setClickable(androidx.wear.tiles.ModifiersBuilders.Clickable);
+ method public androidx.wear.tiles.ModifiersBuilders.ArcModifiers.Builder setSemantics(androidx.wear.tiles.ModifiersBuilders.Semantics);
+ }
+
+ public static final class ModifiersBuilders.Background {
+ method public androidx.wear.tiles.ColorBuilders.ColorProp? getColor();
+ method public androidx.wear.tiles.ModifiersBuilders.Corner? getCorner();
+ }
+
+ public static final class ModifiersBuilders.Background.Builder {
+ ctor public ModifiersBuilders.Background.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.Background build();
+ method public androidx.wear.tiles.ModifiersBuilders.Background.Builder setColor(androidx.wear.tiles.ColorBuilders.ColorProp);
+ method public androidx.wear.tiles.ModifiersBuilders.Background.Builder setCorner(androidx.wear.tiles.ModifiersBuilders.Corner);
+ }
+
+ public static final class ModifiersBuilders.Border {
+ method public androidx.wear.tiles.ColorBuilders.ColorProp? getColor();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getWidth();
+ }
+
+ public static final class ModifiersBuilders.Border.Builder {
+ ctor public ModifiersBuilders.Border.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.Border build();
+ method public androidx.wear.tiles.ModifiersBuilders.Border.Builder setColor(androidx.wear.tiles.ColorBuilders.ColorProp);
+ method public androidx.wear.tiles.ModifiersBuilders.Border.Builder setWidth(androidx.wear.tiles.DimensionBuilders.DpProp);
+ }
+
+ public static final class ModifiersBuilders.Clickable {
+ method public String getId();
+ method public androidx.wear.tiles.ActionBuilders.Action? getOnClick();
+ }
+
+ public static final class ModifiersBuilders.Clickable.Builder {
+ ctor public ModifiersBuilders.Clickable.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable build();
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable.Builder setId(String);
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable.Builder setOnClick(androidx.wear.tiles.ActionBuilders.Action);
+ }
+
+ public static final class ModifiersBuilders.Corner {
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getRadius();
+ }
+
+ public static final class ModifiersBuilders.Corner.Builder {
+ ctor public ModifiersBuilders.Corner.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.Corner build();
+ method public androidx.wear.tiles.ModifiersBuilders.Corner.Builder setRadius(androidx.wear.tiles.DimensionBuilders.DpProp);
+ }
+
+ public static final class ModifiersBuilders.ElementMetadata {
+ method public byte[] getTagData();
+ }
+
+ public static final class ModifiersBuilders.ElementMetadata.Builder {
+ ctor public ModifiersBuilders.ElementMetadata.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.ElementMetadata build();
+ method public androidx.wear.tiles.ModifiersBuilders.ElementMetadata.Builder setTagData(byte[]);
+ }
+
+ public static final class ModifiersBuilders.Modifiers {
+ method public androidx.wear.tiles.ModifiersBuilders.Background? getBackground();
+ method public androidx.wear.tiles.ModifiersBuilders.Border? getBorder();
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable? getClickable();
+ method public androidx.wear.tiles.ModifiersBuilders.ElementMetadata? getMetadata();
+ method public androidx.wear.tiles.ModifiersBuilders.Padding? getPadding();
+ method public androidx.wear.tiles.ModifiersBuilders.Semantics? getSemantics();
+ }
+
+ public static final class ModifiersBuilders.Modifiers.Builder {
+ ctor public ModifiersBuilders.Modifiers.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers build();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers.Builder setBackground(androidx.wear.tiles.ModifiersBuilders.Background);
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers.Builder setBorder(androidx.wear.tiles.ModifiersBuilders.Border);
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers.Builder setClickable(androidx.wear.tiles.ModifiersBuilders.Clickable);
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers.Builder setMetadata(androidx.wear.tiles.ModifiersBuilders.ElementMetadata);
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers.Builder setPadding(androidx.wear.tiles.ModifiersBuilders.Padding);
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers.Builder setSemantics(androidx.wear.tiles.ModifiersBuilders.Semantics);
+ }
+
+ public static final class ModifiersBuilders.Padding {
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getBottom();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getEnd();
+ method public androidx.wear.tiles.TypeBuilders.BoolProp? getRtlAware();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getStart();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getTop();
+ }
+
+ public static final class ModifiersBuilders.Padding.Builder {
+ ctor public ModifiersBuilders.Padding.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.Padding build();
+ method public androidx.wear.tiles.ModifiersBuilders.Padding.Builder setAll(androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public androidx.wear.tiles.ModifiersBuilders.Padding.Builder setBottom(androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public androidx.wear.tiles.ModifiersBuilders.Padding.Builder setEnd(androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public androidx.wear.tiles.ModifiersBuilders.Padding.Builder setRtlAware(androidx.wear.tiles.TypeBuilders.BoolProp);
+ method public androidx.wear.tiles.ModifiersBuilders.Padding.Builder setRtlAware(boolean);
+ method public androidx.wear.tiles.ModifiersBuilders.Padding.Builder setStart(androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public androidx.wear.tiles.ModifiersBuilders.Padding.Builder setTop(androidx.wear.tiles.DimensionBuilders.DpProp);
+ }
+
+ public static final class ModifiersBuilders.Semantics {
+ method public String getContentDescription();
+ }
+
+ public static final class ModifiersBuilders.Semantics.Builder {
+ ctor public ModifiersBuilders.Semantics.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.Semantics build();
+ method public androidx.wear.tiles.ModifiersBuilders.Semantics.Builder setContentDescription(String);
+ }
+
+ public static final class ModifiersBuilders.SpanModifiers {
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable? getClickable();
+ }
+
+ public static final class ModifiersBuilders.SpanModifiers.Builder {
+ ctor public ModifiersBuilders.SpanModifiers.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.SpanModifiers build();
+ method public androidx.wear.tiles.ModifiersBuilders.SpanModifiers.Builder setClickable(androidx.wear.tiles.ModifiersBuilders.Clickable);
+ }
+
+ public final class RequestBuilders {
+ }
+
+ public static final class RequestBuilders.ResourcesRequest {
+ method public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters? getDeviceParameters();
+ method public java.util.List<java.lang.String!> getResourceIds();
+ method public String getVersion();
+ }
+
+ public static final class RequestBuilders.ResourcesRequest.Builder {
+ ctor public RequestBuilders.ResourcesRequest.Builder();
+ method public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder addResourceId(String);
+ method public androidx.wear.tiles.RequestBuilders.ResourcesRequest build();
+ method public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder setDeviceParameters(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder setVersion(String);
+ }
+
+ public static final class RequestBuilders.TileRequest {
+ method public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters? getDeviceParameters();
+ method public androidx.wear.tiles.StateBuilders.State? getState();
+ }
+
+ public static final class RequestBuilders.TileRequest.Builder {
+ ctor public RequestBuilders.TileRequest.Builder();
+ method public androidx.wear.tiles.RequestBuilders.TileRequest build();
+ method public androidx.wear.tiles.RequestBuilders.TileRequest.Builder setDeviceParameters(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public androidx.wear.tiles.RequestBuilders.TileRequest.Builder setState(androidx.wear.tiles.StateBuilders.State);
+ }
+
+ public final class ResourceBuilders {
+ field public static final int IMAGE_FORMAT_RGB_565 = 1; // 0x1
+ field public static final int IMAGE_FORMAT_UNDEFINED = 0; // 0x0
+ }
+
+ public static final class ResourceBuilders.AndroidImageResourceByResId {
+ method @DrawableRes public int getResourceId();
+ }
+
+ public static final class ResourceBuilders.AndroidImageResourceByResId.Builder {
+ ctor public ResourceBuilders.AndroidImageResourceByResId.Builder();
+ method public androidx.wear.tiles.ResourceBuilders.AndroidImageResourceByResId build();
+ method public androidx.wear.tiles.ResourceBuilders.AndroidImageResourceByResId.Builder setResourceId(@DrawableRes int);
+ }
+
+ public static final class ResourceBuilders.ImageResource {
+ method public androidx.wear.tiles.ResourceBuilders.AndroidImageResourceByResId? getAndroidResourceByResId();
+ method public androidx.wear.tiles.ResourceBuilders.InlineImageResource? getInlineResource();
+ }
+
+ public static final class ResourceBuilders.ImageResource.Builder {
+ ctor public ResourceBuilders.ImageResource.Builder();
+ method public androidx.wear.tiles.ResourceBuilders.ImageResource build();
+ method public androidx.wear.tiles.ResourceBuilders.ImageResource.Builder setAndroidResourceByResId(androidx.wear.tiles.ResourceBuilders.AndroidImageResourceByResId);
+ method public androidx.wear.tiles.ResourceBuilders.ImageResource.Builder setInlineResource(androidx.wear.tiles.ResourceBuilders.InlineImageResource);
+ }
+
+ public static final class ResourceBuilders.InlineImageResource {
+ method public byte[] getData();
+ method public int getFormat();
+ method @Dimension(unit=androidx.annotation.Dimension.PX) public int getHeightPx();
+ method @Dimension(unit=androidx.annotation.Dimension.PX) public int getWidthPx();
+ }
+
+ public static final class ResourceBuilders.InlineImageResource.Builder {
+ ctor public ResourceBuilders.InlineImageResource.Builder();
+ method public androidx.wear.tiles.ResourceBuilders.InlineImageResource build();
+ method public androidx.wear.tiles.ResourceBuilders.InlineImageResource.Builder setData(byte[]);
+ method public androidx.wear.tiles.ResourceBuilders.InlineImageResource.Builder setFormat(int);
+ method public androidx.wear.tiles.ResourceBuilders.InlineImageResource.Builder setHeightPx(@Dimension(unit=androidx.annotation.Dimension.PX) int);
+ method public androidx.wear.tiles.ResourceBuilders.InlineImageResource.Builder setWidthPx(@Dimension(unit=androidx.annotation.Dimension.PX) int);
+ }
+
+ public static final class ResourceBuilders.Resources {
+ method public java.util.Map<java.lang.String!,androidx.wear.tiles.ResourceBuilders.ImageResource!> getIdToImageMapping();
+ method public String getVersion();
+ }
+
+ public static final class ResourceBuilders.Resources.Builder {
+ ctor public ResourceBuilders.Resources.Builder();
+ method public androidx.wear.tiles.ResourceBuilders.Resources.Builder addIdToImageMapping(String, androidx.wear.tiles.ResourceBuilders.ImageResource);
+ method public androidx.wear.tiles.ResourceBuilders.Resources build();
+ method public androidx.wear.tiles.ResourceBuilders.Resources.Builder setVersion(String);
+ }
+
+ public final class StateBuilders {
+ }
+
+ public static final class StateBuilders.State {
+ method public String getLastClickableId();
+ }
+
+ public static final class StateBuilders.State.Builder {
+ ctor public StateBuilders.State.Builder();
+ method public androidx.wear.tiles.StateBuilders.State build();
+ }
+
+ public final class TileBuilders {
+ }
+
+ public static final class TileBuilders.Tile {
+ method public long getFreshnessIntervalMillis();
+ method public String getResourcesVersion();
+ method public androidx.wear.tiles.TimelineBuilders.Timeline? getTimeline();
+ }
+
+ public static final class TileBuilders.Tile.Builder {
+ ctor public TileBuilders.Tile.Builder();
+ method public androidx.wear.tiles.TileBuilders.Tile build();
+ method public androidx.wear.tiles.TileBuilders.Tile.Builder setFreshnessIntervalMillis(long);
+ method public androidx.wear.tiles.TileBuilders.Tile.Builder setResourcesVersion(String);
+ method public androidx.wear.tiles.TileBuilders.Tile.Builder setTimeline(androidx.wear.tiles.TimelineBuilders.Timeline);
+ }
+
+ public abstract class TileService extends android.app.Service {
+ ctor public TileService();
+ method public static androidx.wear.tiles.TileUpdateRequester getUpdater(android.content.Context);
+ method public android.os.IBinder? onBind(android.content.Intent);
+ method @MainThread protected abstract com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.ResourceBuilders.Resources!> onResourcesRequest(androidx.wear.tiles.RequestBuilders.ResourcesRequest);
+ method @MainThread protected void onTileAddEvent(androidx.wear.tiles.EventBuilders.TileAddEvent);
+ method @MainThread protected void onTileEnterEvent(androidx.wear.tiles.EventBuilders.TileEnterEvent);
+ method @MainThread protected void onTileLeaveEvent(androidx.wear.tiles.EventBuilders.TileLeaveEvent);
+ method @MainThread protected void onTileRemoveEvent(androidx.wear.tiles.EventBuilders.TileRemoveEvent);
+ method @MainThread protected abstract com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.TileBuilders.Tile!> onTileRequest(androidx.wear.tiles.RequestBuilders.TileRequest);
+ field public static final String ACTION_BIND_TILE_PROVIDER = "androidx.wear.tiles.action.BIND_TILE_PROVIDER";
+ field public static final String EXTRA_CLICKABLE_ID = "androidx.wear.tiles.extra.CLICKABLE_ID";
+ field public static final String METADATA_PREVIEW_KEY = "androidx.wear.tiles.PREVIEW";
+ }
+
+ public interface TileUpdateRequester {
+ method public void requestUpdate(Class<? extends androidx.wear.tiles.TileService>);
+ }
+
+ public final class TimelineBuilders {
+ }
+
+ public static final class TimelineBuilders.TimeInterval {
+ method public long getEndMillis();
+ method public long getStartMillis();
+ }
+
+ public static final class TimelineBuilders.TimeInterval.Builder {
+ ctor public TimelineBuilders.TimeInterval.Builder();
+ method public androidx.wear.tiles.TimelineBuilders.TimeInterval build();
+ method public androidx.wear.tiles.TimelineBuilders.TimeInterval.Builder setEndMillis(long);
+ method public androidx.wear.tiles.TimelineBuilders.TimeInterval.Builder setStartMillis(long);
+ }
+
+ public static final class TimelineBuilders.Timeline {
+ method public static androidx.wear.tiles.TimelineBuilders.Timeline fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public java.util.List<androidx.wear.tiles.TimelineBuilders.TimelineEntry!> getTimelineEntries();
+ }
+
+ public static final class TimelineBuilders.Timeline.Builder {
+ ctor public TimelineBuilders.Timeline.Builder();
+ method public androidx.wear.tiles.TimelineBuilders.Timeline.Builder addTimelineEntry(androidx.wear.tiles.TimelineBuilders.TimelineEntry);
+ method public androidx.wear.tiles.TimelineBuilders.Timeline build();
+ }
+
+ public static final class TimelineBuilders.TimelineEntry {
+ method public static androidx.wear.tiles.TimelineBuilders.TimelineEntry fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.Layout? getLayout();
+ method public androidx.wear.tiles.TimelineBuilders.TimeInterval? getValidity();
+ }
+
+ public static final class TimelineBuilders.TimelineEntry.Builder {
+ ctor public TimelineBuilders.TimelineEntry.Builder();
+ method public androidx.wear.tiles.TimelineBuilders.TimelineEntry build();
+ method public androidx.wear.tiles.TimelineBuilders.TimelineEntry.Builder setLayout(androidx.wear.tiles.LayoutElementBuilders.Layout);
+ method public androidx.wear.tiles.TimelineBuilders.TimelineEntry.Builder setValidity(androidx.wear.tiles.TimelineBuilders.TimeInterval);
+ }
+
+ public final class TypeBuilders {
+ }
+
+ public static final class TypeBuilders.BoolProp {
+ method public boolean getValue();
+ }
+
+ public static final class TypeBuilders.BoolProp.Builder {
+ ctor public TypeBuilders.BoolProp.Builder();
+ method public androidx.wear.tiles.TypeBuilders.BoolProp build();
+ method public androidx.wear.tiles.TypeBuilders.BoolProp.Builder setValue(boolean);
+ }
+
+ public static final class TypeBuilders.FloatProp {
+ method public float getValue();
+ }
+
+ public static final class TypeBuilders.FloatProp.Builder {
+ ctor public TypeBuilders.FloatProp.Builder();
+ method public androidx.wear.tiles.TypeBuilders.FloatProp build();
+ method public androidx.wear.tiles.TypeBuilders.FloatProp.Builder setValue(float);
+ }
+
+ public static final class TypeBuilders.Int32Prop {
+ method public int getValue();
+ }
+
+ public static final class TypeBuilders.Int32Prop.Builder {
+ ctor public TypeBuilders.Int32Prop.Builder();
+ method public androidx.wear.tiles.TypeBuilders.Int32Prop build();
+ method public androidx.wear.tiles.TypeBuilders.Int32Prop.Builder setValue(int);
+ }
+
+ public static final class TypeBuilders.StringProp {
+ method public String getValue();
+ }
+
+ public static final class TypeBuilders.StringProp.Builder {
+ ctor public TypeBuilders.StringProp.Builder();
+ method public androidx.wear.tiles.TypeBuilders.StringProp build();
+ method public androidx.wear.tiles.TypeBuilders.StringProp.Builder setValue(String);
+ }
+
+}
+
diff --git a/wear/tiles/tiles/api/public_plus_experimental_1.1.0-beta01.txt b/wear/tiles/tiles/api/public_plus_experimental_1.1.0-beta01.txt
new file mode 100644
index 0000000..13ed3e4
--- /dev/null
+++ b/wear/tiles/tiles/api/public_plus_experimental_1.1.0-beta01.txt
@@ -0,0 +1,1105 @@
+// Signature format: 4.0
+package androidx.wear.tiles {
+
+ public final class ActionBuilders {
+ method public static androidx.wear.tiles.ActionBuilders.AndroidBooleanExtra booleanExtra(boolean);
+ method public static androidx.wear.tiles.ActionBuilders.AndroidDoubleExtra doubleExtra(double);
+ method public static androidx.wear.tiles.ActionBuilders.AndroidIntExtra intExtra(int);
+ method public static androidx.wear.tiles.ActionBuilders.AndroidLongExtra longExtra(long);
+ method public static androidx.wear.tiles.ActionBuilders.AndroidStringExtra stringExtra(String);
+ }
+
+ public static interface ActionBuilders.Action {
+ }
+
+ public static interface ActionBuilders.Action.Builder {
+ method public androidx.wear.tiles.ActionBuilders.Action build();
+ }
+
+ public static final class ActionBuilders.AndroidActivity {
+ method public String getClassName();
+ method public java.util.Map<java.lang.String!,androidx.wear.tiles.ActionBuilders.AndroidExtra!> getKeyToExtraMapping();
+ method public String getPackageName();
+ }
+
+ public static final class ActionBuilders.AndroidActivity.Builder {
+ ctor public ActionBuilders.AndroidActivity.Builder();
+ method public androidx.wear.tiles.ActionBuilders.AndroidActivity.Builder addKeyToExtraMapping(String, androidx.wear.tiles.ActionBuilders.AndroidExtra);
+ method public androidx.wear.tiles.ActionBuilders.AndroidActivity build();
+ method public androidx.wear.tiles.ActionBuilders.AndroidActivity.Builder setClassName(String);
+ method public androidx.wear.tiles.ActionBuilders.AndroidActivity.Builder setPackageName(String);
+ }
+
+ public static final class ActionBuilders.AndroidBooleanExtra implements androidx.wear.tiles.ActionBuilders.AndroidExtra {
+ method public boolean getValue();
+ }
+
+ public static final class ActionBuilders.AndroidBooleanExtra.Builder implements androidx.wear.tiles.ActionBuilders.AndroidExtra.Builder {
+ ctor public ActionBuilders.AndroidBooleanExtra.Builder();
+ method public androidx.wear.tiles.ActionBuilders.AndroidBooleanExtra build();
+ method public androidx.wear.tiles.ActionBuilders.AndroidBooleanExtra.Builder setValue(boolean);
+ }
+
+ public static final class ActionBuilders.AndroidDoubleExtra implements androidx.wear.tiles.ActionBuilders.AndroidExtra {
+ method public double getValue();
+ }
+
+ public static final class ActionBuilders.AndroidDoubleExtra.Builder implements androidx.wear.tiles.ActionBuilders.AndroidExtra.Builder {
+ ctor public ActionBuilders.AndroidDoubleExtra.Builder();
+ method public androidx.wear.tiles.ActionBuilders.AndroidDoubleExtra build();
+ method public androidx.wear.tiles.ActionBuilders.AndroidDoubleExtra.Builder setValue(double);
+ }
+
+ public static interface ActionBuilders.AndroidExtra {
+ }
+
+ public static interface ActionBuilders.AndroidExtra.Builder {
+ method public androidx.wear.tiles.ActionBuilders.AndroidExtra build();
+ }
+
+ public static final class ActionBuilders.AndroidIntExtra implements androidx.wear.tiles.ActionBuilders.AndroidExtra {
+ method public int getValue();
+ }
+
+ public static final class ActionBuilders.AndroidIntExtra.Builder implements androidx.wear.tiles.ActionBuilders.AndroidExtra.Builder {
+ ctor public ActionBuilders.AndroidIntExtra.Builder();
+ method public androidx.wear.tiles.ActionBuilders.AndroidIntExtra build();
+ method public androidx.wear.tiles.ActionBuilders.AndroidIntExtra.Builder setValue(int);
+ }
+
+ public static final class ActionBuilders.AndroidLongExtra implements androidx.wear.tiles.ActionBuilders.AndroidExtra {
+ method public long getValue();
+ }
+
+ public static final class ActionBuilders.AndroidLongExtra.Builder implements androidx.wear.tiles.ActionBuilders.AndroidExtra.Builder {
+ ctor public ActionBuilders.AndroidLongExtra.Builder();
+ method public androidx.wear.tiles.ActionBuilders.AndroidLongExtra build();
+ method public androidx.wear.tiles.ActionBuilders.AndroidLongExtra.Builder setValue(long);
+ }
+
+ public static final class ActionBuilders.AndroidStringExtra implements androidx.wear.tiles.ActionBuilders.AndroidExtra {
+ method public String getValue();
+ }
+
+ public static final class ActionBuilders.AndroidStringExtra.Builder implements androidx.wear.tiles.ActionBuilders.AndroidExtra.Builder {
+ ctor public ActionBuilders.AndroidStringExtra.Builder();
+ method public androidx.wear.tiles.ActionBuilders.AndroidStringExtra build();
+ method public androidx.wear.tiles.ActionBuilders.AndroidStringExtra.Builder setValue(String);
+ }
+
+ public static final class ActionBuilders.LaunchAction implements androidx.wear.tiles.ActionBuilders.Action {
+ method public androidx.wear.tiles.ActionBuilders.AndroidActivity? getAndroidActivity();
+ }
+
+ public static final class ActionBuilders.LaunchAction.Builder implements androidx.wear.tiles.ActionBuilders.Action.Builder {
+ ctor public ActionBuilders.LaunchAction.Builder();
+ method public androidx.wear.tiles.ActionBuilders.LaunchAction build();
+ method public androidx.wear.tiles.ActionBuilders.LaunchAction.Builder setAndroidActivity(androidx.wear.tiles.ActionBuilders.AndroidActivity);
+ }
+
+ public static final class ActionBuilders.LoadAction implements androidx.wear.tiles.ActionBuilders.Action {
+ method public androidx.wear.tiles.StateBuilders.State? getRequestState();
+ }
+
+ public static final class ActionBuilders.LoadAction.Builder implements androidx.wear.tiles.ActionBuilders.Action.Builder {
+ ctor public ActionBuilders.LoadAction.Builder();
+ method public androidx.wear.tiles.ActionBuilders.LoadAction build();
+ method public androidx.wear.tiles.ActionBuilders.LoadAction.Builder setRequestState(androidx.wear.tiles.StateBuilders.State);
+ }
+
+ public final class ColorBuilders {
+ method public static androidx.wear.tiles.ColorBuilders.ColorProp argb(@ColorInt int);
+ }
+
+ public static final class ColorBuilders.ColorProp {
+ method @ColorInt public int getArgb();
+ }
+
+ public static final class ColorBuilders.ColorProp.Builder {
+ ctor public ColorBuilders.ColorProp.Builder();
+ method public androidx.wear.tiles.ColorBuilders.ColorProp build();
+ method public androidx.wear.tiles.ColorBuilders.ColorProp.Builder setArgb(@ColorInt int);
+ }
+
+ public final class DeviceParametersBuilders {
+ field public static final int DEVICE_PLATFORM_UNDEFINED = 0; // 0x0
+ field public static final int DEVICE_PLATFORM_WEAR_OS = 1; // 0x1
+ field public static final int SCREEN_SHAPE_RECT = 2; // 0x2
+ field public static final int SCREEN_SHAPE_ROUND = 1; // 0x1
+ field public static final int SCREEN_SHAPE_UNDEFINED = 0; // 0x0
+ }
+
+ public static final class DeviceParametersBuilders.DeviceParameters {
+ method public int getDevicePlatform();
+ method @FloatRange(from=0.0, fromInclusive=false, toInclusive=false) public float getScreenDensity();
+ method @Dimension(unit=androidx.annotation.Dimension.DP) public int getScreenHeightDp();
+ method public int getScreenShape();
+ method @Dimension(unit=androidx.annotation.Dimension.DP) public int getScreenWidthDp();
+ }
+
+ public static final class DeviceParametersBuilders.DeviceParameters.Builder {
+ ctor public DeviceParametersBuilders.DeviceParameters.Builder();
+ method public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters build();
+ method public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters.Builder setDevicePlatform(int);
+ method public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters.Builder setScreenDensity(@FloatRange(from=0.0, fromInclusive=false, toInclusive=false) float);
+ method public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters.Builder setScreenHeightDp(@Dimension(unit=androidx.annotation.Dimension.DP) int);
+ method public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters.Builder setScreenShape(int);
+ method public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters.Builder setScreenWidthDp(@Dimension(unit=androidx.annotation.Dimension.DP) int);
+ }
+
+ public final class DimensionBuilders {
+ method public static androidx.wear.tiles.DimensionBuilders.DegreesProp degrees(float);
+ method public static androidx.wear.tiles.DimensionBuilders.DpProp dp(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ method public static androidx.wear.tiles.DimensionBuilders.EmProp em(int);
+ method public static androidx.wear.tiles.DimensionBuilders.EmProp em(float);
+ method public static androidx.wear.tiles.DimensionBuilders.ExpandedDimensionProp expand();
+ method public static androidx.wear.tiles.DimensionBuilders.SpProp sp(@Dimension(unit=androidx.annotation.Dimension.SP) float);
+ method public static androidx.wear.tiles.DimensionBuilders.WrappedDimensionProp wrap();
+ }
+
+ public static interface DimensionBuilders.ContainerDimension {
+ }
+
+ public static interface DimensionBuilders.ContainerDimension.Builder {
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension build();
+ }
+
+ public static final class DimensionBuilders.DegreesProp {
+ method public float getValue();
+ }
+
+ public static final class DimensionBuilders.DegreesProp.Builder {
+ ctor public DimensionBuilders.DegreesProp.Builder();
+ method public androidx.wear.tiles.DimensionBuilders.DegreesProp build();
+ method public androidx.wear.tiles.DimensionBuilders.DegreesProp.Builder setValue(float);
+ }
+
+ public static final class DimensionBuilders.DpProp implements androidx.wear.tiles.DimensionBuilders.ContainerDimension androidx.wear.tiles.DimensionBuilders.ImageDimension androidx.wear.tiles.DimensionBuilders.SpacerDimension {
+ method @Dimension(unit=androidx.annotation.Dimension.DP) public float getValue();
+ }
+
+ public static final class DimensionBuilders.DpProp.Builder implements androidx.wear.tiles.DimensionBuilders.ContainerDimension.Builder androidx.wear.tiles.DimensionBuilders.ImageDimension.Builder androidx.wear.tiles.DimensionBuilders.SpacerDimension.Builder {
+ ctor public DimensionBuilders.DpProp.Builder();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp build();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp.Builder setValue(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ }
+
+ public static final class DimensionBuilders.EmProp {
+ method public float getValue();
+ }
+
+ public static final class DimensionBuilders.EmProp.Builder {
+ ctor public DimensionBuilders.EmProp.Builder();
+ method public androidx.wear.tiles.DimensionBuilders.EmProp build();
+ method public androidx.wear.tiles.DimensionBuilders.EmProp.Builder setValue(float);
+ }
+
+ public static final class DimensionBuilders.ExpandedDimensionProp implements androidx.wear.tiles.DimensionBuilders.ContainerDimension androidx.wear.tiles.DimensionBuilders.ImageDimension {
+ }
+
+ public static final class DimensionBuilders.ExpandedDimensionProp.Builder implements androidx.wear.tiles.DimensionBuilders.ContainerDimension.Builder androidx.wear.tiles.DimensionBuilders.ImageDimension.Builder {
+ ctor public DimensionBuilders.ExpandedDimensionProp.Builder();
+ method public androidx.wear.tiles.DimensionBuilders.ExpandedDimensionProp build();
+ }
+
+ public static interface DimensionBuilders.ImageDimension {
+ }
+
+ public static interface DimensionBuilders.ImageDimension.Builder {
+ method public androidx.wear.tiles.DimensionBuilders.ImageDimension build();
+ }
+
+ public static final class DimensionBuilders.ProportionalDimensionProp implements androidx.wear.tiles.DimensionBuilders.ImageDimension {
+ method @IntRange(from=0) public int getAspectRatioHeight();
+ method @IntRange(from=0) public int getAspectRatioWidth();
+ }
+
+ public static final class DimensionBuilders.ProportionalDimensionProp.Builder implements androidx.wear.tiles.DimensionBuilders.ImageDimension.Builder {
+ ctor public DimensionBuilders.ProportionalDimensionProp.Builder();
+ method public androidx.wear.tiles.DimensionBuilders.ProportionalDimensionProp build();
+ method public androidx.wear.tiles.DimensionBuilders.ProportionalDimensionProp.Builder setAspectRatioHeight(@IntRange(from=0) int);
+ method public androidx.wear.tiles.DimensionBuilders.ProportionalDimensionProp.Builder setAspectRatioWidth(@IntRange(from=0) int);
+ }
+
+ public static final class DimensionBuilders.SpProp {
+ method @Dimension(unit=androidx.annotation.Dimension.SP) public float getValue();
+ }
+
+ public static final class DimensionBuilders.SpProp.Builder {
+ ctor public DimensionBuilders.SpProp.Builder();
+ method public androidx.wear.tiles.DimensionBuilders.SpProp build();
+ method public androidx.wear.tiles.DimensionBuilders.SpProp.Builder setValue(@Dimension(unit=androidx.annotation.Dimension.SP) float);
+ }
+
+ public static interface DimensionBuilders.SpacerDimension {
+ }
+
+ public static interface DimensionBuilders.SpacerDimension.Builder {
+ method public androidx.wear.tiles.DimensionBuilders.SpacerDimension build();
+ }
+
+ public static final class DimensionBuilders.WrappedDimensionProp implements androidx.wear.tiles.DimensionBuilders.ContainerDimension {
+ }
+
+ public static final class DimensionBuilders.WrappedDimensionProp.Builder implements androidx.wear.tiles.DimensionBuilders.ContainerDimension.Builder {
+ ctor public DimensionBuilders.WrappedDimensionProp.Builder();
+ method public androidx.wear.tiles.DimensionBuilders.WrappedDimensionProp build();
+ }
+
+ public final class EventBuilders {
+ }
+
+ public static final class EventBuilders.TileAddEvent {
+ }
+
+ public static final class EventBuilders.TileAddEvent.Builder {
+ ctor public EventBuilders.TileAddEvent.Builder();
+ method public androidx.wear.tiles.EventBuilders.TileAddEvent build();
+ }
+
+ public static final class EventBuilders.TileEnterEvent {
+ }
+
+ public static final class EventBuilders.TileEnterEvent.Builder {
+ ctor public EventBuilders.TileEnterEvent.Builder();
+ method public androidx.wear.tiles.EventBuilders.TileEnterEvent build();
+ }
+
+ public static final class EventBuilders.TileLeaveEvent {
+ }
+
+ public static final class EventBuilders.TileLeaveEvent.Builder {
+ ctor public EventBuilders.TileLeaveEvent.Builder();
+ method public androidx.wear.tiles.EventBuilders.TileLeaveEvent build();
+ }
+
+ public static final class EventBuilders.TileRemoveEvent {
+ }
+
+ public static final class EventBuilders.TileRemoveEvent.Builder {
+ ctor public EventBuilders.TileRemoveEvent.Builder();
+ method public androidx.wear.tiles.EventBuilders.TileRemoveEvent build();
+ }
+
+ public final class LayoutElementBuilders {
+ field public static final int ARC_ANCHOR_CENTER = 2; // 0x2
+ field public static final int ARC_ANCHOR_END = 3; // 0x3
+ field public static final int ARC_ANCHOR_START = 1; // 0x1
+ field public static final int ARC_ANCHOR_UNDEFINED = 0; // 0x0
+ field public static final int CONTENT_SCALE_MODE_CROP = 2; // 0x2
+ field public static final int CONTENT_SCALE_MODE_FILL_BOUNDS = 3; // 0x3
+ field public static final int CONTENT_SCALE_MODE_FIT = 1; // 0x1
+ field public static final int CONTENT_SCALE_MODE_UNDEFINED = 0; // 0x0
+ field public static final int FONT_VARIANT_BODY = 2; // 0x2
+ field public static final int FONT_VARIANT_TITLE = 1; // 0x1
+ field public static final int FONT_VARIANT_UNDEFINED = 0; // 0x0
+ field public static final int FONT_WEIGHT_BOLD = 700; // 0x2bc
+ field @androidx.wear.tiles.TilesExperimental public static final int FONT_WEIGHT_MEDIUM = 500; // 0x1f4
+ field public static final int FONT_WEIGHT_NORMAL = 400; // 0x190
+ field public static final int FONT_WEIGHT_UNDEFINED = 0; // 0x0
+ field public static final int HORIZONTAL_ALIGN_CENTER = 2; // 0x2
+ field public static final int HORIZONTAL_ALIGN_END = 5; // 0x5
+ field public static final int HORIZONTAL_ALIGN_LEFT = 1; // 0x1
+ field public static final int HORIZONTAL_ALIGN_RIGHT = 3; // 0x3
+ field public static final int HORIZONTAL_ALIGN_START = 4; // 0x4
+ field public static final int HORIZONTAL_ALIGN_UNDEFINED = 0; // 0x0
+ field public static final int SPAN_VERTICAL_ALIGN_BOTTOM = 1; // 0x1
+ field public static final int SPAN_VERTICAL_ALIGN_TEXT_BASELINE = 2; // 0x2
+ field public static final int SPAN_VERTICAL_ALIGN_UNDEFINED = 0; // 0x0
+ field public static final int TEXT_ALIGN_CENTER = 2; // 0x2
+ field public static final int TEXT_ALIGN_END = 3; // 0x3
+ field public static final int TEXT_ALIGN_START = 1; // 0x1
+ field public static final int TEXT_ALIGN_UNDEFINED = 0; // 0x0
+ field public static final int TEXT_OVERFLOW_ELLIPSIZE_END = 2; // 0x2
+ field public static final int TEXT_OVERFLOW_TRUNCATE = 1; // 0x1
+ field public static final int TEXT_OVERFLOW_UNDEFINED = 0; // 0x0
+ field public static final int VERTICAL_ALIGN_BOTTOM = 3; // 0x3
+ field public static final int VERTICAL_ALIGN_CENTER = 2; // 0x2
+ field public static final int VERTICAL_ALIGN_TOP = 1; // 0x1
+ field public static final int VERTICAL_ALIGN_UNDEFINED = 0; // 0x0
+ }
+
+ public static final class LayoutElementBuilders.Arc implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public androidx.wear.tiles.DimensionBuilders.DegreesProp? getAnchorAngle();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcAnchorTypeProp? getAnchorType();
+ method public java.util.List<androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement!> getContents();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers? getModifiers();
+ method public androidx.wear.tiles.LayoutElementBuilders.VerticalAlignmentProp? getVerticalAlign();
+ }
+
+ public static final class LayoutElementBuilders.Arc.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public LayoutElementBuilders.Arc.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Arc.Builder addContent(androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.Arc build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Arc.Builder setAnchorAngle(androidx.wear.tiles.DimensionBuilders.DegreesProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Arc.Builder setAnchorType(androidx.wear.tiles.LayoutElementBuilders.ArcAnchorTypeProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Arc.Builder setAnchorType(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Arc.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.Arc.Builder setVerticalAlign(androidx.wear.tiles.LayoutElementBuilders.VerticalAlignmentProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Arc.Builder setVerticalAlign(int);
+ }
+
+ public static final class LayoutElementBuilders.ArcAdapter implements androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement {
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getContent();
+ method public androidx.wear.tiles.TypeBuilders.BoolProp? getRotateContents();
+ }
+
+ public static final class LayoutElementBuilders.ArcAdapter.Builder implements androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement.Builder {
+ ctor public LayoutElementBuilders.ArcAdapter.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcAdapter build();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcAdapter.Builder setContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcAdapter.Builder setRotateContents(androidx.wear.tiles.TypeBuilders.BoolProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcAdapter.Builder setRotateContents(boolean);
+ }
+
+ public static final class LayoutElementBuilders.ArcAnchorTypeProp {
+ method public int getValue();
+ }
+
+ public static final class LayoutElementBuilders.ArcAnchorTypeProp.Builder {
+ ctor public LayoutElementBuilders.ArcAnchorTypeProp.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcAnchorTypeProp build();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcAnchorTypeProp.Builder setValue(int);
+ }
+
+ public static interface LayoutElementBuilders.ArcLayoutElement {
+ }
+
+ public static interface LayoutElementBuilders.ArcLayoutElement.Builder {
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement build();
+ }
+
+ public static final class LayoutElementBuilders.ArcLine implements androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement {
+ method public androidx.wear.tiles.ColorBuilders.ColorProp? getColor();
+ method public androidx.wear.tiles.DimensionBuilders.DegreesProp? getLength();
+ method public androidx.wear.tiles.ModifiersBuilders.ArcModifiers? getModifiers();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getThickness();
+ }
+
+ public static final class LayoutElementBuilders.ArcLine.Builder implements androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement.Builder {
+ ctor public LayoutElementBuilders.ArcLine.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcLine build();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcLine.Builder setColor(androidx.wear.tiles.ColorBuilders.ColorProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcLine.Builder setLength(androidx.wear.tiles.DimensionBuilders.DegreesProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcLine.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.ArcModifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcLine.Builder setThickness(androidx.wear.tiles.DimensionBuilders.DpProp);
+ }
+
+ public static final class LayoutElementBuilders.ArcSpacer implements androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement {
+ method public androidx.wear.tiles.DimensionBuilders.DegreesProp? getLength();
+ method public androidx.wear.tiles.ModifiersBuilders.ArcModifiers? getModifiers();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getThickness();
+ }
+
+ public static final class LayoutElementBuilders.ArcSpacer.Builder implements androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement.Builder {
+ ctor public LayoutElementBuilders.ArcSpacer.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcSpacer build();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcSpacer.Builder setLength(androidx.wear.tiles.DimensionBuilders.DegreesProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcSpacer.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.ArcModifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcSpacer.Builder setThickness(androidx.wear.tiles.DimensionBuilders.DpProp);
+ }
+
+ public static final class LayoutElementBuilders.ArcText implements androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement {
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle? getFontStyle();
+ method public androidx.wear.tiles.ModifiersBuilders.ArcModifiers? getModifiers();
+ method public androidx.wear.tiles.TypeBuilders.StringProp? getText();
+ }
+
+ public static final class LayoutElementBuilders.ArcText.Builder implements androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement.Builder {
+ ctor public LayoutElementBuilders.ArcText.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcText build();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcText.Builder setFontStyle(androidx.wear.tiles.LayoutElementBuilders.FontStyle);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcText.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.ArcModifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcText.Builder setText(androidx.wear.tiles.TypeBuilders.StringProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcText.Builder setText(String);
+ }
+
+ public static final class LayoutElementBuilders.Box implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public java.util.List<androidx.wear.tiles.LayoutElementBuilders.LayoutElement!> getContents();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension? getHeight();
+ method public androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignmentProp? getHorizontalAlignment();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers? getModifiers();
+ method public androidx.wear.tiles.LayoutElementBuilders.VerticalAlignmentProp? getVerticalAlignment();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension? getWidth();
+ }
+
+ public static final class LayoutElementBuilders.Box.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public LayoutElementBuilders.Box.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Box.Builder addContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.Box build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Box.Builder setHeight(androidx.wear.tiles.DimensionBuilders.ContainerDimension);
+ method public androidx.wear.tiles.LayoutElementBuilders.Box.Builder setHorizontalAlignment(androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignmentProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Box.Builder setHorizontalAlignment(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Box.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.Box.Builder setVerticalAlignment(androidx.wear.tiles.LayoutElementBuilders.VerticalAlignmentProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Box.Builder setVerticalAlignment(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Box.Builder setWidth(androidx.wear.tiles.DimensionBuilders.ContainerDimension);
+ }
+
+ public static final class LayoutElementBuilders.ColorFilter {
+ method public androidx.wear.tiles.ColorBuilders.ColorProp? getTint();
+ }
+
+ public static final class LayoutElementBuilders.ColorFilter.Builder {
+ ctor public LayoutElementBuilders.ColorFilter.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.ColorFilter build();
+ method public androidx.wear.tiles.LayoutElementBuilders.ColorFilter.Builder setTint(androidx.wear.tiles.ColorBuilders.ColorProp);
+ }
+
+ public static final class LayoutElementBuilders.Column implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public java.util.List<androidx.wear.tiles.LayoutElementBuilders.LayoutElement!> getContents();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension? getHeight();
+ method public androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignmentProp? getHorizontalAlignment();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers? getModifiers();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension? getWidth();
+ }
+
+ public static final class LayoutElementBuilders.Column.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public LayoutElementBuilders.Column.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Column.Builder addContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.Column build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Column.Builder setHeight(androidx.wear.tiles.DimensionBuilders.ContainerDimension);
+ method public androidx.wear.tiles.LayoutElementBuilders.Column.Builder setHorizontalAlignment(androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignmentProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Column.Builder setHorizontalAlignment(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Column.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.Column.Builder setWidth(androidx.wear.tiles.DimensionBuilders.ContainerDimension);
+ }
+
+ public static final class LayoutElementBuilders.ContentScaleModeProp {
+ method public int getValue();
+ }
+
+ public static final class LayoutElementBuilders.ContentScaleModeProp.Builder {
+ ctor public LayoutElementBuilders.ContentScaleModeProp.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.ContentScaleModeProp build();
+ method public androidx.wear.tiles.LayoutElementBuilders.ContentScaleModeProp.Builder setValue(int);
+ }
+
+ public static final class LayoutElementBuilders.FontStyle {
+ method public androidx.wear.tiles.ColorBuilders.ColorProp? getColor();
+ method public androidx.wear.tiles.TypeBuilders.BoolProp? getItalic();
+ method public androidx.wear.tiles.DimensionBuilders.EmProp? getLetterSpacing();
+ method public androidx.wear.tiles.DimensionBuilders.SpProp? getSize();
+ method public androidx.wear.tiles.TypeBuilders.BoolProp? getUnderline();
+ method @androidx.wear.tiles.TilesExperimental public androidx.wear.tiles.LayoutElementBuilders.FontVariantProp? getVariant();
+ method public androidx.wear.tiles.LayoutElementBuilders.FontWeightProp? getWeight();
+ }
+
+ public static final class LayoutElementBuilders.FontStyle.Builder {
+ ctor public LayoutElementBuilders.FontStyle.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle build();
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setColor(androidx.wear.tiles.ColorBuilders.ColorProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setItalic(androidx.wear.tiles.TypeBuilders.BoolProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setItalic(boolean);
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setLetterSpacing(androidx.wear.tiles.DimensionBuilders.EmProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setSize(androidx.wear.tiles.DimensionBuilders.SpProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setUnderline(androidx.wear.tiles.TypeBuilders.BoolProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setUnderline(boolean);
+ method @androidx.wear.tiles.TilesExperimental public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setVariant(androidx.wear.tiles.LayoutElementBuilders.FontVariantProp);
+ method @androidx.wear.tiles.TilesExperimental public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setVariant(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setWeight(androidx.wear.tiles.LayoutElementBuilders.FontWeightProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setWeight(int);
+ }
+
+ public static class LayoutElementBuilders.FontStyles {
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder body1(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder body2(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder button(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder caption1(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder caption2(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder display1(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder display2(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder display3(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder title1(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder title2(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder title3(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ }
+
+ @androidx.wear.tiles.TilesExperimental public static final class LayoutElementBuilders.FontVariantProp {
+ method public int getValue();
+ }
+
+ public static final class LayoutElementBuilders.FontVariantProp.Builder {
+ ctor public LayoutElementBuilders.FontVariantProp.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.FontVariantProp build();
+ method public androidx.wear.tiles.LayoutElementBuilders.FontVariantProp.Builder setValue(int);
+ }
+
+ public static final class LayoutElementBuilders.FontWeightProp {
+ method public int getValue();
+ }
+
+ public static final class LayoutElementBuilders.FontWeightProp.Builder {
+ ctor public LayoutElementBuilders.FontWeightProp.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.FontWeightProp build();
+ method public androidx.wear.tiles.LayoutElementBuilders.FontWeightProp.Builder setValue(int);
+ }
+
+ public static final class LayoutElementBuilders.HorizontalAlignmentProp {
+ method public int getValue();
+ }
+
+ public static final class LayoutElementBuilders.HorizontalAlignmentProp.Builder {
+ ctor public LayoutElementBuilders.HorizontalAlignmentProp.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignmentProp build();
+ method public androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignmentProp.Builder setValue(int);
+ }
+
+ public static final class LayoutElementBuilders.Image implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public androidx.wear.tiles.LayoutElementBuilders.ColorFilter? getColorFilter();
+ method public androidx.wear.tiles.LayoutElementBuilders.ContentScaleModeProp? getContentScaleMode();
+ method public androidx.wear.tiles.DimensionBuilders.ImageDimension? getHeight();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers? getModifiers();
+ method public androidx.wear.tiles.TypeBuilders.StringProp? getResourceId();
+ method public androidx.wear.tiles.DimensionBuilders.ImageDimension? getWidth();
+ }
+
+ public static final class LayoutElementBuilders.Image.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public LayoutElementBuilders.Image.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Image build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Image.Builder setColorFilter(androidx.wear.tiles.LayoutElementBuilders.ColorFilter);
+ method public androidx.wear.tiles.LayoutElementBuilders.Image.Builder setContentScaleMode(androidx.wear.tiles.LayoutElementBuilders.ContentScaleModeProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Image.Builder setContentScaleMode(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Image.Builder setHeight(androidx.wear.tiles.DimensionBuilders.ImageDimension);
+ method public androidx.wear.tiles.LayoutElementBuilders.Image.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.Image.Builder setResourceId(androidx.wear.tiles.TypeBuilders.StringProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Image.Builder setResourceId(String);
+ method public androidx.wear.tiles.LayoutElementBuilders.Image.Builder setWidth(androidx.wear.tiles.DimensionBuilders.ImageDimension);
+ }
+
+ public static final class LayoutElementBuilders.Layout {
+ method @androidx.wear.tiles.TilesExperimental public static androidx.wear.tiles.LayoutElementBuilders.Layout? fromByteArray(byte[]);
+ method public static androidx.wear.tiles.LayoutElementBuilders.Layout fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getRoot();
+ method @androidx.wear.tiles.TilesExperimental public byte[] toByteArray();
+ }
+
+ public static final class LayoutElementBuilders.Layout.Builder {
+ ctor public LayoutElementBuilders.Layout.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Layout build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Layout.Builder setRoot(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ }
+
+ public static interface LayoutElementBuilders.LayoutElement {
+ }
+
+ public static interface LayoutElementBuilders.LayoutElement.Builder {
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement build();
+ }
+
+ public static final class LayoutElementBuilders.Row implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public java.util.List<androidx.wear.tiles.LayoutElementBuilders.LayoutElement!> getContents();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension? getHeight();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers? getModifiers();
+ method public androidx.wear.tiles.LayoutElementBuilders.VerticalAlignmentProp? getVerticalAlignment();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension? getWidth();
+ }
+
+ public static final class LayoutElementBuilders.Row.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public LayoutElementBuilders.Row.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Row.Builder addContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.Row build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Row.Builder setHeight(androidx.wear.tiles.DimensionBuilders.ContainerDimension);
+ method public androidx.wear.tiles.LayoutElementBuilders.Row.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.Row.Builder setVerticalAlignment(androidx.wear.tiles.LayoutElementBuilders.VerticalAlignmentProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Row.Builder setVerticalAlignment(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Row.Builder setWidth(androidx.wear.tiles.DimensionBuilders.ContainerDimension);
+ }
+
+ public static final class LayoutElementBuilders.Spacer implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public androidx.wear.tiles.DimensionBuilders.SpacerDimension? getHeight();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers? getModifiers();
+ method public androidx.wear.tiles.DimensionBuilders.SpacerDimension? getWidth();
+ }
+
+ public static final class LayoutElementBuilders.Spacer.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public LayoutElementBuilders.Spacer.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Spacer build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Spacer.Builder setHeight(androidx.wear.tiles.DimensionBuilders.SpacerDimension);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spacer.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spacer.Builder setWidth(androidx.wear.tiles.DimensionBuilders.SpacerDimension);
+ }
+
+ public static interface LayoutElementBuilders.Span {
+ }
+
+ public static interface LayoutElementBuilders.Span.Builder {
+ method public androidx.wear.tiles.LayoutElementBuilders.Span build();
+ }
+
+ public static final class LayoutElementBuilders.SpanImage implements androidx.wear.tiles.LayoutElementBuilders.Span {
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanVerticalAlignmentProp? getAlignment();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getHeight();
+ method public androidx.wear.tiles.ModifiersBuilders.SpanModifiers? getModifiers();
+ method public androidx.wear.tiles.TypeBuilders.StringProp? getResourceId();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getWidth();
+ }
+
+ public static final class LayoutElementBuilders.SpanImage.Builder implements androidx.wear.tiles.LayoutElementBuilders.Span.Builder {
+ ctor public LayoutElementBuilders.SpanImage.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanImage build();
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanImage.Builder setAlignment(androidx.wear.tiles.LayoutElementBuilders.SpanVerticalAlignmentProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanImage.Builder setAlignment(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanImage.Builder setHeight(androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanImage.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.SpanModifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanImage.Builder setResourceId(androidx.wear.tiles.TypeBuilders.StringProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanImage.Builder setResourceId(String);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanImage.Builder setWidth(androidx.wear.tiles.DimensionBuilders.DpProp);
+ }
+
+ public static final class LayoutElementBuilders.SpanText implements androidx.wear.tiles.LayoutElementBuilders.Span {
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle? getFontStyle();
+ method public androidx.wear.tiles.ModifiersBuilders.SpanModifiers? getModifiers();
+ method public androidx.wear.tiles.TypeBuilders.StringProp? getText();
+ }
+
+ public static final class LayoutElementBuilders.SpanText.Builder implements androidx.wear.tiles.LayoutElementBuilders.Span.Builder {
+ ctor public LayoutElementBuilders.SpanText.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanText build();
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanText.Builder setFontStyle(androidx.wear.tiles.LayoutElementBuilders.FontStyle);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanText.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.SpanModifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanText.Builder setText(androidx.wear.tiles.TypeBuilders.StringProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanText.Builder setText(String);
+ }
+
+ public static final class LayoutElementBuilders.SpanVerticalAlignmentProp {
+ method public int getValue();
+ }
+
+ public static final class LayoutElementBuilders.SpanVerticalAlignmentProp.Builder {
+ ctor public LayoutElementBuilders.SpanVerticalAlignmentProp.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanVerticalAlignmentProp build();
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanVerticalAlignmentProp.Builder setValue(int);
+ }
+
+ public static final class LayoutElementBuilders.Spannable implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public androidx.wear.tiles.DimensionBuilders.SpProp? getLineHeight();
+ method public androidx.wear.tiles.TypeBuilders.Int32Prop? getMaxLines();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers? getModifiers();
+ method public androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignmentProp? getMultilineAlignment();
+ method public androidx.wear.tiles.LayoutElementBuilders.TextOverflowProp? getOverflow();
+ method public java.util.List<androidx.wear.tiles.LayoutElementBuilders.Span!> getSpans();
+ }
+
+ public static final class LayoutElementBuilders.Spannable.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public LayoutElementBuilders.Spannable.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder addSpan(androidx.wear.tiles.LayoutElementBuilders.Span);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder setLineHeight(androidx.wear.tiles.DimensionBuilders.SpProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder setMaxLines(androidx.wear.tiles.TypeBuilders.Int32Prop);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder setMaxLines(@IntRange(from=1) int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder setMultilineAlignment(androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignmentProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder setMultilineAlignment(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder setOverflow(androidx.wear.tiles.LayoutElementBuilders.TextOverflowProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder setOverflow(int);
+ }
+
+ public static final class LayoutElementBuilders.Text implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle? getFontStyle();
+ method public androidx.wear.tiles.DimensionBuilders.SpProp? getLineHeight();
+ method public androidx.wear.tiles.TypeBuilders.Int32Prop? getMaxLines();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers? getModifiers();
+ method public androidx.wear.tiles.LayoutElementBuilders.TextAlignmentProp? getMultilineAlignment();
+ method public androidx.wear.tiles.LayoutElementBuilders.TextOverflowProp? getOverflow();
+ method public androidx.wear.tiles.TypeBuilders.StringProp? getText();
+ }
+
+ public static final class LayoutElementBuilders.Text.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public LayoutElementBuilders.Text.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Text build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setFontStyle(androidx.wear.tiles.LayoutElementBuilders.FontStyle);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setLineHeight(androidx.wear.tiles.DimensionBuilders.SpProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setMaxLines(androidx.wear.tiles.TypeBuilders.Int32Prop);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setMaxLines(@IntRange(from=1) int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setMultilineAlignment(androidx.wear.tiles.LayoutElementBuilders.TextAlignmentProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setMultilineAlignment(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setOverflow(androidx.wear.tiles.LayoutElementBuilders.TextOverflowProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setOverflow(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setText(androidx.wear.tiles.TypeBuilders.StringProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setText(String);
+ }
+
+ public static final class LayoutElementBuilders.TextAlignmentProp {
+ method public int getValue();
+ }
+
+ public static final class LayoutElementBuilders.TextAlignmentProp.Builder {
+ ctor public LayoutElementBuilders.TextAlignmentProp.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.TextAlignmentProp build();
+ method public androidx.wear.tiles.LayoutElementBuilders.TextAlignmentProp.Builder setValue(int);
+ }
+
+ public static final class LayoutElementBuilders.TextOverflowProp {
+ method public int getValue();
+ }
+
+ public static final class LayoutElementBuilders.TextOverflowProp.Builder {
+ ctor public LayoutElementBuilders.TextOverflowProp.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.TextOverflowProp build();
+ method public androidx.wear.tiles.LayoutElementBuilders.TextOverflowProp.Builder setValue(int);
+ }
+
+ public static final class LayoutElementBuilders.VerticalAlignmentProp {
+ method public int getValue();
+ }
+
+ public static final class LayoutElementBuilders.VerticalAlignmentProp.Builder {
+ ctor public LayoutElementBuilders.VerticalAlignmentProp.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.VerticalAlignmentProp build();
+ method public androidx.wear.tiles.LayoutElementBuilders.VerticalAlignmentProp.Builder setValue(int);
+ }
+
+ public final class ModifiersBuilders {
+ }
+
+ public static final class ModifiersBuilders.ArcModifiers {
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable? getClickable();
+ method public androidx.wear.tiles.ModifiersBuilders.Semantics? getSemantics();
+ }
+
+ public static final class ModifiersBuilders.ArcModifiers.Builder {
+ ctor public ModifiersBuilders.ArcModifiers.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.ArcModifiers build();
+ method public androidx.wear.tiles.ModifiersBuilders.ArcModifiers.Builder setClickable(androidx.wear.tiles.ModifiersBuilders.Clickable);
+ method public androidx.wear.tiles.ModifiersBuilders.ArcModifiers.Builder setSemantics(androidx.wear.tiles.ModifiersBuilders.Semantics);
+ }
+
+ public static final class ModifiersBuilders.Background {
+ method public androidx.wear.tiles.ColorBuilders.ColorProp? getColor();
+ method public androidx.wear.tiles.ModifiersBuilders.Corner? getCorner();
+ }
+
+ public static final class ModifiersBuilders.Background.Builder {
+ ctor public ModifiersBuilders.Background.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.Background build();
+ method public androidx.wear.tiles.ModifiersBuilders.Background.Builder setColor(androidx.wear.tiles.ColorBuilders.ColorProp);
+ method public androidx.wear.tiles.ModifiersBuilders.Background.Builder setCorner(androidx.wear.tiles.ModifiersBuilders.Corner);
+ }
+
+ public static final class ModifiersBuilders.Border {
+ method public androidx.wear.tiles.ColorBuilders.ColorProp? getColor();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getWidth();
+ }
+
+ public static final class ModifiersBuilders.Border.Builder {
+ ctor public ModifiersBuilders.Border.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.Border build();
+ method public androidx.wear.tiles.ModifiersBuilders.Border.Builder setColor(androidx.wear.tiles.ColorBuilders.ColorProp);
+ method public androidx.wear.tiles.ModifiersBuilders.Border.Builder setWidth(androidx.wear.tiles.DimensionBuilders.DpProp);
+ }
+
+ public static final class ModifiersBuilders.Clickable {
+ method public String getId();
+ method public androidx.wear.tiles.ActionBuilders.Action? getOnClick();
+ }
+
+ public static final class ModifiersBuilders.Clickable.Builder {
+ ctor public ModifiersBuilders.Clickable.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable build();
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable.Builder setId(String);
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable.Builder setOnClick(androidx.wear.tiles.ActionBuilders.Action);
+ }
+
+ public static final class ModifiersBuilders.Corner {
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getRadius();
+ }
+
+ public static final class ModifiersBuilders.Corner.Builder {
+ ctor public ModifiersBuilders.Corner.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.Corner build();
+ method public androidx.wear.tiles.ModifiersBuilders.Corner.Builder setRadius(androidx.wear.tiles.DimensionBuilders.DpProp);
+ }
+
+ public static final class ModifiersBuilders.ElementMetadata {
+ method public byte[] getTagData();
+ }
+
+ public static final class ModifiersBuilders.ElementMetadata.Builder {
+ ctor public ModifiersBuilders.ElementMetadata.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.ElementMetadata build();
+ method public androidx.wear.tiles.ModifiersBuilders.ElementMetadata.Builder setTagData(byte[]);
+ }
+
+ public static final class ModifiersBuilders.Modifiers {
+ method public androidx.wear.tiles.ModifiersBuilders.Background? getBackground();
+ method public androidx.wear.tiles.ModifiersBuilders.Border? getBorder();
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable? getClickable();
+ method public androidx.wear.tiles.ModifiersBuilders.ElementMetadata? getMetadata();
+ method public androidx.wear.tiles.ModifiersBuilders.Padding? getPadding();
+ method public androidx.wear.tiles.ModifiersBuilders.Semantics? getSemantics();
+ }
+
+ public static final class ModifiersBuilders.Modifiers.Builder {
+ ctor public ModifiersBuilders.Modifiers.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers build();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers.Builder setBackground(androidx.wear.tiles.ModifiersBuilders.Background);
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers.Builder setBorder(androidx.wear.tiles.ModifiersBuilders.Border);
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers.Builder setClickable(androidx.wear.tiles.ModifiersBuilders.Clickable);
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers.Builder setMetadata(androidx.wear.tiles.ModifiersBuilders.ElementMetadata);
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers.Builder setPadding(androidx.wear.tiles.ModifiersBuilders.Padding);
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers.Builder setSemantics(androidx.wear.tiles.ModifiersBuilders.Semantics);
+ }
+
+ public static final class ModifiersBuilders.Padding {
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getBottom();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getEnd();
+ method public androidx.wear.tiles.TypeBuilders.BoolProp? getRtlAware();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getStart();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getTop();
+ }
+
+ public static final class ModifiersBuilders.Padding.Builder {
+ ctor public ModifiersBuilders.Padding.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.Padding build();
+ method public androidx.wear.tiles.ModifiersBuilders.Padding.Builder setAll(androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public androidx.wear.tiles.ModifiersBuilders.Padding.Builder setBottom(androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public androidx.wear.tiles.ModifiersBuilders.Padding.Builder setEnd(androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public androidx.wear.tiles.ModifiersBuilders.Padding.Builder setRtlAware(androidx.wear.tiles.TypeBuilders.BoolProp);
+ method public androidx.wear.tiles.ModifiersBuilders.Padding.Builder setRtlAware(boolean);
+ method public androidx.wear.tiles.ModifiersBuilders.Padding.Builder setStart(androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public androidx.wear.tiles.ModifiersBuilders.Padding.Builder setTop(androidx.wear.tiles.DimensionBuilders.DpProp);
+ }
+
+ public static final class ModifiersBuilders.Semantics {
+ method public String getContentDescription();
+ }
+
+ public static final class ModifiersBuilders.Semantics.Builder {
+ ctor public ModifiersBuilders.Semantics.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.Semantics build();
+ method public androidx.wear.tiles.ModifiersBuilders.Semantics.Builder setContentDescription(String);
+ }
+
+ public static final class ModifiersBuilders.SpanModifiers {
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable? getClickable();
+ }
+
+ public static final class ModifiersBuilders.SpanModifiers.Builder {
+ ctor public ModifiersBuilders.SpanModifiers.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.SpanModifiers build();
+ method public androidx.wear.tiles.ModifiersBuilders.SpanModifiers.Builder setClickable(androidx.wear.tiles.ModifiersBuilders.Clickable);
+ }
+
+ public final class RequestBuilders {
+ }
+
+ public static final class RequestBuilders.ResourcesRequest {
+ method public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters? getDeviceParameters();
+ method public java.util.List<java.lang.String!> getResourceIds();
+ method public String getVersion();
+ }
+
+ public static final class RequestBuilders.ResourcesRequest.Builder {
+ ctor public RequestBuilders.ResourcesRequest.Builder();
+ method public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder addResourceId(String);
+ method public androidx.wear.tiles.RequestBuilders.ResourcesRequest build();
+ method public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder setDeviceParameters(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder setVersion(String);
+ }
+
+ public static final class RequestBuilders.TileRequest {
+ method public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters? getDeviceParameters();
+ method public androidx.wear.tiles.StateBuilders.State? getState();
+ }
+
+ public static final class RequestBuilders.TileRequest.Builder {
+ ctor public RequestBuilders.TileRequest.Builder();
+ method public androidx.wear.tiles.RequestBuilders.TileRequest build();
+ method public androidx.wear.tiles.RequestBuilders.TileRequest.Builder setDeviceParameters(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public androidx.wear.tiles.RequestBuilders.TileRequest.Builder setState(androidx.wear.tiles.StateBuilders.State);
+ }
+
+ public final class ResourceBuilders {
+ field public static final int IMAGE_FORMAT_RGB_565 = 1; // 0x1
+ field public static final int IMAGE_FORMAT_UNDEFINED = 0; // 0x0
+ }
+
+ public static final class ResourceBuilders.AndroidImageResourceByResId {
+ method @DrawableRes public int getResourceId();
+ }
+
+ public static final class ResourceBuilders.AndroidImageResourceByResId.Builder {
+ ctor public ResourceBuilders.AndroidImageResourceByResId.Builder();
+ method public androidx.wear.tiles.ResourceBuilders.AndroidImageResourceByResId build();
+ method public androidx.wear.tiles.ResourceBuilders.AndroidImageResourceByResId.Builder setResourceId(@DrawableRes int);
+ }
+
+ public static final class ResourceBuilders.ImageResource {
+ method public androidx.wear.tiles.ResourceBuilders.AndroidImageResourceByResId? getAndroidResourceByResId();
+ method public androidx.wear.tiles.ResourceBuilders.InlineImageResource? getInlineResource();
+ }
+
+ public static final class ResourceBuilders.ImageResource.Builder {
+ ctor public ResourceBuilders.ImageResource.Builder();
+ method public androidx.wear.tiles.ResourceBuilders.ImageResource build();
+ method public androidx.wear.tiles.ResourceBuilders.ImageResource.Builder setAndroidResourceByResId(androidx.wear.tiles.ResourceBuilders.AndroidImageResourceByResId);
+ method public androidx.wear.tiles.ResourceBuilders.ImageResource.Builder setInlineResource(androidx.wear.tiles.ResourceBuilders.InlineImageResource);
+ }
+
+ public static final class ResourceBuilders.InlineImageResource {
+ method public byte[] getData();
+ method public int getFormat();
+ method @Dimension(unit=androidx.annotation.Dimension.PX) public int getHeightPx();
+ method @Dimension(unit=androidx.annotation.Dimension.PX) public int getWidthPx();
+ }
+
+ public static final class ResourceBuilders.InlineImageResource.Builder {
+ ctor public ResourceBuilders.InlineImageResource.Builder();
+ method public androidx.wear.tiles.ResourceBuilders.InlineImageResource build();
+ method public androidx.wear.tiles.ResourceBuilders.InlineImageResource.Builder setData(byte[]);
+ method public androidx.wear.tiles.ResourceBuilders.InlineImageResource.Builder setFormat(int);
+ method public androidx.wear.tiles.ResourceBuilders.InlineImageResource.Builder setHeightPx(@Dimension(unit=androidx.annotation.Dimension.PX) int);
+ method public androidx.wear.tiles.ResourceBuilders.InlineImageResource.Builder setWidthPx(@Dimension(unit=androidx.annotation.Dimension.PX) int);
+ }
+
+ public static final class ResourceBuilders.Resources {
+ method @androidx.wear.tiles.TilesExperimental public static androidx.wear.tiles.ResourceBuilders.Resources? fromByteArray(byte[]);
+ method public java.util.Map<java.lang.String!,androidx.wear.tiles.ResourceBuilders.ImageResource!> getIdToImageMapping();
+ method public String getVersion();
+ method @androidx.wear.tiles.TilesExperimental public byte[] toByteArray();
+ }
+
+ public static final class ResourceBuilders.Resources.Builder {
+ ctor public ResourceBuilders.Resources.Builder();
+ method public androidx.wear.tiles.ResourceBuilders.Resources.Builder addIdToImageMapping(String, androidx.wear.tiles.ResourceBuilders.ImageResource);
+ method public androidx.wear.tiles.ResourceBuilders.Resources build();
+ method public androidx.wear.tiles.ResourceBuilders.Resources.Builder setVersion(String);
+ }
+
+ public final class StateBuilders {
+ }
+
+ public static final class StateBuilders.State {
+ method public String getLastClickableId();
+ }
+
+ public static final class StateBuilders.State.Builder {
+ ctor public StateBuilders.State.Builder();
+ method public androidx.wear.tiles.StateBuilders.State build();
+ }
+
+ public final class TileBuilders {
+ }
+
+ public static final class TileBuilders.Tile {
+ method public long getFreshnessIntervalMillis();
+ method public String getResourcesVersion();
+ method public androidx.wear.tiles.TimelineBuilders.Timeline? getTimeline();
+ }
+
+ public static final class TileBuilders.Tile.Builder {
+ ctor public TileBuilders.Tile.Builder();
+ method public androidx.wear.tiles.TileBuilders.Tile build();
+ method public androidx.wear.tiles.TileBuilders.Tile.Builder setFreshnessIntervalMillis(long);
+ method public androidx.wear.tiles.TileBuilders.Tile.Builder setResourcesVersion(String);
+ method public androidx.wear.tiles.TileBuilders.Tile.Builder setTimeline(androidx.wear.tiles.TimelineBuilders.Timeline);
+ }
+
+ public abstract class TileService extends android.app.Service {
+ ctor public TileService();
+ method public static androidx.wear.tiles.TileUpdateRequester getUpdater(android.content.Context);
+ method public android.os.IBinder? onBind(android.content.Intent);
+ method @MainThread protected abstract com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.ResourceBuilders.Resources!> onResourcesRequest(androidx.wear.tiles.RequestBuilders.ResourcesRequest);
+ method @MainThread protected void onTileAddEvent(androidx.wear.tiles.EventBuilders.TileAddEvent);
+ method @MainThread protected void onTileEnterEvent(androidx.wear.tiles.EventBuilders.TileEnterEvent);
+ method @MainThread protected void onTileLeaveEvent(androidx.wear.tiles.EventBuilders.TileLeaveEvent);
+ method @MainThread protected void onTileRemoveEvent(androidx.wear.tiles.EventBuilders.TileRemoveEvent);
+ method @MainThread protected abstract com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.TileBuilders.Tile!> onTileRequest(androidx.wear.tiles.RequestBuilders.TileRequest);
+ field public static final String ACTION_BIND_TILE_PROVIDER = "androidx.wear.tiles.action.BIND_TILE_PROVIDER";
+ field public static final String EXTRA_CLICKABLE_ID = "androidx.wear.tiles.extra.CLICKABLE_ID";
+ field public static final String METADATA_PREVIEW_KEY = "androidx.wear.tiles.PREVIEW";
+ }
+
+ public interface TileUpdateRequester {
+ method public void requestUpdate(Class<? extends androidx.wear.tiles.TileService>);
+ }
+
+ @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.TYPE, java.lang.annotation.ElementType.FIELD}) public @interface TilesExperimental {
+ }
+
+ public final class TimelineBuilders {
+ }
+
+ public static final class TimelineBuilders.TimeInterval {
+ method public long getEndMillis();
+ method public long getStartMillis();
+ }
+
+ public static final class TimelineBuilders.TimeInterval.Builder {
+ ctor public TimelineBuilders.TimeInterval.Builder();
+ method public androidx.wear.tiles.TimelineBuilders.TimeInterval build();
+ method public androidx.wear.tiles.TimelineBuilders.TimeInterval.Builder setEndMillis(long);
+ method public androidx.wear.tiles.TimelineBuilders.TimeInterval.Builder setStartMillis(long);
+ }
+
+ public static final class TimelineBuilders.Timeline {
+ method public static androidx.wear.tiles.TimelineBuilders.Timeline fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public java.util.List<androidx.wear.tiles.TimelineBuilders.TimelineEntry!> getTimelineEntries();
+ }
+
+ public static final class TimelineBuilders.Timeline.Builder {
+ ctor public TimelineBuilders.Timeline.Builder();
+ method public androidx.wear.tiles.TimelineBuilders.Timeline.Builder addTimelineEntry(androidx.wear.tiles.TimelineBuilders.TimelineEntry);
+ method public androidx.wear.tiles.TimelineBuilders.Timeline build();
+ }
+
+ public static final class TimelineBuilders.TimelineEntry {
+ method public static androidx.wear.tiles.TimelineBuilders.TimelineEntry fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.Layout? getLayout();
+ method public androidx.wear.tiles.TimelineBuilders.TimeInterval? getValidity();
+ }
+
+ public static final class TimelineBuilders.TimelineEntry.Builder {
+ ctor public TimelineBuilders.TimelineEntry.Builder();
+ method public androidx.wear.tiles.TimelineBuilders.TimelineEntry build();
+ method public androidx.wear.tiles.TimelineBuilders.TimelineEntry.Builder setLayout(androidx.wear.tiles.LayoutElementBuilders.Layout);
+ method public androidx.wear.tiles.TimelineBuilders.TimelineEntry.Builder setValidity(androidx.wear.tiles.TimelineBuilders.TimeInterval);
+ }
+
+ public final class TypeBuilders {
+ }
+
+ public static final class TypeBuilders.BoolProp {
+ method public boolean getValue();
+ }
+
+ public static final class TypeBuilders.BoolProp.Builder {
+ ctor public TypeBuilders.BoolProp.Builder();
+ method public androidx.wear.tiles.TypeBuilders.BoolProp build();
+ method public androidx.wear.tiles.TypeBuilders.BoolProp.Builder setValue(boolean);
+ }
+
+ public static final class TypeBuilders.FloatProp {
+ method public float getValue();
+ }
+
+ public static final class TypeBuilders.FloatProp.Builder {
+ ctor public TypeBuilders.FloatProp.Builder();
+ method public androidx.wear.tiles.TypeBuilders.FloatProp build();
+ method public androidx.wear.tiles.TypeBuilders.FloatProp.Builder setValue(float);
+ }
+
+ public static final class TypeBuilders.Int32Prop {
+ method public int getValue();
+ }
+
+ public static final class TypeBuilders.Int32Prop.Builder {
+ ctor public TypeBuilders.Int32Prop.Builder();
+ method public androidx.wear.tiles.TypeBuilders.Int32Prop build();
+ method public androidx.wear.tiles.TypeBuilders.Int32Prop.Builder setValue(int);
+ }
+
+ public static final class TypeBuilders.StringProp {
+ method public String getValue();
+ }
+
+ public static final class TypeBuilders.StringProp.Builder {
+ ctor public TypeBuilders.StringProp.Builder();
+ method public androidx.wear.tiles.TypeBuilders.StringProp build();
+ method public androidx.wear.tiles.TypeBuilders.StringProp.Builder setValue(String);
+ }
+
+}
+
diff --git a/wear/tiles/tiles/api/res-1.1.0-beta01.txt b/wear/tiles/tiles/api/res-1.1.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/tiles/tiles/api/res-1.1.0-beta01.txt
diff --git a/wear/tiles/tiles/api/restricted_1.1.0-beta01.txt b/wear/tiles/tiles/api/restricted_1.1.0-beta01.txt
new file mode 100644
index 0000000..1f4b5140
--- /dev/null
+++ b/wear/tiles/tiles/api/restricted_1.1.0-beta01.txt
@@ -0,0 +1,1084 @@
+// Signature format: 4.0
+package androidx.wear.tiles {
+
+ public final class ActionBuilders {
+ method public static androidx.wear.tiles.ActionBuilders.AndroidBooleanExtra booleanExtra(boolean);
+ method public static androidx.wear.tiles.ActionBuilders.AndroidDoubleExtra doubleExtra(double);
+ method public static androidx.wear.tiles.ActionBuilders.AndroidIntExtra intExtra(int);
+ method public static androidx.wear.tiles.ActionBuilders.AndroidLongExtra longExtra(long);
+ method public static androidx.wear.tiles.ActionBuilders.AndroidStringExtra stringExtra(String);
+ }
+
+ public static interface ActionBuilders.Action {
+ }
+
+ public static interface ActionBuilders.Action.Builder {
+ method public androidx.wear.tiles.ActionBuilders.Action build();
+ }
+
+ public static final class ActionBuilders.AndroidActivity {
+ method public String getClassName();
+ method public java.util.Map<java.lang.String!,androidx.wear.tiles.ActionBuilders.AndroidExtra!> getKeyToExtraMapping();
+ method public String getPackageName();
+ }
+
+ public static final class ActionBuilders.AndroidActivity.Builder {
+ ctor public ActionBuilders.AndroidActivity.Builder();
+ method public androidx.wear.tiles.ActionBuilders.AndroidActivity.Builder addKeyToExtraMapping(String, androidx.wear.tiles.ActionBuilders.AndroidExtra);
+ method public androidx.wear.tiles.ActionBuilders.AndroidActivity build();
+ method public androidx.wear.tiles.ActionBuilders.AndroidActivity.Builder setClassName(String);
+ method public androidx.wear.tiles.ActionBuilders.AndroidActivity.Builder setPackageName(String);
+ }
+
+ public static final class ActionBuilders.AndroidBooleanExtra implements androidx.wear.tiles.ActionBuilders.AndroidExtra {
+ method public boolean getValue();
+ }
+
+ public static final class ActionBuilders.AndroidBooleanExtra.Builder implements androidx.wear.tiles.ActionBuilders.AndroidExtra.Builder {
+ ctor public ActionBuilders.AndroidBooleanExtra.Builder();
+ method public androidx.wear.tiles.ActionBuilders.AndroidBooleanExtra build();
+ method public androidx.wear.tiles.ActionBuilders.AndroidBooleanExtra.Builder setValue(boolean);
+ }
+
+ public static final class ActionBuilders.AndroidDoubleExtra implements androidx.wear.tiles.ActionBuilders.AndroidExtra {
+ method public double getValue();
+ }
+
+ public static final class ActionBuilders.AndroidDoubleExtra.Builder implements androidx.wear.tiles.ActionBuilders.AndroidExtra.Builder {
+ ctor public ActionBuilders.AndroidDoubleExtra.Builder();
+ method public androidx.wear.tiles.ActionBuilders.AndroidDoubleExtra build();
+ method public androidx.wear.tiles.ActionBuilders.AndroidDoubleExtra.Builder setValue(double);
+ }
+
+ public static interface ActionBuilders.AndroidExtra {
+ }
+
+ public static interface ActionBuilders.AndroidExtra.Builder {
+ method public androidx.wear.tiles.ActionBuilders.AndroidExtra build();
+ }
+
+ public static final class ActionBuilders.AndroidIntExtra implements androidx.wear.tiles.ActionBuilders.AndroidExtra {
+ method public int getValue();
+ }
+
+ public static final class ActionBuilders.AndroidIntExtra.Builder implements androidx.wear.tiles.ActionBuilders.AndroidExtra.Builder {
+ ctor public ActionBuilders.AndroidIntExtra.Builder();
+ method public androidx.wear.tiles.ActionBuilders.AndroidIntExtra build();
+ method public androidx.wear.tiles.ActionBuilders.AndroidIntExtra.Builder setValue(int);
+ }
+
+ public static final class ActionBuilders.AndroidLongExtra implements androidx.wear.tiles.ActionBuilders.AndroidExtra {
+ method public long getValue();
+ }
+
+ public static final class ActionBuilders.AndroidLongExtra.Builder implements androidx.wear.tiles.ActionBuilders.AndroidExtra.Builder {
+ ctor public ActionBuilders.AndroidLongExtra.Builder();
+ method public androidx.wear.tiles.ActionBuilders.AndroidLongExtra build();
+ method public androidx.wear.tiles.ActionBuilders.AndroidLongExtra.Builder setValue(long);
+ }
+
+ public static final class ActionBuilders.AndroidStringExtra implements androidx.wear.tiles.ActionBuilders.AndroidExtra {
+ method public String getValue();
+ }
+
+ public static final class ActionBuilders.AndroidStringExtra.Builder implements androidx.wear.tiles.ActionBuilders.AndroidExtra.Builder {
+ ctor public ActionBuilders.AndroidStringExtra.Builder();
+ method public androidx.wear.tiles.ActionBuilders.AndroidStringExtra build();
+ method public androidx.wear.tiles.ActionBuilders.AndroidStringExtra.Builder setValue(String);
+ }
+
+ public static final class ActionBuilders.LaunchAction implements androidx.wear.tiles.ActionBuilders.Action {
+ method public androidx.wear.tiles.ActionBuilders.AndroidActivity? getAndroidActivity();
+ }
+
+ public static final class ActionBuilders.LaunchAction.Builder implements androidx.wear.tiles.ActionBuilders.Action.Builder {
+ ctor public ActionBuilders.LaunchAction.Builder();
+ method public androidx.wear.tiles.ActionBuilders.LaunchAction build();
+ method public androidx.wear.tiles.ActionBuilders.LaunchAction.Builder setAndroidActivity(androidx.wear.tiles.ActionBuilders.AndroidActivity);
+ }
+
+ public static final class ActionBuilders.LoadAction implements androidx.wear.tiles.ActionBuilders.Action {
+ method public androidx.wear.tiles.StateBuilders.State? getRequestState();
+ }
+
+ public static final class ActionBuilders.LoadAction.Builder implements androidx.wear.tiles.ActionBuilders.Action.Builder {
+ ctor public ActionBuilders.LoadAction.Builder();
+ method public androidx.wear.tiles.ActionBuilders.LoadAction build();
+ method public androidx.wear.tiles.ActionBuilders.LoadAction.Builder setRequestState(androidx.wear.tiles.StateBuilders.State);
+ }
+
+ public final class ColorBuilders {
+ method public static androidx.wear.tiles.ColorBuilders.ColorProp argb(@ColorInt int);
+ }
+
+ public static final class ColorBuilders.ColorProp {
+ method @ColorInt public int getArgb();
+ }
+
+ public static final class ColorBuilders.ColorProp.Builder {
+ ctor public ColorBuilders.ColorProp.Builder();
+ method public androidx.wear.tiles.ColorBuilders.ColorProp build();
+ method public androidx.wear.tiles.ColorBuilders.ColorProp.Builder setArgb(@ColorInt int);
+ }
+
+ public final class DeviceParametersBuilders {
+ field public static final int DEVICE_PLATFORM_UNDEFINED = 0; // 0x0
+ field public static final int DEVICE_PLATFORM_WEAR_OS = 1; // 0x1
+ field public static final int SCREEN_SHAPE_RECT = 2; // 0x2
+ field public static final int SCREEN_SHAPE_ROUND = 1; // 0x1
+ field public static final int SCREEN_SHAPE_UNDEFINED = 0; // 0x0
+ }
+
+ public static final class DeviceParametersBuilders.DeviceParameters {
+ method public int getDevicePlatform();
+ method @FloatRange(from=0.0, fromInclusive=false, toInclusive=false) public float getScreenDensity();
+ method @Dimension(unit=androidx.annotation.Dimension.DP) public int getScreenHeightDp();
+ method public int getScreenShape();
+ method @Dimension(unit=androidx.annotation.Dimension.DP) public int getScreenWidthDp();
+ }
+
+ public static final class DeviceParametersBuilders.DeviceParameters.Builder {
+ ctor public DeviceParametersBuilders.DeviceParameters.Builder();
+ method public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters build();
+ method public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters.Builder setDevicePlatform(int);
+ method public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters.Builder setScreenDensity(@FloatRange(from=0.0, fromInclusive=false, toInclusive=false) float);
+ method public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters.Builder setScreenHeightDp(@Dimension(unit=androidx.annotation.Dimension.DP) int);
+ method public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters.Builder setScreenShape(int);
+ method public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters.Builder setScreenWidthDp(@Dimension(unit=androidx.annotation.Dimension.DP) int);
+ }
+
+ public final class DimensionBuilders {
+ method public static androidx.wear.tiles.DimensionBuilders.DegreesProp degrees(float);
+ method public static androidx.wear.tiles.DimensionBuilders.DpProp dp(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ method public static androidx.wear.tiles.DimensionBuilders.EmProp em(int);
+ method public static androidx.wear.tiles.DimensionBuilders.EmProp em(float);
+ method public static androidx.wear.tiles.DimensionBuilders.ExpandedDimensionProp expand();
+ method public static androidx.wear.tiles.DimensionBuilders.SpProp sp(@Dimension(unit=androidx.annotation.Dimension.SP) float);
+ method public static androidx.wear.tiles.DimensionBuilders.WrappedDimensionProp wrap();
+ }
+
+ public static interface DimensionBuilders.ContainerDimension {
+ }
+
+ public static interface DimensionBuilders.ContainerDimension.Builder {
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension build();
+ }
+
+ public static final class DimensionBuilders.DegreesProp {
+ method public float getValue();
+ }
+
+ public static final class DimensionBuilders.DegreesProp.Builder {
+ ctor public DimensionBuilders.DegreesProp.Builder();
+ method public androidx.wear.tiles.DimensionBuilders.DegreesProp build();
+ method public androidx.wear.tiles.DimensionBuilders.DegreesProp.Builder setValue(float);
+ }
+
+ public static final class DimensionBuilders.DpProp implements androidx.wear.tiles.DimensionBuilders.ContainerDimension androidx.wear.tiles.DimensionBuilders.ImageDimension androidx.wear.tiles.DimensionBuilders.SpacerDimension {
+ method @Dimension(unit=androidx.annotation.Dimension.DP) public float getValue();
+ }
+
+ public static final class DimensionBuilders.DpProp.Builder implements androidx.wear.tiles.DimensionBuilders.ContainerDimension.Builder androidx.wear.tiles.DimensionBuilders.ImageDimension.Builder androidx.wear.tiles.DimensionBuilders.SpacerDimension.Builder {
+ ctor public DimensionBuilders.DpProp.Builder();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp build();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp.Builder setValue(@Dimension(unit=androidx.annotation.Dimension.DP) float);
+ }
+
+ public static final class DimensionBuilders.EmProp {
+ method public float getValue();
+ }
+
+ public static final class DimensionBuilders.EmProp.Builder {
+ ctor public DimensionBuilders.EmProp.Builder();
+ method public androidx.wear.tiles.DimensionBuilders.EmProp build();
+ method public androidx.wear.tiles.DimensionBuilders.EmProp.Builder setValue(float);
+ }
+
+ public static final class DimensionBuilders.ExpandedDimensionProp implements androidx.wear.tiles.DimensionBuilders.ContainerDimension androidx.wear.tiles.DimensionBuilders.ImageDimension {
+ }
+
+ public static final class DimensionBuilders.ExpandedDimensionProp.Builder implements androidx.wear.tiles.DimensionBuilders.ContainerDimension.Builder androidx.wear.tiles.DimensionBuilders.ImageDimension.Builder {
+ ctor public DimensionBuilders.ExpandedDimensionProp.Builder();
+ method public androidx.wear.tiles.DimensionBuilders.ExpandedDimensionProp build();
+ }
+
+ public static interface DimensionBuilders.ImageDimension {
+ }
+
+ public static interface DimensionBuilders.ImageDimension.Builder {
+ method public androidx.wear.tiles.DimensionBuilders.ImageDimension build();
+ }
+
+ public static final class DimensionBuilders.ProportionalDimensionProp implements androidx.wear.tiles.DimensionBuilders.ImageDimension {
+ method @IntRange(from=0) public int getAspectRatioHeight();
+ method @IntRange(from=0) public int getAspectRatioWidth();
+ }
+
+ public static final class DimensionBuilders.ProportionalDimensionProp.Builder implements androidx.wear.tiles.DimensionBuilders.ImageDimension.Builder {
+ ctor public DimensionBuilders.ProportionalDimensionProp.Builder();
+ method public androidx.wear.tiles.DimensionBuilders.ProportionalDimensionProp build();
+ method public androidx.wear.tiles.DimensionBuilders.ProportionalDimensionProp.Builder setAspectRatioHeight(@IntRange(from=0) int);
+ method public androidx.wear.tiles.DimensionBuilders.ProportionalDimensionProp.Builder setAspectRatioWidth(@IntRange(from=0) int);
+ }
+
+ public static final class DimensionBuilders.SpProp {
+ method @Dimension(unit=androidx.annotation.Dimension.SP) public float getValue();
+ }
+
+ public static final class DimensionBuilders.SpProp.Builder {
+ ctor public DimensionBuilders.SpProp.Builder();
+ method public androidx.wear.tiles.DimensionBuilders.SpProp build();
+ method public androidx.wear.tiles.DimensionBuilders.SpProp.Builder setValue(@Dimension(unit=androidx.annotation.Dimension.SP) float);
+ }
+
+ public static interface DimensionBuilders.SpacerDimension {
+ }
+
+ public static interface DimensionBuilders.SpacerDimension.Builder {
+ method public androidx.wear.tiles.DimensionBuilders.SpacerDimension build();
+ }
+
+ public static final class DimensionBuilders.WrappedDimensionProp implements androidx.wear.tiles.DimensionBuilders.ContainerDimension {
+ }
+
+ public static final class DimensionBuilders.WrappedDimensionProp.Builder implements androidx.wear.tiles.DimensionBuilders.ContainerDimension.Builder {
+ ctor public DimensionBuilders.WrappedDimensionProp.Builder();
+ method public androidx.wear.tiles.DimensionBuilders.WrappedDimensionProp build();
+ }
+
+ public final class EventBuilders {
+ }
+
+ public static final class EventBuilders.TileAddEvent {
+ }
+
+ public static final class EventBuilders.TileAddEvent.Builder {
+ ctor public EventBuilders.TileAddEvent.Builder();
+ method public androidx.wear.tiles.EventBuilders.TileAddEvent build();
+ }
+
+ public static final class EventBuilders.TileEnterEvent {
+ }
+
+ public static final class EventBuilders.TileEnterEvent.Builder {
+ ctor public EventBuilders.TileEnterEvent.Builder();
+ method public androidx.wear.tiles.EventBuilders.TileEnterEvent build();
+ }
+
+ public static final class EventBuilders.TileLeaveEvent {
+ }
+
+ public static final class EventBuilders.TileLeaveEvent.Builder {
+ ctor public EventBuilders.TileLeaveEvent.Builder();
+ method public androidx.wear.tiles.EventBuilders.TileLeaveEvent build();
+ }
+
+ public static final class EventBuilders.TileRemoveEvent {
+ }
+
+ public static final class EventBuilders.TileRemoveEvent.Builder {
+ ctor public EventBuilders.TileRemoveEvent.Builder();
+ method public androidx.wear.tiles.EventBuilders.TileRemoveEvent build();
+ }
+
+ public final class LayoutElementBuilders {
+ field public static final int ARC_ANCHOR_CENTER = 2; // 0x2
+ field public static final int ARC_ANCHOR_END = 3; // 0x3
+ field public static final int ARC_ANCHOR_START = 1; // 0x1
+ field public static final int ARC_ANCHOR_UNDEFINED = 0; // 0x0
+ field public static final int CONTENT_SCALE_MODE_CROP = 2; // 0x2
+ field public static final int CONTENT_SCALE_MODE_FILL_BOUNDS = 3; // 0x3
+ field public static final int CONTENT_SCALE_MODE_FIT = 1; // 0x1
+ field public static final int CONTENT_SCALE_MODE_UNDEFINED = 0; // 0x0
+ field public static final int FONT_VARIANT_BODY = 2; // 0x2
+ field public static final int FONT_VARIANT_TITLE = 1; // 0x1
+ field public static final int FONT_VARIANT_UNDEFINED = 0; // 0x0
+ field public static final int FONT_WEIGHT_BOLD = 700; // 0x2bc
+ field public static final int FONT_WEIGHT_NORMAL = 400; // 0x190
+ field public static final int FONT_WEIGHT_UNDEFINED = 0; // 0x0
+ field public static final int HORIZONTAL_ALIGN_CENTER = 2; // 0x2
+ field public static final int HORIZONTAL_ALIGN_END = 5; // 0x5
+ field public static final int HORIZONTAL_ALIGN_LEFT = 1; // 0x1
+ field public static final int HORIZONTAL_ALIGN_RIGHT = 3; // 0x3
+ field public static final int HORIZONTAL_ALIGN_START = 4; // 0x4
+ field public static final int HORIZONTAL_ALIGN_UNDEFINED = 0; // 0x0
+ field public static final int SPAN_VERTICAL_ALIGN_BOTTOM = 1; // 0x1
+ field public static final int SPAN_VERTICAL_ALIGN_TEXT_BASELINE = 2; // 0x2
+ field public static final int SPAN_VERTICAL_ALIGN_UNDEFINED = 0; // 0x0
+ field public static final int TEXT_ALIGN_CENTER = 2; // 0x2
+ field public static final int TEXT_ALIGN_END = 3; // 0x3
+ field public static final int TEXT_ALIGN_START = 1; // 0x1
+ field public static final int TEXT_ALIGN_UNDEFINED = 0; // 0x0
+ field public static final int TEXT_OVERFLOW_ELLIPSIZE_END = 2; // 0x2
+ field public static final int TEXT_OVERFLOW_TRUNCATE = 1; // 0x1
+ field public static final int TEXT_OVERFLOW_UNDEFINED = 0; // 0x0
+ field public static final int VERTICAL_ALIGN_BOTTOM = 3; // 0x3
+ field public static final int VERTICAL_ALIGN_CENTER = 2; // 0x2
+ field public static final int VERTICAL_ALIGN_TOP = 1; // 0x1
+ field public static final int VERTICAL_ALIGN_UNDEFINED = 0; // 0x0
+ }
+
+ public static final class LayoutElementBuilders.Arc implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public androidx.wear.tiles.DimensionBuilders.DegreesProp? getAnchorAngle();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcAnchorTypeProp? getAnchorType();
+ method public java.util.List<androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement!> getContents();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers? getModifiers();
+ method public androidx.wear.tiles.LayoutElementBuilders.VerticalAlignmentProp? getVerticalAlign();
+ }
+
+ public static final class LayoutElementBuilders.Arc.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public LayoutElementBuilders.Arc.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Arc.Builder addContent(androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.Arc build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Arc.Builder setAnchorAngle(androidx.wear.tiles.DimensionBuilders.DegreesProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Arc.Builder setAnchorType(androidx.wear.tiles.LayoutElementBuilders.ArcAnchorTypeProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Arc.Builder setAnchorType(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Arc.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.Arc.Builder setVerticalAlign(androidx.wear.tiles.LayoutElementBuilders.VerticalAlignmentProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Arc.Builder setVerticalAlign(int);
+ }
+
+ public static final class LayoutElementBuilders.ArcAdapter implements androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement {
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getContent();
+ method public androidx.wear.tiles.TypeBuilders.BoolProp? getRotateContents();
+ }
+
+ public static final class LayoutElementBuilders.ArcAdapter.Builder implements androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement.Builder {
+ ctor public LayoutElementBuilders.ArcAdapter.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcAdapter build();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcAdapter.Builder setContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcAdapter.Builder setRotateContents(androidx.wear.tiles.TypeBuilders.BoolProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcAdapter.Builder setRotateContents(boolean);
+ }
+
+ public static final class LayoutElementBuilders.ArcAnchorTypeProp {
+ method public int getValue();
+ }
+
+ public static final class LayoutElementBuilders.ArcAnchorTypeProp.Builder {
+ ctor public LayoutElementBuilders.ArcAnchorTypeProp.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcAnchorTypeProp build();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcAnchorTypeProp.Builder setValue(int);
+ }
+
+ public static interface LayoutElementBuilders.ArcLayoutElement {
+ }
+
+ public static interface LayoutElementBuilders.ArcLayoutElement.Builder {
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement build();
+ }
+
+ public static final class LayoutElementBuilders.ArcLine implements androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement {
+ method public androidx.wear.tiles.ColorBuilders.ColorProp? getColor();
+ method public androidx.wear.tiles.DimensionBuilders.DegreesProp? getLength();
+ method public androidx.wear.tiles.ModifiersBuilders.ArcModifiers? getModifiers();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getThickness();
+ }
+
+ public static final class LayoutElementBuilders.ArcLine.Builder implements androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement.Builder {
+ ctor public LayoutElementBuilders.ArcLine.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcLine build();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcLine.Builder setColor(androidx.wear.tiles.ColorBuilders.ColorProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcLine.Builder setLength(androidx.wear.tiles.DimensionBuilders.DegreesProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcLine.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.ArcModifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcLine.Builder setThickness(androidx.wear.tiles.DimensionBuilders.DpProp);
+ }
+
+ public static final class LayoutElementBuilders.ArcSpacer implements androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement {
+ method public androidx.wear.tiles.DimensionBuilders.DegreesProp? getLength();
+ method public androidx.wear.tiles.ModifiersBuilders.ArcModifiers? getModifiers();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getThickness();
+ }
+
+ public static final class LayoutElementBuilders.ArcSpacer.Builder implements androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement.Builder {
+ ctor public LayoutElementBuilders.ArcSpacer.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcSpacer build();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcSpacer.Builder setLength(androidx.wear.tiles.DimensionBuilders.DegreesProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcSpacer.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.ArcModifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcSpacer.Builder setThickness(androidx.wear.tiles.DimensionBuilders.DpProp);
+ }
+
+ public static final class LayoutElementBuilders.ArcText implements androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement {
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle? getFontStyle();
+ method public androidx.wear.tiles.ModifiersBuilders.ArcModifiers? getModifiers();
+ method public androidx.wear.tiles.TypeBuilders.StringProp? getText();
+ }
+
+ public static final class LayoutElementBuilders.ArcText.Builder implements androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement.Builder {
+ ctor public LayoutElementBuilders.ArcText.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcText build();
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcText.Builder setFontStyle(androidx.wear.tiles.LayoutElementBuilders.FontStyle);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcText.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.ArcModifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcText.Builder setText(androidx.wear.tiles.TypeBuilders.StringProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.ArcText.Builder setText(String);
+ }
+
+ public static final class LayoutElementBuilders.Box implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public java.util.List<androidx.wear.tiles.LayoutElementBuilders.LayoutElement!> getContents();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension? getHeight();
+ method public androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignmentProp? getHorizontalAlignment();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers? getModifiers();
+ method public androidx.wear.tiles.LayoutElementBuilders.VerticalAlignmentProp? getVerticalAlignment();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension? getWidth();
+ }
+
+ public static final class LayoutElementBuilders.Box.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public LayoutElementBuilders.Box.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Box.Builder addContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.Box build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Box.Builder setHeight(androidx.wear.tiles.DimensionBuilders.ContainerDimension);
+ method public androidx.wear.tiles.LayoutElementBuilders.Box.Builder setHorizontalAlignment(androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignmentProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Box.Builder setHorizontalAlignment(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Box.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.Box.Builder setVerticalAlignment(androidx.wear.tiles.LayoutElementBuilders.VerticalAlignmentProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Box.Builder setVerticalAlignment(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Box.Builder setWidth(androidx.wear.tiles.DimensionBuilders.ContainerDimension);
+ }
+
+ public static final class LayoutElementBuilders.ColorFilter {
+ method public androidx.wear.tiles.ColorBuilders.ColorProp? getTint();
+ }
+
+ public static final class LayoutElementBuilders.ColorFilter.Builder {
+ ctor public LayoutElementBuilders.ColorFilter.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.ColorFilter build();
+ method public androidx.wear.tiles.LayoutElementBuilders.ColorFilter.Builder setTint(androidx.wear.tiles.ColorBuilders.ColorProp);
+ }
+
+ public static final class LayoutElementBuilders.Column implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public java.util.List<androidx.wear.tiles.LayoutElementBuilders.LayoutElement!> getContents();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension? getHeight();
+ method public androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignmentProp? getHorizontalAlignment();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers? getModifiers();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension? getWidth();
+ }
+
+ public static final class LayoutElementBuilders.Column.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public LayoutElementBuilders.Column.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Column.Builder addContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.Column build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Column.Builder setHeight(androidx.wear.tiles.DimensionBuilders.ContainerDimension);
+ method public androidx.wear.tiles.LayoutElementBuilders.Column.Builder setHorizontalAlignment(androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignmentProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Column.Builder setHorizontalAlignment(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Column.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.Column.Builder setWidth(androidx.wear.tiles.DimensionBuilders.ContainerDimension);
+ }
+
+ public static final class LayoutElementBuilders.ContentScaleModeProp {
+ method public int getValue();
+ }
+
+ public static final class LayoutElementBuilders.ContentScaleModeProp.Builder {
+ ctor public LayoutElementBuilders.ContentScaleModeProp.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.ContentScaleModeProp build();
+ method public androidx.wear.tiles.LayoutElementBuilders.ContentScaleModeProp.Builder setValue(int);
+ }
+
+ public static final class LayoutElementBuilders.FontStyle {
+ method public androidx.wear.tiles.ColorBuilders.ColorProp? getColor();
+ method public androidx.wear.tiles.TypeBuilders.BoolProp? getItalic();
+ method public androidx.wear.tiles.DimensionBuilders.EmProp? getLetterSpacing();
+ method public androidx.wear.tiles.DimensionBuilders.SpProp? getSize();
+ method public androidx.wear.tiles.TypeBuilders.BoolProp? getUnderline();
+ method public androidx.wear.tiles.LayoutElementBuilders.FontWeightProp? getWeight();
+ }
+
+ public static final class LayoutElementBuilders.FontStyle.Builder {
+ ctor public LayoutElementBuilders.FontStyle.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle build();
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setColor(androidx.wear.tiles.ColorBuilders.ColorProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setItalic(androidx.wear.tiles.TypeBuilders.BoolProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setItalic(boolean);
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setLetterSpacing(androidx.wear.tiles.DimensionBuilders.EmProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setSize(androidx.wear.tiles.DimensionBuilders.SpProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setUnderline(androidx.wear.tiles.TypeBuilders.BoolProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setUnderline(boolean);
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setWeight(androidx.wear.tiles.LayoutElementBuilders.FontWeightProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder setWeight(int);
+ }
+
+ public static class LayoutElementBuilders.FontStyles {
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder body1(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder body2(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder button(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder caption1(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder caption2(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder display1(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder display2(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder display3(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder title1(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder title2(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public static androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder title3(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ }
+
+ public static final class LayoutElementBuilders.FontWeightProp {
+ method public int getValue();
+ }
+
+ public static final class LayoutElementBuilders.FontWeightProp.Builder {
+ ctor public LayoutElementBuilders.FontWeightProp.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.FontWeightProp build();
+ method public androidx.wear.tiles.LayoutElementBuilders.FontWeightProp.Builder setValue(int);
+ }
+
+ public static final class LayoutElementBuilders.HorizontalAlignmentProp {
+ method public int getValue();
+ }
+
+ public static final class LayoutElementBuilders.HorizontalAlignmentProp.Builder {
+ ctor public LayoutElementBuilders.HorizontalAlignmentProp.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignmentProp build();
+ method public androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignmentProp.Builder setValue(int);
+ }
+
+ public static final class LayoutElementBuilders.Image implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public androidx.wear.tiles.LayoutElementBuilders.ColorFilter? getColorFilter();
+ method public androidx.wear.tiles.LayoutElementBuilders.ContentScaleModeProp? getContentScaleMode();
+ method public androidx.wear.tiles.DimensionBuilders.ImageDimension? getHeight();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers? getModifiers();
+ method public androidx.wear.tiles.TypeBuilders.StringProp? getResourceId();
+ method public androidx.wear.tiles.DimensionBuilders.ImageDimension? getWidth();
+ }
+
+ public static final class LayoutElementBuilders.Image.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public LayoutElementBuilders.Image.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Image build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Image.Builder setColorFilter(androidx.wear.tiles.LayoutElementBuilders.ColorFilter);
+ method public androidx.wear.tiles.LayoutElementBuilders.Image.Builder setContentScaleMode(androidx.wear.tiles.LayoutElementBuilders.ContentScaleModeProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Image.Builder setContentScaleMode(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Image.Builder setHeight(androidx.wear.tiles.DimensionBuilders.ImageDimension);
+ method public androidx.wear.tiles.LayoutElementBuilders.Image.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.Image.Builder setResourceId(androidx.wear.tiles.TypeBuilders.StringProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Image.Builder setResourceId(String);
+ method public androidx.wear.tiles.LayoutElementBuilders.Image.Builder setWidth(androidx.wear.tiles.DimensionBuilders.ImageDimension);
+ }
+
+ public static final class LayoutElementBuilders.Layout {
+ method public static androidx.wear.tiles.LayoutElementBuilders.Layout fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement? getRoot();
+ }
+
+ public static final class LayoutElementBuilders.Layout.Builder {
+ ctor public LayoutElementBuilders.Layout.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Layout build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Layout.Builder setRoot(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ }
+
+ public static interface LayoutElementBuilders.LayoutElement {
+ }
+
+ public static interface LayoutElementBuilders.LayoutElement.Builder {
+ method public androidx.wear.tiles.LayoutElementBuilders.LayoutElement build();
+ }
+
+ public static final class LayoutElementBuilders.Row implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public java.util.List<androidx.wear.tiles.LayoutElementBuilders.LayoutElement!> getContents();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension? getHeight();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers? getModifiers();
+ method public androidx.wear.tiles.LayoutElementBuilders.VerticalAlignmentProp? getVerticalAlignment();
+ method public androidx.wear.tiles.DimensionBuilders.ContainerDimension? getWidth();
+ }
+
+ public static final class LayoutElementBuilders.Row.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public LayoutElementBuilders.Row.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Row.Builder addContent(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.Row build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Row.Builder setHeight(androidx.wear.tiles.DimensionBuilders.ContainerDimension);
+ method public androidx.wear.tiles.LayoutElementBuilders.Row.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.Row.Builder setVerticalAlignment(androidx.wear.tiles.LayoutElementBuilders.VerticalAlignmentProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Row.Builder setVerticalAlignment(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Row.Builder setWidth(androidx.wear.tiles.DimensionBuilders.ContainerDimension);
+ }
+
+ public static final class LayoutElementBuilders.Spacer implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public androidx.wear.tiles.DimensionBuilders.SpacerDimension? getHeight();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers? getModifiers();
+ method public androidx.wear.tiles.DimensionBuilders.SpacerDimension? getWidth();
+ }
+
+ public static final class LayoutElementBuilders.Spacer.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public LayoutElementBuilders.Spacer.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Spacer build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Spacer.Builder setHeight(androidx.wear.tiles.DimensionBuilders.SpacerDimension);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spacer.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spacer.Builder setWidth(androidx.wear.tiles.DimensionBuilders.SpacerDimension);
+ }
+
+ public static interface LayoutElementBuilders.Span {
+ }
+
+ public static interface LayoutElementBuilders.Span.Builder {
+ method public androidx.wear.tiles.LayoutElementBuilders.Span build();
+ }
+
+ public static final class LayoutElementBuilders.SpanImage implements androidx.wear.tiles.LayoutElementBuilders.Span {
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanVerticalAlignmentProp? getAlignment();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getHeight();
+ method public androidx.wear.tiles.ModifiersBuilders.SpanModifiers? getModifiers();
+ method public androidx.wear.tiles.TypeBuilders.StringProp? getResourceId();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getWidth();
+ }
+
+ public static final class LayoutElementBuilders.SpanImage.Builder implements androidx.wear.tiles.LayoutElementBuilders.Span.Builder {
+ ctor public LayoutElementBuilders.SpanImage.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanImage build();
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanImage.Builder setAlignment(androidx.wear.tiles.LayoutElementBuilders.SpanVerticalAlignmentProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanImage.Builder setAlignment(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanImage.Builder setHeight(androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanImage.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.SpanModifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanImage.Builder setResourceId(androidx.wear.tiles.TypeBuilders.StringProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanImage.Builder setResourceId(String);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanImage.Builder setWidth(androidx.wear.tiles.DimensionBuilders.DpProp);
+ }
+
+ public static final class LayoutElementBuilders.SpanText implements androidx.wear.tiles.LayoutElementBuilders.Span {
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle? getFontStyle();
+ method public androidx.wear.tiles.ModifiersBuilders.SpanModifiers? getModifiers();
+ method public androidx.wear.tiles.TypeBuilders.StringProp? getText();
+ }
+
+ public static final class LayoutElementBuilders.SpanText.Builder implements androidx.wear.tiles.LayoutElementBuilders.Span.Builder {
+ ctor public LayoutElementBuilders.SpanText.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanText build();
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanText.Builder setFontStyle(androidx.wear.tiles.LayoutElementBuilders.FontStyle);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanText.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.SpanModifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanText.Builder setText(androidx.wear.tiles.TypeBuilders.StringProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanText.Builder setText(String);
+ }
+
+ public static final class LayoutElementBuilders.SpanVerticalAlignmentProp {
+ method public int getValue();
+ }
+
+ public static final class LayoutElementBuilders.SpanVerticalAlignmentProp.Builder {
+ ctor public LayoutElementBuilders.SpanVerticalAlignmentProp.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanVerticalAlignmentProp build();
+ method public androidx.wear.tiles.LayoutElementBuilders.SpanVerticalAlignmentProp.Builder setValue(int);
+ }
+
+ public static final class LayoutElementBuilders.Spannable implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public androidx.wear.tiles.DimensionBuilders.SpProp? getLineHeight();
+ method public androidx.wear.tiles.TypeBuilders.Int32Prop? getMaxLines();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers? getModifiers();
+ method public androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignmentProp? getMultilineAlignment();
+ method public androidx.wear.tiles.LayoutElementBuilders.TextOverflowProp? getOverflow();
+ method public java.util.List<androidx.wear.tiles.LayoutElementBuilders.Span!> getSpans();
+ }
+
+ public static final class LayoutElementBuilders.Spannable.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public LayoutElementBuilders.Spannable.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder addSpan(androidx.wear.tiles.LayoutElementBuilders.Span);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder setLineHeight(androidx.wear.tiles.DimensionBuilders.SpProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder setMaxLines(androidx.wear.tiles.TypeBuilders.Int32Prop);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder setMaxLines(@IntRange(from=1) int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder setMultilineAlignment(androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignmentProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder setMultilineAlignment(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder setOverflow(androidx.wear.tiles.LayoutElementBuilders.TextOverflowProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Spannable.Builder setOverflow(int);
+ }
+
+ public static final class LayoutElementBuilders.Text implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
+ method public androidx.wear.tiles.LayoutElementBuilders.FontStyle? getFontStyle();
+ method public androidx.wear.tiles.DimensionBuilders.SpProp? getLineHeight();
+ method public androidx.wear.tiles.TypeBuilders.Int32Prop? getMaxLines();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers? getModifiers();
+ method public androidx.wear.tiles.LayoutElementBuilders.TextAlignmentProp? getMultilineAlignment();
+ method public androidx.wear.tiles.LayoutElementBuilders.TextOverflowProp? getOverflow();
+ method public androidx.wear.tiles.TypeBuilders.StringProp? getText();
+ }
+
+ public static final class LayoutElementBuilders.Text.Builder implements androidx.wear.tiles.LayoutElementBuilders.LayoutElement.Builder {
+ ctor public LayoutElementBuilders.Text.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.Text build();
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setFontStyle(androidx.wear.tiles.LayoutElementBuilders.FontStyle);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setLineHeight(androidx.wear.tiles.DimensionBuilders.SpProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setMaxLines(androidx.wear.tiles.TypeBuilders.Int32Prop);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setMaxLines(@IntRange(from=1) int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setModifiers(androidx.wear.tiles.ModifiersBuilders.Modifiers);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setMultilineAlignment(androidx.wear.tiles.LayoutElementBuilders.TextAlignmentProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setMultilineAlignment(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setOverflow(androidx.wear.tiles.LayoutElementBuilders.TextOverflowProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setOverflow(int);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setText(androidx.wear.tiles.TypeBuilders.StringProp);
+ method public androidx.wear.tiles.LayoutElementBuilders.Text.Builder setText(String);
+ }
+
+ public static final class LayoutElementBuilders.TextAlignmentProp {
+ method public int getValue();
+ }
+
+ public static final class LayoutElementBuilders.TextAlignmentProp.Builder {
+ ctor public LayoutElementBuilders.TextAlignmentProp.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.TextAlignmentProp build();
+ method public androidx.wear.tiles.LayoutElementBuilders.TextAlignmentProp.Builder setValue(int);
+ }
+
+ public static final class LayoutElementBuilders.TextOverflowProp {
+ method public int getValue();
+ }
+
+ public static final class LayoutElementBuilders.TextOverflowProp.Builder {
+ ctor public LayoutElementBuilders.TextOverflowProp.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.TextOverflowProp build();
+ method public androidx.wear.tiles.LayoutElementBuilders.TextOverflowProp.Builder setValue(int);
+ }
+
+ public static final class LayoutElementBuilders.VerticalAlignmentProp {
+ method public int getValue();
+ }
+
+ public static final class LayoutElementBuilders.VerticalAlignmentProp.Builder {
+ ctor public LayoutElementBuilders.VerticalAlignmentProp.Builder();
+ method public androidx.wear.tiles.LayoutElementBuilders.VerticalAlignmentProp build();
+ method public androidx.wear.tiles.LayoutElementBuilders.VerticalAlignmentProp.Builder setValue(int);
+ }
+
+ public final class ModifiersBuilders {
+ }
+
+ public static final class ModifiersBuilders.ArcModifiers {
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable? getClickable();
+ method public androidx.wear.tiles.ModifiersBuilders.Semantics? getSemantics();
+ }
+
+ public static final class ModifiersBuilders.ArcModifiers.Builder {
+ ctor public ModifiersBuilders.ArcModifiers.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.ArcModifiers build();
+ method public androidx.wear.tiles.ModifiersBuilders.ArcModifiers.Builder setClickable(androidx.wear.tiles.ModifiersBuilders.Clickable);
+ method public androidx.wear.tiles.ModifiersBuilders.ArcModifiers.Builder setSemantics(androidx.wear.tiles.ModifiersBuilders.Semantics);
+ }
+
+ public static final class ModifiersBuilders.Background {
+ method public androidx.wear.tiles.ColorBuilders.ColorProp? getColor();
+ method public androidx.wear.tiles.ModifiersBuilders.Corner? getCorner();
+ }
+
+ public static final class ModifiersBuilders.Background.Builder {
+ ctor public ModifiersBuilders.Background.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.Background build();
+ method public androidx.wear.tiles.ModifiersBuilders.Background.Builder setColor(androidx.wear.tiles.ColorBuilders.ColorProp);
+ method public androidx.wear.tiles.ModifiersBuilders.Background.Builder setCorner(androidx.wear.tiles.ModifiersBuilders.Corner);
+ }
+
+ public static final class ModifiersBuilders.Border {
+ method public androidx.wear.tiles.ColorBuilders.ColorProp? getColor();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getWidth();
+ }
+
+ public static final class ModifiersBuilders.Border.Builder {
+ ctor public ModifiersBuilders.Border.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.Border build();
+ method public androidx.wear.tiles.ModifiersBuilders.Border.Builder setColor(androidx.wear.tiles.ColorBuilders.ColorProp);
+ method public androidx.wear.tiles.ModifiersBuilders.Border.Builder setWidth(androidx.wear.tiles.DimensionBuilders.DpProp);
+ }
+
+ public static final class ModifiersBuilders.Clickable {
+ method public String getId();
+ method public androidx.wear.tiles.ActionBuilders.Action? getOnClick();
+ }
+
+ public static final class ModifiersBuilders.Clickable.Builder {
+ ctor public ModifiersBuilders.Clickable.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable build();
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable.Builder setId(String);
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable.Builder setOnClick(androidx.wear.tiles.ActionBuilders.Action);
+ }
+
+ public static final class ModifiersBuilders.Corner {
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getRadius();
+ }
+
+ public static final class ModifiersBuilders.Corner.Builder {
+ ctor public ModifiersBuilders.Corner.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.Corner build();
+ method public androidx.wear.tiles.ModifiersBuilders.Corner.Builder setRadius(androidx.wear.tiles.DimensionBuilders.DpProp);
+ }
+
+ public static final class ModifiersBuilders.ElementMetadata {
+ method public byte[] getTagData();
+ }
+
+ public static final class ModifiersBuilders.ElementMetadata.Builder {
+ ctor public ModifiersBuilders.ElementMetadata.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.ElementMetadata build();
+ method public androidx.wear.tiles.ModifiersBuilders.ElementMetadata.Builder setTagData(byte[]);
+ }
+
+ public static final class ModifiersBuilders.Modifiers {
+ method public androidx.wear.tiles.ModifiersBuilders.Background? getBackground();
+ method public androidx.wear.tiles.ModifiersBuilders.Border? getBorder();
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable? getClickable();
+ method public androidx.wear.tiles.ModifiersBuilders.ElementMetadata? getMetadata();
+ method public androidx.wear.tiles.ModifiersBuilders.Padding? getPadding();
+ method public androidx.wear.tiles.ModifiersBuilders.Semantics? getSemantics();
+ }
+
+ public static final class ModifiersBuilders.Modifiers.Builder {
+ ctor public ModifiersBuilders.Modifiers.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers build();
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers.Builder setBackground(androidx.wear.tiles.ModifiersBuilders.Background);
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers.Builder setBorder(androidx.wear.tiles.ModifiersBuilders.Border);
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers.Builder setClickable(androidx.wear.tiles.ModifiersBuilders.Clickable);
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers.Builder setMetadata(androidx.wear.tiles.ModifiersBuilders.ElementMetadata);
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers.Builder setPadding(androidx.wear.tiles.ModifiersBuilders.Padding);
+ method public androidx.wear.tiles.ModifiersBuilders.Modifiers.Builder setSemantics(androidx.wear.tiles.ModifiersBuilders.Semantics);
+ }
+
+ public static final class ModifiersBuilders.Padding {
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getBottom();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getEnd();
+ method public androidx.wear.tiles.TypeBuilders.BoolProp? getRtlAware();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getStart();
+ method public androidx.wear.tiles.DimensionBuilders.DpProp? getTop();
+ }
+
+ public static final class ModifiersBuilders.Padding.Builder {
+ ctor public ModifiersBuilders.Padding.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.Padding build();
+ method public androidx.wear.tiles.ModifiersBuilders.Padding.Builder setAll(androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public androidx.wear.tiles.ModifiersBuilders.Padding.Builder setBottom(androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public androidx.wear.tiles.ModifiersBuilders.Padding.Builder setEnd(androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public androidx.wear.tiles.ModifiersBuilders.Padding.Builder setRtlAware(androidx.wear.tiles.TypeBuilders.BoolProp);
+ method public androidx.wear.tiles.ModifiersBuilders.Padding.Builder setRtlAware(boolean);
+ method public androidx.wear.tiles.ModifiersBuilders.Padding.Builder setStart(androidx.wear.tiles.DimensionBuilders.DpProp);
+ method public androidx.wear.tiles.ModifiersBuilders.Padding.Builder setTop(androidx.wear.tiles.DimensionBuilders.DpProp);
+ }
+
+ public static final class ModifiersBuilders.Semantics {
+ method public String getContentDescription();
+ }
+
+ public static final class ModifiersBuilders.Semantics.Builder {
+ ctor public ModifiersBuilders.Semantics.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.Semantics build();
+ method public androidx.wear.tiles.ModifiersBuilders.Semantics.Builder setContentDescription(String);
+ }
+
+ public static final class ModifiersBuilders.SpanModifiers {
+ method public androidx.wear.tiles.ModifiersBuilders.Clickable? getClickable();
+ }
+
+ public static final class ModifiersBuilders.SpanModifiers.Builder {
+ ctor public ModifiersBuilders.SpanModifiers.Builder();
+ method public androidx.wear.tiles.ModifiersBuilders.SpanModifiers build();
+ method public androidx.wear.tiles.ModifiersBuilders.SpanModifiers.Builder setClickable(androidx.wear.tiles.ModifiersBuilders.Clickable);
+ }
+
+ public final class RequestBuilders {
+ }
+
+ public static final class RequestBuilders.ResourcesRequest {
+ method public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters? getDeviceParameters();
+ method public java.util.List<java.lang.String!> getResourceIds();
+ method public String getVersion();
+ }
+
+ public static final class RequestBuilders.ResourcesRequest.Builder {
+ ctor public RequestBuilders.ResourcesRequest.Builder();
+ method public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder addResourceId(String);
+ method public androidx.wear.tiles.RequestBuilders.ResourcesRequest build();
+ method public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder setDeviceParameters(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public androidx.wear.tiles.RequestBuilders.ResourcesRequest.Builder setVersion(String);
+ }
+
+ public static final class RequestBuilders.TileRequest {
+ method public androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters? getDeviceParameters();
+ method public androidx.wear.tiles.StateBuilders.State? getState();
+ }
+
+ public static final class RequestBuilders.TileRequest.Builder {
+ ctor public RequestBuilders.TileRequest.Builder();
+ method public androidx.wear.tiles.RequestBuilders.TileRequest build();
+ method public androidx.wear.tiles.RequestBuilders.TileRequest.Builder setDeviceParameters(androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters);
+ method public androidx.wear.tiles.RequestBuilders.TileRequest.Builder setState(androidx.wear.tiles.StateBuilders.State);
+ }
+
+ public final class ResourceBuilders {
+ field public static final int IMAGE_FORMAT_RGB_565 = 1; // 0x1
+ field public static final int IMAGE_FORMAT_UNDEFINED = 0; // 0x0
+ }
+
+ public static final class ResourceBuilders.AndroidImageResourceByResId {
+ method @DrawableRes public int getResourceId();
+ }
+
+ public static final class ResourceBuilders.AndroidImageResourceByResId.Builder {
+ ctor public ResourceBuilders.AndroidImageResourceByResId.Builder();
+ method public androidx.wear.tiles.ResourceBuilders.AndroidImageResourceByResId build();
+ method public androidx.wear.tiles.ResourceBuilders.AndroidImageResourceByResId.Builder setResourceId(@DrawableRes int);
+ }
+
+ public static final class ResourceBuilders.ImageResource {
+ method public androidx.wear.tiles.ResourceBuilders.AndroidImageResourceByResId? getAndroidResourceByResId();
+ method public androidx.wear.tiles.ResourceBuilders.InlineImageResource? getInlineResource();
+ }
+
+ public static final class ResourceBuilders.ImageResource.Builder {
+ ctor public ResourceBuilders.ImageResource.Builder();
+ method public androidx.wear.tiles.ResourceBuilders.ImageResource build();
+ method public androidx.wear.tiles.ResourceBuilders.ImageResource.Builder setAndroidResourceByResId(androidx.wear.tiles.ResourceBuilders.AndroidImageResourceByResId);
+ method public androidx.wear.tiles.ResourceBuilders.ImageResource.Builder setInlineResource(androidx.wear.tiles.ResourceBuilders.InlineImageResource);
+ }
+
+ public static final class ResourceBuilders.InlineImageResource {
+ method public byte[] getData();
+ method public int getFormat();
+ method @Dimension(unit=androidx.annotation.Dimension.PX) public int getHeightPx();
+ method @Dimension(unit=androidx.annotation.Dimension.PX) public int getWidthPx();
+ }
+
+ public static final class ResourceBuilders.InlineImageResource.Builder {
+ ctor public ResourceBuilders.InlineImageResource.Builder();
+ method public androidx.wear.tiles.ResourceBuilders.InlineImageResource build();
+ method public androidx.wear.tiles.ResourceBuilders.InlineImageResource.Builder setData(byte[]);
+ method public androidx.wear.tiles.ResourceBuilders.InlineImageResource.Builder setFormat(int);
+ method public androidx.wear.tiles.ResourceBuilders.InlineImageResource.Builder setHeightPx(@Dimension(unit=androidx.annotation.Dimension.PX) int);
+ method public androidx.wear.tiles.ResourceBuilders.InlineImageResource.Builder setWidthPx(@Dimension(unit=androidx.annotation.Dimension.PX) int);
+ }
+
+ public static final class ResourceBuilders.Resources {
+ method public java.util.Map<java.lang.String!,androidx.wear.tiles.ResourceBuilders.ImageResource!> getIdToImageMapping();
+ method public String getVersion();
+ }
+
+ public static final class ResourceBuilders.Resources.Builder {
+ ctor public ResourceBuilders.Resources.Builder();
+ method public androidx.wear.tiles.ResourceBuilders.Resources.Builder addIdToImageMapping(String, androidx.wear.tiles.ResourceBuilders.ImageResource);
+ method public androidx.wear.tiles.ResourceBuilders.Resources build();
+ method public androidx.wear.tiles.ResourceBuilders.Resources.Builder setVersion(String);
+ }
+
+ public final class StateBuilders {
+ }
+
+ public static final class StateBuilders.State {
+ method public String getLastClickableId();
+ }
+
+ public static final class StateBuilders.State.Builder {
+ ctor public StateBuilders.State.Builder();
+ method public androidx.wear.tiles.StateBuilders.State build();
+ }
+
+ public final class TileBuilders {
+ }
+
+ public static final class TileBuilders.Tile {
+ method public long getFreshnessIntervalMillis();
+ method public String getResourcesVersion();
+ method public androidx.wear.tiles.TimelineBuilders.Timeline? getTimeline();
+ }
+
+ public static final class TileBuilders.Tile.Builder {
+ ctor public TileBuilders.Tile.Builder();
+ method public androidx.wear.tiles.TileBuilders.Tile build();
+ method public androidx.wear.tiles.TileBuilders.Tile.Builder setFreshnessIntervalMillis(long);
+ method public androidx.wear.tiles.TileBuilders.Tile.Builder setResourcesVersion(String);
+ method public androidx.wear.tiles.TileBuilders.Tile.Builder setTimeline(androidx.wear.tiles.TimelineBuilders.Timeline);
+ }
+
+ public abstract class TileService extends android.app.Service {
+ ctor public TileService();
+ method public static androidx.wear.tiles.TileUpdateRequester getUpdater(android.content.Context);
+ method public android.os.IBinder? onBind(android.content.Intent);
+ method @MainThread protected abstract com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.ResourceBuilders.Resources!> onResourcesRequest(androidx.wear.tiles.RequestBuilders.ResourcesRequest);
+ method @MainThread protected void onTileAddEvent(androidx.wear.tiles.EventBuilders.TileAddEvent);
+ method @MainThread protected void onTileEnterEvent(androidx.wear.tiles.EventBuilders.TileEnterEvent);
+ method @MainThread protected void onTileLeaveEvent(androidx.wear.tiles.EventBuilders.TileLeaveEvent);
+ method @MainThread protected void onTileRemoveEvent(androidx.wear.tiles.EventBuilders.TileRemoveEvent);
+ method @MainThread protected abstract com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.TileBuilders.Tile!> onTileRequest(androidx.wear.tiles.RequestBuilders.TileRequest);
+ field public static final String ACTION_BIND_TILE_PROVIDER = "androidx.wear.tiles.action.BIND_TILE_PROVIDER";
+ field public static final String EXTRA_CLICKABLE_ID = "androidx.wear.tiles.extra.CLICKABLE_ID";
+ field public static final String METADATA_PREVIEW_KEY = "androidx.wear.tiles.PREVIEW";
+ }
+
+ public interface TileUpdateRequester {
+ method public void requestUpdate(Class<? extends androidx.wear.tiles.TileService>);
+ }
+
+ public final class TimelineBuilders {
+ }
+
+ public static final class TimelineBuilders.TimeInterval {
+ method public long getEndMillis();
+ method public long getStartMillis();
+ }
+
+ public static final class TimelineBuilders.TimeInterval.Builder {
+ ctor public TimelineBuilders.TimeInterval.Builder();
+ method public androidx.wear.tiles.TimelineBuilders.TimeInterval build();
+ method public androidx.wear.tiles.TimelineBuilders.TimeInterval.Builder setEndMillis(long);
+ method public androidx.wear.tiles.TimelineBuilders.TimeInterval.Builder setStartMillis(long);
+ }
+
+ public static final class TimelineBuilders.Timeline {
+ method public static androidx.wear.tiles.TimelineBuilders.Timeline fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public java.util.List<androidx.wear.tiles.TimelineBuilders.TimelineEntry!> getTimelineEntries();
+ }
+
+ public static final class TimelineBuilders.Timeline.Builder {
+ ctor public TimelineBuilders.Timeline.Builder();
+ method public androidx.wear.tiles.TimelineBuilders.Timeline.Builder addTimelineEntry(androidx.wear.tiles.TimelineBuilders.TimelineEntry);
+ method public androidx.wear.tiles.TimelineBuilders.Timeline build();
+ }
+
+ public static final class TimelineBuilders.TimelineEntry {
+ method public static androidx.wear.tiles.TimelineBuilders.TimelineEntry fromLayoutElement(androidx.wear.tiles.LayoutElementBuilders.LayoutElement);
+ method public androidx.wear.tiles.LayoutElementBuilders.Layout? getLayout();
+ method public androidx.wear.tiles.TimelineBuilders.TimeInterval? getValidity();
+ }
+
+ public static final class TimelineBuilders.TimelineEntry.Builder {
+ ctor public TimelineBuilders.TimelineEntry.Builder();
+ method public androidx.wear.tiles.TimelineBuilders.TimelineEntry build();
+ method public androidx.wear.tiles.TimelineBuilders.TimelineEntry.Builder setLayout(androidx.wear.tiles.LayoutElementBuilders.Layout);
+ method public androidx.wear.tiles.TimelineBuilders.TimelineEntry.Builder setValidity(androidx.wear.tiles.TimelineBuilders.TimeInterval);
+ }
+
+ public final class TypeBuilders {
+ }
+
+ public static final class TypeBuilders.BoolProp {
+ method public boolean getValue();
+ }
+
+ public static final class TypeBuilders.BoolProp.Builder {
+ ctor public TypeBuilders.BoolProp.Builder();
+ method public androidx.wear.tiles.TypeBuilders.BoolProp build();
+ method public androidx.wear.tiles.TypeBuilders.BoolProp.Builder setValue(boolean);
+ }
+
+ public static final class TypeBuilders.FloatProp {
+ method public float getValue();
+ }
+
+ public static final class TypeBuilders.FloatProp.Builder {
+ ctor public TypeBuilders.FloatProp.Builder();
+ method public androidx.wear.tiles.TypeBuilders.FloatProp build();
+ method public androidx.wear.tiles.TypeBuilders.FloatProp.Builder setValue(float);
+ }
+
+ public static final class TypeBuilders.Int32Prop {
+ method public int getValue();
+ }
+
+ public static final class TypeBuilders.Int32Prop.Builder {
+ ctor public TypeBuilders.Int32Prop.Builder();
+ method public androidx.wear.tiles.TypeBuilders.Int32Prop build();
+ method public androidx.wear.tiles.TypeBuilders.Int32Prop.Builder setValue(int);
+ }
+
+ public static final class TypeBuilders.StringProp {
+ method public String getValue();
+ }
+
+ public static final class TypeBuilders.StringProp.Builder {
+ ctor public TypeBuilders.StringProp.Builder();
+ method public androidx.wear.tiles.TypeBuilders.StringProp build();
+ method public androidx.wear.tiles.TypeBuilders.StringProp.Builder setValue(String);
+ }
+
+}
+
diff --git a/wear/tiles/tiles/build.gradle b/wear/tiles/tiles/build.gradle
index 92fb660..f316baf 100644
--- a/wear/tiles/tiles/build.gradle
+++ b/wear/tiles/tiles/build.gradle
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+import androidx.build.Publish
import androidx.build.LibraryType
plugins {
@@ -59,6 +60,7 @@
androidx {
name = "Android Wear Tiles"
type = LibraryType.PUBLISHED_LIBRARY
+ publish = Publish.SNAPSHOT_AND_RELEASE
mavenGroup = LibraryGroups.WEAR_TILES
inceptionYear = "2020"
description = "Android Wear Tiles"
diff --git a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatDarkThemeTest.java b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatDarkThemeTest.java
index 4d95812..a940dba 100644
--- a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatDarkThemeTest.java
+++ b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatDarkThemeTest.java
@@ -28,6 +28,7 @@
import androidx.test.filters.MediumTest;
import androidx.test.filters.SdkSuppress;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -61,6 +62,7 @@
* Test the algorithmic darkening on web content that doesn't support dark style.
*/
@Test
+ @Ignore("b/235864049") // Find a way to run with targetSdk T
public void testSimplifiedDarkMode_rendersDark() throws Throwable {
WebkitUtils.checkFeature(WebViewFeature.ALGORITHMIC_DARKENING);
WebkitUtils.checkFeature(WebViewFeature.OFF_SCREEN_PRERASTER);
diff --git a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java
index 6808524..d67a87b 100644
--- a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java
+++ b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java
@@ -40,7 +40,7 @@
public WebSettingsCompatLightThemeTest() {
// targetSdkVersion to T, it is min version the algorithmic darkening works.
// TODO(http://b/214741472): Use VERSION_CODES.TIRAMISU once available.
- super(WebViewLightThemeTestActivity.class, VERSION_CODES.CUR_DEVELOPMENT);
+ super(WebViewLightThemeTestActivity.class, 33);
}
/**
diff --git a/window/extensions/extensions/api/current.txt b/window/extensions/extensions/api/current.txt
index 1534daf..c067593 100644
--- a/window/extensions/extensions/api/current.txt
+++ b/window/extensions/extensions/api/current.txt
@@ -2,6 +2,7 @@
package androidx.window.extensions {
public interface WindowExtensions {
+ method public default androidx.window.extensions.embedding.ActivityEmbeddingComponent? getActivityEmbeddingComponent();
method public default int getVendorApiLevel();
method public androidx.window.extensions.layout.WindowLayoutComponent? getWindowLayoutComponent();
}
@@ -12,6 +13,90 @@
}
+package androidx.window.extensions.embedding {
+
+ public interface ActivityEmbeddingComponent {
+ method public boolean isActivityEmbedded(android.app.Activity);
+ method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
+ method public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
+ }
+
+ public class ActivityRule extends androidx.window.extensions.embedding.EmbeddingRule {
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivity(android.app.Activity);
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesIntent(android.content.Intent);
+ method public boolean shouldAlwaysExpand();
+ }
+
+ public static final class ActivityRule.Builder {
+ ctor public ActivityRule.Builder(java.util.function.Predicate<android.app.Activity!>, java.util.function.Predicate<android.content.Intent!>);
+ method public androidx.window.extensions.embedding.ActivityRule build();
+ method public androidx.window.extensions.embedding.ActivityRule.Builder setShouldAlwaysExpand(boolean);
+ }
+
+ public class ActivityStack {
+ ctor public ActivityStack(java.util.List<android.app.Activity!>, boolean);
+ method public java.util.List<android.app.Activity!> getActivities();
+ method public boolean isEmpty();
+ }
+
+ public abstract class EmbeddingRule {
+ }
+
+ public class SplitInfo {
+ ctor public SplitInfo(androidx.window.extensions.embedding.ActivityStack, androidx.window.extensions.embedding.ActivityStack, float);
+ method public androidx.window.extensions.embedding.ActivityStack getPrimaryActivityStack();
+ method public androidx.window.extensions.embedding.ActivityStack getSecondaryActivityStack();
+ method public float getSplitRatio();
+ }
+
+ public class SplitPairRule extends androidx.window.extensions.embedding.SplitRule {
+ method public int getFinishPrimaryWithSecondary();
+ method public int getFinishSecondaryWithPrimary();
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivityIntentPair(android.app.Activity, android.content.Intent);
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivityPair(android.app.Activity, android.app.Activity);
+ method public boolean shouldClearTop();
+ }
+
+ public static final class SplitPairRule.Builder {
+ ctor public SplitPairRule.Builder(java.util.function.Predicate<android.util.Pair<android.app.Activity!,android.app.Activity!>!>, java.util.function.Predicate<android.util.Pair<android.app.Activity!,android.content.Intent!>!>, java.util.function.Predicate<android.view.WindowMetrics!>);
+ method public androidx.window.extensions.embedding.SplitPairRule build();
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setFinishPrimaryWithSecondary(int);
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setFinishSecondaryWithPrimary(int);
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setLayoutDirection(int);
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldClearTop(boolean);
+ method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldFinishPrimaryWithSecondary(boolean);
+ method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldFinishSecondaryWithPrimary(boolean);
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setSplitRatio(float);
+ }
+
+ public class SplitPlaceholderRule extends androidx.window.extensions.embedding.SplitRule {
+ method public int getFinishPrimaryWithSecondary();
+ method public android.content.Intent getPlaceholderIntent();
+ method public boolean isSticky();
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivity(android.app.Activity);
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesIntent(android.content.Intent);
+ }
+
+ public static final class SplitPlaceholderRule.Builder {
+ ctor public SplitPlaceholderRule.Builder(android.content.Intent, java.util.function.Predicate<android.app.Activity!>, java.util.function.Predicate<android.content.Intent!>, java.util.function.Predicate<android.view.WindowMetrics!>);
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule build();
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithSecondary(int);
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setLayoutDirection(int);
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSplitRatio(float);
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSticky(boolean);
+ }
+
+ public abstract class SplitRule extends androidx.window.extensions.embedding.EmbeddingRule {
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean checkParentMetrics(android.view.WindowMetrics);
+ method public int getLayoutDirection();
+ method public float getSplitRatio();
+ field public static final int FINISH_ADJACENT = 2; // 0x2
+ field public static final int FINISH_ALWAYS = 1; // 0x1
+ field public static final int FINISH_NEVER = 0; // 0x0
+ }
+
+}
+
package androidx.window.extensions.layout {
public interface DisplayFeature {
diff --git a/window/extensions/extensions/api/public_plus_experimental_current.txt b/window/extensions/extensions/api/public_plus_experimental_current.txt
index 6a28e00..c067593 100644
--- a/window/extensions/extensions/api/public_plus_experimental_current.txt
+++ b/window/extensions/extensions/api/public_plus_experimental_current.txt
@@ -1,11 +1,8 @@
// Signature format: 4.0
package androidx.window.extensions {
- @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalWindowExtensionsApi {
- }
-
public interface WindowExtensions {
- method @androidx.window.extensions.ExperimentalWindowExtensionsApi public androidx.window.extensions.embedding.ActivityEmbeddingComponent? getActivityEmbeddingComponent();
+ method public default androidx.window.extensions.embedding.ActivityEmbeddingComponent? getActivityEmbeddingComponent();
method public default int getVendorApiLevel();
method public androidx.window.extensions.layout.WindowLayoutComponent? getWindowLayoutComponent();
}
@@ -18,13 +15,13 @@
package androidx.window.extensions.embedding {
- @androidx.window.extensions.ExperimentalWindowExtensionsApi public interface ActivityEmbeddingComponent {
+ public interface ActivityEmbeddingComponent {
method public boolean isActivityEmbedded(android.app.Activity);
method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
method public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
}
- @androidx.window.extensions.ExperimentalWindowExtensionsApi public class ActivityRule extends androidx.window.extensions.embedding.EmbeddingRule {
+ public class ActivityRule extends androidx.window.extensions.embedding.EmbeddingRule {
method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivity(android.app.Activity);
method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesIntent(android.content.Intent);
method public boolean shouldAlwaysExpand();
@@ -36,23 +33,23 @@
method public androidx.window.extensions.embedding.ActivityRule.Builder setShouldAlwaysExpand(boolean);
}
- @androidx.window.extensions.ExperimentalWindowExtensionsApi public class ActivityStack {
+ public class ActivityStack {
ctor public ActivityStack(java.util.List<android.app.Activity!>, boolean);
method public java.util.List<android.app.Activity!> getActivities();
method public boolean isEmpty();
}
- @androidx.window.extensions.ExperimentalWindowExtensionsApi public abstract class EmbeddingRule {
+ public abstract class EmbeddingRule {
}
- @androidx.window.extensions.ExperimentalWindowExtensionsApi public class SplitInfo {
+ public class SplitInfo {
ctor public SplitInfo(androidx.window.extensions.embedding.ActivityStack, androidx.window.extensions.embedding.ActivityStack, float);
method public androidx.window.extensions.embedding.ActivityStack getPrimaryActivityStack();
method public androidx.window.extensions.embedding.ActivityStack getSecondaryActivityStack();
method public float getSplitRatio();
}
- @androidx.window.extensions.ExperimentalWindowExtensionsApi public class SplitPairRule extends androidx.window.extensions.embedding.SplitRule {
+ public class SplitPairRule extends androidx.window.extensions.embedding.SplitRule {
method public int getFinishPrimaryWithSecondary();
method public int getFinishSecondaryWithPrimary();
method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivityIntentPair(android.app.Activity, android.content.Intent);
@@ -72,7 +69,7 @@
method public androidx.window.extensions.embedding.SplitPairRule.Builder setSplitRatio(float);
}
- @androidx.window.extensions.ExperimentalWindowExtensionsApi public class SplitPlaceholderRule extends androidx.window.extensions.embedding.SplitRule {
+ public class SplitPlaceholderRule extends androidx.window.extensions.embedding.SplitRule {
method public int getFinishPrimaryWithSecondary();
method public android.content.Intent getPlaceholderIntent();
method public boolean isSticky();
@@ -89,7 +86,7 @@
method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSticky(boolean);
}
- @androidx.window.extensions.ExperimentalWindowExtensionsApi public abstract class SplitRule extends androidx.window.extensions.embedding.EmbeddingRule {
+ public abstract class SplitRule extends androidx.window.extensions.embedding.EmbeddingRule {
method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean checkParentMetrics(android.view.WindowMetrics);
method public int getLayoutDirection();
method public float getSplitRatio();
diff --git a/window/extensions/extensions/api/restricted_current.txt b/window/extensions/extensions/api/restricted_current.txt
index 1534daf..c067593 100644
--- a/window/extensions/extensions/api/restricted_current.txt
+++ b/window/extensions/extensions/api/restricted_current.txt
@@ -2,6 +2,7 @@
package androidx.window.extensions {
public interface WindowExtensions {
+ method public default androidx.window.extensions.embedding.ActivityEmbeddingComponent? getActivityEmbeddingComponent();
method public default int getVendorApiLevel();
method public androidx.window.extensions.layout.WindowLayoutComponent? getWindowLayoutComponent();
}
@@ -12,6 +13,90 @@
}
+package androidx.window.extensions.embedding {
+
+ public interface ActivityEmbeddingComponent {
+ method public boolean isActivityEmbedded(android.app.Activity);
+ method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
+ method public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
+ }
+
+ public class ActivityRule extends androidx.window.extensions.embedding.EmbeddingRule {
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivity(android.app.Activity);
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesIntent(android.content.Intent);
+ method public boolean shouldAlwaysExpand();
+ }
+
+ public static final class ActivityRule.Builder {
+ ctor public ActivityRule.Builder(java.util.function.Predicate<android.app.Activity!>, java.util.function.Predicate<android.content.Intent!>);
+ method public androidx.window.extensions.embedding.ActivityRule build();
+ method public androidx.window.extensions.embedding.ActivityRule.Builder setShouldAlwaysExpand(boolean);
+ }
+
+ public class ActivityStack {
+ ctor public ActivityStack(java.util.List<android.app.Activity!>, boolean);
+ method public java.util.List<android.app.Activity!> getActivities();
+ method public boolean isEmpty();
+ }
+
+ public abstract class EmbeddingRule {
+ }
+
+ public class SplitInfo {
+ ctor public SplitInfo(androidx.window.extensions.embedding.ActivityStack, androidx.window.extensions.embedding.ActivityStack, float);
+ method public androidx.window.extensions.embedding.ActivityStack getPrimaryActivityStack();
+ method public androidx.window.extensions.embedding.ActivityStack getSecondaryActivityStack();
+ method public float getSplitRatio();
+ }
+
+ public class SplitPairRule extends androidx.window.extensions.embedding.SplitRule {
+ method public int getFinishPrimaryWithSecondary();
+ method public int getFinishSecondaryWithPrimary();
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivityIntentPair(android.app.Activity, android.content.Intent);
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivityPair(android.app.Activity, android.app.Activity);
+ method public boolean shouldClearTop();
+ }
+
+ public static final class SplitPairRule.Builder {
+ ctor public SplitPairRule.Builder(java.util.function.Predicate<android.util.Pair<android.app.Activity!,android.app.Activity!>!>, java.util.function.Predicate<android.util.Pair<android.app.Activity!,android.content.Intent!>!>, java.util.function.Predicate<android.view.WindowMetrics!>);
+ method public androidx.window.extensions.embedding.SplitPairRule build();
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setFinishPrimaryWithSecondary(int);
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setFinishSecondaryWithPrimary(int);
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setLayoutDirection(int);
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldClearTop(boolean);
+ method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldFinishPrimaryWithSecondary(boolean);
+ method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldFinishSecondaryWithPrimary(boolean);
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setSplitRatio(float);
+ }
+
+ public class SplitPlaceholderRule extends androidx.window.extensions.embedding.SplitRule {
+ method public int getFinishPrimaryWithSecondary();
+ method public android.content.Intent getPlaceholderIntent();
+ method public boolean isSticky();
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivity(android.app.Activity);
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesIntent(android.content.Intent);
+ }
+
+ public static final class SplitPlaceholderRule.Builder {
+ ctor public SplitPlaceholderRule.Builder(android.content.Intent, java.util.function.Predicate<android.app.Activity!>, java.util.function.Predicate<android.content.Intent!>, java.util.function.Predicate<android.view.WindowMetrics!>);
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule build();
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithSecondary(int);
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setLayoutDirection(int);
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSplitRatio(float);
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSticky(boolean);
+ }
+
+ public abstract class SplitRule extends androidx.window.extensions.embedding.EmbeddingRule {
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean checkParentMetrics(android.view.WindowMetrics);
+ method public int getLayoutDirection();
+ method public float getSplitRatio();
+ field public static final int FINISH_ADJACENT = 2; // 0x2
+ field public static final int FINISH_ALWAYS = 1; // 0x1
+ field public static final int FINISH_NEVER = 0; // 0x0
+ }
+
+}
+
package androidx.window.extensions.layout {
public interface DisplayFeature {
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
index 6ac25bd..c1a9bd8 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
@@ -58,6 +58,7 @@
* @return the OEM implementation of {@link ActivityEmbeddingComponent}
*/
@Nullable
- @ExperimentalWindowExtensionsApi
- ActivityEmbeddingComponent getActivityEmbeddingComponent();
+ default ActivityEmbeddingComponent getActivityEmbeddingComponent() {
+ return null;
+ }
}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java
index f4dd8a4..e87736c 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java
@@ -19,7 +19,6 @@
import android.app.Activity;
import androidx.annotation.NonNull;
-import androidx.window.extensions.ExperimentalWindowExtensionsApi;
import java.util.List;
import java.util.Set;
@@ -32,7 +31,6 @@
* <p>This interface should be implemented by OEM and deployed to the target devices.
* @see androidx.window.extensions.WindowExtensions
*/
-@ExperimentalWindowExtensionsApi
public interface ActivityEmbeddingComponent {
/**
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityRule.java
index dbf8233..2d1fc33 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityRule.java
@@ -23,14 +23,12 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
-import androidx.window.extensions.ExperimentalWindowExtensionsApi;
import java.util.function.Predicate;
/**
* Split configuration rule for individual activities.
*/
-@ExperimentalWindowExtensionsApi
public class ActivityRule extends EmbeddingRule {
@NonNull
private final Predicate<Activity> mActivityPredicate;
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStack.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStack.java
index 928a7b9..db275bb 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStack.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStack.java
@@ -19,7 +19,6 @@
import android.app.Activity;
import androidx.annotation.NonNull;
-import androidx.window.extensions.ExperimentalWindowExtensionsApi;
import java.util.ArrayList;
import java.util.List;
@@ -28,7 +27,6 @@
* Description of a group of activities stacked on top of each other and shown as a single
* container, all within the same task.
*/
-@ExperimentalWindowExtensionsApi
public class ActivityStack {
@NonNull
private final List<Activity> mActivities;
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddingRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddingRule.java
index beac689..571eda0 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddingRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddingRule.java
@@ -16,13 +16,10 @@
package androidx.window.extensions.embedding;
-import androidx.window.extensions.ExperimentalWindowExtensionsApi;
-
/**
* Base interface for activity embedding rules. Used to group different types of rules together when
* updating from the core library.
*/
-@ExperimentalWindowExtensionsApi
public abstract class EmbeddingRule {
EmbeddingRule() {}
}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitInfo.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitInfo.java
index 351fd1b..cfd8b1a 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitInfo.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitInfo.java
@@ -17,10 +17,8 @@
package androidx.window.extensions.embedding;
import androidx.annotation.NonNull;
-import androidx.window.extensions.ExperimentalWindowExtensionsApi;
/** Describes a split of two containers with activities. */
-@ExperimentalWindowExtensionsApi
public class SplitInfo {
@NonNull
private final ActivityStack mPrimaryActivityStack;
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java
index 2cbb747..db9f65c 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java
@@ -25,14 +25,12 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
-import androidx.window.extensions.ExperimentalWindowExtensionsApi;
import java.util.function.Predicate;
/**
* Split configuration rules for activity pairs.
*/
-@ExperimentalWindowExtensionsApi
public class SplitPairRule extends SplitRule {
@NonNull
private final Predicate<Pair<Activity, Activity>> mActivityPairPredicate;
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPlaceholderRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPlaceholderRule.java
index 83bfc32..1f8a8fe 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPlaceholderRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPlaceholderRule.java
@@ -24,7 +24,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
-import androidx.window.extensions.ExperimentalWindowExtensionsApi;
import java.util.function.Predicate;
@@ -32,7 +31,6 @@
* Split configuration rules for split placeholders - activities used to occupy additional
* available space on the side before the user selects content to show.
*/
-@ExperimentalWindowExtensionsApi
public class SplitPlaceholderRule extends SplitRule {
@NonNull
private final Predicate<Activity> mActivityPredicate;
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java
index 4b1d4e5..39b7df8 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java
@@ -24,7 +24,6 @@
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
-import androidx.window.extensions.ExperimentalWindowExtensionsApi;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -37,7 +36,6 @@
* new activities started from the same process automatically by the embedding implementation on
* the device.
*/
-@ExperimentalWindowExtensionsApi
public abstract class SplitRule extends EmbeddingRule {
@NonNull
private final Predicate<WindowMetrics> mParentWindowMetricsPredicate;
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java
index 1b5b5ac..a917e8e 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java
@@ -25,6 +25,7 @@
import static org.hamcrest.Matchers.greaterThan;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -37,6 +38,7 @@
import android.content.Context;
import android.content.Intent;
import android.database.sqlite.SQLiteCantOpenDatabaseException;
+import android.database.sqlite.SQLiteException;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -199,6 +201,30 @@
}
@Test
+ public void test_InitializationExceptionHandler_migrationFailures() {
+ mContext = mock(Context.class);
+ when(mContext.getApplicationContext()).thenReturn(mContext);
+ mWorkDatabase = WorkDatabase.create(mContext, mConfiguration.getTaskExecutor(), true);
+ when(mWorkManager.getWorkDatabase()).thenReturn(mWorkDatabase);
+ mRunnable = new ForceStopRunnable(mContext, mWorkManager);
+
+ InitializationExceptionHandler handler = mock(InitializationExceptionHandler.class);
+ Configuration configuration = new Configuration.Builder(mConfiguration)
+ .setInitializationExceptionHandler(handler)
+ .build();
+
+ when(mWorkManager.getConfiguration()).thenReturn(configuration);
+ // This is what WorkDatabasePathHelper uses under the hood to migrate the database.
+ when(mContext.getDatabasePath(anyString())).thenThrow(
+ new SQLiteException("Unable to migrate database"));
+
+ ForceStopRunnable runnable = spy(mRunnable);
+ doNothing().when(runnable).sleep(anyLong());
+ runnable.run();
+ verify(handler, times(1)).handleException(any(Throwable.class));
+ }
+
+ @Test
public void test_completeOnMultiProcessChecks() {
ForceStopRunnable runnable = spy(mRunnable);
doReturn(false).when(runnable).multiProcessChecks();
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
index 805b8ee..cf9679b 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
@@ -38,6 +38,7 @@
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabaseCorruptException;
import android.database.sqlite.SQLiteDatabaseLockedException;
+import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteTableLockedException;
import android.os.Build;
import android.text.TextUtils;
@@ -102,8 +103,29 @@
return;
}
while (true) {
- // Migrate the database to the no-backup directory if necessary.
- WorkDatabasePathHelper.migrateDatabase(mContext);
+
+ try {
+ // Migrate the database to the no-backup directory if necessary.
+ // Migrations are not retry-able. So if something unexpected were to happen
+ // here, the best we can do is to hand things off to the
+ // InitializationExceptionHandler.
+ WorkDatabasePathHelper.migrateDatabase(mContext);
+ } catch (SQLiteException sqLiteException) {
+ // This should typically never happen.
+ String message = "Unexpected SQLite exception during migrations";
+ Logger.get().error(TAG, message);
+ IllegalStateException exception =
+ new IllegalStateException(message, sqLiteException);
+ InitializationExceptionHandler exceptionHandler =
+ mWorkManager.getConfiguration().getInitializationExceptionHandler();
+ if (exceptionHandler != null) {
+ exceptionHandler.handleException(exception);
+ break;
+ } else {
+ throw exception;
+ }
+ }
+
// Clean invalid jobs attributed to WorkManager, and Workers that might have been
// interrupted because the application crashed (RUNNING state).
Logger.get().debug(TAG, "Performing cleanup operations.");
@@ -111,11 +133,11 @@
forceStopRunnable();
break;
} catch (SQLiteCantOpenDatabaseException
- | SQLiteDatabaseCorruptException
- | SQLiteDatabaseLockedException
- | SQLiteTableLockedException
- | SQLiteConstraintException
- | SQLiteAccessPermException exception) {
+ | SQLiteDatabaseCorruptException
+ | SQLiteDatabaseLockedException
+ | SQLiteTableLockedException
+ | SQLiteConstraintException
+ | SQLiteAccessPermException exception) {
mRetryCount++;
if (mRetryCount >= MAX_ATTEMPTS) {
// ForceStopRunnable is usually the first thing that accesses a database