Android Compose 框架导航动画之路由切换动画深度剖析(三十七)

348 阅读14分钟

Android Compose 框架导航动画之路由切换动画深度剖析

一、引言

1.1 背景与意义

在移动应用开发中,用户体验至关重要。而流畅且富有吸引力的导航动画能够显著提升用户在应用内的操作感受,使界面的切换更加自然和直观。Android Compose 作为现代 Android UI 工具包,为开发者提供了强大而灵活的导航动画功能,尤其是路由切换动画。通过深入了解和运用这些动画,开发者可以打造出更加生动、有趣的应用界面。

1.2 文章目标

本文旨在深入剖析 Android Compose 框架中导航动画的路由切换动画部分。从基础概念入手,逐步引导读者了解路由切换动画的原理、使用方法,进而深入到源码级别进行分析,揭示其背后的实现机制。同时,通过实际案例展示如何在项目中应用这些动画,最后对未来的发展进行展望。

二、Android Compose 导航基础回顾

2.1 导航组件概述

在 Android Compose 中,导航主要通过 NavHostNavControllerNavGraph 等组件来实现。

2.1.1 NavHost

NavHost 是一个可组合函数,它是导航图的容器。在 NavHost 中,我们可以定义导航图的起始目的地和各个路由的配置。以下是一个简单的 NavHost 使用示例:

kotlin

import androidx.compose.runtime.Composable
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController

@Composable
fun AppNavigation() {
    // 创建一个 NavController 实例,用于控制导航操作
    val navController = rememberNavController()
    // 定义 NavHost,指定导航控制器和起始目的地
    NavHost(
        navController = navController,
        startDestination = "screen1"
    ) {
        // 定义路由 "screen1" 对应的可组合函数
        composable("screen1") {
            Screen1(navController)
        }
        // 定义路由 "screen2" 对应的可组合函数
        composable("screen2") {
            Screen2(navController)
        }
    }
}

@Composable
fun Screen1(navController: NavHostController) {
    // 屏幕 1 的 UI 内容
}

@Composable
fun Screen2(navController: NavHostController) {
    // 屏幕 2 的 UI 内容
}

在上述代码中,NavHost 作为导航的核心容器,通过 navController 来管理导航操作,startDestination 指定了应用启动时显示的第一个屏幕。composable 函数用于定义每个路由对应的可组合函数,即当导航到该路由时显示的界面。

2.1.2 NavController

NavController 负责处理导航操作,如跳转、返回等。它提供了 navigate 方法用于跳转到指定的路由,以及 popBackStack 方法用于返回上一个路由。例如:

kotlin

// 在 Screen1 中跳转到 Screen2
Button(onClick = { navController.navigate("screen2") }) {
    Text("Go to Screen 2")
}

// 在 Screen2 中返回 Screen1
Button(onClick = { navController.popBackStack() }) {
    Text("Go back to Screen 1")
}
2.1.3 NavGraph

NavGraph 定义了应用的导航图,它包含了所有的路由和它们之间的关系。在 NavHost 中,我们通过一系列的 composable 函数来构建导航图。

2.2 路由的基本概念

路由(Route)在 Android Compose 导航中代表一个特定的屏幕或界面。每个路由都有一个唯一的标识符,通常是一个字符串。当我们调用 navigate 方法时,需要指定目标路由的标识符。例如,在上面的示例中,"screen1""screen2" 就是两个不同的路由。

三、路由切换动画基础

3.1 动画的基本概念

在 Android Compose 中,动画是通过改变可组合元素的属性值来实现的。例如,改变元素的透明度、位置、大小等属性,从而产生动画效果。动画的核心在于控制属性值随时间的变化,而 Compose 提供了丰富的 API 来实现这一点。

3.2 路由切换动画的作用

路由切换动画用于在不同的路由之间进行过渡,使界面的切换更加平滑和自然。它可以增强用户体验,让用户更清晰地感知到界面的变化。例如,在从一个列表页面切换到详情页面时,使用动画可以让用户更容易理解页面的跳转逻辑。

