概覽
圖片常用的格式主要有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的載入速度:
最後附上原始碼:
初學者,還有我做的一個簡單的包: