Jetpack Compose 将动画实现的门槛降低了,不过Compose目前还不支持共享元素过渡。
(上篇文章Jetpack Compose开发的本地笔记本)的动画效果的实现
转跳前的准备工作
定义State
枚举类来表示页面的三种状态:
Closing(关闭状态)
Closed(关闭完成状态)
Opening(展开状态)
\
enum class CreateNoteState {
Closing, Closed, Opening
}
Jetpack Compose中的mutableStateOf()
函数来创建可变状态,并分别初始化了三个变量cardSize
、createNoteUIOffset
和currentCreateNoteState
。
cardSize
是一个IntSize
类型的可变状态,用来表示页面的尺寸,初始值为(0, 0)
。
createNoteUIOffset
是一个IntOffset
类型的可变状态,用来表示创建笔记界面的偏移量,初始值为(0, 0)
。
currentCreateNoteState
是一个枚举类型CreateNoteState
的可变状态,用来表示创建笔记界面的当前状态,初始值为State.Closed
,即关闭状态。这个枚举类型可能包括Closing
、Closed
、Opening
等状态。
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状态:调用offsetAnimatable
的animateTo()
函数实现展开动画,将页面偏移从cardOffset
变化到fullOffset
;设置animFinish为true。 - Closing状态:调用offsetAnimatable
的animateTo()
函数实现关闭动画,将页面偏移从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() })
) {
...
你的界面
...
}
}
完整效果图
完整代码
转跳按钮
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)