3.3 简单的路由切换动画示例

在 Android Compose 中,我们可以通过 navOptions 参数为 navigate 方法添加路由切换动画。以下是一个简单的淡入淡出动画示例:

kotlin

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.navigation.NavOptions
import androidx.navigation.Navigation
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController

@Composable
fun AnimatedNavigationExample() {
    // 创建一个 NavController 实例
    val navController = rememberNavController()
    // 定义 NavHost,指定导航控制器和起始目的地
    NavHost(
        navController = navController,
        startDestination = "screen1"
    ) {
        // 定义路由 "screen1" 对应的可组合函数
        composable("screen1") {
            Screen1(navController)
        }
        // 定义路由 "screen2" 对应的可组合函数
        composable("screen2") {
            Screen2(navController)
        }
    }
}

@Composable
fun Screen1(navController: NavHostController) {
    Column(modifier = Modifier.fillMaxSize()) {
        Text("Screen 1")
        // 定义一个淡入淡出动画的 NavOptions
        val navOptions = NavOptions.Builder()
           .setEnterAnim(androidx.compose.animation.core.tween<Int>(durationMillis = 300))
           .setExitAnim(androidx.compose.animation.core.tween<Int>(durationMillis = 300))
           .setPopEnterAnim(androidx.compose.animation.core.tween<Int>(durationMillis = 300))
           .setPopExitAnim(androidx.compose.animation.core.tween<Int>(durationMillis = 300))
           .build()
        // 点击按钮跳转到 Screen2,并应用动画
        Button(onClick = { navController.navigate("screen2", navOptions) }) {
            Text("Go to Screen 2")
        }
    }
}

@Composable
fun Screen2(navController: NavHostController) {
    Column(modifier = Modifier.fillMaxSize()) {
        Text("Screen 2")
        // 点击按钮返回 Screen1,并应用动画
        Button(onClick = { navController.popBackStack() }) {
            Text("Go back to Screen 1")
        }
    }
}

在上述代码中,我们通过 NavOptions.Builder 创建了一个 NavOptions 对象,并设置了进入动画、退出动画、返回进入动画和返回退出动画的持续时间为 300 毫秒。在 navigate 方法中传入这个 NavOptions 对象,就可以为路由切换添加动画效果。

四、常见的路由切换动画类型及实现

4.1 淡入淡出动画

淡入淡出动画是一种常见的路由切换动画,它通过改变界面的透明度来实现过渡效果。在 Android Compose 中,我们可以使用 fadeInfadeOut 动画来实现淡入淡出效果。

4.1.1 实现代码

kotlin

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.navigation.NavOptions
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController

@Composable
fun FadeAnimationExample() {
    // 创建一个 NavController 实例
    val navController = rememberNavController()
    // 定义 NavHost,指定导航控制器和起始目的地
    NavHost(
        navController = navController,
        startDestination = "screen1"
    ) {
        // 定义路由 "screen1" 对应的可组合函数
        composable("screen1") {
            Screen1(navController)
        }
        // 定义路由 "screen2" 对应的可组合函数
        composable("screen2") {
            Screen2(navController)
        }
    }
}

@Composable
fun Screen1(navController: NavHostController) {
    Column(modifier = Modifier.fillMaxSize()) {
        Text("Screen 1")
        // 定义淡入淡出动画的 NavOptions
        val navOptions = NavOptions.Builder()
           .setEnterAnim(androidx.compose.animation.fadeIn(animationSpec = tween(durationMillis = 500)).animationSpec)
           .setExitAnim(androidx.compose.animation.fadeOut(animationSpec = tween(durationMillis = 500)).animationSpec)
           .setPopEnterAnim(androidx.compose.animation.fadeIn(animationSpec = tween(durationMillis = 500)).animationSpec)
           .setPopExitAnim(androidx.compose.animation.fadeOut(animationSpec = tween(durationMillis = 500)).animationSpec)
           .build()
        // 点击按钮跳转到 Screen2,并应用动画
        Button(onClick = { navController.navigate("screen2", navOptions) }) {
            Text("Go to Screen 2")
        }
    }
}

