Compose 动画 - 共享元素过渡动画 SharedTransitionLayout

官方介绍页

一、概念

共享元素过渡是一种在内容之间具有一致的可组合项之间的无缝过渡方式。它们通常用于导航,当用户在这些屏幕之间导航时,可以直观地将不同的屏幕连接起来。

SharedTransitionLayout

fun SharedTransitionLayout(
    modifier: Modifier = Modifier,
    content: @Composable SharedTransitionScope.() -> Unit
)

实现共享元素转换所需的最外层布局,提供了 SharedTransitionScope,可组合项需要位于该作用域中才能使用以下两个共享元素修饰符(子布局如果是通过调用而不是直接写在作用域内,需要将作用域传递给子布局)。

Modifier.sharedElement()

fun Modifier.sharedElement(
        state: SharedContentState,
        animatedVisibilityScope: AnimatedVisibilityScope,
        boundsTransform: BoundsTransform = DefaultBoundsTransform,        //用于更改大小和位置移动的动画规范 AnimationSpec
        placeHolderSize: PlaceHolderSize = contentSize,
        renderInOverlayDuringTransition: Boolean = true,
        zIndexInOverlay: Float = 0f,
        clipInOverlayDuringTransition: OverlayClip = ParentClip
): Modifier

适用于内容相同。只有该元素在页面转换时可见。

Modifier.sharedBounds()

fun Modifier.sharedBounds(
        sharedContentState: SharedContentState,
        animatedVisibilityScope: AnimatedVisibilityScope,
        enter: EnterTransition = fadeIn(),        //入场动画
        exit: ExitTransition = fadeOut(),        //出场动画
        boundsTransform: BoundsTransform = DefaultBoundsTransform,        //用于更改大小和位置移动的动画规范 AnimationSpec
        resizeMode: ResizeMode = ScaleToBounds(ContentScale.FillWidth, Center),        //调整大小模式,确定共享元素如何在两种状态之间转换
        placeHolderSize: PlaceHolderSize = contentSize,
        renderInOverlayDuringTransition: Boolean = true,
        zIndexInOverlay: Float = 0f,
        clipInOverlayDuringTransition: OverlayClip = ParentClip
    ): Modifier

适用于视觉上不同但有相同元素。不相同的元素在页面转换时可见变换。

SharedContentState

fun rememberSharedContentState(key: Any): SharedContentState

用于区分和匹配共享的元素

  • 注意共享元素修饰符的顺序, 如果匹配的两个元素设置尺寸的修饰符顺序不同(如padding),会造成大小不一失去了“同一元素”的初衷。 

二、基本使用

结合 AnimatedContent() 或 AnimatedVisibility() 使用,将内容变化的动画交由系统处理。

implementation "androidx.compose.animation:animation:1.7.0-beta01"

2.1 共享元素 sharedElement

共享了 PageOne 和 PageTwo 中的图片,从 PageOne 切换至 PageTwo 能够以这张图片作为过渡元素展示动画。

@Composable
fun Screen() {
    var isShowDetail by remember { mutableStateOf(true) }
    //使用共享元素布局
    SharedTransitionLayout {
        //将界面切换动画交给系统处理
        AnimatedContent(targetState = isShowDetail) { targetState ->
            if (targetState) {
                PageOne(
                    onClick = { isShowDetail = false },
                    //共享作用域传递给子布局,为了使用sharedElement
                    sharedTransitionScope = this@SharedTransitionLayout,
                    //动画作用于传递给子布局,用于协调动画
                    animatedVisibilityScope = this@AnimatedContent
                )
            } else {
                PageTwo(
                    onClick = { isShowDetail = true },
                    sharedTransitionScope = this@SharedTransitionLayout,
                    animatedVisibilityScope = this@AnimatedContent
                )
            }
        }
    }
}
@Composable
fun PageOne(
    onClick: () -> Unit,
    sharedTransitionScope: SharedTransitionScope,
    animatedVisibilityScope: AnimatedVisibilityScope
) {
    Box(
        modifier = Modifier.fillMaxSize().clickable { onClick() },
        contentAlignment = Alignment.TopStart   //图在左上角
    ) {
        with(sharedTransitionScope) {
            Image(
                painter = painterResource(R.drawable.pic_baidu),
                contentDescription = null,
                modifier = Modifier.sharedElement(  //sharedElement标记用于共享的元素
                    state = rememberSharedContentState(key = "image"),  //key用于区分和匹配元素
                    animatedVisibilityScope = animatedVisibilityScope
                )
            )
        }
    }
}

@Composable
fun PageTwo(
    onClick: () -> Unit,
    sharedTransitionScope: SharedTransitionScope,
    animatedVisibilityScope: AnimatedVisibilityScope
) {
    Box(
        modifier = Modifier.fillMaxSize().clickable { onClick() },
        contentAlignment = Alignment.BottomEnd  //图在右下角
    ) {
        with(sharedTransitionScope) {
            Image(
                painter = painterResource(R.drawable.pic_baidu),
                contentDescription = null,
                modifier = Modifier.sharedElement(    //sharedElement标记用于共享的元素
                    state = rememberSharedContentState(key = "image"),    //key用于区分和匹配元素
                    animatedVisibilityScope = animatedVisibilityScope
                )
            )
        }
    }
}

2.2 共享边界 sharedBounds

三、自定义

3.1 “调整大小”模式 resizeMode

ScaleToBoundsRemeasureToBounds
首先会使用先行(或目标)约束条件测量子布局。然后,系统会缩放子项的稳定布局,以适应共享边界。可以视为状态之间的“图形缩放”。会根据目标尺寸,通过固定的动画约束条件重新测量并重新布局 sharedBounds 的子布局。边界大小变化(可能是每一帧的变化)都会触发重新测量。
  • 对于 Text 可组合项,建议使用 ScaleToBounds,因为它可以避免对文本进行重新布局和重排到不同的行。对于宽高比不同的边界,以及如果您希望两个共享元素之间具有流畅的连续性,建议使用 RemeasureToBounds。

3.2 使用动画规范 boundsTransform

使用 AnimationSpec 来更改大小和位置移动,如文本随弧形运动而移动使用 keyframes。

val textBoundsTransform = BoundsTransform { initialBounds, targetBounds ->
    keyframes {
        durationMillis = boundsAnimationDurationMillis
        initialBounds at 0 using ArcMode.ArcBelow using FastOutSlowInEasing
        targetBounds at boundsAnimationDurationMillis
    }
}
Text(
    "Cupcake", fontSize = 28.sp,
    modifier = Modifier.sharedBounds(
        rememberSharedContentState(key = "title"),
        animatedVisibilityScope = animatedVisibilityScope,
        boundsTransform = textBoundsTransform
    )
)

3.3 跳至最终布局(避免文字重排版)Modifier.skipToLookahead()

默认情况下,在两个布局之间转换时,布局尺寸会在其起始状态和最终状态之间以动画形式显示。在为文本等内容添加动画效果时,这可能是不希望出现的行为。

使用未使用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值