动画实现更简单,Navigation Compose 帮您忙

3,024 阅读8分钟

Jetpack Compose 将动画实现的门槛降低了——从 "如果有时间再慢慢打磨" 到 "动画实现很简单,没有理由不试试看了"。这里有个很大的课题是页面级的过渡动画,这也是 Navigation Compose 一直致力解决的问题,具体是满足下面三种场景:

  • 仅使用 Compose 1.0.0 中稳定的动画 API
  • 开始对 Compose 1.0.0 中存在的实验性动画 API 提供支持
  • 构建在 Compose 1.1.0 及更高版本中面向未来的动画 API (共享元素过渡)

每一种情况的实现方法都稍有不同,我们将在本文中介绍。

Compose 💚 动画

从首次发布 Jetpack Compose 0.1.0-dev01 到最新的 Compose 1.0.1,经历了漫长的过程。相对于 View 系统而言,它巨大的改进之一便是动画和过渡。在追求完美的动画 API 的过程中,对 Compose 进行了大量的修改才一步步迭代到 版本 1.0.0

虽然许多底层的动画 API,比如非常强大的 animateTo()animate*AsState() 到目前为止是 Compose 稳定的基础构成部分,但仍有许多基于这些代码构建的 API 被标记为 @ExperimentalAnimationApi

实验性 API 和语义化版本控制

实验性 API (任何在 Kotlin 领域使用 @RequiresOptIn 注解的 API) 可能随时会被更改。这意味着这些 API 可能在未来任一版本 (可能是 Compose 1.1.0-alpha04 或者 1.2.0-alpha08) 中被更改、优化或替换。因此,如果您使用了任何一个基于这些实验性 API 构建的库,当您更新了您使用的 Compose 版本但没有同时更新这些库的版本时,这些库可能会直接崩溃并构建失败。(如果您使用了早期发布的 Compose 版本,您就会知道这种痛苦。)

所有 AndroidX 库 (包括 Navigation 和 Compose),都遵循 严格的语义化版本控制,如 AndroidX 版本页面 所述。这意味着一旦某个库迭代至候选版本 (Release Candidate,即 RC),任何非实验性 API 将不会再被更改。对这些稳定的 API 进行破坏性变更需要增加主版本号 (如,'2.0')。

这对向前和向后兼容很友好。例如,您可以升级 Fragment 版本以尝试新的 alpha 内容,同时将其他依赖项保持在其稳定版本上,一切工作如常。

然而,这也意味着严格禁止实验性 API (即可以从您底层移除的 API) 跨越不同的库使用。例如,升级您的 androidx.fragment 版本不应该破坏 androidx.appcompat。这一规则同样应用于 androidx.navigation 和 androidx.compose.animation。

使 Navigation 2.4 稳定

Navigation 2.4 是一个重要的版本,它既是第一个 Navigation Compose 版本,也是第一个对 Navigation Compose 和带有 Fragment 的 Navigation 支持 多返回栈 的版本。这意味着我们正在整理剩余的相关 API 需求以准备通过测试版、RC 版和稳定版。

对于 Navigation Compose 而言,这意味着我们正基于 Compose 1.0.1 进行构建,并为那些想要 (或者已经) 开始依赖 Compose 1.1.0-alpha01 或更新版本的开发者提供向前兼容。

这种向前兼容性要求意味着 Navigation Compose 2.4.0 的任何代码只能依赖于稳定的 Compose 动画 API。这也是我们在 Navigation 2.4.0-alpha05 中增加交叉淡入淡出支持的方式——在 Compose 的世界中,您应该首先消除生硬的页面跳转。

这种仅使用稳定 Compose 动画 API 的限制意味着 Navigation 2.4 不能直接使用 AnimatedContent 之类的 API,您不能将它们直接作为 Navigation 2.4 的一部分来使用以实现那种丰富的动画控制。但是,Navigation 的可扩展性意味着底层框架已经被构建好了并且是可用的。

介绍: Accompanist 导航动画!

对于目的地之间动画切换的支持是我们能发布 Accompanist Navigation Animation 的原因,它基于最近发布的 Navigation 2.4.0-alpha06。导航动画库为您一直在使用的 Navigation Compose API 提供一套带动画的版本:

  • 使用 rememberAnimatedNavController() 替换 rememberNavController()

  • 使用 AnimatedNavHost 替换 NavHost

  • 使用 import com.google.accompanist.navigation.animation.navigation 替换 import androidx.navigation.compose.navigation

  • 使用 import com.google.accompanist.navigation.animation.composable 替换 import androidx.navigation.compose.composable

乍一看,您应用的外观没有发生改变——默认动画仍然是 fadeIn 和 fadeOut 类型,与 Navigation 2.4 中所提供的淡入淡出类型相同。然而,您将获得一项重要的新功能——能够配置这些动画并在页面之间替换您自己的过渡动画

每个 composable 目的地都有四个新参数可以设置:

  • enterTransition: 指定当您使用 navigate() 导航至该目的地时执行的动画。
  • exitTransition: 指定当您通过导航至另一个目的地的方式离开该目的地时执行的动画。
  • popEnterTransition: 指定当该目的地在经过调用 popBackStack() 后重新入场时执行的动画。默认为 enterTransition。
  • popExitTransition: 指定当该目的地在以弹出返回栈的方式离开屏幕时执行的动画。默认为 exitTransition

在每种情况下,这些参数都具有相同的格式:

enterTransition: (
   (
       initial: NavBackStackEntry,
       target: NavBackStackEntry
   ) -> EnterTransition?
)? = null,

