Web 開發者如何理解 Flutter 佈局之 —— 3. Image

弘樹 發表於 2019-10-15

影象元件。

  • 通過資源嵌入(Image.asset) 可載入本地資源圖片

  • 通過檔案嵌入(Image.file) 可載入本地圖片檔案

  • 通過網路嵌入(Image.network) 可載入網路圖片

  • 通過記憶體嵌入(Image.memory) 可載入Uint8List(Byte 陣列)資源

  • 通過盒適配(BoxFit) 可分別設定影象的適配配型

    1. BoxFit.contain
    2. BoxFit.cover
    3. BoxFit.fill
    4. BoxFit.none
    5. BoxFit.scaleDown
  • 通過(BlendMode) 可設定圖片的顏色混合選項

1、插入本地資源圖片

首次插入本地圖片前,需要修改 本地專案根路徑 中的檔案 "pubspec.yaml"

找到包含下列程式碼的行:

flutter:
複製程式碼

新增下列行:

  assets:
    - assets/images/
複製程式碼

操作完畢後,你的檔案應該看起來像這樣:

# …
flutter:
  assets:
    - assets/images/
# …
複製程式碼

隨後,你應在專案根路徑中(與 "pubspec.yaml" 同級) 建立下列目錄: assets/images/

即可使用下列程式碼引入存在於專案中的圖片

  new Image.asset(
    "assets/images/thumbnail.png"
  )
複製程式碼

上述程式碼等效於:

  <img src="assets/images/thumbnail.png" />
複製程式碼

有關於 "pubspec.yaml" 檔案修改的詳細資訊,請參照 flutter 中文網中相關章節的描述。

指定 assets flutterchina.club/assets-and-…

2、從檔案系統中插入圖片

[BECARE!] 本示例[2、從檔案系統中插入圖片]程式碼以安卓平臺為執行環境,不能確保其他平臺的穩健及可執行性。

此方法依賴於 path_provider

因此,需要在 "pubspec.yaml" 中新增並安裝此依賴。

開啟 "pubspec.yaml",

在 dependencies: 下新增如下依賴

  path_provider: ^1.3.0
複製程式碼

最新版本的版本號可從下列地址中獲得:

pub.dev/packages/pa…

新增依賴後,你的 "pubspec.yaml" 檔案應該看起來像這樣:

# …
dependencies:
  flutter:
    sdk: flutter
# …
  path_provider: ^1.3.0
# …
複製程式碼

隨後,我們執行下列指令碼安裝所需依賴。

flutter pub get

下列程式實現了從檔案系統中讀取圖片。

** 一開始,檔案系統中不存在這張圖,因此我們需要在裝置中手動儲存一張圖片。 **

這裡使用 getExternalStorageDirectory()進行實現,其在Android系統中的位置為:

/storage/emulated/0/Android/data/應用包名/files/

請在其中新建名為 thumbnail.png 的圖片

下列程式碼實現非同步讀取外部儲存目錄位置,並通過 Image.File 讀取其根路徑下的圖片。

import "package:flutter/material.dart";

// getExternalStorageDirectory 方法依賴於下列 package
import 'package:path_provider/path_provider.dart';

// File 元件依賴於下列 package
import "dart:io";

class Image$FileState extends State<Image$File> {
  //定義 _pathName state, 用於儲存非同步讀取到的系統外部儲存路徑
  String _pathName;

  //定義初始狀態
  @override
  void initState() {
    super.initState();
    //呼叫 _getLocalFile(), 非同步讀取系統外部儲存路徑
    _getLocalFile().then((value) {
      //將其寫入 _pathName 中
      setState(() {
        _pathName = value;
      });
    });
  }

  Future<String> _getLocalFile() async {
    // 獲取外部儲存路徑,暫存於 dir 中
    String dir = (await getExternalStorageDirectory()).path;

    //返回外部儲存路徑下名為 thumbnail.png 的檔案
    return "$dir/thumbnail.png";
  }

  //建立狀態示例
  @override
  Image$FileState createState() => Image$FileState();

  //進行元件具體實現
  Widget build(BuildContext context){
    //由於 _pathName 是非同步獲得,所以在 _pathName 未取到時,需要返回 null
    return null != _pathName ? Image.file(
        File(_pathName)
    ): null;
  }
}

class Image$File extends StatefulWidget {
  @override
  Image$FileState createState() => Image$FileState();
}
複製程式碼

上述示例等效於:

<img src="file:///storage/emulated/0/Android/data/com.demo.flutterapp/files/thumbnail.png" />
複製程式碼

注: 此方法可能需要系統許可權。

安卓可以修改 AndroidManifest.xml

增加下列程式碼即可:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
複製程式碼

3、從網路中插入圖片

new Image.network(
  "https://www.baidu.com/img/bd_logo1.png"
)
複製程式碼

等效於:

