Jetpack Compose implements a silky smooth page expansion and closing effect animation

Jetpack Compose lowers the threshold for animation implementation, but Compose does not currently support shared element transitions.

Realization of animation effects (local notebook developed by Jetpack Compose in the previous article)

Preparation before jumping

Define the State enumeration class to represent the three states of the page:
Closing (closed state)
Closed (closed completion status)
Opening (expanded state)\

enum class CreateNoteState {<!-- -->
    Closing, Closed, Opening
}

The mutableStateOf() function in Jetpack Compose creates a mutable state, and initializes three variables cardSize, createNoteUIOffset and currentCreateNoteState respectively .
cardSize is a variable state of IntSize type, used to represent the size of the page, the initial value is (0, 0).
createNoteUIOffset is a variable state of IntOffset type, which is used to represent the offset of creating note interface, the initial value is (0, 0) .
currentCreateNoteState is a variable state of the enumeration type CreateNoteState, which is used to represent the current state of the note creation interface. The initial value is State.Closed, That is, the closed state. This enumeration type may include Closing, Closed, Opening and other states.

var cardSize by mutableStateOf(IntSize(0, 0))
var createNoteUIOffset by mutableStateOf(IntOffset(0, 0))
var currentCreateNoteState by mutableStateOf(CreateNoteState.Closed)

Click the jump button

onSizeChanged is used to update the layout when the size of the jump button changes, and pass the new size to the onSizedChanged callback function.
onGloballyPositioned is used to update the layout when the position of the jump button changes, passing the new position to the intOffset variable.
Finally, when the user clicks the jump button , the onClick callback function is called, passing the intOffset variable as a parameter.

@Composable
fun HomeAddButton(
    onSizedChanged: (IntSize) -> Unit,
    onClick: (offset: IntOffset) -> Unit,
) {<!-- -->
    var intOffset: IntOffset? by remember {<!-- --> mutableStateOf(null) }
    FloatingActionButton(onClick = {<!-- -->
        onClick(intOffset!!)
    },
        Modifier
            .padding(16.dp)
            .onSizeChanged {<!-- --> onSizedChanged(it)}
            .onGloballyPositioned {<!-- -->
                val offset = it.localToRoot(Offset(0f, 0f))
                intOffset = IntOffset(offset. x. toInt(), offset. y. toInt())
            }
    ) {<!-- -->
      ?…
    }
}
HomeAddButton(
    onSizedChanged = {<!-- -->
        viewModel. cardSize = it
    }
) {<!-- --> offset ->
    //click event
    viewModel.currentCreateNoteState = CreateNoteState.Opening
    viewModel.createNoteUIOffset = offset
}

Jump interface

Record the size information of the page, including
cardSize (folded state size),
fullSize (fully expanded state size)
cardOffset (the offset position of the folded state page in the screen).

CreateNotePage(
    viewModel.currentCreateNoteState,
    viewModel. cardSize,
    viewModel. fullSize,
    viewModel.createNoteUIOffset,
    {<!-- -->
        viewModel.currentCreateNoteState = CreateNoteState.Closing
    },
    {<!-- -->
        viewModel.currentCreateNoteState = CreateNoteState.Closed
    })

Define offsetAnimatable to record and control the offset change of the page in the screen during the animation process. Use the animateTo() function to realize the translation animation effect from cardOffset to fullOffset.

var animReady by remember {<!-- --> mutableStateOf(false) }//mark animation ready
var animFinish by remember {<!-- --> mutableStateOf(false) }//mark animation completion
val offsetAnimatable = remember {<!-- --> Animatable(IntOffset(0, 0), IntOffset. VectorConverter) }
val DEPLOYMENT_DURATION = 500 // animation speed

val cornerSize by animateDpAsState(if (animFinish) 0.dp else 16.dp) //rounded corners

Use LaunchedEffect to monitor the changes of CreateNoteState, and trigger corresponding animation effects according to different states: – Opening state: call offsetAnimatable‘s The animateTo() function realizes the expansion animation, and changes the page offset from cardOffset to fullOffset; set animFinish to true. – Closing state: Call the animateTo() function of offsetAnimatable to close the animation, and change the page offset from fullOffset to cardOffset; Sets animFinish to false and animReady to false. – Closed state: The page is closed and no action is required.

LaunchedEffect(pageState) {<!-- -->
    when (pageState) {<!-- -->
        CreateNoteState. Opening -> {<!-- -->
            animReady = true
            offsetAnimatable. snapTo(cardOffset)
            offsetAnimatable.animateTo(fullOffset, animationSpec = tween(DEPLOYMENT_DURATION))
            animFinish = true
        }
        CreateNoteState.Closing -> {<!-- -->
            animFinish = false
            offsetAnimatable. snapTo(fullOffset)
            offsetAnimatable.animateTo(cardOffset, animationSpec = tween(DEPLOYMENT_DURATION))
            animReady = false
            onPageClosed()
        }
        else -> {<!-- -->}
    }
}

