Flutter中的Image入門講解

saka發表於2018-12-12

概覽

圖片常用的格式主要有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中的Image入門講解

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模式下的樣子。

Flutter中的Image入門講解

  • fit屬性使用的是Boxfit的列舉值,看一下效果:

  • fill

填充,忽略原有的寬高比,填滿為止

Flutter中的Image入門講解

  • contain

包含,不改變原有比例讓容器包含整個圖片,容器多餘部分填充背景

Flutter中的Image入門講解

  • cover

覆蓋,不改變原有比例,讓圖片充滿整個容器,圖片多餘部分裁剪 ///

Flutter中的Image入門講解

  • fitWidth

橫向圖片填充

Flutter中的Image入門講解

  • fitHeight

縱向圖片填充

Flutter中的Image入門講解

  • none

原始大小居中

Flutter中的Image入門講解

  • scaleDown

圖片大小小於容器事相當於none,圖片大小大於容器時縮小圖片大小實現contain

Flutter中的Image入門講解


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資源中獲取圖片顯示

這個方法是ImageProviderAssetImage的簡單用法:

Widget image = Image.asset("images/yuan.png")
複製程式碼
  • Image.network, 從URL獲取網路圖片顯示

這個方法是ImageProviderNetworkImage的簡單用法:

Widget image = Image.network("http://img.rangaofei.cn/01b18.jpg")
複製程式碼
  • Image.file, 從檔案中獲取圖片顯示

這個方法是ImageProviderFileImage的簡單用法:

Widget image = Image.file(file)
複製程式碼
  • Image.memory 從記憶體中獲取圖片顯示.

這個方法是ImageProviderMemoryImage的簡單用法:

Widget image = Image.memory(byteList)
複製程式碼

CircleAvatar

主要用來顯示使用者的頭像,任何圖片都會被剪下為圓形。

一個簡單用法:

 CircleAvatar(
          child: Text("頭像"),
          backgroundImage: AssetImage("images/yuan.png"),
          backgroundColor: Colors.red,
          radius: 50.0,
        ),
複製程式碼

生成的影像如下:

Flutter中的Image入門講解

CircleAvatar內建了許多的功能。radius用來控制圖片的大小,同時它可以自動感知當前theme是白天模式還是夜間模式來切換圖片顏色,另外它實際是包裝了AnimatedContainer,設定的動畫時間是200ms。在改變它的一些相關屬性時會自動使用動畫來執行。看一個簡單的動圖:

Flutter中的Image入門講解

程式碼如下:

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動畫讓佔點陣圖淡出,網路圖片淡入的效果。

看一下效果圖

Flutter中的Image入門講解

但是這個並不是一個銀彈,在載入單幀圖片時確實可以達到很好的效果,但是在載入網路動圖時會丟失顯示plcaceholder淡出時的所有幀,同樣在網路圖片錯誤時並不會有錯誤的佔點陣圖,而是直接丟擲異常。這裡不詳細解釋了。

Image的載入過程

上邊介紹了一些基本的Image控制元件,這些都是屬於Widget層使用的,但是他們的主要依賴都是ImageProvider,我們可以通過訂製ImageProvider來實現自己的載入方式。

具體流程如圖

Flutter中的Image入門講解

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的資料格式:

  1. rawRgba,未解碼的byte,每個通道佔8bit
  2. rawUnmodified,未解碼且為修改的byte,例如灰度圖
  3. png,最常見的無損資料格式

我們可以通過給這個多幀解析器做一些手腳來達到控制gif動畫的目的。

這裡有我做的一個控制元件來控制gif的載入速度:

Flutter中的Image入門講解

最後附上原始碼:

github.com/rangaofei/s…

初學者,還有我做的一個簡單的包:

pub.flutter-io.cn/packages/sa…

相關文章