本文为稀土技能社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!

前语

Jetpack Compose(简称 Compose )是 Google 官方推出的依据 Kotlin 语言的 Android 新一代 UI 开发结构,其选用声明式的 UI 编程特性使得 Android 应用界面的编写和保护变得愈加简略。

本专栏将详细介绍在运用 Compose 进行 UI 开发中怎么完结炫酷的动画作用。动画作用在 App 运用中至关重要,它使得 App 的交互愈加自然流通,用户运用体验愈加杰出。

在传统的 Android 开发中有古老的 View 动画和现在流行的特点动画,现在 View 动画简直已被广阔开发者所抛弃,特点动画因其能够作用于任何方针的灵敏和强壮特性而被开发者所拥抱。已然特点动画这么强壮,那么它是否能用在 Compose 开发中呢?假如能那跟传统 UI 开发中运用又有什么区别呢?本篇就带领你来探究一下在 Compose 中特点动画的运用。

运用探究

在传统 Android 开发中,特点动画运用得最多的是 ObjectAnimatorValueAnimator,接下来就探究一下在 Compose 中怎么运用它们来完结动画作用。

ObjectAnimator 运用探究

首要看一下在传统 Android 开发中怎么运用特点动画,比方运用特点动画完结竖直方向向下移动的动画:

val animator = ObjectAnimator.ofFloat(view, "translationY", 10f, 100f)
animator.start()

经过 ObjectAnimator作用于 View 的 translationY特点,不断改动 translationY 的值然后完结动画作用,一个很简略的特点动画,这儿就不贴运转作用了。

那在 Compose 中能否运用 ObjectAnimator 呢?

下面运用 Compose 在界面上显现一个 100dp*100dp 的蓝色正方形方块,代码如下:

class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      Box(Modifier.padding(start = 10.dp, top = 10.dp)
         .size(100.dp)
         .background(Color.Blue)
       )
     }
   }
}

运转作用如下:

Android Compose 动画使用详解(一) Compose 中属性动画的使用探索

现在要相同完结一个竖直方向移动的动画作用,让方块从上往下移动。在上面的特点动画完结中 ObjectAnimator是作用于 View 组件上的,按照这个思路在这儿 ObjectAnimator 就应该作用于 Box 上,但实践上咱们这儿压根拿不到 Box 的实例,因为这儿的 Box 实践是一个函数且没有回来值,看一下 Box 的源码:

@Composable
fun Box(modifier: Modifier) {
  Layout({}, measurePolicy = EmptyBoxMeasurePolicy, modifier = modifier)
}

已然作用于 Box 上不可,那能不能作用于 State 上呢,Compose 是数据驱动 UI 改写,经过数据状况改动重组 UI 完结界面的改写,把上面的 top 提取为一个 State 再经过 ObjectAnimator 去改动是否可行呢?改造代码实验一下:

class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    
    val topPadding:MutableState<Int> = mutableStateOf(10)
    
    val animator = ObjectAnimator.ofInt(topPadding, "value", 10, 100)
    animator.duration = 1000
    
    setContent {
      Box(Modifier.padding(start = 10.dp, top = topPadding.value.dp)
         .size(100.dp)
         .background(Color.Blue)
        // 增加点击事情
         .clickable {
          // 发动动画
          animator.start()
         }
       )
     }
   }
}

改造如下:

  1. 将之前 top 的固定值提取成了一个 State 变量 topPadding,当 topPadding 的值发生改动时会重组界面然后让界面改写
  2. 声明了 ObjectAnimator 的 animator 变量,作用于 topPadding 的 value 特点上,并设置动画值从 10 到 100,动画时长 1000ms
  3. 给 Box 增加点击监听事情发动动画

实践上写完这段代码,编辑器就现已有报错提示了,提示如下:

Android Compose 动画使用详解(一) Compose 中属性动画的使用探索

说没有找到带 Int 参数的 setValue办法,那来看看 MutableState是否有 setValue 办法:

interface MutableState<T> : State<T> {
  override var value: T
  operator fun component1(): T
  operator fun component2(): (T) -> Unit
}

