如何在Android Compose中实现两个视图之间的幻灯片效果动画同步?

15
我知道可以使用AnimatedVisibility组合函数实现可见性动画的滑入动画,但我想要实现的是当一个布局处于进入动画时,另一个布局处于退出动画,类似于下面的图像。
注意:我知道应该使用Navigation compose来处理不同屏幕之间的转换动画,但目前这个功能还在开发中,我想在屏幕的一部分内容上实现这个效果,类似于CrossFade Animation。

enter image description here


1
你应该试试Navigator Compose。在导航到其他目的地时,它提供了一些内置的enterexit动画。 - kaustubhpatange
5个回答

14

如您所提到的,这个动画应该由导航库实现,并且已经有相关问题单

考虑到这一点,我会在此留下我的答案,希望对您有所帮助……

我来分成三个部分:

  1. 容器:
@Composable
fun SlideInAnimationScreen() {
    // I'm using the same duration for all animations. 
    val animationTime = 300 

    // This state is controlling if the second screen is being displayed or not
    var showScreen2 by remember { mutableStateOf(false) }

    // This is just to give that dark effect when the first screen is closed...
    val color = animateColorAsState(
        targetValue = if (showScreen2) Color.DarkGray else Color.Red,
        animationSpec = tween(
            durationMillis = animationTime,
            easing = LinearEasing
        )
    )
    Box(Modifier.fillMaxSize()) {
       // Both Screen1 and Screen2 are declared here...
    }
}
  1. 第一个屏幕只需进行小幅滑动即可创建视差效果。我还将背景色从红色更改为黑色,以产生重叠/隐藏/暗淡的效果。

// Screen 1
AnimatedVisibility(
    !showScreen2,
    modifier = Modifier.fillMaxSize(),
    enter = slideInHorizontally(
        initialOffsetX = { -300 }, // small slide 300px
        animationSpec = tween(
            durationMillis = animationTime, 
            easing = LinearEasing // interpolator
        )
    ),
    exit = slideOutHorizontally(
        targetOffsetX = { -300 }, =
        animationSpec = tween(
            durationMillis = animationTime, 
            easing = LinearEasing
        )
    )
) {
    Box(
        Modifier
            .fillMaxSize()
            .background(color.value) // animating the color
    ) {
        Button(modifier = Modifier.align(Alignment.Center),
            onClick = {
                showScreen2 = true
            }) {
            Text(text = "Ok")
        }
    }
}
  1. 第二个问题实际上是来自边缘滑动。
// Screen 2
AnimatedVisibility(
    showScreen2,
    modifier = Modifier.fillMaxSize(),
    enter = slideInHorizontally(
        initialOffsetX = { it }, // it == fullWidth
        animationSpec = tween(
            durationMillis = animationTime, 
            easing = LinearEasing
        )
    ),
    exit = slideOutHorizontally(
        targetOffsetX = { it },
        animationSpec = tween(
            durationMillis = animationTime, 
            easing = LinearEasing
        )
    )
) {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Blue)
    ) {
        Button(modifier = Modifier.align(Alignment.Center),
            onClick = {
                showScreen2 = false
            }) {
            Text(text = "Back")
        }
    }
}

这是结果:

输入图像描述


2
能否将您的解决方案进行重构,使其更加通用并支持多屏幕,类似于CrossFade? - David Ibrahim

5

在研究了CrossFade的代码后,我为交叉幻灯片实现了类似的功能,并且它还支持在按下后退按钮时进行反向动画。

这里是代码:https://gist.github.com/DavidIbrahim/5f4c0387b571f657f4de976822c2a225

使用示例

@Composable
fun CrossSlideExample(){
    var currentPage by remember { mutableStateOf("A") }
    CrossSlide(targetState = currentPage, reverseAnimation: Boolean = false) { screen ->
        when (screen) {
            "A" -> Text("Page A")
            "B" -> Text("Page B")
        }
    }
}

很好,干净的解决方案。 - Rafael

3

1
具有讽刺意味的是,Google Accompanist并不允许这种精确类型的动画。 - Serhii Pokrovskyi
有人已经想出如何使用Accompanist库实现这种动画了吗? - Patrick

0
这是一个基于@nglauber解决方案的综合解决方案,可以给人一种独立于视图背景的滑动视图的错觉。

View Sliding

演示部分——
@Composable
fun ViewSliderDemo() {
    val slider = ViewSlider()

    Box(Modifier.fillMaxSize()) {
        slider.View(
            type= ViewSlider.Type.parent,
        ) {
            Button(modifier = Modifier.align(Alignment.Center),
                onClick = {
                    slider.state.value = true
                }) {
                Text(text = "Child >>>>")
            }
        }
        slider.View(
            type= ViewSlider.Type.child,
        ) {
            Button(modifier = Modifier.align(Alignment.Center),
                onClick = {
                    slider.state.value = false
                }) {
                Text(text = "<<<< Parent")
            }
        }
    }

}

代码部分 — — —

class Observed<T>(startWith: T) : MutableState<T> {
    private var _value by mutableStateOf(startWith)
    override var value: T = startWith
        get() = _value
        set(value) {
            _value = value
            field = value
        }
    override fun component1(): T = value
    override fun component2(): (T) -> Unit = { value = it }
}

class ViewSlider {
    val animationTime = 300
    var state = Observed(startWith = false)

    enum class Type { parent, child }

    @OptIn(ExperimentalAnimationApi::class)
    @Composable
    fun View(
        type: Type,
        content: @Composable () -> Unit
    ) {
        var shadowSize by remember { mutableStateOf( 10.dp ) }
        AnimatedVisibility(
            visible = if (type == Type.parent) !state.value else state.value,
            modifier = Modifier.fillMaxSize(),
            enter = slideInHorizontally(
                initialOffsetX = {
                    if (type == Type.parent) -300 else it
                },
                animationSpec = tween(
                    durationMillis = animationTime,
                    easing = LinearEasing // interpolator
                )
            ),
            exit = slideOutHorizontally(
                targetOffsetX = {
                    if (type == Type.parent) -300 else it
                },
                animationSpec = tween(
                    durationMillis = animationTime,
                    easing = LinearEasing
                )
            )
        ) {
            shadowSize = if (this.transition.currentState == this.transition.targetState) 0.dp else 10.dp
            Box {
                if (type == Type.child) {
                    Row {
                        Box(Modifier
                                .fillMaxHeight()
                                .width(shadowSize)
                                .background(
                                    brush = Brush.horizontalGradient(
                                        colors = listOf(Color.LightGray, Color.Gray)
                                    )
                                )
                        )
                        Spacer(Modifier.weight(1f))
                    }
                }
                content()
                if (type == Type.parent) {
                    Row {
                        Spacer(Modifier.weight(1f))
                        Box(Modifier
                                .fillMaxHeight()
                                .width(shadowSize)
                                .background(
                                    brush = Brush.horizontalGradient(
                                        colors = listOf(Color.Gray, Color.LightGray)
                                    )
                                )
                        )
                    }
                }
            }

        }
    }

}

-4

目前,Compose 中没有与 Activity 转换相媲美的东西。 我希望 Jetpack 正在开发它们。许多转换 API 要么是 internal,要么是 private,因此实现一个好的转换更加困难。

如果用于生产,请使用带有 Navigation Host 的 Activity/Fragment。如果不使用导航组件,则可以使用 AnimatedVisibility 进行滑动。

https://issuetracker.google.com/issues/172112072


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接