@Composable
fun Screen2(navController: NavHostController) {
    Column(modifier = Modifier.fillMaxSize()) {
        Text("Screen 2")
        // 点击按钮返回 Screen1,并应用动画
        Button(onClick = { navController.popBackStack() }) {
            Text("Go back to Screen 1")
        }
    }
}
4.1.2 源码分析

在上述代码中,我们使用 fadeInfadeOut 函数创建了淡入和淡出动画。tween 函数用于指定动画的持续时间。在 NavOptions 中,setEnterAnimsetExitAnimsetPopEnterAnimsetPopExitAnim 方法分别设置了进入、退出、返回进入和返回退出时的动画。

4.2 滑动动画

滑动动画通过改变界面的位置来实现过渡效果,例如从左到右、从右到左、从上到下或从下到上滑动。

4.2.1 实现代码

kotlin

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.navigation.NavOptions
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController

@Composable
fun SlideAnimationExample() {
    // 创建一个 NavController 实例
    val navController = rememberNavController()
    // 定义 NavHost,指定导航控制器和起始目的地
    NavHost(
        navController = navController,
        startDestination = "screen1"
    ) {
        // 定义路由 "screen1" 对应的可组合函数
        composable("screen1") {
            Screen1(navController)
        }
        // 定义路由 "screen2" 对应的可组合函数
        composable("screen2") {
            Screen2(navController)
        }
    }
}

@Composable
fun Screen1(navController: NavHostController) {
    Column(modifier = Modifier.fillMaxSize()) {
        Text("Screen 1")
        // 定义滑动动画的 NavOptions
        val navOptions = NavOptions.Builder()
           .setEnterAnim(androidx.compose.animation.slideInHorizontally(
                initialOffsetX = { fullWidth -> fullWidth },
                animationSpec = tween(durationMillis = 500)
            ).animationSpec)
           .setExitAnim(androidx.compose.animation.slideOutHorizontally(
                targetOffsetX = { fullWidth -> -fullWidth },
                animationSpec = tween(durationMillis = 500)
            ).animationSpec)
           .setPopEnterAnim(androidx.compose.animation.slideInHorizontally(
                initialOffsetX = { fullWidth -> -fullWidth },
                animationSpec = tween(durationMillis = 500)
            ).animationSpec)
           .setPopExitAnim(androidx.compose.animation.slideOutHorizontally(
                targetOffsetX = { fullWidth -> fullWidth },
                animationSpec = tween(durationMillis = 500)
            ).animationSpec)
           .build()
        // 点击按钮跳转到 Screen2,并应用动画
        Button(onClick = { navController.navigate("screen2", navOptions) }) {
            Text("Go to Screen 2")
        }
    }
}

@Composable
fun Screen2(navController: NavHostController) {
    Column(modifier = Modifier.fillMaxSize()) {
        Text("Screen 2")
        // 点击按钮返回 Screen1,并应用动画
        Button(onClick = { navController.popBackStack() }) {
            Text("Go back to Screen 1")
        }
    }
}
4.2.2 源码分析

在上述代码中,slideInHorizontallyslideOutHorizontally 函数用于创建水平滑动动画。initialOffsetXtargetOffsetX 参数分别指定了动画开始和结束时的偏移量。tween 函数同样用于指定动画的持续时间。

4.3 缩放动画

缩放动画通过改变界面的大小来实现过渡效果,例如放大或缩小。

4.3.1 实现代码

kotlin

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.navigation.NavOptions
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController

