刚刚完毕的 2022 年,不少运用都给出了自己的 2022 年度报告。趁着这股热潮,我自己保护的运用《译站》 也来凑个热闹,用 Jetpack Compose 写了个报告页面。作用如下:
作用还算不错?假如需求实践体验的,能够前往 这儿 下载翻译后翻开底部最右侧 tab,即可现场看到。
制作进程
调查上图,需求完结的有三个难点:
- 闪烁的数字
- 淡出 + 向上位移的微件们
- 有一部分微件不参与淡出(如 Spacer)
下面将具体介绍
闪烁的数字
在我的上一篇文章 Jetpack Compose 十几行代码快速仿照立刻点赞数字切换作用 中,我基于 AnimatedContent
完结了 数字增加时主动做动画
的 Text,它的作用如下:
诶,已然如此,那完结这个数字跳动不就简略了吗?咱们只需求让数字主动从 0
变成 方针数字
,不就有了动画的作用吗?
此处我挑选 Animatable
,而且运用 LauchedEffect
让数字主动开端递增,并把数字格式化为 0013
(长度为方针数字的长度)传入到前次完结的微件中,这样一个主动跳动的动画就做好啦。
代码如下:
@Composable
fun AutoIncreaseAnimatedNumber(
modifier: Modifier = Modifier,
number: Int,
durationMills: Int = 10000,
textPadding: PaddingValues = PaddingValues(horizontal = 8.dp, vertical = 12.dp),
textSize: TextUnit = 24.sp,
textColor: Color = Color.Black,
textWeight: FontWeight = FontWeight.Normal
) {
// 动画,Animatable 相关介绍能够见 https://compose.funnysaltyfish.fun/docs/design/animation/animatable?source=trans
val animatedNumber = remember {
androidx.compose.animation.core.Animatable(0f)
}
// 数字格式化后的长度
val l = remember {
number.toString().length
}
// Composable 进入 Composition 阶段时敞开动画
LaunchedEffect(number) {
animatedNumber.animateTo(
targetValue = number.toFloat(),
animationSpec = tween(durationMillis = durationMills)
)
}
NumberChangeAnimatedText(
modifier = modifier,
text = "%0${l}d".format(animatedNumber.value.roundToInt()),
textPadding = textPadding,
textColor = textColor,
textSize = textSize,
textWeight = textWeight
)
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun NumberChangeAnimatedText(
modifier: Modifier = Modifier,
text: String,
textPadding: PaddingValues = PaddingValues(horizontal = 8.dp, vertical = 12.dp),
textSize: TextUnit = 24.sp,
textColor: Color = Color.Black,
textWeight: FontWeight = FontWeight.Normal,
) {
Row(modifier = modifier) {
text.forEach {
AnimatedContent(
targetState = it,
transitionSpec = {
slideIntoContainer(AnimatedContentScope.SlideDirection.Up) with
fadeOut() + slideOutOfContainer(AnimatedContentScope.SlideDirection.Up)
}
) { char ->
Text(text = char.toString(), modifier = modifier.padding(textPadding), fontSize = textSize, color = textColor, fontWeight = textWeight)
}
}
}
}
这样就完结啦~
淡出 + 向上位移的微件们
实践上,这个标题的难点在于“们”这个字,这意味着不但要完结“向上+淡出”的作用,还要有序,一个一个来。
关于这个问题,因为我的需求很简略:一切微件竖着摆放,自上而下逐渐淡出。因而,我挑选的解决思路是:自定义布局。(这不一定是唯一的思路,假如你有更好的办法,也欢迎一起讨论)。下面咱们渐渐拆解:
微件竖着放
这其实是最简略的一步,你能够阅览我曾经写的 深化Jetpack Compose——布局原理与自定义布局(一) 来了解。简略来说,咱们只需求顺次摆放一切微件,然后把总宽度设为宽度最大值,总高度设为高度之和即可。代码如下:
@Composable
fun AutoFadeInComposableColumn(
modifier: Modifier = Modifier,
content: @Composable FadeInColumnScope.() -> Unit
) {
val measurePolicy = MeasurePolicy { measurables, constraints ->
val placeables = measurables.map { measurable ->
measurable.measure(constraints.copy(minHeight = 0, minWidth = 0))
}
var y = 0
// 宽度:父组件答应的最大宽度,高度:微件高之和
layout(constraints.maxWidth, placeables.sumOf { it.height }) {
// 顺次摆放
placeables.forEachIndexed { index, placeable ->
placeable.placeRelativeWithLayer(0, y){
alpha = 1
}
y += placeable.height
}.also {
// 重置高度
y = 0
}
}
}
Layout(modifier = modifier, content = { FadeInColumnScopeInstance.content() }, measurePolicy = measurePolicy)
}
上面的例子便是最简略的自定义布局了,它能够完结内部的 Composable 从上到下竖着摆放。注意的是,在 place
的时候,咱们运用了 placeRelativeWithLayer
,它能够调整组件的 alpha
(还有 rotation
/transform
),这个未来会被用于完结淡出作用。
一个一个淡出
到了要害的一步了。咱们无妨想一想,淡出便是 alpha 从 0->1,y 偏移从 offsetY
-> 0 的进程,因而咱们只需求在 place
时控制一下两者的值就行。作为一个动画进程,自然能够运用 Animatable
。现在的问题是:需求几个 Animatable 呢?
自然,你能够挑选运用 n 个 Animatable
分别控制 n 个微件,不过考虑到同一时刻其实只有一个 @Composable
在做动画,因而我挑选只用一个。因而咱们需求增加一些变量:
- currentFadeIndex 记载当时是哪个微件在播映动画
- finishedFadeIndex 记载播映完结的最终一个微件的 index,用于检查动画是否完毕了
实话说这两个变量或许能够组成一个,不过已然写成了两个,那就先这样写下去吧。
两个状态能够只放到 Layout
里边,也能够放到专门的 State
中,考虑到外部可能要用到(嘿嘿,其实是真的要用到)两个值,咱们单独写一个 State
吧
class AutoFadeInColumnState {
var currentFadeIndex by mutableStateOf(-1)
var finishedFadeIndex by mutableStateOf(0)
companion object {
val Saver = listSaver<AutoFadeInColumnState, Int>(
save = { listOf(it.currentFadeIndex, it.finishedFadeIndex) },
restore = {
AutoFadeInColumnState().apply {
currentFadeIndex = it[0]; finishedFadeIndex = it[1]
}
}
)
}
}
@Composable
fun rememberAutoFadeInColumnState(): AutoFadeInColumnState {
return rememberSaveable(saver = AutoFadeInColumnState.Saver) { AutoFadeInColumnState() }
}
接下来,为咱们的自定义 Composable 增加几个参数吧
@Composable
fun AutoFadeInComposableColumn(
modifier: Modifier = Modifier,
state: AutoFadeInColumnState = rememberAutoFadeInColumnState(),
fadeInTime: Int = 1000, // 单个微件动画的时刻
fadeOffsetY: Int = 100, // 单个微件动画的偏移量
content: @Composable FadeInColumnScope.() -> Unit
)
接下来便是要害,修改 place
的代码完结动画作用。
// ...
placeables.forEachIndexed { index, placeable ->
// @1 实践的 y,关于动画中的微件减去偏移量,关于未动画的微件不变
val actualY = if (state.currentFadeIndex == index) {
y + (( 1 - fadeInAnimatable.value) * fadeOffsetY).toInt()
} else {
y
}
placeable.placeRelativeWithLayer(0, actualY){
// @2
alpha = if (index == state.currentFadeIndex) fadeInAnimatable.value else
if (index <= state.finishedFadeIndex) 1f else 0f
}
y += placeable.height
}.also {
y = 0
}
相较于之前,代码有两处首要更改。@1
处更改微件的 y
,关于动画中的微件减去偏移量,关于未动画的微件不变,以完结 “位移” 的作用; @2
处则设置 alpha
值完结淡出作用,具体逻辑如下:
- 假如是正在动画的那个,alpha 便是当时动画的值,完结渐渐淡出的作用
- 否则,关于现已履行完动画的,alpha 正常为 1;否则为 0(还没轮到它们显示)
接下来,问题在于履行完一个怎么履行下一个了。我的思路是这样的:增加一个 LauchedState(state.currentFadeIndex)
使得在 currentFadeIndex
变化时(这表示当时履行动画的微件变了)重新把 Animatable
置0,敞开动画作用。动画完结后又把 currentFadeIndex
加一,直至完结一切。代码如下:
@Composable
fun xxx(...){
LaunchedEffect(state.currentFadeIndex){
if (state.currentFadeIndex == -1) {
// 找到第一个需求渐入的元素
state.currentFadeIndex = 0
}
// 开端动画
fadeInAnimatable.animateTo(
targetValue = 1f,
animationSpec = tween(
durationMillis = fadeInTime,
easing = LinearEasing
)
)
// 动画播映完了,更新 finishedFadeIndex
state.finishedFadeIndex = state.currentFadeIndex
// 全部动画完了,退出
if(state.finishedFadeIndex >= whetherFadeIn.size - 1) return@LaunchedEffect
state.currentFadeIndex += 1
fadeInAnimatable.snapTo(0f) // snapTo(0f) 无动画直接置0
}
}
到这儿,一个 内部子微件顺次淡出
的自定义布局现已基本完结了。下面问题来了:在 Compose 中,咱们运用 Spacer
创建间隔,但是往往 Spacer
是不需求动画的。因而咱们需求支持一个特性:答应设置某些 Composable 不做动画,也便是直接跳过它们。这种子微件告知父微件信息的时期,当然要交给 ParentData
来做
答应部分 Composable 不做动画
要了解 ParentData
,您能够参考我的文章 深化Jetpack Compose——布局原理与自定义布局(四)ParentData,此处不再赘述。
咱们增加一个 class FadeInColumnData(val fade: Boolean = true)
和 对应的 Modifier,用于指定某些 Composable 跳过动画。考虑到这个特定的 Modifier
只能用在咱们这个布局,因而需求加上 scope
的限制。这些代码如下:
class FadeInColumnData(val fade: Boolean = true) : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any =
this@FadeInColumnData
}
interface FadeInColumnScope {
@Stable
fun Modifier.fadeIn(whetherFadeIn: Boolean = true): Modifier
}
object FadeInColumnScopeInstance : FadeInColumnScope {
override fun Modifier.fadeIn(whetherFadeIn: Boolean): Modifier = this.then(FadeInColumnData(whetherFadeIn))
}
有了这个,咱们上面的布局也得做相应的更改,具体来说:
- 需求增加一个列表
whetherFadeIn
记载ParentData
供给的值 - 开端的动画 index 不再是
0
,而是找到的第一个需求做动画的元素 -
currentFadeIndex
的更新需求找到下一个需求做动画的值
具体代码如下:
@Composable
fun AutoFadeInComposableColumn() {
var whetherFadeIn: List<Boolean> = arrayListOf()
// ...
LaunchedEffect(state.currentFadeIndex){
// 等候初始化完结
while (whetherFadeIn.isEmpty()){ delay(50) }
if (state.currentFadeIndex == -1) {
// 找到第一个需求渐入的元素
state.currentFadeIndex = whetherFadeIn.indexOf(true)
}
// 开端动画
// - state.currentFadeIndex = 0
for (i in state.finishedFadeIndex + 1 until whetherFadeIn.size){
if (whetherFadeIn[i]){
state.currentFadeIndex = i
fadeInAnimatable.snapTo(0f)
break
}
}
}
val measurePolicy = MeasurePolicy { measurables, constraints ->
// ...
whetherFadeIn = placeables.map { placeable ->
((placeable.parentData as? FadeInColumnData) ?: FadeInColumnData()).fade
}
// 宽度:父组件答应的最大宽度,高度:微件高之和
layout(constraints.maxWidth, placeables.sumOf { it.height }) {
// ...
}
}
Layout(modifier = modifier, content = { FadeInColumnScopeInstance.content() }, measurePolicy = measurePolicy)
}
完结啦!
一点小问题
事实上,整个布局的大体到目前现已趋于完结,不过目前有点小问题:关于 AutoIncreaseAnimatedNumber
,它的动画履行机遇是错误的。你能够想象:尽管数字没有显示出来(alpha 为 0),但实践上它现已被摆放了,因而数字跳动的动画现已开端了。关于这个问题,我的解决方案是为 AutoIncreaseAnimatedNumber
额定增加一个 Boolean 参数 startAnim
,只有该值为 true
时才真实开端履行动画。
那么 startAnim
什么时候为 true 呢?便是 currentFadeIndex == 这个微件的 Index
时,这样就能够手工指定什么时候开端动画了。
代码如下:
@Composable
fun AutoIncreaseAnimatedNumber(
startAnim: Boolean = true,
...
) {
// Composable 进入 Composition 阶段,且 startAnim 为 true 时敞开动画
LaunchedEffect(number, startAnim) {
if (startAnim)
animatedNumber.animateTo(
targetValue = number.toFloat(),
animationSpec = tween(durationMillis = durationMills)
)
}
NumberChangeAnimatedText(
...
)
}
实践运用时
Row(verticalAlignment = Alignment.CenterVertically) {
AnimatedNumber(number = 110, startAnim = state.currentFadeIndex == 7) // 或许 >=,假如动画时刻善于 fadeInTime 的话
ResultText(text = "次")
}
完工!
Pager?
如你所想,全体的布局是用 Pager
完结的,这个用的是 google/accompanist: A collection of extension libraries for Jetpack Compose 内的完结。鉴于不是本篇重点,此处略过,感兴趣的能够看下面的代码。
代码
完整代码见 FunnyTranslation/AnnualReportScreen.kt at compose。
假如有用,欢迎 Star库房 / 此处点赞 / 谈论 ~