From bb979cb52d826134de2fcc96898b0c692c597cdf Mon Sep 17 00:00:00 2001 From: Artem Chepurnoy <artemchep@gmail.com> Date: Sat, 28 Dec 2024 17:22:37 +0200 Subject: [PATCH] ui: Only animate height where possible to smooth out animation when you are resizing the window --- .../attachments/compose/ItemAttachment.kt | 1 - .../feature/auth/login/LoginScreen.kt | 4 - .../feature/barcodetype/BarcodeTypeScreen.kt | 4 +- .../confirmation/ConfirmationScreen.kt | 3 - .../OrganizationConfirmationScreen.kt | 1 - .../feature/generator/GeneratorScreen.kt | 34 ++- .../home/vault/component/VaultViewInfoItem.kt | 5 +- .../home/vault/component/VaultViewTotpItem.kt | 3 - .../vault/component/VaultViewValueItem.kt | 48 ++-- .../keyguard/feature/sync/SyncScreen.kt | 1 - .../artemchep/keyguard/ui/AutofillWindow.kt | 4 +- .../ui/animation/ContentSizeAnimation.kt | 215 ++++++++++++++++++ 12 files changed, 259 insertions(+), 64 deletions(-) create mode 100644 common/src/commonMain/kotlin/com/artemchep/keyguard/ui/animation/ContentSizeAnimation.kt diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/attachments/compose/ItemAttachment.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/attachments/compose/ItemAttachment.kt index 5825adbd..362ca661 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/attachments/compose/ItemAttachment.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/attachments/compose/ItemAttachment.kt @@ -2,7 +2,6 @@ package com.artemchep.keyguard.feature.attachments.compose import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.Crossfade -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.fadeIn diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/auth/login/LoginScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/auth/login/LoginScreen.kt index 727cabdb..db244fd4 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/auth/login/LoginScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/auth/login/LoginScreen.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -21,7 +20,6 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.Login import androidx.compose.material.icons.outlined.Add -import androidx.compose.material.icons.outlined.AppRegistration import androidx.compose.material.icons.outlined.Security import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu @@ -31,7 +29,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -86,7 +83,6 @@ import com.artemchep.keyguard.ui.UrlFlatTextField import com.artemchep.keyguard.ui.icons.ChevronIcon import com.artemchep.keyguard.ui.icons.DropdownIcon import com.artemchep.keyguard.ui.icons.IconBox -import com.artemchep.keyguard.ui.icons.icon import com.artemchep.keyguard.ui.skeleton.SkeletonText import com.artemchep.keyguard.ui.skeleton.SkeletonTextField import com.artemchep.keyguard.ui.theme.Dimens diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/barcodetype/BarcodeTypeScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/barcodetype/BarcodeTypeScreen.kt index 325489ff..7460ba97 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/barcodetype/BarcodeTypeScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/barcodetype/BarcodeTypeScreen.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.QrCode -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -44,6 +43,7 @@ import com.artemchep.keyguard.res.* import com.artemchep.keyguard.ui.FlatDropdown import com.artemchep.keyguard.ui.FlatItemTextContent import com.artemchep.keyguard.ui.KeepScreenOnEffect +import com.artemchep.keyguard.ui.animation.animateContentHeight import com.artemchep.keyguard.ui.icons.DropdownIcon import com.artemchep.keyguard.ui.icons.icon import org.jetbrains.compose.resources.stringResource @@ -120,7 +120,7 @@ private fun BarcodeTypeContent( BarcodeImage( modifier = Modifier .fillMaxWidth() - .animateContentSize(), + .animateContentHeight(), imageModel = { imageRequest ?.copy( diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/confirmation/ConfirmationScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/confirmation/ConfirmationScreen.kt index 9da8b5b5..db49225b 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/confirmation/ConfirmationScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/confirmation/ConfirmationScreen.kt @@ -2,7 +2,6 @@ package com.artemchep.keyguard.feature.confirmation import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateContentSize -import androidx.compose.animation.core.animateSizeAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi @@ -42,7 +41,6 @@ import com.artemchep.keyguard.feature.auth.common.VisibilityState import com.artemchep.keyguard.feature.auth.common.VisibilityToggle import com.artemchep.keyguard.feature.dialog.Dialog import com.artemchep.keyguard.feature.filepicker.FilePickerEffect -import com.artemchep.keyguard.feature.filepicker.humanReadableByteCountBin import com.artemchep.keyguard.feature.filepicker.humanReadableByteCountSI import com.artemchep.keyguard.feature.navigation.LocalNavigationController import com.artemchep.keyguard.feature.navigation.NavigationIntent @@ -57,7 +55,6 @@ import com.artemchep.keyguard.ui.FlatItemLayout import com.artemchep.keyguard.ui.FlatTextField import com.artemchep.keyguard.ui.MediumEmphasisAlpha import com.artemchep.keyguard.ui.PasswordStrengthBadge -import com.artemchep.keyguard.ui.icons.IconBox import com.artemchep.keyguard.ui.skeleton.SkeletonItem import com.artemchep.keyguard.ui.theme.Dimens import com.artemchep.keyguard.ui.theme.combineAlpha diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/confirmation/organization/OrganizationConfirmationScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/confirmation/organization/OrganizationConfirmationScreen.kt index 711a2940..d0a734e2 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/confirmation/organization/OrganizationConfirmationScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/confirmation/organization/OrganizationConfirmationScreen.kt @@ -1,7 +1,6 @@ package com.artemchep.keyguard.feature.confirmation.organization import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/generator/GeneratorScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/generator/GeneratorScreen.kt index 2f8275f2..c4d49088 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/generator/GeneratorScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/generator/GeneratorScreen.kt @@ -106,6 +106,7 @@ import com.artemchep.keyguard.ui.MediumEmphasisAlpha import com.artemchep.keyguard.ui.OptionsButton import com.artemchep.keyguard.ui.PasswordStrengthBadge import com.artemchep.keyguard.ui.ScaffoldColumn +import com.artemchep.keyguard.ui.animation.animateContentHeight import com.artemchep.keyguard.ui.collectIsInteractedWith import com.artemchep.keyguard.ui.colorizePassword import com.artemchep.keyguard.ui.icons.DropdownIcon @@ -556,7 +557,7 @@ fun ColumnScope.GeneratorValue( Crossfade( targetState = value.password, modifier = Modifier - .animateContentSize(), + .animateContentHeight(), ) { password -> AutoResizeText( text = colorizePasswordOrEmpty(password), @@ -1189,23 +1190,18 @@ fun AutoResizeText( minFontSize: Float = 14f, maxLines: Int = Int.MAX_VALUE, ) { - Box( - modifier = modifier, - ) { - val fontSize = kotlin.run { - val ratio = 1f - (text.length / 128f) - .coerceAtMost(1f) - minFontSize + (maxFontSize - minFontSize) * ratio - } - Text( - modifier = Modifier - .animateContentSize(), - text = text, - fontFamily = monoFontFamily, - fontSize = fontSize.sp, - maxLines = maxLines, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.titleLarge, - ) + val fontSize = run { + val ratio = 1f - (text.length / 128f) + .coerceAtMost(1f) + minFontSize + (maxFontSize - minFontSize) * ratio } + Text( + modifier = modifier, + text = text, + fontFamily = monoFontFamily, + fontSize = fontSize.sp, + maxLines = maxLines, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleLarge, + ) } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultViewInfoItem.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultViewInfoItem.kt index f518ffc0..7488c056 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultViewInfoItem.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultViewInfoItem.kt @@ -1,7 +1,6 @@ package com.artemchep.keyguard.feature.home.vault.component import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.animateContentSize import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background @@ -19,7 +18,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ArrowDownward -import androidx.compose.material.icons.outlined.ArrowDropDown import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.material3.ColorScheme import androidx.compose.material3.Icon @@ -47,6 +45,7 @@ import com.artemchep.keyguard.res.* import com.artemchep.keyguard.ui.DisabledEmphasisAlpha import com.artemchep.keyguard.ui.ExpandedIfNotEmpty import com.artemchep.keyguard.ui.MediumEmphasisAlpha +import com.artemchep.keyguard.ui.animation.animateContentHeight import com.artemchep.keyguard.ui.theme.Dimens import com.artemchep.keyguard.ui.theme.combineAlpha import com.artemchep.keyguard.ui.theme.info @@ -114,7 +113,7 @@ fun VaultViewInfoItem( .fillMaxWidth() .padding(top = 4.dp) .padding(horizontal = 8.dp) - .animateContentSize(), + .animateContentHeight(), text = message, style = MaterialTheme.typography.bodySmall, color = LocalContentColor.current diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultViewTotpItem.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultViewTotpItem.kt index 781fe26c..93fd56a5 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultViewTotpItem.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultViewTotpItem.kt @@ -28,7 +28,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.State @@ -39,8 +38,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.isSpecified import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import arrow.core.getOrElse diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultViewValueItem.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultViewValueItem.kt index 3505ac0e..ca91814c 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultViewValueItem.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultViewValueItem.kt @@ -1,7 +1,7 @@ package com.artemchep.keyguard.feature.home.vault.component -import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow @@ -35,12 +35,11 @@ import com.artemchep.keyguard.ui.FlatDropdown import com.artemchep.keyguard.ui.FlatItemAction import com.artemchep.keyguard.ui.MediumEmphasisAlpha import com.artemchep.keyguard.ui.animatedConcealedText +import com.artemchep.keyguard.ui.animation.animateContentHeight import com.artemchep.keyguard.ui.colorizePassword import com.artemchep.keyguard.ui.theme.combineAlpha import com.artemchep.keyguard.ui.theme.monoFontFamily import org.jetbrains.compose.resources.stringResource -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlin.Int @OptIn(ExperimentalLayoutApi::class) @Composable @@ -83,28 +82,27 @@ fun VaultViewValueItem( } else { null }, - text = if (shownValue.isNotBlank()) { - // composable - { - Text( - modifier = Modifier - .animateContentSize(), - text = shownValue, - fontFamily = if (item.monospace) monoFontFamily else null, - maxLines = item.maxLines, - overflow = TextOverflow.Ellipsis, - ) - } - } else { - // composable - { - Text( - modifier = Modifier - .animateContentSize() - .alpha(MediumEmphasisAlpha), - text = stringResource(Res.string.empty_value), - fontFamily = if (item.monospace) monoFontFamily else null, - ) + text = { + Box( + modifier = Modifier + .animateContentHeight(), + ) { + val fontFamily = if (item.monospace) monoFontFamily else null + if (shownValue.isNotBlank()) { + Text( + text = shownValue, + fontFamily = fontFamily, + maxLines = item.maxLines, + overflow = TextOverflow.Ellipsis, + ) + } else { + Text( + modifier = Modifier + .alpha(MediumEmphasisAlpha), + text = stringResource(Res.string.empty_value), + fontFamily = fontFamily, + ) + } } }, ) diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/sync/SyncScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/sync/SyncScreen.kt index 1b499dec..e90b6292 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/sync/SyncScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/sync/SyncScreen.kt @@ -19,7 +19,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/AutofillWindow.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/AutofillWindow.kt index 46bfb9ba..c33100c0 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/AutofillWindow.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/AutofillWindow.kt @@ -1,7 +1,6 @@ package com.artemchep.keyguard.ui import androidx.compose.animation.Crossfade -import androidx.compose.animation.animateContentSize import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope @@ -52,6 +51,7 @@ import com.artemchep.keyguard.feature.home.vault.component.Section import com.artemchep.keyguard.feature.localization.wrap import com.artemchep.keyguard.res.Res import com.artemchep.keyguard.res.* +import com.artemchep.keyguard.ui.animation.animateContentHeight import com.artemchep.keyguard.ui.icons.icon import com.artemchep.keyguard.ui.skeleton.SkeletonItem import com.artemchep.keyguard.ui.theme.combineAlpha @@ -320,7 +320,7 @@ private fun ColumnScope.GeneratorValue2( Crossfade( targetState = value.password, modifier = Modifier - .animateContentSize(), + .animateContentHeight(), ) { password -> AutoResizeText( text = if (password.isEmpty()) { diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/animation/ContentSizeAnimation.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/animation/ContentSizeAnimation.kt new file mode 100644 index 00000000..ca009dd9 --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/ui/animation/ContentSizeAnimation.kt @@ -0,0 +1,215 @@ +package com.artemchep.keyguard.ui.animation + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationEndReason +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.AnimationVector2D +import androidx.compose.animation.core.FiniteAnimationSpec +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.VectorConverter +import androidx.compose.animation.core.VisibilityThreshold +import androidx.compose.animation.core.spring +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.layout.IntrinsicMeasurable +import androidx.compose.ui.layout.IntrinsicMeasureScope +import androidx.compose.ui.layout.LayoutModifier +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.node.LayoutModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.constrain +import kotlinx.coroutines.launch + +fun Modifier.animateContentHeight( + animationSpec: FiniteAnimationSpec<IntSize> = + spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntSize.VisibilityThreshold, + ), + finishedListener: ((initialValue: IntSize, targetValue: IntSize) -> Unit)? = null, +): Modifier = + this.clipToBounds() then + SizeAnimationModifierElement(animationSpec, Alignment.TopStart, finishedListener) + +fun Modifier.animateContentHeight( + animationSpec: FiniteAnimationSpec<IntSize> = + spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntSize.VisibilityThreshold, + ), + alignment: Alignment = Alignment.TopStart, + finishedListener: ((initialValue: IntSize, targetValue: IntSize) -> Unit)? = null, +): Modifier = + this.clipToBounds() then + SizeAnimationModifierElement(animationSpec, alignment, finishedListener) + +private data class SizeAnimationModifierElement( + val animationSpec: FiniteAnimationSpec<IntSize>, + val alignment: Alignment, + val finishedListener: ((initialValue: IntSize, targetValue: IntSize) -> Unit)?, +) : ModifierNodeElement<SizeAnimationModifierNode>() { + override fun create(): SizeAnimationModifierNode = + SizeAnimationModifierNode(animationSpec, alignment, finishedListener) + + override fun update(node: SizeAnimationModifierNode) { + node.animationSpec = animationSpec + node.listener = finishedListener + node.alignment = alignment + } + + override fun InspectorInfo.inspectableProperties() { + name = "animateContentSize" + properties["animationSpec"] = animationSpec + properties["alignment"] = alignment + properties["finishedListener"] = finishedListener + } +} + +internal val InvalidSize = IntSize(Int.MIN_VALUE, Int.MIN_VALUE) +internal val IntSize.isValid: Boolean + get() = this != InvalidSize + +/** + * This class creates a [LayoutModifier] that measures children, and responds to children's size + * change by animating to that size. The size reported to parents will be the animated size. + */ +private class SizeAnimationModifierNode( + var animationSpec: AnimationSpec<IntSize>, + var alignment: Alignment = Alignment.TopStart, + var listener: ((startSize: IntSize, endSize: IntSize) -> Unit)? = null, +) : LayoutModifierNodeWithPassThroughIntrinsics() { + private var lookaheadSize: IntSize = InvalidSize + private var lookaheadConstraints: Constraints = Constraints() + set(value) { + field = value + lookaheadConstraintsAvailable = true + } + + private var lookaheadConstraintsAvailable: Boolean = false + + private fun targetConstraints(default: Constraints) = + if (lookaheadConstraintsAvailable) { + lookaheadConstraints + } else { + default + } + + data class AnimData(val anim: Animatable<IntSize, AnimationVector2D>, var startSize: IntSize) + + var animData: AnimData? by mutableStateOf(null) + + override fun onReset() { + super.onReset() + // Reset is an indication that the node may be re-used, in such case, animData becomes stale + animData = null + } + + override fun onAttach() { + super.onAttach() + // When re-attached, we may be attached to a tree without lookahead scope. + lookaheadSize = InvalidSize + lookaheadConstraintsAvailable = false + } + + override fun MeasureScope.measure( + measurable: Measurable, + constraints: Constraints, + ): MeasureResult { + val placeable = + if (isLookingAhead) { + lookaheadConstraints = constraints + measurable.measure(constraints) + } else { + // Measure with lookahead constraints when available, to avoid unnecessary relayout + // in child during the lookahead animation. + measurable.measure(targetConstraints(constraints)) + } + val measuredSize = IntSize(placeable.width, placeable.height) + val (width, height) = + if (isLookingAhead) { + lookaheadSize = measuredSize + measuredSize + } else { + animateTo(if (lookaheadSize.isValid) lookaheadSize else measuredSize).let { + // Constrain the measure result to incoming constraints, so that parent doesn't + // force center this layout. + constraints.constrain(it) + } + } + return layout(width, height) { + val offset = + alignment.align( + size = measuredSize, + space = IntSize(width, height), + layoutDirection = this@measure.layoutDirection, + ) + placeable.placeRelative(offset) + } + } + + fun animateTo(targetSize: IntSize): IntSize { + val data = + animData?.apply { + val axisTargetSize = IntSize( + width = 1, + height = targetSize.height, + ) + // TODO(b/322878517): Figure out a way to seamlessly continue the animation after + // re-attach. Note that in some cases restarting the animation is the correct + // behavior. + val wasInterrupted = (axisTargetSize != anim.value && !anim.isRunning) + + if (axisTargetSize != anim.targetValue || wasInterrupted) { + startSize = anim.value + coroutineScope.launch { + val result = anim.animateTo(axisTargetSize, animationSpec) + if (result.endReason == AnimationEndReason.Finished) { + listener?.invoke(startSize, result.endState.value) + } + } + } + } + ?: AnimData( + Animatable(targetSize, IntSize.VectorConverter, IntSize(1, 1)), + targetSize, + ) + + animData = data + return IntSize( + width = targetSize.width, + height = data.anim.value.height, + ) + } +} + +internal abstract class LayoutModifierNodeWithPassThroughIntrinsics : + LayoutModifierNode, Modifier.Node() { + override fun IntrinsicMeasureScope.minIntrinsicWidth( + measurable: IntrinsicMeasurable, + height: Int, + ) = measurable.minIntrinsicWidth(height) + + override fun IntrinsicMeasureScope.minIntrinsicHeight( + measurable: IntrinsicMeasurable, + width: Int, + ) = measurable.minIntrinsicHeight(width) + + override fun IntrinsicMeasureScope.maxIntrinsicWidth( + measurable: IntrinsicMeasurable, + height: Int, + ) = measurable.maxIntrinsicWidth(height) + + override fun IntrinsicMeasureScope.maxIntrinsicHeight( + measurable: IntrinsicMeasurable, + width: Int, + ) = measurable.maxIntrinsicHeight(width) +}