@Composable
fun ScaleAnimationExample() {
    // 创建一个 NavController 实例
    val navController = rememberNavController()
    // 定义 NavHost,指定导航控制器和起始目的地
    NavHost(
        navController = navController,
        startDestination = "screen1"
    ) {
        // 定义路由 "screen1" 对应的可组合函数
        composable("screen1") {
            Screen1(navController)
        }
        // 定义路由 "screen2" 对应的可组合函数
        composable("screen2") {
            Screen2(navController)
        }
    }
}

@Composable
fun Screen1(navController: NavHostController) {
    Column(modifier = Modifier.fillMaxSize()) {
        Text("Screen 1")
        // 定义缩放动画的 NavOptions
        val navOptions = NavOptions.Builder()
           .setEnterAnim(androidx.compose.animation.scaleIn(
                initialScale = 0.5f,
                animationSpec = tween(durationMillis = 500)
            ).animationSpec)
           .setExitAnim(androidx.compose.animation.scaleOut(
                targetScale = 0.5f,
                animationSpec = tween(durationMillis = 500)
            ).animationSpec)
           .setPopEnterAnim(androidx.compose.animation.scaleIn(
                initialScale = 0.5f,
                animationSpec = tween(durationMillis = 500)
            ).animationSpec)
           .setPopExitAnim(androidx.compose.animation.scaleOut(
                targetScale = 0.5f,
                animationSpec = tween(durationMillis = 500)
            ).animationSpec)
           .build()
        // 点击按钮跳转到 Screen2,并应用动画
        Button(onClick = { navController.navigate("screen2", navOptions) }) {
            Text("Go to Screen 2")
        }
    }
}

@Composable
fun Screen2(navController: NavHostController) {
    Column(modifier = Modifier.fillMaxSize()) {
        Text("Screen 2")
        // 点击按钮返回 Screen1,并应用动画
        Button(onClick = { navController.popBackStack() }) {
            Text("Go back to Screen 1")
        }
    }
}
4.3.2 源码分析

在上述代码中,scaleInscaleOut 函数用于创建缩放动画。initialScaletargetScale 参数分别指定了动画开始和结束时的缩放比例。tween 函数用于指定动画的持续时间。

五、路由切换动画的源码分析

5.1 NavOptions 源码分析

NavOptions 类用于配置导航操作的选项,包括动画、过渡效果等。以下是 NavOptions 类的部分源码:

kotlin

data class NavOptions(
    // 进入动画资源 ID
    val enterAnim: Int = -1,
    // 退出动画资源 ID
    val exitAnim: Int = -1,
    // 返回进入动画资源 ID
    val popEnterAnim: Int = -1,
    // 返回退出动画资源 ID
    val popExitAnim: Int = -1,
    // 其他配置参数...
) {
    class Builder {
        private var enterAnim = -1
        private var exitAnim = -1
        private var popEnterAnim = -1
        private var popExitAnim = -1

        // 设置进入动画
        fun setEnterAnim(enterAnim: Int): Builder {
            this.enterAnim = enterAnim
            return this
        }

        // 设置退出动画
        fun setExitAnim(exitAnim: Int): Builder {
            this.exitAnim = exitAnim
            return this
        }

        // 设置返回进入动画
        fun setPopEnterAnim(popEnterAnim: Int): Builder {
            this.popEnterAnim = popEnterAnim
            return this
        }

        // 设置返回退出动画
        fun setPopExitAnim(popExitAnim: Int): Builder {
            this.popExitAnim = popExitAnim
            return this
        }

        // 构建 NavOptions 对象
        fun build(): NavOptions {
            return NavOptions(
                enterAnim = enterAnim,
                exitAnim = exitAnim,
                popEnterAnim = popEnterAnim,
                popExitAnim = popExitAnim
            )
        }
    }
}

在上述源码中,NavOptions 类包含了进入动画、退出动画、返回进入动画和返回退出动画的资源 ID。Builder 类提供了一系列的设置方法,用于配置这些动画资源 ID,最后通过 build 方法构建 NavOptions 对象。

5.2 NavController 源码分析

