学习 Android(七)动画

137 阅读14分钟

简介

在Android开发中,动画是提升用户体验的重要工具。Android提供了多种动画框架,适用于不同场景和需求,接下来,本章节将会详细的讲解。

1.补间动画(Tween Animation)

  • 补间动画的核心概念

    补间动画通过 XML或代码 定义动画的其实和结束状态,系统 自动计算 中间过渡帧。

    其支持的四种类型:平移(Translate)旋转(Rotate)缩放(Scale)透明度(Alpha)

    需要注意的是,补间动画仅改变 View 的绘制效果,不改变实际属性(如点击事件位置不变)。

  • 属性详解

    • 通用属性(所有动画类型共有,之后不在重复赘述)

      • android:duration :动画持续时间(单位:毫秒)。
      • android:startOffset :延迟开始时间(单位:毫秒)。
      • android:fillAfter :动画结束后是否保存最终状态(true/false)。
      • android:fillBefore :动画结束后是否回到初始状态(默认true)。
      • android:interpolator :插值器(控制动画速度变化,如加速、减速)。
    • 各类型动画持有属性

      • 平移(translate)

        android:fromXDelta="0%"   // 从自身宽度的 0% 位置开始(原始位置)
        android:toXDelta="100%"   // 移动到自身宽度的 100% 位置(向右移动一个自己宽度)
        android:fromYDelta="0%"   // 从自身高度的 0% 开始(原始位置)
        android:toYDelta="50%"    // 移动到自身高度的 50% 位置(向下移动自己高度的一半)
        
      • 旋转(Rotate)

        android:fromDegrees="0"    // 起始角度
        android:toDegrees="360"    // 结束角度
        android:pivotX="50%"       // 旋转中心X(相对于View自身)
        android:pivotY="50%"       // 旋转中心Y
        
      • 缩放(Scale)

        android:fromXScale="1.0"   // 起始 X 轴缩放比例为 1.0(即正常宽度)
        android:toXScale="2.0"     // 最终 X 轴缩放比例为 2.0(宽度变为原来的 2 倍)
        android:fromYScale="1.0"   // 起始 Y 轴缩放比例为 1.0(即正常高度)
        android:toYScale="0.5"     // 最终 Y 轴缩放比例为 0.5(高度变为原来的一半)
        android:pivotX="50%"       // 缩放中心点 X,50% 表示 View 自身宽度的中心
        android:pivotY="50%"       // 缩放中心点 Y,50% 表示 View 自身高度的中心
        
        
      • 透明度(Alpha)

        android:fromAlpha="1.0"    // 起始透明度:1.0,表示完全不透明
        android:toAlpha="0.0"      // 最终透明度:0.0,表示完全透明
        
  • 示例

    • XML 定义动画

      <?xml version="1.0" encoding="utf-8"?>
      <set xmlns:android="http://schemas.android.com/apk/res/android"
          android:duration="2000"
          android:fillAfter="true"
          android:interpolator="@android:anim/accelerate_decelerate_interpolator">
      
          <!-- 平移 -->
          <translate
              android:fromXDelta="0%"
              android:toXDelta="50%"
              android:fromYDelta="0%"
              android:toYDelta="0%" />
      
          <!-- 旋转 -->
          <rotate
              android:fromDegrees="0"
              android:toDegrees="360"
              android:pivotX="50%"
              android:pivotY="50%" />
      
          <!-- 缩放 -->
          <scale
              android:fromXScale="1.0"
              android:toXScale="1.5"
              android:fromYScale="1.0"
              android:toYScale="1.5"
              android:pivotX="50%"
              android:pivotY="50%" />
      
          <!-- 透明度 -->
          <alpha
              android:fromAlpha="1.0"
              android:toAlpha="0.5" />
      </set>
      
    • activity_main.xml

      <?xml version="1.0" encoding="utf-8"?>
      <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:app="http://schemas.android.com/apk/res-auto"
          xmlns:tools="http://schemas.android.com/tools"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:orientation="vertical"
          tools:context=".MainActivity">
      
          <Button
              android:id="@+id/btn_tween_animate"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="开启补间动画" />
      
          <ImageView
              android:id="@+id/iv_target"
              android:layout_width="100dp"
              android:layout_height="100dp"
              android:src="@drawable/ic_launcher_foreground" />
      
      </LinearLayout>
      
    • MainActivity

      class MainActivity : AppCompatActivity() {
          // 懒加载按钮视图:用于触发补间动画
          private val btnTweenAnimate by lazy { findViewById<Button>(R.id.btn_tween_animate) }
      
          // 懒加载图片视图:动画作用对象
          private val ivTarget by lazy { findViewById<ImageView>(R.id.iv_target) }
      
          override fun onCreate(savedInstanceState: Bundle?) {
              super.onCreate(savedInstanceState)
              setContentView(R.layout.activity_main)
      
              // 加载动画资源(位于 res/anim/anim_combined.xml)
              val animation = AnimationUtils.loadAnimation(this, R.anim.anim_combined)
      
              btnTweenAnimate.setOnClickListener {
                  // 给 ivTarget 启动动画(组合动画:位移、旋转、缩放、透明度)
                  ivTarget.startAnimation(animation)
              }
      
          }
      }
      

2. 属性动画(Propertry Animation)

  • 属性动画的核心概念

    属性动画通过动态修改对象的 属性值 (如坐标、透明度) 等实现动画,适用于任何独享(不限于View)。

    核心类

    • ValueAnimator :计算动画过程中的属性值,需要手动更新目标属性。
    • ObjectAnimator :直接操作目标对象的属性(自动更新)。
    • AnimatorSet :组合多个动画(顺序或并行执行)。
  • 示例

    • activity_main.xml

      <?xml version="1.0" encoding="utf-8"?>
      <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:tools="http://schemas.android.com/tools"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:orientation="vertical"
          tools:context=".MainActivity">
      
          <Button
              android:id="@+id/btn_property_animate"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="开启属性动画" />
      
          <TextView
              android:id="@+id/tv_target"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="Hello Anim!"
              android:textSize="24sp"
              android:background="#FF2196F3"
              android:padding="16dp" />
      
      </LinearLayout>
      
    • MainActivity

      class MainActivity : AppCompatActivity() {
          // 懒加载按钮视图:用于触发补间动画
          private val btnPropertyAnimate by lazy { findViewById<Button>(R.id.btn_property_animate) }
      
          // 懒加载图片视图:动画作用对象
          private val tvTarget by lazy { findViewById<TextView>(R.id.tv_target) }
      
          override fun onCreate(savedInstanceState: Bundle?) {
              super.onCreate(savedInstanceState)
              setContentView(R.layout.activity_main)
      
      
              btnPropertyAnimate.setOnClickListener {
                  startPropertyAnimation()
              }
          }
      
          private fun startPropertyAnimation() {
              // 水平移动动画:将 tvTarget 从 X=0 平移到 X=1000px
              val moveX = ObjectAnimator.ofFloat(tvTarget, "translationX", 0f, 1000f).apply {
                  duration = 3000L               // 动画持续时长:3 秒
                  repeatCount = 1                // 重复次数 = 1,表示除首次播放外再重复一次(总共播放两次)
                  repeatMode = ValueAnimator.REVERSE // 重复时使用反向模式:第二次会从 1000px 平移回 0px
              }
      
              // 透明度动画:将 tvTarget 的 alpha 从 1.0f 动画到 0.1f
              val alpha = ObjectAnimator.ofFloat(tvTarget, "alpha", 1f, 0.1f).apply {
                  duration = 3000L               // 持续 3 秒
                  repeatCount = 1                // 重复一次 → 共两次播放
                  repeatMode = ValueAnimator.REVERSE // 第二次反向播放:从 0.1f 动画回 1.0f
              }
      
              // 背景色动画:使用 ArgbEvaluator 在蓝色与红色之间进行过渡
              val colorAnima = ObjectAnimator.ofObject(
                  tvTarget,
                  "backgroundColor",
                  ArgbEvaluator(),               // 颜色插值器
                  Color.BLUE,                    // 起始颜色
                  Color.RED                      // 结束颜色
              ).apply {
                  duration = 3000L               // 持续 3 秒
                  repeatCount = 1                // 重复一次 → 共两次播放
                  repeatMode = ValueAnimator.REVERSE // 第二次反向播放:从红色过渡回蓝色
              }
      
              // 组合动画:并行执行上述三个动画
              AnimatorSet().apply {
                  playTogether(moveX, alpha, colorAnima) // 同时开始三个动画
                  start()                               // 启动动画
              }
          }
      
      }
      

3. 帧动画(Drawable Animation)

  • 帧动画的核心概念

    帧动画通过 逐帧播放图片序列(类似 GIF)实现动画效果。 适用场景:步骤引导、简单动作效果(如加载动画、游戏角色动作)。 特点

    • 实现简单,但图片资源较多时可能占用较高内存。
    • 适合图片数量少且尺寸小的动画。
  • 示例

    • res/drawble/anim_loading.xml 中定义动画:

      <?xml version="1.0" encoding="utf-8"?>
      <!-- res/drawable/anim_loading.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" />
          <item android:drawable="@drawable/frame4" android:duration="100" />
      </animation-list>
      
    • res/drawble/frame1.xml 中定义帧动画资源:

      <?xml version="1.0" encoding="utf-8"?>
      <vector xmlns:android="http://schemas.android.com/apk/res/android"
          android:width="100dp"
          android:height="200dp"
          android:viewportWidth="100"
          android:viewportHeight="200">
      
          <!-- 头部:圆形 -->
          <path
              android:strokeColor="#000000"
              android:strokeWidth="4"
              android:pathData="M50,20
                                A20,20 0 1,1 49.99,20Z" />
      
          <!-- 身体:竖线 -->
          <path
              android:strokeColor="#000000"
              android:strokeWidth="4"
              android:pathData="M50,40 L50,100" />
      
          <!-- 左臂:向左上45度 -->
          <path
              android:strokeColor="#000000"
              android:strokeWidth="4"
              android:pathData="M50,60 L20,10" />
      
          <!-- 右臂:向右上45度 -->
          <path
              android:strokeColor="#000000"
              android:strokeWidth="4"
              android:pathData="M50,60 L80,80" />
      
          <!-- 左腿:向左下 -->
          <path
              android:strokeColor="#000000"
              android:strokeWidth="4"
              android:pathData="M50,100 L30,150" />
      
          <!-- 右腿:向右下 -->
          <path
              android:strokeColor="#000000"
              android:strokeWidth="4"
              android:pathData="M50,100 L70,150" />
      
      </vector>
      
    • res/drawble/frame2.xml 中定义帧动画资源:

      <?xml version="1.0" encoding="utf-8"?>
      <vector xmlns:android="http://schemas.android.com/apk/res/android"
          android:width="100dp"
          android:height="200dp"
          android:viewportWidth="100"
          android:viewportHeight="200">
      
          <!-- 头部:圆形 -->
          <path
              android:strokeColor="#000000"
              android:strokeWidth="4"
              android:pathData="M50,20
                                A20,20 0 1,1 49.99,20Z" />
      
          <!-- 身体:竖线 -->
          <path
              android:strokeColor="#000000"
              android:strokeWidth="4"
              android:pathData="M50,40 L50,100" />
      
          <!-- 左臂:向左上45度 -->
          <path
              android:strokeColor="#000000"
              android:strokeWidth="4"
              android:pathData="M50,60 L20,20" />
      
          <!-- 右臂:向右上45度 -->
          <path
              android:strokeColor="#000000"
              android:strokeWidth="4"
              android:pathData="M50,60 L80,80" />
      
          <!-- 左腿:向左下 -->
          <path
              android:strokeColor="#000000"
              android:strokeWidth="4"
              android:pathData="M50,100 L30,150" />
      
          <!-- 右腿:向右下 -->
          <path
              android:strokeColor="#000000"
              android:strokeWidth="4"
              android:pathData="M50,100 L70,150" />
      
      </vector>
      
    • res/drawble/frame3.xml 中定义帧动画资源:

      <?xml version="1.0" encoding="utf-8"?>
      <vector xmlns:android="http://schemas.android.com/apk/res/android"
          android:width="100dp"
          android:height="200dp"
          android:viewportWidth="100"
          android:viewportHeight="200">
      
          <!-- 头部:圆形 -->
          <path
              android:strokeColor="#000000"
              android:strokeWidth="4"
              android:pathData="M50,20
                                A20,20 0 1,1 49.99,20Z" />
      
          <!-- 身体:竖线 -->
          <path
              android:strokeColor="#000000"
              android:strokeWidth="4"
              android:pathData="M50,40 L50,100" />
      
          <!-- 左臂:向左上45度 -->
          <path
              android:strokeColor="#000000"
              android:strokeWidth="4"
              android:pathData="M50,60 L20,30" />
      
          <!-- 右臂:向右上45度 -->
          <path
              android:strokeColor="#000000"
              android:strokeWidth="4"
              android:pathData="M50,60 L80,80" />
      
          <!-- 左腿:向左下 -->
          <path
              android:strokeColor="#000000"
              android:strokeWidth="4"
              android:pathData="M50,100 L30,150" />
      
          <!-- 右腿:向右下 -->
          <path
              android:strokeColor="#000000"
              android:strokeWidth="4"
              android:pathData="M50,100 L70,150" />
      
      </vector>
      
    • res/drawble/frame4.xml 中定义帧动画资源:

      <?xml version="1.0" encoding="utf-8"?>
      <vector xmlns:android="http://schemas.android.com/apk/res/android"
          android:width="100dp"
          android:height="200dp"
          android:viewportWidth="100"
          android:viewportHeight="200">
      
          <!-- 头部:圆形 -->
          <path
              android:strokeColor="#000000"
              android:strokeWidth="4"
              android:pathData="M50,20
                                A20,20 0 1,1 49.99,20Z" />
      
          <!-- 身体:竖线 -->
          <path
              android:strokeColor="#000000"
              android:strokeWidth="4"
              android:pathData="M50,40 L50,100" />
      
          <!-- 左臂:向左上45度 -->
          <path
              android:strokeColor="#000000"
              android:strokeWidth="4"
              android:pathData="M50,60 L20,40" />
      
          <!-- 右臂:向右上45度 -->
          <path
              android:strokeColor="#000000"
              android:strokeWidth="4"
              android:pathData="M50,60 L80,80" />
      
          <!-- 左腿:向左下 -->
          <path
              android:strokeColor="#000000"
              android:strokeWidth="4"
              android:pathData="M50,100 L30,150" />
      
          <!-- 右腿:向右下 -->
          <path
              android:strokeColor="#000000"
              android:strokeWidth="4"
              android:pathData="M50,100 L70,150" />
      
      </vector>
      
    • activity_main.xml

      <?xml version="1.0" encoding="utf-8"?>
      <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:tools="http://schemas.android.com/tools"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:orientation="vertical"
          tools:context=".MainActivity">
      
          <ImageView
              android:id="@+id/iv_loading"
              android:layout_width="100dp"
              android:layout_height="100dp"
              android:src="@drawable/anim_loading" /> <!-- 直接引用动画XML -->
      
          <Button
              android:id="@+id/btn_start"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="开始动画" />
      
          <Button
              android:id="@+id/btn_stop"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="停止动画" />
      
      </LinearLayout>
      
    • MainActivity

      import android.graphics.drawable.AnimationDrawable
      import android.os.Bundle
      import android.widget.Button
      import android.widget.ImageView
      import androidx.appcompat.app.AppCompatActivity
      
      // MainActivity.kt
      class MainActivity : AppCompatActivity() {
      
          private lateinit var animationDrawable: AnimationDrawable
      
          override fun onCreate(savedInstanceState: Bundle?) {
              super.onCreate(savedInstanceState)
              setContentView(R.layout.activity_main)
      
              val ivLoading = findViewById<ImageView>(R.id.iv_loading)
              val btnStart = findViewById<Button>(R.id.btn_start)
              val btnStop = findViewById<Button>(R.id.btn_stop)
      
              // 获取 AnimationDrawable 对象
              animationDrawable = ivLoading.drawable as AnimationDrawable
      
              btnStart.setOnClickListener {
                  if (!animationDrawable.isRunning) {
                      animationDrawable.start() // 启动动画
                  }
              }
      
              btnStop.setOnClickListener {
                  if (animationDrawable.isRunning) {
                      animationDrawable.stop() // 停止动画
                  }
              }
          }
      
          // 建议在界面可见时启动动画(避免 onCreat 中直接调用)
          override fun onWindowFocusChanged(hasFocus: Boolean) {
              super.onWindowFocusChanged(hasFocus)
              if (hasFocus) {
                  animationDrawable.start()
              } else {
                  animationDrawable.stop()
              }
          }
      }
      

4. 过渡动画(Transition Framework)

  • 过渡动画的核心概念

    过渡动画用于 布局变化 (如添加/移除 View、修改属性) 或 界面切换 (Acitivity/Fragment) 时实现平滑的视觉效果。

    适用场景

    • 布局动态更新(如展开/折叠菜单)
    • Activity/Fragment 切换动画
    • 共享元素过度(如点击图片后放大到详情页)

    核心类

    • TransitionManager:管理过渡动画的触发和执行。
    • Transition:定义动画类型(如平移、淡入淡出)。
    • Scene:描述布局的起始和结束状态。
  • 过渡动画类型

    • 内置过渡效果

      过度类说明
      Fade淡入淡出效果
      Slide滑动进入/退出
      Explode爆炸效果(类似碎片飞散)
      ChangeBounds处理 View 位置和尺寸变化
      ChangeTransform处理 View 的旋转和缩放
      ChangeClipBounds处理裁剪区域变化
      AutoTransition默认组合过渡(Fade + ChangeBounds)
    • 自定义过渡

      可继承 Transition 类,实现自定义动画逻辑。

  • 核心方法和属性

    • TransitionManager

      方法说明
      beginDelayedTransition(ViewGroup)标记后续布局变化需要应用过渡动画。
      go(Scene, Transition)通过 Scene 切换布局并应用过渡动画。
      setTransition(Scene, Scene, Transition)定义两个 Scene 之间的过渡动画。
    • Transition

      方法/属性说明
      setDuration(long)设置动画时长(毫秒)。
      setInterpolator(Interpolator)设置插值器(控制动画速度曲线)。
      addTarget(View)指定应用动画的目标 View。
      excludeTarget(View, boolean)排除特定 View 的动画效果。
  • 示例

    • activity_main.xml

      <?xml version="1.0" encoding="utf-8"?>
      <LinearLayout
          xmlns:android="http://schemas.android.com/apk/res/android"
          android:id="@+id/root_layout"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:orientation="vertical"
          android:padding="16dp">
      
          <!-- 控制按钮 -->
          <Button
              android:id="@+id/btn_fade"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:text="Fade 过渡" />
      
          <Button
              android:id="@+id/btn_slide"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:text="Slide 过渡" />
      
          <Button
              android:id="@+id/btn_explode"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:text="Explode 过渡" />
      
          <Button
              android:id="@+id/btn_change_bounds"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:text="ChangeBounds 过渡" />
      
          <Button
              android:id="@+id/btn_change_transform"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:text="ChangeTransform 过渡" />
      
          <Button
              android:id="@+id/btn_change_clip_bounds"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:text="ChangeClipBounds 过渡" />
      
          <Button
              android:id="@+id/btn_auto_transition"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:text="AutoTransition 过渡" />
      
          <Button
              android:id="@+id/btn_custom_transition"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:text="自定义 ColorTransition" />
      
          <!-- 动画目标 View -->
          <View
              android:id="@+id/target_view"
              android:layout_width="100dp"
              android:layout_height="100dp"
              android:background="#FF5722"
              android:layout_marginTop="16dp" />
      
      </LinearLayout>
      
    • MainActivity

      import android.animation.Animator
      import android.animation.ValueAnimator
      import android.graphics.Color
      import android.graphics.Rect
      import android.graphics.drawable.ColorDrawable
      import android.os.Bundle
      import android.view.Gravity
      import android.view.View
      import android.view.ViewGroup
      import android.view.animation.AccelerateDecelerateInterpolator
      import android.view.animation.AccelerateInterpolator
      import android.view.animation.BounceInterpolator
      import android.view.animation.DecelerateInterpolator
      import android.view.animation.LinearInterpolator
      import android.view.animation.OvershootInterpolator
      import android.widget.Button
      import android.widget.LinearLayout
      import androidx.appcompat.app.AppCompatActivity
      import androidx.transition.AutoTransition
      import androidx.transition.ChangeBounds
      import androidx.transition.ChangeClipBounds
      import androidx.transition.ChangeTransform
      import androidx.transition.Explode
      import androidx.transition.Fade
      import androidx.transition.Slide
      import androidx.transition.Transition
      import androidx.transition.TransitionManager
      import androidx.transition.TransitionValues
      
      // MainActivity.kt
      class MainActivity : AppCompatActivity() {
      
          private lateinit var targetView: View
          private var isOriginalState = true
      
          override fun onCreate(savedInstanceState: Bundle?) {
              super.onCreate(savedInstanceState)
              setContentView(R.layout.activity_main)
              targetView = findViewById(R.id.target_view)
      
              // 绑定按钮点击事件(每个过渡单独配置)
              findViewById<Button>(R.id.btn_fade).setOnClickListener {
                  applyTransition(Fade().apply {
                      duration = 1000  // 1秒淡入淡出
                      interpolator = LinearInterpolator()
                  })
              }
      
              findViewById<Button>(R.id.btn_slide).setOnClickListener {
                  applyTransition(Slide().apply {
                      duration = 800   // 0.8秒滑动
                      interpolator = AccelerateInterpolator()
                      slideEdge = Gravity.START // 从左侧滑入
                  })
              }
      
              findViewById<Button>(R.id.btn_explode).setOnClickListener {
                  applyTransition(Explode().apply {
                      duration = 1200  // 1.2秒爆炸效果
                      interpolator = OvershootInterpolator()
                  })
              }
      
              findViewById<Button>(R.id.btn_change_bounds).setOnClickListener {
                  applyTransition(ChangeBounds().apply {
                      duration = 1500  // 1.5秒尺寸/位置变化
                      interpolator = BounceInterpolator()
                  })
              }
      
              findViewById<Button>(R.id.btn_change_transform).setOnClickListener {
                  applyTransition(ChangeTransform().apply {
                      duration = 1000  // 1秒旋转缩放
                      interpolator = DecelerateInterpolator()
                  })
              }
      
              findViewById<Button>(R.id.btn_change_clip_bounds).setOnClickListener {
                  applyTransition(ChangeClipBounds().apply {
                      duration = 1000  // 1秒裁剪区域变化
                  })
              }
      
              findViewById<Button>(R.id.btn_auto_transition).setOnClickListener {
                  applyTransition(AutoTransition().apply {
                      duration = 1000  // 1秒默认组合动画
                  })
              }
      
              findViewById<Button>(R.id.btn_custom_transition).setOnClickListener {
                  applyTransition(ColorTransition().apply {
                      duration = 2000  // 2秒颜色渐变
                  })
              }
          }
      
          private fun applyTransition(transition: Transition) {
              // 确保过渡动画生效
              TransitionManager.beginDelayedTransition(findViewById(R.id.root_layout), transition)
              toggleViewState()
          }
      
          private fun toggleViewState() {
              if (isOriginalState) {
                  // 大范围修改属性以确保动画可见
                  targetView.layoutParams = LinearLayout.LayoutParams(300.dpToPx(), 300.dpToPx()).apply {
                      gravity = Gravity.END
                  }
                  targetView.rotation = 180f  // 旋转180度
                  targetView.scaleX = 2.0f    // X轴放大2倍
                  targetView.scaleY = 2.0f    // Y轴放大2倍
                  targetView.clipBounds = Rect(0, 0, 150.dpToPx(), 150.dpToPx()) // 裁剪至中心区域
                  targetView.setBackgroundColor(Color.BLUE) // 修改颜色(用于自定义过渡)
              } else {
                  // 恢复初始状态
                  targetView.layoutParams = LinearLayout.LayoutParams(100.dpToPx(), 100.dpToPx())
                  targetView.rotation = 0f
                  targetView.scaleX = 1f
                  targetView.scaleY = 1f
                  targetView.clipBounds = null
                  targetView.setBackgroundColor(Color.parseColor("#FF5722")) // 恢复初始颜色
              }
              isOriginalState = !isOriginalState
          }
      
          private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).toInt()
      }
      
      // 自定义过渡:颜色渐变(修复版)
      class ColorTransition : Transition() {
      
          override fun captureStartValues(transitionValues: TransitionValues) {
              captureColor(transitionValues)
          }
      
          override fun captureEndValues(transitionValues: TransitionValues) {
              captureColor(transitionValues)
          }
      
          private fun captureColor(transitionValues: TransitionValues) {
              val view = transitionValues.view
              if (view.background is ColorDrawable) {
                  transitionValues.values[PROPNAME_COLOR] = (view.background as ColorDrawable).color
              }
          }
      
          override fun createAnimator(
              sceneRoot: ViewGroup,
              startValues: TransitionValues?,
              endValues: TransitionValues?
          ): Animator? {
              if (startValues == null || endValues == null) return null
      
              val startColor = startValues.values[PROPNAME_COLOR] as Int
              val endColor = endValues.values[PROPNAME_COLOR] as Int
              val view = startValues.view
      
              return ValueAnimator.ofArgb(startColor, endColor).apply {
                  addUpdateListener { animator ->
                      view.background = ColorDrawable(animator.animatedValue as Int)
                  }
                  duration = 2000 // 显式设置动画时长
                  interpolator = AccelerateDecelerateInterpolator()
              }
          }
      
          companion object {
              private const val PROPNAME_COLOR = "ColorTransition:color"
          }
      }
      

5. 矢量动画(AnimatedVectorDrawable)

  • 矢量动画核心概念

    矢量动画基于 VectorDrawableAnimatedVectorDrawable 实现,特点:

    • 使用矢量图形(SVG 路径),放大不失真
    • 通过属性动画驱动矢量图形变化
    • 适合复杂路径形变、颜色渐变等效果
  • 核心类与属性

    • VectorDrawable(静态矢量图)

      • XML 标签<vector>
      • 关键属性
        • android:width/height:画布尺寸
        • android:viewportWidth/Height:视口坐标系
        • <path> 子元素定义路径:
          • android:name:路径名称(用于动画绑定)
          • android:pathData:路径指令(M、L、C 等)
          • android:fillColor / strokeColor:填充/描边颜色
    • AnimatedVectorDrawable(动画矢量图)

      • XML 标签<animated-vector>
      • 关键属性
        • 通过 <target> 标签绑定动画到 VectorDrawable 属性:
          • android:name:目标路径名称
          • android:animation:关联的属性动画资源
    • 属性动画(驱动变化)

      • 常用类ObjectAnimator(XML 或代码创建)
      • 关键属性(XML 中):
        • android:propertyName:要动画的属性(如 pathDatafillColor
        • android:duration:持续时间
        • android:valueFrom / android:valueTo:起始/结束值
        • android:interpolator:插值器(如 @android:interpolator/accelerate_decelerate
  • 示例

    • 创建 VectorDrawable(res/drawable/heart.xml

      <?xml version="1.0" encoding="utf-8"?>
      <vector
          xmlns:android="http://schemas.android.com/apk/res/android"
          android:width="64dp"
          android:height="64dp"
          android:viewportWidth="24"
          android:viewportHeight="24">
      
          <path
              android:name="heart_path"
              android:pathData="M12,21.35L10.55,20.03C5.4,15.36 2,12.28 2,8.5C2,5.42 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.09C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.42 22,8.5C22,12.28 18.6,15.36 13.45,20.04L12,21.35Z"
              android:fillColor="#FF0000"
              android:strokeWidth="0.1"/>
      
      </vector>
      
    • 定义颜色动画(res/animator/color_animator.xml

      <?xml version="1.0" encoding="utf-8"?>
      <objectAnimator
          xmlns:android="http://schemas.android.com/apk/res/android"
          android:propertyName="fillColor"
          android:duration="1000"
          android:valueFrom="#FF0000"
          android:valueTo="#00FF00"
          android:valueType="colorType"
          android:interpolator="@android:interpolator/accelerate_decelerate"/>
      
    • 定义路径形变动画(res/animator/path_morph.xml

      <?xml version="1.0" encoding="utf-8"?>
      <objectAnimator
          xmlns:android="http://schemas.android.com/apk/res/android"
          android:propertyName="pathData"
          android:duration="1000"
          android:valueFrom="M12,21.35L10.55,20.03C5.4,15.36 2,12.28 2,8.5C2,5.42 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.09C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.42 22,8.5C22,12.28 18.6,15.36 13.45,20.04L12,21.35Z"
          android:valueTo="M12,21.35L10.55,20.03C3.4,15.36 2,12.28 2,8.5C2,5.42 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.09C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.42 22,8.5C22,12.28 18.6,15.36 13.45,20.04L12,21.35Z"
          android:valueType="pathType"
          android:interpolator="@android:interpolator/bounce""/>
      
    • 创建 AnimatedVectorDrawable(res/drawable/animated_heart.xml

      <?xml version="1.0" encoding="utf-8"?>
      <animated-vector
          xmlns:android="http://schemas.android.com/apk/res/android"
          android:drawable="@drawable/heart">
      
          <target
              android:name="heart_path"
              android:animation="@animator/color_animator"/>
      
          <target
              android:name="heart_path"
              android:animation="@animator/path_morph"/>
      
      </animated-vector>
      
    • activity_main.xml

      <?xml version="1.0" encoding="utf-8"?>
      <LinearLayout
          xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:app="http://schemas.android.com/apk/res-auto"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:orientation="vertical"
          android:gravity="center"
          android:padding="16dp">
      
          <!-- 动画容器 -->
          <ImageView
              android:id="@+id/iv_heart"
              android:layout_width="100dp"
              android:layout_height="100dp"
              app:srcCompat="@drawable/animated_heart" />
      
          <!-- 状态显示 -->
          <TextView
              android:id="@+id/tv_status"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_marginTop="16dp"
              android:text="Status: Ready"/>
      
          <!-- 控制按钮 -->
          <Button
              android:id="@+id/btn_control"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_marginTop="16dp"
              android:text="Start Animation"/>
      
          <Button
              android:id="@+id/btn_reset"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_marginTop="8dp"
              android:text="Reset"/>
      
      </LinearLayout>
      
    • MainActivity

      class MainActivity : AppCompatActivity() {
      
          private lateinit var ivHeart: ImageView
          private lateinit var tvStatus: TextView
          private var heartAnim: Animatable? = null
          private var isAnimating = false
      
          override fun onCreate(savedInstanceState: Bundle?) {
              super.onCreate(savedInstanceState)
              setContentView(R.layout.activity_main)
      
              // 初始化视图
              ivHeart = findViewById(R.id.iv_heart)
              tvStatus = findViewById(R.id.tv_status)
              val btnControl: Button = findViewById(R.id.btn_control)
              val btnReset: Button = findViewById(R.id.btn_reset)
      
              // 初始化动画对象
              ivHeart.post {
                  (ivHeart.drawable as? Animatable)?.let {
                      heartAnim = it
                      updateStatus("Initialized")
                  }
              }
      
              // 控制按钮点击
              btnControl.setOnClickListener {
                  heartAnim?.let { anim ->
                      when {
                          anim.isRunning -> {
                              anim.stop()
                              isAnimating = false
                              updateStatus("Stopped")
                              btnControl.text = "Start"
                          }
                          else -> {
                              anim.start()
                              isAnimating = true
                              updateStatus("Running")
                              btnControl.text = "Stop"
                          }
                      }
                  }
              }
      
              // 重置按钮点击
              btnReset.setOnClickListener {
                  heartAnim?.let { anim ->
                      if (anim.isRunning) anim.stop()
                      resetAnimation()
                      updateStatus("Reset")
                      btnControl.text = "Start"
                  }
              }
          }
      
          private fun updateStatus(text: String) {
              tvStatus.text = "Status: $text"
          }
      
          private fun resetAnimation() {
              (ivHeart.drawable as? AnimatedVectorDrawable)?.reset()
              ivHeart.postDelayed({
                  ivHeart.invalidate()  // 强制重绘视图
              }, 100)
          }
      }