每个参数都接收一个 lambda。该 lambda 有两个 NavBackStackEntry 类型的参数,分别表示您来自何处 (initial) 和您要去往何处 (target)。以 enterTransition 为例,将要进入的目的地为 target—— 也就是将要启用 enterTransition 的目的地。而 exitTransition 则相反: initial 为将要执行退出动画的目的地。

这使得您可以像这样编写目的地:

composable(
  "profile/{id}",
  enterTransition = { _, _ ->
    // 让我们写一个很长的淡入
    fadeIn(animationSpec = tween(2000)
  }
) {
  // 像往常一样添加内容
}

或者,根据您来自/去往何处来控制您的动画:

composable(
  "friendList"
  exitTransition = { _, target ->
    when (target.destination.route) {
      "profile/{id}" -> ExitTransition.fadeOut(
          animationSpec = tween(2000)
      ) // 慢慢地淡出
      else -> null // 使用默认值
    }
  }
) {
  // 像往常一样添加内容
}
composable(
  "profile/{id}",
  enterTransition = { initial, _ ->
    when (initial.destination.route) {
      "friendList" -> slideInVertically(
          initialOffsetY = { 1800 }
      ) // 滑入 profile 页面
      else -> null // 使用默认值
  }
) {
  // 像往常一样添加内容
}

在这里,friendList 页控制其退出到 profile 页的过渡动画,profile 页控制其从 friendList 页进入的过渡动画,并且允许在这两个目的地之间自定义滑动动画。同时,我们可以使用 null 表示 "使用默认值"。这些默认值依次来自父导航图、父导航图的父导航图,一直向上到根 AnimatedNavHost。这意味着想要设置默认动画 (例如,交叉淡入淡出的时机),只需要在您的 AnimatedNavHost 中修改全局的 enterTransitionexitTransition

如果您只想修改某个子图的默认值 (例如,您的登录子图中的页面总是使用横向滑动动画),您也可以在嵌套图级别设置动画:

navigation(
  startDestination = "ask_username"
  route = "login"
  enterTransition = { initial, _ ->
    // 检查上一个页面是否在登录子图中
    if (initial.destination.hierarchy.any { it.route == "login" }) {
      slideInHorizontally(initialOffsetX = { 1000 }
    } else
      null // 使用默认值
  }
  exitTransition = { _, target ->
    // 检查新的页面是否在登录子图中
    if (target.destination.hierarchy.any { it.route == "login" }) {
      slideOutHorizontally(targetOffsetX = { -1000 }
    } else
      null // 使用默认值
  }
  popEnterTransition = { initial, _ ->
    // 检查上一个页面是否在登录子图中
    if (initial.destination.hierarchy.any { it.route == "login" }) {
      // 请注意我们在 pop 操作时从相反的方向做动画
      slideInHorizontally(initialOffsetX = { -1000 }
    } else
      null // 使用默认值
  }
  popExitTransition = { _, target ->
    // 检查新的页面是否在登录子图中
    if (target.destination.hierarchy.any { it.route == "login" }) {
      // 请注意我们在 pop 操作时从相反的方向做动画
      slideOutHorizontally(targetOffsetX = { 1000 }
    } else
      null // 使用默认值
  }
) {
  composable("ask_username") {
    // 添加内容
  }
  composable("ask_password") {
    // 添加内容
  }
  composable("register") {
    // 添加内容
  }
}

请注意我们使用 hierarchy 扩展方法 来判断某个目的地是否属于登录子图的一部分——这样一来,我们进入登录子图和离开登录子图的过渡动画将使用默认值 (或者您在更高一级设置的任何过渡动画)。

每当您有一个方向性的过渡动画,比如水平滑动时,enterTransition 和 popEnterTransition 之间的区别就非常方便——您将能够避免造成一个页面向右滑动而另一个页面向左滑动的情况。

Accompanist 充当了 Jetpack 库的助推器,使得我们可以在 Compose 1.1 的开发过程中立即获得实验性功能。

添加 Accompanist 导航动画 依赖:

implementation "com.google.accompanist:accompanist-navigation-animation:0.16.0"

Navigation Compose 和动画的未来

随着基于 Compose 1.0.1 的 Navigation 2.4 和 Accompanist 导航动画库通过实验性 API 突破了 Compose 1.0 的限制,还有其他内容即将展现: Compose 1.1。通过 Compose 路线图 可以发现,有一个非常重要的、令人兴奋的功能即将推出:

支持共享元素过渡

我们对于 Navigation 2.5 的目标是将 Compose 1.1 的所有优点带到 Navigation Compose 中。这意味着当动画 API 解除实验性状态时,我们可以直接将其带到 Navigation Compose。这也意味着我们可以构建支持共享元素过渡的 API。

这还意味着 Accompanist 导航动画应该被视为一种临时措施: 一旦 Navigation Compose 自身提供了相同级别的动画 API (根据您的反馈量身定做),您将可以直接依赖于它并且可以完全移除 Accompanist 导航动画库。

继续前进

平衡稳定性以及我们作为 Jetpack 库对自己提出的向前和向后兼容性要求,并具有快速交付功能的能力,这并不像我们想象的那么简单。随着 Jetpack Compose 不断发展,对不断超前的需求而言,Accompanist 是一个巨大的福音。我要感谢 Chris Banes 和所有投入时间在 Accompanist 上的开发者、Compose 背后的整个团队,以及大家帮助塑造 Android 开发的未来。

备注 : 如果您正在寻找更多关于 Navigation+Accompanist 的资料,请查阅:

欢迎您 点击这里 向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!