Web 开发者如何理解 Flutter 布局之 —— 3. Image

1,909 阅读8分钟

《Web 开发者如何理解 Flutter 布局》

本系列旨在帮助 Web 前端开发者更好的理解 Flutter 的布局组件,只做对应的语法映射描述和最基本的技术说明。


图像组件。

  • 通过资源嵌入(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/

即可使用下列代码引入存在于项目中的图片

  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、从网络中插入图片

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 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 Scaffold(
        body: Center(
          child: Container(
              color: Colors.blue,
              child: Image.network(
                "https://www.baidu.com/img/bd_logo1.png"
              ),
              constraints: BoxConstraints.expand(
                  width: 300,
                  height: 200
              )
          )
        )
    );
  }
}

这时,可以看到页面中央有一个蓝色容器,里面伴随一张图片。

我们替换此 Image 中的内容,以浏览不同的图片适配效果。

5.1、contain

在 contain 模式中, 被替换的内容将被缩放, 以在填充元素的内容框时保持其宽高比。

整个对象在填充盒子的同时保留其长宽比,因此如果宽高比与框的宽高比不匹配,该对象将被添加 "白边"。

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 模式中, 被替换的内容在保持其宽高比的同时填充元素的整个内容框。

如果对象的宽高比与内容框不相匹配, 该对象将被剪裁以适应内容框。

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 模式中, 被替换的内容正好填充元素的内容框。

整个对象将完全填充此框, 如果对象的宽高比与内容框不相匹配, 那么该对象将被拉伸以适应内容框。

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 模式中, 被替换的内容将保持其原有的尺寸。

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 中的一个相同, 取决于它们两个之间谁得到的对象尺寸会更小一些。

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、混合模式

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…