• 分析Jetpack Compose动画内部是如何实现的


    前言

    Compose的动画Api用起来很简单,效果看起来很神奇,那么它内部到底是如何运转的呢?

    使用动画的代码示例:

    1. var isOffset by remember { mutableStateOf(false) }
    2. val offsetAnimation by animateDpAsState(targetValue = if (isOffset) 100.dp else 0.dp)
    3. Button(
    4. onClick = { isOffset = !isOffset },
    5. modifier = Modifier.offset(0.dp, offsetAnimation)
    6. ) {
    7. Text(text = "点我进行位移")
    8. }

      

    可以看到有一个Boolean类型的isOffset状态,控制着offsetAnimation动画,然后动画又控制着Button的offset,最终实现了动画效果

    正文

    我们主要就看一下animateDpAsState(animate*AsState)做了什么

    跟一下animateDpAsState最后会走进animateValueAsState方法中:

     方法内部创建了一个Animatable动画类

    然后我们跟着上图的箭头看,在targetValue发生变化后,会通过channel来发送targetValue值的变化,然后launch启动一个协程,并在其中调用了animatable的animateTo方法

    接着我们就看看Animatable类是如何做动画的:

    主要就是内部持有一个AnimationState类型(State)的internalState 变量,然后我们接着上面的代码跟一下animateTo方法:

     

     没什么好说的,包了一下targetValue,接着就调用了runAnimation方法,接着往下跟(截不下分成两张图):

     主要逻辑就是endState.animate()
    endState就是copy的Animatable中的AnimationState对象internalState,也就是动画的初始值

    然后animation就是在animateTo方法中包装了动画目标值的对象

    接着跟animate方法:

    这个方法主要分为两部分,第一部分就是构建了一个AnimationScope,一个数据结构,用来存放动画需要的一些信息

     第二个部分就是真正动画计算和生效的地方,doAnimationFrame

    首先会在第一次创建AnimationScope的时候执行一次(或再一下帧执行一次,通过callWithFrameNanos方法)

    然后会判断如果动画还未执行完毕,就一直循环(while),一帧一帧执行doAnimationFrame计算动画的值

     doAnimationFrame方法代码:

     其中通过时间等参数计算当前动画应该设置的值,包括lastFrameTimeNanos和value等属性,然后再最后调用updateState方法去将AnimationScope的值更新到AnimationState中

    updatState方法代码:

     这个state对象其实也就是animateDpAsState中的Animatable动画类中的AnimationState对象internalState

    所以上面其实就是Compose中动画的简化流程

    总结

     由于AnimationState是一个State,在Compose中使用会自动监听其变化,只要其value变化了,就会导致相应位置重组,然后Composable就会使用新的值来展示不同的效果,比如最开始的示例代码:

    1. var isOffset by remember { mutableStateOf(false) }
    2. val offsetAnimation by animateDpAsState(targetValue = if (isOffset) 100.dp else 0.dp)
    3. Button(
    4. onClick = { isOffset = !isOffset },
    5. modifier = Modifier.offset(0.dp, offsetAnimation)
    6. ) {
    7. Text(text = "点我进行位移")
    8. }

     在修改了isOffset后,animateDpAsState中的值就会因为动画的计算和修改内部state的value,导致Button的offset函数一直被重新调用,使Button不停的向下移动

    其实Compose中的动画如果不考虑那么多东西的话,可以简化为如下代码:

    1. /**
    2. * creator: lt
    3. * effect : 自定义的动画播放器,逻辑更简单
    4. * warning:
    5. * [initialValueWithState]动画要改变的状态,起始动画值为其value值
    6. * [targetValue]要通过动画转化到的目标值
    7. * [duration]动画的持续时间
    8. */
    9. @OptIn(ExperimentalComposeApi::class)
    10. suspend fun animateWithFloat(
    11. initialValueWithState: MutableState<Float>,
    12. targetValue: Float,
    13. duration: Int = AnimationConstants.DefaultDurationMillis,
    14. ) {
    15. //动画起始值,目标差值
    16. val startValue = initialValueWithState.value
    17. val valueToBeTransformed = targetValue - startValue
    18. //动画起始时间,持续时间
    19. val startTime = System.nanoTime()
    20. val duration = duration * 1000000L
    21. //通过循环在下一帧计算动画的值
    22. val frameClock = coroutineContext.monotonicFrameClock
    23. while (System.nanoTime() <= startTime + duration) {
    24. frameClock.withFrameNanos {
    25. //计算动画的值,并设置值给状态
    26. val progress = minOf(it - startTime, duration).toFloat() / duration
    27. val increase = progress * valueToBeTransformed
    28. initialValueWithState.value = startValue + increase
    29. }
    30. }
    31. }

    使用方式如下,效果跟示例差不多:

     ps:不建议线上项目用这个api,还是用系统的比较好,如果想使用也可以参考(欢迎star): ComposeViews/MAnimator.kt at main · ltttttttttttt/ComposeViews (github.com)

    如果有分析的不对的地方请大佬们指出

    end

  • 相关阅读:
    SystemVerilog Assertions应用指南 Chapter1.20“ $past”构造
    MySQL 关键字 IN 与 EXISTS 的使用与区别
    NOIP 2007 普及组初赛试题 第21题
    VTK未引用报错
    【通信】基于CSM实现自适应波束形成附完整matlab代码
    [Machine Learning]pytorch手搓一个神经网络模型
    Zabbix监控平台环境部署
    2022学习进阶之路:高并发+性能优化+Spring boot等大型项目实战
    java基础之成员与局部变量以及this与static关键字[15]
    游戏里的猎头
  • 原文地址:https://blog.csdn.net/qq_33505109/article/details/126623549