作者简介: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 图片压缩库的源码中得到了答案。
不求甚解篇
啰嗦几句,以后本菜鸡写文章都会分为四个阶段,第一阶段,木本水源,主要简单介绍问题的来源,或技一项技术的背景。第二阶段,就是不求甚解,旨在快速针对问题,给出解决方案,和实现步骤。第三阶段,叫做格物致知,丁肇中先生《应有格物致知精神》一文中对本菜鸡受益颇深,做自然科学都要有格物致知的精神,虽然本菜鸡只是个小码农,但是作为一个理工男的职业素养还是要有的。本阶段旨在通过举例或查看源码,进行源码解析,探究某项技术或功能的原理。第四阶段,豹尾小结,少年时期老师作文讲究凤头豹尾猪肚,没错本菜鸡也是个讲究人,最终总结是少不了的哈,也算是聊天吹水环节吧。
Flutter for Web 的图片选择库目前本菜鸡接触到有四个,分别是
image.png一般本菜鸡在为了完成某一项工作,又不想造轮子,那就只好拿来主义(站在巨人的肩膀上),选用好用的第三方库,本菜鸡在选用第三方库时,一般有几个原则,不会选用刚上线,且迭代版本或提交数量小于5次的库;不会选用年久失修的库;如若以上都满足,优先选用官方维护库,优先选择==stars==或者==likes==比较多的库。
所以在集成的过程中,除了第三个库没有尝试以外,都做了尝试,总结下来比较推荐前两个库,分别是image_picker_for_web
和 image_picker_web
,下面也会重点对这两个库的使用和优缺点进行介绍。
image_picker_for_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.
首先在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中都有实现。
image_picker_web
此库推荐的原因是因为支持选择返回类型,相比之前的库多了一层封装,暴露了一个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在手机上时,也是可以访问到相册和相机,集成使用并无难度,所以用法介绍就到此结束了。
dart图片压缩
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”格式==
JPEG格式,也叫做JPG或JPE格式,是最常用的一种文件格式,Photoshop“存储为”命令中默认的图片格式就是JPEG,大部分手机相机拍照的照片也是JPE格式。
JPEG格式的压缩技术十分先进,能够将图像压缩在很小的储存空间,不过这种压缩是有损耗的,过度压缩会降低图片的质量。JPEG格式压缩的主要是高频信息,对色彩的信息保留较好,因此特别适合应用于互联网,可减少图像的传输和加载时间。
- ==“PNG”格式==
PNG也是常见的一种图片格式,它最重要的特点是支持 alpha 通道透明度,也就是说,PNG图片支持透明背景。比如在使用Photoshop制作透明背景的圆形logo时,如果使用JPG格式,则图片背景会默认地存为白色,使用PNG格式则可以存为透明背景图片。
PNG格式图片也支持有损耗压缩,虽然PNG 提供的压缩量比JPG少,但PNG图片却比JPEG图片有更小的文档尺寸,因此现在越来越多的网络图像开始采用PNG格式。
- ==“GIF”格式==
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层面的实现。在做图片转码的过程,是曲折又辛酸的,确实找了很多资料,看了很多博客软文,还是资料太少,不科学上网的话,局限性太大了。虽然Flutter的入门文章教程很多,但是有深度、有质量的文章还是少了一些,特别是Flutter for web,可能大家都是在尝试的原因。Flutter能否一统前端,就要看大家的努力了,让我们一起为Flutter生态建设添砖加瓦吧~
我是努力成为Flutter架构的Flutter小菜鸡,我为自己带盐!
网友评论