前言

本篇文章是此专栏的第三篇文章,如果想阅读前两篇文章的话请点击下方链接:

  • Compose 动画艺术探索之可见性动画示例详解
  • Compose开发之动画艺术探索及实现示例

Compose的属性动画

属性动画是通过不断地修改值来实现的,而初始值和结束值之间的过渡动画就需要来计算了。在 Compose 中为我们提供了一整套 api 来实现属性动画,具体有哪些呢?让我们一起来看下吧!

官方为我们提供了上图这十种方法,我们可以根据实际项目中的需求进行挑选使用。

在第一篇文章中也提到了 Compose 的属性动画,但只是简单使用了下,告诉大家 Compose 有这个东西,今天咱们来具体看下!

先来看下 animateColorAsState 的代码吧:

@Composable
fun animateColorAsState(
    targetValue: Color,
    animationSpec: AnimationSpec<Color> = colorDefaultSpring,
    label: String = "ColorAnimation",
    finishedListener: ((Color) -> Unit)? = null
): State<Color> {
    val converter = remember(targetValue.colorSpace) {
        (Color.VectorConverter)(targetValue.colorSpace)
    }
    return animateValueAsState(
        targetValue, converter, animationSpec, label = label, finishedListener = finishedListener
    )
}

可以看到一共接收四个参数,来分别看下代表什么吧:

  • targetValue:顾名思义,目标值,这里对应的就是想要转换成的颜色
  • animationSpec:动画规格,动画随着时间改变值的一种规格吧,上一篇文章中也提到了,但由于上一篇文章主要内容并不是这个,也就没有讲,本篇文章会详细说明
  • label:标签,以区别于其他动画
  • finishedListener:在动画完成时会进行回调

参数并不算多,而且有三个是可选参数,也就只有 targetValue 必须要进行设置。方法体内只通过 Color.colorSpace 强转构建了一个 TwoWayConverter 。

前面说过,大多数 Compose 动画 API 支持将 FloatColorDp 以及其他基本数据类型作为 开箱即用的动画值,但有时我们需要为其他数据类型(比如自定义类型)添加动画效果。在动画播放期间,任何动画值都表示为 AnimationVector。使用相应的 TwoWayConverter 即可将值转换为 AnimationVector,反之亦然,这样一来,核心动画系统就可以统一对其进行处理了。由于颜色有 argb,所以构建的时候使用的是 AnimationVector4D ,来看下吧:

val Color.Companion.VectorConverter:
    (colorSpace: ColorSpace) -> TwoWayConverter<Color, AnimationVector4D>
        get() = ColorToVector

如果按照我之前的习惯肯定要接着看 animateValueAsState 方法内部的代码了,但今天等会再看!再来看看 animateDpAsState 的代码吧!

@Composable
fun animateDpAsState(
    targetValue: Dp,
    animationSpec: AnimationSpec<Dp> = dpDefaultSpring,
    label: String = "DpAnimation",
    finishedListener: ((Dp) -> Unit)? = null
): State<Dp> {
    return animateValueAsState(
        targetValue,
        Dp.VectorConverter,
        animationSpec,
        label = label,
        finishedListener = finishedListener
    )
}

发现了点什么没有,参数基本一摸一样,别着急,咱们再看看别的!

@Composable
fun animateIntAsState(
    targetValue: Int,
    animationSpec: AnimationSpec<Int> = intDefaultSpring,
    label: String = "IntAnimation",
    finishedListener: ((Int) -> Unit)? = null
)
​
@Composable
fun animateSizeAsState(
    targetValue: Size,
    animationSpec: AnimationSpec<Size> = sizeDefaultSpring,
    label: String = "SizeAnimation",
    finishedListener: ((Size) -> Unit)? = null
)
​
@Composable
fun animateRectAsState(
    targetValue: Rect,
    animationSpec: AnimationSpec<Rect> = rectDefaultSpring,
    label: String = "RectAnimation",
    finishedListener: ((Rect) -> Unit)? = null
)

不能说是大同小异,只能说是一摸一样!既然一摸一样的话咱们就以文章开头的 animateColorAsState 来看吧!

上面的说法其实是不对的,并不是有十种,而是九种,因为九种都调用了 animateValueAsState ,其实也可以说有无数种,因为可以自定义。。。。

参数

下面先来看下 animateValueAsState 的方法体吧:

@Composable
fun <T, V : AnimationVector> animateValueAsState(
    targetValue: T,
    typeConverter: TwoWayConverter<T, V>,
    animationSpec: AnimationSpec<T> = remember { spring() },
    visibilityThreshold: T? = null,
    label: String = "ValueAnimation",
    finishedListener: ((T) -> Unit)? = null
): State<T>

来看看接收的参数吧,可以发现有两个参数没有见过:

  • typeConverter:类型转换器,将需要的类型转换为 AnimationVector
  • visibilityThreshold:一个可选的阈值,用于定义何时动画值可以被认为足够接近targetValue以结束动画

OK,剩下的参数在上面都介绍过,就不重复进行介绍了。

方法体

由于 animateValueAsState 方法有点长,所以分开来看吧,接下来看下 animateValueAsState 方法中的前半部分:

