Adding Immersive List composable
Test: Tested manually on sample app and added an integration test
Relnote: Adding ImmersiveList
Change-Id: Ica405d7086a069ad213512fae9994a53a9f82a5c
diff --git a/tv/tv-material/api/current.txt b/tv/tv-material/api/current.txt
index 0ed6747..24ca3bd 100644
--- a/tv/tv-material/api/current.txt
+++ b/tv/tv-material/api/current.txt
@@ -9,9 +9,9 @@
}
-package androidx.tv.material.pager {
+package androidx.tv.material.immersivelist {
- public final class PagerKt {
+ 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 4e063ddc4..8712654 100644
--- a/tv/tv-material/api/public_plus_experimental_current.txt
+++ b/tv/tv-material/api/public_plus_experimental_current.txt
@@ -50,9 +50,27 @@
}
-package androidx.tv.material.pager {
+package androidx.tv.material.immersivelist {
- public final class PagerKt {
+ @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 0ed6747..24ca3bd 100644
--- a/tv/tv-material/api/restricted_current.txt
+++ b/tv/tv-material/api/restricted_current.txt
@@ -9,9 +9,9 @@
}
-package androidx.tv.material.pager {
+package androidx.tv.material.immersivelist {
- public final class PagerKt {
+ public final class ImmersiveListKt {
}
}
diff --git a/tv/tv-material/build.gradle b/tv/tv-material/build.gradle
index 3e5244d..19f04632 100644
--- a/tv/tv-material/build.gradle
+++ b/tv/tv-material/build.gradle
@@ -41,6 +41,7 @@
implementation("androidx.compose.ui:ui-text:$composeVersion")
implementation("androidx.compose.ui:ui-util:$composeVersion")
implementation("androidx.profileinstaller:profileinstaller:1.2.0")
+ implementation(project(":tv:tv-foundation"))
androidTestImplementation(libs.truth)
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt
index 0a19b0c..971069d 100644
--- a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt
@@ -24,27 +24,17 @@
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.border
-import androidx.compose.foundation.focusable
import androidx.compose.foundation.horizontalScroll
-import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
-import androidx.compose.material3.Card
-import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -59,7 +49,6 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Color.Companion.Cyan
import androidx.compose.ui.graphics.Color.Companion.Gray
-import androidx.compose.ui.graphics.Color.Companion.Red
import androidx.compose.ui.graphics.Color.Companion.Yellow
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@@ -72,19 +61,11 @@
@Composable
fun FeaturedCarousel() {
val carouselState = remember { CarouselState(0) }
- LazyColumn {
- item {
- Carousel(
- modifier = Modifier
- .height(400.dp)
- .width(950.dp),
- carouselState = carouselState,
- slideCount = 3
- ) { SampleFrame(it) }
- }
-
- items(7) { SampleLazyRow() }
- }
+ Carousel(
+ modifier = Modifier.height(130.dp).width(950.dp).border(1.dp, Color.Black),
+ carouselState = carouselState,
+ slideCount = mediaItems.size
+ ) { SampleFrame(it) }
}
@OptIn(ExperimentalTvMaterialApi::class)
@@ -162,61 +143,6 @@
}
}
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun SampleLazyRow() {
- LazyRow(
- state = rememberLazyListState(),
- contentPadding = PaddingValues(2.dp),
- horizontalArrangement = Arrangement.spacedBy(4.dp),
- modifier = Modifier
- .fillMaxWidth()
- .height(100.dp)) {
- items((1..10).map { it.toString() }) {
- var cardScale by remember { mutableStateOf(0.5f) }
- val borderGlowColorTransition = rememberInfiniteTransition()
- var initialValue by remember { mutableStateOf(Color.Transparent) }
- val glowingColor by borderGlowColorTransition.animateColor(
- initialValue = initialValue,
- targetValue = Color.Transparent,
- animationSpec = infiniteRepeatable(
- animation = tween(1000, easing = LinearEasing),
- repeatMode = RepeatMode.Reverse
- )
- )
-
- Card(
- modifier = Modifier
- .width(100.dp)
- .height(100.dp)
- .scale(cardScale)
- .border(2.dp, glowingColor, RoundedCornerShape(12.dp))
- .onFocusChanged { focusState ->
- if (focusState.isFocused) {
- cardScale = 1.0f
- initialValue = Color.White
- } else {
- cardScale = 0.5f
- initialValue = Color.Transparent
- }
- }
- .focusable()
- ) {
- Text(
- text = it,
- modifier = Modifier
- .fillMaxWidth()
- .height(100.dp)
- .padding(12.dp),
- color = Red,
- fontWeight = FontWeight.Bold
-
- )
- }
- }
- }
-}
-
val mediaItems = listOf(
Media(id = "1", title = "Title 1", description = "Description 1", backgroundColor = Gray),
Media(id = "2", title = "Title 2", description = "Description 2", backgroundColor = Yellow),
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/MainActivity.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/MainActivity.kt
index c329df0..b3f2c76 100644
--- a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/MainActivity.kt
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/MainActivity.kt
@@ -19,8 +19,40 @@
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
+import androidx.compose.animation.animateColor
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+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.draw.scale
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -28,8 +60,70 @@
setContent {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colorScheme.background) {
- FeaturedCarousel()
+ LazyColumn {
+ item { FeaturedCarousel() }
+ item { SampleImmersiveList() }
+
+ items(7) { SampleLazyRow() }
+ }
}
}
}
+
+ @Composable
+ fun SampleLazyRow() {
+ LazyRow(
+ state = rememberLazyListState(),
+ contentPadding = PaddingValues(2.dp),
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(100.dp)) {
+ items((1..10).map { it.toString() }) { SampleCard(it) }
+ }
+ }
+
+ @Composable
+ private fun SampleCard(it: String) {
+ var cardScale by remember { mutableStateOf(0.5f) }
+ val borderGlowColorTransition = rememberInfiniteTransition()
+ var initialValue by remember { mutableStateOf(Color.Transparent) }
+ val glowingColor by borderGlowColorTransition.animateColor(
+ initialValue = initialValue,
+ targetValue = Color.Transparent,
+ animationSpec = infiniteRepeatable(
+ animation = tween(1000, easing = LinearEasing),
+ repeatMode = RepeatMode.Reverse
+ )
+ )
+
+ Card(
+ modifier = Modifier
+ .width(100.dp)
+ .height(100.dp)
+ .scale(cardScale)
+ .border(2.dp, glowingColor, RoundedCornerShape(12.dp))
+ .onFocusChanged { focusState ->
+ if (focusState.isFocused) {
+ cardScale = 1.0f
+ initialValue = Color.White
+ } else {
+ cardScale = 0.5f
+ initialValue = Color.Transparent
+ }
+ }
+ .focusable()
+ ) {
+ Text(
+ text = it,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(100.dp)
+ .padding(12.dp),
+ color = Color.Red,
+ fontWeight = FontWeight.Bold
+
+ )
+ }
+ }
}
\ No newline at end of file
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/SampleImmersiveList.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/SampleImmersiveList.kt
new file mode 100644
index 0000000..45dffab
--- /dev/null
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/SampleImmersiveList.kt
@@ -0,0 +1,138 @@
+/*
+ * 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.tvmaterial.samples
+
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.animateColor
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Card
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+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.draw.scale
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.lazy.list.TvLazyRow
+import androidx.tv.material.ExperimentalTvMaterialApi
+import androidx.tv.material.immersivelist.ImmersiveList
+
+@OptIn(ExperimentalTvMaterialApi::class, ExperimentalAnimationApi::class)
+@Composable
+fun SampleImmersiveList() {
+ ImmersiveList(
+ modifier = Modifier
+ .height(130.dp)
+ .width(950.dp)
+ .border(1.dp, Color.Black),
+ background = { index, _ ->
+ AnimatedContent(targetState = index) { SampleBackground(it) } },
+ ) {
+ TvLazyRow {
+ items(immersiveClusterMediaItems.size) {
+ SampleCard(Modifier.focusableItem(it), (it + 1).toString())
+ }
+ }
+ }
+}
+
+@Composable
+fun SampleBackground(idx: Int) {
+ val item = immersiveClusterMediaItems[idx]
+
+ Box(
+ Modifier
+ .background(item.backgroundColor)
+ .fillMaxWidth()
+ .height(90.dp)) {
+ Text(
+ text = item.title,
+ style = MaterialTheme.typography.headlineSmall,
+ color = Color.Black,
+ fontWeight = FontWeight.Bold
+ )
+ }
+}
+
+@Composable
+private fun SampleCard(modifier: Modifier, cardText: String) {
+ var cardScale by remember { mutableStateOf(0.5f) }
+ val borderGlowColorTransition = rememberInfiniteTransition()
+ var initialValue by remember { mutableStateOf(Color.Transparent) }
+ val glowingColor by borderGlowColorTransition.animateColor(
+ initialValue = initialValue,
+ targetValue = Color.Transparent,
+ animationSpec = infiniteRepeatable(
+ animation = tween(1000, easing = LinearEasing),
+ repeatMode = RepeatMode.Reverse
+ )
+ )
+
+ Card(
+ modifier = modifier
+ .width(100.dp)
+ .height(100.dp)
+ .scale(cardScale)
+ .border(2.dp, glowingColor, RoundedCornerShape(12.dp))
+ .onFocusChanged { focusState ->
+ if (focusState.isFocused) {
+ cardScale = 1.0f
+ initialValue = Color.White
+ } else {
+ cardScale = 0.5f
+ initialValue = Color.Transparent
+ }
+ }
+ .focusable()
+ ) {
+ Text(
+ text = cardText,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(100.dp)
+ .padding(12.dp),
+ color = Color.Red,
+ fontWeight = FontWeight.Bold
+
+ )
+ }
+}
+
+val immersiveClusterMediaItems = listOf(
+ Media(id = "1", title = "Title 1", description = "Description 1", backgroundColor = Color.Gray),
+ Media(id = "2", title = "Title 2", description = "Description 2", backgroundColor = Color.Blue),
+ Media(id = "3", title = "Title 3", description = "Description 3", backgroundColor = Color.Cyan)
+)
\ No newline at end of file
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/immersivelist/ImmersiveListTest.kt
new file mode 100644
index 0000000..5b22bdd
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material/immersivelist/ImmersiveListTest.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material.immersivelist
+
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.tv.foundation.lazy.list.TvLazyRow
+import androidx.tv.material.ExperimentalTvMaterialApi
+import org.junit.Rule
+import org.junit.Test
+
+class ImmersiveListTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ @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
+ .testTag("background-$it")
+ .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)
+ }
+
+ Box(modifier.focusableItem(index)) { BasicText("card-$index") }
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle { firstCard.requestFocus() }
+
+ rule.onNodeWithTag("card-0").assertIsFocused()
+ rule.onNodeWithTag("background-0").assertIsDisplayed()
+ rule.onNodeWithTag("background-1").assertDoesNotExist()
+ rule.onNodeWithTag("background-2").assertDoesNotExist()
+
+ rule.waitForIdle()
+ keyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+ rule.onNodeWithTag("card-1").assertIsFocused()
+ rule.onNodeWithTag("background-1").assertIsDisplayed()
+ rule.onNodeWithTag("background-0").assertDoesNotExist()
+ rule.onNodeWithTag("background-2").assertDoesNotExist()
+
+ rule.waitForIdle()
+ keyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
+
+ rule.onNodeWithTag("card-0").assertIsFocused()
+ rule.onNodeWithTag("background-0").assertIsDisplayed()
+ rule.onNodeWithTag("background-1").assertDoesNotExist()
+ rule.onNodeWithTag("background-2").assertDoesNotExist()
+ }
+
+ private fun keyPress(keyCode: Int, numberOfPresses: Int = 1) {
+ for (index in 0 until numberOfPresses)
+ InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keyCode)
+ }
+}
\ No newline at end of file
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material/pager/PagerTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material/pager/PagerTest.kt
deleted file mode 100644
index 23df054..0000000
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material/pager/PagerTest.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.material.pager
-
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.tv.material.ExperimentalTvMaterialApi
-import org.junit.Rule
-import org.junit.Test
-
-class PagerTest {
- @get:Rule
- val rule = createComposeRule()
-
- @OptIn(ExperimentalTvMaterialApi::class)
- @Test
- fun pager_zeroSlideCount_drawsSomething() {
- val testTag = "pager"
- rule.setContent {
- Pager(slideCount = 0, modifier = Modifier.testTag(testTag)) {}
- }
-
- rule.onNodeWithTag(testTag).assertExists()
- }
-}
\ No newline at end of file
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt b/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
index ebd71f6..d579e94a 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
@@ -16,11 +16,14 @@
package androidx.tv.material.carousel
+import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
+import androidx.compose.animation.with
import androidx.compose.foundation.background
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Arrangement
@@ -49,7 +52,6 @@
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.unit.dp
import androidx.tv.material.ExperimentalTvMaterialApi
-import androidx.tv.material.pager.Pager
import java.lang.Math.floorMod
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
@@ -69,7 +71,7 @@
*/
@Suppress("IllegalExperimentalApiUsage")
-@OptIn(ExperimentalComposeUiApi::class)
+@OptIn(ExperimentalComposeUiApi::class, ExperimentalAnimationApi::class)
@ExperimentalTvMaterialApi
@Composable
fun Carousel(
@@ -82,7 +84,9 @@
carouselIndicator:
@Composable BoxScope.() -> Unit = {
CarouselDefaults.Indicator(
- modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp),
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .padding(16.dp),
carouselState = carouselState,
slideCount = slideCount)
},
@@ -105,13 +109,10 @@
}
}
.focusable()) {
- Pager(
- enterTransition = enterTransition,
- exitTransition = exitTransition,
- currentSlide = carouselState.slideIndex,
- slideCount = slideCount
+ AnimatedContent(
+ targetState = carouselState.slideIndex,
+ transitionSpec = { enterTransition.with(exitTransition) }
) { content.invoke(it) }
-
this.carouselIndicator()
}
}
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/ImmersiveList.kt
new file mode 100644
index 0000000..091c16e
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material/immersivelist/ImmersiveList.kt
@@ -0,0 +1,191 @@
+/*
+ * 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.material.immersivelist
+
+import androidx.compose.animation.AnimatedContentScope
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.ContentTransform
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.core.tween
+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
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusDirection
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.tv.material.ExperimentalTvMaterialApi
+
+/**
+ * Immersive List consists of a list with multiple items and a background that displays content
+ * based on the item in focus.
+ * To animate the background's entry and exit, use [ImmersiveListBackgroundScope.AnimatedContent].
+ * To display the background only when the list is in focus, use
+ * [ImmersiveListBackgroundScope.AnimatedVisibility].
+ *
+ * @param background Composable defining the background to be displayed for a given item's
+ * index.
+ * @param modifier applied to Immersive List.
+ * @param listAlignment Alignment of the List with respect to the Immersive List.
+ * @param list composable defining the list of items that has to be rendered.
+ */
+@Suppress("IllegalExperimentalApiUsage")
+@OptIn(ExperimentalComposeUiApi::class)
+@ExperimentalTvMaterialApi
+@Composable
+fun ImmersiveList(
+ background:
+ @Composable ImmersiveListBackgroundScope.(index: Int, listHasFocus: Boolean) -> Unit,
+ modifier: Modifier = Modifier,
+ listAlignment: Alignment = Alignment.BottomEnd,
+ list: @Composable ImmersiveListScope.() -> Unit,
+) {
+ var currentItemIndex by remember { mutableStateOf(0) }
+ var listHasFocus by remember { mutableStateOf(false) }
+
+ Box(modifier) {
+ ImmersiveListBackgroundScope(this).background(currentItemIndex, listHasFocus)
+
+ val focusManager = LocalFocusManager.current
+
+ Box(Modifier.align(listAlignment).onFocusChanged { listHasFocus = it.hasFocus }) {
+ ImmersiveListScope {
+ currentItemIndex = it
+ focusManager.moveFocus(FocusDirection.Enter)
+ }.list()
+ }
+ }
+}
+
+@ExperimentalTvMaterialApi
+object ImmersiveListDefaults {
+ /**
+ * Default transition used to bring the background content into view
+ */
+ val EnterTransition: EnterTransition = fadeIn(animationSpec = tween(300))
+
+ /**
+ * Default transition used to remove the background content from view
+ */
+ val ExitTransition: ExitTransition = fadeOut(animationSpec = tween(300))
+}
+
+@Immutable
+@ExperimentalTvMaterialApi
+public class ImmersiveListBackgroundScope internal constructor(boxScope: BoxScope) : BoxScope
+by boxScope {
+
+ /**
+ * [ImmersiveListBackgroundScope.AnimatedVisibility] composable animates the appearance and
+ * disappearance of its content, as [visible] value changes. Different [EnterTransition]s and
+ * [ExitTransition]s can be defined in [enter] and [exit] for the appearance and disappearance
+ * animation.
+ *
+ * @param visible defines whether the content should be visible
+ * @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 content Content to appear or disappear based on the value of [visible]
+ *
+ * @link androidx.compose.animation.AnimatedVisibility
+ * @see androidx.compose.animation.AnimatedVisibility
+ * @see EnterTransition
+ * @see ExitTransition
+ * @see AnimatedVisibilityScope
+ */
+ @Composable
+ fun AnimatedVisibility(
+ visible: Boolean,
+ modifier: Modifier = Modifier,
+ enter: EnterTransition = ImmersiveListDefaults.EnterTransition,
+ exit: ExitTransition = ImmersiveListDefaults.ExitTransition,
+ label: String = "AnimatedVisibility",
+ content: @Composable AnimatedVisibilityScope.() -> Unit
+ ) {
+ androidx.compose.animation.AnimatedVisibility(
+ visible,
+ modifier,
+ enter,
+ exit,
+ label,
+ content)
+ }
+
+ /**
+ * [ImmersiveListBackgroundScope.AnimatedContent] is a container that automatically animates its
+ * content when [targetState] changes. Its [content] for different target states is defined in a
+ * mapping between a target state and a composable function.
+ *
+ * @param targetState defines the key to choose the content to be displayed
+ * @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 content Content to appear or disappear based on the value of [targetState]
+ *
+ * @link androidx.compose.animation.AnimatedContent
+ * @see androidx.compose.animation.AnimatedContent
+ * @see ContentTransform
+ * @see AnimatedContentScope
+ */
+ @Suppress("IllegalExperimentalApiUsage")
+ @ExperimentalAnimationApi
+ @Composable
+ fun AnimatedContent(
+ targetState: Int,
+ modifier: Modifier = Modifier,
+ transitionSpec: AnimatedContentScope<Int>.() -> ContentTransform = {
+ ImmersiveListDefaults.EnterTransition.with(ImmersiveListDefaults.ExitTransition)
+ },
+ contentAlignment: Alignment = Alignment.TopStart,
+ content: @Composable AnimatedVisibilityScope.(targetState: Int) -> Unit
+ ) {
+ androidx.compose.animation.AnimatedContent(
+ targetState,
+ modifier,
+ transitionSpec,
+ contentAlignment,
+ content)
+ }
+}
+
+@Immutable
+@ExperimentalTvMaterialApi
+public 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.
+ *
+ * @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()
+ }
+}
\ No newline at end of file
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/pager/Pager.kt b/tv/tv-material/src/main/java/androidx/tv/material/pager/Pager.kt
deleted file mode 100644
index 24f88ed..0000000
--- a/tv/tv-material/src/main/java/androidx/tv/material/pager/Pager.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.tv.material.pager
-
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.EnterTransition
-import androidx.compose.animation.ExitTransition
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.with
-import androidx.compose.foundation.layout.Box
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.tv.material.ExperimentalTvMaterialApi
-
-/**
- * Composable that accepts a lambda that generates the slides based on the index provided and
- * displays the slide associated with the index [currentSlide].
- *
- * @param modifier the modifier to apply to this component.
- * @param enterTransition defines how the slide is animated into view.
- * @param exitTransition defines how the slide is animated out of view.
- * @param currentSlide the slide that is currently displayed by the pager.
- * @param slideCount the total number of slides.
- * @param content defines the slide composable for a given index.
- */
-@Suppress("IllegalExperimentalApiUsage")
-@OptIn(ExperimentalAnimationApi::class)
-@ExperimentalTvMaterialApi
-@Composable
-internal fun Pager(
- slideCount: Int,
- modifier: Modifier = Modifier,
- enterTransition: EnterTransition = PagerDefaults.EnterTransition,
- exitTransition: ExitTransition = PagerDefaults.ExitTransition,
- currentSlide: Int = 0,
- content: @Composable (index: Int) -> Unit
-) {
- if (slideCount <= 0) {
- Box(modifier)
- } else {
- AnimatedContent(
- modifier = modifier,
- targetState = currentSlide.coerceIn(0, slideCount - 1),
- transitionSpec = { enterTransition.with(exitTransition) }
- ) {
- content(it)
- }
- }
-}
-
-@ExperimentalTvMaterialApi
-private object PagerDefaults {
- /**
- * Default transition used to bring a slide into view
- */
- val EnterTransition: EnterTransition = fadeIn(animationSpec = tween(900))
-
- /**
- * Default transition used to remove a slide from view
- */
- val ExitTransition: ExitTransition = fadeOut(animationSpec = tween(900))
-}