能够发现 MutableState 中是有一个 var 修饰的 value 变量的,阐明是有 setValue 办法的,但是错误提示是找不到带 Int 参数的 setValue 办法,实践上 MutableState 的 setValue 的界说应该是这样的:

fun setValue(value:T){
  this.value = value
}

这儿参数类型是泛型 T,而 ObjectAnimator 找的是清晰的 Int 类型参数的办法,所以找不到。那怎么办呢?是不是就意味着在 Compose 中无法运用 ObjectAnimator 了呢?

直接运用的确是不可,那咱们能不能对其进行封装,不是找不到对应的 setValue 办法嘛,那我封装一下供给一个 setValue 办法不就行了。界说一个 IntState类,再供给一个 mutableIntStateOf办法:

class IntState(private val state: MutableState<Int>){
  var value : Int = state.value
    get() = state.value
    set(value) {
      field = value
      state.value = value
     }
}
​
fun mutableIntStateOf(value: Int, policy: SnapshotMutationPolicy<Int> = structuralEqualityPolicy()) : IntState{
  val state = mutableStateOf(value, policy)
  return IntState(state)
​
}

IntState结构办法传入一个 MutableState 类型的 state 参数,然后供给一个 value 变量,get 办法回来 state.value ,set 办法将传入值设置给 state.value,这样 IntState 就有了一个清晰的 setValue(value:Int) 的办法。

为了便于运用,封装一个 mutableIntStateOf办法,完结里先选用 Compose 供给的 mutableStateOf 办法获取一个 MutableState ,然后用其构建一个 IntState 进行回来。

再改造一下上面动画完结代码将 mutableStateOf替换成 mutableIntStateOf

class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
​
    // 替换为 mutableIntStateOf
    val topPadding = mutableIntStateOf(10)
​
    // 创立 ObjectAnimator 方针为 topPadding,作用特点为 value,值从 10 改动到 100
    val animator = ObjectAnimator.ofInt(topPadding, "value", 10, 100)
    // 设置动画时长 1s
    animator.duration = 1000
    
    setContent {
      Box(Modifier.padding(start = 10.dp, top = topPadding.value.dp)
         .size(100.dp)
         .background(Color.Blue)
        // 增加点击事情
         .clickable {
          // 发动动画
          animator.start()
         }
       )
     }
   }
}

现在不报错了,运转一下看看是否有动画作用:

Android Compose 动画使用详解(一) Compose 中属性动画的使用探索

作用符合预期,阐明这种办法是可行,也阐明 ObjectAnimator 在 Compose 中也是能够运用的,仅仅不能像传统 Android 开发那样直接作用于 View 组件上,而是需求进行二次封装后运用。

ValueAnimator 运用探究

ObjectAnimator 运用探究完了,那么 ValueAnimator能否运用呢?Compose 以声明式的方式经过数据驱动界面改写,而ValueAnimator首要用于数据的改动,如同很符合的样子,运用 ValueAnimator 不断改动 State 的值理论上就能够完结动画作用。还是上面的例子,改造成运用 ValueAnimator来完结:

class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // 运用 mutableStateOf 创立 topPadding 的 State
    var topPadding by mutableStateOf(10)
    // 创立 ValueAnimator 从 10 改动到 100
    val animator = ValueAnimator.ofInt(10, 100)
    // 动画时长 1s
    animator.duration = 1000
    // 设置监听,当动画改动时动态修正 topPadding 的值
    animator.addUpdateListener {
      topPadding = it.animatedValue as Int
     }
    setContent {
      Box(Modifier.padding(start = 10.dp, top = topPadding.dp)
         .size(100.dp)
         .background(Color.Blue)
         .clickable {
          animator.start()
         }
       )
     }
   }
}

是否有作用呢?运转一下看看作用:

Android Compose 动画使用详解(一) Compose 中属性动画的使用探索

跟上面运用 ObjectAnimator 完结的作用共同,阐明 ValueAnimator 在 Compose 中完结动画是可行的,仅仅需求手动去监听 ValueAnimator 值的改动然后去动态更新 State 的值,稍微麻烦了一点,实践上咱们也能够对其进行封装简化其运用。

经过上面的代码发现,假如要在 Compose 中运用 ValueAnimator 来完结动画,对动画数值的改动进行监听并动态更新 State 的值是必不可少的一步,那么咱们就能够将其提取进行封装。

/**
 * @param state 动画作用的方针 State
 * @param values 动画的改动值,可变参数
 */
fun animatorOfInt(state:MutableState<Int>, vararg values: Int) : ValueAnimator{
  // 创立 ValueAnimator ,参数为传入的 values
  val animator = ValueAnimator.ofInt(*values)
  // 增加监听
  animator.addUpdateListener {
    // 更新 state 的 value 值
    state.value = it.animatedValue as Int
   }
  return animator
}

然后将上面的创立动画替换成运用 animatorOfInt 创立:

class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    
    val topPadding = mutableStateOf(10)
​
    // 运用封装的 animatorOfInt 办法创立动画
    val animator = animatorOfInt(topPadding, 10, 100)
    animator.duration = 1000
​
    setContent {
      Box(Modifier.padding(start = 10.dp, top = topPadding.value.dp)
         .size(100.dp)
         .background(Color.Blue)
         .clickable {
          animator.start()
         }
       )
     }
   }
}

运用是不是要简略许多,不需求手动去处理动画值改动的监听了,有点运用 ObjectAnimator 的感觉,仅仅不需求指定方针特点。运转作用跟上面共同就不贴图了。

Compose 函数中运用特点动画

前面在 Compose 中运用的动画都是创立在 Compose 函数外面的,假如咱们想把这个组件封装成一个独立的 Compose 组件就需求将动画的创立放到 Compose 函数里面,比方将上面的作用封装成一个 AnimationBox组件:

@Composable
fun AnimationBox(){
  val topPadding = mutableStateOf(10) 
  val animator = animatorOfInt(topPadding, 10, 100)
  animator.duration = 1000
  Box(modifier = Modifier.padding(start = 10.dp, top = topPadding.value.dp)
     .size(100.dp)
     .background(Color.Blue)
     .clickable {
      animator.start()
     })
}

首要 mutableStateOf 会报错:

Android Compose 动画使用详解(一) Compose 中属性动画的使用探索

意思是在组合过程中创立 state 需求运用 remember,原因是当 state 里的值发生改动时 Compose 会进行重组导致函数重新履行,假如 mutableStateOf 不加 remember则会每次重组都重新创立 state,导致 UI 上运用的值每次都是初始值而得不到改写。

已然报错那就给他加上 remember:

@Composable
fun AnimationBox(){
  val topPadding = remember { mutableStateOf(10) }
   ...
}

然后在运用的地方直接运用 AnimationBox() 即可:

setContent {
  AnimationBox()
}

运转后发现作用跟之前相同,那是不是就能够了呢?

实践上上面的代码是还存在问题的,前面说在 Compose 重组时会重新履行 Compose 组件的代码,也就是在界面改写时会屡次重复创立动画方针,咱们在 animatorOfInt 函数里增加一个日志再看看运转时的日志输出:

fun animatorOfInt(state:MutableState<Int>, vararg values: Int) : ValueAnimator{
  println("-------call animatorOfInt--------")
  ...
}

输出成果:

I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------

日志的确输出了屡次,意味着动画的确创立了屡次,那怎么处理呢?

前面说了 remember能够处理重组时重复创立的问题,所以只需在创立动画上套上 remember即可,如下:

val animator = remember { animatorOfInt(topPadding, 10, 100) }

修正后再看日志,发现就只在第一次进行了创立,动画履行过程中并没有再次创立。

为了方便运用,能够再封装一个 rememberAnimatorOfInt办法:

@Composable
fun rememberAnimatorOfInt(state:MutableState<Int>, vararg values: Int) : ValueAnimator{
  return remember { animatorOfInt(state, *values) }
}

在 animatorOfInt 上套了一个 remember,这样运用时就能够直接运用 rememberAnimatorOfInt 办法:

val animator = rememberAnimatorOfInt(topPadding, 10, 100)

remember 是 Compose 供给的在 Compose 函数中缓存状况的办法,处理在 Compose 重组时重复创立的问题,关于 remember 更多运用大家能够自行查询相关资料,本专栏首要讲解动画的运用就不过多赘述。

实战

前面介绍了特点动画在 Compose 中的运用,那在实践开发中究竟好不好用呢?接下来咱们经过一个实例来看看。

