Flutter动图加载机制解析

2022-05-10 20:49:33 浏览数 (1)

上文研究完 Flutter 的图片加载和缓存管理

Flutter图片加载和缓存机制探究

今天继续研究下 Flutter 是怎么处理动图的。Flutter 的 Image 加载默认会支持 gif、webp 等动态图片。在之前的文章中,我们会看到不同类型的图片加载逻辑是大致一样的,只是异步加载的逻辑不一样,

代码语言:javascript复制
@override
ImageStreamCompleter load(
 final StreamController<ImageChunkEvent> chunkEvents =
        StreamController<ImageChunkEvent>();
  
  return MultiFrameImageStreamCompleter(
        chunkEvents: chunkEvents.stream,
        codec: _loadAsync(key as NetworkImage, decode, chunkEvents),
        scale: key.scale,
        debugLabel: key.url,
        informationCollector: _imageStreamInformationCollector(key));
}

load 方法都会返回一个 MultiFrameImageStreamCompleter 对象。从名字我们就有可以看到,这个可以处理多个帧的图片。这也是 Flutter 在加载图片的时候默认会使用的 Completer 对象。它的基类是 ImageStreamCompleter�

代码语言:javascript复制
codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) {
      reportError(
        context: ErrorDescription('resolving an image codec'),
        exception: error,
        stack: stack,
        informationCollector: informationCollector,
        silent: true,
      );
});

图片加载完成后,会执行 _handleCodecReady 的逻辑。可以看到这里已经是开始图片解码了。

代码语言:javascript复制
// _handleCodecReady
void _handleCodecReady(ui.Codec codec) {
    _codec = codec;
    assert(_codec != null);

    if (hasListeners) {
      _decodeNextFrameAndSchedule();
    }
}

这里会进行下一帧的解码:

代码语言:javascript复制
// _handleCodecReady
void _handleCodecReady(ui.Codec codec) {
    _codec = codec;
    assert(_codec != null);

    if (hasListeners) {
      _decodeNextFrameAndSchedule();
    }
  }

// _decodeNextFrameAndSchedule
Future<void> _decodeNextFrameAndSchedule() async {
 try {
  // 获取下一帧
      _nextFrame = await _codec!.getNextFrame();
    } catch (exception, stack) {

  return;
 }
 if (_codec!.frameCount == 1) {
 // 只有一帧
 _emitFrame(ImageInfo(image: _nextFrame!.image, scale: _scale, debugLabel: debugLabel));
 return;
 }
 // 不止一帧,说明是动图
 _scheduleAppFrame();
}

void _scheduleAppFrame() {
 if (_frameCallbackScheduled) {
      return;
    }
    _frameCallbackScheduled = true;
    SchedulerBinding.instance!.scheduleFrameCallback(_handleAppFrame);
}

我们来梳理一下这里的逻辑:解析过程会尝试读取图片的下一帧。当图片解码信息里图片只有一帧的话,那么直接提交这一帧内容并结束, 如果 frameCount > 1 的话,则说明图片不止一帧内容,说明此时加载的是一张动图。继续执行 _sheduleAppFrame 方法。这个会在 ShedulerBinding 执行下一帧的时候执行 _handleAppFrame 方法。

代码语言:javascript复制
void _handleAppFrame(Duration timestamp) {
 _frameCallbackScheduled = false;
 // 如果是第一帧
 if (_isFirstFrame() || _hasFrameDurationPassed(timestamp)) {
 
   // 提交一帧图片
   _emitFrame(ImageInfo(
    image: _nextFrame!.image, 
    scale: _scale, debugLabel: debugLabel));
   
   _shownTimestamp = timestamp;
   _frameDuration = _nextFrame!.duration;
   _nextFrame = null;
   // 计算这是第几次播放
   final int completedCycles = _framesEmitted ~/ _codec!.frameCount;


   // 如果重复次数是-1 或者完成的次数小于等于动图循环次数,继续执行_decodeNextFrameAndSchedule
   if (_codec!.repetitionCount == -1 || completedCycles <= _codec!.repetitionCount) {
        _decodeNextFrameAndSchedule();
      }
   return;
  
 }


 final Duration delay = _frameDuration! - (timestamp - _shownTimestamp);
    _timer = Timer(delay * timeDilation, () {
   // 循环到下一帧
      _scheduleAppFrame();
    });
}

这个方法里会处理当前的帧。如果是第一帧或者应该是下一帧出现的时间了,就会去提交该帧的内容。接下来会判断这张图是否播放完毕,如果没有,则会继续执行上面的解码工作,去解析下一帧图片。这里判断图片是否播放完毕依赖于两个条件:

  • 设置的重复次数是 -1,需要一直循环播
  • 播放的轮数小于设置的重复次数,轮数就是当前提交的帧数和图片总帧数取整。比如图片总共有10帧,播放到第32帧的时候,说明当前是第三轮。

整个动图的加载流程如图:

总结

从上面的代码中我们可以获取一些结论:

  • Flutter 默认是支持解析动图的,包括 webp、gif 这些
  • 我们可以自己参考上述内容去实现我们的动图播放,增加例如动画控制、动图播放监听等功能

0 人点赞