<img src="https://www.baidu.com/img/bd_logo1.png" />
複製程式碼

4、從 Uint8List 中獲取圖片

本示例中, 我們使用 base64Decode 方法將 base64 字串轉換為 UInt8List,

之後, 利用 Image.memory() 從記憶體中讀取圖片。

import "package:flutter/material.dart";

//base64Decode 方法依賴於下列 package
import "dart:convert";

class Image$Memory extends StatelessWidget {
  //使用 base64Decode方法將base64字串解碼為 UInt8List
  final List _base64 = base64Decode("R0lGODlhdQAmAKIAAOYyL+rU4llg6Jmd8e92dCky4eEGAv///yH5BAAAAAAALAAAAAB1ACYAAAP/eLrc/jC2IEoZMATJu/9gyFVWIUyksIls677LUJbrEcxWDe98f+CWk4I0w/iOSNANKJQBC8mo9LEEDp8F3XR7rOIU2Cx3jHwKsUKyWqRhEEvGN3xN91BoCq8l9tTW/244Rk4mOkBGgIl8VjF+d4V5A5KKf3IWiCCEOZRraGxPnGqeIZpzoVyjDBptDpYmp1yumI9BWq5QUQS6fn+lm3lYmLdSBsW8xcjJBgAABLwfAMhXRQt6ODWuJ8rb3AYK0d3h4OHIBC7jC0TCYb/ZB+Th3/Dc4/PmLOgSvkwK+xjMAAEmCwhQnrc8rFSxGkiwnoFnEnTp0mdqlJw0DgIggwgu/08xABM+KkhGJdm9JJqMhGkyQIAATA3HxVwATuO8jyQfECiXJyYzZwzyOSilcqWEmzkP1ES6LGmDcStshjspNJAgMOwwNmBajGZXGyJVhU22quxOaR7hrahqQ52DC1jh/nj1AG0Eux3BLgu59x3SkxHO9mVLhJfcirUc2IWA96vUbvf+QkNbldCgIGCKHi6h1e/Bu1+VOiYHYAVTwBHyCfWyocoZzZovbf3ok1njzx/IltUg+GG13twMfta0gdDruLH3MOD6uaPDcI8hCAZp4/k24elmtOacGXlcPAuYe/VmnVt0ncioTyeYU+ibQRWOHyCRXEz40BDyivYWYBzQAIbTbeBUNTIZRF1QlNlVimGdfffLSPg9oJ9+vVV4H23smWSgFqrZZc0AWiTWACMXcjAhfgAqgxpXKwDXDXbZsSPjVcth6NNtZj3nzCpM1dDfPDACM+OQ9l2I1HiPlfNceentMoGLygQp5Eq3aKfYaUgOdBIB9RyYSEsmvASiFgG0RMRLEK2RAAA7");

  //進行元件具體實現
  Widget build(BuildContext context){
    return new Image.memory(
        _base64
    );
  }
}
複製程式碼

上述示例等效於:

<img src="data:image/gif;base64,R0lGODlhdQAmAKIAAOYyL+rU4llg6Jmd8e92dCky4eEGAv///yH5BAAAAAAALAAAAAB1ACYAAAP/eLrc/jC2IEoZMATJu/9gyFVWIUyksIls677LUJbrEcxWDe98f+CWk4I0w/iOSNANKJQBC8mo9LEEDp8F3XR7rOIU2Cx3jHwKsUKyWqRhEEvGN3xN91BoCq8l9tTW/244Rk4mOkBGgIl8VjF+d4V5A5KKf3IWiCCEOZRraGxPnGqeIZpzoVyjDBptDpYmp1yumI9BWq5QUQS6fn+lm3lYmLdSBsW8xcjJBgAABLwfAMhXRQt6ODWuJ8rb3AYK0d3h4OHIBC7jC0TCYb/ZB+Th3/Dc4/PmLOgSvkwK+xjMAAEmCwhQnrc8rFSxGkiwnoFnEnTp0mdqlJw0DgIggwgu/08xABM+KkhGJdm9JJqMhGkyQIAATA3HxVwATuO8jyQfECiXJyYzZwzyOSilcqWEmzkP1ES6LGmDcStshjspNJAgMOwwNmBajGZXGyJVhU22quxOaR7hrahqQ52DC1jh/nj1AG0Eux3BLgu59x3SkxHO9mVLhJfcirUc2IWA96vUbvf+QkNbldCgIGCKHi6h1e/Bu1+VOiYHYAVTwBHyCfWyocoZzZovbf3ok1njzx/IltUg+GG13twMfta0gdDruLH3MOD6uaPDcI8hCAZp4/k24elmtOacGXlcPAuYe/VmnVt0ncioTyeYU+ibQRWOHyCRXEz40BDyivYWYBzQAIbTbeBUNTIZRF1QlNlVimGdfffLSPg9oJ9+vVV4H23smWSgFqrZZc0AWiTWACMXcjAhfgAqgxpXKwDXDXbZsSPjVcth6NNtZj3nzCpM1dDfPDACM+OQ9l2I1HiPlfNceentMoGLygQp5Eq3aKfYaUgOdBIB9RyYSEsmvASiFgG0RMRLEK2RAAA7"
/>
複製程式碼