先看一下最终完结的作用:

Android Compose 动画使用详解(一) Compose 中属性动画的使用探索

一个上传按钮的动画作用,动画首要分为三阶段:

  1. 上传开端
  • 按钮从圆角矩形变成圆形
  • 按钮色彩从蓝色变成中间白色,边框灰色
  • 文字逐步消失
  1. 上传进展
  • 边框依据进展变为蓝色
  1. 上传完结
  • 按钮从圆形变成圆角矩形
  • 按钮色彩变成赤色
  • 文字逐步显现,且文字变为 “Success”

上传开端动画

先把按钮的初始状况运用 Compose 完结:

@Composable
fun UploadButton() {
  Box(
    modifier = Modifier
       .padding(start = 10.dp, top = 10.dp)
       .width(180.dp),
    contentAlignment = Alignment.Center
   ) {
    Box(
      modifier = Modifier
         .clip(RoundedCornerShape(24.dp))
         .background(Color.Blue)
         .size(180.dp, 48.dp),
      contentAlignment = Alignment.Center,
     ) {
      Text("Upload", color = Color.White)
     }
   }
}

运转作用如下:

Android Compose 动画使用详解(一) Compose 中属性动画的使用探索

下面就为这按钮增加动画,前面讲了动画首要作用于 State 上,所以需求先将运用到的数据提取成对应的状况:

@Composable
fun UploadButton() {
  val originWidth = 180.dp
  val circleSize = 48.dp
  var text by remember { mutableStateOf("Upload") }
  val textAlpha = remember { mutableStateOf(1.0f) }
  val backgroundColor = remember { mutableStateOf(Color.Blue) }
  val boxWidth = remember { mutableStateOf(originWidth) }
​
  Box(
    modifier = Modifier
       .padding(start = 10.dp, top = 10.dp)
       .width(originWidth),
    contentAlignment = Alignment.Center
   ) {
    Box(
      modifier = Modifier
         .clip(RoundedCornerShape(height/2))
         .background(backgroundColor.value)
         .size(boxWidth.value, height),
      contentAlignment = Alignment.Center,
     ) {
      Text(text, color = Color.White, modifier = Modifier.alpha(textAlpha.value))
     }
   }
}

创立开端上传的动画:

@Composable
fun UploadButton() {
  ...
  val uploadStartAnimator = remember {
    // 创立 AnimatorSet
    val animatorSet = AnimatorSet()
    // 按钮宽度改动动画
    val widthAnimator = animatorOfDp(boxWidth, arrayOf(originWidth, circleSize))
    // 文字消失动画
    val textAnimator = animatorOfFloat(textAlpha, 1f, 0.0f)
    // 按钮色彩动画
    val colorAnimator = animatorOfColor(backgroundColor, arrayOf(Color.Blue, Color.Gray))
    // 动画增加到 AnimatorSet
    animatorSet.playTogether(widthAnimator, textAnimator, colorAnimator)
    animatorSet
   }
​
  Box(...) {
    Box(
      modifier = Modifier
         ...
         .clickable {
          // 点击履行动画
          uploadStartAnimator.start()
         },
       ...
     )
   }
}

分别创立按钮宽度、按钮色彩和文字 alpha 值改动的动画,因需一起履行多个动画,这儿运用 AnimatorSet 进行一起履行,然后在按钮上增加点击事情进行动画履行。

上面的 animatorOfDpanimatorOfFloatanimatorOfColor都是自界说封装的函数,封装办法与上面介绍的 animatorOfInt根本相同,源码可经过文章最终附的源码地址进行检查。

运转作用如下:

Android Compose 动画使用详解(一) Compose 中属性动画的使用探索

如同还差点,中间应该是白色的,在 Box 下再增加一个白色圆形的 Box,默许 alpha 是 0,上传开端时 alpha 从 0 变成 1 :

