MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Kotlin Android动画与过渡

2024-05-184.6k 阅读

Kotlin 中的 Android 动画基础

1. 视图动画(View Animation)

视图动画主要包括四种类型:平移(Translate)、缩放(Scale)、旋转(Rotate)和透明度(Alpha)。在 Kotlin 中,我们可以通过 Animation 类及其子类来实现这些动画。

平移动画示例

val translateAnimation = TranslateAnimation(
    Animation.RELATIVE_TO_SELF, 0f,
    Animation.RELATIVE_TO_SELF, 0.5f,
    Animation.RELATIVE_TO_SELF, 0f,
    Animation.RELATIVE_TO_SELF, 0f
)
translateAnimation.duration = 1000
view.startAnimation(translateAnimation)

上述代码实现了一个水平方向上从自身位置平移到自身位置 50% 处的动画,时长为 1000 毫秒。

缩放动画示例

val scaleAnimation = ScaleAnimation(
    1f, 2f,
    1f, 2f,
    Animation.RELATIVE_TO_SELF, 0.5f,
    Animation.RELATIVE_TO_SELF, 0.5f
)
scaleAnimation.duration = 1000
view.startAnimation(scaleAnimation)

此代码创建了一个以视图中心为基准,水平和垂直方向都从 1 倍放大到 2 倍的缩放动画,时长同样为 1000 毫秒。

旋转动画示例

val rotateAnimation = RotateAnimation(
    0f, 360f,
    Animation.RELATIVE_TO_SELF, 0.5f,
    Animation.RELATIVE_TO_SELF, 0.5f
)
rotateAnimation.duration = 1000
view.startAnimation(rotateAnimation)

这是一个围绕视图中心旋转 360 度的动画,时长 1000 毫秒。

透明度动画示例

val alphaAnimation = AlphaAnimation(1f, 0f)
alphaAnimation.duration = 1000
view.startAnimation(alphaAnimation)

这段代码实现了一个从完全不透明(1f)到完全透明(0f)的透明度动画,时长 1000 毫秒。

2. 帧动画(Frame Animation)

帧动画是通过按顺序显示一系列图像来创建动画效果。在 Kotlin 中,我们需要在 res/drawable 目录下创建一个 XML 文件来定义帧动画。

首先,在 res/drawable 目录下创建 frame_animation.xml 文件:

<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">
    <item android:drawable="@drawable/frame1" android:duration="100" />
    <item android:drawable="@drawable/frame2" android:duration="100" />
    <item android:drawable="@drawable/frame3" android:duration="100" />
</animation-list>

然后在 Kotlin 代码中使用这个帧动画:

val imageView = findViewById<ImageView>(R.id.image_view)
imageView.setBackgroundResource(R.drawable.frame_animation)
val frameAnimation = imageView.background as AnimationDrawable
frameAnimation.start()

这里创建了一个循环播放的帧动画,每帧显示时长为 100 毫秒。

属性动画(Property Animation)

1. ObjectAnimator

ObjectAnimator 是属性动画中最常用的类之一,它可以对对象的某个属性进行动画操作。例如,我们可以对视图的 translationX 属性进行动画,实现视图的水平移动。

示例

val objectAnimator = ObjectAnimator.ofFloat(view, "translationX", 0f, 300f)
objectAnimator.duration = 1000
objectAnimator.start()

这段代码让视图在 1000 毫秒内从初始位置水平移动到 translationX 为 300 的位置。

我们还可以对多个属性同时进行动画操作,比如同时改变视图的 translationXtranslationY

val propertyValuesHolderX = PropertyValuesHolder.ofFloat("translationX", 0f, 300f)
val propertyValuesHolderY = PropertyValuesHolder.ofFloat("translationY", 0f, 300f)
val animatorSet = AnimatorSet()
animatorSet.playTogether(
    ObjectAnimator.ofPropertyValuesHolder(view, propertyValuesHolderX),
    ObjectAnimator.ofPropertyValuesHolder(view, propertyValuesHolderY)
)
animatorSet.duration = 1000
animatorSet.start()

这样,视图会在 1000 毫秒内同时在水平和垂直方向移动 300 像素。

2. ValueAnimator

ValueAnimator 主要用于产生一个值的动画,它并不直接作用于对象的属性,而是产生一个随时间变化的值,我们可以在 ValueAnimatorAnimatorUpdateListener 中根据这个值来手动更新对象的属性。

示例

val valueAnimator = ValueAnimator.ofFloat(0f, 1f)
valueAnimator.duration = 1000
valueAnimator.addUpdateListener { animation ->
    val fraction = animation.animatedValue as Float
    view.alpha = fraction
}
valueAnimator.start()

