Fixed "clickable" modifier doesn't work with ImmersiveList
* In the "focusableItem" modifier, we add a "focusable" modifier. Now, if the user adds the "clickable" modifier, the
"focusable" modifier added by us has no effect. So, we are now removing the "focusable" modifier
from it. Now, user will have to manually add "focusable()" or "clickable()" along with
"focusableItem". Since this is an immersive list item, ideally, the item will always be clickable.
* Renamed "focusableItem" modifier to "immersiveListItem" modifier which is a better name for the new API and also this
makes it clear, while reading, that this modifier is only part of "ImmersiveList" and also makes
doesn't hint that "focusable()" modifier would be auto-applied
* Moved the immersive list component out of the sub-package
* Added missing ktdocs for params
* Added sample immersive list usage
Bug: b/263061052
Test: Updated tests
Relnote: "Renamed `focusableItem` to `immersiveListItem` and removed `focusable()` modifier. Now
users will have to manually add `focusable()` or `clickable()` modifier along with
`immersiveListItem`"
Change-Id: I1fd6c8cca3d7f4e810ad838ec1aad0b3c3e41450
diff --git a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/ImmersiveList.kt b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/ImmersiveList.kt
index a3bcc25..3f0e238 100644
--- a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/ImmersiveList.kt
+++ b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/ImmersiveList.kt
@@ -16,8 +16,10 @@
package androidx.tv.integration.demos
+import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
@@ -35,7 +37,7 @@
import androidx.compose.ui.unit.dp
import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.material.ExperimentalTvMaterialApi
-import androidx.tv.material.immersivelist.ImmersiveList
+import androidx.tv.material.ImmersiveList
@Composable
fun ImmersiveListContent() {
@@ -60,7 +62,9 @@
)
ImmersiveList(
- modifier = Modifier.height(immersiveListHeight + cardHeight / 2).fillMaxWidth(),
+ modifier = Modifier
+ .height(immersiveListHeight + cardHeight / 2)
+ .fillMaxWidth(),
background = { index, _ ->
Box(
modifier = Modifier
@@ -81,7 +85,10 @@
.height(cardHeight)
.border(5.dp, Color.White.copy(alpha = if (isFocused) 1f else 0.3f))
.onFocusChanged { isFocused = it.isFocused }
- .focusableItem(index)
+ .immersiveListItem(index)
+ .clickable {
+ Log.d("ImmersiveList", "Item $index was clicked")
+ }
)
}
}
diff --git a/tv/samples/src/main/java/androidx/tv/samples/ImmersiveListSamples.kt b/tv/samples/src/main/java/androidx/tv/samples/ImmersiveListSamples.kt
new file mode 100644
index 0000000..5a7f655
--- /dev/null
+++ b/tv/samples/src/main/java/androidx/tv/samples/ImmersiveListSamples.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.samples
+
+import android.util.Log
+import androidx.annotation.Sampled
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+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.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.tv.material.ExperimentalTvMaterialApi
+import androidx.tv.material.ImmersiveList
+
+@OptIn(ExperimentalTvMaterialApi::class)
+@Sampled
+@Composable
+private fun SampleImmersiveList() {
+ val immersiveListHeight = 300.dp
+ val cardSpacing = 10.dp
+ val cardWidth = 200.dp
+ val cardHeight = 150.dp
+ val backgrounds = listOf(
+ Color.Red,
+ Color.Blue,
+ Color.Magenta,
+ )
+
+ ImmersiveList(
+ modifier = Modifier
+ .height(immersiveListHeight + cardHeight / 2)
+ .fillMaxWidth(),
+ background = { index, _ ->
+ Box(
+ modifier = Modifier
+ .background(backgrounds[index].copy(alpha = 0.3f))
+ .height(immersiveListHeight)
+ .fillMaxWidth()
+ )
+ }
+ ) {
+ Row(horizontalArrangement = Arrangement.spacedBy(cardSpacing)) {
+ backgrounds.forEachIndexed { index, backgroundColor ->
+ var isFocused by remember { mutableStateOf(false) }
+
+ Box(
+ modifier = Modifier
+ .background(backgroundColor)
+ .width(cardWidth)
+ .height(cardHeight)
+ .border(5.dp, Color.White.copy(alpha = if (isFocused) 1f else 0.3f))
+ .onFocusChanged { isFocused = it.isFocused }
+ .immersiveListItem(index)
+ .clickable {
+ Log.d("ImmersiveList", "Item $index was clicked")
+ }
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tv/tv-material/api/current.txt b/tv/tv-material/api/current.txt
index 0c788a4..f6c00dc 100644
--- a/tv/tv-material/api/current.txt
+++ b/tv/tv-material/api/current.txt
@@ -9,6 +9,9 @@
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
}
+ public final class ImmersiveListKt {
+ }
+
public final class TabColors {
}
@@ -48,10 +51,3 @@
}
-package androidx.tv.material.immersivelist {
-
- public final class ImmersiveListKt {
- }
-
-}
-
diff --git a/tv/tv-material/api/public_plus_experimental_current.txt b/tv/tv-material/api/public_plus_experimental_current.txt
index a086e3e..ea7dbee 100644
--- a/tv/tv-material/api/public_plus_experimental_current.txt
+++ b/tv/tv-material/api/public_plus_experimental_current.txt
@@ -12,6 +12,27 @@
@kotlin.RequiresOptIn(message="This tv-material API is experimental and likely to change or be removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalTvMaterialApi {
}
+ @androidx.compose.runtime.Immutable @androidx.tv.material.ExperimentalTvMaterialApi public final class ImmersiveListBackgroundScope implements androidx.compose.foundation.layout.BoxScope {
+ method @androidx.compose.animation.ExperimentalAnimationApi @androidx.compose.runtime.Composable public void AnimatedContent(int targetState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentScope<java.lang.Integer>,androidx.compose.animation.ContentTransform> transitionSpec, optional androidx.compose.ui.Alignment contentAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.animation.AnimatedVisibilityScope,? super java.lang.Integer,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public void AnimatedVisibility(boolean visible, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.animation.EnterTransition enter, optional androidx.compose.animation.ExitTransition exit, optional String label, kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedVisibilityScope,kotlin.Unit> content);
+ }
+
+ @androidx.tv.material.ExperimentalTvMaterialApi public final class ImmersiveListDefaults {
+ method public androidx.compose.animation.EnterTransition getEnterTransition();
+ method public androidx.compose.animation.ExitTransition getExitTransition();
+ property public final androidx.compose.animation.EnterTransition EnterTransition;
+ property public final androidx.compose.animation.ExitTransition ExitTransition;
+ field public static final androidx.tv.material.ImmersiveListDefaults INSTANCE;
+ }
+
+ public final class ImmersiveListKt {
+ method @androidx.compose.runtime.Composable @androidx.tv.material.ExperimentalTvMaterialApi public static void ImmersiveList(kotlin.jvm.functions.Function3<? super androidx.tv.material.ImmersiveListBackgroundScope,? super java.lang.Integer,? super java.lang.Boolean,kotlin.Unit> background, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment listAlignment, kotlin.jvm.functions.Function1<? super androidx.tv.material.ImmersiveListScope,kotlin.Unit> list);
+ }
+
+ @androidx.compose.runtime.Immutable @androidx.tv.material.ExperimentalTvMaterialApi public final class ImmersiveListScope {
+ method public androidx.compose.ui.Modifier immersiveListItem(androidx.compose.ui.Modifier, int index);
+ }
+
public final class TabColors {
}
@@ -83,28 +104,3 @@
}
-package androidx.tv.material.immersivelist {
-
- @androidx.compose.runtime.Immutable @androidx.tv.material.ExperimentalTvMaterialApi public final class ImmersiveListBackgroundScope implements androidx.compose.foundation.layout.BoxScope {
- method @androidx.compose.animation.ExperimentalAnimationApi @androidx.compose.runtime.Composable public void AnimatedContent(int targetState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentScope<java.lang.Integer>,androidx.compose.animation.ContentTransform> transitionSpec, optional androidx.compose.ui.Alignment contentAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.animation.AnimatedVisibilityScope,? super java.lang.Integer,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public void AnimatedVisibility(boolean visible, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.animation.EnterTransition enter, optional androidx.compose.animation.ExitTransition exit, optional String label, kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedVisibilityScope,kotlin.Unit> content);
- }
-
- @androidx.tv.material.ExperimentalTvMaterialApi public final class ImmersiveListDefaults {
- method public androidx.compose.animation.EnterTransition getEnterTransition();
- method public androidx.compose.animation.ExitTransition getExitTransition();
- property public final androidx.compose.animation.EnterTransition EnterTransition;
- property public final androidx.compose.animation.ExitTransition ExitTransition;
- field public static final androidx.tv.material.immersivelist.ImmersiveListDefaults INSTANCE;
- }
-
- public final class ImmersiveListKt {
- method @androidx.compose.runtime.Composable @androidx.tv.material.ExperimentalTvMaterialApi public static void ImmersiveList(kotlin.jvm.functions.Function3<? super androidx.tv.material.immersivelist.ImmersiveListBackgroundScope,? super java.lang.Integer,? super java.lang.Boolean,kotlin.Unit> background, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment listAlignment, kotlin.jvm.functions.Function1<? super androidx.tv.material.immersivelist.ImmersiveListScope,kotlin.Unit> list);
- }
-
- @androidx.compose.runtime.Immutable @androidx.tv.material.ExperimentalTvMaterialApi public final class ImmersiveListScope {
- method public androidx.compose.ui.Modifier focusableItem(androidx.compose.ui.Modifier, int index);
- }
-
-}
-
diff --git a/tv/tv-material/api/restricted_current.txt b/tv/tv-material/api/restricted_current.txt
index 0c788a4..f6c00dc 100644
--- a/tv/tv-material/api/restricted_current.txt
+++ b/tv/tv-material/api/restricted_current.txt
@@ -9,6 +9,9 @@
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
}
+ public final class ImmersiveListKt {
+ }
+
public final class TabColors {
}
@@ -48,10 +51,3 @@
}
-package androidx.tv.material.immersivelist {
-
- public final class ImmersiveListKt {
- }
-
-}
-
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material/immersivelist/ImmersiveListTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material/ImmersiveListTest.kt
similarity index 86%
rename from tv/tv-material/src/androidTest/java/androidx/tv/material/immersivelist/ImmersiveListTest.kt
rename to tv/tv-material/src/androidTest/java/androidx/tv/material/ImmersiveListTest.kt
index d128958..2a0e9ac 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material/immersivelist/ImmersiveListTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material/ImmersiveListTest.kt
@@ -14,9 +14,10 @@
* limitations under the License.
*/
-package androidx.tv.material.immersivelist
+package androidx.tv.material
import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
@@ -26,25 +27,30 @@
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.input.key.NativeKeyEvent
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsFocused
import androidx.compose.ui.test.getUnclippedBoundsInRoot
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performSemanticsAction
import androidx.compose.ui.unit.dp
import androidx.test.platform.app.InstrumentationRegistry
import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.foundation.lazy.list.TvLazyRow
-import androidx.tv.material.ExperimentalTvMaterialApi
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
@@ -56,40 +62,43 @@
@OptIn(ExperimentalTvMaterialApi::class, ExperimentalAnimationApi::class)
@Test
fun immersiveList_scroll_backgroundChanges() {
- val firstCard = FocusRequester()
- val secondCard = FocusRequester()
-
rule.setContent {
ImmersiveList(
background = { index, _ ->
AnimatedContent(targetState = index) {
Box(
- Modifier
+ modifier = Modifier
.testTag("background-$it")
- .size(200.dp)) {
+ .size(200.dp)
+ ) {
BasicText("background-$it")
}
}
}) {
TvLazyRow {
items(3) { index ->
- var modifier = Modifier
- .testTag("card-$index")
- .size(100.dp)
- when (index) {
- 0 -> modifier = modifier
- .focusRequester(firstCard)
- 1 -> modifier = modifier
- .focusRequester(secondCard)
- }
+ var isFocused by remember { mutableStateOf(false) }
- Box(modifier.focusableItem(index)) { BasicText("card-$index") }
+ Box(
+ modifier = Modifier
+ .size(100.dp)
+ .testTag("card-$index")
+ .background(if (isFocused) Color.Red else Color.Transparent)
+ .size(100.dp)
+ .onFocusChanged { isFocused = it.isFocused }
+ .immersiveListItem(index)
+ .focusable(true)
+ ) {
+ BasicText("card-$index")
+ }
}
}
}
}
- rule.runOnIdle { firstCard.requestFocus() }
+ rule.waitForIdle()
+ rule.onNodeWithTag("card-0").performSemanticsAction(SemanticsActions.RequestFocus)
+ rule.waitForIdle()
rule.onNodeWithTag("card-0").assertIsFocused()
rule.onNodeWithTag("background-0").assertIsDisplayed()
@@ -275,7 +284,12 @@
for (item in frList) {
modifier = modifier.focusRequester(frList[index])
}
- Box(modifier.focusableItem(index)) { BasicText("list-card-$index") }
+ Box(
+ modifier
+ .immersiveListItem(index)
+ .focusable(true)) {
+ BasicText("list-card-$index")
+ }
}
}
}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/immersivelist/ImmersiveList.kt b/tv/tv-material/src/main/java/androidx/tv/material/ImmersiveList.kt
similarity index 88%
rename from tv/tv-material/src/main/java/androidx/tv/material/immersivelist/ImmersiveList.kt
rename to tv/tv-material/src/main/java/androidx/tv/material/ImmersiveList.kt
index e750f38..209e285 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material/immersivelist/ImmersiveList.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material/ImmersiveList.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.tv.material.immersivelist
+package androidx.tv.material
import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.AnimatedVisibilityScope
@@ -26,7 +26,6 @@
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.with
-import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable
@@ -41,8 +40,6 @@
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.platform.LocalFocusManager
-import androidx.tv.material.ExperimentalTvMaterialApi
-import androidx.tv.material.bringIntoViewIfChildrenAreFocused
/**
* Immersive List consists of a list with multiple items and a background that displays content
@@ -51,6 +48,8 @@
* To display the background only when the list is in focus, use
* [ImmersiveListBackgroundScope.AnimatedVisibility].
*
+ * @sample androidx.tv.samples.SampleImmersiveList
+ *
* @param background Composable defining the background to be displayed for a given item's
* index. `listHasFocus` argument can be used to hide the background when the list is not in focus
* @param modifier applied to Immersive List.
@@ -100,7 +99,7 @@
@Immutable
@ExperimentalTvMaterialApi
-public class ImmersiveListBackgroundScope internal constructor(boxScope: BoxScope) : BoxScope
+class ImmersiveListBackgroundScope internal constructor(boxScope: BoxScope) : BoxScope
by boxScope {
/**
@@ -113,6 +112,7 @@
* @param modifier modifier for the Layout created to contain the [content]
* @param enter EnterTransition(s) used for the appearing animation, fading in by default
* @param exit ExitTransition(s) used for the disappearing animation, fading out by default
+ * @param label used to differentiate different transitions in Android Studio
* @param content Content to appear or disappear based on the value of [visible]
*
* @link androidx.compose.animation.AnimatedVisibility
@@ -149,6 +149,8 @@
* @param modifier modifier for the Layout created to contain the [content]
* @param transitionSpec defines the EnterTransition(s) and ExitTransition(s) used to display
* and remove the content, fading in and fading out by default
+ * @param contentAlignment specifies how the background content should be aligned in the
+ * container
* @param content Content to appear or disappear based on the value of [targetState]
*
* @link androidx.compose.animation.AnimatedContent
@@ -180,15 +182,21 @@
@Immutable
@ExperimentalTvMaterialApi
-public class ImmersiveListScope internal constructor(private val onFocused: (Int) -> Unit) {
+class ImmersiveListScope internal constructor(private val onFocused: (Int) -> Unit) {
/**
* Modifier to be added to each of the items of the list within ImmersiveList to inform the
- * ImmersiveList of the index of the item in focus.
+ * ImmersiveList of the index of the item in focus
*
- * @param index index of the item within the list.
+ * > **NOTE**: This modifier needs to be paired with either the "focusable" or the "clickable"
+ * modifier for it to work
+ *
+ * @param index index of the item within the list
*/
- fun Modifier.focusableItem(index: Int): Modifier {
- return onFocusChanged { if (it.hasFocus || it.isFocused) { onFocused(index) } }
- .focusable()
+ fun Modifier.immersiveListItem(index: Int): Modifier {
+ return onFocusChanged {
+ if (it.isFocused) {
+ onFocused(index)
+ }
+ }
}
}