ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Glide 动图循环控制:播放次数、结束回调与资源清理怎么做

Glide 动图循环控制:播放次数、结束回调与资源清理怎么做

Glide 动图循环控制:播放次数、结束回调与资源清理怎么做

【免费下载链接】glideAn image loading and caching library for Android focused on smooth scrolling项目地址: https://gitcode.com/gh_mirrors/gl/glide

Glide 加载 GIF/WebP 动图时,默认行为是无限循环,而setLoopCount加动画结束回调是打破这个默认的关键。掌握它们之后,你能判断一个动图需求该在哪个环节介入,也能快速定位"播放次数不生效""结束后没停"这类问题的根源。

什么需求下你需要控制播放次数

三个典型场景,先对号入座:

  • 引导类动效:首页新功能引导动画,用户首次进入播放 2 次后停在最后一帧,不干扰操作;触发条件是首次加载,需要控制的是一次性播放次数。
  • 状态提示动画:搜索结果页的加载占位动图,正常时循环播放,切走页面必须停止,否则后台还在解码帧;这里要控制的是生命周期内的启停。
  • 任务完成动画:上传成功后播放一次庆祝动画,播完弹出"查看结果"按钮;需要控制的不仅是次数,还有"播完这一刻"的回调时机。

共同点:只要动画结束要触发别的 UI 动作,就不能只靠无限循环,必须拿到"播放结束"这个事件。

理解三个角色:GifDrawable、帧加载器与循环计数

用一句话说清分工,方便你复述给别人听:

  1. GifDrawable是 Glide 给动图返回的 Drawable 对象,你从into的资源回调里拿到它。它持有maxLoopCount字段,默认值是LOOP_FOREVER(-1),表示无限循环;LOOP_INTRINSIC(0)表示沿用动图文件里自带的循环次数,GIF 场景下就是 netscape 扩展块里的数值。
  2. GifFrameLoader(帧加载器)负责按帧解码和推进。每次start()时 GifDrawable 会向它订阅,每解出一帧就回调onFrameReady()。
  3. 循环计数在onFrameReady()里推进:当帧索引走到最后一帧时loopCount++,一旦loopCount >= maxLoopCount且上限不是 -1,就自动stop()并通知所有已注册的AnimationCallback。

由此可以推出两条重要性质:回调只在"因达到次数上限而停止"时触发,手动stop()不会触发;start()会重置loopCount为 0,所以重启播放就是再调一次start()。

最短验证路径:三步跑通

结论先说:在资源就绪的回调里设置次数,注册结束后要执行的逻辑,页面销毁时停掉并清理。不要试图在into之前设置,那时 Drawable 还不存在。

第一步,拿到资源后设置次数。用asGif()或带RequestListener的into都行:

// onResourceReady 回调内 if (resource instanceof GifDrawable) { gifDrawable = (GifDrawable) resource; gifDrawable.setLoopCount(2); // 只播 2 轮 }

第二步,注册结束回调。registerAnimationCallback来自Animatable2Compat.AnimationCallback:

gifDrawable.registerAnimationCallback(drawable -> { // 仅当达到 setLoopCount 上限自动停止时触发 showRetryButton(); }); gifDrawable.start();

第三步,在生命周期出口清理。调用链是:onDestroy -> stop() -> clearAnimationCallbacks(),防止回调持有页面引用导致泄漏:

@Override protected void onDestroy() { super.onDestroy(); if (gifDrawable != null) { gifDrawable.stop(); gifDrawable.clearAnimationCallbacks(); } }

如果你走的是Target方式(SimpleTarget/CustomTarget),清理动作应放在onLoadCleared里,而不是只依赖 Activity 生命周期。仓库的 samples/giphy 示例展示了用RequestListener捕获 GifDrawable 的完整写法,可直接对照阅读。

三类故障信号与对应处理动作

信号一:setLoopCount 调了,动图仍无限循环。先查三个点:是否调用了asGif()(用asDrawable()或普通load().into()拿到的可能不是 GifDrawable);是否在资源真正就绪后才调用;是否误传了非法值(setLoopCount接受正整数、LOOP_FOREVER或LOOP_INTRINSIC,其他值会直接抛IllegalArgumentException)。定位方法很简单:在回调里打印resource.getClass(),确认类型再谈后续。

信号二:页面已销毁,动画回调仍在触发或内存持续增长。说明AnimationCallback持有 Activity 强引用且未注销。判断方法:在onDestroy打断点,确认执行了stop()和clearAnimationCallbacks();若回调写在匿名内部类里捕获了页面,优先改成弱引用持有,或把回调对象声明为成员变量以便精确unregisterAnimationCallback。

信号三:设了 3 次,视觉上像播了 4 次或 2 次。两个常见原因:一是在start()之后才设的次数没赶上第一轮计数(start()会重置计数,但设置时机与播放顺序要一致);二是LOOP_INTRINSIC模式下文件自带循环数与预期不符。处理动作:统一按"设次数 → 注册回调 → start"的固定顺序执行,且重启播放前不要自行维护额外计数器,以onAnimationEnd触发次数为准做验收。

性能、兼容与边界

  • 尺寸:动图内存与目标尺寸平方级相关,RequestBuilder上先用override(width, height)或sizeMultiplier约束,再谈次数控制,顺序反了会白白多占内存。
  • 格式边界:asGif()返回的始终是 GifDrawable,WebP 动图能否走这条路取决于解码器注册与系统版本,行为与 GIF 不完全一致,跨机型要实测。
  • 可见性:GifDrawable 在setVisible(false)时会自动停止解码、恢复可见后重启,列表滚动天然受益;但反过来,手动stop()后不会因重新可见而自动恢复,两者语义不同。
  • 单帧边界:帧数为 1 时不进入解码循环,onAnimationEnd不会触发,别把这类资源当动图处理。
  • ROM 差异:低端机和部分定制 ROM 的解码速度不一,结束回调的到达时间会波动,UI 跳转不要依赖精确时长。
关键方法触发时机结果
setLoopCount(n)资源就绪后、start 前覆盖默认无限循环
registerAnimationCallback任意,生效于下一次达标停止达标停止时回调
start()动画未运行时重置计数并开播
stop()任意立即停,不触发结束回调

发布前检查清单

  • 确认在资源就绪回调中拿到GifDrawable实例后再调用setLoopCount
  • 确认加载入口用的是asGif(),且资源确实是动图而非静态图
  • 确认setLoopCount传值为正整数、LOOP_FOREVER或LOOP_INTRINSIC之一
  • 确认结束回调按"达标停止"语义使用,不依赖手动 stop 触发
  • 确认onDestroy/onLoadCleared中执行了stop与clearAnimationCallbacks
  • 确认回调不持有 Activity 强引用,或已有弱引用/成员变量方案
  • 用override约束了目标尺寸,并在低端机验证过播放与结束时机

核心实现位于 GifDrawable.java,帧推进逻辑在同目录的 GifFrameLoader.java,整体项目说明见 README.md。

【免费下载链接】glideAn image loading and caching library for Android focused on smooth scrolling项目地址: https://gitcode.com/gh_mirrors/gl/glide

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进