Created
October 21, 2024 22:11
-
-
Save alashow/09152d76c090667015faafa2f8ad5647 to your computer and use it in GitHub Desktop.
animateContentSizeWithoutClipping
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.layout.IntrinsicMeasurable | |
import androidx.compose.ui.layout.IntrinsicMeasureScope | |
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 | |
// Fork of https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimationModifier.kt | |
// Simply removes clipToBounds from the animateContentSize | |
fun Modifier.animateContentSizeWithoutClipping( | |
animationSpec: FiniteAnimationSpec<IntSize> = spring( | |
stiffness = Spring.StiffnessMediumLow, | |
visibilityThreshold = IntSize.VisibilityThreshold | |
), | |
finishedListener: ((initialValue: IntSize, targetValue: IntSize) -> Unit)? = null | |
): Modifier = this then SizeAnimationModifierElement(animationSpec, Alignment.TopStart, 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 | |
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 = [email protected] | |
) | |
placeable.placeRelative(offset) | |
} | |
} | |
fun animateTo(targetSize: IntSize): IntSize { | |
val data = animData?.apply { | |
// 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 = (targetSize != anim.value && !anim.isRunning) | |
if (targetSize != anim.targetValue || wasInterrupted) { | |
startSize = anim.value | |
coroutineScope.launch { | |
val result = anim.animateTo(targetSize, animationSpec) | |
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 | |
} | |
} | |
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) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment