在 Flutter 中,通常使用 Image.file 来从文件加载一个图片。下面一个小 Demo:

import 'dart:io';

import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter_utils/dialog_tool.dart';

class DDD extends StatefulWidget {
  @override
  _DDDState createState() => _DDDState();
}

class _DDDState extends State<DDD> {
  List<CameraDescription> cameras;
  CameraController controller;

  Future<void> initCam() async {
    cameras = await availableCameras();
    controller = CameraController(cameras[1], ResolutionPreset.ultraHigh,
        enableAudio: false);
    await controller.initialize();
    setState(() {});
  }

  @override
  void initState() {
    initCam();
    super.initState();
  }

  File file;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          SizedBox(width: 400, child: buildCamera(controller)),
          file == null
              ? const SizedBox()
              : SizedBox(width: 300, child: Image.file(file)),
          RaisedButton(
            onPressed: () async {
              showLoadingDialog(context);
              final ff = File('/sdcard/temp2.jpg');
              if (ff.existsSync()) {
                await ff.delete();
              }
              await controller.takePicture(ff.path);
              file = ff;
              Navigator.pop(context);
              setState(() {});
            },
          )
        ],
      ),
    );
  }

  Widget buildCamera(CameraController controller) {
    if (controller == null) return const SizedBox();

    if (!controller.value.isInitialized) {
      return Container();
    }

    return RotatedBox(
      quarterTurns: 1,
      child: AspectRatio(
        aspectRatio: controller.value.aspectRatio,
        child: CameraPreview(controller),
      ),
    );
  }
}
DEMO 如图所示

在这个demo中,左侧是一个前置摄像机预览,中间是一个 Image Widget,用于加载摄像机拍照所得的文件,右侧是一个按钮,用于出发摄像机拍照。

当点击右侧按钮时,对前置摄像机进行拍照,中间 Image Widget 会加载照片文件。

在第一次拍照加载时没事没有问题的,但当第二次点击这个右侧按钮,中间的 Image Widget 并不会更新。这是由于 Image Widget 更新机制的所造成的问题。

  @override
  void didUpdateWidget(Image oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (_isListeningToStream &&
        (widget.loadingBuilder == null) != (oldWidget.loadingBuilder == null)) {
      final ImageStreamListener oldListener = _getListener();
      _imageStream!.addListener(_getListener(recreateListener: true));
      _imageStream!.removeListener(oldListener);
    }
    if (widget.image != oldWidget.image)
      _resolveImage();
  }

if (widget.image != oldWidget.image) 这行代码用于决定是否更新 Image Widget。由于此处时 FileImage 所以我们再看一下 FileImage 的内部实现,时重写了 == 操作符。

  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType)
      return false;
    return other is FileImage
        && other.file.path == file.path
        && other.scale == scale;
  }

所以在对 FileImage 进行 == 操作时,只会验证 类型、文件路径、scale。所以在我们这个demo中,由于拍照会写入到同一个文件(/sdcard/temp2.jpg),即使图片不一样也不会出发更新操作。

这时候你可以选择更改文件存储路径:

          RaisedButton(
            onPressed: () async {
              showLoadingDialog(context);
              final ff = File('/sdcard/temp2${DateTime.now().millisecondsSinceEpoch}.jpg');
              if (ff.existsSync()) {
                await ff.delete();
              }
              await controller.takePicture(ff.path);
              file = ff;
              Navigator.pop(context);
              setState(() {});
            },
          )

这样我们就可以愉快的更新图片了😝。

但是当你不想更改存储路径时,这该怎么办?

这时候,我们可以绕过 Image.file,使用 Image.memory 来解决图片不更新:

// ...
file == null ? const SizedBox() : SizedBox(width: 300, child: Image.memory(file.readAsBytesSync())),
// ...

(不规范写法,请勿模仿)

除了这两种方法我们还可以通过清楚 ImageCache 同时更新 Image.file:

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          SizedBox(width: 400, child: buildCamera(controller)),
          file == null
              ? const SizedBox()
              : SizedBox(
                  width: 300, child: Image.file(file, key: ValueKey(DateTime.now()),)),
          RaisedButton(
            onPressed: () async {
              showLoadingDialog(context);
              final ff = File('/sdcard/temp2.jpg');
              if (ff.existsSync()) await ff.delete();
              await controller.takePicture(ff.path);
              file = ff;

              imageCache.clear();
              imageCache.clearLiveImages();

              Navigator.pop(context);
              setState(() {});
            },
          )
        ],
      ),
    );
  }

清除 ImageCache 还有另一种方式,是调用 FlieImage 的 evict 方法:


  Future<bool> evict({ ImageCache? cache, ImageConfiguration configuration = ImageConfiguration.empty }) async {
    cache ??= imageCache;
    final T key = await obtainKey(configuration);
    return cache!.evict(key);
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          SizedBox(width: 400, child: buildCamera(controller)),
          file == null
              ? const SizedBox()
              : SizedBox(
                  width: 300, child: Image(image: file, key: ValueKey(DateTime.now()),)),
          RaisedButton(
            onPressed: () async {
              showLoadingDialog(context);
              final ff = File('/sdcard/temp2.jpg');
              if (ff.existsSync()) await ff.delete();
              await controller.takePicture(ff.path);
              await file?.evict();
              file = FileImage(ff);
              Navigator.pop(context);
              setState(() {});
            },
          )
        ],
      ),
    );
  }

参考:

https://github.com/flutter/flutter/issues/17419


Android 开发者、影视后期内容(包装)制作者、Unity 2D游戏开发者