降Compose十八掌之『震惊百里』| Animations

公众号「稀有猿诉」        原文链接 降Compose十八掌之『震惊百里』| Animations

动画对于UI来说无疑是最重要的核心功能,它能够让UI变得生动有吸引力。适当的使用动画可以提升UI的流畅性,让UI体验更为顺滑。在Jetpack Compose中有丰富的函数可以用来实现动画,今天就从一些最为常用的学起,闲话就说这么多,赶紧开工。

banner

所见即所得的动画函数

最为方便和快速上手的就是使用封装的最好的动画函数(Animation Composables)。

给Composable出现和隐藏加上动画

UI元素的出现和隐藏是动画最为常用的场景,让视觉体验平滑过度,不那么的突兀。使用函数AnimatedVisibility就可以方便的给单个部件的出现和隐藏加上动画:

        var visible by remember {mutableStateOf(true)}AnimatedVisibility(visible) {Box(modifier = Modifier.size(200.dp).clip(RoundedCornerShape(8.dp)).background(colorGreen))        }Button(modifier = Modifier.align(Alignment.BottomCenter), onClick = {visible = !visible}) {Text("Toggle Show/Hide")}

anim_visibility

**注意:**AnimatedVisibility做完淡出动画时,会把其子布局从渲染树中移除。

AnimatedVisibility默认会使用淡入(fadeIn)/淡出(fadeOut)+缩放(shrinking)作为内容的出现/隐藏动画,如果要指定不同的动画,可以通过参数enter和参数exit来指定。EnterTransition和ExitTransition有很多预定义的动画可以使用,并且可以通过+进行组合:

