Search code examples
androidkotlinandroid-jetpack-compose

Why scroll doesn't work in Column? (jetpack compose)


I need to make a layout like this: enter image description here

The layout is: an image with fixed size and a layout with grey background that should fill the remaining space of the screen. These elements should be scrollable. Though the layout is quite simple I can't handle it. The reason is that I need to to add .weight(1f) modifier to make grey background fill the rest of the screen, but it disables scroll instead. And vice versa, without this modifier scroll works properly, but grey background doesn't fill the rest of the screen.

I suppose scroll doesn't work because it need to know size of it's content and it doesn't. But I could be wrong. Could you please help me to fix this problem?

    @Composable
fun TestCompose(
    text: String,
    modifier: Modifier = Modifier,
) {
    val scrollState = rememberScrollState()

    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(color = Color.White),
    ) {
        Column(
            modifier = Modifier
                .verticalScroll(state = scrollState)
                .weight(1f)
        ) {

            Image(
                modifier = Modifier
                    .fillMaxWidth()
                    .background(Color.White)
                    .align(Alignment.CenterHorizontally),
                painter = painterResource(id = drawable.check_status_icon),
                contentDescription = null
            )

            Column(
                modifier = Modifier
                    .weight(1f) // This line makes grey background fill the rest of the screen as it is demanded, but disables scroll instead.
                    .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp))
                    .background(Color.LightGray)
            ) {
                // Here are different optional custom layouts inside a column but I left Text only
                Text(text = text)
                AnotherLayout()
            }

        }


        Button(
            modifier = Modifier.fillMaxWidth(),
            onClick = {}){
            Text("Go to main", fontSize = 25.sp)
        }
    }
}

Btw if I use .fillMaxHeight() modifier instead of .weight(1f) the layout wraps content. This is behaviour I don't understand as well:

enter image description here


Solution

  • There is nothing wrong how scroll works and assigning Modifier.weight(1f) doesn't disable scroll, it lets content to fit parent perfectly, scroll is still being invoked but there is no place to scroll. You can verify this with scrollState.isScrollInProgress.

    When you assign Modifier.weight Column gets exact size that is left from Image above. And total height of contents or child Composables of Column with Modifier.verticalScroll() is equal to itself and because of that you can't scroll.

    If you change that sample that takes 4 Boxes with each 100.dp height

    @Preview
    @Composable
    fun TestCompose() {
        val scrollState = rememberScrollState()
    
        Column(
            modifier = Modifier
                .fillMaxSize()
                .background(color = Color.White),
        ) {
            Column(
                modifier = Modifier
                    .verticalScroll(state = scrollState)
                    .weight(1f)
            ) {
    
                Image(
                    modifier = Modifier
                        .fillMaxWidth()
                        .background(Color.White)
                        .align(Alignment.CenterHorizontally),
                    painter = painterResource(id = R.drawable.avatar_1_raster),
                    contentScale = ContentScale.FillWidth,
                    contentDescription = null
                )
    
                val density = LocalDensity.current
    
                var height by remember {
                    mutableStateOf(0.dp)
                }
    
                Text("Column height: $height")
    
                Column(
                    modifier = Modifier
                        .onSizeChanged {
                            height = with(density) {
                                it.height.toDp()
                            }
                        }
                        .border(8.dp, Color.Black)
                        .weight(1f) // This line makes grey background fill the rest of the screen as it is demanded, but disables scroll instead.
                        .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp))
                        .background(Color.LightGray)
                ) {
    
                    Box(modifier = Modifier.fillMaxWidth().height(100.dp).background(Color.Red))
                    Box(modifier = Modifier.fillMaxWidth().height(100.dp).background(Color.Yellow))
                    Box(modifier = Modifier.fillMaxWidth().height(100.dp).background(Color.Green))
                    Box(modifier = Modifier.fillMaxWidth().height(100.dp).background(Color.Blue))
    
                }
    
            }
    
            Button(
                modifier = Modifier.fillMaxWidth(),
                onClick = {}) {
                Text("Go to main", fontSize = 25.sp)
            }
        }
    }
    

    And based on your image and device screen size you will see something like

    It tells that Column with black border fits screen completely while some of its content, Box with Blue background, is not fit because its total height is 293.dp which is remaining space after Image and Text is measured, and fits completely.

    If you comment weight in inner Column you will see that its total height is 400.dp and border goes below Blue box and since 400.dp is bigger than available place, it was 293.dp, you can scroll (400-293).dp

    You can check this below while content of Column also equal to its height.

    @Preview
    @Composable
    fun ScrollTest() {
    
        val scrollState = rememberScrollState()
        Column {
    
            Text("Is scrolling: ${scrollState.isScrollInProgress}")
            Column(
                modifier = Modifier.fillMaxWidth().height(200.dp).border(2.dp, Color.Blue)
                    .verticalScroll(scrollState)
            ) {
                Box(modifier = Modifier.fillMaxWidth().height(100.dp).background(Color.Red))
                Box(modifier = Modifier.fillMaxWidth().height(100.dp).background(Color.Yellow))
    //        Box(modifier = Modifier.fillMaxWidth().height(100.dp).background(Color.Green))
            }
        }
    }
    

    then uncomment Green box and you will see that scroll

    You can consider it like having a LazyColumn that all elements exactly fits its height, do you expect it to scroll with an empty space below? For scroll to happen dimension of contents of Composable should be bigger in orientation of scroll gesture.

    For instance, if your Column has 200.dp and total height of its content is 200.dp you won't be able to see its content move up or down.

    It's not clear how big height should be in image on the left for it to scroll while there is no content.