likes
comments
collection
share

【Flutter】控制GIF播放

作者站长头像
站长
· 阅读数 23

前言

需求:

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播放控制方案可以选择,根据需求方需要有针对性的选择。

参考

flutter_gifimage

转载自:https://juejin.cn/post/6881589444194140173
评论
请登录