@Composable
fun UploadButton() {
   ...
  val progressAlpha = remember { mutableStateOf(0.0f) }
​
  val uploadStartAnimator = remember {
     ...
    // 中间白色透明度改动动画
    val centerAlphaAnimator = animatorOfFloat(progressAlpha, 0.0f, 1f)
    animatorSet.playTogether(widthAnimator, textAnimator, colorAnimator, centerAlphaAnimator)
    animatorSet
   }
​
  Box(...) {
    Box(...) {
      // 白色圆形
      Box(
        modifier = Modifier.size(40.dp).clip(RoundedCornerShape(20.dp))
           .alpha(progressAlpha.value).background(Color.White)
       )
      Text(text, color = Color.White, modifier = Modifier.alpha(textAlpha.value))
     }
   }
}

运转作用如下:

Android Compose 动画使用详解(一) Compose 中属性动画的使用探索

上传进展动画

这儿经过自界说 clip 的一个弧形的 shape 来完结进展,自界说代码如下:

class ArcShape(private val progress: Int) : Shape {override fun createOutline(
    size: Size,
    layoutDirection: LayoutDirection,
    density: Density
   ): Outline {
    val path = Path().apply {
      moveTo(size.width / 2f, size.height / 2f)
      arcTo(Rect(0f, 0f, size.width, size.height), -90f, progress / 100f * 360f, false)
      close()
     }
    return Outline.Generic(path)
   }
}

传入一个进展值(0-100),然后依据进展值算出一个绘制的弧度,运用这个自界说的 ArcShape 代码如下:

 Box(Modifier.size(48.dp).clip(ArcShape(30)).background(Color.Blue))

作用:

Android Compose 动画使用详解(一) Compose 中属性动画的使用探索

所以只需动态改动 ArcShape 的 progress 参数的值就能完结上传进展作用,修正代码如下:

@Composable
fun PreviewUploadButton() {
   ...
  val progress = remember { mutableStateOf(0) }
​
  //上传进展动画
  val progressAnimator = remember {
    val animator = animatorOfInt(progress, 0, 100)
    animator.duration = 1000
    animator
   }
​
  val uploadStartAnimator = remember {
     ...
    // 增加动画监听,完结后履行进展动画
    animatorSet.addListener(onEnd = {
      progressAnimator.start()
     })
    animatorSet
   }
​
  Box(...) {
    Box(...) {
      // 进展 Box
      Box(
        modifier = Modifier.size(height).clip(ArcShape(progress.value))
           .alpha(progressAlpha.value).background(Color.Blue)
       )
       ...
     }
   }
}

运转作用:

Android Compose 动画使用详解(一) Compose 中属性动画的使用探索

上传完结动画

最终是上传完结动画就很简略了,根本就是开端动画的反向,仅仅按钮色彩从蓝色变成了赤色,动画在上传进展动画完结时履行:

@Composable
fun PreviewUploadButton() {
   ...
  
  val endAnimatorSet = remember {
    val animatorSet = AnimatorSet()
    val widthAnimator = animatorOfDp(boxWidth, arrayOf(circleSize, originWidth))
    val centerAnimator = animatorOfFloat(progressAlpha, 1f, 0f)
    val textAnimator = animatorOfFloat(textAlpha, 0f, 1f)
    val colorAnimator = animatorOfColor(backgroundColor, arrayOf(Color.Blue, Color.Red))
    animatorSet.playTogether(widthAnimator, centerAnimator, textAnimator, colorAnimator)
    animatorSet.addListener(onStart = {
      text = "Success"
     })
    animatorSet
   }
​
  val progressAnimator = remember {
    val animator = animatorOfInt(progress, 0, 100)
    animator.duration = 1000
    animator.addListener(onEnd = {
      endAnimatorSet.start()
     })
    animator
   }
​
   ...
}

最终作用:

Android Compose 动画使用详解(一) Compose 中属性动画的使用探索

最终

经过本篇文章的探究能够发现特点动画在 Compose 中的确是能够运用的,虽然跟传统 UI 开发中运用特点动画有所区别,但的确能用,并且经过一个简略的实战示例发现如同还挺好用的。好了,我现已学会 Compose 的动画开发了,什么?Compose 还单独供给了一套动画 API ?,特点动画这不是挺好使的么,这不是多此一举么,难道 Compose 的动画 API 比特点动画还好用、还强壮?假如感兴趣请关注本专栏,从下一篇开端带你真正走进 Compose 的动画国际。

源码地址:ComposeAnimationDemo