在现代移动应用中,加载动画不仅能向用户传达“正在努力加载中”的信号,缓解等待焦虑,还能通过视觉效果提升品牌识别度与用户体验。无论是网络请求、复杂计算、还是页面过渡,都离不开加载状态的优雅表达。本项目旨在演示如何在 Android 中从零实现多种常见的加载动画:
系统原生 ProgressBar:基于 indeterminateDrawable
的自定义风格
属性动画:通过 ObjectAnimator
、ValueAnimator
实现自定义 View 动画
帧动画:基于 AnimationDrawable
的帧序列加载
AnimatedVectorDrawable:矢量动画,兼容性好、文件轻量
Lottie 动画:基于 Airbnb Lottie 库,支持 After Effects 导出 JSON 动画
并封装为易用的组件与工具类,使业务层仅需一行调用,即可实现高质感加载动画。
多种加载样式:圆形旋转、淡入淡出、波浪、心跳、渐变环
可配置:颜色、尺寸、速度、插值器、循环次数
组件化:封装为 LoadingView
,支持 XML 属性与代码动态配置
简易集成:只需在布局中添加
Lifecycle 感知:自动在 onAttachedToWindow
开启动画,在 onDetachedFromWindow
停止
性能优化:硬件加速、资源复用、避免过度重绘
View 动画 vs 属性动画 vs 帧动画
View Animation(补间动画):旧版ScaleAnimation
/RotateAnimation
,仅改 UI 表现,不改变属性
Property Animation(属性动画):API 11+,通过 ObjectAnimator.ofFloat(view,"rotation",0,360)
直接修改属性
Frame Animation:AnimationDrawable
逐帧切换图片,适合复杂帧序列
AnimatedVectorDrawable
基于矢量资源(vector.xml
),用
配合
对路径或属性做动画
文件小、可缩放,无像素损失
Lottie
Airbnb 开源,支持将 After Effects 动画导出为 JSON,在原生端渲染
支持无缝循环、动态替换颜色、图层控制
Canvas 绘制与自定义 View
通过重写 onDraw(Canvas)
,结合 ValueAnimator
驱动 invalidate()
实现自绘动画
可绘制圆环、波浪、心形等自定义形状
插值器(Interpolator)
LinearInterpolator
:匀速
AccelerateDecelerateInterpolator
:先加速再减速
BounceInterpolator
、OvershootInterpolator
:弹跳、回弹
本文将按照由浅入深的顺序,依次实现以下几种加载动画,并最终封装:
系统 ProgressBar
自定义 indeterminateDrawable
:一个旋转的矢量圆环
属性动画 + 自定义 View
LoadingView1
:一个手写的圆环旋转动画
AnimatedVectorDrawable
LoadingView2
:在 XML 中定义 animated-vector
,在 View 中直接调用 setImageDrawable()
Lottie
LoadingView3
:集成 Lottie 库,加载 *.json
动画文件
统一封装
LoadingView
:通过 XML 属性 app:loadingType="progress|custom|vector|lottie"
选择实现
动画生命周期管理、API 暴露 start()
, stop()
// app/build.gradle
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 34
defaultConfig {
applicationId "com.example.loading"
minSdkVersion 21
targetSdkVersion 34
}
buildFeatures { viewBinding true }
kotlinOptions { jvmTarget="1.8" }
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'com.airbnb.android:lottie:6.0.0'
}
// =======================================================
// 文件: res/values/attrs.xml
// 描述: LoadingView 的自定义属性,type、color、size、speed
// =======================================================
// =======================================================
// 文件: res/drawable/progress_ring.xml
// 描述: 矢量圆环,用于 ProgressBar drawable
// =======================================================
// =======================================================
// 文件: res/drawable/anim_progress.xml
// 描述: ProgressBar 的 indeterminateAnimationList
// =======================================================
// =======================================================
// 文件: LoadingView.kt
// 描述: 统一封装 LoadingView,根据 type 加载不同实现
// =======================================================
package com.example.loading
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import android.widget.ImageView
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.content.withStyledAttributes
import com.airbnb.lottie.LottieAnimationView
import com.airbnb.lottie.LottieDrawable
class LoadingView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {
companion object {
const val TYPE_PROGRESS = 0
const val TYPE_CUSTOM = 1
const val TYPE_VECTOR = 2
const val TYPE_LOTTIE = 3
}
private var type = TYPE_PROGRESS
private var color = 0xFF000000.toInt()
private var sizePx = 100
private var duration = 1000
private var lottieFile: String? = null
// 子 View
private var progressBar: ImageView? = null
private var customView: CustomLoadingView? = null
private var vectorView: ImageView? = null
private var lottieView: LottieAnimationView? = null
init {
context.withStyledAttributes(attrs, R.styleable.LoadingView) {
type = getInt(R.styleable.LoadingView_lv_type, TYPE_PROGRESS)
color = getColor(R.styleable.LoadingView_lv_color, color)
sizePx = getDimensionPixelSize(R.styleable.LoadingView_lv_size, sizePx)
duration = getInt(R.styleable.LoadingView_lv_duration, duration)
lottieFile = getString(R.styleable.LoadingView_lv_lottieFile)
}
when (type) {
TYPE_PROGRESS -> initProgressBar()
TYPE_CUSTOM -> initCustom()
TYPE_VECTOR -> initVector()
TYPE_LOTTIE -> initLottie()
}
}
private fun initProgressBar() {
progressBar = AppCompatImageView(context).apply {
setImageResource(R.drawable.anim_progress)
(drawable as? android.graphics.drawable.AnimatedRotateDrawable)?.start()
}
addView(progressBar, LayoutParams(sizePx, sizePx))
}
private fun initCustom() {
customView = CustomLoadingView(context).apply {
setColor(color); setDuration(duration)
start()
}
addView(customView, LayoutParams(sizePx, sizePx))
}
private fun initVector() {
vectorView = AppCompatImageView(context).apply {
setImageResource(R.drawable.progress_ring)
val avd = drawable as android.graphics.drawable.AnimatedVectorDrawable
avd.registerAnimationCallback(object: android.graphics.drawable.AnimatedVectorDrawable.AnimationCallback(){
override fun onAnimationEnd(drawable: android.graphics.drawable.Drawable?) { avd.start() }
})
avd.start()
}
addView(vectorView, LayoutParams(sizePx, sizePx))
}
private fun initLottie() {
lottieView = LottieAnimationView(context).apply {
lottieFile?.let { setAnimation(it) }
repeatCount = LottieDrawable.INFINITE
playAnimation()
}
addView(lottieView, LayoutParams(sizePx, sizePx))
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
customView?.stop()
(vectorView?.drawable as? android.graphics.drawable.AnimatedVectorDrawable)?.stop()
(progressBar?.drawable as? android.graphics.drawable.AnimatedRotateDrawable)?.stop()
lottieView?.cancelAnimation()
}
}
// =======================================================
// 文件: CustomLoadingView.kt
// 描述: 自定义 Property Animation 圆环旋转 View
// =======================================================
package com.example.loading
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import android.view.animation.LinearInterpolator
class CustomLoadingView @JvmOverloads constructor(
ctx: Context, attrs: AttributeSet? = null
) : View(ctx, attrs) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE; strokeCap = Paint.Cap.ROUND; strokeWidth = 8f
}
private val rectF = RectF()
private var sweepAngle = 0f
private var startAngle = 0f
private var duration = 1000
private val animator = ValueAnimator.ofFloat(0f, 360f).apply {
interpolator = LinearInterpolator()
repeatCount = ValueAnimator.INFINITE
addUpdateListener {
startAngle = it.animatedValue as Float
sweepAngle = 90f
invalidate()
}
}
fun setColor(c: Int) { paint.color = c }
fun setDuration(d: Int) { duration = d; animator.duration = d.toLong() }
fun start() { animator.start() }
fun stop() { animator.cancel() }
override fun onSizeChanged(w: Int, h: Int, ow: Int, oh: Int) {
super.onSizeChanged(w, h, ow, oh)
val pad = paint.strokeWidth/2
rectF.set(pad, pad, w-pad, h-pad)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawArc(rectF, startAngle, sweepAngle, false, paint)
}
}
// =======================================================
// 文件: res/layout/activity_main.xml
// 描述: 示例页面,使用四种 LoadingView
// =======================================================
自定义属性 (attrs.xml
)
lv_type
决定使用哪种加载方式:系统 ProgressBar
、自定义 View、Animated Vector、Lottie;
其它属性如 lv_color
、lv_size
、lv_duration
、lv_lottieFile
支持在 XML 中灵活配置。
系统 ProgressBar 方案
progress_ring.xml
定义了一个半开的圆环矢量;
anim_progress.xml
将其包装成 AnimatedRotateDrawable
自动旋转;
在 LoadingView
中用 ImageView
展示并启动旋转动画。
自定义 Property Animation 方案
CustomLoadingView
重写 onDraw
,用 drawArc
绘制一个固定角度的弧段;
通过 ValueAnimator
不断更新 startAngle
,实现旋转效果;
AnimatedVectorDrawable 方案
直接在代码中将 progress_ring
当作 AnimatedVectorDrawable
使用;
注册 AnimationCallback
循环播放;
Lottie 方案
依赖 Lottie 库,加载指定 JSON 动画文件;
设置 repeatCount = INFINITE
并 playAnimation()
;
统一封装 LoadingView
构造时读取属性并创建对应子 View;
在 onDetachedFromWindow
中停止动画,防止内存泄露;
业务层仅需在布局中添加一行即可使用四种动画。
硬件加速
矢量和属性动画在硬件加速下渲染效率高,无需额外设置;
资源复用
AnimatedRotateDrawable
与 AnimatedVectorDrawable
均可复用动画对象;
ValueAnimator
在 CustomLoadingView
中作为成员变量,仅创建一次;
局部重绘
自定义 View 调用 invalidate()
时默认全屏重绘,可考虑 invalidate(rect)
限定区域;
Lottie 最优化
合理拆分 JSON 动画,移除无用图层,减少渲染开销;
只在可见时播放,离屏时 cancelAnimation()
。
本文深入讲解了 Android 中加载动画的多种实现方式,并通过 LoadingView
组件将它们统一封装,极大降低业务层集成成本。可根据产品需求,在不同场景下快速切换加载样式。
更多动画:比如波浪、心跳、圆点脉冲等;
交互引导:结合加载动画与指引气泡,引导用户首次使用;
Compose 重构:使用 Canvas
与 animate*AsState
迁移到 Jetpack Compose;
主题适配:支持夜间模式、品牌色切换;
性能监测:结合 Systrace 或 GPU Overdraw Checker 监测动画性能瓶颈。
Q:AnimatedVectorDrawable 支持哪些 API?
A:Android 5.0+ 原生支持,低版本可用支持库 androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
。
Q:Lottie JSON 文件从哪儿来?
A:可从 LottieFiles 下载,或在 After Effects 中用 Bodymovin 插件导出。
Q:如何在列表中复用 LoadingView?
A:在 RecyclerView Adapter 中直接在 item 布局里引入 LoadingView
,自动启动/停止动画。
Q:自定义 View 性能卡顿怎么办?
A:减少绘制复杂度,使用 PathMeasure
预计算,启用硬件加速。
Q:如何动态切换加载类型?
A:在 LoadingView
中提供 setType()
方法,销毁旧子 View 并重新 initXXX()
。