上述代码中,ValueAnimator 产生一个从 0 到 1 的值,通过 AnimatorUpdateListener,我们将这个值赋给视图的透明度属性,从而实现视图从完全透明到完全不透明的动画效果。

Android 过渡动画

1. 活动过渡(Activity Transitions)

在 Android 中,我们可以为活动之间的切换添加过渡动画。Kotlin 提供了简洁的方式来实现这一点。

首先,在 styles.xml 文件中定义过渡风格:

<style name="AppTheme.Transition" parent="Theme.MaterialComponents.NoActionBar">
    <item name="android:windowEnterTransition">@transition/slide_right</item>
    <item name="android:windowExitTransition">@transition/slide_left</item>
    <item name="android:windowReenterTransition">@transition/slide_right</item>
    <item name="android:windowReturnTransition">@transition/slide_left</item>
</style>

这里定义了进入、退出、重新进入和返回活动时的过渡动画。假设 slide_right.xmlslide_left.xml 分别是从右侧滑入和从左侧滑出的过渡动画 XML 文件。

然后在 AndroidManifest.xml 中应用这个风格:

<activity
    android:name=".SecondActivity"
    android:theme="@style/AppTheme.Transition">
</activity>

这样,当启动 SecondActivity 时,就会应用定义好的过渡动画。

2. 共享元素过渡(Shared Element Transitions)

共享元素过渡允许我们在两个活动之间共享某些元素,使它们的过渡更加流畅。

例如,我们有一个图片在第一个活动中,点击后在第二个活动中放大显示。

在第一个活动布局中,为图片添加 transitionName

<ImageView
    android:id="@+id/image_view"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/image"
    android:transitionName="shared_image" />

在第二个活动布局中,同样为图片添加相同的 transitionName

<ImageView
    android:id="@+id/enlarged_image_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:src="@drawable/image"
    android:transitionName="shared_image" />

在第一个活动中启动第二个活动时,使用 ActivityOptionsCompat 来设置共享元素过渡:

val intent = Intent(this, SecondActivity::class.java)
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
    this,
    findViewById<View>(R.id.image_view),
    "shared_image"
)
startActivity(intent, options.toBundle())

在第二个活动中,设置进入过渡:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_second)
    postponeEnterTransition()
    enlargedImageView.viewTreeObserver.addOnPreDrawListener(
        ViewTreeObserver.OnPreDrawListener {
            startPostponedEnterTransition()
            true
        }
    )
}

这样,图片在两个活动之间的过渡就会非常流畅,给用户带来更好的体验。

动画的高级应用

1. 动画的组合与嵌套

在实际应用中,我们常常需要将多个动画组合起来,或者在一个动画中嵌套其他动画,以实现更复杂的效果。

例如,我们可以创建一个组合动画,让视图先进行旋转,然后再进行平移:

val rotateAnimator = ObjectAnimator.ofFloat(view, "rotation", 0f, 360f)
rotateAnimator.duration = 1000

val translateAnimator = ObjectAnimator.ofFloat(view, "translationX", 0f, 300f)
translateAnimator.duration = 1000
translateAnimator.startDelay = 1000

val animatorSet = AnimatorSet()
animatorSet.playSequentially(rotateAnimator, translateAnimator)
animatorSet.start()

这里,AnimatorSet 实现了动画的顺序播放,先旋转 1 秒,然后延迟 1 秒后开始平移 1 秒。

我们还可以进行动画的嵌套,比如在一个 AnimatorSet 中再包含其他 AnimatorSet

val innerAnimatorSet1 = AnimatorSet()
val scaleXAnimator = ObjectAnimator.ofFloat(view, "scaleX", 1f, 2f)
val scaleYAnimator = ObjectAnimator.ofFloat(view, "scaleY", 1f, 2f)
innerAnimatorSet1.playTogether(scaleXAnimator, scaleYAnimator)
innerAnimatorSet1.duration = 1000

val innerAnimatorSet2 = AnimatorSet()
val alphaAnimator = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f)
val translationYAnimator = ObjectAnimator.ofFloat(view, "translationY", 0f, 300f)
innerAnimatorSet2.playTogether(alphaAnimator, translationYAnimator)
innerAnimatorSet2.duration = 1000
innerAnimatorSet2.startDelay = 1000

val outerAnimatorSet = AnimatorSet()
outerAnimatorSet.playSequentially(innerAnimatorSet1, innerAnimatorSet2)
outerAnimatorSet.start()

这段代码实现了先对视图进行缩放,然后延迟 1 秒后进行透明度降低和垂直平移的复杂动画效果。

2. 基于物理效果的动画

Android 提供了 SpringAnimation 类来实现基于物理效果的动画,比如弹性效果。

示例

val springAnimation = SpringAnimation(view, SpringForce.TranslationX, 300f)
springAnimation.spring.dampingRatio = SpringForce.DAMPING_RATIO_HIGH_BOUNCY
springAnimation.spring.stiffness = SpringForce.STIFFNESS_LOW
springAnimation.start()