var visible by remember { mutableStateOf(true) }
val density = LocalDensity.current
AnimatedVisibility(visible = visible,enter = slideInVertically {// Slide in from 40 dp from the top.with(density) { -40.dp.roundToPx() }} + expandVertically(// Expand from the top.expandFrom = Alignment.Top) + fadeIn(// Fade in with the initial alpha of 0.3f.initialAlpha = 0.3f),exit = slideOutVertically() + shrinkVertically() + fadeOut()
) {Text("降Compose十八掌", Modifier.fillMaxWidth().height(200.dp))
}

仔细看AnimatedVisibility的实现,不难发现,它其实相当于是一个Column,可以当成一个Column来使用,动画是加在此布局上面的。它还支持对其子布局设置单独的动画。可以使用这个扩展函数animateEnterExit来对子布局的出现/隐藏加上特定的动画:

@Composable
fun CustomForChildren(modifier: Modifier = Modifier.fillMaxSize()) {Column(horizontalAlignment = Alignment.CenterHorizontally) {var visible by remember { mutableStateOf(true) }AnimatedVisibility(visible = visible,enter = fadeIn(),exit = fadeOut()) {// Fade in/out the background and the foreground.Box(Modifier.fillMaxWidth().height(200.dp).background(Color.LightGray)) {Box(Modifier.align(Alignment.Center).animateEnterExit(// Slide in/out the inner box.enter = slideInHorizontally(),exit = slideOutHorizontally()).sizeIn(minWidth = 256.dp, minHeight = 64.dp).background(Color.Magenta)) {Text(text = "你会看到不同的风景!",style = MaterialTheme.typography.headlineLarge,modifier = Modifier.padding(16.dp).align(Alignment.Center))}}}Button(onClick = { visible = !visible },modifier = Modifier.padding(16.dp)) {Text("点击有惊喜!!!")}}
}

custom_for_children.gif

两个布局之间淡入淡出

AnimatedVisibility只能用于单个部件或者单个布局的出现隐藏。但有时会涉及两个部件之间的切换,虽然也是一个出现,前一个隐藏,但它们是有联动的,这时就需要使用专门的切换动画函数Crossfade,最为典型的场景就是加载内容,先是显示加载进度,有数据可显示时就把进度隐藏,让内容显示:

@Composable
fun CrossfadeDemo(modifier: Modifier = Modifier.fillMaxSize()) {var done by remember { mutableStateOf(false) }LaunchedEffect(Unit) {delay(5000)done = true}Crossfade(modifier = modifier,targetState = !done,label = "crossfade") { loading ->Box(modifier = Modifier.fillMaxSize(),contentAlignment = if (loading) Alignment.Center else Alignment.TopStart) {if (loading) {Column(horizontalAlignment = Alignment.CenterHorizontally) {CircularProgressIndicator(Modifier.size(66.dp))Text(text = "玩命加载中...",modifier = Modifier.padding(16.dp),style = MaterialTheme.typography.headlineMedium)}} else {Text(text ="""“降龙十八掌可说是【武学中的巅峰绝诣】,当真是无坚不摧、无固不破。虽招数有限,但每一招均具绝大威力。北宋年间,丐帮帮主萧峰以此邀斗天下英雄,极少有人能挡得他三招两式,气盖当世,群豪束手。当时共有“降龙廿八掌”,后经萧峰及他义弟虚竹子删繁就简,取精用宏,改为降龙十八掌,掌力更厚。这掌法传到洪七公手上,在华山绝顶与王重阳、黄药师等人论剑时施展出来,王重阳等尽皆称道。”""".trimIndent(),modifier = Modifier.padding(16.dp),style = MaterialTheme.typography.headlineMedium)}}}
}

crossfade.gif

通用的布局切换动画

如果变幻不止有淡入淡出,或者说布局不只有两个,这时就要用更为通用也更为强大的切换动画函数AnimatedContent,它可以用来定制多个布局之间两两切换的动画:

var state by remember {mutableStateOf(UiState.Loading)
}
AnimatedContent(state,transitionSpec = {fadeIn(animationSpec = tween(3000)) togetherWith fadeOut(animationSpec = tween(3000))},modifier = Modifier.clickable(interactionSource = remember { MutableInteractionSource() },indication = null) {state = when (state) {UiState.Loading -> UiState.LoadedUiState.Loaded -> UiState.ErrorUiState.Error -> UiState.Loading}},label = "Animated Content"
) { targetState ->when (targetState) {UiState.Loading -> {LoadingScreen()}UiState.Loaded -> {LoadedScreen()}UiState.Error -> {ErrorScreen()}}
}

animated_content

尺寸改变动画

UI元素的尺寸变化也是非常常用的一类动画,通常作为出场和入场比较合适。对于尺寸的改变可以通过Modifier.animateContentSize来实现。它会自己感知尺寸的变化,然后触发动画。可以设置一个参数finishedListener以接收动画做完了的通知。需要特别注意的是,调用的顺序很重要,animateContentSize必须在任何的尺寸设置之前,还要注意的是尺寸必须根据不同的条件有所变化,要不然动画没机会展示:

var expanded by remember { mutableStateOf(false) }
Box(modifier = Modifier.background(colorBlue).animateContentSize().height(if (expanded) 400.dp else 200.dp).fillMaxWidth().clickable(interactionSource = remember { MutableInteractionSource() },indication = null) {expanded = !expanded})

属性状态驱动动画

一般来说,动画的本质是让参数随时间变化,然后再让UI元素响应这些参数的变化,通过重新渲染,或者做渲染图层的变幻。在Compose中参数变化想影响部件的渲染,就必须把其封装成状态(State),这样参数变化就能被Compose感知到并做Recomposition。然后我们把状态的变化再通过属性设置给Composables,让其做渲染或者变幻,就形成了动画效果。这就是属性动画。

Compose定义了很多方法可以把参数转变成为状态,不同的参数可以通过不同的函数作用于不同的属性:

animateFloatAsState

可以把值的类型为浮点数的属性变为状态驱动动画,比如透明度(alpha),尺寸(size),间隔(padding/offset),字体大小(textSize)以及像旋转/缩放/位移等等。只要是浮点类型就可以用这个来转成状态,然后再通过相应的属性设置给Composable即可。

最为常用的就是结合graphicsLayer图层做变幻:

@Composable
fun PropertyAnimation(modifier: Modifier = Modifier.fillMaxSize()) {var showing by remember { mutableStateOf(false) }val scale by animateFloatAsState(targetValue = if (showing) 0f else 1f,label = "property")val alpha by animateFloatAsState(targetValue = if (showing) 0f else 1f,label = "property")Column(modifier = Modifier.padding(20.dp),horizontalAlignment = Alignment.CenterHorizontally,) {Text(text = "降Compose十八掌",modifier = Modifier.padding(20.dp).graphicsLayer {this.alpha = alphascaleX = scalescaleY = scale},style = MaterialTheme.typography.headlineLarge)Spacer(Modifier.height(50.dp))Button(onClick = { showing = !showing}) {Text("再点一下试试!(试试就试试!)")}}
}

property.gif

至于其他的像尺寸和间隔,虽然也可以,但因为像padding和offset会直接用dp或者Offset作为值,有更为舒适的API可以直接用(尽管浮点值也可以转换成为dp或者Offset)。

animateColorAsState

专门用于颜色值变化,指定两个值后,会在它们中间进行的插值作为动画的帧,能让颜色变化更为平滑和细腻。

animateIntOffsetAsState

用于把值Offset的变化变成动画,适合于使用Offset的地方,如Modifier.offset,Modifier.layout等。

animateDpAsState

把类型为Dp的值变为动画,适合所有能使用Dp作为参数值的地方,如padding,shadowElavation等。

小结:可以发现由属性状态驱动的动画使用起来比较麻烦,先是要把参数转化为状态,要管理好不同状态下参数的值,还要使用正确的函数把状态作用于Composable的属性。复杂的同时意味着强大,它能实现一些更为复杂的动画。推荐优先使用动画函数,如果无法满足再考虑用属性状态动画。

页面切换转场动画

页面是应用中较为完整的一屏UI,比如说新闻应用,列表页是一个页面,点开进入单篇新闻又是一个页面,用户中心是一个页面,设置又是一个页面。不同的页面之间的跳转称之为导航,用的是Jetpack中的库navigation,在Compose中通过navigation-compose做了桥接,所以在Compose中可以直接使用navigation,可以通过创建NavHost时通过参数enterTransition和exitTransition来为页面设置转场动画。可以为每个页面设置单独的转场,也可以设置一个统一的默认的转场动画:

val navController = rememberNavController()
NavHost(navController = navController, startDestination = "landing",enterTransition = { EnterTransition.None },exitTransition = { ExitTransition.None }
) {composable("landing") {ScreenLanding(// ...)}composable("detail/{photoUrl}",arguments = listOf(navArgument("photoUrl") { type = NavType.StringType }),enterTransition = {fadeIn(animationSpec = tween(300, easing = LinearEasing)) + slideIntoContainer(animationSpec = tween(300, easing = EaseIn),towards = AnimatedContentTransitionScope.SlideDirection.Start)},exitTransition = {fadeOut(animationSpec = tween(300, easing = LinearEasing)) + slideOutOfContainer(animationSpec = tween(300, easing = EaseOut),towards = AnimatedContentTransitionScope.SlideDirection.End)}) { backStackEntry ->ScreenDetails(// ...)}
}

navigation_transition

组合首次运行时动画

对于像AnimatedVisibility以及使用animate&42;AsState属性状态动画来说,可以发现它们都是在组合发生之后才能生效,这是因为状态是为了重组而设置的,状态只会在组合之后部件渲染完了,响应事件由事件触发状态变化。组合首次运行的时候,状态仅是初始值,但不会变化,也就不会触发动画。

所以需要一个能在首次组合时就能运行的事件来触发动画依赖的状态,LaunchedEffect正合适。LaunchedEffect会在首次组合时运行,可以在里面执行一些「副作用」也就是Compose组合之外的行为。可以在这里触发动画的状态,就能够让动画在首次组合时生效了。这一般用作部件的出场动画:

@Composable
fun LaunchAnimation(modifier: Modifier = Modifier.fillMaxSize()) {val alphaAnimation = remember {Animatable(0f)}LaunchedEffect(Unit) {alphaAnimation.animateTo(targetValue = 1f,animationSpec = tween(durationMillis = 30000))}Box(modifier = Modifier.offset(16.dp, 16.dp).size(200.dp).graphicsLayer {alpha = alphaAnimation.value}.background(Color.Magenta))
}

launch_anim.gif

需要注意,LaunchedEffect的参数用作标识,参数有变化时,会再次运行,因此对于出场动画,LaunchedEffect的参数要设置为不可变的常量,如Unit。还需要注意的是,LaunchedEffect会在首次组合时运行,对于像集合性布局,会重复的使用子布局来展示元素项,所以每次元素项进入屏幕可视范围时,LaunchedEffect都会运行,动画都会触发,这并不是想要的结果,因为我们只想列表首次加载时触发动画。一个解决办法就是把状态和LaunchedEffect提高到列表的上一级Composable中。

调整动画参数进行定制

动画除了具体的形式以外,还有一些共性的参数可以设置,像时长,速度和是否重复,有过View动画经验的同学对此一定不会陌生。可以通过AnimationSpec对象来对动画参数进行定制,所有的动画API都能接受一个animationSpec参数。Compose提供了很多AnimationSpec的构造函数可以直接使用:

  • spring 刚性动画(或者叫做弹性动画)是模拟物理中刚性物体运动和碰撞的动画,与生活中的体验类似,所以这是默认的动画参数。可以通过调整硬度(stiffness)和阻尼系数(dampingRatio)来改变动画效果。
  • tween 补间动画,有一定时长,在两个值之间通过Easing函数插值形成的动画。
  • keyframes 关键帧动画,指定一定的关键节点作为动画中的帧。
  • repeatable 可重复一定次数的动画,通过RepeatMode指定重复方式(简单重复,或者反向播放)。
  • infiniteRepeatable 无限重复动画,通过RepeatMode指定重复方式。
  • snap 猛跳到目标值,无动画。

根据不同的动画参数,可以进一步的做更细致的参数调整,比如像时长和速度。

修改动画的时长和延时

动画肯定都有时长,即使无限重复的动画,其每一次也是有时长的。大部分AnimationSpec函数都能接受一个durationMillis参数来调整动画的时长。

动画被触发后,也不一定立马播放,参数本身有一个延时可以控制,大部分都能接受一个delayMilis参数来控制播放的延时。

注意,刚性动画(spring)比较特殊,它不能直接控制时长和延时,刚性动画是通过硬度和阻尼来调整,时长是根据它们计算出来的,而物理世界的刚硬碰撞哪有延时?

修改动画的播放速度

对于补间动画和关键帧动画,还可以通过Easing函数来改变动画的播放速度,比如匀速,先快后慢,先慢后快,匀速等等。Easing函数与View动画中的Interpolator是同样的东西。它是一个简单的数学函数,一个浮点数输入代表当前的时间点,一个浮点数输出代表动画应该到达的位置,取值都是0到1之间。可以理解为物理题,输入参数是时间t,输出则是位移s。

有很多定义好的Easing函数可以使用,当然也可以自定义:

val CustomEasing = Easing { fraction -> fraction * fraction }@Composable
fun EasingUsage() {val value by animateFloatAsState(targetValue = 1f,animationSpec = tween(durationMillis = 300,easing = CustomEasing))// ……
}

未完待续

动画是UI中比较复杂的话题,在Compose中更是如此,动画的本质是把参数转成状态随时间变化,状态再去驱动部件做渲染或者做变幻。本文总结了最常用的和封装层次较为高级的创建动画的方法,足以应付较为常见的动画需求场景。但涉及动画的内容还有很多,比如像与手势交互相关的动画,多个不同的部件联动的动画,以及像动画的性能调优等一些较复杂的话题,将在后续的文章中讲解。

References

  • Quick guide to Animations in Compose
  • Animation modifiers and composables
  • Animations In Jetpack Compose
  • Jetpack Compose Animation for Beginners: A Step-by-Step Guide
  • Jetpack Compose Animations
  • Customize animations

subscription

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

保护原创,请勿转载!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/47135.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

六西格玛设计:以客户为中心,驱动企业持续创新

在当今竞争激烈的市场环境中,企业要想脱颖而出,就必须在产品质量、服务效率和客户满意度上不断追求卓越。六西格玛设计(Six Sigma Design)作为一种高度规范化的管理方法,正逐步成为众多企业实现这一目标的重要工具。张…

NSSCTF中24网安培训day2中web题目【下】

[NISACTF 2022]easyssrf 这道题目考察的是php伪协议的知识点 首先利用file协议进行flag查找 file:///flag.php 接着我们用file协议继续查找fl4g file:///fl4g 接着我们访问此文件,得到php代码如下 这里存在着stristr的函数&#x…

Linux中的环境变量

一、基本概念 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。 如:我们在编写C/C代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功&#xff…

Cesium能做啥,加载哪些数据源,开源免费用商用吗?这里告诉你。

很多小伙伴对Cesium是什么,一知半解,本文是基础知识的扫盲,为大家分享cesium是什么、能做什么、默认数据是什么,为什么首先要进行数据加载,要加载哪些数据,希望通过这些带你入个门,欢迎点赞评论…

vue仿甘特图开发工程施工进度表

前言 本文是根据项目实际开发中一个需求开发的demo,仅用了elementUI,可当作独立组件使用,C V即用。 当然没考虑其他的扩展性和一些数据的校验,主要是提供一个处理思路,有需要的小伙伴可以直接复制;本demo的…

高职院校人工智能人才培养成果导向系统构建、实施要点与评量方法

一、引言 近年来,人工智能技术在全球范围内迅速发展,对各行各业产生了深远的影响。高职院校作为培养高技能人才的重要基地,肩负着培养人工智能领域专业人才的重任。为了适应社会对人工智能人才的需求,高职院校需要构建一套科学、…

【node-RED 4.0.2】连接 Oracle 数据库踩坑解决,使用模组:node-red-contrib-agur-connector

关于 Oracle Oracle 就好像一张吸满水的面巾纸,你稍一用力它就烂了。 PS:我更新了更好的模组的教程,这篇已经是旧款的教程,但是它仍旧包含了必要的配置环境变量等操作。 最新的模组教程:node-red-contrib-agur-connec…

AI时代:探索个人潜能的新视角

文章目录 Al时代的个人发展1 AI的高速发展意味着什么1.1 生产力大幅提升1.2 生产关系的改变1.3 产品范式1.4 产业革命1.5 Al的局限性1.5.1局限一:大模型的幻觉1.5.2 局限二:Token 2 个体如何应对这种改变?2.1 职场人2.2 K12家长2.3 大学生2.4 创业者 3 人工智能发展…

解决vue3中el-input在form表单按下回车刷新页面

问题:在input框中点击回车之后不是调用我写的回车事件,而是刷新页面 原因: 如果表单中只有一个input 框则按下回车会直接关闭表单 所以导致刷新页面 解决方法 : 再写一个input 表单 ,并设置style"display:none&…

SimMIM:一个类BERT的计算机视觉的预训练框架

1、前言 呃…好久没有写博客了,主要是最近时间比较少。今天来做一期视频博客的内容。本文主要讲SimMIM,它是一个将计算机视觉(图像)进行自监督训练的框架。 原论文:SimMIM:用于掩码图像建模的简单框架 (a…

解决虚拟机与主机ping不通,解决主机没有vmware网络

由于注册表文件缺失导致,使用这个工具 下载cclean 白嫖就行 https://www.ccleaner.com/ 是 点击修复就可以了

防火墙双机热备带宽管理综合实验

一、实验拓扑 二、实验要求 12,对现有网络进行改造升级,将当个防火墙组网改成双机热备的组网形式,做负载分担模式,游客区和DMZ区走FW3,生产区和办公区的流量走FW1 13,办公区上网用户限制流量不超过100M&am…

技术速递|Let’s Learn .NET Aspire – 开始您的云原生之旅!

作者:James Montemagno 排版:Alan Wang Let’s Learn .NET 是我们全球性的直播学习活动。在过去 3 年里,来自世界各地的开发人员与团队成员一起学习最新的 .NET 技术,并参加现场研讨会学习如何使用它!最重要的是&#…

Java IO中的 InputStreamReader 和 OutputStreamWriter

Java IO 的流,有三个分类的维度: 输入流 or 输出流节点流 or 处理流字节流 or 字符流 在Java IO库中,InputStreamReader和OutputStreamWriter是两个非常重要的类,它们作为字符流和字节流之间的桥梁。 这两个类使得开发者可以方…

整数或小数点后补0操作

效果展示: 整数情况: 小数情况: 小编这里是以微信小程序举例,代码通用可兼容vue等。 1.在utils文件下创建工具util.js文本 util.js页面: // 格式…

淘宝扭蛋机小程序:旋转惊喜,开启购物新篇章!

在追求创新与惊喜的购物时代,淘宝再次引领潮流,精心打造——淘宝扭蛋机小程序,为您的购物之旅增添一抹不同寻常的色彩。这不仅仅是一个购物工具,更是一个充满趣味、互动与惊喜的宝藏盒子,等待您来探索与发现。 【旋转…

通过Dockerfile构建镜像

案例一: 使用Dockerfile构建tomcat镜像 cd /opt mkdir tomcat cd tomcat/ 上传tomcat所需的依赖包 使用tar xf 解压三个压缩包vim Dockerfile FROM centos:7 LABEL function"tomcat image" author"tc" createtime"2024-07-16"ADD j…

【 香橙派 AIpro评测】烧系统运行部署LLMS大模型跑开源yolov5物体检测并体验Jupyter Lab AI 应用样例(新手入门)

文章目录 一、引言⭐1.1下载镜像烧系统⭐1.2开发板初始化系统配置远程登陆💖 远程ssh💖查看ubuntu桌面💖 远程向日葵 二、部署LLMS大模型&yolov5物体检测⭐2.1 快速启动LLMS大模型💖拉取代码💖下载mode数据&#x…

第九课:服务器发布(静态nat配置)

一个要用到静态NAT的场景,当内网有一台服务器server1,假如一个用户在外网,从外网访问内网怎么访问呢,无法访问,这是因为外网没办法直接访问内网,这时候需要给服务器做一个静态NAT。 静态NAT指的是给服务器…

gltf模型加载 与3d背景贴图

Poly Haveny 用于3d模型跟贴图下载资源 Sketchfab 里面有免费的模型 模型放到public里面 const loader new GLTFLoader()// 加载GLTF模型loader.load(/scene.gltf,(gltf) > {// 将加载的模型添加到场景中scene.add(gltf.scene)// 现在你可以开始渲染循环了let angle …