5、圖片的適配

在瞭解此示例之前,請拷貝下列模板程式碼至專案中。

替換其 Image 元件的內容以檢視效果:

import "package:flutter/material.dart";
import "dart:convert";

class Image$Fit extends StatelessWidget {
  //進行元件具體實現
  Widget build(BuildContext context){
    return new Scaffold(
        body: new Center(
          child: new Container(
              color: Colors.blue,
              child: new Image.network(
                "https://www.baidu.com/img/bd_logo1.png"
              ),
              constraints: BoxConstraints.expand(
                  width: 300,
                  height: 200
              )
          )
        )
    );
  }
}
複製程式碼

這時,可以看到頁面中央有一個藍色容器,裡面伴隨一張圖片。

我們替換此 new Image 中的內容,以瀏覽不同的圖片適配效果。

5.1、contain

在 contain 模式中, 被替換的內容將被縮放, 以在填充元素的內容框時保持其寬高比。

整個物件在填充盒子的同時保留其長寬比,因此如果寬高比與框的寬高比不匹配,該物件將被新增 "白邊"。

new Image.network(
  "https://www.baidu.com/img/bd_logo1.png",
  fit: BoxFit.contain
)
複製程式碼

等效於:

<!--
  此 #demo-wrapper 僅為方便檢視效果而設,
  後續有關 object-fit 的示例均將在此容器內表現 👇
-->
<div
  id="demo-wrapper"
  style=
  "
    width: 300px;
    height: 200px;
    background: blue;
    display: flex;
    flex-wrap: nowrap;
    align-items: flex-start;
  "
>
<!--
  此 #demo-wrapper 僅為方便檢視效果而設,
  後續有關 object-fit 的示例均將在此容器內表現 👆
-->
  <img
    src="https://www.baidu.com/img/bd_logo1.png"
    style=
      "
        width: 100%;
        height: 100%;
        obsect-fit: contain;
      "
  />
</div>
複製程式碼

5.2、contain

在 cover 模式中, 被替換的內容在保持其寬高比的同時填充元素的整個內容框。

如果物件的寬高比與內容框不相匹配, 該物件將被剪裁以適應內容框。

new Image.network(
  "https://www.baidu.com/img/bd_logo1.png",
  fit: BoxFit.cover
)
複製程式碼

等效於:

  <img
    src="https://www.baidu.com/img/bd_logo1.png"
    style=
      "
        width: 100%;
        height: 100%;
        obsect-fit: cover;
      "
  />
複製程式碼

5.3、fill

在 fill 模式中, 被替換的內容正好填充元素的內容框。

整個物件將完全填充此框, 如果物件的寬高比與內容框不相匹配, 那麼該物件將被拉伸以適應內容框。

new Image.network(
  "https://www.baidu.com/img/bd_logo1.png",
  fit: BoxFit.fill
)
複製程式碼

等效於:

  <img
    src="https://www.baidu.com/img/bd_logo1.png"
    style=
      "
        width: 100%;
        height: 100%;
        object-fit: fill;
      "
  />
複製程式碼

5.4、none

在 none 模式中, 被替換的內容將保持其原有的尺寸。

new Image.network(
  "https://www.baidu.com/img/bd_logo1.png",
  fit: BoxFit.none
)
複製程式碼

等效於:

  <img
    src="https://www.baidu.com/img/bd_logo1.png"
    style=
      "
        width: 100%;
        height: 100%;
        object-fit: none;
      "
  />
複製程式碼

5.5、scale-down

在 scale-down 模式中, 內容的尺寸與 none 或 contain 中的一個相同, 取決於它們兩個之間誰得到的物件尺寸會更小一些。

new Image.network(
  "https://www.baidu.com/img/bd_logo1.png",
  fit: BoxFit.scaleDown
)
複製程式碼

等效於:

  <img
    src="https://www.baidu.com/img/bd_logo1.png"
    style=
      "
        width: 100%;
        height: 100%;
        object-fit: scale-down;
      "
  />
複製程式碼

更多有關 object-fit 的資料, 請查閱下面的連結:

developer.mozilla.org/zh-CN/docs/…

5.6、其他適配模式

除上述適配模式外, flutter 支援 Boxfit.fitWidth / Boxfit.fitHeight 等獨佔的適配模式, 這些獨佔的適配模式沒有對應的 web 程式碼實現,請自行了解。

參考資料:

