https://kotlinlang.org logo
Title
s

Stylianos Gakis

05/08/2023, 10:44 PM
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
val (width, height) = animateTo(measuredSize)
return layout(width, height) {
to
val (_, height) = animateTo(measuredSize)
return layout(measuredSize.width, height) {
And it just works 😛arty-parrot: Here’s the entire thing
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

Chris Fillmore

05/09/2023, 12:32 AM
Out of curiosity, what’s your use case?
s

Stylianos Gakis

05/09/2023, 8:55 AM
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
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()
  }
}