Use the Box component and its Modifier to apply offsetAnimatable.value, size change size and rounded cornerSize animation effects in displayed on the page.

if (pageState != CreateNoteState.Closed & amp; & amp; animReady) {<!-- -->
    Box(
        Modifier
            .offset {<!-- --> offsetAnimatable.value }
            .clip(RoundedCornerShape(cornerSize))
            .width(with(LocalDensity.current) {<!-- --> size.width.toDp() })
            .height(with(LocalDensity.current) {<!-- --> size.height.toDp() })

    ) {<!-- -->
       ...
       your interface
       ...
    }
}

Please add a picture description

Complete renderings

Complete code

Jump button

HomeAddButton(
    Modifier
        .navigationBarsPadding()
        .align(Alignment.BottomEnd),
    onSizedChanged = {<!-- -->
        viewModel. cardSize = it
    }
) {<!-- --> offset ->
    //click event
    viewModel.currentCreateNoteState = CreateNoteState.Opening
    viewModel.createNoteUIOffset = offset
    //shock
    feedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
}
@Composable
fun HomeAddButton(
    modifier: Modifier,
    onSizedChanged: (IntSize) -> Unit,
    onClick: (offset: IntOffset) -> Unit,
) {<!-- -->
    var intOffset: IntOffset? by remember {<!-- --> mutableStateOf(null) }
    FloatingActionButton(onClick = {<!-- -->
        onClick(intOffset!!)
    },
        modifier
            .padding(16.dp)
            .onSizeChanged {<!-- --> onSizedChanged(it)}
            .onGloballyPositioned {<!-- -->
                val offset = it.localToRoot(Offset(0f, 0f))
                intOffset = IntOffset(offset. x. toInt(), offset. y. toInt())
            }
    ) {<!-- -->
        Icon(
         ?…
        )
    }
}

Record the size information of the page

/** create notes */
var cardSize by mutableStateOf(IntSize(0, 0))
var createNoteUIOffset by mutableStateOf(IntOffset(0, 0))
var currentCreateNoteState by mutableStateOf(CreateNoteState.Closed)

Jump interface

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun CreateNotePage(
    pageState: CreateNoteState,
    cardSize: IntSize,
    fullSize: IntSize,
    cardOffset: IntOffset,
    onPageClosing: () -> Unit,
    onPageClosed: () -> Unit
) {
    var animReady by remember { mutableStateOf(false) }
    var animFinish by remember { mutableStateOf(false) }
    val background by animateColorAsState(
        if (pageState == CreateNoteState.Closing) AppColor.themeColor else Color.Transparent)
    val alpha by animateFloatAsState(
        targetValue = if (pageState == CreateNoteState.Closing) 1f else 0.6f,
        animationSpec = tween(durationMillis = 300)
    )

    val DEPLOYMENT_DURATION = 500
    val size by animateIntSizeAsState(if (pageState > CreateNoteState.Closed) fullSize else cardSize,
        animationSpec = tween(DEPLOYMENT_DURATION))

    val fullOffset = remember { IntOffset(0, 0) }
    val offsetAnimatable = remember { Animatable(IntOffset(0, 0), IntOffset. VectorConverter) }
    val cornerSize by animateDpAsState(if (animFinish) 0.dp else 16.dp)


    LaunchedEffect(pageState) {
        when (pageState) {
            CreateNoteState. Opening -> {
                animReady = true
                offsetAnimatable. snapTo(cardOffset)
                offsetAnimatable.animateTo(fullOffset, animationSpec = tween(DEPLOYMENT_DURATION))
                animFinish = true
            }
            CreateNoteState. Closing -> {
                animFinish = false
                offsetAnimatable. snapTo(fullOffset)
                offsetAnimatable.animateTo(cardOffset, animationSpec = tween(DEPLOYMENT_DURATION))
                animReady = false
                onPageClosed()
            }
            else -> {}
        }
    }
    if (pageState != CreateNoteState.Closed & amp; & amp; animReady) {
        Box(
            Modifier
                .offset { offsetAnimatable.value }
                .clip(RoundedCornerShape(cornerSize))
                .width(with(LocalDensity.current) { size.width.toDp() })
                .height(with(LocalDensity.current) { size.height.toDp() })

        ) {
            CreateNoteUI(onBack = onPageClosing) // real interface
            if (pageState == CreateNoteState. Closing){
                Box(Modifier. fillMaxSize()
                    .alpha(alpha)
                    .background(background))
            }
        }
    }

Full source code

JIULANG9/WordsFairyNote: WordsFairyNote source code (github.com)