val animatable = remember { Animatable(targetValue, typeConverter, visibilityThreshold, label) }
val listener by rememberUpdatedState(finishedListener)
val animSpec: AnimationSpec<T> by rememberUpdatedState(
    animationSpec.run {
        if (visibilityThreshold != null && this is SpringSpec &&
            this.visibilityThreshold != visibilityThreshold
        ) {
            spring(dampingRatio, stiffness, visibilityThreshold)
        } else {
            this
        }
    }
)
val channel = remember { Channel<T>(Channel.CONFLATED) }
SideEffect {
    channel.trySend(targetValue)
}
LaunchedEffect(channel) {
    for (target in channel) {
        val newTarget = channel.tryReceive().getOrNull() ?: target
        launch {
            if (newTarget != animatable.targetValue) {
                animatable.animateTo(newTarget, animSpec)
                listener?.invoke(animatable.value)
            }
        }
    }
}

可以看到首先构建了一个 Animatable ,然后记录了完成回调,又记录了 AnimationSpec ,之后有个判断,如果 visibilityThreshold 不为空并且 AnimationSpec 为 SpringSpec 的时候为新构建的一个 AnimationSpec ,反之则还是传进来的 AnimationSpec 。

那 Animatable 是个啥呢?它是一个值容器,它可以在通过 animateTo 更改值时为值添加动画效果,它可确保一致的连续性和互斥性,这意味着值变化始终是连续的,并且会取消任何正在播放的动画。Animatable 的许多功能(包括 animateTo)以挂起函数的形式提供,所以需要封装在适当的协程作用域内,所以下面使用了 LaunchedEffect 来包裹执行 animateTo 方法,最后调用了动画完成的回调。

由于 Animatable 类中代码比较多,先来看下类的初始化及构造方法吧!

class Animatable<T, V : AnimationVector>(
    initialValue: T,
    val typeConverter: TwoWayConverter<T, V>,
    private val visibilityThreshold: T? = null,
    val label: String = "Animatable"
) 

可以看到这里使用到的参数在 animateValueAsState 中都有,就不一一介绍了,挑着重点来,来看看上面使用到的 animateTo 吧:

suspend fun animateTo(
    targetValue: T,
    animationSpec: AnimationSpec<T> = defaultSpringSpec,
    initialVelocity: T = velocity,
    block: (Animatable<T, V>.() -> Unit)? = null
): AnimationResult<T, V> {
    val anim = TargetBasedAnimation(
        animationSpec = animationSpec,
        initialValue = value,
        targetValue = targetValue,
        typeConverter = typeConverter,
        initialVelocity = initialVelocity
    )
    return runAnimation(anim, initialVelocity, block)
}

可以看到 animateTo 使用传进来的参数构建了一个 TargetBasedAnimation ,这是一个方便的动画包装类,适用于所有基于目标的动画,即具有预定义结束值的动画。然后返回调用了 runAnimation ,返回值为 AnimationResult ,来看下吧:

class AnimationResult<T, V : AnimationVector>(
    
    val endState: AnimationState<T, V>,
    
    val endReason: AnimationEndReason
) {
    override fun toString(): String = "AnimationResult(endReason=$endReason, endState=$endState)"
}

AnimationResult 在动画结尾包含关于动画的信息,endState 捕获动画在最后一帧的值 evelocityframe time 等。它可以用于启动另一个动画以从先前中断的动画继续速度。endReason 描述动画结束的原因。

下面看下 runAnimation 吧:

private suspend fun runAnimation(
    animation: Animation<T, V>,
    initialVelocity: T,
    block: (Animatable<T, V>.() -> Unit)?
): AnimationResult<T, V> {
​
    val startTime = internalState.lastFrameTimeNanos
    return mutatorMutex.mutate {
        try {
            ......
            endState.animate(
                animation,
                startTime
            ) {
                updateState(internalState)
                ......
            }
            val endReason = if (clampingNeeded) BoundReached else Finished
            endAnimation()
            AnimationResult(endState, endReason)
        } catch (e: CancellationException) {
            // Clean up internal states first, then throw.
            endAnimation()
            throw e
        }
    }
}

这里需要注意:所有不同类型的动画代码路径最终都会汇聚到这个方法中。

好了,基本快见到阳光了!

天亮了

上面方法中有一行:endState.animate ,这个是关键,来看下!

internal suspend fun <T, V : AnimationVector> AnimationState<T, V>.animate(
    animation: Animation<T, V>,
    startTimeNanos: Long = AnimationConstants.UnspecifiedTime,
    block: AnimationScope<T, V>.() -> Unit = {}
) {
    val initialValue = animation.getValueFromNanos(0)
    val initialVelocityVector = animation.getVelocityVectorFromNanos(0)
    var lateInitScope: AnimationScope<T, V>? = null
    try {
        if (startTimeNanos == AnimationConstants.UnspecifiedTime) {
            val durationScale = coroutineContext.durationScale
            animation.callWithFrameNanos {
                lateInitScope = AnimationScope(...).apply {
                    // 第一帧
                    doAnimationFrameWithScale(it, durationScale, animation, this@animate, block)
                }
            }
        } else {
            lateInitScope = AnimationScope(...).apply {
                // 第一帧
                doAnimationFrameWithScale()
            }
        }
        // 后续帧
        while (lateInitScope!!.isRunning) {
            val durationScale = coroutineContext.durationScale
            animation.callWithFrameNanos {
                lateInitScope!!.doAnimationFrameWithScale(it, durationScale, animation, this, block)
            }
        }
        // 动画结束
    } catch (e: CancellationException) {
        lateInitScope?.isRunning = false
        if (lateInitScope?.lastFrameTimeNanos == lastFrameTimeNanos) {
            isRunning = false
        }
        throw e
    }
}

