androidkotlinuser-interfaceandroid-jetpack-compose

Scrollable TimeText in Jetpack Compose


I am making an app for the Wear OS using Jetpack Compose. I want the UI to look somewhat like this:
enter image description here

This is the code that I have now:

@Composable
fun DefaultPreview() {
    TimeText()
    ScalingLazyColumn(
        Modifier.fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        item {
            Text(
                text = "Title",
                style = MaterialTheme.typography.title1,
                color = MaterialTheme.colors.primary
            )
        }
        item {
            Text(
                text = "Caption",
                style = MaterialTheme.typography.caption1,
                color = MaterialTheme.colors.primary
            )
        }
        item {
            Column(horizontalAlignment = Alignment.CenterHorizontally) {
                Text(text = "Text content")
                repeat(10) { Text("Line $it") }
            }
        }
    }
}

The problem that I have with this is that when I scroll the list, the text comes underneath the TimeText which doesn't really look that good.
enter image description here

So I want to make the TimeText scroll along with the other text elements. If I put the TimeText() as an item in the ScalingLazyColumn, then it does work, but now there's a huge empty gap below the TimeText which is apparently a CurvedLayout.
enter image description here

How do I get rid of this empty space? Is there a better way to make the TimeText scrollable?


Solution

  • The empty gap is generated due to a CurvedLayout which is created by TimeText, which has the dimensions equal to the size of the entire screen. Outside of a list, this wouldn't be a problem since our content would then just be superimposed on top of the CurvedLayout. But when we use TimeText in a list, the rest of the list items appear below TimeText, not on top of it.

    I couldn't figure out any way to make the TimeText behave as a list item while at the same time layering the other list items on top. I also couldn't figure out any way to cut off the empty portion of TimeText (Modifier.clip only changes what is drawn, it doesn't change the dimensions of the composable.

    So I tried a different approach, by giving a vertical offset to TimeText which would synchronize with the scroll of the ScalingLazyColumn. Unfortunately there is no direct way to get the absolute scroll value from a ScalingLazyListState, so I had to resort to a hacky method by using Modifier.onGloballyPositioned on the first item of the ScalingLazyColumn.

    @Composable
    fun DefaultPreview() {
        val scalingLazyListState = rememberScalingLazyListState()
    
        var initialTimeTextOffset by remember { mutableFloatStateOf(0f) }
        var timeTextOffset by remember { mutableFloatStateOf(0f) }
    
        TimeText(Modifier.offset(y=timeTextOffset.dp))
        ScalingLazyColumn(
            Modifier.fillMaxWidth(),
            horizontalAlignment = Alignment.CenterHorizontally,
            state = scalingLazyListState
        ) {
            item {
                Text(
                    text = "Title",
                    style = MaterialTheme.typography.title1,
                    color = MaterialTheme.colors.primary,
                    modifier = Modifier.onGloballyPositioned {
                        if (scalingLazyListState.centerItemIndex == 1 && scalingLazyListState.centerItemScrollOffset == 0)
                            initialTimeTextOffset = it.positionInRoot().y
                        
                        // for some reason dividing the whole thing by 2 seems to make the animation more 'natural'
                        timeTextOffset = (it.positionInRoot().y - initialTimeTextOffset)/2
                    }
                )
            }
            item {
                Text(
                    text = "Caption",
                    style = MaterialTheme.typography.caption1,
                    color = MaterialTheme.colors.primary
                )
            }
            item {
                Column(horizontalAlignment = Alignment.CenterHorizontally) {
                    Text(text = "Text content")
                    repeat(10) { Text("Line $it") }
                }
            }
        }
    }
    

    This approach works for me, so I will mark the question as answered for now. But I don't believe this is the 'correct' way of doing it, and this approach might also be bad performance-wise. So if anyone answers with a better approach, then I will mark that as the answer.