2

I want to achieve the following effect: Three sections - image, header, and text. When I scroll up, I want the image section to shrink until its gone, the two lower sections to move up and occupy the freed up space, the header to become a sticky header at the top, and then the text scroll to kick in last.

effect sought

My first attempt was with placing Modifier.verticalScroll both in the overall container and the child text, have the container view scroll first, shrink the image section until completely hidden, and then stop the container scroll (based on it reaching a certain scroll value) and then have the text scroll kick in. So, without going into specific detail, the principle was:

Container: .verticalScroll(scrollState, scrollState.value <= 99)
Child text: .verticalScroll(scrollState, scrollState.value > 99)

However, when I disable the container scroll, it also disables the child scroll.

So my next attempt was with Modifier.Scrollable. Per Jetpack Compose documentation:

The scrollable modifier differs from the scroll modifiers in that scrollable detects the scroll gestures, but does not offset its contents.The scrollable modifier does not affect the layout of the element it is applied to. This means that any changes to the element layout or its children must be handled through the delta provided by ScrollableState.

So that means the control is in our hands. This seems to be working (code below shown in action in the gif), but I am wondering if this is really the right way to go about it, or if there is a better way? (I still need to figure out exact offsets, but the q is more about the approach in general)

@Composable
fun Detail(){
    val scrollState = rememberScrollState()
    var offset by remember { mutableStateOf(0f) }

    Column(
        modifier = Modifier
            .scrollable(
                orientation = Orientation.Vertical,
                state = rememberScrollableState { delta ->
                    offset += delta
                    delta
                }
            )
    ) {
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .size(250.dp - (-offset).dp),
            contentAlignment = Alignment.Center
        ) {
            Image(
                painter = rememberImagePainter(
                    data =  "https://www.gstatic.com/webp/gallery/1.jpg",
                ),
                alignment = Alignment.TopCenter,
                contentDescription = null,
                contentScale = ContentScale.Crop,
                modifier = Modifier
                    .size(225.dp - (-offset).dp)
                    .clip(CircleShape)
            )
        }
            Column() {
                    Text("This is a header",
                        Modifier
                            .padding(20.dp, 10.dp)
                            .fillMaxWidth()
                            .align(Alignment.CenterHorizontally),
                        textAlign = TextAlign.Center,
                        style = MaterialTheme.typography.h5)
                    Text("Start here...." + "lorem ipsum ".repeat(120),
                        Modifier
                            .padding(20.dp, 10.dp, 20.dp, 5.dp)
                             .verticalScroll(scrollState, offset < -40)
                            .align(Alignment.CenterHorizontally),
                        lineHeight = 20.sp,
                        style = MaterialTheme.typography.body2)
            }
    }
}
Dmitri
  • 2,563
  • 1
  • 22
  • 30
  • Does this answer helps you? https://stackoverflow.com/questions/67227755/jetpack-compose-collapsing-toolbar/67248441#67248441 – nglauber Jul 27 '22 at 19:41
  • Unfortunately, no. This doesn't kick in an internal scroll for the main text. It goes over thru the top. I need for the header to stick at the top, and the main text to swtich to internal scroll once the image section is collapsed. – Dmitri Jul 28 '22 at 15:44
  • 1
    actually, maybe this will work, if i add a stickyHeader, let me experiment, will let you know – Dmitri Jul 28 '22 at 16:26
  • Yeah, with a few modifications, it gives me what I want, many thanks. Will post the code as an answer in a few minutes, and credit you of course. – Dmitri Jul 28 '22 at 17:06
  • Great to hear that. Please give an up to the other answer if you don't mind :) – nglauber Jul 28 '22 at 17:08
  • absolutely, it helped me a lot, just did – Dmitri Jul 28 '22 at 17:13

1 Answers1

1

Thanks to the comment provided by @nglauber and a link to this answer - a similar problem, I was able to work out the effect I want. My original solution also works, but this one I think is more elegant and - most importantly - gives a smoother effect - so going with it.

To recap, I needed three sections - image, header, and text. When I scroll up, I want the image section to shrink until its gone, the two lower sections to move up and occupy the freed up space, with the header becoming a sticky header at the top, and then the text scroll kicking in last.

So here is my modified code from the linked answer to achieve the result I want:

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CollapsingEffectScreen() {

    val lazyListState = rememberLazyListState()
    var scrolledY = 0f
    var previousOffset = 0
    LazyColumn(
        Modifier.fillMaxSize(),
        lazyListState,
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        item {
            Modifier.fillMaxWidth()
            Image(
                painter = rememberImagePainter(
                    data =  "https://www.gstatic.com/webp/gallery/1.jpg",
                ),
                alignment = Alignment.Center,
                contentDescription = null,
                contentScale = ContentScale.Crop,
                modifier = Modifier
                    .graphicsLayer {
                        scrolledY += lazyListState.firstVisibleItemScrollOffset - previousOffset
                        translationY = scrolledY * 0.5f
                        scaleX = 1/((scrolledY * 0.01f) + 1f)
                        scaleY = 1/((scrolledY * 0.01f) + 1f)
                        previousOffset = lazyListState.firstVisibleItemScrollOffset
                    }
                    .size(225.dp)
                    .padding(20.dp)
                    .fillMaxSize()
                    .clip(CircleShape)
            )
        }
        stickyHeader {
            Text(
                text = "this is header ",
                Modifier
                    .background(Color.White)
                    .height(80.dp)
                    .fillMaxWidth()
                    .padding(20.dp),
                textAlign = TextAlign.Center,
                style = MaterialTheme.typography.h5
            )
        }
        items(1) {
            Text(
                text = "this is body text  ".repeat(200),
                Modifier
                    .background(Color.White)
                    .fillMaxWidth()
                    .padding(35.dp),
                lineHeight = 20.sp,
                style = MaterialTheme.typography.body2

            )
        }
    }
} 

In action: enter image description here

P.S: also works great in reverse.

Dmitri
  • 2,563
  • 1
  • 22
  • 30
  • Do you know how I can get this to work to shrink the header, in order to display less information? Let's say you have two text boxes and an image and you want the image to move to the left side, one textbox to the right and the other textbox to fade out. Do you know how this could be achieved? – Captain Allergy Aug 14 '22 at 21:31