概览
图片常用的格式主要有bmp,jpg,png,gif,webp
等。图片也是一种二进制文件,每种格式的图片都由固定的头信息和真实数据块组成。图片原始数据每个像素在内存中的占用一般从2byte-4byte。
type | bits | memory |
---|---|---|
ARGB_8888 | 32 | 4*W*H |
ARGB_4444 | 16 | 2*W*H |
RGB_565 | 16 | 2*W*H |
ALPHA_8 | 8 | 1*W*H |
图片加载到内存中的时候,假如是压缩格式则会解压缩为RAW格式,并占用上图中memory中所占用的内存。本文的一些内容会涉及到这些简单的知识,有兴趣的同学可以上网寻找更多资料。
安卓开发者应该都知道Android中并不是天生支持gif和webp动图,但是这一特性在flutter中被很好的支持了。放一张官方的图:

flutter的图像处理是在fluter engine中完成的,但是这个引擎提供的接口都是最基本的图片信息,如何根据设定的属性展示到屏幕上是在flutter中完成的。
在engine中的./lib/ui/painting/codec.cc
文件中展示了调用dart代码的方法:
static sk_sp<SkImage> DecodeImage(fml::WeakPtr<GrContext> context,
sk_sp<SkData> buffer,
size_t trace_id)
这个方法用于生成一个SkImage,并将主要属性映射到flutter中的ui.Image
类中。这个ui.Image
就是可以直接通过canvas渲染到屏幕上的数据。
常见控件
flutter提供了丰富的控件库,但是我们首先要搞清楚一个原理,所有的widget是不能直接绘制图片的,而是作为控制的图片的主要属性的容器,负责绘制的是RenderObject,他们中间通过ElementTree来联系起来。有了这个基础后,所有的widget都不会提供画布(canvas)来直接绘制image,所以在任何一个Widget源码中都不会提供绘制的代码。来看一下主要的Widget:
1. RawImage
这是一个最基础图片容器Widget。它能直接将原始图片呈现到屏幕上,并且有一些列的属性可供操作:
const RawImage({
Key key,
this.image,
this.width,
this.height,
this.scale = 1.0,
this.color,
this.colorBlendMode,
this.fit,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.centerSlice,
this.matchTextDirection = false,
this.invertColors = false,
this.filterQuality = FilterQuality.low,
}) : assert(scale != null),
assert(alignment != null),
assert(repeat != null),
assert(matchTextDirection != null),
super(key: key);
其中image类型就是上边提到的ui.Image
,这个数据的获取官方推荐通过ImageStream
添加listener来获取。
-
color和colorBlendMode
这两个属性可以做出许多的效果。
看一段简单的代码
class _MyHomePageState extends State<MyHomePage> {
ImageInfo info;//图片信息
List<BlendMode> blendModes = BlendMode.values;//所有的混合模式转换为list
@override
void didChangeDependencies() {
super.didChangeDependencies();
Image.asset("images/yuan.png")
.image
.resolve(createLocalImageConfiguration(context))
.addListener((ImageInfo image, bool synchronousCall) {
setState(() {
info = image; //刷新状态
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: GridView.builder(
itemCount: blendModes.length - 1,
padding: EdgeInsets.only(top: 10.0),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
),
itemBuilder: getItemBuilder,
),
);
}
Widget getItemBuilder(BuildContext context, int index) {
return Column(
children: <Widget>[
RawImage(
image: info?.image,
color: Colors.red,
width: 40,
height: 40,
colorBlendMode: blendModes[index + 1],
fit: BoxFit.cover,
),
Container(
padding: EdgeInsets.only(top: 10.0),
child: Text(
blendModes[index + 1].toString().split("\.")[1],
style: TextStyle(
color: Colors.black,
fontSize: 15.0,
),
),
),
],
);
}
}
flutter中的混合模式是枚举类型,和Android中的图片混合模式画笔混合模式基本保持一致。上面的代码描述了所有的混合模式并配有图,除了clear没有在里边(clear模式会清除所有内容)。image是一个简单的图片,带透明通道的绿色的圆,在图中就是dst模式下的样子,背景是一个纯红色,在图中就是src模式下的样子。

-
fit属性使用的是Boxfit的枚举值,看一下效果:
-
fill
填充,忽略原有的宽高比,填满为止

-
contain
包含,不改变原有比例让容器包含整个图片,容器多余部分填充背景

-
cover
覆盖,不改变原有比例,让图片充满整个容器,图片多余部分裁剪 ///

-
fitWidth
横向图片填充

-
fitHeight
纵向图片填充

-
none
原始大小居中

-
scaleDown
图片大小小于容器事相当于none,图片大小大于容器时缩小图片大小实现contain

centerSlice属性专门用于nine-patch文件。
其他属性暂时不讲。
一般情况下这个控件很少使用,但是他是其他Image控件的实现基础,所以必须要拎出来讲一下。
Image
这是一个通用包装类,它包装了RawImage,同时提供了一些简便的Named constructors
来使用AssetsImage,ExactAssetImage等ImageProvider的子类。
- Image, 从ImageProvider来获取图片显示
这个类的使用基本和RawImage一致,在使用的时候只是将参数ui.Image
包装为了ImageProvider
,不用再自己监听ImageStream。典型简单用法:
Widget image = Image(AssetImage("images/yuan.png"))
- Image.asset, 从Asset资源中获取图片显示
这个方法是ImageProvider
为AssetImage
的简单用法:
Widget image = Image.asset("images/yuan.png")
- Image.network, 从URL获取网络图片显示
这个方法是ImageProvider
为NetworkImage
的简单用法:
Widget image = Image.network("http://img.rangaofei.cn/01b18.jpg")
- Image.file, 从文件中获取图片显示
这个方法是ImageProvider
为FileImage
的简单用法:
Widget image = Image.file(file)
- Image.memory 从内存中获取图片显示.
这个方法是ImageProvider
为MemoryImage
的简单用法:
Widget image = Image.memory(byteList)
CircleAvatar
主要用来显示用户的头像,任何图片都会被剪切为圆形。
一个简单用法:
CircleAvatar(
child: Text("头像"),
backgroundImage: AssetImage("images/yuan.png"),
backgroundColor: Colors.red,
radius: 50.0,
),
生成的图像如下:

CircleAvatar
内置了许多的功能。radius用来控制图片的大小,同时它可以自动感知当前theme是白天模式还是夜间模式来切换图片颜色,另外它实际是包装了AnimatedContainer
,设置的动画时间是200ms。在改变它的一些相关属性时会自动使用动画来执行。看一个简单的动图:

代码如下:
class _MyHomePageState extends State<MyHomePage> {
double radius = 10.0;
@override
void initState() {
super.initState();
Future<Duration>.delayed(Duration(milliseconds: 2 * 1000), () {
setState(() {
radius = 20.0;
});
return Duration(milliseconds: 210);
}).then((Duration d) {
Future<Duration>.delayed(d, () {
setState(() {
radius = 40.0;
});
return Duration(milliseconds: 210);
}).then((Duration d) {
Future<Duration>.delayed(d, () {
setState(() {
radius = 30.0;
});
});
});
;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: CircleAvatar(
child: Text("头像"),
backgroundImage: AssetImage("images/yuan.png"),
radius: radius,
),
),
);
}
}
这里我并没有使用AnimationController来控制radius值的变化,而是通过一个Future延时来控制。初始化的时候radius是10.0,延迟两秒后变为20.0,因为CircleAvatar默认的过度时间是200ms,为了有一个平滑的过渡效果,我把下一次改变时间设置为了210ms,这时半径是40.0,最后经过210ms后半径设置为30.0,整个变化过程为:
10->20->40->30
DecorationImage
主要用于BoxDecoration
中的image属性,可以讲图片展示为boxdecoration。这里不做详细解释。
Widget getBoxImage() {
return Container(
decoration: BoxDecoration(
image: DecorationImage(image: AssetImage("images/yuan.png"))),
);
}
这个并不是一个Widget,只能用在BoxDecoration
Ink.image
同样很简单,用于显示一张图片。 这里相当于Ink一个简单写法,这个Ink控件里边只有decoration.image属性的话,可以直接替代Ink。
Widget getInkedImage() {
return Ink.image(image: AssetImage("images/timg.jpeg"));
}
ImageIcon
基本和上边的一致效果:
Widget getImageIcon() {
return ImageIcon(AssetImage("images/timg.jpeg"));
}
FadeInImage
占位图渐变动画控件,这个控件在加载网络图片时经常用到。主要作用是在加载网络图片这个耗时操作时显示一个占位图,并在获取到网络图片时以alpha动画让占位图淡出,网络图片淡入的效果。
看一下效果图

但是这个并不是一个银弹,在加载单帧图片时确实可以达到很好的效果,但是在加载网络动图时会丢失显示plcaceholder淡出时的所有帧,同样在网络图片错误时并不会有错误的占位图,而是直接抛出异常。这里不详细解释了。
Image的加载过程
上边介绍了一些基本的Image控件,这些都是属于Widget层使用的,但是他们的主要依赖都是ImageProvider
,我们可以通过订制ImageProvider
来实现自己的加载方式。
具体流程如图

uint8list是dart中的一个高效的byte数组存储类,用它来存储图片的二进制数据可以更好的缩短转换时间。
绝大多数图片的加载过程都是将图片资源转换为uint8list,处理uint8list为ui.Image的过程是在Flutter engine中的skia中完成的,它不仅包装好了ImageInfo,同时包装好了FrameInfo到Codec中,然后ImageStream
通过ImageStreamCompleter
来读取Codec中的数据每一帧数据.ImageStreamCompleter
是一个抽象类,共有两个实现类,单帧解析器和多帧解析器,一般默认使用多帧解析器。
在解析出来的ImageInfo中可以通过Future<ByteData> toByteData({ImageByteFormat format: ImageByteFormat.rawRgba})
方法改变ImageByte的数据格式:
- rawRgba,未解码的byte,每个通道占8bit
- rawUnmodified,未解码且为修改的byte,例如灰度图
- png,最常见的无损数据格式
我们可以通过给这个多帧解析器做一些手脚来达到控制gif动画的目的。
这里有我做的一个控件来控制gif的加载速度:

最后附上源码:
初学者,还有我做的一个简单的包: