update tab-row default spacing
Refactor the demos application
Test: NA
Relnote: NA
Change-Id: I83429a816cefa21aec877c15df42f47a49376034
diff --git a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/App.kt b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/App.kt
index 00ea81e..d966d00 100644
--- a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/App.kt
+++ b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/App.kt
@@ -16,8 +16,10 @@
package androidx.tv.integration.demos
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -25,22 +27,26 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.darkColorScheme
+val pageColor = Color.Black
+
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun App() {
var selectedTab by remember { mutableStateOf(Navigation.FeaturedCarousel) }
- MaterialTheme(
- colorScheme = darkColorScheme()
- ) {
+ MaterialTheme(colorScheme = darkColorScheme()) {
Column(
- modifier = Modifier.padding(20.dp),
- verticalArrangement = Arrangement.spacedBy(20.dp)
+ modifier = Modifier
+ .background(pageColor)
+ .fillMaxSize()
+ .padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(20.dp),
) {
TopNavigation(updateSelectedTab = { selectedTab = it })
selectedTab.action.invoke()
diff --git a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/Card.kt b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/Card.kt
new file mode 100644
index 0000000..db5bbb1
--- /dev/null
+++ b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/Card.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2023 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.integration.demos
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+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.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun Card(
+ modifier: Modifier = Modifier,
+ backgroundColor: Color = Color.Transparent,
+) {
+ Box(
+ modifier = modifier
+ .background(backgroundColor.copy(alpha = 0.3f))
+ .width(200.dp)
+ .height(150.dp)
+ .drawBorderOnFocus()
+ .focusable()
+ )
+}
\ No newline at end of file
diff --git a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/FeaturedCarousel.kt b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/FeaturedCarousel.kt
index 6a5007f..bc21280 100644
--- a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/FeaturedCarousel.kt
+++ b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/FeaturedCarousel.kt
@@ -42,8 +42,8 @@
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.compose.ui.unit.dp
+import androidx.tv.foundation.ExperimentalTvFoundationApi
import androidx.tv.material3.Carousel
import androidx.tv.material3.CarouselDefaults
import androidx.tv.material3.CarouselState
@@ -88,15 +88,9 @@
}
}
-@Composable
-fun Modifier.drawBorderOnFocus(borderColor: Color = Color.White, width: Dp = 5.dp): Modifier {
- var isFocused by remember { mutableStateOf(false) }
- return this
- .border(width, borderColor.copy(alpha = if (isFocused) 1f else 0.2f))
- .onFocusChanged { isFocused = it.isFocused }
-}
-
-@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class)
+@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class,
+ ExperimentalTvFoundationApi::class
+)
@Composable
internal fun FeaturedCarousel(modifier: Modifier = Modifier) {
val backgrounds = listOf(
@@ -111,35 +105,42 @@
)
val carouselState = remember { CarouselState() }
- Carousel(
- itemCount = backgrounds.size,
- carouselState = carouselState,
- modifier = modifier
- .height(300.dp)
- .fillMaxWidth(),
- carouselIndicator = {
- CarouselDefaults.IndicatorRow(
- itemCount = backgrounds.size,
- activeItemIndex = carouselState.activeItemIndex,
- modifier = Modifier
- .align(Alignment.BottomEnd)
- .padding(16.dp),
- )
- }
- ) { itemIndex ->
- CarouselItem(
- background = {
- Box(
+ FocusGroup {
+ Carousel(
+ itemCount = backgrounds.size,
+ carouselState = carouselState,
+ modifier = modifier
+ .height(300.dp)
+ .fillMaxWidth(),
+ carouselIndicator = {
+ CarouselDefaults.IndicatorRow(
+ itemCount = backgrounds.size,
+ activeItemIndex = carouselState.activeItemIndex,
modifier = Modifier
- .background(backgrounds[itemIndex])
- .fillMaxSize()
+ .align(Alignment.BottomEnd)
+ .padding(16.dp),
)
}
- ) {
- Box(modifier = Modifier) {
- OverlayButton(
- modifier = Modifier
- )
+ ) { itemIndex ->
+ CarouselItem(
+ background = {
+ Box(
+ modifier = Modifier
+ .background(backgrounds[itemIndex])
+ .fillMaxSize()
+ )
+ },
+ modifier =
+ if (itemIndex == 0)
+ Modifier.initiallyFocused()
+ else
+ Modifier.restorableFocus()
+ ) {
+ Box(modifier = Modifier) {
+ OverlayButton(
+ modifier = Modifier
+ )
+ }
}
}
}
diff --git a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/FocusGroup.kt b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/FocusGroup.kt
new file mode 100644
index 0000000..6d19cbd
--- /dev/null
+++ b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/FocusGroup.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2023 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.integration.demos
+
+import android.annotation.SuppressLint
+import android.util.Log
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.currentCompositeKeyHash
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusDirection
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.tv.foundation.ExperimentalTvFoundationApi
+
+/**
+ * Composable container that provides modifier extensions to allow focus to be restored to the
+ * element that was previously focused within the TvFocusGroup.
+ *
+ * @param modifier the modifier to apply to this group.
+ * @param content the content that is present within the group and can use focus-group modifier
+ * extensions.
+ */
+@OptIn(ExperimentalComposeUiApi::class)
+@ExperimentalTvFoundationApi
+@Composable
+fun FocusGroup(
+ modifier: Modifier = Modifier,
+ content: @Composable FocusGroupScope.() -> Unit
+) {
+ val focusManager = LocalFocusManager.current
+ val focusGroupKeyHash = currentCompositeKeyHash
+
+ // TODO: Is this the intended way to call rememberSaveable
+ // with key set to parentHash?
+ val previousFocusedItemHash: MutableState<Int?> = rememberSaveable(
+ key = focusGroupKeyHash.toString()
+ ) {
+ mutableStateOf(null)
+ }
+
+ val state = FocusGroupState(previousFocusedItemHash = previousFocusedItemHash)
+
+ Box(
+ modifier = modifier
+ .onFocusChanged {
+ if (it.isFocused) {
+ if (state.noRecordedState()) {
+ focusManager.moveFocus(FocusDirection.Enter)
+ } else {
+ if (state.focusRequester != FocusRequester.Default) {
+ try {
+ state.focusRequester.requestFocus()
+ } catch (e: Exception) {
+ Log.w("TvFocusGroup", "TvFocusGroup: Failed to request focus", e)
+ }
+ } else {
+ focusManager.moveFocus(FocusDirection.Enter)
+ }
+ }
+ }
+ }
+ .focusable(),
+ content = { FocusGroupScope(state).content() }
+ )
+}
+
+/**
+ * Scope containing the modifier extensions to be used within [FocusGroup].
+ */
+@ExperimentalTvFoundationApi
+class FocusGroupScope internal constructor(private val state: FocusGroupState) {
+ private var currentFocusableIdIndex = 0
+
+ private fun generateUniqueFocusableId(): Int = currentFocusableIdIndex++
+
+ /**
+ * Modifier that records if the item was in focus before it moved out of the group. When focus
+ * enters the [FocusGroup], the item will be returned focus.
+ */
+ @SuppressLint("ComposableModifierFactory")
+ @Composable
+ fun Modifier.restorableFocus(): Modifier =
+ this.restorableFocus(focusId = rememberSaveable { generateUniqueFocusableId() })
+
+ /**
+ * Modifier that marks the current composable as the item to gain focus initially when focus
+ * enters the [FocusGroup]. When focus enters the [FocusGroup], the item will be returned focus.
+ */
+ @SuppressLint("ComposableModifierFactory")
+ @Composable
+ fun Modifier.initiallyFocused(): Modifier {
+ val focusId = rememberSaveable { generateUniqueFocusableId() }
+ if (state.noRecordedState()) {
+ state.recordFocusedItemHash(focusId)
+ }
+ return this.restorableFocus(focusId)
+ }
+
+ @SuppressLint("ComposableModifierFactory")
+ @OptIn(ExperimentalComposeUiApi::class)
+ @Composable
+ private fun Modifier.restorableFocus(focusId: Int): Modifier {
+ val focusRequester = remember { FocusRequester() }
+ var isFocused = remember { false }
+ val isCurrentlyFocused by rememberUpdatedState(isFocused)
+ val focusManager = LocalFocusManager.current
+ state.associatedWith(focusId, focusRequester)
+ DisposableEffect(Unit) {
+ onDispose {
+ state.clearDisposedFocusRequester(focusId)
+ if (isCurrentlyFocused) {
+ focusManager.moveFocus(FocusDirection.Exit)
+ focusManager.moveFocus(FocusDirection.Enter)
+ }
+ }
+ }
+
+ return this
+ .focusRequester(focusRequester)
+ .onFocusChanged {
+ isFocused = it.isFocused || it.hasFocus
+ if (isFocused) {
+ state.recordFocusedItemHash(focusId)
+ state.associatedWith(focusId, focusRequester)
+ }
+ }
+ }
+}
+
+@Stable
+@ExperimentalTvFoundationApi
+internal class FocusGroupState(
+ private var previousFocusedItemHash: MutableState<Int?>
+) {
+ internal var focusRequester: FocusRequester = FocusRequester.Default
+ private set
+
+ internal fun recordFocusedItemHash(itemHash: Int) {
+ previousFocusedItemHash.value = itemHash
+ }
+
+ internal fun clearDisposedFocusRequester(itemHash: Int) {
+ if (previousFocusedItemHash.value == itemHash) {
+ focusRequester = FocusRequester.Default
+ }
+ }
+
+ internal fun associatedWith(itemHash: Int, focusRequester: FocusRequester) {
+ if (previousFocusedItemHash.value == itemHash) {
+ this.focusRequester = focusRequester
+ }
+ }
+
+ internal fun noRecordedState(): Boolean =
+ previousFocusedItemHash.value == null && focusRequester == FocusRequester.Default
+}
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 a690908..ac636f2 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
@@ -35,6 +35,7 @@
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.ExperimentalTvFoundationApi
import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.ImmersiveList
@@ -48,7 +49,7 @@
}
}
-@OptIn(ExperimentalTvMaterial3Api::class)
+@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalTvFoundationApi::class)
@Composable
private fun SampleImmersiveList() {
val immersiveListHeight = 300.dp
@@ -61,36 +62,44 @@
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) }
-
+ FocusGroup {
+ ImmersiveList(
+ modifier = Modifier
+ .height(immersiveListHeight + cardHeight / 2)
+ .fillMaxWidth(),
+ background = { index, _ ->
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")
- }
+ .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))
+ .then(
+ if (index == 0)
+ Modifier.initiallyFocused()
+ else
+ Modifier.restorableFocus()
+ )
+ .onFocusChanged { isFocused = it.isFocused }
+ .immersiveListItem(index)
+ .clickable {
+ Log.d("ImmersiveList", "Item $index was clicked")
+ }
+ )
+ }
+ }
}
}
}
diff --git a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/LazyRowsAndColumns.kt b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/LazyRowsAndColumns.kt
index 72f62aa..7abb30e 100644
--- a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/LazyRowsAndColumns.kt
+++ b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/LazyRowsAndColumns.kt
@@ -16,16 +16,12 @@
package androidx.tv.integration.demos
-import androidx.compose.foundation.background
-import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-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.ui.graphics.Color
import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.ExperimentalTvFoundationApi
import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.foundation.lazy.list.TvLazyRow
@@ -39,22 +35,25 @@
}
}
+@OptIn(ExperimentalTvFoundationApi::class)
@Composable
fun SampleLazyRow() {
val colors = listOf(Color.Red, Color.Magenta, Color.Green, Color.Yellow, Color.Blue, Color.Cyan)
val backgroundColors = List(columnsCount) { colors.random() }
- TvLazyRow(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
- backgroundColors.forEach { backgroundColor ->
- item {
- Box(
- modifier = Modifier
- .background(backgroundColor.copy(alpha = 0.3f))
- .width(200.dp)
- .height(150.dp)
- .drawBorderOnFocus()
- .focusable()
- )
+ FocusGroup {
+ TvLazyRow(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+ backgroundColors.forEachIndexed { index, backgroundColor ->
+ item {
+ Card(
+ backgroundColor = backgroundColor,
+ modifier =
+ if (index == 0)
+ Modifier.initiallyFocused()
+ else
+ Modifier.restorableFocus()
+ )
+ }
}
}
}
diff --git a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/NavigationDrawer.kt b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/NavigationDrawer.kt
new file mode 100644
index 0000000..95de519
--- /dev/null
+++ b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/NavigationDrawer.kt
@@ -0,0 +1,209 @@
+/*
+ * 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.integration.demos
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.background
+import androidx.compose.foundation.focusable
+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.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.KeyboardArrowLeft
+import androidx.compose.material.icons.filled.KeyboardArrowRight
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
+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.Modifier
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.zIndex
+import androidx.tv.foundation.ExperimentalTvFoundationApi
+import androidx.tv.material3.DrawerValue
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.Icon
+import androidx.tv.material3.NavigationDrawer
+import androidx.tv.material3.Text
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+fun StandardNavigationDrawer() {
+ val direction = remember { mutableStateOf(LayoutDirection.Ltr) }
+
+ CompositionLocalProvider(LocalLayoutDirection provides direction.value) {
+ Row(Modifier.fillMaxSize()) {
+ Box(modifier = Modifier.height(400.dp)) {
+ NavigationDrawer(
+ drawerContent = { drawerValue ->
+ Sidebar(
+ drawerValue = drawerValue,
+ direction = direction,
+ )
+ }
+ ) {
+ CommonBackground()
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+fun ModalNavigationDrawer() {
+ val direction = remember { mutableStateOf(LayoutDirection.Ltr) }
+
+ CompositionLocalProvider(LocalLayoutDirection provides direction.value) {
+ Row(Modifier.fillMaxSize()) {
+ Box(modifier = Modifier.height(400.dp)) {
+ androidx.tv.material3.ModalNavigationDrawer(
+ drawerContent = { drawerValue ->
+ Sidebar(
+ drawerValue = drawerValue,
+ direction = direction,
+ )
+ }
+ ) {
+ CommonBackground()
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun CommonBackground() {
+ Row(modifier = Modifier.padding(start = 10.dp)) {
+ Card(backgroundColor = Color.Red)
+ }
+}
+
+@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalTvFoundationApi::class)
+@Composable
+private fun Sidebar(
+ drawerValue: DrawerValue,
+ direction: MutableState<LayoutDirection>,
+) {
+ val selectedIndex = remember { mutableStateOf(0) }
+
+ LaunchedEffect(selectedIndex.value) {
+ direction.value = when (selectedIndex.value) {
+ 0 -> LayoutDirection.Ltr
+ else -> LayoutDirection.Rtl
+ }
+ }
+
+ FocusGroup {
+ Column(
+ modifier = Modifier
+ .fillMaxHeight()
+ .background(pageColor)
+ .focusable(false),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ NavigationItem(
+ imageVector = Icons.Default.KeyboardArrowRight,
+ text = "LTR",
+ drawerValue = drawerValue,
+ selectedIndex = selectedIndex,
+ index = 0,
+ modifier = Modifier.initiallyFocused(),
+ )
+ NavigationItem(
+ imageVector = Icons.Default.KeyboardArrowLeft,
+ text = "RTL",
+ drawerValue = drawerValue,
+ selectedIndex = selectedIndex,
+ index = 1,
+ modifier = Modifier.restorableFocus(),
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+private fun NavigationItem(
+ imageVector: ImageVector,
+ text: String,
+ drawerValue: DrawerValue,
+ selectedIndex: MutableState<Int>,
+ index: Int,
+ modifier: Modifier = Modifier,
+) {
+ var isFocused by remember { mutableStateOf(false) }
+
+ Button(
+ onClick = { selectedIndex.value = index },
+ modifier = modifier
+ .onFocusChanged { isFocused = it.isFocused },
+ colors = ButtonDefaults.filledTonalButtonColors(
+ containerColor = if (isFocused) Color.White else Color.Transparent,
+ )
+ ) {
+ Box(modifier = Modifier) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(5.dp),
+ ) {
+ Icon(
+ imageVector = imageVector,
+ tint = if (isFocused) pageColor else Color.White,
+ contentDescription = null,
+ )
+ AnimatedVisibility(visible = drawerValue == DrawerValue.Open) {
+ Text(
+ text = text,
+ modifier = Modifier,
+ softWrap = false,
+ color = if (isFocused) pageColor else Color.White,
+ )
+ }
+ }
+ if (selectedIndex.value == index) {
+ Box(
+ modifier = Modifier
+ .width(10.dp)
+ .height(3.dp)
+ .offset(y = 5.dp)
+ .align(Alignment.BottomCenter)
+ .background(Color.Red)
+ .zIndex(10f)
+ )
+ }
+ }
+ }
+}
diff --git a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/SampleModalNavDrawer.kt b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/SampleModalNavDrawer.kt
deleted file mode 100644
index e36b1df..0000000
--- a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/SampleModalNavDrawer.kt
+++ /dev/null
@@ -1,114 +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.integration.demos
-
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.shrinkHorizontally
-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.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxHeight
-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.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.material3.Button
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.tv.material3.DrawerValue
-import androidx.tv.material3.ExperimentalTvMaterial3Api
-import androidx.tv.material3.ModalNavigationDrawer
-
-@OptIn(ExperimentalTvMaterial3Api::class)
-@Composable
-fun SampleModalDrawer() {
- Row(Modifier.fillMaxSize()) {
- Box(modifier = Modifier
- .height(400.dp)
- .width(400.dp)
- .border(2.dp, Color.Magenta)) {
- ModalNavigationDrawer(drawerContent = drawerContent()) {
- Button(modifier = Modifier
- .height(100.dp)
- .fillMaxWidth(), onClick = {}) {
- Text("BUTTON")
- }
- }
- }
-
- CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
- Box(
- modifier = Modifier
- .height(400.dp)
- .width(400.dp)
- .border(2.dp, Color.Magenta)
- ) {
- ModalNavigationDrawer(drawerContent = drawerContent()) {
- Button(modifier = Modifier
- .height(100.dp)
- .fillMaxWidth(), onClick = {}) {
- Text("BUTTON")
- }
- }
- }
- }
- }
-}
-
-@Composable
-@OptIn(ExperimentalTvMaterial3Api::class)
-internal fun drawerContent(): @Composable (DrawerValue) -> Unit =
- {
- Column(Modifier.background(Color.Gray).fillMaxHeight()) {
- NavigationRow(it, Color.Red, "Red")
- NavigationRow(it, Color.Blue, "Blue")
- NavigationRow(it, Color.Yellow, "Yellow")
- }
- }
-
-@OptIn(ExperimentalTvMaterial3Api::class)
-@Composable
-private fun NavigationRow(drawerValue: DrawerValue, color: Color, text: String) {
- Row(Modifier.padding(10.dp).drawBorderOnFocus(width = 2.dp).focusable()) {
- Box(Modifier.size(50.dp).background(color).padding(end = 20.dp))
- AnimatedVisibility(
- drawerValue == DrawerValue.Open,
- // intentionally slow to test animation
- exit = shrinkHorizontally(tween(2000))
- ) {
- Text(
- text = text,
- softWrap = false,
- modifier = Modifier.padding(15.dp).width(50.dp),
- textAlign = TextAlign.Center
- )
- }
- }
-}
diff --git a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/SampleNavDrawer.kt b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/SampleNavDrawer.kt
deleted file mode 100644
index cf1ed6a..0000000
--- a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/SampleNavDrawer.kt
+++ /dev/null
@@ -1,72 +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.integration.demos
-
-import androidx.compose.foundation.border
-import androidx.compose.foundation.layout.Box
-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.width
-import androidx.compose.material3.Button
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.tv.material3.ExperimentalTvMaterial3Api
-import androidx.tv.material3.NavigationDrawer
-
-@OptIn(ExperimentalTvMaterial3Api::class)
-@Composable
-fun SampleDrawer() {
- Row(Modifier.fillMaxSize()) {
- Box(modifier = Modifier
- .height(400.dp)
- .width(400.dp)
- .border(2.dp, Color.Magenta)) {
- NavigationDrawer(drawerContent = drawerContent()) {
- Button(modifier = Modifier
- .height(100.dp)
- .fillMaxWidth(), onClick = {}) {
- Text("BUTTON")
- }
- }
- }
-
- CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
- Box(
- modifier = Modifier
- .height(400.dp)
- .width(400.dp)
- .border(2.dp, Color.Magenta)
- ) {
- NavigationDrawer(drawerContent = drawerContent()) {
- Button(modifier = Modifier
- .height(100.dp)
- .fillMaxWidth(), onClick = {}) {
- Text("BUTTON")
- }
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/TopNavigation.kt b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/TopNavigation.kt
index 95a6f7e..a41b4e5 100644
--- a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/TopNavigation.kt
+++ b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/TopNavigation.kt
@@ -16,9 +16,7 @@
package androidx.tv.integration.demos
-import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -28,6 +26,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.tv.foundation.ExperimentalTvFoundationApi
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Tab
import androidx.tv.material3.TabRow
@@ -35,13 +34,13 @@
import kotlinx.coroutines.delay
enum class Navigation(val displayName: String, val action: @Composable () -> Unit) {
- Drawer("Drawer", { SampleDrawer() }),
- ModalDrawer("Modal Drawer", { SampleModalDrawer() }),
+ StandardNavigationDrawer("Standard Navigation Drawer", { StandardNavigationDrawer() }),
+ ModalNavigationDrawer("Modal Navigation Drawer", { ModalNavigationDrawer() }),
LazyRowsAndColumns("Lazy Rows and Columns", { LazyRowsAndColumns() }),
FeaturedCarousel("Featured Carousel", { FeaturedCarouselContent() }),
ImmersiveList("Immersive List", { ImmersiveListContent() }),
- StickyHeader("Sticky Header", { StickyHeaderContent() }),
TextField("Text Field", { TextFieldContent() }),
+ StickyHeader("Sticky Header", { StickyHeaderContent() }),
}
@Composable
@@ -69,27 +68,31 @@
/**
* Pill indicator tab row for reference
*/
-@OptIn(ExperimentalTvMaterial3Api::class)
+@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalTvFoundationApi::class)
@Composable
fun PillIndicatorTabRow(
tabs: List<String>,
selectedTabIndex: Int,
updateSelectedTab: (Int) -> Unit
) {
- TabRow(
- selectedTabIndex = selectedTabIndex,
- separator = { Spacer(modifier = Modifier.width(12.dp)) },
- ) {
- tabs.forEachIndexed { index, tab ->
- Tab(
- selected = index == selectedTabIndex,
- onFocus = { updateSelectedTab(index) },
- ) {
- Text(
- text = tab,
- fontSize = 12.sp,
- modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp)
- )
+ FocusGroup {
+ TabRow(selectedTabIndex = selectedTabIndex) {
+ tabs.forEachIndexed { index, tab ->
+ Tab(
+ selected = index == selectedTabIndex,
+ onFocus = { updateSelectedTab(index) },
+ modifier =
+ if (tab == Navigation.StandardNavigationDrawer.displayName)
+ Modifier.initiallyFocused()
+ else
+ Modifier.restorableFocus()
+ ) {
+ Text(
+ text = tab,
+ fontSize = 12.sp,
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp)
+ )
+ }
}
}
}
diff --git a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/drawBorderOnFocus.kt b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/drawBorderOnFocus.kt
new file mode 100644
index 0000000..633887f
--- /dev/null
+++ b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/drawBorderOnFocus.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023 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.integration.demos
+
+import androidx.compose.foundation.border
+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.compose.ui.unit.dp
+
+@Composable
+fun Modifier.drawBorderOnFocus(borderColor: Color = Color.White, width: Dp = 5.dp): Modifier {
+ var isFocused by remember { mutableStateOf(false) }
+ return this
+ .border(width, borderColor.copy(alpha = if (isFocused) 1f else 0.2f))
+ .onFocusChanged { isFocused = it.isFocused }
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/TabRow.kt b/tv/tv-material/src/main/java/androidx/tv/material3/TabRow.kt
index a1af7e7..1b49193 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/TabRow.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/TabRow.kt
@@ -170,7 +170,7 @@
/** Space between tabs in the tab row */
@Composable
fun TabSeparator() {
- Spacer(modifier = Modifier.width(20.dp))
+ Spacer(modifier = Modifier.width(8.dp))
}
/** Default accent color for the TabRow */