为了账号安全,请及时绑定邮箱和手机立即绑定

Flutter中ui.Image加载探索

标签:
WebApp

https://img1.sycdn.imooc.com//5d7bbfe80001c67406720318.jpg

想必大家Image组件都玩得挺6的,那么如何在Canvas上画一个图片,实现图片的放大等变换又该如何操呢?如何去监听一个图片流。这些Image组件就无法完成了。


16729e5dc6829617?imageslim



16729e5dc685fcc2?imageslim



import 'dart:ui' as ui; class ImagePage extends StatefulWidget {   ImagePage({Key key,}):super(key:key);   @override   _ImagePageState createState() => _ImagePageState(); } class _ImagePageState extends State<ImagePage> {      @override   Widget build(BuildContext context) {     return Container(       child: CustomPaint(painter: ImagePainter(),),     );   } } class ImagePainter extends CustomPainter {   Paint mainPaint;   ImagePainter(){     mainPaint=Paint()..isAntiAlias=true;   }      @override   void paint(Canvas canvas, Size size) {     canvas.drawImage(Image.asset("images/wy_300x200.jpg"), //报错         Offset(0,0), mainPaint);   }      @override   bool shouldRepaint(CustomPainter oldDelegate) {     // TODO: implement shouldRepaint     return true;   } } 复制代码

1.如何使用Canvas绘制图片

上面在Canvas的drawImage中,你会看到一个Image参数,你会想,这不好办吗?Image传呗!
但是你传入一个Image组件它会神奇般地报错:意思是说人家要的是ui/painting文件的Image。

https://img1.sycdn.imooc.com//5d7bc1e3000161b906470125.jpg

1.1.Canvas绘制图片源码及Image源码
---->[sky_engine/lib/ui/painting.dart:Canvas#drawImage]---- void drawImage(Image image, Offset p, Paint paint) {   assert(image != null); // image is checked on the engine side   assert(_offsetIsValid(p));   assert(paint != null);   _drawImage(image, p.dx, p.dy, paint._objects, paint._data); } 复制代码

当跳入Image中是发现是ui/painting的Image,而且该类被私有化构造
就说明无法被直接创建,更有意思的是几乎都是native方法。

---->[sky_engine/lib/ui/painting.dart:Image]---- @pragma('vm:entry-point') class Image extends NativeFieldWrapperClass2 {   @pragma('vm:entry-point')   Image._();//私有化构造      int get width native 'Image_width';//获取宽   int get height native 'Image_height';//获取高      Future<ByteData> toByteData({ImageByteFormat format = ImageByteFormat.rawRgba}) {//转换成字节数据     return _futurize((_Callback<ByteData> callback) {       return _toByteData(format.index, (Uint8List encoded) {         callback(encoded?.buffer?.asByteData());       });     });   }   String _toByteData(int format, _Callback<Uint8List> callback) native 'Image_toByteData';   void dispose() native 'Image_dispose';//释放图片   @override   String toString() => '[$width\u00D7$height]'; } 复制代码

1.2.通过instantiateImageCodec获取图片编解码器

既然无法创建对象,那怎么玩?源码中为我们指明道路:使用instantiateImageCodec 那instantiateImageCodec又是什么鬼。它是返回一个Future的方法,而且传入一个Uint8List
也许这时你会说: 好复杂,臣妾做不到。我不画了还不行吗。稍安勿躁,先看Codec何许人也...

To obtain an [Image] object, use [instantiateImageCodec]. ---->[sky_engine/lib/ui/painting.dart:instantiateImageCodec]---- Future<Codec> instantiateImageCodec(Uint8List list, {   int targetWidth,   int targetHeight, }) {   return _futurize(     (_Callback<Codec> callback) => _instantiateImageCodec(list, callback, null, targetWidth ?? _kDoNotResizeDimension, targetHeight ?? _kDoNotResizeDimension)   ); } 复制代码

Codec是一个图片编解码器的句柄,这还了得,简直是极品红装啊。它也是私有化构造
所以显得instantiateImageCodec是多么重要。其中getNextFrame方法返回FrameInfo的未来对象
看到Frame你应该立刻联想到图片帧,于是看到在FrameInfo中Image对象就在那等着你。

---->[sky_engine/lib/ui/painting.dart:Codec]---- @pragma('vm:entry-point') class Codec extends NativeFieldWrapperClass2 {   @pragma('vm:entry-point')   Codec._();    Future<FrameInfo> getNextFrame() {   return _futurize(_getNextFrame); } ---->[sky_engine/lib/ui/painting.dart:FrameInfo]---- @pragma('vm:entry-point') class FrameInfo extends NativeFieldWrapperClass2 {   @pragma('vm:entry-point')   FrameInfo._();   Duration get duration => Duration(milliseconds: _durationMillis);   int get _durationMillis native 'FrameInfo_durationMillis';   Image get image native 'FrameInfo_image';//获取Image对象。 } 复制代码

好了,现在似乎一条路已经走通了,唯一一点就是Uint8List的图片数据如何获取
如果你不知道,那么至少可以先写出下面的这个方法:

//通过[Uint8List]获取图片 Future<ui.Image> loadImageByUint8List(Uint8List list) async{   ui.Codec codec= await ui.instantiateImageCodec(list);   ui.FrameInfo frame= await codec.getNextFrame();   return frame.image; } 复制代码

1.3.绘制你的第一张图

这就要考验基本功了,记得在File中有一个方法可以将文件读成Uint8List

//通过 文件读取Image Future<ui.Image> loadImageByFile(String path) async{   var list =await File(path).readAsBytes();   return loadImageByUint8List(list); } 复制代码

这里将一张图片放入缓存文件夹。再用FutureBuilder优雅地将未来的Image对象传入画板中
在画板中当_image非空时就可以将Image对象绘制出来。

https://img1.sycdn.imooc.com//5d7bc1cf0001b59806640308.jpg

---->[ImagePage.dart:_ImagePageState#build]---- class _ImagePageState extends State<ImagePage> {   @override   Widget build(BuildContext context) {     return Container(       child: FutureBuilder<ui.Image>(         future: loadImageByFile("/data/data/com.toly1994.flutter_image/cache/wy_300x200.jpg"),         builder:(context,snapshot)=>CustomPaint(           painter: ImagePainter(snapshot.data),         ),       ),     );   } } ---->[ImagePage.dart:ImagePainter#paint]---- @override void paint(Canvas canvas, Size size) {   if (_image != null) {     canvas.drawImage(_image, Offset(0, 0), mainPaint);   } } 复制代码

也许细心的你可以看到instantiateImageCodec中有两个键值参数,可以确定图片加载出来的宽高
未了使用方便,这里提取一个ImageLoader用于加载图片,使用单例模式:使用 ImageLoader.loader.loadImageByFile("the path",width: 150,height: 100),就可以指定编解码图片的尺寸
实验表明尺寸越大,加载的速度就越慢,超过一定的尺寸image_decoder.cc会不允许加载

https://img1.sycdn.imooc.com//5d7bc1bf0001b86b06280257.jpg

---->[ImageLoader.dart#ImageLoader]---- class ImageLoader {   ImageLoader._();//私有化构造   static final ImageLoader loader= ImageLoader._();//单例模式   //通过 文件读取Image   Future<ui.Image> loadImageByFile(     String path, {     int width,     int height,   }) async {     var list = await File(path).readAsBytes();     return loadImageByUint8List(list, width: width, height: height);   } //通过[Uint8List]获取图片,默认宽高[width][height]   Future<ui.Image> loadImageByUint8List(     Uint8List list, {     int width,     int height,   }) async {     ui.Codec codec = await ui.instantiateImageCodec(list,         targetWidth: width, targetHeight: height);     ui.FrameInfo frame = await codec.getNextFrame();     return frame.image;   } } 复制代码

2.从ImageProvider获取及Image

如果是Asset图片资源或是网络图片如何获取Image呢?
ImageProvider有一个resolve方法可以返回一个图片流ImageStream
作为它孩子的几种图片加载方式都会有该方法,切入点便在此处:

https://img1.sycdn.imooc.com//5d7bc1ab00018b5c06550208.jpg

2.1 :ImageProvider相关源码
---->[src/painting/image_provider.dart:ImageProvider#resolve]---- ImageStream resolve(ImageConfiguration configuration) {     //略... } 复制代码

ImageStream可以添加一个监听器,其中传入ImageStreamListener对象

---->[src/painting/image_stream.dart:ImageStream#addListener]---- void addListener(ImageStreamListener listener) {   if (_completer != null)     return _completer.addListener(listener);   _listeners ??= <ImageStreamListener>[];   _listeners.add(listener); } 复制代码

ImageStreamListener种有三个回调函数:onChunk在接收到一块字节触发监听
onError在错误时触发监听,onImage在完成时触发监听,如果只是想获取Image,onImage即可

---->[src/painting/image_stream.dart:#ImageStreamListener]---- class ImageStreamListener {   const ImageStreamListener(     this.onImage, {     this.onChunk,     this.onError,   }) : assert(onImage != null);      final ImageListener onImage;   final ImageChunkListener onChunk;   final ImageErrorListener onError; 复制代码

onImage对应的是ImageListener对象,在回调中可以获取ImageInfo对象
Image对象就在这里静静地等着你来。

typedef ImageListener = void Function(ImageInfo image, bool synchronousCall); ---->[src/painting/image_stream.dart:17]---- class ImageInfo {   const ImageInfo({ @required this.image, this.scale = 1.     : assert(image != null),       assert(scale != null);   final ui.Image image;   final double scale; } 复制代码

2.2 :ImageProvider获取Image方法封装

这样的话,完全可以先封装一个通过ImageProvider获取Image的方法

//通过ImageProvider读取Image Future<ui.Image> loadImageByProvider(   ImageProvider provider, {   ImageConfiguration config = ImageConfiguration.empty, }) async {   Completer<ui.Image> completer = Completer<ui.Image>(); //完成的回调   ImageStreamListener listener;   ImageStream stream = provider.resolve(config); //获取图片流   listener = ImageStreamListener((ImageInfo frame, bool sync) {     //监听     final ui.Image image = frame.image;     completer.complete(image); //完成     stream.removeListener(listener); //移除监听   });   stream.addListener(listener); //添加监听   return completer.future; //返回 } 复制代码

2.3 :ImageProvider加载图片

现在使用网络图片测试一下:

https://img1.sycdn.imooc.com//5d7bc1970001ed9e06640255.jpg

  @override   Widget build(BuildContext context) { //var futureFile=ImageLoader.loader.loadImageByFile("/data/data/com.toly1994.flutter_image/cache/wy_300x200.jpg",width: 150,height: 100);   //从资源部获取Image   var futureAsset= ImageLoader.loader.loadImageByProvider(AssetImage("images/wy_300x200.jpg"));   //从网络获取Image   var imageUrl='https://user-gold-cdn.xitu.io/2018/7/9/1647cc06a3e9e9c4?imageView2'       '/1/w/180/h/180/q/85/format/webp/interlace/1';   var futureNet= ImageLoader.loader.loadImageByProvider(NetworkImage(imageUrl));    //从文件获取Image   var path="/data/data/com.toly1994.flutter_image/cache/wy_300x200.jpg";   var futureFile= ImageLoader.loader.loadImageByProvider(FileImage(File(path)));        return Container(       child: FutureBuilder<ui.Image>(         future:futureNet,         builder:(context,snapshot)=>CustomPaint(           painter: ImagePainter(snapshot.data),         ),       ),     );   } } 复制代码

不过发现ImageConfiguration的Size并不能改变图片的展示大小,那该怎么办?
网络图片太大的,想要在本地保存一个缩略图,如何实现?


3.保存网络图片的缩略图

主要通过PictureRecorder对Canvas进行录制,使用Canvas对图片进行重定尺寸。

https://img1.sycdn.imooc.com//5d7bc18100010bf306700216.jpg

///对图片重定义宽高尺寸[dstWidth],[dstHeight] Future<ui.Image> _resize(ui.Image image, int dstWidth,int dstHeight) {   var recorder = ui.PictureRecorder();//使用PictureRecorder对图片进行录制   Paint paint = Paint();   Canvas canvas = Canvas(recorder);   double srcWidth = image.width.toDouble();   double srcHeight = image.height.toDouble();   canvas.drawImageRect(image, //使用drawImageRect对图片进行定尺寸填充       Rect.fromLTWH(0, 0, srcWidth, srcHeight),       Rect.fromLTWH(0, 0, dstWidth.toDouble() ,           dstHeight.toDouble()), paint);   return recorder.endRecording().toImage(dstWidth, dstHeight);//返回图片 } 复制代码

这样就可以定义出重设尺寸的加载方式

///缩放加载[provider],缩放比例[scale] Future<ui.Image> scaleLoad(ImageProvider provider, double scale) async {   var img = await loadImageByProvider(provider);   return _resize(img, (img.width*scale).toInt(),(img.height*scale).toInt()); } ///缩放加载[provider],缩放比例[scale] Future<ui.Image> resizeLoad(ImageProvider provider, int dstWidth,int dstHeight) async {   var img = await loadImageByProvider(provider);   return _resize(img, dstWidth,dstHeight); } 复制代码

如何将一个Image对象保存到本地?Image对象可以转化成字节流,再通过文件写入。

https://img1.sycdn.imooc.com//5d7bc16700016a5e06390191.jpg

//保存一个Image  Future<File> saveImage(ui.Image image,String path,{format=ui.ImageByteFormat.png}) async{   var file= File(path);   if(!await file.exists()){     await file.create(recursive: true);   }   ByteData byteData = await image.toByteData(format:format);   Uint8List pngBytes = byteData.buffer.asUint8List();   return file.writeAsBytes(pngBytes); } 复制代码

通过ImageLoader.loader.saveImage便可以将,缩小0.3倍的图片保存到本地。

var imageUrl='https://user-gold-cdn.xitu.io/2018/7/9/1647cc06a3e9e9c4?imageView2' var path="/data/data/com.toly1994.flutter_image/cache/net/wy_300x200_mini.png"; ImageLoader.loader.scaleLoad(NetworkImage(imageUrl),0.3)         .then((img)=>ImageLoader.loader.saveImage(img,path)); 复制代码

4.网络图片的加载及缓存文件的有效期

对于缓存文件的期限,可以用一个追踪文件进行记录,在访问网络图片时首先看有没有缓存文件
然后看缓存文件有没有过期,如果过期,则删除,重新加载并缓存到本地。
当然你也可以更高级一点使用Json对或数据库,或xml配置来记录缓存的失效期。

https://img1.sycdn.imooc.com//5d7bc13e0001612806730204.jpg

//通过ImageProvider读取Image Future<ui.Image> loadNetImage(String url,     {bool cache = true, scale = 1.0, int deathSecond = 60}) async {   ui.Image image;   var dir = await getTemporaryDirectory();   var name = md5.convert(utf8.encode(url)).toString();   var imgPath = File(path.join(dir.path, name));   var fileDeath = File(imgPath.path + "._cache_death");      if (cache && await imgPath.exists() && !await isCacheDeath(fileDeath)) {//表示有缓存且缓存有效     //设置缓存,并且有缓存文件,并且缓存失效时,写入缓存     image= await loadImageByProvider(FileImage(imgPath));     print("使用缓存");   }else{     image = await loadImageByProvider(NetworkImage(url));     var death = DateTime.now().millisecondsSinceEpoch + deathSecond + 1000 * 60; //过期时间     await fileDeath.writeAsString("$death");     await saveImage(image, imgPath.path);     print("使用网络图片---缓存已重置");   }  return _scale(image, scale); } /// 检查缓存是否过期 Future<bool> isCacheDeath(File fileDeath) async {   if(!await fileDeath.exists()){     return true;   }   var death = await fileDeath.readAsString();   print("$death ==== ${DateTime.now().millisecondsSinceEpoch}--${int.parse(death) > DateTime.now().millisecondsSinceEpoch}");   return int.parse(death) < DateTime.now().millisecondsSinceEpoch; } 复制代码

文章到这就结束了,也许你是被开头的图片吸引来的,这里为了不扫你的兴,源码在此:

/// 图片放大镜的配置类,将图片提供器中的[image],/// 在半径为[radius]的[outlineColor]色圆中局部放大比例[rate]倍,class BiggerConfig {  double rate;  ImageProvider image;  double radius;  Color outlineColor;  bool isClip;  BiggerConfig(      {this.rate = 3,      @required this.image,      this.isClip = true,      this.radius = 30,      this.outlineColor = Colors.white});}class BiggerView extends StatefulWidget {  BiggerView({    Key key,    @required this.config,  }) : super(key: key);  final BiggerConfig config;  @override  _BiggerViewState createState() => _BiggerViewState();}class _BiggerViewState extends State<BiggerView> {  var posX = 0.0;  var posY = 0.0;  bool canDraw = false;  var width =0.0;  var height =0.0;  @override  Widget build(BuildContext context) {    return FutureBuilder<ui.Image>(      future: ImageLoader.loader.loadImageByProvider(widget.config.image),      builder: (context, snapshot) {        if(snapshot.connectionState==ConnectionState.done){           width = snapshot.data.width.toDouble() / widget.config.rate;           height = snapshot.data.height.toDouble() / widget.config.rate;        }        return GestureDetector(          onPanDown: (detail) {            posX = detail.localPosition.dx;            posY = detail.localPosition.dy;            canDraw = true;            setState(() {});          },          onPanUpdate: (detail) {            posX = detail.localPosition.dx;            posY = detail.localPosition.dy;            if (judgeRectArea(posX, posY, width + 2, height + 2)) {              setState(() {});            }          },          onPanEnd: (detail) {            canDraw = false;            setState(() {});          },          child: Container(            width: width,            height: height,            child: CustomPaint(              painter: BiggerPainter(snapshot.data, posX, posY, canDraw,                  widget.config.radius, widget.config.rate, widget.config.isClip),            ),          ),        );      },    );  }  //判断落点是否在矩形区域  bool judgeRectArea(double dstX, double dstY, double w, double h) {    return (dstX - w / 2).abs() < w / 2 && (dstY - h / 2).abs() < h / 2;  }}class BiggerPainter extends CustomPainter {  final ui.Image _img; //图片  Paint mainPaint; //主画笔  Path circlePath; //圆路径  double _x; //触点x  double _y; //触点y  double _radius; //圆形放大区域  double _rate; //放大倍率  bool _canDraw; //是否绘制放大图  bool _isClip; //是否是裁切模式  BiggerPainter(this._img, this._x, this._y, this._canDraw, this._radius, this._rate, this._isClip) {    mainPaint = Paint()      ..color = Colors.white      ..style = PaintingStyle.stroke      ..strokeWidth = 1;    circlePath = Path();  }  @override  void paint(Canvas canvas, Size size) {    Rect rect = Offset.zero & size;    canvas.clipRect(rect); //裁剪区域    if (_img != null) {      Rect src =          Rect.fromLTRB(0, 0, _img.width.toDouble(), _img.height.toDouble());      canvas.drawImageRect(_img, src, rect, mainPaint);      if (_canDraw) {        var tempY = _y;        _y = _y > 2 * _radius ? _y - 2 * _radius : _y + _radius;        circlePath            .addOval(Rect.fromCircle(center: Offset(_x, _y), radius: _radius));        if (_isClip) {          canvas.clipPath(circlePath);          canvas.drawImage(              _img, Offset(-_x * (_rate - 1), -tempY * (_rate - 1)), mainPaint);          canvas.drawPath(circlePath, mainPaint);               } else {          canvas.drawImage(              _img, Offset(-_x * (_rate - 1), -tempY * (_rate - 1)), mainPaint);        }      }    }  }  @override  bool shouldRepaint(CustomPainter oldDelegate) => true;}/// 测试var showBiggerView = Center(  child: BiggerView(    config: BiggerConfig(        image: AssetImage("images/sabar.jpg"), rate: 3, isClip: true),  ),);作者:张风捷特烈链接:https://juejin.im/post/5d79c6dbe51d4561e43a6d3e来源:掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
移动开发工程师
手记
粉丝
20
获赞与收藏
56

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消