PivotOffset without TvLazyList

Dawid Hyży
3 min readJun 28, 2024

--

After androidx.tv:tv-material was recently promoted to beta I started wondering why androidx.tv:tv-foundation didn’t. I looked into the change requests of tv-foundation and found one interesting feat: deprecate lazy layout forks from tv-foundation. More research brought me to an interesting PR merged into tv-samples. My conclusion was TvLazyLists are going away and androidx.tv:tv-foundation might too.

At my job, I build two TV apps that rely heavily on those components and their pivot offset capabilities. If you don’t know what pivot offset is, it allows you to keep the focused item in a certain position. You can find more about it in Google’s last year's blog post.

TvLazyRow pivot behavior. Source https://android-developers.googleblog.com/2023/05/building-pixel-perfect-living-room-experiences-compose-for-tv.html

But how can we achieve a pivot without TvLazyLists? Digging more into change requests that deprecate them gave me some clues - BringIntoViewSpec! While looking for information on how to use BringIntoViewSpec and LazyLists gits that solve this problem.

While adapting it to our project I found out that BringIntoViewSpec was added in Compose 1.7. I had to pump androidx.compose.foundation:foundation to 1.7.0-beta04 to have it available. And here is what I got:

/**
* Provides a [BringIntoViewSpec] that calculates the scroll offset for a child item in a LazyList
* based on a pivot offset.
*
* This allows for custom positioning of the child item within the visible area of the LazyList.
*
* @param parentFraction The fraction of the parent container that should be visible above the child item.
* @param childFraction The fraction of the child item that should be visible below the parent container.
* @param content The content to be placed inside containing the LazyList.
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ProvideLazyListPivotOffset(
parentFraction: Float = 0.3f,
childFraction: Float = 0f,
content: @Composable () -> Unit,
) {
val bringIntoViewSpec = object : BringIntoViewSpec {
override fun calculateScrollDistance(
offset: Float,
size: Float,
containerSize: Float
): Float = calculatePivotOffset(
parentFraction = parentFraction,
childFraction = childFraction,
offset = offset,
size = size,
containerSize = containerSize
)
}
CompositionLocalProvider(
LocalBringIntoViewSpec provides bringIntoViewSpec,
content = content,
)
}

/**
* Calculates the offset of the pivot point for an item requesting focus.
*
* @param parentFraction The fraction of the parent container that the item requesting focus is located.
* @param childFraction The fraction of the item requesting focus that is visible within the parent container.
* @param offset The initial position of the item requesting focus.
* @param size The size of the item requesting focus.
* @param containerSize The size of the lazy container.
* @return The offset of the pivot point.
*/
private fun calculatePivotOffset(
parentFraction: Float,
childFraction: Float,
offset: Float,
size: Float,
containerSize: Float
): Float {
val childSmallerThanParent = size <= containerSize
val initialTargetForLeadingEdge =
parentFraction * containerSize - (childFraction * size)
val spaceAvailableToShowItem = containerSize - initialTargetForLeadingEdge

val targetForLeadingEdge =
if (childSmallerThanParent && spaceAvailableToShowItem < size) {
containerSize - size
} else {
initialTargetForLeadingEdge
}

return offset - targetForLeadingEdge
}

Now we need to replace TvLazyList usage with LazyList and ProvideLazyListPivotOffset.

Replacing TvLazyColum with LazyColum wrapped with ProvideLazyListPivotOffset.

And we are done! After some research and googling we can no longer depend on deprecated tv-foundation’s components. I hope this will help you in the same migration🙂.

--

--