NavController 类负责处理导航操作,在导航过程中会应用 NavOptions 中配置的动画。以下是 NavController 类中与动画相关的部分源码:

kotlin

class NavController(
    private val context: Context,
    private val navigatorProvider: NavigatorProvider
) : NavigatorProvider by navigatorProvider {
    // 导航到指定路由
    fun navigate(
        route: String,
        navOptions: NavOptions? = null,
        navigatorExtras: Navigator.Extras? = null
    ) {
        // 获取目标目的地
        val destination = graph.findDestination(route)
        if (destination!= null) {
            // 创建导航请求
            val request = NavDestination.Directions(
                destination = destination,
                arguments = Bundle()
            )
            // 应用导航选项
            applyNavOptions(request, navOptions)
            // 执行导航操作
            navigate(request, navigatorExtras)
        }
    }

    // 应用导航选项
    private fun applyNavOptions(
        request: NavDestination.Directions,
        navOptions: NavOptions?
    ) {
        if (navOptions!= null) {
            // 设置进入动画
            request.enterAnim = navOptions.enterAnim
            // 设置退出动画
            request.exitAnim = navOptions.exitAnim
            // 设置返回进入动画
            request.popEnterAnim = navOptions.popEnterAnim
            // 设置返回退出动画
            request.popExitAnim = navOptions.popExitAnim
        }
    }
}

在上述源码中,navigate 方法用于导航到指定的路由。在导航过程中,会调用 applyNavOptions 方法将 NavOptions 中配置的动画资源 ID 应用到导航请求中。

5.3 动画执行源码分析

在导航过程中,动画的执行是通过 Animator 来实现的。以下是简化的动画执行源码:

kotlin

import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.view.View

// 执行进入动画
fun executeEnterAnimation(view: View, enterAnim: Int) {
    if (enterAnim!= -1) {
        // 根据动画资源 ID 创建动画
        val animator = createAnimator(view, enterAnim)
        animator.start()
    }
}

// 执行退出动画
fun executeExitAnimation(view: View, exitAnim: Int) {
    if (exitAnim!= -1) {
        // 根据动画资源 ID 创建动画
        val animator = createAnimator(view, exitAnim)
        animator.start()
    }
}

// 创建动画
private fun createAnimator(view: View, animResId: Int): Animator {
    // 根据动画资源 ID 创建动画集合
    val animatorSet = AnimatorSet()
    // 从资源中加载动画
    val animators = loadAnimators(view.context, animResId)
    animatorSet.playTogether(animators)
    return animatorSet
}

