前语
最近小鹅事务所新增了一些动画,包括转场动画、布局动画和交互动效。本文经过多个动效实战来介绍其间的动效规划思想和 动画实战。
我现已想写动效开发这个体裁很久了,但一直未能动笔。这主要是因为动效开发不只是是编写代码就能完结的,它与其背面的APP规划密不可分,因而需求做到知其然知其所以然。不然,制造出来的动效或许无法提升 APP 运用体会,甚至或许会遭到用户反感。
本文从简略到杂乱,介绍一下以 Compose 为编码完成(View 同理),以 After Effects、Figma、Lottie为动画规划的动效开发体系,并尽量削减陷入源码圈套。这篇文章尽管依据Android,可是也合适规划、产品观看,咱们开始吧!
特点动画
关于调整视图的透明度、尺寸、份额、方位等等特点来做动画我都统称为特点动画。
独自修正某项特点而完成的动画如上图所示,它的完成原理是在每一帧调整某些特点而完成的动画。
例如,在View体系中,能够运用ValueAnimator
等动画辅佐类来完成在每一帧对 View 进行特点设置改变,以到达每一帧展现不同的特点的作用。
AndroidView(
factory = { context ->
// 新建一个展现图片的 ImageView
ImageView(context).apply {
setImageResource(R.drawable.ic_little_goose)
}
}
) { imageView ->
// 新建一个动画辅佐类,设置在两秒内将动画数值从 1 慢慢变成 0 再变成1
val animator = ValueAnimator.ofFloat(1F, 0F, 1F).apply {
duration = 2000L
}.also { it.start() }
// 监听数值改变,在每一次改变都调整 ImageView 的透明度
animator.addUpdateListener { a ->
imageView.alpha = a.animatedValue as Float
}
}
以上代码完成的功用是:在2秒钟内的每一帧将一张图片的透明度从1不断变成0再变成1。如下所示:
在介绍 Compose 特点动画体系时需求解说一个概念,声明式函数。它和 View 的 UI 体系在开发上有本质的差异,它由数据驱动 UI 改变。和上面相同的比如,它放在Compose的写法就是这样:
val alpha = remember { Animatable(1F) }
Image(
painter = painterResource(id = R.drawable.ic_little_goose),
contentDescription = "little goose",
modifier = Modifier.alpha(alpha.value)
)
LaunchedEffect(Unit) {
// 将 alpha 以动画的方法从1F变成0F再变成1F
alpha.animateTo(0F, tween(2000, easing = LinearEasing))
alpha.animateTo(1F, tween(2000, easing = LinearEasing))
}
代码中经过名为alpha的这个Animatable
内部value改变来驱动Compose函数的重组,从而导致透明度渐隐动画,如下所示:
绘图动画
绘图动画是经过对图形的制造进程进行动画处理,来展现出视觉上的改变。一般运用Canvas在每一帧制造不同的图形以到达动画作用。
在 View 体系中需求频繁调用invalidate()
,并在onDraw
中制造不同的图形,在Compose中需求频繁改变制造参数,内部会主动重组。
举一个制造圆形的比如:
@Composable
fun AnimatedCircle(modifier: Modifier = Modifier) {
val animatedSweepAngle = remember { Animatable(0f) }
LaunchedEffect(Unit) {
animatedSweepAngle.animateTo(
targetValue = 360f,
animationSpec = tween(durationMillis = 2000)
)
}
Canvas(modifier) {
drawArc(
color = Color.Blue,
startAngle = 0f,
sweepAngle = animatedSweepAngle.value,
useCenter = false,
topLeft = Offset.Zero,
size = this.size
)
}
}
在这个比如中,咱们运用 Animatable
创立了一个能够进行动画处理的变量 animatedSweepAngle
。然后在 LaunchedEffect
中运用 animateTo
方法来操控这个变量的改变进程。最后在 Canvas
中运用 drawArc
方法来制造圆形。因为 animatedSweepAngle
变量的值在每一帧都会发生改变,圆形会在2秒内逐步制造完结,形成了绘图动画的作用。
除了制造圆形,Canvas 还能够制造其他的图形,比如矩形、直线、途径等等。只需求运用相应的制造函数即可。
例如能够制造以下动画,用作小鹅事务所的查找Icon动画:
结合下拉手势能够做出这样的下拉查找界面,图标的方位、透明度、巨细、动画进展跟随着滑动手势来走,而每一个动画的关键帧的时刻、方位都并不是从始至终的,它跟手的一起也有些兴趣。
它的代码有一百多行,我就不全放出来了,对代码感兴趣的能够拉小鹅事务所库房检查,只放一小部分:
@Composable
fun PullToSearchIcon(
modifier: Modifier = Modifier,
progress: Float,
color: Color = MaterialTheme.colorScheme.onSurface,
contentDescription: String?
) {
...
val drawCache = remember {
PullToSearchIconDrawCache(
cachePath = Path(),
cachePathMeasure = PathMeasure(),
cachePathToDraw = Path()
)
}
Canvas {
// 依据关键帧核算直线进展
val lineProgress = if (cacheCurrentState.state) {
if (progress < 0.16F) 0F else ((progress - 0.16F) / 0.72F).coerceAtMost(1F)
} else {
if (progress < 0.12F) 0F else ((progress - 0.12F) / 0.72F).coerceAtMost(1F)
}
// 依据关键帧核算箭头进展
val arrowProgress = 1F - if (cacheCurrentState.state) {
(progress / 0.36F).coerceAtMost(1F)
} else {
if (progress < 0.12F) 0F else {
((progress - 0.12F) / 0.36F).coerceAtMost(1F)
}
}
// 核算制造的坐标
val lineStartOffset = Offset(
width / 2 + (width * 0.375F - radius - radius * cos45) * lineProgress,
(height * 0.1675F - (0.045F * height * lineProgress)) +
(radius + radius * cos45) * lineProgress
)
val lineEndOffset = Offset(
width / 2 - lineProgress * width * 0.375F,
height * 0.875F
)
// 制造
drawLine(
color = color,
start = lineStartOffset,
end = lineEndOffset,
strokeWidth = strokeWidth,
cap = StrokeCap.Round
)
...
}
}
经过费劲地核算和不断地调试,咱们能够做出一个看起来还不错的“箭头 → 查找”动画,假如把握了绘图动画,咱们或许会想:
我是不是能够对线动效规划师了?
我只想说:别急。上图只是一个十分简略的动画,就需求写一百多行代码、花上几个小时调试才干做出来。甚至还有一个小问题:arrowProgress
,lineProgress
的动画曲线是依据progress核算出来的,也就是说没有自己的动画标准曲线,动画标准曲线能够参阅我之前的文章。在子progress核算的时分,沿用了父progress的曲线。假如需求每个progress都有自己的动画曲线,则需求进一步曲线映射。
要是你和规划师对线之后,规划师做出了比较杂乱的动画:
这个时分就简直没有办法经过手写代码的方法来完成了,尽管理论上也能够。
- 找到组件(每个制造内容)关键帧的时刻,并记录下来。
- 在对应关键帧时刻点调整组件的方位,制造相关组件。
- 动画标准曲线核算。
- 对每一个组件重复上面的步骤。
这种工作量就不是以小时为单位了,是以天为单位了!所以这种状况下只能对规划师投降认输。那么有没有办法能够削减这种工作量呢?是有的,Airbnb的Lottie、腾讯的PAG都便利了规划、开发对接。
Lottie
Lottie是由Airbnb创立的库,它答应规划师在Adobe After Effects(AE)中创立动画并将其导出为JSON文件。然后能够运用Lottie库在Android、iOS和Web运用程序中运用这些JSON文件。这使得规划师能够创立杂乱的动画,并且开发人员能够无需额外的编码就能将其无缝地集成到运用程序中。
关于AE怎样运用Lottie导出动效文件能够检查我之前的文章,里边介绍了怎样导出AVD文件,而需求导出Json文件只需求勾选Standard即可:
运用AE制造动画并运用Lottie插件导出需求留意一个工作,也是十分重要的工作:
- 尽量运用矢量图形制造动画
尽量削减AE的一些特效插件运用,像是粒子、FX之类的,在能运用矢量图形的地方就不要运用其他东西。不然导出的动效文件或许会包括一个文件夹,里边包括了许多帧的位图,众所周知,这东西很占用内存和增大包体积。
刚刚的动画导出的Json文件如图所示,一个18kb的文件,咱们将它放在raw资源文件夹:
那么该如何运用呢?以下以Compose代码为例,只需求只是几行代码即可完成。
当规划师无可奈何运用了一些特效制造动图,例如发光等等,这个时分的文件比较大。咱们能够经过服务器下发到手机,再展现动画也能够,这样也能够削减包体积,可是内存占用是不可避免了。
优化内存和包体积并不只是是开发人员的责任,规划师也能够参加其间。在咱们的项目中,drawable 文件夹中或许包括一些 png 格局的图片资源。假如这些资源没有运用特效,而只是纯矢量图标,那么能够让规划师将其替换为 SVG 矢量图。
并且在Figma中也能够运用LottieFiles制造动画并直接生成Lottie动画文件,能够生成Json格局和文件巨细更小的dotLottie格局,详情能够见我之前编写的Lottie生成动效。
动画规划思想:契合直觉
因为自己并不是专业动画规划师,关于规划思想只是也是个人见解,仅供参阅。
一个动效是否优异的评判标准有许多,我觉得十分重要的是:这个动效需求契合直觉。我举一个小比如:
在如上动效中,我觉得稀土是不太契合直觉的,头像从下方进入AppTopBar里边了,可是它却从上往下出来了,略微给人一种违和感。可是它又是移动了一大段间隔才降下来,削减了违和感,规划师应该也花了心思在里边。小红薯则是正派地从下往上呈现,契合直觉。而这类型的动效我反而比较喜爱QQ音乐的,它的AppTopBar渐现的一起标题内容渐隐,在滑动转场的进程中给人一种顺滑的感觉。
当然,动效规划并没有高低之分。每位规划师和开发人员都希望寻求最优解,但受制于代码解耦、完成难度、无障碍等多种因素,也就是说动效开发这项工作是需求取舍的。
以下举几个小鹅事务所的比如来说明什么是契合直觉:
我在记账页面中给当时Icon做了一个切换动画,运用的是AnimatedContent
,它的作用如下所示:
在点击上方新的Icon的时分需求切换当时Icon,因为动画需求契合直觉,切换动画应该从手指的方向弹到目标方位,即从上往下,为了更舒畅的过渡,我加上了渐隐和渐现。代码如下所示:
AnimatedContent(
targetState = iconAndContent,
transitionSpec = {
val inDurationMillis = 180
val outDurationMillis = 160
fadeIn(
animationSpec = tween(
durationMillis = inDurationMillis,
delayMillis = 36,
easing = LinearOutSlowInEasing
)
) + slideIntoContainer(
towards = AnimatedContentScope.SlideDirection.Down,
animationSpec = tween(
durationMillis = inDurationMillis,
delayMillis = 36,
easing = LinearOutSlowInEasing
),
initialOffset = { it / 2 }
) with fadeOut(
animationSpec = tween(outDurationMillis, easing = LinearOutSlowInEasing)
) + slideOutOfContainer(
towards = AnimatedContentScope.SlideDirection.Down,
animationSpec = tween(outDurationMillis, easing = LinearOutSlowInEasing),
targetOffset = { it / 2 }
)
},
label = "transaction content item"
) { iac ->
// Icon and Text
...
}
在完成这个动画的时分,有两个细节需求留意,第一个是缓动曲线,第二个是delayMillis。
动画标准缓动曲线
缓动曲线我挑选了一个带有初始速度的LinearOutSlowInEasing
曲线,它是Compose库预制的几个曲线之一。
val LinearOutSlowInEasing: Easing = CubicBezierEasing(0.0f, 0.0f, 0.2f, 1.0f)
这里需求介绍一下CubicBezierEasing
,这是一个以两个端点(0,0)(1, 1)组成的贝塞尔曲线。
横轴为动画时刻、竖轴为动画进展,咱们能够操控的是它的两个把手来制造一个贝塞尔曲线作为动画曲线,四个参数分别为第一个把手的xy轴,第二个把手的xy轴。上图的曲线能够运用CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f)
来完成,这个也是官方预制的的FastOutLinearInEasing
。
官方预制的常用的曲线如下所示,咱们能够很容易得知这条曲线的切线为动画的速度:
其间中心的曲线为咱们目前运用的曲线LinearOutSlowInEasing
,它是一条带有初始速度并缓慢减速到结束的曲线。咱们的目标是制造一个从视线外弹到视线内的动画,是一个十分迅速的动画,给它一个初始速度是契合直觉的,因而LinearOutSlowInEasing
完美契合了我的需求。
动画标准选型
这里需求刺进一个动画标准的选型参阅,动画的其间一个作用是招引用户留意,而一条舒畅的缓动曲线还有一个十分重要的作用,便利用户视觉盯梢,运用这个曲线的图标带有速度进来,招引到了用户留意,并且有一个减速的流程,因而用户在看到这个动画的时分能够快速追踪到这个图标停下来。
用户潜意识会觉得是自己的动态视力优异暗暗自喜。
而一个现已在视图中的内容需求动起来,最好给它一个缓动的曲线,由停止慢慢加快,在快要结束的时分减速停下来,例如上方的第一个曲线,便利用户盯梢的一起也契合直觉。因为自然界的东西在移动的时分都是从停止加快再减速到停止。
如何规划一个舒畅的动画标准曲线其实是一件很重要的工作。在Compose中完成一个好用的动画标准曲线是一件轻而易举的工作,假如项目答应运用Compose的话,优化动效体会应该提上日程了。
delayMillis
即进入动画标准中有一个 delayMillis,我将其设置为 36ms。为什么要推迟呢?在切换动画时,你需求等旧的走出去,新的才干进来,这是一个契合直觉的标准,需求让后进来的图标推迟一会。
假如进入和退出两个转场动画一起进行,会有一种十分“抢”的感觉,就好像进来的图标插队想要挤出旧的相同。
在官方默许的动画标准中也配有90ms的推迟,这一个理念是得到官方认可的。
关键帧
上方设置了delay其实也是关键帧的一种完成,一个完好的动画或许由十分多的子动画组成,未必所有子动画都从头到尾履行,例如上方的下拉查找Icon动画就包括了关键帧核算,在不同的时刻做不同的工作。在箭头变成查找的时分,箭头先回收,棍子再移动,然后再制造圆圈,这三个动画交替并行。
下面介绍一下小鹅事务所中的纪念日模块中的动画,它如下所示:
它的动画全体由两步组成:
- 纪念日内容往右移动将日期躲藏掉
- 日期再从纪念日打开出来
能够看到第二步会比第一步慢半拍,为什么要这么做?我计划给人一种日期从右边藏起来再从下边跑出来的风趣感觉。一旦两步一起履行的话会给人一种违和感:我能够在某个时刻点看到两个日期,动画也失去了之前风趣的感觉。
在Compose中关于这种动画的完成是十分简略的工作,咱们能够用到updateTransition
这个API。
val transition = updateTransition(
targetState = isExpended, label = "memorial expend animation"
)
从transition
身上能够延伸出大量的子动画参数,transition
作为触发器能够触发子动画的履行。
val contentHeight by transition.animateDp(
transitionSpec = {
tween(
durationMillis = TOTAL_ANIM_MILLIS,
delayMillis = if (targetState) DELAY_ANIM_MILLIS else 0,
easing = FastOutSlowInEasing
)
},
label = "content height"
) {
if (it) 180.dp else 52.dp
}
val titleBackgroundColor by transition.animateColor(
transitionSpec = {
tween(
durationMillis = TOTAL_ANIM_MILLIS,
delayMillis = if (targetState) 0 else DELAY_ANIM_MILLIS,
easing = FastOutSlowInEasing
)
},
label = "background color"
) {
if (it) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.surfaceVariant
}
val boxWidth by transition.animateDp(
transitionSpec = {
tween(durationMillis = TOTAL_ANIM_MILLIS, easing = FastOutSlowInEasing)
},
label = "box width"
) {
if (it) 0.dp else 64.dp
}
并在对应的内容运用这些参数即可,例如:
val contentHeight by transition.animateDp(
transitionSpec = {
tween(
durationMillis = TOTAL_ANIM_MILLIS,
delayMillis = if (targetState) DELAY_ANIM_MILLIS else 0,
easing = FastOutSlowInEasing
)
},
label = "content height"
) {
if (it) 180.dp else 52.dp
}
Column(
modifier = Modifier.height(contentHeight)
) {
// ...
}
咱们或许会留意到我一直故意地传入label参数,其实它便利在Android Studio中用作动画调试,在咱们对Compose带有动画的函数进行Preview之后,咱们能够在Preview的视图中看到动画调试器:
它是一个十分有用的功用,点击该按钮能够呈现一个动画调试器,它能够显现咱们设置的label
值,便利咱们在编写动画的时分进行调试,Color有四行的原因是其实内部会独自对RGBA四个值进行变换。
这个动画比之前的动画要稍长一些,因为这个涉及到了比较大的内容。而一般内容越大,幅度越大的动画时长就越长,可是也不能过长。动画的时长一般在100 – 300ms左右。
最好不要小于100,不然该动画让人应接不暇。普通的动画建议不要超越300ms,例如上方动画的时长为266ms,全屏的界面过渡作用则能够略微超越300ms,可是不要超越500ms,不然用户等待时刻过长,影响到用户体会了。
“约好俗称”
在动画规划上假如没有头绪也能够考虑遵循一下“约好俗成”准则,例如带有横杆的地方能够拖动就能够称之为规划的约好俗成:
不过有许多约好俗成其实让人摸不着头脑,因为用户现已习惯了,假如不依照约好来反而或许会让人觉得奇怪,例如Activity的跳转,以下APP在一个页面呈现了三种跳转动画。
咱们有没有想过为什么默许的页面跳转是从右往左进来的,底部的页面也要跟着从右往左移动一定的间隔?其实我没太搞理解,可是觉得依照默许的来完成才舒畅,让用户感觉到了解才干够削减用户的心思担负。
在Compose的Navigation结构中支持给页面跳转设置过渡动画,可是没有办法使底部页面变黑,所以我思来想去了很久,决定模仿Activity跳转的作用,加上一些新的想法来完成小鹅事务所的页面跳转动画。
既然页面的数据结构是栈,新的页面从右往左移进来。那么旧的页面可不能够缩小伪装压下去让个方位给新的页面呢?所以我写下了如下代码:
private fun AnimatedContentScope<NavBackStackEntry>.activityEnterTransition(): EnterTransition {
return slideIntoContainer(
towards = AnimatedContentScope.SlideDirection.Start,
animationSpec = tween(defaultDurationMillis),
initialOffset = { it }
)
}
private fun AnimatedContentScope<NavBackStackEntry>.activityExitTransition(): ExitTransition {
return scaleOut(animationSpec = tween(defaultDurationMillis), targetScale = 0.96F)
}
private fun AnimatedContentScope<NavBackStackEntry>.activityPopEnterTransition(): EnterTransition {
return scaleIn(animationSpec = tween(defaultDurationMillis), initialScale = 0.96F)
}
private fun AnimatedContentScope<NavBackStackEntry>.activityPopExitTransition(): ExitTransition {
return slideOutOfContainer(
towards = AnimatedContentScope.SlideDirection.End,
animationSpec = tween(defaultDurationMillis),
targetOffset = { it }
)
}
它的作用是这样的,感觉也挺舒畅的:
关于不同交互方法的跳转能够选用不同的进入方法,例如项目中做了一个下拉查找的功用,在下拉进入第二个页面的时分,页面从上往下是最契合直觉的,所以我在跳转到查找页面中运用了渐现+缓动的动画。
composable(
route = ...
enterTransition = {
fadeIn(
animationSpec = tween(200, easing = LinearOutSlowInEasing)
) + slideIntoContainer(
towards = AnimatedContentScope.SlideDirection.Down,
animationSpec = tween(200, easing = LinearOutSlowInEasing),
initialOffset = { it / 6 }
)
},
exitTransition = null,
popExitTransition = {
fadeOut(
animationSpec = tween(200, easing = FastOutLinearInEasing)
) + slideOutOfContainer(
towards = AnimatedContentScope.SlideDirection.Up,
animationSpec = tween(200, easing = FastOutLinearInEasing),
targetOffset = { it / 6 }
)
},
popEnterTransition = null
) { }
在动画曲线上我挑选了在进来的时分带有初始速度并慢慢停下的曲线,即刚刚介绍的LinearOutSlowInEasing
,详细原因刚刚也解说过了。在脱离的时分我挑选了到结束也不会减速的曲线,给人一种页面脱离不牵丝攀藤的感觉。
至于开始和目标间隔我都对它除以6,上下滑动进入界面的过渡尽量避免完好地从屏幕边缘进来的,假如继续时刻少了,会给人一种很忽然的感觉。假如继续的时刻长了,则给人一种很磨蹭的感觉,影响用户体会。有一种状况破例:新页面和旧页面在视觉上有交互。举个比如,在音乐软件中一般会在下边放一个音乐播映bar,点击跳转到完好的播映页面,这种状况下的新页面就能够完好地从底部出来,并且不会有违和感。
spring动效标准
尽管官方大部分动画API默许的动效标准是模仿实在的运动,运用了spring
,默许带有阻力且无弹性。
package androidx.compose.animation.core
private val defaultAnimation = spring<Float>()
可是这个API最好慎用,它模仿了绷簧实在的运动和阻力,也就是说它存在惯性和弹性,一旦随意修正参数把控不住,做出来的动画弹来弹去十分影响视觉体会,咱们的APP一般是扁平的,假如呈现弹跳的动画是一件跳脱的工作,咱们的动画最好做到招引留意而不抢眼。
可是,这并不是一件绝对的工作,在一些需求强提示或者需求给用户更强的反应的场景,就能够运用弹跳动效标准来增加“惊喜感”。
小鹅事务地点查找模块中运用了弹跳动画标准,查找本就是一件用户输入并寻求反应的进程。
在这个场景运用弹跳并不会觉得跳脱,动画本身也是为事务服务的工具,动效规划应该依据不同场景运用不同的动画标准来进一步加强视觉作用和进步反应作用。它的代码如下,运用了低弹性和中低的阻力,供咱们参阅。
spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessMediumLow
)
交互反应
在滑动、点击等等交互的时分应该要给予用户反应,一般选用动画的方法来反应,我发现许多APP并没有做点击反应,这点关于用户的体会不太好,经常让用户觉得是不是自己手机卡了没点上。
Material Design的按钮点击默许选用水波纹的方法来进行反应。而实际项目中咱们能够运用许多种反应方法:
- 圆角半径
- 色彩
- 透明度
- 份额
- 阴影
- …
便利的动效API
主动自适应布局巨细
它在Compose的API是Modifier::animateContentSize,它的作用是:父布局依据丈量的巨细运用动画主动调整本身的巨细和方位。它的实际作用是这样的:
它的好处是:简略,只需求经过一行代码即可完成巨细调整动画。它能够传入两个参数:其间一个是动画标准,另外一个是动画完结监听器。
fun Modifier.animateContentSize(
animationSpec: FiniteAnimationSpec<IntSize> = spring(),
finishedListener: ((initialValue: IntSize, targetValue: IntSize) -> Unit)? = null
): Modifier
因为其API十分简略,没有太大自定义空间,因而只能用于比较简略的场景。它有以下问题
- 内容消失忽然
- 水波纹无法填满布局
简略的规划也造成了这个问题没有很好的办法去做调整。
主动适应软键盘高度
在Android API 30以上能够在软键盘出来或躲藏的时分监听软键盘高度,我曾经运用这个特性完成了一个比较优雅的输入DialogFragment,TODO放链接,并且适配了不同的Android API,而在Compose中只需求传入一个Modifier
即可完成该功用,Modifier.windowInsetsPadding(WindowInsets.ime)
。
也能够配合BottomAppBar运用完成如下作用:
BottomAppBar(
modifier = modifier.fillMaxWidth(),
windowInsets = if (WindowInsets.isImeVisible) {
WindowInsets.ime.union(BottomAppBarDefaults.windowInsets)
} else {
BottomAppBarDefaults.windowInsets
},
...
)
是否运用动态监听软键盘高度的API其实在视觉体会上距离不大,该功用合适追求更加完美的开发(和不在意性能损耗的运用)。
打破规矩
在上面我介绍了许多动画规矩,它不该成为约束你思想的桎梏,发挥你的想象力,打破以上规矩,你能够制造更优异的动效,更舒适的交互!以下放一些我很喜爱的动效事例。
结束
祝贺咱们入门Android动效规划,动效规划是一门很大的学识,咱们有兴趣的话能够多探究!欢迎分享给其他开发、规划小伙伴一起观看。
相关项目
小鹅事务所:github.com/MReP1/Littl…
动效Demo:github.com/MReP1/Anima…
参阅
- www.youtube.com/watch?v=nV0…
- m3.material.io/styles/moti…
- lottiefiles.com/
- 动效参阅APP列表
- 小红书
- 米家
- 小宇宙
- 稀土
- MIGI
- 小米时钟
- Gmail