Flutter 圖片如何充滿父佈局 www.jianshu.com/p/8810bacfe…

6、混合模式

new Image.network(
  "https://www.baidu.com/img/bd_logo1.png",
  colorBlendMode: BlendMode.color,
  color: Color(0xFF0000FF)
)
複製程式碼

等效於下列 web 程式碼:

  <div
    style=
    "
      display: inline-block;
      position: relative;
      font-size: 0;
    "
  >
    <div
      style=
      "
        mix-blend-mode: color;
        width: 100%;
        height: 100%;
        position: absolute;
        background-color: #0000FFFF;
      "
    >
    </div>
    <img src="https://www.baidu.com/img/bd_logo1.png" />
  </div>
複製程式碼

其中,Flutter 與 web 等效的表格對映如下:

混合模式 web 實現 flutter 實現 說明
正常 normal 未實現 混合色的畫素會透過所用的顏色顯示出來
正片疊底 multiply BlendMode.multiply 在 「正片疊底」 模式中, 檢視每個通道中的顏色資訊, 並將 「基色」 與「混合色」複合。
濾色 screen BlendMode.screen 「濾色」模式與「正片疊底」模式正好相反, 它將影象的「基色」顏色與「混合色」顏色結合起來產生比兩種顏色都淺的第三種顏色
疊加 overlay BlendMode.overlay 「疊加」模式把影象的「基色」顏色與「混合色」顏色相混合產生一種中間色。
變暗 darken BlendMode.darken 在「變暗」模式中, 檢視每個通道中的顏色資訊, 並選擇「基色」或「混合色」中較暗的顏色作為「結果色」。
變亮 lighten BlendMode.lighten 在「變亮」模式中, 檢視每個通道中的顏色資訊, 並選擇「基色」或「混合色」中較亮的顏色作為「結果色」。
顏色減淡 color-dodge BlendMode.colorDodge 在「顏色減淡」模式中, 檢視每個通道中的顏色資訊, 並通過減小對比度使基色變亮以反映混合色。與黑色混合則不發生變化。
顏色加深 color-burn BlendMode.colorBurn 在「顏色加深」模式中, 檢視每個通道中的顏色資訊, 並通過增加對比度使基色變暗以反映混合色, 如果與白色混合的話將不會產生變化。
強光 hard-light BlendMode.hardLight 「強光」模式將產生一種強光照射的效果。如果「混合色」顏色比「基色」顏色的畫素更亮一些, 那麼「結果色」顏色將更亮;如果「混合色」顏色比「基色」顏色的畫素更暗一些, 那麼「結果色」將更暗。
柔光 soft-light BlendMode.softLight 「柔光」模式會產生一種柔光照射的效果。如果「混合色」顏色比「基色」顏色的畫素更亮一些, 那麼「結果色」將更亮; 如果「混合色」顏色比「基色」顏色的畫素更暗一些, 那麼「結果色」顏色將更暗, 使影象的亮度反差增大。
差值 difference BlendMode.difference 在「差值」模式中, 檢視每個通道中的顏色資訊, 「差值」模式是將從影象中「基色」顏色的亮度值減去「混合色」顏色的亮度值, 如果結果為負, 則取正值, 產生反相效果。
排除 exclusion BlendMode.exclusion 「排除」模式與「差值」模式相似, 但是具有高對比度和低飽和度的特點。比用「差值」模式獲得的顏色要柔和、更明亮一些。
色相 hue BlendMode.hue 「色相」模式只用「混合色」顏色的色相值進行著色, 而使飽和度和亮度值保持不變。
飽和度 saturation BlendMode.saturation 「飽和度」模式的作用方式與「色相」模式相似, 它只用「混合色」顏色的飽和度值進行著色, 而使色相值和亮度值保持不變。
顏色 color BlendMode.color 「顏色」模式能夠使用「混合色」顏色的飽和度值和色相值同時進行著色, 而使「基色」顏色的亮度值保持不變。「顏色」模式模式可以看成是「飽和度」模式和「色相」模式的綜合效果。
亮度 luminosity BlendMode.luminosity 「亮度」模式能夠使用「混合色」顏色的亮度值進行著色, 而保持「基色」顏色的飽和度和色相數值不變。其實就是用「基色」中的「色相」和「飽和度」以及「混合色」的亮度建立「結果色」。

此外, Flutter 支援 BlendMode.plus / BlendMode.dst / BlendMode.src 等其他獨佔混合模式。 這些獨佔的混合模式沒有對應的 web 程式碼實現,請自行了解。

參考資料:

BlendMode(影象混合模式) www.jianshu.com/p/4fb8f1a08…

參考文獻

[1] Flutter基礎視訊教程 技術胖 2018年11月 P10 09.Image元件的使用 www.bilibili.com/video/av358…

[2] css mix-blend-mode 混合模式 blog.csdn.net/Geoooo/arti…