0.前言
Image.memory 可以通过 Uint8List 存储的图像字节数据来构造一个 Image 部件,数据的内容需要包含格式头,裸数据他是没法解析的。
Uint8List? bytes;
int width;
int height;Image.memory(bytes!,width: width.toDouble(),height: height.toDouble(),// 无间隔播放,防止闪烁gaplessPlayback: true,// 保持宽高比例fit: BoxFit.contain,
)
如果是从文件读取的带格式头的数据,可以直接使用;如果是自己生成或者处理过的位图裸数据,可以自己编码生成位图格式的数据。
1.使用 image 工具包进行编码
链接:image | Dart Package (flutter-io.cn)
先用命令安装这个包:
flutter pub add image
主要用到两个接口: Image.fromBytes 和 BmpEncoder.encode
import 'dart:typed_data';
import 'package:flutter/material.dart';// 先安装 image 组件:flutter pub add image
import 'package:image/image.dart' as image_lib;void main() {runApp(const MyApp());
}class MyApp extends StatefulWidget {const MyApp({super.key});@overrideState<MyApp> createState() => _MyApp();
}class _MyApp extends State<MyApp> {Uint8List? _bytes;int _width = 0;int _height = 0;@overrideWidget build(BuildContext context) {// MaterialApp 创建一个基于 Material Design 风格的应用return MaterialApp(title: "Test Flutter",// Scaffold 是一个布局,有 appBar body persistentFooterButtons 等区域home: Scaffold(backgroundColor: Colors.white,// ConstrainedBox 对其子组件添加额外的约束body: ConstrainedBox(// 如果没传宽高参数就填充父组件constraints: const BoxConstraints.expand(),child: _bytes == null? null// memory 从内存加载数据,bytes 参数需要带格式头: Image.memory(_bytes!,width: _width.toDouble(),height: _height.toDouble(),// 无间隔播放,防止闪烁gaplessPlayback: true,// 保持宽高比例fit: BoxFit.contain,)),persistentFooterButtons: [TextButton(child: const Text("Update"),onPressed: () {// 点击按钮生成位图数据并显示createAndUpdate();},),],),);}void createAndUpdate() {// 填充位图数据,单通道int width = 255;int height = 255;Uint8List bytes = Uint8List(width * height);for (int i = 0; i < height; i++) {for (int j = 0; j < width; j++) {bytes[i * width + j] = (i + j) ~/ 2;}}updateImage(width, height, bytes);}void updateImage(int imgWidth, int imgHeight, Uint8List imgBytes) {// https://pub.dev/documentation/image/latest/image/Image/Image.fromBytes.htmlimage_lib.Image img = image_lib.Image.fromBytes(width: imgWidth,height: imgHeight,bytes: imgBytes.buffer,rowStride: imgWidth,numChannels: 1,format: image_lib.Format.uint8);image_lib.Encoder encoder = image_lib.BmpEncoder();setState(() {_width = imgWidth;_height = imgHeight;_bytes = encoder.encode(img);});}
}
2.自己根据位图格式填充格式头
位图格式相对比较简单,可以自己根据图片信息生成格式头:
import 'dart:typed_data';
import 'package:flutter/material.dart';void main() {runApp(const MyApp());
}class MyApp extends StatefulWidget {const MyApp({super.key});@overrideState<MyApp> createState() => _MyApp();
}class _MyApp extends State<MyApp> {Uint8List? _bytes;int _width = 0;int _height = 0;@overrideWidget build(BuildContext context) {// MaterialApp 创建一个基于 Material Design 风格的应用return MaterialApp(title: "Test Flutter",// Scaffold 是一个布局,有 appBar body persistentFooterButtons 等区域home: Scaffold(backgroundColor: Colors.white,// ConstrainedBox 对其子组件添加额外的约束body: ConstrainedBox(// 如果没传宽高参数就填充父组件constraints: const BoxConstraints.expand(),child: _bytes == null? null// memory 从内存加载数据,bytes 参数需要带格式头: Image.memory(_bytes!,width: _width.toDouble(),height: _height.toDouble(),// 无间隔播放,防止闪烁gaplessPlayback: true,// 保持宽高比例fit: BoxFit.contain,)),persistentFooterButtons: [TextButton(child: const Text("Update"),onPressed: () {// 点击按钮生成位图数据并显示createAndUpdate();},),],),);}void createAndUpdate() {// 填充位图数据int width = 255;int height = 255;int channel = 3;Uint8List bytes = Uint8List(width * height * channel);for (int i = 0; i < height; i++) {for (int j = 0; j < width; j++) {for (int k = 0; k < channel; k++) {bytes[i * width * channel + j * channel + k] = (i + j) ~/ 2;}}}updateImage(width, height, bytes);}void updateImage(int imgWidth, int imgHeight, Uint8List imgBytes) {BMP24 encoder = BMP24(imgWidth, imgHeight);Uint8List bmp = encoder.encode(imgBytes);setState(() {_width = imgWidth;_height = imgHeight;_bytes = bmp;});}
}// 给位图数据添加格式头,并进行字节对齐
class BMP24 {final int _headerSize = 54;// channel 3 = 24 位 RGB888final int _channel = 3;final int _width;final int _height;late Uint8List _bmp;BMP24(this._width, this._height) {// 位图需要四字节对齐// assert((_width * _channel) & 3 == 0);int rowStride = 4 * ((_width * _channel * 8 + 31) ~/ 32);int fileSize = _headerSize + rowStride * _height;debugPrint("stride $rowStride, file size $fileSize .");_bmp = Uint8List(fileSize);ByteData bd = _bmp.buffer.asByteData();// -- 14 字节位图文件头bd.setUint8(0, 0x42); // 固定两字节格式头,字母 Bbd.setUint8(1, 0x4d); // 固定两字节格式头,字母 Mbd.setUint32(2, fileSize, Endian.little); // 文件大小// 2 保留字段// 2 保留字段bd.setUint32(10, _headerSize, Endian.little); // 图片数据开始字节偏移// -- 40 字节位图信息头bd.setUint32(14, 40, Endian.little); // 信息头字节长度bd.setUint32(18, _width, Endian.little); // 图片的像素宽度bd.setUint32(22, -_height, Endian.little); // 图片的像素高度,bmp 的填充是上下颠倒的bd.setUint16(26, 1, Endian.little); // 为目标设备说明颜色平面数,为1bd.setUint16(28, 8 * _channel, Endian.little); // 一个像素点的位数bd.setUint16(30, 0); // 压缩类型,0不压缩bd.setUint32(34, rowStride * _height, Endian.little); // 数据字节大小// 4 水平分辨率,有符号数,单位像素/米// 4 垂直分辨率,有符号数,单位像素/米// 4 颜色索引数// 4 重要颜色索引数}// 图像数据Uint8List encode(Uint8List bytes) {int size = _width * _height * _channel;assert(bytes.length == size);// 如果本来就是四字节对齐的,可以直接 copy// _bmp.setRange(_headerSize, _headerSize + size + 1, bytes);int rowStride = 4 * ((_width * _channel * 8 + 31) ~/ 32);// 逐行填充,四字节对齐for (int row = 0; row < _height; row++) {var subs =bytes.sublist(row * _width * _channel, (row + 1) * _width * _channel);_bmp.setRange(_headerSize + row * rowStride,_headerSize + row * rowStride + _width * _channel, subs);}return _bmp;}
}