上述代码让视图以高弹性和低刚度的物理特性移动到 translationX 为 300 的位置,给人一种弹性的动画效果。

我们还可以结合 ValueAnimatorSpringForce 来实现更灵活的物理效果动画:

val springForce = SpringForce()
springForce.dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY
springForce.stiffness = SpringForce.STIFFNESS_MEDIUM

val valueAnimator = ValueAnimator.ofFloat(0f, 1f)
valueAnimator.duration = 3000
valueAnimator.addUpdateListener { animation ->
    val fraction = animation.animatedFraction
    springForce.updatePosition(fraction)
    view.translationX = springForce.currentPosition * 300
}
valueAnimator.start()

这段代码通过 ValueAnimator 来更新 SpringForce 的位置,从而实现视图基于物理效果的移动,并且可以通过 ValueAnimator 的时长和更新逻辑来灵活控制动画的节奏。

动画性能优化

1. 避免过度绘制

过度绘制是指在屏幕的同一区域绘制多次。在动画中,如果不注意,很容易出现过度绘制的情况,导致性能下降。

我们可以通过 Android 开发者选项中的“显示过度绘制区域”来查看应用中的过度绘制情况。

为了避免过度绘制,我们应该尽量减少不必要的视图层级,使用 clipToPaddingclipChildren 属性来限制绘制区域。

例如,如果一个布局中有多个重叠的视图,我们可以通过调整布局结构,减少重叠部分,或者使用 ViewStub 来延迟加载不常用的视图。

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipChildren="false"
    android:clipToPadding="false">
    <ImageView
        android:id="@+id/image_view1"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/image1" />
    <ImageView
        android:id="@+id/image_view2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/image2"
        android:layout_centerInParent="true" />
</RelativeLayout>

在上述代码中,通过设置 clipChildrenclipToPaddingfalse,我们可以确保子视图不会因为父视图的 padding 或子视图之间的裁剪而出现不必要的绘制问题。

2. 合理使用动画时长和帧率

动画的时长和帧率对性能也有很大影响。如果动画时长过短,可能会导致动画过于急促,用户体验不佳;而如果时长过长,又可能会让用户觉得等待时间过长。

帧率方面,Android 系统的理想帧率是 60fps。如果动画的帧率不稳定,就会出现卡顿现象。

我们可以通过 ValueAnimatorsetFrameDelay 方法来控制帧率。例如:

val valueAnimator = ValueAnimator.ofFloat(0f, 1f)
valueAnimator.duration = 1000
valueAnimator.setFrameDelay(16) // 大约 60fps
valueAnimator.start()

在设置动画时长时,要根据具体的动画效果和用户体验来决定。对于一些简单的过渡动画,100 - 300 毫秒可能就足够了;而对于复杂的动画,可能需要 1000 毫秒甚至更长时间。

3. 使用硬件加速

Android 从 3.0 版本开始支持硬件加速,通过将图形渲染工作交给 GPU 来提高性能。

我们可以在 AndroidManifest.xml 文件中为整个应用启用硬件加速:

<application
    android:hardwareAccelerated="true"
    ...>
</application>

也可以为单个活动或视图启用硬件加速:

// 为活动启用硬件加速
activity.window.setFlags(
    WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
    WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
)

// 为视图启用硬件加速
view.setLayerType(View.LAYER_TYPE_HARDWARE, null)

启用硬件加速后,动画的渲染速度会明显提高,但也要注意一些兼容性问题,比如某些自定义视图在硬件加速下可能会出现绘制异常,这时可能需要禁用硬件加速或进行特殊处理。

与其他框架结合使用动画

1. 与 ConstraintLayout 结合

ConstraintLayout 是 Android 中常用的布局框架,它与动画结合可以实现非常灵活和强大的动画效果。

例如,我们可以通过改变 ConstraintLayout 中视图的约束条件来实现动画。假设我们有一个视图,初始时位于左上角,通过动画将其移动到右下角:

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <View
        android:id="@+id/moving_view"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="#FF0000"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

在 Kotlin 代码中,我们可以通过 ConstraintSet 来改变视图的约束:

val constraintLayout = findViewById<ConstraintLayout>(R.id.constraint_layout)
val constraintSet = ConstraintSet()
constraintSet.clone(constraintLayout)

val animator = ObjectAnimator.ofObject(
    constraintSet, "constraintSet",
    ConstraintSet.LayoutParamsAnimator(),
    constraintSet
)
animator.duration = 1000
animator.addUpdateListener {
    constraintSet.applyTo(constraintLayout)
}

