[go: nahoru, domu]

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)
+            }
+        }
     }
 }