// 从资源中加载动画
private fun loadAnimators(context: Context, animResId: Int): List<Animator> {
    // 从资源中解析动画 XML 文件
    val animators = mutableListOf<Animator>()
    val animXml = context.resources.getXml(animResId)
    try {
        val parser = XmlPullParserFactory.newInstance().newPullParser()
        parser.setInput(animXml, null)
        var eventType = parser.eventType
        while (eventType!= XmlPullParser.END_DOCUMENT) {
            if (eventType == XmlPullParser.START_TAG) {
                if (parser.name == "objectAnimator") {
                    // 解析 ObjectAnimator
                    val propertyName = parser.getAttributeValue(null, "propertyName")
                    val duration = parser.getAttributeValue(null, "duration").toLong()
                    val fromValue = parser.getAttributeValue(null, "fromValue").toFloat()
                    val toValue = parser.getAttributeValue(null, "toValue").toFloat()
                    val animator = ObjectAnimator.ofFloat(
                        null,
                        propertyName,
                        fromValue,
                        toValue
                    )
                    animator.duration = duration
                    animators.add(animator)
                }
            }
            eventType = parser.next()
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
    return animators
}

在上述源码中,executeEnterAnimationexecuteExitAnimation 方法分别用于执行进入和退出动画。createAnimator 方法根据动画资源 ID 创建动画集合,loadAnimators 方法从资源中解析动画 XML 文件并创建相应的 Animator 对象。

六、复杂路由切换动画的实现

6.1 组合动画

组合动画是将多个简单动画组合在一起,实现更加复杂的过渡效果。例如,同时进行淡入和缩放动画。

6.1.1 实现代码

kotlin

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.navigation.NavOptions
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController

@Composable
fun CombinedAnimationExample() {
    // 创建一个 NavController 实例
    val navController = rememberNavController()
    // 定义 NavHost,指定导航控制器和起始目的地
    NavHost(
        navController = navController,
        startDestination = "screen1"
    ) {
        // 定义路由 "screen1" 对应的可组合函数
        composable("screen1") {
            Screen1(navController)
        }
        // 定义路由 "screen2" 对应的可组合函数
        composable("screen2") {
            Screen2(navController)
        }
    }
}

@Composable
fun Screen1(navController: NavHostController) {
    Column(modifier = Modifier.fillMaxSize()) {
        Text("Screen 1")
        // 定义组合动画的 NavOptions
        val navOptions = NavOptions.Builder()
           .setEnterAnim(
                androidx.compose.animation.AnimatedVisibilityScope.AnimationSpec(
                    enter = fadeIn(animationSpec = tween(durationMillis = 500)) + scaleIn(
                        initialScale = 0.5f,
                        animationSpec = tween(durationMillis = 500)
                    )
                ).animationSpec
            )
           .setExitAnim(
                androidx.compose.animation.AnimatedVisibilityScope.AnimationSpec(
                    exit = fadeOut(animationSpec = tween(durationMillis = 500)) + scaleOut(
                        targetScale = 0.5f,
                        animationSpec = tween(durationMillis = 500)
                    )
                ).animationSpec
            )
           .setPopEnterAnim(
                androidx.compose.animation.AnimatedVisibilityScope.AnimationSpec(
                    enter = fadeIn(animationSpec = tween(durationMillis = 500)) + scaleIn(
                        initialScale = 0.5f,
                        animationSpec = tween(durationMillis = 500)
                    )
                ).animationSpec
            )
           .setPopExitAnim(
                androidx.compose.animation.AnimatedVisibilityScope.AnimationSpec(
                    exit = fadeOut(animationSpec = tween(durationMillis = 500)) + scaleOut(
                        targetScale = 0.5f,
                        animationSpec = tween(durationMillis = 500)
                    )
                ).animationSpec
            )
           .build()
        // 点击按钮跳转到 Screen2,并应用动画
        Button(onClick = { navController.navigate("screen2", navOptions) }) {
            Text("Go to Screen 2")
        }
    }
}

@Composable
fun Screen2(navController: NavHostController) {
    Column(modifier = Modifier.fillMaxSize()) {
        Text("Screen 2")
        // 点击按钮返回 Screen1,并应用动画
        Button(onClick = { navController.popBackStack() }) {
            Text("Go back to Screen 1")
        }
    }
}
6.1.2 源码分析

在上述代码中,我们通过 + 运算符将 fadeInscaleIn 动画组合在一起,实现了淡入和缩放的组合动画。同样,在退出动画中,将 fadeOutscaleOut 组合在一起。

6.2 自定义动画

除了使用系统提供的动画,我们还可以自定义动画。自定义动画可以实现更加独特的过渡效果。

6.2.1 实现代码

kotlin

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.navigation.NavOptions
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController

@Composable
fun CustomAnimationExample() {
    // 创建一个 NavController 实例
    val navController = rememberNavController()
    // 定义 NavHost,指定导航控制器和起始目的地
    NavHost(
        navController = navController,
        startDestination = "screen1"
    ) {
        // 定义路由 "screen1" 对应的可组合函数
        composable("screen1") {
            Screen1(navController)
        }
        // 定义路由 "screen2" 对应的可组合函数
        composable("screen2") {
            Screen2(navController)
        }
    }
}

@Composable
fun Screen1(navController: NavHostController) {
    Column(modifier = Modifier.fillMaxSize()) {
        Text("Screen 1")
        // 定义自定义动画的 NavOptions
        val navOptions = NavOptions.Builder()
           .setEnterAnim(
                androidx.compose.animation.AnimatedVisibilityScope.AnimationSpec(
                    enter = {
                        val alpha = remember { Animatable(0f) }
                        LaunchedEffect(Unit) {
                            alpha.animateTo(1f, animationSpec = tween(durationMillis = 500))
                        }
                        androidx.compose.animation.core.animateFloatAsState(
                            targetValue = alpha.value,
                            animationSpec = tween(durationMillis = 500)
                        ).value
                    }
                ).animationSpec
            )
           .setExitAnim(
                androidx.compose.animation.AnimatedVisibilityScope.AnimationSpec(
                    exit = {
                        val alpha = remember { Animatable(1f) }
                        LaunchedEffect(Unit) {
                            alpha.animateTo(0f, animationSpec = tween(durationMillis = 500))
                        }
                        androidx.compose.animation.core.animateFloatAsState(
                            targetValue = alpha.value,
                            animationSpec = tween(durationMillis = 500)
                        ).value
                    }
                ).animationSpec
            )
           .setPopEnterAnim(
                androidx.compose.animation.AnimatedVisibilityScope.AnimationSpec(
                    enter = {
                        val alpha = remember { Animatable(0f) }
                        LaunchedEffect(Unit) {
                            alpha.animateTo(1f, animationSpec = tween(durationMillis = 500))
                        }
                        androidx.compose.animation.core.animateFloatAsState(
                            targetValue = alpha.value,
                            animationSpec = tween(durationMillis = 500)
                        ).value
                    }
                ).animationSpec
            )
           .setPopExitAnim(
                androidx.compose.animation.AnimatedVisibilityScope.AnimationSpec(
                    exit = {
                        val alpha = remember { Animatable(1f) }
                        LaunchedEffect(Unit) {
                            alpha.animateTo(0f, animationSpec = tween(durationMillis = 500))
                        }
                        androidx.compose.animation.core.animateFloatAsState(
                            targetValue = alpha.value,
                            animationSpec = tween(durationMillis = 500)
                        ).value
                    }
                ).animationSpec
            )
           .build()
        // 点击按钮跳转到 Screen2,并应用动画
        Button(onClick = { navController.navigate("screen2", navOptions) }) {
            Text("Go to Screen 2")
        }
    }
}

@Composable
fun Screen2(navController: NavHostController) {
    Column(modifier = Modifier.fillMaxSize()) {
        Text("Screen 2")
        // 点击按钮返回 Screen1,并应用动画
        Button(onClick = { navController.popBackStack() }) {
            Text("Go back to Screen 1")
        }
    }
}
6.2.2 源码分析

在上述代码中,我们使用 AnimatableLaunchedEffect 来创建自定义的淡入淡出动画。Animatable 用于管理动画的状态,LaunchedEffect 用于启动动画。通过自定义动画,我们可以根据需求实现各种独特的过渡效果。

七、路由切换动画的性能优化

7.1 减少动画资源的使用

过多的动画资源会增加应用的内存占用和加载时间。因此,我们应该尽量减少不必要的动画资源。例如,避免使用复杂的动画 XML 文件,优先使用代码实现简单的动画。

7.2 优化动画的帧率

低帧率的动画会给用户带来卡顿的感觉,影响用户体验。为了优化动画的帧率,我们可以采取以下措施:

  • 减少动画的复杂度:避免同时进行过多的动画操作,尽量简化动画效果。
  • 使用合适的动画时长:过长的动画时长会导致帧率下降,因此要根据实际情况选择合适的动画时长。

7.3 避免动画的重复执行

在某些情况下,动画可能会重复执行,导致性能问题。我们可以通过合理的逻辑判断来避免动画的重复执行。例如,在导航过程中,只有当路由发生真正的变化时才执行动画。