想必你們Image組件都玩得挺6的,那麼如何在Canvas上畫一個圖片,實現圖片的放大等變換又該如何操呢?如何去監聽一個圖片流。這些Image組件就沒法完成了。web
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;
}
}
複製代碼
上面在Canvas的drawImage中,你會看到一個Image參數,你會想,這很差辦嗎?Image傳唄!
可是你傳入一個Image組件它會神奇般地報錯:意思是說人家要的是ui/painting
文件的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方法。canvas
---->[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]';
}
複製代碼
既然沒法建立對象,那怎麼玩?源碼中爲咱們指明道路:使用
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對象
就在那等着你。bash
---->[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;
}
複製代碼
這就要考驗基本功了,記得在File中有一個方法能夠將文件讀成Uint8List網絡
//經過 文件讀取Image
Future<ui.Image> loadImageByFile(String path) async{
var list =await File(path).readAsBytes();
return loadImageByUint8List(list);
}
複製代碼
這裏將一張圖片放入緩存文件夾。再用FutureBuilder優雅地將將來的Image對象傳入畫板中
在畫板中當_image非空時就能夠將Image對象繪製出來。app
---->[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
會不容許加載async
---->[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;
}
}
複製代碼
若是是Asset圖片資源或是網絡圖片如何獲取Image呢?
ImageProvider有一個resolve方法能夠返回一個圖片流ImageStream
做爲它孩子的幾種圖片加載方式都會有該方法,切入點便在此處:ide
---->[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;
}
複製代碼
這樣的話,徹底能夠先封裝一個經過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; //返回
}
複製代碼
如今使用網絡圖片測試一下:
@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並不能改變圖片的展現大小,那該怎麼辦?
網絡圖片太大的,想要在本地保存一個縮略圖,如何實現?
主要經過PictureRecorder對Canvas進行錄製,使用Canvas對圖片進行重定尺寸。
///對圖片重定義寬高尺寸[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對象能夠轉化成字節流,再經過文件寫入。
//保存一個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));
複製代碼
對於緩存文件的期限,能夠用一個追蹤文件進行記錄,在訪問網絡圖片時首先看有沒有緩存文件
而後看緩存文件有沒有過時,若是過時,則刪除,從新加載並緩存到本地。
固然你也能夠更高級一點使用Json對或數據庫,或xml配置來記錄緩存的失效期。
//經過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),
),
);
複製代碼
本文到此接近尾聲了,若是想快速嚐鮮Flutter,《Flutter七日》會是你的必備佳品;若是想細細探究它,那就跟隨個人腳步,完成一次Flutter之旅。
另外本人有一個Flutter微信交流羣,歡迎小夥伴加入,共同探討Flutter的問題,本人微信號:zdl1994328
,期待與你的交流與切磋。