嗯,柳暗花明!这个动画函数从头到尾运行给定 animation 中定义的动画。在动画过程中,AnimationState 将被更新为最新的值,速度,帧时间等。

到这里 animateColorAsState 大概过了一遍,但也只是简单走了一遍流程,并没有深究里面的细节,比如 Animatable 类中都没看,runAnimation 方法也只是看了主要的代码等等。

结尾

到此这篇关于Compose 动画艺术之属性动画探索的文章就介绍到这了,更多相关Compose属性动画内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

Compose 动画艺术之属性动画探索的更多相关文章

  1. Canvas实现贝赛尔曲线轨迹动画的示例代码

    这篇文章主要介绍了Canvas实现贝赛尔曲线轨迹动画的示例代码的相关资料,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. HTML5 直播疯狂点赞动画实现代码 附源码

    为了烘托直播间的氛围,直播相对于普通视频或者文本内容,点赞动作通常无限次,引导用户疯狂点赞,今天小编给大家分享HTML5 直播疯狂点赞动画实现代码 附源码,感兴趣的朋友一起看看吧

  3. CSS中实现动画效果-附案例

    这篇文章主要介绍了 CSS中实现动画效果并附上案例代码及实现效果,就是CSS动画样式处理,动画声明需要使用@keyframes name,后面的name是人为定义的动画名称,下面我们来看看文章的具体实现内容吧,需要的小伙伴可以参考一下

  4. 基于canvas的骨骼动画的示例代码

    这篇文章主要介绍了基于canvas的骨骼动画的示例代码的相关资料,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  5. html5如何在Canvas中实现自定义路径动画示例

    本篇文章主要介绍了html5如何在Canvas中实现自定义路径动画示例,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  6. 基于HTML5+Webkit实现树叶飘落动画

    本文给大家分享一段实例代码给大家介绍基于HTML5+Webkit实现树叶飘落动画效果,需要的朋友参考下吧

  7. Html5页面内使用JSON动画的实现

    这篇文章主要介绍了Html5页面内使用JSON动画的实现的相关资料,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  8. html5实现图片转圈的动画效果——让页面动起来

    这篇文章主要介绍了html5实现图片转圈的动画效果——让页面动起来的相关资料,需要的朋友可以参考下

  9. 基于 HTML5 WebGL 实现的医疗物流系统

    物联网( IoT ),简单的理解就是物体之间通过互联网进行链接。这篇文章给大家介绍基于 HTML5 WebGL 实现的医疗物流系统,感兴趣的朋友跟随小编一起看看吧

  10. 为什么这个简单的动画无法在iOS 7上运行?

    在我的项目中,我有一个简单的动画,我只是从左到右移动一个视图.这在iOS6中运行良好,但是当我在iOS7中运行它没有做任何事情.有人知道为什么吗?如果动画非常简单,我该如何修复iOS7?我的代码是:我做了更新,我使用Xcode5和iOS7所以任何帮助人,你知道如何解决这个问题吗?

随机推荐

  1. Flutter 网络请求框架封装详解

    这篇文章主要介绍了Flutter 网络请求框架封装详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. Android单选按钮RadioButton的使用详解

    今天小编就为大家分享一篇关于Android单选按钮RadioButton的使用详解,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧

  3. 解决android studio 打包发现generate signed apk 消失不见问题

    这篇文章主要介绍了解决android studio 打包发现generate signed apk 消失不见问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧

  4. Android 实现自定义圆形listview功能的实例代码

    这篇文章主要介绍了Android 实现自定义圆形listview功能的实例代码,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  5. 详解Android studio 动态fragment的用法

    这篇文章主要介绍了Android studio 动态fragment的用法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  6. Android用RecyclerView实现图标拖拽排序以及增删管理

    这篇文章主要介绍了Android用RecyclerView实现图标拖拽排序以及增删管理的方法,帮助大家更好的理解和学习使用Android,感兴趣的朋友可以了解下

  7. Android notifyDataSetChanged() 动态更新ListView案例详解

    这篇文章主要介绍了Android notifyDataSetChanged() 动态更新ListView案例详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下

  8. Android自定义View实现弹幕效果

    这篇文章主要为大家详细介绍了Android自定义View实现弹幕效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  9. Android自定义View实现跟随手指移动

    这篇文章主要为大家详细介绍了Android自定义View实现跟随手指移动,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. Android实现多点触摸操作

    这篇文章主要介绍了Android实现多点触摸操作,实现图片的放大、缩小和旋转等处理,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部