Created
December 3, 2024 15:06
-
-
Save Mikkareem/3964e00c261aa83268eb96fd2fcdd2a5 to your computer and use it in GitHub Desktop.
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
@file:OptIn(ExperimentalAnimatableApi::class) | |
package com.techullurgy.retest.sharedelement | |
import androidx.compose.animation.core.DeferredTargetAnimation | |
import androidx.compose.animation.core.ExperimentalAnimatableApi | |
import androidx.compose.animation.core.VectorConverter | |
import androidx.compose.animation.core.tween | |
import androidx.compose.foundation.background | |
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.Spacer | |
import androidx.compose.foundation.layout.fillMaxSize | |
import androidx.compose.foundation.layout.fillMaxWidth | |
import androidx.compose.foundation.layout.height | |
import androidx.compose.foundation.layout.padding | |
import androidx.compose.foundation.layout.width | |
import androidx.compose.foundation.lazy.grid.GridCells | |
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid | |
import androidx.compose.foundation.lazy.grid.items | |
import androidx.compose.foundation.shape.RoundedCornerShape | |
import androidx.compose.material3.RadioButton | |
import androidx.compose.material3.Text | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableStateOf | |
import androidx.compose.runtime.remember | |
import androidx.compose.runtime.setValue | |
import androidx.compose.ui.Alignment | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.draw.clip | |
import androidx.compose.ui.geometry.Offset | |
import androidx.compose.ui.graphics.Color | |
import androidx.compose.ui.layout.ApproachLayoutModifierNode | |
import androidx.compose.ui.layout.ApproachMeasureScope | |
import androidx.compose.ui.layout.LayoutCoordinates | |
import androidx.compose.ui.layout.LookaheadScope | |
import androidx.compose.ui.layout.Measurable | |
import androidx.compose.ui.layout.MeasureResult | |
import androidx.compose.ui.layout.Placeable | |
import androidx.compose.ui.node.ModifierNodeElement | |
import androidx.compose.ui.tooling.preview.Preview | |
import androidx.compose.ui.unit.Constraints | |
import androidx.compose.ui.unit.IntOffset | |
import androidx.compose.ui.unit.IntSize | |
import androidx.compose.ui.unit.dp | |
import androidx.compose.ui.unit.round | |
private const val animationDuration = 1000 | |
private class AnimatedPlacementModifierNode( | |
var lookaheadScope: LookaheadScope | |
): ApproachLayoutModifierNode, Modifier.Node() { | |
val offsetAnimation | |
= DeferredTargetAnimation(IntOffset.VectorConverter) | |
val sizeAnimation | |
= DeferredTargetAnimation(IntSize.VectorConverter) | |
override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean { | |
sizeAnimation.updateTarget( | |
lookaheadSize, | |
coroutineScope, | |
tween(animationDuration) | |
) | |
return !sizeAnimation.isIdle | |
} | |
override fun Placeable.PlacementScope.isPlacementApproachInProgress( | |
lookaheadCoordinates: LayoutCoordinates | |
): Boolean { | |
val target = with(lookaheadScope) { | |
lookaheadScopeCoordinates.localLookaheadPositionOf(lookaheadCoordinates).round() | |
} | |
offsetAnimation.updateTarget( | |
target, | |
coroutineScope, | |
tween(animationDuration) | |
) | |
return !offsetAnimation.isIdle | |
} | |
override fun ApproachMeasureScope.approachMeasure( | |
measurable: Measurable, | |
constraints: Constraints | |
): MeasureResult { | |
val currentSizeAnimation = sizeAnimation.updateTarget( | |
lookaheadSize, | |
coroutineScope, | |
tween(animationDuration) | |
) | |
val width = currentSizeAnimation.width | |
val height = lookaheadSize.height | |
val animatedConstraints = Constraints.fixed(width, height) | |
val placeable = measurable.measure(animatedConstraints) | |
return layout(placeable.width, placeable.height) { | |
val coordinates = coordinates | |
if (coordinates != null) { | |
// Calculates the target offset within the lookaheadScope | |
val target = | |
with(lookaheadScope) { | |
lookaheadScopeCoordinates.localLookaheadPositionOf(coordinates).round() | |
} | |
// Uses the target offset to start an offset animation | |
val animatedOffset = offsetAnimation.updateTarget( | |
target, | |
coroutineScope, | |
tween(animationDuration) | |
) | |
// Calculates the *current* offset within the given LookaheadScope | |
val placementOffset = | |
with(lookaheadScope) { | |
lookaheadScopeCoordinates | |
.localPositionOf(coordinates, Offset.Zero) | |
.round() | |
} | |
// Calculates the delta between animated position in scope and current | |
// position in scope, and places the child at the delta offset. This puts | |
// the child layout at the animated position. | |
val (x, y) = animatedOffset - placementOffset | |
placeable.place(x, y) | |
} else { | |
placeable.place(0, 0) | |
} | |
} | |
} | |
} | |
// Creates a custom node element for the AnimatedPlacementModifierNode above. | |
private data class AnimatePlacementNodeElement(val lookaheadScope: LookaheadScope) : | |
ModifierNodeElement<AnimatedPlacementModifierNode>() { | |
override fun update(node: AnimatedPlacementModifierNode) { | |
node.lookaheadScope = lookaheadScope | |
} | |
override fun create(): AnimatedPlacementModifierNode { | |
return AnimatedPlacementModifierNode(lookaheadScope) | |
} | |
} | |
sealed class ColumnState(val name: String, val count: Int) { | |
data object SingleColumn: ColumnState("Single Column", 1) | |
data object DoubleColumn: ColumnState("Double Column", 2) | |
data object TripleColumn: ColumnState("Triple Column", 3) | |
} | |
val colors = listOf( | |
Color.Red, | |
Color.Green, | |
Color.Blue, | |
Color.Black, | |
Color.Magenta, | |
Color.Cyan, | |
Color(0xff8e782f), | |
Color(0xff8e2f71), | |
Color(0xff198e2f), | |
) | |
@Preview | |
@Composable | |
private fun LookaheadScopeTest() { | |
var state by remember { mutableStateOf<ColumnState>(ColumnState.SingleColumn) } | |
Column ( | |
modifier = Modifier | |
.fillMaxSize() | |
.padding(16.dp), | |
) { | |
RadioGroup( | |
selectedOption = state, | |
options = listOf( | |
ColumnState.SingleColumn, | |
ColumnState.DoubleColumn, | |
ColumnState.TripleColumn | |
), | |
onSelectedChange = { state = it }, | |
label = { | |
Text(it.name) | |
} | |
) | |
LookaheadScope { | |
LazyVerticalGrid( | |
modifier = Modifier.weight(1f).background(Color.White.copy(0.4f)), | |
columns = GridCells.Fixed(state.count), | |
verticalArrangement = Arrangement.spacedBy(16.dp), | |
horizontalArrangement = Arrangement.spacedBy(16.dp) | |
) { | |
items(colors) { | |
Box( | |
modifier = Modifier | |
.fillMaxWidth() | |
.height(60.dp) | |
.then(AnimatePlacementNodeElement(this@LookaheadScope)) | |
.clip(RoundedCornerShape(20)) | |
.background(it) | |
) | |
} | |
} | |
} | |
} | |
} | |
@Composable | |
private fun <T> RadioGroup( | |
options: List<T>, | |
selectedOption: T, | |
onSelectedChange: (T) -> Unit, | |
modifiers: List<Modifier> = emptyList(), | |
label: (@Composable (T) -> Unit)? = null | |
) { | |
check(modifiers.isEmpty() || modifiers.size == options.size) | |
options.forEachIndexed { index, option -> | |
Row( | |
modifier = if(index < modifiers.size) modifiers[index] else Modifier, | |
verticalAlignment = Alignment.CenterVertically | |
) { | |
RadioButton( | |
selected = selectedOption == option, | |
onClick = { onSelectedChange(option) } | |
) | |
label?.let { | |
Spacer(Modifier.width(8.dp)) | |
it(option) | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Screen.Recording.2024-12-03.at.8.32.52.PM.mov