“我正在参加「初夏创意投稿大赛」详情请看:初夏创意投稿大赛”
引言
Compose 在动画方面下足了功夫,提供了种类丰富的 API。但也正由于 API 种类繁多容器苗,如果想一气儿学下来,可能会消化不良导致似懂非懂。结合例子学习是一个不错的方法,本文就带大家边学边做,通过高仿微博长按点赞的彩虹动画,学习和实践canvas翻译 Compose 动画的相关技巧。
原版:微博长按点赞 | 本文:携程网飞机票预订官网夏日主题 |
---|---|
代码地址: github.com/vitavivaspringboot面试题/An…
1. Compose 动画 API 概览
Compose 动画 API 在使用场景的维度canvas上海交通大学上大体分为两类携程:高级别 API 和低级别 API。就像编程语 言分android手机为高级语言和低级语言一样,这列高级低级指 API 的易用性:
-
高级别 API 主打开箱即用,适容器对桌面的压强怎么算用于一些 UI 元素的展现/退出/切换等常见场景,例如常见的
AnimatedVisibility
以及AnimatedCocanvas上海交通大学ntent
等,它们被设计成 Composspring面试题able 组件,可以在声明式布局携程电话中与其他组件融为一体。
//Text通过动画淡入
var editable by remember { mutableStateOf(true) }
AnimatedVisibility(visible = editable) {
Text(text = "Edit")
}
-
低级别 API 使用成本更高但是更加android是什么手机牌子灵活,可以更精canvas英语准地实现 UI 元素个别属性的动画,多个低级别动画还可以组合实现更复杂的动画效果。最常见的低级别
animateFloatAsState
系列了,它们也是 Composa携程网站官网ble 函数,可以容器苗参与 Composition 的组合过程。
//动画改变 Box 透明度
val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f)
Box(
Modifier.fillMaxSize()
.graphicsLayer(alpha = alpha)
.background(Color.Red)
)
处于上层的 API 由底层 API 支撑实现,TargetBasedAnimation
是开发者可直接使用的最低级 API。Animatable 也是一个springboot相对低级的 API,它是一个动画值的包装器,在容器的容积一定比它的体积小协程中完成状态值的变化,向上提供对 animate*AsState
的支撑。它与其他 API 不同,是一个普通类而非一个 Composable 函数,所以可以在 Composable 之外使用,因此更具灵活性。本例子的动画主要也是依靠它完成的。
// Animtable 包装了一个颜色状态值
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(ok) {
// animateTo 是个挂起函数,驱动状态之变化
color.animateTo(if (ok) Color.Green else Color.Gray)
}
Box(Modifier.fillMaxSize().background(color.value))
无论高级别 API 还是低级别 API ,它们都遵循状态android下载驱动的动画方式,即目标对象通过观察状态变化实现自身的动画。
2. 长按点赞动画分解
长按点赞的动画乍看之下非常复杂,但是稍加分解后,不难发现它也是由一些常见的动画形式组合而成,因此我们可携程电话以对其拆解后逐个实现:
- 彩虹canvas软件动画:全屏范围内不断扩散的spring面试题彩虹效果。可以通过半径不断扩大的圆形图案并依次叠加来实现
- 表情动画:从按压位置不断抛出的表情。可以进一步拆解为三个动画:透明度动画,旋转动画以及抛物线轨迹动画。
- 烟花动画:抛出的springboot面试题表情在消失时会有一个烟花炸裂的效果。其实就是围绕中心的八个圆容器对桌面的压强怎么算点逐渐消失的过程,圆点携程客服电话的颜色提取自表情本身。
传统视图动画可以作用在 View 上,通过动画改变其属性;也可以在 onDraw
中通过不断canvas平台重绘实现容器的容积一定比它的体积小逐帧的动画效果。Canvas Compose 也同样,我们可以在 Composable 中观察动画状态,通过重组实现动画效果(本质是改变 UI 组件的布局属性),也可以在 Canvas 中观察动画状态,只在重绘中实现动画(跳过组合)。这个例子的动画效果也需要通过 Canvas 的不断重绘来实现。
Compose 的 Canvas 也可以像 Compo协程sable 一样声明式的调用,基本写法如下:
Canvas {
...
drawRainbow(rainbowState) //绘制彩虹
...
drawEmoji(emojiState) //绘制表情
...
drawFlow(flowState) //绘制烟花
...
}
State 的变化会驱动 Canvas 会自动重绘,无需手动调用 invali容器是什么date
之类的方法。那么接下来针对彩虹、表情、烟花等各种动画的实现,我们的工作主要有两个:
- 状态管理:定义相关 State,并在在动画中驱动其变化,如前所述这android是什么手机牌子主要依靠 Animatable 实现。
- 内容绘制:通过 Canvas API 基于当前状态绘制图案
3. 彩虹动画
3.1 状态管理
对于彩虹动画,唯一的动画状态就是圆的半径,其值从 0F 过渡到 screensize,圆形面积铺满至整个屏幕。我们使用 Animatable
包装这个状态值,调用 aniandroid什么意思mateTo
方法可以Android驱动状态变化:
val raduis = Animatable(0f) //初始值 0f
radius.animateTo(
targetValue = screenSize, //目标值
animationSpec = tween(
durationMillis = duration, //动画时长
easing = FastOutSlowInEasing //动画衰减效果
)
)
animationS携程旅行app官方下载pspring翻译ec
用来指定动画规格,不同的动画规格决定了了状态值变化的节奏。Com容器对桌面的压强怎么算pose 中常用的创建动画规格的方法有以下几种,它们创建不同类型的动画规格,但都是 AnimationSpec
的子类:
- tween:创建补间动画canvas规格,补间动画是一个固定时长动画,比如上面例子中这样设canvas上海交通大学置时长 durat携程网上订票飞机ion,此外,tween 还能通过 easiing 指定动画衰减效果,后文详细介绍。
- spring: 弹跳动画:spring 可以创建基于物理特性的弹簧动画,它通过设置阻尼比实现符合物理规Android律的动画衰减,因此不需要也不能指定动画时长
- Keyframes:创建关键帧动画规格,关键帧动画可以逐帧设置当前动画的轨迹,后文会详细介绍。
AnimatedRainbow
要实现上面这样多个彩虹叠携程客服电话加的效果,我们还需有多个 Animtaandroid下载安装ble
同时运行,在 Canvas 中依次对它们进行绘制。绘制彩虹除了依靠 Animtable 的状态值,还有 Color 等其他信息,因此我们定义一个 AnimatedRainbow
类保存包括 Animtable 在内的绘制所需的的状态
class AnimatedRainbow(
//屏幕尺寸(宽边长边大的一方)
private val screenSize: Float,
//RainbowColors是彩虹的候选颜色
private val color: Brush = RainbowColors.random(),
//动画时长
private val duration: Int = 3000
) {
private val radius = Animatable(0f)
suspend fun startAnim() = radius.animateTo(
targetValue = screenSize * 1.6f, // 关于 1.6f 后文说明
animationSpec = tween(
durationMillis = duration,
easing = FastOutSlowInEasing
)
)
}
animatedRainbows 列表canvas软件
我们还需要一个集合来管理运行中的 An携程网官网imatedRainbow
。这里我们使canvas英语用 Compose 的 MutableStateList
作为集合容器android下载安装,Mut容器英文ableStateList
中的元素发生增减时,可以被观察到,而当我们观察到新的 AnimatedRainbow
被添加时,为它启动动画。关键代码如下:
//MutableStateList 保存 AnimatedRainbow
val animatedRainbows = mutableStateListOf<AnimatedRainbow>()
//长按屏幕时,向列表加入 AnimtaedRainbow, 意味着增加一个新的彩虹
animatedRainbows.add(
AnimatedRainbow(
screenHeightPx.coerceAtLeast(screenWidthPx),
RainbowColors.random()
)
)
我们使用 LaunchedEffect
+ snapshotFlow
观察 animatedRainbows 的变化,代码如下:
LaunchedEffect(Unit) {
//监听到新添加的 AnimatedRainbow
snapshotFlow { animatedRainbows.lastOrNull() }
.filterNotNull()
.collect {
launch {
//启动 AnimatedRainbow 动画
val result = it.startAnim()
//动画结束后,从列表移除,避免泄露
if (result.endReason == AnimationEndReason.Finished) {
animatedRainbows.remove(it)
}
}
}
}
LaunchedEffect
和 snapshotFlow
都是 Compose 处理副作用的 API,由于不是本文重点就不做深入介绍了,这里只需要知道 LaunchedEffect
是一个提供了执行副作用的协程环境,而 snapshotFlow
可以将 animatedRainbows
中的变化转化为 Flow 发射给下游。当通过 Flow 收集到新加入的 AnimtaedRainbow
时,调用 startAnim
启动动画,这里充分发挥了挂起函数的优势,同步等待动画执行完毕,从 animatedRainbows
中移除 AnimtaedRainbow
即可。
值得一提携程网飞机票预订官网的是,MutableStateList
的主要目的是在组合中观察列表的状态变化,本例子的动画不发生在组合中(只发生在重绘中),完全可以使canvas上海交通大学用普通的集合类型替代,这里使用 MutableStateList
有两个好处android是什么手机牌子:
- 可以响应式地观察列表变化
- 在 LaunchEffect 中响应变化并启动动画,协程可以随当前 Comp携程网上订票火车票osable 的生androidstudio安装教程命周期结束而终止,避免泄露。
3.2 内容Android绘制
我们在 Canvas 中遍历 animatedRainbows 所有的 AnimtaedR容器英文ainbow 完成彩虹的绘制容器苗。彩虹的图形主要依靠 DrawScope
的 drawCircle
完成,比较简单。一点需要特别注意,彩虹动画结束时也要以一个圆形图案逐渐退出直至漏出底部内容,要实现这个效果,用到一个小技巧,我们的圆形绘制使用空心圆 (Stroke ) 而非 实心圆( Fill )
- 出现彩虹:圆环逐渐铺满屏幕却不能漏出空心。这要求 StrokeWidth 宽度覆spring漏洞盖 Scrcanvasee携程旅游网nSize,且始终保持 CircleRadius 的两倍
- 结束彩虹:圆环空心springboot面试题部分逐渐覆盖屏幕。此时要求 CircleRad携程网官网ius 减去 StrokeWidth / 2 之后依然能覆盖 ScreenSize
基于以上原则,我们为 AnimatedRainbow 添spring面试题加单个 AnnimatedRainbo容器设计w 的绘制方法:
fun DrawScope.draw() {
drawCircle(
brush = color, //圆环颜色
center = center, //圆心:点赞位置
radius = radius.value,// Animtable 中变化的 radius 值,
style = Stroke((radius.value * 2).coerceAtMost(_screenSize)),
)
}
如上,StrokeWidth 覆盖 ScreenSize 之后无需继续增长,而 CircleRadius 的最终尺寸除去 ScreenSize 之外还要将 StrokeWidth 考虑进去,因此前面代码中将 Animtable 的 targetValue 设置为 ScreenSize 的 1.6 倍。
4. 表情动画
4.携程旅游网1 状态管理
表情动画又由三个子动画组携程电话成:旋转动画、透明度canvas什么意思动画以及抛物线轨迹动画。像 AnimtaedRainbow 一样,我们定义 AnimatedEmoji
管理每个表情动画的状态,AnimatedEmcanvas什么意思oji 中通过多个 Animata携程网上订票火车票ble 分别管理前面提到的几个子动画
AnimatedEmoji
class AnimatedEmoji(
private val start: Offset, //表情抛点位置,即长按的屏幕位置
private val screenWidth: Float, //屏幕宽度
private val screenHeight: Float, //屏幕高度
private val duration: Int = 1500 //动画时长
) {
//抛出距离(x方向移动终点),在左右一个屏幕之间取随机数
private val throwDistance by lazy {
((start.x - screenWidth).toInt()..(start.x + screenWidth).toInt()).random()
}
//抛出高度(y方向移动终点),在屏幕顶端到抛点之间取随机数
private val throwHeight by lazy {
(0..start.y.toInt()).random()
}
private val x = Animatable(start.x)//x方向移动动画值
private val y = Animatable(start.y)//y方向移动动画值
private val rotate = Animatable(0f)//旋转动画值
private val alpha = Animatable(1f)//透明度动画值
suspend fun CoroutineScope.startAnim() {
async {
//执行旋转动画
rotate.animateTo(
360f, infiniteRepeatable(
animation = tween(_duration / 2, easing = LinearEasing),
repeatMode = RepeatMode.Restart
)
)
}
awaitAll(
async {
//执行x方向移动动画
x.animateTo(
throwDistance.toFloat(),
animationSpec = tween(durationMillis = duration, easing = LinearEasing)
)
},
async {
//执行y方向移动动画(上升)
y.animateTo(
throwHeight.toFloat(),
animationSpec = tween(
duration / 2,
easing = LinearOutSlowInEasing
)
)
//执行y方向移动动画(下降)
y.animateTo(
screenHeight,
animationSpec = tween(
duration / 2,
easing = FastOutLinearInEasing
)
)
},
async {
//执行透明度动画,最终状态是半透明
alpha.animateTo(
0.5f,
tween(duration, easing = CubicBezierEasing(1f, 0f, 1f, 0.8f))
)
}
)
}
infiniteRepeatable
上面代码中,旋转动画的携程网站官网 Anim容器的容积一定比它的体积小ationSpec 使用 infinispring漏洞teR容器英文epeatable
创建了一个无限循环的动画,RepeatspringbootMode.Restart
表示它的从 0F
过渡到 360F
之后,再次重复这个过程。
除了旋转动画之外,其他动画都会在 duration
之后结束,它们分别在 async
中启动并行执行,awaitAll
等待它们容器设计全部结束。容器英文而由于旋转动画不会结束,因此不能放到 awaitAll 中,否则 startAnim 的调用方将永远无法恢复执行。
CubicBezierEasing
透明度动画中的 easing
指定了一个 CubicBezierEasing
。ea携程客服电话sing 是动画衰减效果,即动画状态以何canvas软件种速率逼近目标值。Compose 提供了几个默认的 Easing 类型可供使用,分别是:
//默认的 Easing 类型,以加速度起步,减速度收尾
val FastOutSlowInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f)
//匀速起步,减速度收尾
val LinearOutSlowInEasing: Easing = CubicBezierEasing(0.0f, 0.0f, 0.2f, 1.0f)
//加速度起步,匀速收尾
val FastOutLinearInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f)
//匀速接近目标值
val LinearEasing: Easing = Easing { fraction -> fraction }
上图横轴是时间,纵轴是逼近目标值的进度,可以看到除了 LinearEasing
之外,其它的的曲线变化都满足 CubicBezierEasing
三阶贝塞尔曲线,如果默认 Easing 不符合你的android是什么手机牌子使用要求,可以使用 CubicBezierEasing
,通过参数,自定义合适的曲线效果。携程网站官网比如例子中曲线如下:
这个曲线前半程状态值进度非常缓慢,临近时间结束才快速逼近最终状态。因为我们希望表情动画全程spring漏洞清晰可见,透明度的衰减尽量后置,默认 easiing 无法提供这种效果,因此我们自定义 CubicBezierEasing
抛物线动画
再来看一下抛物携程线动画的实现。通常我们可以借助抛物线公式,基于一些动画状态变量计算抛物线坐标来实现动容器苗画,但这个例子中我们借助 Easiandroid的drawable类ng 更加巧妙的实现了抛android下载安装物线动画。 我们将抛物线动画拆解为 x 轴和 ycanvas网页版 轴两个方向两个并行执行的位移动画,x 轴位移通过 Linecanvas英语arEasing 匀速完成,y 轴又拆分成两个过程
- 上升到最高点,使用 Lineacanvas下载rOutSlowInEasing容器云 上升时速度加速衰减
- 下落到屏幕底端,使用 FastOutLinearInEasing 下落时速度加速增加
上升和下降的 Easing 曲线互相对称,符合抛物线规spring漏洞律
animatedEmojis 列表
像彩虹动画一样,我们同样携程电话使用一个 Muta容器的容积一定比它的体积小bleStateList 集合管理 AnimatedEmoji 对象,并在 LaunchedEffect 中监听新元素android什么意思的插入,并执行动画。只是表情动画每次会批量增加多个
//MutableStateList 保存 animatedEmojis
val animatedEmojis = mutableStateListOf<AnimatedEmoji>()
//一次增加 EmojiCnt 个表情
animatedEmojis.addAll(buildList {
repeat(EmojiCnt) {
add(AnimatedEmoji(offset, screenWidthPx, screenHeightPx, res))
}
})
//监听 animatedEmojis 变化
LaunchedEffect(Unit) {
//监听到新加入的 EmojiCnt 个表情
snapshotFlow { animatedEmojis.takeLast(EmojiCnt) }
.flatMapMerge { it.asFlow() }
.collect {
launch {
with(it) {
startAnim()//启动表情动画,等待除了旋转动画外的所有动画结束
animatedEmojis.remove(it) //从列表移除
}
}
}
}
4.2 内容绘制
单个 AnimatedEmoji 绘制代码很简单,借助 DrawScopespringboot面试题
的 drawImage
绘制表情素材即可
//当前 x,y 位移的位置
val offset get() = Offset(x.value, y.value)
//图片topLeft相对于offset的距离
val d by lazy { Offset(img.width / 2f, img.height / 2f) }
//绘制表情
fun DrawScope.draw() {
rotate(rotate.value, pivot = offset) {
drawImage(
image = img, //表情素材
topLeft = offset - dCenter,//当前位置
alpha = alpha.value, //透明度
)
}
}
注意旋转动画实际上是借助 DrawScope
的 rotate
方法实现的,在 block 内部调用 drawIspring是什么意思mage
指定当前的 alpha
和 topLeft
即可canvas平台。
5. 烟花动画
5.1 状态管理
烟花动画紧跟在表情动画结束时android是什么系统发生,动画不涉及位置变化,主要是几个花瓣不断缩小的过程。花瓣用圆形绘制,动画状态值就是圆形半径,使用 Animatable 包装。
AnimatedFlower
烟花的绘制还要用到颜色等信息,我们定义 AnimatedFlower 保存包括 Animtable 在内的相关状态。
class AnimatedFlower(
private val intial: Float, //花瓣半径初始值,一般是表情的尺寸
private val duration: Int = 2500
) {
//花瓣半径
private val radius = Animatable(intial)
suspend fun startAnim() {
radius.animateTo(0f, keyframes {
durationMillis = duration
intial / 3 at 0 with FastOutLinearInEasing
intial / 5 at (duration * 0.95f).toInt()
})
}
keyframes
这里又出现了一种 AnimationSpec,即帧动画 keyframes
,相canvas软件对于 tween ,keyframes
可以更精确指定时间区间内的动画进携程网上订票飞机度。比如代码中 radius / 3 at 0
表示 0 秒时状canvas下载态值达到 intial / 3
,相当于以初始值的 1/3
尺寸出现,这是一般的 tween 难以实现的。另外我们希望花瓣可以持久可见,所以使用 keyframe
确保时间进行到 95% 时,radius 的尺寸仍然清晰可见。
animatedFlower 列spring漏洞表
由于烟花动画设计是表情动画的延续,所以它紧跟表情动画执行,共享 CoroutienScope ,不需要借助 LaunchedEffect ,所以spring面试题使用普通列表定义 aniandroid手机matedFlower 即可:
//animatedFlowers 使用普通列表创建
val animatedFlowers = mutableListOf<AnimatedFlower>()
launch {
with(it) {//表情动画执行
startAnim()
animatedEmojis.remove(it)
}
//创建 AnimatedFlower 动画
val anim = AnimatedFlower(
center = it.offset,
//使用 Palette 从表情图片提取烟花颜色
color = Palette.from(it.img.asAndroidBitmap()).generate().let {
arrayOf(
Color(it.getDominantColor(Color.Transparent.toArgb())),
Color(it.getVibrantColor(Color.Transparent.toArgb()))
)
},
initial = it.img.run { width.coerceAtLeast(height) / 2 }.toFloat()
)
animatedFlowers.add(anim) //添加进列表
anim.startAnim() //执行烟花动画
animatedFlowers.remove(anim) //移除动画
}
5.2 内容绘制
烟花的内容绘制,需要计算每个花瓣的位置,一共8个花瓣,各自位置计算如下:
//计算 sin45 的值
val sin by lazy { sin(Math.PI / 4).toFloat() }
val points
get() = run {
val d1 = initial - radius.value
val d2 = (initial - radius.value) * sin
arrayOf(
center.copy(y = center.y - d1), //0点方向
center.copy(center.x + d2, center.y - d2),
center.copy(x = center.x + d1),//3点方向
center.copy(center.x + d2, center.y + d2),
center.copy(y = center.y + d1),//6点方向
center.copy(center.x - d2, center.y + d2),
center.copy(x = center.x - d1),//9点方向
center.copy(center.x - d2, center.y - d2),
)
}
center
是烟花的中心位置,随着花瓣的变小,同时越来越远离中心位置,因此 d1
和 d2
就是偏离 cespring框架nter 的距离,与 radius 大小成反比。
最后在 Canvas 中绘容器制这些 points 即可:
fun DrawScope.draw() {
points.forEachIndexed { index, point ->
drawCircle(color = color[index % 2], center = point, radius = radius.value)
}
}
6. 合体效果
最后我们定义一个 AnimatedCanvasLike
的Spring Composable ,整合上面代码
@Composable
fun AnimatedLike(modifier: Modifier = Modifier, state: LikeAnimState = rememberLikeAnimState()) {
LaunchedEffect(Unit) {
//监听新增表情
snapshotFlow { state.animatedEmojis.takeLast(EmojiCnt) }
.flatMapMerge { it.asFlow() }
.collect {
launch {
with(it) {
startAnim()
state.animatedEmojis.remove(it)
}
//添加烟花动画
val anim = AnimatedFlower(
center = it.offset,
color = Palette.from(it.img.asAndroidBitmap()).generate().let {
arrayOf(
Color(it.getDominantColor(Color.Transparent.toArgb())),
Color(it.getVibrantColor(Color.Transparent.toArgb()))
)
},
initial = it.img.run { width.coerceAtLeast(height) / 2 }.toFloat()
)
state.animatedFlowers.add(anim)
anim.startAnim()
state.animatedFlowers.remove(anim)
}
}
}
LaunchedEffect(Unit) {
//监听新增彩虹
snapshotFlow { state.animatedRainbows.lastOrNull() }
.filterNotNull()
.collect {
launch {
val result = it.startAnim()
if (result.endReason == AnimationEndReason.Finished) {
state.animatedRainbows.remove(it)
}
}
}
}
//绘制动画
Canvas(modifier.fillMaxSize()) {
//绘制彩虹
state.animatedRainbows.forEach { animatable ->
with(animatable) { draw() }
}
//绘制表情
state.animatedEmojis.forEach { animatable ->
with(animatable) { draw() }
}
//绘制烟花
state.animatedFlowers.forEach { animatable ->
with(animatable) { draw() }
}
}
}
我们使用 AnimatedLike
布局就可以容器为页面添加动画效spring果了,由于 Canvas 本身是基于 modifier.drawBe容器的容积一定比它的体积小hind
实现的,我们也可以将 AnimatedLike 改为 Modifier 修饰符使用,这容器的容积一定比它的体积小里就不赘述携程网上订票飞机了。
最容器技术后,复习一下本文例子中的内容:
-
Animatable
:包装动画状态值,并且在协程中执行动画,同步返回动画结果 -
AnimationSpec
:动画规格,可以配置动画时长、Easing 等,例子中用到了 tween,k容器云eyframes,infiniteRepeatable 等多个动画规格spring是什么意思 -
Easing
:动画状态值随时间变化的趋势,通常使用默认类型即可, 也可以基于 CubicBezierEasing 定制。
一个例子不可能覆盖到 Compose 所有的动画 API,但是我们只要掌握了上述几个关键知容器是什么识点,再学习其他 API 就是水到渠成的事情了。