// gif-player:微信 组件不播放 GIF 动画(只渲染首帧), // 这里用 Canvas 2D + 纯 JS 解码器(omggif)逐帧绘制,实现真正的动图播放。 const { GifReader } = require('../../utils/vendor/omggif.js'); Component({ properties: { // GIF 地址(通常是 ex.gifUrl,超分后的清晰动图)。 // 组件可能在 attached 之后才拿到 src(父页面接口异步返回),用 observer 兜底重新加载。 src: { type: String, value: '', observer(value) { if (value) this._load(); } } }, data: { failed: false, loading: true }, lifetimes: { attached() { this._aborted = false; this._timer = null; this._reader = null; this._ctx = null; this._rgba = null; this._frame = 0; this._loadedSrc = ''; if (this.data.src) this._load(); }, detached() { // 仅 detached 时彻底停止;不要把 _aborted 留给异步回调误判 this._aborted = true; if (this._timer) { clearTimeout(this._timer); this._timer = null; } } }, methods: { // 清除上一次可能的循环定时器(不动 _aborted 标志,否则会误杀正在进行的异步请求) _clearTimer() { if (this._timer) { clearTimeout(this._timer); this._timer = null; } }, _load() { const src = this.data.src; if (!src || this._aborted) return; // 同一 src 且已解码完成则跳过,避免 attached 与 observer 重复触发 if (this._loadedSrc === src && this._reader) return; this._loadedSrc = src; 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, loading: false }); } }, fail: () => { if (!this._aborted) this.setData({ failed: true, loading: false }); } }); }, // 等待 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 < 15) setTimeout(() => this._start(attempt + 1), 60); return; } const canvas = rs[0].node; const ctx = canvas.getContext('2d'); const w = this._reader.width; const h = this._reader.height; // 画布内部分辨率 = GIF 原始分辨率(已超分到 720);CSS 负责缩放到容器 canvas.width = w; canvas.height = h; this._ctx = ctx; this._rgba = new Uint8ClampedArray(w * h * 4); this._frame = 0; this._render(); }); }, _render() { if (this._aborted || !this._reader || !this._ctx) return; const r = this._reader; const n = r.numFrames(); if (!n) { this.setData({ failed: true, loading: false }); return; } try { // decodeAndBlitFrameRGBA 按 GIF 的 disposal 规则在 rgba 上累积绘制 r.decodeAndBlitFrameRGBA(this._frame, this._rgba); const img = this._ctx.createImageData(r.width, r.height); img.data.set(this._rgba); this._ctx.putImageData(img, 0, 0); } catch (e) { console.error('[gif-player] frame fail', e); 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; this._frame = (this._frame + 1) % n; // 单帧 GIF 只画一次;多帧才循环。最小 40ms 避免极端快帧 if (n > 1) this._timer = setTimeout(() => this._render(), Math.max(40, delay)); } } });