From ca0af6f7d23ad708f89aa365200ed3caf903b89d Mon Sep 17 00:00:00 2001 From: wm <2747639460@qq.com> Date: Wed, 22 Jul 2026 17:01:53 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20gif-player=20=E4=BF=AE=E5=A4=8D=5Fabort?= =?UTF-8?q?=E8=AF=AF=E6=9D=80=E5=BC=82=E6=AD=A5=E5=9B=9E=E8=B0=83=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=E5=BD=BB=E5=BA=95=E4=B8=8D=E6=92=AD=E6=94=BE=20+=20?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E4=B8=AD=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/gif-player/gif-player.js | 37 +++++++++++-------- .../components/gif-player/gif-player.wxml | 5 +++ .../components/gif-player/gif-player.wxss | 22 ++++++++++- 3 files changed, 48 insertions(+), 16 deletions(-) diff --git a/miniprogram/components/gif-player/gif-player.js b/miniprogram/components/gif-player/gif-player.js index 7bc29ce..c82c541 100644 --- a/miniprogram/components/gif-player/gif-player.js +++ b/miniprogram/components/gif-player/gif-player.js @@ -4,18 +4,19 @@ const { GifReader } = require('../../utils/vendor/omggif.js'); Component({ properties: { - // GIF 地址(通常是 ex.gifUrl,超分后的 720px 动图) + // GIF 地址(通常是 ex.gifUrl,超分后的清晰动图)。 + // 组件可能在 attached 之后才拿到 src(父页面接口异步返回),用 observer 兜底重新加载。 src: { type: String, value: '', - // src 可能在组件 attached 之后才由父页面异步设上,用 observer 兜底重新加载 observer(value) { if (value) this._load(); } } }, data: { - failed: false + failed: false, + loading: true }, lifetimes: { attached() { @@ -25,51 +26,55 @@ Component({ this._ctx = null; this._rgba = null; this._frame = 0; + this._loadedSrc = ''; if (this.data.src) this._load(); }, detached() { - this._abort(); + // 仅 detached 时彻底停止;不要把 _aborted 留给异步回调误判 + this._aborted = true; + if (this._timer) { clearTimeout(this._timer); this._timer = null; } } }, methods: { - _abort() { - this._aborted = true; + // 清除上一次可能的循环定时器(不动 _aborted 标志,否则会误杀正在进行的异步请求) + _clearTimer() { if (this._timer) { clearTimeout(this._timer); this._timer = null; } }, - // 下载 GIF 二进制并构造解码器(解耦于画布,便于画布未就绪时重试渲染) _load() { const src = this.data.src; if (!src || this._aborted) return; - // 防重复:attached 与 observer 可能都触发;同一 src 只加载一次 + // 同一 src 且已解码完成则跳过,避免 attached 与 observer 重复触发 if (this._loadedSrc === src && this._reader) return; this._loadedSrc = src; - this._abort(); // 清掉上一次可能的循环定时器 + this._clearTimer(); + this.setData({ loading: true, failed: false }); wx.request({ url: src, responseType: 'arraybuffer', success: (res) => { + // detached 之后才允许放弃 if (this._aborted) return; try { this._reader = new GifReader(new Uint8Array(res.data)); this._start(); } catch (e) { console.error('[gif-player] decode fail', e); - this.setData({ failed: true }); + this.setData({ failed: true, loading: false }); } }, fail: () => { - if (!this._aborted) this.setData({ failed: true }); + if (!this._aborted) this.setData({ failed: true, loading: false }); } }); }, - // 等待 Canvas 2D 节点就绪(自定义组件里 attached 时可能还没布局好,最多重试 10 次) + // 等待 Canvas 2D 节点就绪(自定义组件 attached 时可能还没布局好,最多重试 15 次) _start(attempt = 0) { if (this._aborted || !this._reader) return; const q = wx.createSelectorQuery().in(this); q.select('#gifCanvas').fields({ node: true, size: true }).exec((rs) => { if (this._aborted) return; if (!rs || !rs[0] || !rs[0].node) { - if (attempt < 10) setTimeout(() => this._start(attempt + 1), 60); + if (attempt < 15) setTimeout(() => this._start(attempt + 1), 60); return; } const canvas = rs[0].node; @@ -89,7 +94,7 @@ Component({ if (this._aborted || !this._reader || !this._ctx) return; const r = this._reader; const n = r.numFrames(); - if (!n) { this.setData({ failed: true }); return; } + if (!n) { this.setData({ failed: true, loading: false }); return; } try { // decodeAndBlitFrameRGBA 按 GIF 的 disposal 规则在 rgba 上累积绘制 r.decodeAndBlitFrameRGBA(this._frame, this._rgba); @@ -98,9 +103,11 @@ Component({ this._ctx.putImageData(img, 0, 0); } catch (e) { console.error('[gif-player] frame fail', e); - this.setData({ failed: true }); + this.setData({ failed: true, loading: false }); return; } + // 首帧渲染完成,关闭加载提示 + if (this.data.loading) this.setData({ loading: false }); const info = r.frameInfo(this._frame); // omggif 的 delay 单位是 1/100 秒,需 ×10 转毫秒;缺失时给 100ms const delay = (info && info.delay) ? info.delay * 10 : 100; diff --git a/miniprogram/components/gif-player/gif-player.wxml b/miniprogram/components/gif-player/gif-player.wxml index efd3643..034a070 100644 --- a/miniprogram/components/gif-player/gif-player.wxml +++ b/miniprogram/components/gif-player/gif-player.wxml @@ -1,3 +1,8 @@ + + + + 动图加载中… + diff --git a/miniprogram/components/gif-player/gif-player.wxss b/miniprogram/components/gif-player/gif-player.wxss index bf201bd..df41f77 100644 --- a/miniprogram/components/gif-player/gif-player.wxss +++ b/miniprogram/components/gif-player/gif-player.wxss @@ -1,2 +1,22 @@ -.gif-player { width: 100%; height: 100%; } +.gif-player { width: 100%; height: 100%; position: relative; } .canvas { width: 100%; height: 100%; display: block; } + +/* 加载中提示:居中、半透明,避免用户误以为动图没播 */ +.gif-loading { + position: absolute; left: 0; right: 0; bottom: 24rpx; + display: flex; align-items: center; justify-content: center; gap: 12rpx; + pointer-events: none; +} +.gif-loading__dot { + width: 18rpx; height: 18rpx; border-radius: 999rpx; + background: var(--gold, #c9a24b); + animation: gif-pulse 1s ease-in-out infinite; +} +.gif-loading__txt { + font-size: 22rpx; color: rgba(255,255,255,0.82); + text-shadow: 0 1rpx 4rpx rgba(0,0,0,0.5); +} +@keyframes gif-pulse { + 0%, 100% { opacity: 0.35; transform: scale(0.8); } + 50% { opacity: 1; transform: scale(1.1); } +}