Is there a way for `Modifier.animateContentSize` t...
# compose
s
Is there a way for
Modifier.animateContentSize
to only work on one axis? Something like
animatedContentHeight
would be what I am looking at, or a way to modify what
animateContentSize
animates but can’t seem to find some API which does what I want.
Hmm if someone knows of something that already exists I’d love to hear about it, but for now I just took the source code of
animateContentSize
and made it not animate the height 😅 Changed
Copy code
val (width, height) = animateTo(measuredSize)
return layout(width, height) {
to
Copy code
val (_, height) = animateTo(measuredSize)
return layout(measuredSize.width, height) {
And it just works 🦜 Here’s the entire thing
Copy code
fun Modifier.animateContentHeight(
  animationSpec: FiniteAnimationSpec<IntSize> = spring(),
  finishedListener: ((initialValue: IntSize, targetValue: IntSize) -> Unit)? = null,
): Modifier = composed(
  inspectorInfo = debugInspectorInfo {
    name = "animateContentSize"
    properties["animationSpec"] = animationSpec
    properties["finishedListener"] = finishedListener
  },
) {
  val scope = rememberCoroutineScope()
  val animModifier = remember(scope) {
    HeightAnimationModifier(animationSpec, scope)
  }
  animModifier.listener = finishedListener
  this.clipToBounds().then(animModifier)
}

private class HeightAnimationModifier(
  val animSpec: AnimationSpec<IntSize>,
  val scope: CoroutineScope,
) : LayoutModifierWithPassThroughIntrinsics() {
  var listener: ((startSize: IntSize, endSize: IntSize) -> Unit)? = null

  data class AnimData(
    val anim: Animatable<IntSize, AnimationVector2D>,
    var startSize: IntSize,
  )

  var animData: AnimData? by mutableStateOf(null)

  override fun MeasureScope.measure(
    measurable: Measurable,
    constraints: Constraints,
  ): MeasureResult {
    val placeable = measurable.measure(constraints)

    val measuredSize = IntSize(placeable.width, placeable.height)

    val (_, height) = animateTo(measuredSize)
    return layout(measuredSize.width, height) {
      placeable.placeRelative(0, 0)
    }
  }

  fun animateTo(targetSize: IntSize): IntSize {
    val data = animData?.apply {
      if (targetSize != anim.targetValue) {
        startSize = anim.value
        scope.launch {
          val result = anim.animateTo(targetSize, animSpec)
          if (result.endReason == AnimationEndReason.Finished) {
            listener?.invoke(startSize, result.endState.value)
          }
        }
      }
    } ?: AnimData(
      Animatable(
        targetSize,
        IntSize.VectorConverter,
        IntSize(1, 1),
      ),
      targetSize,
    )

    animData = data
    return data.anim.value
  }
}

private abstract class LayoutModifierWithPassThroughIntrinsics : LayoutModifier {
  final override fun IntrinsicMeasureScope.minIntrinsicWidth(
    measurable: IntrinsicMeasurable,
    height: Int,
  ) = measurable.minIntrinsicWidth(height)

  final override fun IntrinsicMeasureScope.minIntrinsicHeight(
    measurable: IntrinsicMeasurable,
    width: Int,
  ) = measurable.minIntrinsicHeight(width)

  final override fun IntrinsicMeasureScope.maxIntrinsicWidth(
    measurable: IntrinsicMeasurable,
    height: Int,
  ) = measurable.maxIntrinsicWidth(height)

  final override fun IntrinsicMeasureScope.maxIntrinsicHeight(
    measurable: IntrinsicMeasurable,
    width: Int,
  ) = measurable.maxIntrinsicHeight(width)
}
c
Out of curiosity, what’s your use case?
s
I am building a custom TextField, again took the m3 one, copied all the code and started doing modifications (I seem to be doing this a lot with compose 😅) since just like the code above, there’s a lot of internally visible stuff that I can’t modify otherwise. And I want the “supporting text” aka the little text that comes under the TextField (usually used for an error message) to animate in, but only vertically, instead of just appearing instantly. So I basically replaced this https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/[…]TextFieldLayout&amp;ss=androidx%2Fplatform%2Fframeworks%2Fsupport with
Copy code
Box(
  Modifier
    .layoutId(SupportingId)
    .animateContentHeight()
    .then(if (supporting == null) Modifier.requiredSize(0.dp) else Modifier)
    .heightIn(min = MinSupportingTextLineHeight)
    .wrapContentHeight()
    .padding(HedvigTextFieldDefaults.supportingTextPadding()),
) {
  if (supporting != null) {
    supporting()
  }
}