Jetpack Compose - Modify scroll behavior to hold focus on a fix position on screen

1.9k Views Asked by At

Using jetpack compose to create an AP for TV platforms and I've been trying to create a row in jetpack compose which scrolls about the fix position on screen, here is the target behaviour as observed on google TV launcher:

Target Behaviour

Here we can see that the view is fixed on the lefthand side of the screen while the entire row moves around it

And how here is the current behaviour with my lazyrow composable:

Current Behaviour

As we can see here, the focus moves all the way to the right or the left before the list starts moving around it.

Any help in making the lazyrow scroll behave like in the first example?

2

There are 2 best solutions below

3
On BEST ANSWER

You can make use of TvLazyRow from the Compose for TV library which solves exactly this problem. It is part of the androidx.tv.foundation.lazy.list package.

To use it, you can make use of the pivotOffsets argument which accepts a PivotOffsets class instance. To that, you can pass 2 arguments:

  • parentFraction which defines the offset of the starting edge of the child element from the starting edge of the parent element
  • childFraction defines the offset of the starting edge of the child from the pivot defined by parentFraction

Following is the sample usage which places the item exactly at the center:

import androidx.tv.foundation.lazy.list.TvLazyRow

TvLazyRow(
    pivotOffsets = PivotOffsets(0.5f, 0.5f),
    horizontalArrangement = Arrangement.spacedBy(20.dp)
) {
    items(10) {
        Card(backgroundColor = Color.Red)
    }
}

If you want the Google TV launcher like look, you can update the PivotOffsets to the following:

import androidx.tv.foundation.lazy.list.TvLazyRow

TvLazyRow(
    pivotOffsets = PivotOffsets(0.1f, 0f),
    horizontalArrangement = Arrangement.spacedBy(20.dp)
) {
    items(10) {
        Card(backgroundColor = Color.Red)
    }
}
0
On

We can use the snap fling behavior that would snap at the middle item but the video you provided has snapping enabled on the first visible item, there's no property of snap fling that we could change to snap at the first item.

So I have implemented something that calculates and stop at the very first item and highlights it.

I did not see that you were implementing it for tv and I created a sample for mobile :___( . If there is any tv specific api available for this behavior you should surely go for that, if not then you can have a look at my implementation.

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun MainScreen(
    modifier: Modifier
) {
    
    val lazyRowState = rememberLazyListState()
    val snappingLayout = remember(lazyRowState) { SnapLayoutInfoProvider(lazyRowState) }
    val flingBehavior = rememberSnapFlingBehavior(snappingLayout)

    Box(
        modifier = modifier
        .fillMaxSize()
    ) {
        val firstVisibleItem by remember {
            derivedStateOf {
                lazyRowState.layoutInfo.visibleItemsInfo.firstOrNull()
            }
        }
        val viewPortStartOffset by remember {
            derivedStateOf {
                lazyRowState.layoutInfo.viewportStartOffset
            }
        }
        LaunchedEffect(key1 = firstVisibleItem ) {
            firstVisibleItem?.let {
                if(it.offset < viewPortStartOffset){
                    lazyRowState.animateScrollToItem(it.index)

                }
            }
        }
        LazyRow(
            modifier = Modifier
                .align(Alignment.Center)
                .wrapContentSize(),
            state = lazyRowState,
            flingBehavior = flingBehavior
        ) {

            items(50) { index ->
                val isFirstItem by remember { derivedStateOf { lazyRowState.firstVisibleItemIndex == index} }
                val scale by animateFloatAsState(targetValue = if(isFirstItem) 1.2f else 1f,
                    label = "scale animation"
                )
                Card(
                    modifier = Modifier
                        .padding(start = 30.dp, end = 10.dp)
                        .size(80.dp)
                        .scale(scale = scale)
                        .border(
                            if (isFirstItem) 2.dp else 0.dp,
                            if (isFirstItem) Color.White else Color.Transparent,
                            RoundedCornerShape(15.dp)
                        )
                        ,
                    colors = CardDefaults.cardColors(Color.Blue),
                    shape = RoundedCornerShape(15.dp),

                    ) {
                    Text(text = "$index")
                }
            }
        }
    }
}

I am actually just calculating if the offset of very first visible item goes out of the bounds of viewport and if it does then scrolling the lazy list to that item.

visual representation of my masterpiece