I am using `animateScrollTo()` to vertically scrol...
# compose
r
I am using
animateScrollTo()
to vertically scroll a fixed height
Text
. The animation occurs as expected when the text length increases past the bottom of the
Text
but not when it decreases. I’d like to know how to get it to animate for the latter. (Please see code and screen recording in thread.)
Copy code
@Composable
fun AnimateScrollToOnlyForwards() {
  var text by remember { mutableStateOf("") }
  val scrollState = rememberScrollState()

  Column(modifier = Modifier.padding(20.dp)) {
    Text(
      modifier = Modifier
        .height(42.dp)
        .width(100.dp)
        .border(
          BorderStroke(
            width = 1.dp,
            color = Color.Black
          )
        )
        .verticalScroll(scrollState)
        .padding(4.dp),
      text = text
    )
    Button(
      modifier = Modifier.padding(30.dp),
      onClick = {
        val digit = "${Random.nextInt(48, 58).toChar()}"
        text += digit
      }
    ) {
      Text(
        text = "Add digit",
        color = Color.White
      )
    }
    Button(
      modifier = Modifier.padding(30.dp),
      onClick = { text = text.substring(0, text.length - 1) }
    ) {
      Text(
        text = "Remove digit",
        color = Color.White
      )
    }
  }

  LaunchedEffect(scrollState.maxValue) {
    println("Launched Effect, scrollState.maxValue = ${scrollState.maxValue}")
    scrollState.animateScrollTo(
      value = scrollState.maxValue,
      animationSpec = tween(2000) // This spec added just for demonstration purposes, to lengthen animation time
    )
  }
}
AnimateScrollToOnlyForwards.mp4
e
I believe you'll need to add additional height or padding inside the verticalScroll so that it can be scrolled past the end of content when a line is removed
r
I’m not sure if you meant this but I tried putting a
.padding()
and
.height()
(first I tried one, then the other, then both) before the .verticalScroll() …it didn’t help.
e
I mean after
r
Putting
.height()
after made it not scroll for some reason, and putting more`.padding()` after didn’t help. Thanks for the suggestions though.
d
You can use
Modifier.animateContentSize()
Copy code
Text(
      modifier = Modifier
        .height(42.dp)
        .width(100.dp)
        .border(
          BorderStroke(
            width = 1.dp,
            color = Color.Black
          )
        )
        .verticalScroll(scrollState)
        .animateContentSize(
          animationSpec = tween(durationMillis = 2000)
        )
        .padding(4.dp),
      text = text
    )
r
Hey that’s nice, thanks! I added
.animateContentSize()
as you suggested (with no parameters, since the animationSpec was just for demo purposes) and switched to
scrollTo()
instead of
animateScrollTo()
and it works like I was hoping — at least in this simplified demo. What’s interesting though is that I can’t get it to work quite like that in my app. It needs a second added character to scroll, and sometimes even a third character to get it to fully scroll. I’ll look more into that.
d
I think it works like this: • When the text gets bigger, more space is allocated for the text. And the
animateScrollTo()
function works as expected. • When the text becomes smaller, the space for the text decreases instantly. The
animateScrollTo()
function does nothing because the text is already at the bottom. Therefore, there is no animation in this case. How can you solve the problem: • When the text gets bigger, use the
animateScrollTo()
function. Do not call
animateContentSize()
. • When the text gets smaller, use the
animateContentSize()
function. Do not call
animateScrollTo()
. Perhaps the combination of
animateScrollTo()
and
animateContentSize()
does not give a good result. Because every height change in the
animateContentSize()
animation triggers
LaunchedEffect(scrollState.maxValue)
.
Another solution is to add a custom layout. And place the text always at the bottom. And add animation for content resizing. Example:
Copy code
fun Modifier.scrollToBottom() =
  layout { measurable, constraints ->
    val placeable = measurable.measure(
      constraints.copy(maxHeight = Constraints.Infinity)
    )
    layout(constraints.maxWidth, constraints.maxHeight) {
      placeable.placeRelative(0, constraints.maxHeight - placeable.height)
    }
  }

@Composable
fun AnimateScrollToOnlyForwards() {
  var text by remember { mutableStateOf("") }
  Column(modifier = Modifier.padding(20.dp)) {
    Spacer(modifier = Modifier.height(56.dp))
    Row {
      Box(
        modifier = Modifier
          .height(42.dp)
          .width(100.dp)
          .border(
            BorderStroke(
              width = 1.dp,
              color = Color.Black
            )
          )
          .clip(RectangleShape)
      ) {
        Text(
          modifier = Modifier
            .fillMaxWidth()
            .height(42.dp)
            .scrollToBottom()
            .animateContentSize(
              animationSpec = tween(durationMillis = 2000)
            ),
          text = text
        )
      }
      Spacer(modifier = Modifier.width(56.dp))
      Box(
        modifier = Modifier
          .height(42.dp)
          .width(100.dp)
          .border(
            BorderStroke(
              width = 1.dp,
              color = Color.Black
            )
          )
      ) {
        Text(
          modifier = Modifier
            .fillMaxWidth()
            .height(42.dp)
            .scrollToBottom()
            .animateContentSize(
              animationSpec = tween(durationMillis = 2000)
            )
            .background(color = Color.Gray),
          text = text
        )
      }
    }

    Button(
      modifier = Modifier.padding(30.dp),
      onClick = {
        val digit = "${Random.nextInt(48, 58).toChar()}"
        text += digit
      }
    ) {
      Text(
        text = "Add digit",
        color = Color.White
      )
    }
    Button(
      modifier = Modifier.padding(30.dp),
      onClick = { text = text.substring(0, text.length - 1) }
    ) {
      Text(
        text = "Remove digit",
        color = Color.White
      )
    }
  }
}
r
Thanks for looking into this. I tried your first solution, which is a decent workaround in this example, but for some reason still doesn’t behave quite right in my app. (There must be some interaction with other composables or modifiers I don’t yet understand.) I am probably going to chose to live with this asymmetric behavior as I don’t think Google will make it a priority to make
animateScrollTo()
work like I had wished.
e
so the issue is correctly identified, when the item shrinks it is already too small to be scrolled past the end
that's why I suggested adding padding or height, although that does require additional work to recover what the original height would have been so that you can animate to it
if you're going to create a custom layout anyway though, it's easy to simply place the items wherever you want
AnimatedPinnedToBottomPreview.kt,AnimatedPinnedToBottomPreview.mp4
actually that leads me to realize that it could be done in a
Modifier
instead of a custom
Layout
, as long as you don't need to handle touch input
461 Views