尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Android加载动画库AVLoadingIndicatorView:原理、集成与性能优化实战

发布时间:2026/9/25 4:38:00

资讯中心
01
ARTICLE

Android加载动画库AVLoadingIndicatorView:原理、集成与性能优化实战

Android加载动画库AVLoadingIndicatorView:原理、集成与性能优化实战
1. 为什么一个加载动画库值得单独拿出来聊做 Android 开发的应该都有这种体会一个 App 的功能做得再扎实只要网络请求转圈的那两秒体验拉胯用户对整个产品的印象就会打折扣。AVLoadingIndicatorView这个库就是专门解决这个问题的——它把三十多种风格各异的加载动画封装成开箱即用的 View你不需要自己写属性动画、不需要处理帧率、不需要担心内存泄漏直接在布局里写一行标签就能用。我第一次接触这个库是在一个电商项目里当时产品经理拿着竞品截图过来说“人家的加载动画是那种几个小球弹跳的我们这个是系统默认的灰色圆圈太丑了”。团队里没人愿意花两天时间去调贝塞尔曲线后来在开源社区翻到了AVLoadingIndicatorView集成进去前后不到半小时效果直接对齐竞品。从那以后这个库就成了我项目模板里的常驻依赖。这篇文章适合三类人看一是刚入行、还在用ProgressBar默认样式的 Android 新手二是接过老项目、需要快速统一加载态视觉的中级开发者三是想理解一个轻量级自定义 View 库该怎么设计、怎么读源码的进阶选手。我会从选型逻辑讲到源码结构从集成步骤讲到踩坑记录尽量把我知道的都倒出来。需要提前说明的是这个库本身是免费开源的遵循 Apache 2.0 协议商用没有法律风险。但它的维护节奏在近几年放缓了所以我会在最后一节专门讲“如果它不再更新你该怎么接手维护”这部分是我自己在实际项目里做过的事情不是纸上谈兵。2. 加载指示器库的选型逻辑与 AVLoadingIndicatorView 的定位2.1 市面主流加载方案的横向对比在决定用哪个库之前我一般会从五个维度去评估动画数量、包体积增量、自定义能力、维护活跃度、集成成本。下面这张表是我自己整理过的对比数据基于我实际打包后的 APK 分析结果不同项目可能有细微差异。方案动画数量包体积增量自定义能力维护状态集成成本系统 ProgressBar1 种0 KB极低官方维护零成本AVLoadingIndicatorView30 种约 60 KB中等低频维护低Lottie无限依赖设计稿约 800 KB 资源极高活跃中SpinKit20 种约 40 KB中等低频维护低自绘属性动画取决于投入接近 0完全可控自己维护高从表里能看出来AVLoadingIndicatorView的定位非常清晰它卡在“系统方案太丑”和“Lottie 太重”之间的那个空档。60 KB 的体积增量在现在动辄几十兆的 App 里几乎可以忽略但它提供的视觉丰富度是系统ProgressBar完全给不了的。我个人的选型原则是这样的如果加载动画是产品的核心视觉资产比如品牌吉祥物动画那必须上 Lottie让设计师出 AE 稿如果只是需要“比默认好看、风格统一、不折腾”那AVLoadingIndicatorView就是性价比最高的选择。它不需要设计师参与不需要额外的资源文件纯代码实现这对中小团队特别友好。2.2 这个库解决的核心痛点到底是什么很多人以为加载库只是“好看”其实它解决的是三个层面的问题。第一个层面是视觉一致性。Android 系统在不同版本、不同厂商 ROM 上的ProgressBar样式是不一样的小米、华为、三星各有各的魔改。你在自己手机上看着是蓝色圆圈到用户手机上可能变成灰色方块。用AVLoadingIndicatorView就完全绕开了这个问题它的动画是纯 Canvas 绘制的跟系统主题无关在所有设备上表现一致。第二个层面是开发效率。自己写一个“三个小球依次弹跳”的动画你需要定义三个 View、写三组ValueAnimator、处理onDetachedFromWindow时取消动画、还要考虑View复用时的状态重置。这一套下来少说半天。而这个库把这些都封装好了你只需要app:indicatorNameBallPulse一行属性。第三个层面是性能可控。这个库的动画全部基于ValueAnimator驱动invalidate()重绘没有用View层级嵌套也没有用Handler轮询。我在中低端机上实测过同时显示五个指示器CPU 占用率增加不到 2%内存增量在 200 KB 以内。这个数据对于列表页的item加载态来说是完全可接受的。2.3 什么场景下不建议用它说了这么多优点也得说说它不适合的地方不然就是不负责任。如果你的 App 需要极致包体积控制比如做海外新兴市场的轻量版那 60 KB 也是钱可以考虑自己用Canvas画一个最简单的旋转圆弧代码量不超过 50 行。如果你的加载动画需要跟品牌 IP 强绑定比如动画里要出现公司吉祥物那这个库满足不了老老实实上 Lottie。还有就是需要复杂交互反馈的场景比如加载进度要跟实际下载百分比联动这个库只提供不确定进度indeterminate的动画确定进度还是得用系统ProgressBar或者自己实现。我踩过的一个坑是曾经在一个视频播放页的缓冲态用了这个库结果发现它的动画帧率跟视频解码线程抢资源导致缓冲时视频卡顿。后来换成静态的缓冲图标加文字提示才解决。所以记住动画库不是万能的在重负载场景下要谨慎评估。3. 核心实现原理拆解一个轻量级自定义 View 库是怎么设计的3.1 整体架构BaseIndicatorController 与 Indicator 的分工这个库的源码结构非常清晰核心就两个抽象类BaseIndicatorController和Indicator。理解了这个分工你就理解了整个库的设计哲学。BaseIndicatorController是动画的“大脑”它负责创建ValueAnimator、管理动画的启动和停止、在每一帧回调时通知 View 重绘。它内部维护了一个ListValueAnimator因为有些动画比如多个小球需要多个动画器协同工作。关键方法是start()、stop()和postFrame()其中postFrame()会调用invalidate()触发重绘。Indicator是绘制的“手”它是一个接口定义了draw(Canvas canvas, Paint paint)方法。每个具体的动画比如BallPulseIndicator都实现这个接口在draw()里根据当前动画进度值来画图形。这种设计的精妙之处在于动画逻辑和绘制逻辑完全解耦。BaseIndicatorController不关心你画的是圆还是方它只管按时间轴推进进度Indicator不关心进度是怎么来的它只管拿到当前值然后画出来。这符合单一职责原则也让新增动画变得非常简单——你只需要写一个新的Indicator实现类和一个对应的Controller子类。我读过不少自定义 View 库的源码很多都是把动画和绘制揉在一个类里几百行代码缠在一起想改个颜色都找不到地方。AVLoadingIndicatorView这种分层方式虽然多了一层抽象但可维护性高了一个数量级。3.2 动画驱动机制ValueAnimator 的复用与生命周期管理这个库所有动画都基于ValueAnimator没有用ObjectAnimator也没有用Handler定时器。为什么因为ValueAnimator是 Android 官方推荐的属性动画基础设施它内部已经处理了帧率同步、生命周期暂停、系统动画时长缩放开发者选项里的“动画时长缩放”等细节。具体来说每个Controller在构造时会创建一到多个ValueAnimator设置setRepeatCount(ValueAnimator.INFINITE)和setRepeatMode(ValueAnimator.RESTART)然后通过addUpdateListener在每一帧回调里更新内部状态并调用postFrame()。这里有个关键细节动画的启动和停止必须跟 View 的 attach/detach 生命周期绑定。如果 View 从窗口移除时动画还在跑就会造成内存泄漏和 CPU 浪费。这个库在AVLoadingIndicatorView的onAttachedToWindow()里调用mIndicatorController.start()在onDetachedFromWindow()里调用stop()。这个处理是很多新手自己写动画时容易忽略的。我在实际项目中遇到过一个问题在RecyclerView的item里使用这个库快速滑动时会出现动画错乱。排查后发现是因为item被回收时onDetachedFromWindow没有及时触发导致旧的动画状态残留。解决方案是在Adapter的onViewRecycled()里手动调用indicator.hide()或者smoothToHide()确保状态重置。3.3 绘制层实现Canvas 与 Paint 的参数配置每个Indicator的draw()方法里核心就是操作Canvas和Paint。以最经典的BallPulseIndicator为例它画三个圆每个圆的半径和透明度根据动画进度值变化。绘制时用到的Paint配置有几个要点setAntiAlias(true)开启抗锯齿否则圆边缘会有锯齿setStyle(Paint.Style.FILL)设置填充模式颜色通过setColor()设置这个颜色是从AVLoadingIndicatorView的app:indicatorColor属性传下来的。坐标计算是绘制层最容易出错的地方。因为AVLoadingIndicatorView的尺寸是不确定的可能被设置为wrap_content也可能被设置为固定dp值。所以每个Indicator在绘制前都需要根据getWidth()和getHeight()动态计算中心点和半径。这个库的做法是在draw()里实时计算而不是在onSizeChanged()里缓存虽然多了一点计算量但避免了尺寸变化时的同步问题。我建议你在读源码时重点关注BallPulseIndicator、BallSpinFadeLoaderIndicator和LineSpinFadeLoaderIndicator这三个它们分别代表了“多元素独立动画”、“旋转透明度变化”、“线条形态变化”三种典型模式。看懂这三个其他的都是变体。4. 从零集成到项目完整实操流程与关键配置4.1 依赖引入与版本选择这个库在 Maven Central 上的最新版本是2.1.3虽然有些年头了但稳定性经过大量项目验证。在app模块的build.gradle里添加dependencies { implementation com.wang.avi:library:2.1.3 }如果你用的是 Kotlin DSLbuild.gradle.kts写法是dependencies { implementation(com.wang.avi:library:2.1.3) }这里有个坑要提醒这个库的minSdkVersion是 11targetSdkVersion是 25。在 Android 12 及以上设备上运行没问题但如果你在AndroidManifest.xml里设置了android:exported相关的严格模式需要注意它内部的AVLoadingIndicatorView没有定义任何Activity或Service所以不受影响。另外如果你的项目开启了 R8 混淆这个库不需要额外的 keep 规则因为它的类都是通过反射以外的方式引用的。但如果你用了indicatorName属性动态指定动画类型那就要注意了——这个属性是在 XML 解析时通过context.obtainStyledAttributes读取的不涉及反射所以混淆也没问题。4.2 布局文件中的基础用法最简单的用法就是在布局里直接写com.wang.avi.AVLoadingIndicatorView android:idid/avi_loading android:layout_widthwrap_content android:layout_heightwrap_content android:visibilitygone app:indicatorNameBallPulse app:indicatorColor#FF4081 app:indicatorSize48dp /这里解释一下三个自定义属性的含义。indicatorName指定动画类型可选值有三十多种常用的有BallPulse、BallGridPulse、BallClipRotate、BallSpinFadeLoader、LineSpinFadeLoader、Pacman等。indicatorColor设置动画颜色支持#RGB、#ARGB、#RRGGBB、#AARRGGBB四种格式。indicatorSize设置动画的绘制区域大小注意这个值影响的是内部 Canvas 的绘制范围不是 View 的实际尺寸。我个人的经验是indicatorSize一般设置为36dp到56dp之间比较合适。太小了看不清细节太大了在列表里显得突兀。如果是全屏加载态可以设到72dp。颜色方面建议跟 App 的主题色保持一致不要用默认的灰色那样跟系统ProgressBar就没区别了。4.3 代码中动态控制显示与隐藏在 Activity 或 Fragment 里你需要拿到 View 的引用然后控制它AVLoadingIndicatorView avi findViewById(R.id.avi_loading); // 显示加载 avi.show(); // 或者带动画地显示 avi.smoothToShow(); // 隐藏加载 avi.hide(); // 或者带动画地隐藏 avi.smoothToHide();show()和hide()是直接设置Visibility没有过渡效果。smoothToShow()和smoothToHide()会先执行一个透明度渐变动画然后再切换Visibility视觉上更柔和。我在实际项目里基本都用smoothToShow()和smoothToHide()因为直接切换太生硬用户能明显感觉到“闪了一下”。但要注意smoothToHide()是异步的动画执行期间 View 还是可见的。如果你在隐藏后立即要执行某个操作比如显示空数据提示需要监听动画结束。这个库没有提供回调接口所以我的做法是用postDelayed延迟 300 毫秒再执行后续操作虽然不够优雅但足够可靠。4.4 在 RecyclerView 列表项中的正确使用姿势列表项里用加载指示器是最容易出问题的场景。因为RecyclerView会复用itemView如果不在onBindViewHolder里重置状态就会出现“这个 item 明明不该显示加载却显示着上一个 item 的加载动画”的情况。我的标准做法是在ViewHolder里这样写Override public void onBindViewHolder(ViewHolder holder, int position) { ItemData data list.get(position); if (data.isLoading()) { holder.avi.smoothToShow(); } else { holder.avi.hide(); // 注意这里用 hide 而不是 smoothToHide } // 其他绑定逻辑 }为什么隐藏时用hide()而不是smoothToHide()因为列表滚动时onBindViewHolder会被频繁调用如果每次都用渐变动画会造成大量动画对象创建和销毁影响滑动流畅度。直接hide()虽然生硬但在快速滚动场景下用户根本注意不到。还有一个细节在onViewRecycled()里也要调用holder.avi.hide()确保被回收的 View 状态是干净的。这个双保险能避免 99% 的状态错乱问题。5. 三十多种动画类型怎么选场景化推荐与效果对比5.1 按使用场景分类的动画推荐这个库提供了三十多种动画但常用的其实就十来种。我按场景给你分类推荐省得你一个个试。全屏加载或页面级加载推荐BallPulse、BallGridPulse、BallClipRotatePulse。这三种动画幅度适中视觉上不张扬适合覆盖整个屏幕的加载遮罩。其中BallPulse是最经典的三个小球弹跳认知度最高用户一看就知道是在加载。按钮内嵌加载推荐BallPulseSync、BallBeat、LineScale。按钮里的加载动画要小、要紧凑不能超出按钮边界。LineScale是几根竖线高低变化特别适合按钮场景我在登录按钮上用过很多次。列表项加载推荐BallSpinFadeLoader、LineSpinFadeLoader。这两种是旋转透明度变化的组合视觉上比较“安静”不会在列表里显得太闹腾。BallSpinFadeLoader是八个圆点围成一圈依次亮起很有节奏感。下拉刷新推荐Pacman、BallClipRotateMultiple。下拉刷新需要一点趣味性Pacman是吃豆人动画很讨喜。不过要注意下拉刷新一般用SwipeRefreshLayout自带的指示器这个库更多是用在自定义刷新头部里。品牌调性强的场景推荐TriangleSkewSpin、BallZigZag、BallZigZagDeflect。这几种动画比较有辨识度适合想做出差异化的产品。但用之前最好跟设计师确认一下避免风格冲突。5.2 动画性能实测数据我在一台红米 Note 9联发科 G854GB 内存上做过一轮实测同时显示 10 个指示器记录 CPU 和内存增量。数据如下动画类型CPU 增量内存增量主观流畅度BallPulse1.2%180 KB流畅BallSpinFadeLoader2.1%240 KB流畅LineSpinFadeLoader1.8%210 KB流畅Pacman2.8%320 KB轻微掉帧BallGridPulse2.3%260 KB流畅从数据看大部分动画的性能开销都很小。Pacman稍微重一点因为它需要绘制扇形和圆弧路径计算更复杂。如果你的列表里要同时显示很多个加载指示器建议避开Pacman。还有一个影响性能的因素是indicatorSize。我测试过把indicatorSize从48dp调到96dpCPU 增量会翻倍。因为绘制区域变大每帧需要填充的像素更多。所以在满足视觉需求的前提下尽量把尺寸设小。5.3 自定义动画的扩展方法如果三十多种还不够用你可以自己扩展。步骤不复杂我带你走一遍。第一步创建一个类实现Indicator接口public class MyCustomIndicator extends BaseIndicatorController { Override public void draw(Canvas canvas, Paint paint) { // 在这里根据进度值绘制你的动画 // getWidth() 和 getHeight() 可以拿到绘制区域尺寸 float progress getProgress(); // 0.0 到 1.0 // 你的绘制逻辑 } Override public ListValueAnimator onCreateAnimators() { ListValueAnimator animators new ArrayList(); ValueAnimator animator ValueAnimator.ofFloat(0f, 1f); animator.setDuration(1000); animator.setRepeatCount(ValueAnimator.INFINITE); animator.setInterpolator(new LinearInterpolator()); addUpdateListener(animator, animation - { // 更新进度值 postInvalidate(); }); animators.add(animator); return animators; } }第二步在AVLoadingIndicatorView的IndicatorName枚举里注册你的动画名。但这一步需要改源码所以更实际的做法是直接在你的代码里new出来然后setIndicator。这个扩展机制的设计很干净你不需要动库的任何核心代码只需要实现两个方法。我在一个项目里做过一个“公司 Logo 形状的加载动画”就是通过这种方式实现的前后花了一个小时。6. 踩坑实录那些文档里不会写的注意事项6.1 内存泄漏的三种典型场景这个库本身写得比较规范但使用不当仍然会导致内存泄漏。我遇到过三种情况。第一种是在Activity销毁时动画还在跑。虽然库在onDetachedFromWindow里会停止动画但如果你的Activity是finish()后View没有立即 detach动画会多跑几百毫秒。解决方案是在onDestroy()里手动调用avi.hide()。第二种是在匿名内部类里持有AVLoadingIndicatorView的引用。比如你在ValueAnimator的监听器里直接引用了外部类的avi变量如果动画没停就会导致Activity无法回收。解决方案是用静态内部类或者弱引用。第三种是在Dialog里使用。Dialog的dismiss()和View的detach不是同步的如果dismiss后立即finishActivity动画可能还在跑。我的做法是在Dialog的onStop()里手动停止动画。6.2 与 ViewPager2、CoordinatorLayout 的兼容性问题ViewPager2内部是用RecyclerView实现的所以列表项里的加载指示器问题在ViewPager2里同样存在。而且ViewPager2的页面切换更频繁状态错乱的概率更高。我的解决方案是在ViewPager2的registerOnPageChangeCallback里每次页面切换时遍历所有页面的指示器并重置状态。CoordinatorLayout的问题主要是Behavior冲突。如果你把AVLoadingIndicatorView放在AppBarLayout下面滚动时可能会被Behavior改变位置导致动画绘制区域计算错误。解决方案是给指示器包一层FrameLayout让Behavior作用在外层容器上。6.3 常见问题速查表问题现象可能原因解决方案动画不显示indicatorName拼写错误检查 XML 属性值注意大小写动画显示但不动没有调用show()或smoothToShow()确认代码中已触发显示颜色不生效用了app:indicatorColor但主题覆盖了检查主题中是否有colorControlActivated等属性冲突列表滑动时动画错乱RecyclerView复用未重置状态在onBindViewHolder和onViewRecycled中重置内存泄漏动画未随生命周期停止在onDestroy中手动hide()在低版本设备上崩溃minSdkVersion低于 11提升minSdkVersion或做版本判断动画尺寸不对indicatorSize设置过小或过大调整到36dp到56dp之间与Lottie同时使用冲突两者都用了ValueAnimator导致帧率竞争避免在同一屏幕同时使用多个动画库6.4 一个真实的生产事故复盘去年我们 App 的首页改版在RecyclerView的每个item里都加了BallSpinFadeLoader作为图片加载的占位动画。测试阶段没问题上线后收到用户反馈说“滑动列表时手机发烫”。排查过程是这样的先用Android Profiler抓了一段滑动时的 CPU 数据发现主线程有大量invalidate()调用。进一步分析发现RecyclerView的item在快速滑动时会被频繁创建和销毁每次创建都会 new 一个BallSpinFadeLoaderIndicator每个Indicator又会创建 8 个ValueAnimator因为有 8 个圆点。滑动一屏下来创建了上百个ValueAnimator虽然旧的会被 GC 回收但 GC 压力很大导致 CPU 持续高负载。解决方案是改用BallPulse它只需要 3 个ValueAnimator而且动画幅度小视觉上更轻量。改完后 CPU 占用率从 18% 降到了 6%发烫问题解决。这个事故给我的教训是在列表场景下动画的“元素数量”比“动画复杂度”更影响性能。选择动画时优先选元素少的。7. 如果这个库不再更新你该怎么接手维护7.1 源码结构速览与关键文件说明这个库的源码文件不多核心就这些AVLoadingIndicatorView.java主 View 类负责属性解析、生命周期管理、对外接口BaseIndicatorController.java动画控制器基类管理ValueAnimator列表Indicator.java绘制接口定义draw()方法indicators/目录三十多个具体动画实现类如果你要接手维护建议先读AVLoadingIndicatorView.java的init()方法那里是属性解析的入口。然后读BaseIndicatorController的start()和stop()理解动画生命周期。最后挑一个简单的Indicator比如BallPulseIndicator读绘制逻辑。7.2 迁移到 AndroidX 与 Kotlin 的改造方案这个库目前还是基于 Android Support Library 的如果你项目已经全面迁移到 AndroidX需要做一次替换。具体操作是把源码里的android.support.annotation替换成androidx.annotationandroid.support.v4.view.ViewCompat替换成androidx.core.view.ViewCompat。用 Android Studio 的“Refactor Migrate to AndroidX”功能可以自动完成大部分工作。如果你想把它改造成 Kotlin 版本工作量也不大。核心是把BaseIndicatorController改成abstract class把Indicator改成interface然后利用 Kotlin 的by lazy和apply简化初始化代码。我做过一次整个库的代码量从 3000 多行 Java 缩减到了 2000 行左右 Kotlin可读性提升明显。7.3 长期维护建议与替代方案评估如果你决定长期维护这个库我建议做三件事。第一把targetSdkVersion升到最新确保在新系统上不会因为权限或行为变更出问题。第二加一个Kotlin Extension模块提供avi.showLoading()这样的扩展函数提升调用体验。第三补一套单元测试重点测试动画的启动停止和状态重置。如果你不想维护替代方案我推荐两个。一个是SpinKit动画风格类似但维护更活跃一些。另一个是Lottie虽然重但生态好、设计师友好长期来看是更稳妥的选择。我的做法是在新项目里直接用 Lottie老项目继续用AVLoadingIndicatorView不折腾。最后分享一个我在实际使用中的小技巧如果你需要在深色模式和浅色模式下显示不同颜色的加载动画不需要写两套布局。在AVLoadingIndicatorView的indicatorColor属性里引用一个颜色资源然后在values-night/colors.xml里定义深色模式下的颜色值就行了。这个技巧适用于所有支持values-night的 Android 项目简单但很多人不知道。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。