1. 项目缘起与核心价值拆解
做过Android开发的人都有一个共同的痛点:网络请求要等、图片要加载、页面要跳转,这些场景下如果界面上什么都不显示,用户就会觉得App卡死了。加载指示器这个东西看起来简单,但真正要做得好看、好用、不卡顿,里面的门道比想象中多得多。
AVLoadingIndicatorView这个库,是我在几年前做一款电商App时偶然发现的。当时产品经理拿着竞品截图跟我说:“人家的加载动画怎么那么丝滑?我们这转圈圈也太寒碜了。”我试过自己写属性动画,也试过用GIF,效果都不理想——要么掉帧,要么内存占用高,要么在不同分辨率设备上表现不一致。后来在开源社区翻到了这个库,集成进去之后,产品那边直接过了。
这个库的核心价值在于:它把几十种加载动画效果封装成了开箱即用的View组件,你不需要懂属性动画的底层原理,不需要自己算贝塞尔曲线,只要在布局文件里写一行代码,就能得到一个流畅、美观的加载指示器。它解决的核心问题是开发效率和视觉一致性——让开发者用最低的成本获得高质量的加载动画效果。
适合谁来参考这篇文章?如果你是有一定Android基础的开发者,正在做App的UI优化或者新项目搭建,这个库值得你花时间了解。即使你最终不选用它,它里面关于属性动画的实现思路、自定义View的绘制逻辑,也值得学习。对于刚入门的Android新手,这篇文章会从集成到定制一步步讲清楚,你跟着做就能跑起来。
注意:这个库虽然名字叫AVLoadingIndicatorView,但它和音视频(Audio/Video)没有任何关系,AV是开发者名字的缩写。我第一次看到这个名字的时候也误会了,以为是个视频加载库。
2. 加载指示器的技术选型与方案对比
2.1 为什么不用系统自带的ProgressBar
Android系统自带的ProgressBar是最省事的方案,一行代码就能用。但实际项目里,我几乎不用它,原因有三:
第一,样式太单一。系统自带的就是一个转圈圈,颜色、大小、速度虽然可以通过属性调整,但整体形态就那一种。现在的App设计越来越讲究品牌调性,一个千篇一律的转圈圈很难满足UI设计师的要求。
第二,定制成本高。你要想改成一个自定义的动画效果,需要写自定义Drawable或者自定义View,工作量不比直接引入一个成熟库小。而且自己写的动画往往在性能优化上不到位,容易出现掉帧。
第三,多设备一致性差。不同Android版本、不同厂商ROM对ProgressBar的默认样式做了修改,你在模拟器上看着好好的,到了某些国产手机上就变了样。用第三方库反而能保证一致性,因为绘制逻辑完全由库控制。
2.2 几种常见加载指示器方案的横向对比
| 方案 | 开发成本 | 视觉效果 | 性能表现 | 可定制性 | 适用场景 |
|---|---|---|---|---|---|
| 系统ProgressBar | 极低 | 单一 | 优秀 | 低 | 快速原型、内部工具 |
| GIF动图 | 低 | 取决于素材 | 差(内存高) | 中 | 简单页面、H5混合 |
| Lottie动画 | 中 | 极佳 | 良好 | 高 | 品牌动画、复杂效果 |
| AVLoadingIndicatorView | 低 | 丰富 | 优秀 | 中高 | 通用App、列表加载 |
| 自定义View | 高 | 完全可控 | 取决于实现 | 极高 | 特殊需求、深度定制 |
从表格可以看出,AVLoadingIndicatorView在开发成本和性能之间找到了一个很好的平衡点。Lottie虽然效果更好,但需要设计师产出JSON文件,而且库体积较大。对于一个只需要加载动画的场景来说,AVLoadingIndicatorView的性价比是最高的。
2.3 这个库的核心设计思路
AVLoadingIndicatorView的设计思路很清晰:用最小的代码量提供最多的动画选择。它内部维护了一个指示器样式的枚举集合,每种样式对应一个独立的绘制实现。当你设置样式的时候,它通过工厂模式创建对应的指示器对象,然后统一由父View负责调度动画刷新。
这种设计的巧妙之处在于:所有指示器共享同一个刷新机制,避免了每个动画单独开线程或者单独注册Choreographer回调带来的性能开销。你可以把它理解成一个“动画容器”,容器负责心跳,里面的内容负责根据心跳绘制不同的形状。
提示:这个库的动画刷新是基于ValueAnimator驱动的,默认刷新频率跟随系统VSYNC,在大多数设备上能稳定在60fps。如果你的页面本身已经很卡了,加载动画也会跟着卡,这时候要优先排查主线程是否有耗时操作。
3. 集成步骤与基础使用实操
3.1 依赖引入的两种方式
最直接的方式是通过Gradle引入。在你的app模块的build.gradle文件中添加依赖:
dependencies { implementation 'com.wang.avi:library:2.1.3' }如果你用的是Kotlin DSL(build.gradle.kts),写法稍有不同:
dependencies { implementation("com.wang.avi:library:2.1.3") }另一种方式是把源码下载下来,把library模块导入到你的项目中。这种方式适合你需要深度定制动画效果的情况,因为你可以直接修改源码里的绘制逻辑。不过对于大多数项目来说,直接依赖远程仓库就够了。
注意:这个库的最后一次更新比较早,如果你项目的目标SDK版本很高,可能会遇到一些编译警告。实测在targetSdk 34的项目中依然可以正常使用,但建议在引入后跑一遍Lint检查,看看有没有需要处理的问题。
3.2 在布局文件中使用
在XML布局中添加AVLoadingIndicatorView非常简单:
<com.wang.avi.AVLoadingIndicatorView android:id="@+id/avi_loading" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" app:indicatorName="BallPulse" app:indicatorColor="@color/primary_color" />这里有几个关键属性需要说明:
- indicatorName:指定动画样式名称,这个是最核心的属性。库内置了几十种样式,比如BallPulse、BallGridPulse、BallClipRotate、LineScale等等。名字都很直观,看到名字基本能猜到动画长什么样。
- indicatorColor:设置指示器的颜色,可以是一个颜色值,也可以引用colors.xml里的资源。
- layout_width/height:建议用wrap_content,因为每种指示器的尺寸不一样,写死大小可能导致动画被裁剪。
3.3 在代码中控制显示和隐藏
布局写好后,在Activity或Fragment中控制它的显示和隐藏:
AVLoadingIndicatorView avi = findViewById(R.id.avi_loading); // 显示加载动画 avi.show(); // 隐藏加载动画 avi.hide(); // 或者用smoothToHide()实现淡出效果 avi.smoothToHide();这里有个细节值得注意:show()和hide()是直接切换可见性,没有过渡效果。如果你想要更平滑的体验,用smoothToHide(),它会有一个渐隐动画。我在实际项目里通常用smoothToHide(),因为突然消失的加载动画会让用户觉得突兀。
3.4 动态切换指示器样式
有时候你需要在运行时切换不同的加载样式,比如下拉刷新用一种,页面加载用另一种。代码很简单:
avi.setIndicator("BallClipRotatePulse");传入的字符串就是样式名称。如果你不确定有哪些样式可用,可以查看库源码中的Indicator枚举类,里面列出了所有支持的样式。
实操心得:我建议在项目的constants类里把用到的指示器名称定义成常量,避免在代码里到处写字符串。一旦拼写错误,指示器会显示不出来,而且不会报错,排查起来很费时间。
4. 内置动画样式全解析与选型建议
4.1 按动画形态分类
这个库内置的样式大概可以分成几大类,我按照实际使用场景来分类说明:
旋转类:BallClipRotate、BallClipRotatePulse、BallClipRotateMultiple、Pacman。这类动画以旋转为核心,视觉上比较有动感,适合网络请求、页面加载等场景。其中Pacman是吃豆人造型,比较有趣,适合休闲类App。
缩放类:BallPulse、BallGridPulse、BallScale、BallScaleRipple、BallScaleMultiple。这类动画通过元素的放大缩小来表现节奏感,视觉上比较柔和,适合图片加载、内容刷新等场景。
线条类:LineScale、LineScaleParty、LineScalePulseOut、LineScalePulseOutRapid。这类动画用线条的伸缩来表现,风格比较简洁现代,适合工具类App或者设计风格偏扁平化的产品。
组合类:BallSpinFadeLoader、BallGridBeat、SemiCircleSpin。这类动画结合了旋转和透明度变化,视觉效果更丰富,适合作为品牌加载动画使用。
4.2 不同场景下的选型建议
选加载动画不是越花哨越好,要考虑你的App整体设计风格和使用场景。我总结了一个简单的选型参考:
| 场景 | 推荐样式 | 理由 |
|---|---|---|
| 列表下拉刷新 | BallPulse | 体积小,不抢眼,适合频繁出现 |
| 页面首次加载 | BallClipRotatePulse | 有辨识度,给用户明确的等待信号 |
| 图片加载占位 | BallScaleRipple | 柔和,不会干扰图片内容的呈现 |
| 按钮提交等待 | LineScalePulseOutRapid | 节奏快,暗示操作正在快速处理 |
| 全屏Loading | BallSpinFadeLoader | 视觉填充感强,适合大面积展示 |
提示:不要在一个页面里同时使用超过两种加载样式,否则会让界面显得杂乱。统一使用一种样式作为主加载动画,特殊场景再用另一种,保持视觉一致性。
4.3 颜色与尺寸的适配策略
加载指示器的颜色应该和你的品牌色保持一致。如果品牌色是深色,指示器也用深色;如果品牌色是亮色,指示器用亮色。但有一个例外:如果加载指示器出现在深色背景上,一定要用浅色指示器,反之亦然。
尺寸方面,库默认的指示器大小是根据View的宽高自动计算的。如果你设置了固定的宽高,指示器会在这个范围内绘制。我一般建议给一个适中的固定尺寸,比如48dp x 48dp,这样在不同屏幕上表现比较一致。
<com.wang.avi.AVLoadingIndicatorView android:layout_width="48dp" android:layout_height="48dp" app:indicatorName="BallSpinFadeLoader" app:indicatorColor="#FF6B35" />5. 性能优化与常见问题排查
5.1 内存泄漏的预防
加载指示器最常见的问题就是内存泄漏。如果你的Activity在加载过程中被销毁了,但动画还在跑,就会导致Activity无法被回收。解决办法很简单:在onDestroy中停止动画。
@Override protected void onDestroy() { super.onDestroy(); if (avi != null) { avi.hide(); } }更稳妥的做法是在onPause中就隐藏,因为用户离开页面后动画继续跑没有意义,还浪费CPU资源。
5.2 动画卡顿的排查思路
如果你发现加载动画不流畅,可以按照以下步骤排查:
第一步,检查主线程是否有耗时操作。加载动画的刷新依赖主线程的VSYNC信号,如果主线程被数据库查询、大量计算等操作占用了,动画自然会卡。用Android Studio的Profiler工具可以快速定位。
第二步,检查是否同时运行了太多动画。一个页面上如果有多个AVLoadingIndicatorView同时显示,每个都会独立驱动动画,叠加起来可能超过主线程的渲染能力。这种情况下建议只保留一个加载指示器。
第三步,检查硬件加速是否开启。在AndroidManifest.xml的application标签中确保android:hardwareAccelerated="true",这能显著提升动画的渲染性能。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 指示器不显示 | indicatorName拼写错误 | 检查字符串是否与库中定义一致 |
| 指示器显示但不动 | 没有调用show() | 确认在代码中调用了show()方法 |
| 动画颜色不对 | indicatorColor未设置或值错误 | 检查颜色值格式,建议用资源引用 |
| 指示器被裁剪 | View宽高设置过小 | 增大宽高或使用wrap_content |
| 页面退出后崩溃 | 动画未停止 | 在onDestroy中调用hide() |
| 低版本设备不显示 | 硬件加速未开启 | 在Manifest中开启硬件加速 |
5.4 一个容易被忽略的细节
这个库的动画刷新是基于ValueAnimator的,而ValueAnimator在页面不可见时(比如Activity进入后台)默认会暂停。这本来是个好事,能省电。但如果你在Fragment切换时没有正确处理,可能会出现动画状态不一致的情况——比如Fragment A的动画暂停了,切回来之后没有恢复。
我的做法是在Fragment的onResume中重新调用show(),在onPause中调用hide()。这样虽然简单粗暴,但能保证状态始终正确。
6. 进阶定制与源码改造思路
6.1 自定义指示器的实现路径
库内置的样式虽然多,但总有不够用的时候。比如你的品牌有一个特定的吉祥物,想让它作为加载动画。这时候就需要自定义指示器。
自定义指示器的核心是实现Indicator接口,这个接口定义了三个方法:draw()负责绘制,onAnimationUpdate()负责接收动画进度,getAnimation()返回驱动动画的ValueAnimator。你只需要在这三个方法里填入自己的逻辑即可。
public class MyCustomIndicator extends Indicator { @Override public void draw(Canvas canvas, Paint paint) { // 在这里实现你的绘制逻辑 // canvas是画布,paint是画笔 // 根据mCurrentProgress计算当前帧的形态 } @Override protected void onAnimationUpdate(ValueAnimator animation) { // 更新进度值 mCurrentProgress = (float) animation.getAnimatedValue(); // 触发重绘 postInvalidate(); } @Override public ValueAnimator onCreateAnimation() { // 创建并返回驱动动画 ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); animator.setDuration(1000); animator.setRepeatCount(ValueAnimator.INFINITE); animator.setInterpolator(new LinearInterpolator()); return animator; } }写完之后,通过setIndicator()方法传入你的自定义指示器实例即可。
6.2 修改源码实现更精细的控制
如果你需要更精细的控制,比如调整动画的刷新频率、修改默认的插值器,可以直接把库源码导入项目进行修改。源码结构很清晰,主要文件就几个:
AVLoadingIndicatorView.java:主View类,负责调度和绘制Indicator.java:指示器接口indicators/目录:各种内置指示器的实现
我建议在修改源码之前先fork一份,这样后续库有更新时还能合并。修改的时候注意保持接口兼容,不要改动公共方法的签名,否则升级时会很痛苦。
6.3 与其他加载框架的配合使用
AVLoadingIndicatorView只负责“显示动画”这一件事,它不处理网络请求、不管理数据加载。在实际项目中,它通常和网络框架(如OkHttp、Retrofit)或者图片加载框架(如Glide、Coil)配合使用。
以Glide为例,你可以自定义一个RequestListener,在onLoadStarted时显示指示器,在onLoadFailed和onResourceReady时隐藏指示器。这样就能实现图片加载过程中的动画展示。
Glide.with(context) .load(imageUrl) .listener(new RequestListener<Drawable>() { @Override public boolean onLoadFailed(Exception e, Object model, Target<Drawable> target, boolean isFirstResource) { avi.smoothToHide(); return false; } @Override public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) { avi.smoothToHide(); return false; } }) .into(imageView);实操心得:在RecyclerView的ViewHolder中使用加载指示器时,要注意复用问题。因为ViewHolder会被回收复用,如果不小心处理,可能会出现指示器状态错乱的情况。我的做法是在onBindViewHolder中根据数据状态显式设置指示器的显示或隐藏,不要依赖默认状态。
7. 实际项目中的经验总结
7.1 什么时候该用加载指示器
不是所有等待都需要加载指示器。如果操作能在200毫秒内完成,用户几乎感知不到等待,这时候显示加载动画反而会让界面闪烁,体验更差。我的经验是:预计耗时超过500毫秒的操作才需要显示加载指示器,200到500毫秒之间的可以考虑用骨架屏或者占位图。
另外,加载指示器的显示和隐藏要有延迟。不要一发起请求就立刻显示,因为很多请求其实很快就返回了。我通常设置一个300毫秒的延迟,如果请求在300毫秒内完成了,就不显示指示器;超过300毫秒才显示。这样能避免界面频繁闪烁。
7.2 多个加载状态的协调
一个页面可能有多个区域同时加载,比如顶部有刷新、中间有列表加载、底部有分页加载。这时候如果每个区域都显示一个加载指示器,界面会显得很乱。我的做法是:只保留一个全局的加载指示器,其他区域的加载状态用文字或者骨架屏来表示。
如果确实需要多个指示器,要确保它们的样式一致、大小协调。不要一个用BallPulse,另一个用Pacman,那样看起来像两个不同的App拼在一起。
7.3 这个库的局限性与替代方案
AVLoadingIndicatorView虽然好用,但也有它的局限性。首先,它的动画样式是固定的,不能通过参数调整动画的速度、幅度等细节。其次,它不支持矢量动画,在高分辨率屏幕上放大后可能会有锯齿。最后,它的更新维护频率较低,如果遇到Android新版本的兼容性问题,可能需要自己解决。
如果你需要更灵活的动画控制,可以考虑Lottie;如果你需要矢量级别的清晰度,可以考虑AnimatedVectorDrawable;如果你需要更现代的API设计,可以考虑Compose中的加载组件。但对于大多数传统View体系的项目来说,AVLoadingIndicatorView依然是一个省时省力的选择。
我在最近的一个项目中,把AVLoadingIndicatorView和Compose的加载组件混用了——传统View部分用AVLoadingIndicatorView,Compose部分用CircularProgressIndicator。两者在视觉上做了统一,用户完全感知不到底层是两套不同的实现。这种渐进式的迁移策略,对于老项目改造来说比较实用。
最后分享一个小技巧:如果你觉得库内置的某个动画效果不错,但速度太快或太慢,可以通过反射修改它内部的ValueAnimator的duration。虽然这不是官方推荐的做法,但在不改源码的情况下能快速达到目的。具体代码就不贴了,思路是用Java反射拿到Indicator对象中的animator字段,然后调用setDuration()。用之前记得做好版本兼容判断,因为不同版本的字段名可能不一样。