Jetpack Compose 将动画实现的门槛降低了,不过Compose目前还不支持共享元素过渡。

(上篇文章Jetpack Compose开发的本地笔记本)的动画效果的实现

转跳前的准备工作

定义State枚举类来表示页面的三种状态:
Closing(关闭状态)
Closed(关闭完成状态)
Opening(展开状态)\

enum class CreateNoteState {
    Closing, Closed, Opening
}

Jetpack Compose中的mutableStateOf()函数来创建可变状态,并分别初始化了三个变量cardSizecreateNoteUIOffsetcurrentCreateNoteState
cardSize是一个IntSize类型的可变状态,用来表示页面的尺寸,初始值为(0, 0)
createNoteUIOffset是一个IntOffset类型的可变状态,用来表示创建笔记界面的偏移量,初始值为(0, 0)
currentCreateNoteState是一个枚举类型CreateNoteState的可变状态,用来表示创建笔记界面的当前状态,初始值为State.Closed,即关闭状态。这个枚举类型可能包括ClosingClosedOpening等状态。

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

点击转跳的按钮

onSizeChanged 用于在转跳按钮 的大小发生变化时更新布局,并将新的大小传递给 onSizedChanged 回调函数。
onGloballyPositioned 用于在 转跳按钮 的位置发生变化时更新布局,并将新的位置传递给 intOffset 变量。
最后,当用户点击 转跳按钮 时,会调用 onClick 回调函数,并将 intOffset 变量作为参数传递出去。

@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 ->
    //点击事件
    viewModel.currentCreateNoteState = CreateNoteState.Opening
    viewModel.createNoteUIOffset = offset
}

转跳界面

记录页面的大小信息,包括
cardSize(折叠状态大小)、
fullSize(完全展开状态大小)
cardOffset(折叠状态页面在屏幕中的偏移位置)。

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

定义offsetAnimatable来记录和控制页面在动画过程中在屏幕中的偏移变化。使用animateTo()函数来实现从cardOffset变化到fullOffset的平移动画效果。

var animReady by remember { mutableStateOf(false) }//标记动画准备
var animFinish by remember { mutableStateOf(false) }//标记动完成
val offsetAnimatable = remember { Animatable(IntOffset(0, 0), IntOffset.VectorConverter) }
val DEPLOYMENT_DURATION = 500 //动画速度

val cornerSize by animateDpAsState(if (animFinish) 0.dp  else 16.dp) //圆角

使用LaunchedEffect来监听CreateNoteState的变化,并根据不同的状态触发相应的动画效果: - Opening状态:调用offsetAnimatableanimateTo()函数实现展开动画,将页面偏移从cardOffset变化到fullOffset;设置animFinish为true。 - Closing状态:调用offsetAnimatableanimateTo()函数实现关闭动画,将页面偏移从fullOffset变化到cardOffset;设置animFinish为false和animReady为false。 - Closed状态:页面关闭完成,无需执行任何操作。

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 -> {}
    }
}

使用Box组件及其Modifier应用offsetAnimatable.value、大小变化size和圆角cornerSize的动画效果在页面上显示。

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

    ) {
       ...
       你的界面
       ...
    }
}

Jetpack Compose 实现了一个丝滑流畅的页面展开和关闭的效果动画-LMLPHP

完整效果图

完整代码

转跳按钮

HomeAddButton(
    Modifier
        .navigationBarsPadding()
        .align(Alignment.BottomEnd),
    onSizedChanged = {
        viewModel.cardSize = it
    }
) { offset ->
    //点击事件
    viewModel.currentCreateNoteState = CreateNoteState.Opening
    viewModel.createNoteUIOffset = offset
    //震动
    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(
         ......
        )
    }
}

记录页面的大小信息

/** 创建笔记 */
var cardSize by mutableStateOf(IntSize(0, 0))
var createNoteUIOffset by mutableStateOf(IntOffset(0, 0))
var currentCreateNoteState by mutableStateOf(CreateNoteState.Closed)

转跳的界面

@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 && 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) // 真正的界面
            if (pageState == CreateNoteState.Closing){
                Box(Modifier.fillMaxSize()
                    .alpha(alpha)
                    .background(background))
            }
        }
    }

完整源码

JIULANG9/WordsFairyNote: 词仙笔记源码 (github.com)

05-24 14:10