Search code examples
androidkotlinandroid-jetpack-compose

Best practice for conditionally positioning a view based on available height with vertical scroll in Compose


What I have is a column with vertical scroll, as components in it can be added/removed dynamically. What I am trying to achieve is position the button on the bottom of the screen, if the components don't fill it, and position it below them in the scrollable column if they do. I have implemented a solution, but it doesn't work perfectly at the threshold where the content height is near the screen height. I am wondering if there is a better way to achieve this effect. Any help is greatly appreciated!

BoxWithConstraints {
    val screenHeight = constraints.maxHeight
    val contentHeight = remember { mutableStateOf(0) }
    val bottomLayoutHeight = remember { mutableStateOf(0) }

    Column(
        Modifier
            .fillMaxWidth()
            .verticalScroll(rememberScrollState())
            .onGloballyPositioned { coordinates ->
                contentHeight.value = coordinates.size.height
            }
            .padding(horizontal = Constants.HORIZONTAL_PADDING)
    ) {
        // Indefinite views here
        // ...

        // If content does not fit within the screen, place the button inside the scrollable column
        if (contentHeight.value + bottomLayoutHeight.value >= screenHeight) {
            Column(Modifier.onGloballyPositioned { bottomLayoutHeight.value = it.size.height }) {
                BottomLayout(viewModel)
            }
        }
    }
    // If content fits within the screen, place the button outside the scrollable column
    if (contentHeight.value + bottomLayoutHeight.value < screenHeight) {
        Column(Modifier
            .fillMaxSize()
            .padding(horizontal = Constants.HORIZONTAL_PADDING),
            verticalArrangement = Arrangement.Bottom
        ) {
            Column(Modifier.onGloballyPositioned { bottomLayoutHeight.value = it.size.height }) {
                BottomLayout(viewModel)
            }
        }
    }
}

** UPDATE Code is modified to work perfectly even at thresholds. What I was missing was calculating the bottom layout height and adding it to the content height, to determine if its more or less than the screen height! ^^


Solution

  • You can detect whether the LazyColumn is scrollable or not. Then, if it is scrollable, display the button as the last element in the LazyColumn, otherwise display it at the bottom of the screen.

    Please try the following code:

    Column(
        horizontalAlignment = Alignment.End
    ) {
    
        val scrollState = rememberLazyListState()
        val canScroll by remember {
            derivedStateOf { scrollState.canScrollForward || scrollState.canScrollBackward }
        }
    
        LazyColumn(
            modifier = Modifier.weight(1f),
            horizontalAlignment = Alignment.End,
            state = scrollState
        ) {
            items(50) {
                Text(modifier = Modifier
                    .padding(16.dp)
                    .fillMaxWidth(), text = "ITEM $it");
            }
            item {
                if (canScroll) {
                    Button(onClick = { /*TODO*/ }) {
                        Text(text = "BUTTON")
                    }
                }
            }
        }
        if (!canScroll) {
            Button(onClick = { /*TODO*/ }) {
                Text(text = "BUTTON")
            }
        }
    }
    

    Output with a short list on the left and a long list on the right: