Skip to content

Instantly share code, notes, and snippets.

@jisungbin
Created July 21, 2022 14:43
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jisungbin/f451771329b7d96117c93516abd0759a to your computer and use it in GitHub Desktop.
Save jisungbin/f451771329b7d96117c93516abd0759a to your computer and use it in GitHub Desktop.
@file:OptIn(ExperimentalComposeUiApi::class)
@file:Suppress("unused")
@file:NoLiveLiterals
package land.sungbin.androidplayground.extension
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.AnimationVector2D
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.spring
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.NoLiveLiterals
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.LookaheadLayoutScope
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.round
import kotlinx.coroutines.launch
private val Spring.StiffnessDebug
get() = 1f
private fun <T> defaultSpring() = spring<T>(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessLow, // default: StiffnessLow
)
inline val DefaultMeasurePolicy: MeasureScope.(measurables: List<Measurable>, constraints: Constraints) -> MeasureResult
get() = { measurables, constraints ->
val placeables = measurables.map { measurable -> measurable.measure(constraints) }
val maxWidth = placeables.maxOf { placeable -> placeable.width }
val maxHeight = placeables.maxOf { placeable -> placeable.height }
layout(width = maxWidth, height = maxHeight) {
placeables.forEach { placeable ->
placeable.place(x = 0, y = 0)
}
}
}
// for test
fun Modifier.movement(lookaheadScope: LookaheadLayoutScope) = composed {
var targetOffset: IntOffset? by remember { mutableStateOf(null) } // 배치할 오프셋
var placementOffset by remember { mutableStateOf(IntOffset.Zero) } // 현재 오프셋
with(lookaheadScope) {
this@composed
.onPlaced { lookaheadScopeCoordinates, layoutCoordinates ->
// LookaheadLayout 의 로컬 coordinate 에서 이 modifier 의 lookahead 위치를 반환
targetOffset = lookaheadScopeCoordinates
.localLookaheadPositionOf(sourceCoordinates = layoutCoordinates)
.round() // 가장 가까운 IntOffset 값으로 오프셋 반올림
// LookaheadLayout 의 로컬 coordinate 에서 이 modifier 의 현재 위치를 반환
placementOffset = lookaheadScopeCoordinates
.localPositionOf(
sourceCoordinates = layoutCoordinates,
relativeToSource = Offset.Zero
)
.round()
}
.intermediateLayout { measurable, constraints, _ ->
val placeable = measurable.measure(constraints)
layout(width = placeable.width, height = placeable.height) {
// 이동한 오프셋에 배치
val (x, y) = targetOffset!! - placementOffset
placeable.place(x = x, y = y)
}
}
}
}
fun Modifier.animateMovement(
lookaheadScope: LookaheadLayoutScope,
animationSpec: AnimationSpec<IntOffset> = defaultSpring()
) = composed {
var placementOffset by remember { mutableStateOf(IntOffset.Zero) }
var targetOffset: IntOffset? by remember { mutableStateOf(null) }
var targetOffsetAnimation: Animatable<IntOffset, AnimationVector2D>? by remember {
mutableStateOf(null)
}
LaunchedEffect(Unit) {
snapshotFlow { targetOffset }.collect { target ->
if (target != null && target != targetOffsetAnimation?.targetValue) {
targetOffsetAnimation?.run {
launch {
animateTo(
targetValue = target,
animationSpec = animationSpec
)
}
} ?: Animatable(
initialValue = target,
typeConverter = IntOffset.VectorConverter
).let { offsetAnimatable ->
targetOffsetAnimation = offsetAnimatable
}
}
}
}
with(lookaheadScope) {
this@composed
.onPlaced { lookaheadScopeCoordinates, layoutCoordinates ->
targetOffset = lookaheadScopeCoordinates
.localLookaheadPositionOf(sourceCoordinates = layoutCoordinates)
.round()
placementOffset = lookaheadScopeCoordinates
.localPositionOf(
sourceCoordinates = layoutCoordinates,
relativeToSource = Offset.Zero
)
.round()
}
.intermediateLayout { measurable, constraints, _ ->
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
val (x, y) = (
targetOffsetAnimation?.value ?: targetOffset!!
) - placementOffset
placeable.place(x, y)
}
}
}
}
// for test
fun Modifier.transformation(lookaheadScope: LookaheadLayoutScope) = with(lookaheadScope) {
intermediateLayout { measurable, _, lookaheadSize ->
val (width, height) = lookaheadSize // lookahead 크기로 width, height 결정
val animatedConstraints = Constraints.fixed(
width = width.coerceAtLeast(0), // 최소 0 으로 설정
height = height.coerceAtLeast(0)
)
val placeable = measurable.measure(animatedConstraints)
layout(width = placeable.width, height = placeable.height) { // lookahead 크기에 맞게 배치
placeable.place(x = 0, y = 0)
}
}
}
fun Modifier.animateTransformation(
lookaheadScope: LookaheadLayoutScope,
animationSpec: AnimationSpec<IntSize> = defaultSpring(),
) = composed {
var targetSize: IntSize? by remember { mutableStateOf(null) }
var targetSizeAnimation: Animatable<IntSize, AnimationVector2D>? by remember {
mutableStateOf(null)
}
LaunchedEffect(Unit) {
snapshotFlow { targetSize }.collect { target ->
if (target != null && target != targetSizeAnimation?.targetValue) {
targetSizeAnimation?.run {
launch {
animateTo(
targetValue = target,
animationSpec = animationSpec
)
}
} ?: Animatable(
initialValue = target,
typeConverter = IntSize.VectorConverter
).let { sizeAnimatable ->
targetSizeAnimation = sizeAnimatable
}
}
}
}
with(lookaheadScope) {
this@composed.intermediateLayout { measurable, _, lookaheadSize ->
targetSize = lookaheadSize
val (width, height) = targetSizeAnimation?.value ?: lookaheadSize
val animatedConstraints = Constraints.fixed(
width = width.coerceAtLeast(0),
height = height.coerceAtLeast(0)
)
val placeable = measurable.measure(animatedConstraints)
layout(width = placeable.width, height = placeable.height) {
placeable.place(x = 0, y = 0)
}
}
}
}
@SmartToolFactory
Copy link

SmartToolFactory commented May 11, 2023

Good job explaining this with complex subject in clear and simple terms. It's so difficult to find any resources about LookAheadLayout on web. I'm not still fully able to understand difference between this and using Modifier.onPlaced, custom layout or Modifier.layout and animation as in official document. Is performance difference only issue we overcome with LookAheadLayout?

@jisungbin
Copy link
Author

jisungbin commented May 12, 2023

Hi @SmartToolFactory, thanks for the question and sorry for the late awnser. Did you come from Introducing Jetpack Compose’s New Layout: “LookaheadLayout” this post?

In the case of LookaheadLayout, it provides an intermediate layout of the target layout to be animated. This is not the case with Layout.

@SmartToolFactory
Copy link

Thanks for your response. Yes, i came from your amazing article on Medium. I will check it out again when i have time and try with other options to see how it will change outcome or call of layout, draw phases. Thanks again for such an awesome article.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment