【Flutter】控制GIF播放
前言
需求:
1.签到入口图片是gif,循环播放gif动画两次,暂停3秒再循环播放两次。
2.点击gif图片进入签到页面,返回后gif图片不再播放只显示第一帧图像。
了解需求后先分析一下Flutter的图片组件Image好像原生不支持控制gif播放功能。最简单粗暴的解决方案如下:
方案一: 分别准备一张静态图片png和动态gif,通过定时器方式每三秒切换一次图片显示。通过Visibility组件控制先显示gif图然后3秒后显示静态图。
Visibility(
visible: isPlay,
child: Image.asset("images/gif_player_demo.gif"),
replacement: Image.asset("images/gif_player_demo.png"),
);
这个方案并不完美,实际情况会发现静态图和动态gif切换并不流畅存在动画效果会有截断的情况并且gif显示有时候起播的并不是第一帧动画。通过源码分析会发现原因在于gif下一帧动画刷新是通过SchedulerBinding.instance.scheduleFrameCallback更新的。
MultiFrameImageStreamCompleter部分源码:
void _scheduleAppFrame() {
if (_frameCallbackScheduled) {
return;
}
_frameCallbackScheduled = true;
SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame);
}
动画由页面刷新fps所决定,这也就导致动画衔接并不精准,通过定时器控制显示和隐藏做不到gif播放时序。
方案二: 直接设计一个播放两次动画和静态3秒的gif图片,但这样的gif图片会特别大因为静止的3秒动画并不会优化压缩成一帧图像,也就是每帧动画在3秒内都是一张图像,加之gif还需要支持停止播放只显示第一帧图像,因此这个方案也可以放弃了。
源码分析
自定义实现控制gif播放之前先了解一下Image组件源码是如何实现多帧图像播放。这里就通过Image.network来进行源码分析: 找到加载图像资源关键类image_provider.NetworkImage,其中load是加载并获取图像数据的重要方法,通过MultiFrameImageStreamCompleter获取到图像资源数据流结果正是通过它来实现多帧图像数据播放。接下来就重点看看MultiFrameImageStreamCompleter中的实现。
MultiFrameImageStreamCompleter分析
MultiFrameImageStreamCompleter实质上就是图像数据管理者,可以理解为图像播放是由它管理控制,也是之后对gif控制播放改造对象。
MultiFrameImageStreamCompleter则是通过图像解码器Codec获取图像,图像解码器结果通过定时器以及scheduleFrameCallback刷新循环获取下一帧图像执行_emitFrame发送ImageInfo。
void _handleAppFrame(Duration timestamp) {
_frameCallbackScheduled = false;
if (!hasListeners)
return;
if (_isFirstFrame() || _hasFrameDurationPassed(timestamp)) {
_emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale));
_shownTimestamp = timestamp;
_frameDuration = _nextFrame.duration;
_nextFrame = null;
final int completedCycles = _framesEmitted ~/ _codec.frameCount;
if (_codec.repetitionCount == -1 || completedCycles <= _codec.repetitionCount) {
_decodeNextFrameAndSchedule();
}
return;
}
final Duration delay = _frameDuration - (timestamp - _shownTimestamp);
_timer = Timer(delay * timeDilation, () {
_scheduleAppFrame();
});
}
void _scheduleAppFrame() {
if (_frameCallbackScheduled) {
return;
}
_frameCallbackScheduled = true;
SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame);
}
查看图像解码器Codec源码可知能够获取到图像帧数、下一帧图像数据,但也只能获取到下一帧数据并不能指定获取某一帧图像数据。
int get frameCount native 'Codec_frameCount';
int get repetitionCount native 'Codec_repetitionCount';
Future<FrameInfo> getNextFrame() {
return _futurize(_getNextFrame);
}
String _getNextFrame(_Callback<FrameInfo> callback) native 'Codec_getNextFrame';
源码改造
通过上述源码分析已知控制gif播放类是由MultiFrameImageStreamCompleter控制。因此网络图片加载可以通过自定义实现ImageProvider改造重写load方法中MultiFrameImageStreamCompleter即可。
GifImage
通过extension方法为Image组件拓展加载gif图片功能。ImageProvider则是Image组件图片资源加载器,也是要做gif加载改造的类。
extension GifImage on Image {
static gif({
@required ImageProvider image,
Key key,
ImageFrameBuilder frameBuilder,
ImageLoadingBuilder loadingBuilder,
ImageErrorWidgetBuilder errorBuilder,
String semanticLabel,
bool excludeFromSemantics = false,
double width,
double height,
Color color,
BlendMode colorBlendMode,
BoxFit fit,
Alignment alignment = Alignment.center,
ImageRepeat repeat = ImageRepeat.noRepeat,
Rect centerSlice,
bool matchTextDirection = false,
bool gaplessPlayback = false,
FilterQuality filterQuality = FilterQuality.low,
}) {
return Image(
key: key,
frameBuilder: frameBuilder,
loadingBuilder: loadingBuilder,
errorBuilder: errorBuilder,
semanticLabel: semanticLabel,
excludeFromSemantics: excludeFromSemantics,
width: width,
height: height,
color: color,
colorBlendMode: colorBlendMode,
fit: fit,
alignment: alignment,
repeat: repeat,
centerSlice: centerSlice,
matchTextDirection: matchTextDirection,
gaplessPlayback: gaplessPlayback,
filterQuality: filterQuality,
image: image,
);
}
}
GifNetworkImage
拷贝NetworkImage(image_provider.ImageProvider)的源码
1.GifNetworkImage构造函数增加图片缓存检查。在加载图片之前删除已有缓存,避免从缓存中获取ImageProvider导致新配置参数不生效(更好的方式单独开辟一份gif缓存单例保持数据这里省略了这个步骤)。
2.修改load方法中返回的MultiFrameImageStreamCompleter对象,替换成自定义的GifMultiFrameImageStreamCompleter类并增加了replayDuration和repetitionCount参数。
GifNetworkImage(
this.url, {
this.repetitionCount = -1,
this.replayDuration,
this.scale = 1.0,
this.headers,
}) {
assert(url != null);
assert(scale != null);
// *******Add
// 加载前必须先检查缓存资源有缓存就清理
// 否则在缓存中取出codec获取资源不一定是从第一帧图像开始
if (PaintingBinding.instance.imageCache.containsKey(this)) {
print("<gifImage> imageCache containsKey ");
PaintingBinding.instance.imageCache.evict(this, includeLive: true);
}
}
....
@override
ImageStreamCompleter load(NetworkImage key, DecoderCallback decode) {
final StreamController<ImageChunkEvent> chunkEvents =
StreamController<ImageChunkEvent>();
streamCompleter = GifMultiFrameImageStreamCompleter(
codec: _loadAsync(key, chunkEvents, decode),
chunkEvents: chunkEvents.stream,
scale: key.scale,
informationCollector: () {
return <DiagnosticsNode>[
DiagnosticsProperty<ImageProvider>('Image provider', this),
DiagnosticsProperty<NetworkImage>('Image key', key),
];
},
replayDuration: replayDuration,
repetitionCount: repetitionCount,
);
return streamCompleter;
}
....
GifMultiFrameImageStreamCompleter
GifMultiFrameImageStreamCompleter主要改写_handleCodecReady和_handleAppFrame方法。
1.在获取图片解码器Codec时预加载所有帧图像缓存到List<ui.FrameInfo> _frames中。作用主要是方便获取每一帧图像数据,更好的计算动画播放到哪一帧。
2.改写_nextFrame获取方式,通过_frames获取某帧数据避免动画出现跳帧和丢帧的情况。
3.改写定时器方法,当有replayDuration参数时重新实例化一个Timer做倒计时方法重置_framesEmitted计数,重新开始播放gif动画。
...
void _handleCodecReady(ui.Codec codec) async {
_codec = codec;
assert(_codec != null);
print(
"<gifImage> repetitionCount ${_codec.repetitionCount} frameCount ${_codec.frameCount}");
if (_codec.frameCount > 1) {
_frames = List();
for (int i = 0; i < _codec.frameCount; i++) {
ui.FrameInfo frameInfo = await _codec.getNextFrame();
_frames.add(frameInfo);
}
}
if (hasListeners) {
_decodeNextFrameAndSchedule();
}
}
void _handleAppFrame(Duration timestamp) {
_frameCallbackScheduled = false;
if (!hasListeners) return;
if (_isFirstFrame() || _hasFrameDurationPassed(timestamp)) {
_emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale));
_shownTimestamp = timestamp;
_frameDuration = _nextFrame.duration;
_nextFrame = null;
final int completedCycles = _framesEmitted ~/ _codec.frameCount;
// *******Add
// 自定义循环次数
// 不使用_codec本身的循环次数
// if (_codec.repetitionCount == -1 ||
// completedCycles <= _codec.repetitionCount) {
// _decodeNextFrameAndSchedule();
// }
print("<gifImage> _handleAppFrame completedCycles $completedCycles");
if (repetitionCount == -1 || completedCycles <= repetitionCount) {
_decodeNextFrameAndSchedule();
} else if (replayDuration != null) {
_timer?.cancel();
_timer = null;
_timer = Timer(replayDuration, () {
_framesEmitted = 0;
_decodeNextFrameAndSchedule();
});
}
return;
}
print("<gifImage> _handleAppFrame _timer");
final Duration delay = _frameDuration - (timestamp - _shownTimestamp);
_timer = Timer(delay * timeDilation, () {
_scheduleAppFrame();
});
}
...
GifAssetImage
上述实现了网络gif动画改造,本地Asset图片也亦然。同样也是拷贝ImageProvider源码对load方法进行修改将MultiFrameImageStreamCompleter替换为GifMultiFrameImageStreamCompleter即可。
GifImage.gif的使用
最后若需要实时变更gif播放参数需要将GifNetworkImage和GifAssetImage单独实例化对象,对其参数做修改操作。原因是在图片加载过程中ImageProvider只会实例化一次,即使执行setState更新参数但ImageProvider实例已存在就不会重新更新ImageProvider对象。
因此这里为自定义ImageProvider添加了updatePlayConfig方法实时更新gif播放间隔和播放次数。
class _GifPlayerDemoState extends State<GifPlayerDemo> {
String picRes1 =
"http://wx2.sinaimg.cn/bmiddle/ceeb653ely1g4xhw7xasrg207d054njs.gif";
int repetitionCount = 1;
Duration duration = Duration(seconds: 3);
GifNetworkImage gifNetworkImage;
@override
void initState() {
super.initState();
gifNetworkImage = GifNetworkImage(
picRes1,
repetitionCount: repetitionCount,
replayDuration: duration,
);
}
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
GifImage.gif(image: gifNetworkImage),
Row(
children: <Widget>[
RaisedButton(
child: Text("无限循环"),
onPressed: () {
repetitionCount = -1;
duration = null;
gifNetworkImage?.updatePlayConfig(
repetitionCount: repetitionCount,
replayDuration: duration,
);
setState(() {});
},
),
RaisedButton(
child: Text("循环3+重播"),
onPressed: () {
repetitionCount = 1;
duration = Duration(seconds: 3);
gifNetworkImage?.updatePlayConfig(
repetitionCount: repetitionCount,
replayDuration: duration,
);
setState(() {});
},
)
],
),
],
);
}
@override
void dispose() {
super.dispose();
gifNetworkImage?.dispose();
}
}
这种自定义方式就能够对gif图片实现单次播放次数和播放间隔。 当然也有flutter_gifimage第三方组件库提供更全面的gif播放控制方案可以选择,根据需求方需要有针对性的选择。
参考
转载自:https://juejin.cn/post/6881589444194140173