做者简介:Flutter小菜鸡,5年经验移动端开发工程师,努力成为Flutter架构的小菜鸡,现就任于某丰某大数据产品开发处,担任移动端搬砖码农,专一于移动端数据可视化研究,目前没有任何能够拿出来讲的成绩【手动狗头】前端
Flutter for web自发布以来,很多高等级的玩家已经对此进行了尝鲜,评论也褒贬不一,有的说很难用,谁用谁知道。先不说好很差用,但从格局出发,Flutter的野心很大,想要在大前端领域实现 大一统,勇气可嘉。Flutter for web整体上手感受,对于一个没有web开发经验的人来讲,稍稍有一点点难度,但难度自己并非来自Flutter for web 框架,而是对web领域的未知,整体来讲,作过Flutter app开发,上手web开发一些简单的页面是没有问题的。可是也会有些很经常使用的进阶一点的操做,好比操做数据库,好比上传图片、或文件,拍照、定位获取GIS信息等。其中在作图片选择上传时,作图片选择并无什么难度,图片选择库支持web的也有几个。可是在上传后端时(起初方案不对,后面会介绍到)并无想象的那么顺利,觉得拍照的问题,图片转码后的base64字符串会很是大,若是只是在本地作转码解码显示的操做,Flutter的性能是彻底可以支撑的,而且不会形成卡顿,可是在和后台交互时,可能会由于图片过大,而致使接口缓慢。由于Flutter web生态环境的问题,不少图片选择库,并不支持web项目,且Flutter 官方的image_picker_for_web,也是标注了[UNIDENTIFIED],本菜鸡也是尝试了不少种方法。最终在Flutter_luban 图片压缩库的源码中获得了答案。git
啰嗦几句,之后本菜鸡写文章都会分为四个阶段,第一阶段,木本水源,主要简单介绍问题的来源,或技一项技术的背景。第二阶段,就是不求甚解,旨在快速针对问题,给出解决方案,和实现步骤。第三阶段,叫作格物致知,丁肇中先生《应有格物致知精神》一文中对本菜鸡受益颇深,作天然科学都要有格物致知的精神,虽然本菜鸡只是个小码农,可是做为一个理工男的职业素养仍是要有的。本阶段旨在经过举例或查看源码,进行源码解析,探究某项技术或功能的原理。第四阶段,豹尾小结,少年时期老师做文讲究凤头豹尾猪肚,没错本菜鸡也是个讲究人,最终总结是少不了的哈,也算是聊天吹水环节吧。github
Flutter for Web 的图片选择库目前本菜鸡接触到有四个,分别是web
库名 | 最新版本 -最后更新 | GitHub Stars & Likes |
---|---|---|
image_picker_for_web | 0.1.0+1 - [Jun 5, 2020] | 官方维护 |
image_picker_web | 1.0.9 - [May 16, 2020] | 15 |
flutter_web_image_picker | 0.0.2 - [Oct 8, 2019] | 28 |
file_picker_cross | 2.1.0 - [Jun 16, 2020] | 15 |
通常本菜鸡在为了完成某一项工做,又不想造轮子,那就只好拿来主义(站在巨人的肩膀上),选用好用的第三方库,本菜鸡在选用第三方库时,通常有几个原则,不会选用刚上线,且迭代版本或提交数量小于5次的库;不会选用年久失修的库;如若以上都知足,优先选用官方维护库,优先选择==stars==或者==likes==比较多的库。算法
因此在集成的过程当中,除了第三个库没有尝试之外,都作了尝试,总结下来比较推荐前两个库,分别是image_picker_for_web
和 image_picker_web
,下面也会重点对这两个库的使用和优缺点进行介绍。数据库
先说第一个,官方维护的image_picker_for_web
这个库在介绍文档中也有提到,须要配合另外一个官方库image_picker
,后端
This package is an ==unendorsed== web platform implementation of image_picker. In order to use this, you'll need to depend in image_picker: ^0.6.7 (which was the first version of the plugin that allowed federation), and image_picker_for_web: ^0.1.0.api
首先在pubspec.yaml文件中的dependencies中添加依赖,导入这两个库数组
...
dependencies:
...
image_picker: ^0.6.7
image_picker_for_web: ^0.1.0
...
...
复制代码
在使用的地方导入文件浏览器
import 'package:image_picker/image_picker.dart';
复制代码
==须要注意的是,image_picker从0.6.7以后使用方法有所变动,抛弃了以前ImagePicker().getImage 相似这样的写法==
这里就只写出最新的写法:
File _image;
//须要先构造一个ImagePicker对象
final picker = ImagePicker();
//获取图片方法
Future getImage() async {
//返回一个pickedFile对象
final pickedFile = await picker.getImage(source: ImageSource.camera);
//更新状态
setState(() {
_image = File(pickedFile.path);
});
}
复制代码
须要注意的是getImage方法返回的是PickedFile类型的对象,跟File的关系能够看一下官方给PickedFile的解释
The interface for a PickedFile.
A PickedFile is a container that wraps the path of a selected file by the user and (in some platforms, like web) the bytes with the contents of the file.
This class is a very limited subset of dart:io [File], so all the methods should seem familiar.
根据字面意思很好理解,说PickedFile是Flie的一个有限子集,而且Flie class经常使用的属性有path,经常使用的方法有readAsBytes()、openRead()等,在PickedFile中都有实现。
此库推荐的缘由是由于支持选择返回类型,相比以前的库多了一层封装,暴露了一个ImgaeType
给到咱们已于调用。这也是本菜鸡认为这个库比较人性化的地方
用法:
首先在pubspec.yaml文件中的dependencies中添加依赖,导入这个库
dependencies:
image_picker_web: ^1.0.9
复制代码
在使用的地方导入文件
import 'package:image_picker_web/image_picker_web.dart';
复制代码
Image pickedImage;
pickImage() async {
//ImageType一共有三种类型可选
//file
//bytes
//widget
Image fromPicker = await ImagePickerWeb.getImage(outputType: ImageType.widget);
if (fromPicker != null) {
setState(() {
pickedImage = fromPicker;
});
}
}
复制代码
上面两个库不只是支持PC环境的web(目前只测试了Chrome浏览器) 图片选择,并且web项目run在手机上时,也是能够访问到相册和相机,集成使用并没有难度,因此用法介绍就到此结束了。
ok,集成完成以后,就要考虑适用性和优化的问题了。如今的手机像素都很高,拍一张无损高清照片,3-5M算是正常,可是即便再高清的图片在微信的传输中是很是丝滑的,一方面是微信的图片优化无疑是很是棒的,还有就是缩略图和原图分时异步加载,微信的原图只有当你点击下载原图才会从服务器下载,因此平时看到的都是微信已经压缩过的图片,内存已经很是小了,固然在图片压缩处理方面也是有不少优秀的第三方算法或者已经集成过的Flutter pub库,好比luban(鲁班)压缩算法flutter_luban
等。
固然了Flutter官方也会考虑到这个事情,因此在
image_picker_for_web
库也是继承了image_picker
的属性,可以传入maxWidth
maxHeight
imageQuality
三个属性来约束图片的大小和质量,可是不知为什么,在web项目上,这几个属性并无生效。已经在Github上的Flutter项目中提交了Issuee。或者有知道的巨佬也能够告知一下本菜鸡,还望不吝赐教。
其实图片压缩,自己并非很复杂的东西,在APP项目中使用MethodChannel调用native的压缩api,其中flutter_image_compress
库就是这么作的,固然还有借助dart:Image的压缩方法来实现的,此方式在web端和app端一样适用,因此我在作图片压缩时,借鉴了flutter_luban
库中的源码,使用dart自带的压缩方法,只在质量上进行了压缩。(只压缩了质量,图片会失真)
import 'dart:convert';
import 'package:image/image.dart';
class ImageDelegate {
//...
//图片压缩部分代码
String compressImgage(List<int> data) {
Image image = decodeImage(data);
List<int> result = encodeJpg(image, quality: 70);
String imageStr = base64.encode(result);
return imageStr;
}
}
复制代码
须要注意的是,这里用到的Image类型,是package:image/image.dart
中的类型,并不是咱们经常使用的widget组件package:flutter/src/widgets/image.dart
类型,因此建议把压缩方法单独写一个工具类。这种方式就是运用的dart系统方法对图片进行压缩。
还有人会有疑问了,为何不直接用flutter_image_compress
相似的压缩库直接压缩便可,在这琢磨什么dart自带的压缩方法有什么意义,须要了解的是flutter_image_compress
等图片压缩库目前所支持的平台依旧是Android&iOS,因此web平台是没有办法经过目前除了flutter_luban
以外的库进行压缩的,由于目前图片压缩库通常都是methodChannel调用原生API进行的压缩,iOS代码上通常调用的的是SDWebImgae
的图片压缩方法,在Android代码使用Android系统api。
接下来的部分咱们就来详细剖析一下,Flutter的图片选择器和上图片压缩的问题吧。本菜鸡也是查阅了海量资料,写了不少demo,有了一些本身的理解,接下来就讲一下我本身的理解,你们一块儿来探究一下图片选择器的一些细节问题和图片压缩的原理吧。发现问题的或者有不一样意见的均可以私聊本菜鸡微信,或者在留言,欢迎交流。
先说一下图片选择器的流程,咱们手机中的图片是怎么转换为Image对象或者字节流而上传到后台呢?
以image_picker
为例:
源码解析
///method_channel_image_picker.dart
//...
@override
Future<PickedFile> pickImage({
@required ImageSource source,
double maxWidth,
double maxHeight,
int imageQuality,
CameraDevice preferredCameraDevice = CameraDevice.rear,
}) async {
String path = await pickImagePath(
source: source,
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: imageQuality,
preferredCameraDevice: preferredCameraDevice,
);
return path != null ? PickedFile(path) : null;
}
//核心方法
//maxWidth 返回图片的最大宽度
//maxHeight 返回图片的最大高度
//imageQuality 返回图片的质量
// 在image_picker_for_web中,上面三个属性失效(缘由未查明)
//经过MerthodChannel发起_channel.invokeMethod()调用 method name = 'pickImage'的通道方法。
@override
Future<String> pickImagePath({
@required ImageSource source,
double maxWidth,
double maxHeight,
int imageQuality,
CameraDevice preferredCameraDevice = CameraDevice.rear,
}) {
assert(source != null);
if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) {
throw ArgumentError.value(
imageQuality, 'imageQuality', 'must be between 0 and 100');
}
if (maxWidth != null && maxWidth < 0) {
throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative');
}
if (maxHeight != null && maxHeight < 0) {
throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative');
}
return _channel.invokeMethod<String>(
'pickImage',
<String, dynamic>{
'source': source.index,
'maxWidth': maxWidth,
'maxHeight': maxHeight,
'imageQuality': imageQuality,
'cameraDevice': preferredCameraDevice.index
},
);
}
复制代码
前面有提到PickedFile类型的返回值,无论是Flie仍是PickedFile都有一个path属性具体打印值为: blob:http://localhost:62137/b7239cc7-ec85-40fc-a4c7-07f5b920771e
,能够看到是一个blob对象,前端范畴,为此本菜鸡特地百度了一下,此处的path为何不是图片的绝对路径,且blob究竟是什么玩意儿,本菜鸡的理解为,blob是基于浏览器内部对绝对路径的一个封装,防止爬虫爬取数据,而且此连接只能在浏览器内部进行访问,那就没什么问题了。
这个话题提及来就很大了,先从图片类型入手,列举几个图片格式
JPEG格式,也叫作JPG或JPE格式,是最经常使用的一种文件格式,Photoshop“存储为”命令中默认的图片格式就是JPEG,大部分手机相机拍照的照片也是JPE格式。
JPEG格式的压缩技术十分先进,可以将图像压缩在很小的储存空间,不过这种压缩是有损耗的,过分压缩会下降图片的质量。JPEG格式压缩的主要是高频信息,对色彩的信息保留较好,所以特别适合应用于互联网,可减小图像的传输和加载时间。
PNG也是常见的一种图片格式,它最重要的特色是支持 alpha 通道透明度,也就是说,PNG图片支持透明背景。好比在使用Photoshop制做透明背景的圆形logo时,若是使用JPG格式,则图片背景会默认地存为白色,使用PNG格式则能够存为透明背景图片。
PNG格式图片也支持有损耗压缩,虽然PNG 提供的压缩量比JPG少,但PNG图片却比JPEG图片有更小的文档尺寸,所以如今愈来愈多的网络图像开始采用PNG格式。
GIF也是一种压缩的图片格式,分为动态GIF和静态GIF两种。
GIF格式的最大特色是支持动态图片,而且支持透明背景。网络上绝大部分动图、表情包都是GIF格式的,相比与动画,GIF动态图片占用的存储空间小,加载速度快,所以很是流行。
除了罗列的三种,还有==BMP、PSD、SVG==等图片格式。
图片压缩的技术原理层面本菜鸡在此就不作太多解释了,感兴趣的能够看一下
小蝌蚪传记:PNG图片压缩原理--屌丝的眼泪 #1 (==关于图片、色彩基础理论、视频等,在这篇文章最后有连接==)
咱们在此只关心Flutter端的图片压缩处理在dart层的展示,因为前面说到flutter_image_compress
是借助native api实现的图片压缩,且目前只支持在APP端运行,最近一直在看dart源码层面的东西,因此咱们仍是拿flutter_luban
库来进行源码解析,由于只有flutter_luban
库是实现了web端的图片压缩。
其实flutter_luban
库并无很复杂的项目结构,只有一个flutter_luban.dart
文件,只是鲁班压缩算法在Flutter端的移植,因此咱们直接贴出关键源码逐句分析便可。
//鲁班压缩库核心代码
static String _lubanCompress(CompressObject object) {
//根据CompressObject对象中的File经过Uint8List的readAsBytesSync()方法获取到List<int>数组
//经过Image中的decodeImage()初始化image对象
//注意:此Imgae对象为'package:image/image.dart'中的对象,并不是咱们经常使用的Widget对象
Image image = decodeImage(object.imageFile.readAsBytesSync());
//获取file长度并打印
var length = object.imageFile.lengthSync();
print(object.imageFile.path);
bool isLandscape = false;
//jpg类型数组
const List<String> jpgSuffix = ["jpg", "jpeg", "JPG", "JPEG"];
//png类型数组
const List<String> pngSuffix = ["png", "PNG"];
//调用_parseType()方法判断图片类型
bool isJpg = _parseType(object.imageFile.path, jpgSuffix);
bool isPng = false;
if (!isJpg) isPng = _parseType(object.imageFile.path, pngSuffix);
//初始化size width height
double size;
int fixelW = image.width;
int fixelH = image.height;
//
double thumbW = (fixelW % 2 == 1 ? fixelW + 1 : fixelW).toDouble();
double thumbH = (fixelH % 2 == 1 ? fixelH + 1 : fixelH).toDouble();
//横纵比
double scale = 0;
if (fixelW > fixelH) {
scale = fixelH / fixelW;
var tempFixelH = fixelW;
var tempFixelW = fixelH;
fixelH = tempFixelH;
fixelW = tempFixelW;
isLandscape = true;
} else {
scale = fixelW / fixelH;
}
var decodedImageFile;
//目前只支持jpg和png的压缩,不然抛出异常提示
if (isJpg)
decodedImageFile = new File(
object.path + '/img_${DateTime.now().millisecondsSinceEpoch}.jpg');
else if (isPng)
decodedImageFile = new File(
object.path + '/img_${DateTime.now().millisecondsSinceEpoch}.png');
else
throw Exception("flutter_luban don't support this image type");
//同步检查decodedImageFile文件是否存在
if (decodedImageFile.existsSync()) {
//同步删除decodedImageFile文件
decodedImageFile.deleteSync();
}
//根据图片的横纵比例和传入的图片大小从新计算图片size
var imageSize = length / 1024;
if (scale <= 1 && scale > 0.5625) {
if (fixelH < 1664) {
if (imageSize < 150) {
decodedImageFile
.writeAsBytesSync(encodeJpg(image, quality: object.quality));
return decodedImageFile.path;
}
size = (fixelW * fixelH) / pow(1664, 2) * 150;
size = size < 60 ? 60 : size;
} else if (fixelH >= 1664 && fixelH < 4990) {
thumbW = fixelW / 2;
thumbH = fixelH / 2;
size = (thumbH * thumbW) / pow(2495, 2) * 300;
size = size < 60 ? 60 : size;
} else if (fixelH >= 4990 && fixelH < 10240) {
thumbW = fixelW / 4;
thumbH = fixelH / 4;
size = (thumbW * thumbH) / pow(2560, 2) * 300;
size = size < 100 ? 100 : size;
} else {
int multiple = fixelH / 1280 == 0 ? 1 : fixelH ~/ 1280;
thumbW = fixelW / multiple;
thumbH = fixelH / multiple;
size = (thumbW * thumbH) / pow(2560, 2) * 300;
size = size < 100 ? 100 : size;
}
} else if (scale <= 0.5625 && scale >= 0.5) {
if (fixelH < 1280 && imageSize < 200) {
decodedImageFile
.writeAsBytesSync(encodeJpg(image, quality: object.quality));
return decodedImageFile.path;
}
int multiple = fixelH / 1280 == 0 ? 1 : fixelH ~/ 1280;
thumbW = fixelW / multiple;
thumbH = fixelH / multiple;
size = (thumbW * thumbH) / (1440.0 * 2560.0) * 200;
size = size < 100 ? 100 : size;
} else {
int multiple = (fixelH / (1280.0 / scale)).ceil();
thumbW = fixelW / multiple;
thumbH = fixelH / multiple;
size = ((thumbW * thumbH) / (1280.0 * (1280 / scale))) * 500;
size = size < 100 ? 100 : size;
}
//若是原始图片size小于计算完毕后图片size
//则调用Image encodeJpg()方法进行质量压缩,并同步写入缓存且返回路径
if (imageSize < size) {
decodedImageFile
.writeAsBytesSync(encodeJpg(image, quality: object.quality));
return decodedImageFile.path;
}
//若是原始图片size大于计算完毕后图片size
//根据横竖方向,调用copyResize()方法重设宽高属性给smallerImage赋值
Image smallerImage;
if (isLandscape) {
smallerImage = copyResize(image,
width: thumbH.toInt(),
height: object.autoRatio ? null : thumbW.toInt());
} else {
smallerImage = copyResize(image,
width: thumbW.toInt(),
height: object.autoRatio ? null : thumbH.toInt());
}
if (decodedImageFile.existsSync()) {
decodedImageFile.deleteSync();
}
//根据传入的CompressMode枚举类型,调用对应的CompressImage()方法
//本质都是调用Image encodeJpg()方法进行质量压缩,只是在image size上作了调整
if (object.mode == CompressMode.LARGE2SMALL) {
_large2SmallCompressImage(
image: smallerImage,
file: decodedImageFile,
quality: object.quality,
targetSize: size,
step: object.step,
isJpg: isJpg,
);
} else if (object.mode == CompressMode.SMALL2LARGE) {
_small2LargeCompressImage(
image: smallerImage,
file: decodedImageFile,
quality: object.step,
targetSize: size,
step: object.step,
isJpg: isJpg,
);
} else {
if (imageSize < 500) {
_large2SmallCompressImage(
image: smallerImage,
file: decodedImageFile,
quality: object.quality,
targetSize: size,
step: object.step,
isJpg: isJpg,
);
} else {
_small2LargeCompressImage(
image: smallerImage,
file: decodedImageFile,
quality: object.step,
targetSize: size,
step: object.step,
isJpg: isJpg,
);
}
}
return decodedImageFile.path;
}
复制代码
==图片压缩步骤总结:==
传入图片CompressObject对象(此对象为鲁班库自定义对象类型),主要理解为传入图片path便可
根据图片路径获取Uint8List并转换为'package:image/image.dart'
库对应的Image对象。
鲁班压缩算法,主要用于图片size计算,纵横比比例分为四个区间,分别计算出结果size
利用copyResize()
方法传入计算结果size生成smallImage对象
利用dart原生api encodeJpg()
方法进行质量压缩
这篇文章主要是针对Flutter for web的图片选择及压缩,经过对比图片选择库,图片压缩库,进行了源码分析,并列出了图片压缩的大概步骤。整体来讲仍是比较详细的分析了图片选择和压缩的过程及步骤,包括dart层面的实现。在作图片转码的过程,是曲折又辛酸的,确实找了不少资料,看了不少博客软文,仍是资料太少,不【science network】的话,局限性太大了。虽然Flutter的入门文章教程不少,可是有深度、有质量的文章仍是少了一些,特别是Flutter for web,可能你们都是在尝试的缘由。Flutter可否一统前端,就要看你们的努力了,让咱们一块儿为Flutter生态建设添砖加瓦吧~
我是努力成为Flutter架构的Flutter小菜鸡,我为本身带盐!