Use BringIntoViewResponder to bring element into view.
By default, scrollable-containers ensure that the focused element
is brought into view. We need to bring the entire FeaturedCarousel
or ImmersiveList into view even when a sub-composable is in focus.
The earlier approach called BringIntoViewRequester.bringIntoView.
This change uses a custom BringIntoViewResponder to intercept the
request to bringIntoView and return the entire FeaturedCarousel
or ImmersiveList rectangle rather than only the rectangle of the
focused sub-composable.
Test: Existing tests cover this change.
Relnote: NA
Change-Id: I232436a2c7ee3a72fdac762463c5167470964400
diff --git a/tv/tv-material/api/current.txt b/tv/tv-material/api/current.txt
index 85bc920..0c788a4 100644
--- a/tv/tv-material/api/current.txt
+++ b/tv/tv-material/api/current.txt
@@ -1,6 +1,9 @@
// Signature format: 4.0
package androidx.tv.material {
+ public final class BringIntoViewIfChildrenAreFocusedKt {
+ }
+
public final class ContentColorKt {
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> getLocalContentColor();
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
diff --git a/tv/tv-material/api/public_plus_experimental_current.txt b/tv/tv-material/api/public_plus_experimental_current.txt
index 05824aa..1b4f018 100644
--- a/tv/tv-material/api/public_plus_experimental_current.txt
+++ b/tv/tv-material/api/public_plus_experimental_current.txt
@@ -1,6 +1,9 @@
// Signature format: 4.0
package androidx.tv.material {
+ public final class BringIntoViewIfChildrenAreFocusedKt {
+ }
+
public final class ContentColorKt {
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> getLocalContentColor();
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
diff --git a/tv/tv-material/api/restricted_current.txt b/tv/tv-material/api/restricted_current.txt
index 85bc920..0c788a4 100644
--- a/tv/tv-material/api/restricted_current.txt
+++ b/tv/tv-material/api/restricted_current.txt
@@ -1,6 +1,9 @@
// Signature format: 4.0
package androidx.tv.material {
+ public final class BringIntoViewIfChildrenAreFocusedKt {
+ }
+
public final class ContentColorKt {
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> getLocalContentColor();
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/BringIntoViewIfChildrenAreFocused.kt b/tv/tv-material/src/main/java/androidx/tv/material/BringIntoViewIfChildrenAreFocused.kt
new file mode 100644
index 0000000..0c4e5a8
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material/BringIntoViewIfChildrenAreFocused.kt
@@ -0,0 +1,55 @@
+/*
+ * 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
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.relocation.BringIntoViewResponder
+import androidx.compose.foundation.relocation.bringIntoViewResponder
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.debugInspectorInfo
+
+@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
+@OptIn(ExperimentalFoundationApi::class)
+internal fun Modifier.bringIntoViewIfChildrenAreFocused(): Modifier = composed(
+ inspectorInfo = debugInspectorInfo { name = "bringIntoViewIfChildrenAreFocused" },
+ factory = {
+ var myRect: Rect = Rect.Zero
+ this
+ .onSizeChanged {
+ myRect = Rect(Offset.Zero, Offset(it.width.toFloat(), it.height.toFloat()))
+ }
+ .bringIntoViewResponder(
+ remember {
+ object : BringIntoViewResponder {
+ // return the current rectangle and ignoring the child rectangle received.
+ @ExperimentalFoundationApi
+ override fun calculateRectForParent(localRect: Rect): Rect = myRect
+
+ // The container is not expected to be scrollable. Hence the child is
+ // already in view with respect to the container.
+ @ExperimentalFoundationApi
+ override suspend fun bringChildIntoView(localRect: () -> Rect?) {}
+ }
+ }
+ )
+ }
+)
\ 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 6200545..61500e9 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
@@ -58,6 +58,7 @@
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.tv.material.ExperimentalTvMaterialApi
+import androidx.tv.material.bringIntoViewIfChildrenAreFocused
import java.lang.Math.floorMod
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
@@ -111,7 +112,9 @@
carouselState,
focusState,
onAutoScrollChange = { isAutoScrollActive = it })
+
Box(modifier = modifier
+ .bringIntoViewIfChildrenAreFocused()
.focusRequester(carouselOuterBoxFocusRequester)
.onFocusChanged {
focusState = it
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt b/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt
index 6f34d1e..c676109 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt
@@ -25,14 +25,11 @@
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.relocation.BringIntoViewRequester
-import androidx.compose.foundation.relocation.bringIntoViewRequester
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
@@ -45,7 +42,6 @@
import androidx.tv.material.ExperimentalTvMaterialApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.launch
/**
* This composable is intended for use in Carousel.
@@ -77,33 +73,19 @@
val overlayVisible = remember { MutableTransitionState(initialState = false) }
var focusState: FocusState? by remember { mutableStateOf(null) }
val focusManager = LocalFocusManager.current
- val bringIntoViewRequester = remember { BringIntoViewRequester() }
- val coroutineScope = rememberCoroutineScope()
LaunchedEffect(overlayVisible) {
overlayVisible.onAnimationCompletion {
// slide has loaded completely.
- if (focusState?.isFocused == true) {
- // Using bringIntoViewRequester here instead of in Carousel.kt as when the focusable
- // item is within an animation, bringIntoView scrolls excessively and loses focus.
- // b/241591211
- // By using bringIntoView inside the snapshotFlow, we ensure that the focusable has
- // completed animating into position.
- bringIntoViewRequester.bringIntoView()
- focusManager.moveFocus(FocusDirection.Enter)
- }
+ if (focusState?.isFocused == true) { focusManager.moveFocus(FocusDirection.Enter) }
}
}
Box(modifier = modifier
- .bringIntoViewRequester(bringIntoViewRequester)
.onFocusChanged {
focusState = it
if (it.isFocused && overlayVisible.isIdle && overlayVisible.currentState) {
- coroutineScope.launch {
- bringIntoViewRequester.bringIntoView()
- focusManager.moveFocus(FocusDirection.Enter)
- }
+ focusManager.moveFocus(FocusDirection.Enter)
}
}
.focusable()) {
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
index 54acbd9..e750f38 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/ImmersiveList.kt
@@ -26,18 +26,14 @@
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.with
-import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
-import androidx.compose.foundation.relocation.BringIntoViewRequester
-import androidx.compose.foundation.relocation.bringIntoViewRequester
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.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
@@ -46,7 +42,7 @@
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.platform.LocalFocusManager
import androidx.tv.material.ExperimentalTvMaterialApi
-import kotlinx.coroutines.launch
+import androidx.tv.material.bringIntoViewIfChildrenAreFocused
/**
* Immersive List consists of a list with multiple items and a background that displays content
@@ -56,13 +52,13 @@
* [ImmersiveListBackgroundScope.AnimatedVisibility].
*
* @param background Composable defining the background to be displayed for a given item's
- * index.
+ * index. `listHasFocus` argument can be used to hide the background when the list is not in focus
* @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, ExperimentalFoundationApi::class)
+@OptIn(ExperimentalComposeUiApi::class)
@ExperimentalTvMaterialApi
@Composable
fun ImmersiveList(
@@ -74,19 +70,8 @@
) {
var currentItemIndex by remember { mutableStateOf(0) }
var listHasFocus by remember { mutableStateOf(false) }
- val bringIntoViewRequester = remember { BringIntoViewRequester() }
- val coroutineScope = rememberCoroutineScope()
- Box(modifier
- .bringIntoViewRequester(bringIntoViewRequester)
- .onFocusChanged {
- if (it.isFocused) {
- coroutineScope.launch {
- bringIntoViewRequester.bringIntoView()
- }
- }
- }
- ) {
+ Box(modifier.bringIntoViewIfChildrenAreFocused()) {
ImmersiveListBackgroundScope(this).background(currentItemIndex, listHasFocus)
val focusManager = LocalFocusManager.current
@@ -151,7 +136,8 @@
enter,
exit,
label,
- content)
+ content
+ )
}
/**
@@ -205,4 +191,4 @@
return onFocusChanged { if (it.hasFocus || it.isFocused) { onFocused(index) } }
.focusable()
}
-}
\ No newline at end of file
+}