constraintSet.connect(
    R.id.moving_view,
    ConstraintSet.END,
    ConstraintSet.PARENT_ID,
    ConstraintSet.END
)
constraintSet.connect(
    R.id.moving_view,
    ConstraintSet.BOTTOM,
    ConstraintSet.PARENT_ID,
    ConstraintSet.BOTTOM
)

animator.start()

这样,通过改变视图在 ConstraintLayout 中的约束,实现了视图从左上角移动到右下角的动画效果。

2. 与 RecyclerView 结合

RecyclerView 是 Android 中用于展示列表数据的常用组件,与动画结合可以为列表项的展示和删除等操作添加动画效果。

我们可以使用 RecyclerView.ItemAnimator 来为列表项添加动画。例如,为 RecyclerView 设置默认的动画:

val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.itemAnimator = DefaultItemAnimator()

如果我们想要自定义动画效果,可以继承 DefaultItemAnimator 类并实现自己的动画逻辑。

示例

class CustomItemAnimator : DefaultItemAnimator() {
    override fun animateRemove(holder: RecyclerView.ViewHolder): Boolean {
        val view = holder.itemView
        val animator = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f)
        animator.duration = 300
        animator.addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator) {
                dispatchRemoveFinished(holder)
            }
        })
        view.animate().alpha(0f).setDuration(300).withEndAction {
            dispatchRemoveFinished(holder)
        }
        return true
    }

    override fun animateAdd(holder: RecyclerView.ViewHolder): Boolean {
        val view = holder.itemView
        view.alpha = 0f
        val animator = ObjectAnimator.ofFloat(view, "alpha", 0f, 1f)
        animator.duration = 300
        animator.addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator) {
                dispatchAddFinished(holder)
            }
        })
        view.animate().alpha(1f).setDuration(300).withEndAction {
            dispatchAddFinished(holder)
        }
        return true
    }
}

然后在 RecyclerView 中使用这个自定义的 ItemAnimator

val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.itemAnimator = CustomItemAnimator()

这样,当列表项添加或删除时,就会应用我们自定义的动画效果,提升用户体验。

动画调试与错误处理

1. 动画调试工具

在开发动画过程中,我们可以使用一些调试工具来帮助我们分析动画的性能和问题。

Android Profiler 是一个非常强大的工具,它可以实时监测应用的 CPU、内存、网络等性能指标,对于动画调试也很有帮助。我们可以通过它查看动画过程中 CPU 的使用情况,是否存在帧率不稳定等问题。

另外,Log 日志也是一个常用的调试手段。我们可以在动画的关键节点添加 Log 输出,比如在 AnimatorListener 的各个回调方法中输出日志,以了解动画的执行流程。

示例

val animator = ObjectAnimator.ofFloat(view, "translationX", 0f, 300f)
animator.duration = 1000
animator.addListener(object : AnimatorListenerAdapter() {
    override fun onAnimationStart(animation: Animator) {
        Log.d("AnimationDebug", "Animation started")
    }

    override fun onAnimationEnd(animation: Animator) {
        Log.d("AnimationDebug", "Animation ended")
    }

    override fun onAnimationCancel(animation: Animator) {
        Log.d("AnimationDebug", "Animation cancelled")
    }
})
animator.start()

通过这些日志输出,我们可以清楚地知道动画何时开始、结束或被取消,有助于排查动画过程中的逻辑问题。

2. 常见动画错误及解决方法

动画不显示

  • 原因:可能是动画没有正确绑定到视图,或者动画的起始和结束值设置不合理,导致动画效果不可见。
  • 解决方法:检查 startAnimationObjectAnimator.start() 等方法是否正确调用,以及动画的属性值设置是否符合预期。例如,在透明度动画中,如果起始和结束透明度值相同,动画就不会有效果。

动画卡顿

  • 原因:可能是过度绘制、动画帧率不稳定、硬件加速未启用等原因导致。
  • 解决方法:使用前面提到的性能优化方法,如减少过度绘制、合理设置动画帧率、启用硬件加速等。同时,检查动画逻辑中是否存在复杂的计算或频繁的内存分配,这些都可能导致卡顿。

动画与布局冲突

  • 原因:当动画改变视图的位置、大小等属性时,可能与布局的约束或其他视图产生冲突。
  • 解决方法:仔细检查布局结构和动画逻辑,确保动画操作不会破坏布局的稳定性。例如,在使用 ConstraintLayout 时,改变视图的约束要注意与其他视图的关系,避免出现重叠或布局错乱的情况。

通过合理使用调试工具和正确解决常见错误,我们可以开发出更加流畅和稳定的动画效果,提升应用的用户体验。在 Kotlin 的 Android 开发中,动画与过渡效果是提升应用交互性和美观度的重要手段,掌握这些技术并不断优化,可以让我们的应用在众多竞品中脱颖而出。