使用 Camera 插件实现拍照功能

很多应用都需要使用到设备的相机模块拍摄图片和视频。因此,Flutter 提供了 camera 插件。 camera 插件提供了一系列可用的相机,并使用特定的相机展示相机预览、拍照、录视频。

这个章节将会讲解如何使用 camera 插件去展示相机预览、拍照并显示。

步骤

  1. 添加所需依赖

  2. 获取可用相机列表

  3. 创建并初始化 CameraController

  4. 使用 CameraPreview 展示相机的帧流

  5. 使用 CameraController 拍摄一张图片

  6. 使用 Image 组件展示图片

1. 添加所需依赖

为了完成这个章节,你需要向你的应用添加三个依赖:

camera
提供使用设备相机模块的工具

path_provider
寻找存储图片的正确路径

path
创建适配任何平台的路径

运行 flutter pub add 将其添加为依赖:

$ flutter pub add camera path_provider path

2. 获取可用相机列表

接着,你可以使用 camera 插件获取可用相机列表。

// Ensure that plugin services are initialized so that `availableCameras()`
// can be called before `runApp()`
WidgetsFlutterBinding.ensureInitialized();

// Obtain a list of the available cameras on the device.
final cameras = await availableCameras();

// Get a specific camera from the list of available cameras.
final firstCamera = cameras.first;

3. 创建并初始化 CameraController

在选择了一个相机后,你需要创建并初始化 CameraController。在这个过程中,与设备相机建立了连接并允许你控制相机并展示相机的预览帧流。

实现这个过程,请依照以下步骤:

  1. 创建一个带有 State 类的 StatefulWidget 组件

  2. 添加一个变量到 State 类来存放 CameraController

  3. 添加另外一个变量到 State 类中来存放 CameraController.initialize() 返回的 Future

  4. initState() 方法中创建并初始化控制器

  5. dispose() 方法中销毁控制器

// A screen that allows users to take a picture using a given camera.
class TakePictureScreen extends StatefulWidget {
  const TakePictureScreen({
    super.key,
    required this.camera,
  });

  final CameraDescription camera;

  @override
  TakePictureScreenState createState() => TakePictureScreenState();
}

class TakePictureScreenState extends State<TakePictureScreen> {
  late CameraController _controller;
  late Future<void> _initializeControllerFuture;

  @override
  void initState() {
    super.initState();
    // To display the current output from the Camera,
    // create a CameraController.
    _controller = CameraController(
      // Get a specific camera from the list of available cameras.
      widget.camera,
      // Define the resolution to use.
      ResolutionPreset.medium,
    );

    // Next, initialize the controller. This returns a Future.
    _initializeControllerFuture = _controller.initialize();
  }

  @override
  void dispose() {
    // Dispose of the controller when the widget is disposed.
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // Fill this out in the next steps.
    return Container();
  }
}

4. 在 initState 方法中创建并初始化控制器

接着,你能够使用 camera 中的 CameraPreview 组件来展示相机预览帧流。

你可以使用 FutureBuilder 完成这个任务。

// You must wait until the controller is initialized before displaying the
// camera preview. Use a FutureBuilder to display a loading spinner until the
// controller has finished initializing.
FutureBuilder<void>(
  future: _initializeControllerFuture,
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.done) {
      // If the Future is complete, display the preview.
      return CameraPreview(_controller);
    } else {
      // Otherwise, display a loading indicator.
      return const Center(child: CircularProgressIndicator());
    }
  },
)

5. 使用 CameraController 拍照

You can use the CameraController to take pictures using the takePicture() method, which returns an XFile, a cross-platform, simplified File abstraction. On both Android and IOS, the new image is stored in their respective cache directories, and the path to that location is returned in the XFile.

In this example, create a FloatingActionButton that takes a picture using the CameraController when a user taps on the button.

Taking a picture requires 2 steps:

  1. Ensure that the camera is initialized.
  2. Use the controller to take a picture and ensure that it returns a Future<XFile>.

最好把这些操作都放在 try / catch 方法块中来处理可能发生的异常。

FloatingActionButton(
  // Provide an onPressed callback.
  onPressed: () async {
    // Take the Picture in a try / catch block. If anything goes wrong,
    // catch the error.
    try {
      // Ensure that the camera is initialized.
      await _initializeControllerFuture;

      // Attempt to take a picture and then get the location
      // where the image file is saved.
      final image = await _controller.takePicture();
    } catch (e) {
      // If an error occurs, log the error to the console.
      print(e);
    }
  },
  child: const Icon(Icons.camera_alt),
)

6. 在 dispose 方法中销毁控制器

如果你能成功拍摄图片,你就可以使用 Image 组件展示被保存的图片。在这个示例中,这张图片是以文件的形式存储在设备中。

因此,你需要提供一个 FileImage.file 构造函数。你能够通过传递你在上一步中创建的路径来创建一个 File 类的实例。

Image.file(File('path/to/my/picture.png'));

完整样例

import 'dart:async';
import 'dart:io';

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

Future<void> main() async {
  // Ensure that plugin services are initialized so that `availableCameras()`
  // can be called before `runApp()`
  WidgetsFlutterBinding.ensureInitialized();

  // Obtain a list of the available cameras on the device.
  final cameras = await availableCameras();

  // Get a specific camera from the list of available cameras.
  final firstCamera = cameras.first;

  runApp(
    MaterialApp(
      theme: ThemeData.dark(),
      home: TakePictureScreen(
        // Pass the appropriate camera to the TakePictureScreen widget.
        camera: firstCamera,
      ),
    ),
  );
}

// A screen that allows users to take a picture using a given camera.
class TakePictureScreen extends StatefulWidget {
  const TakePictureScreen({
    super.key,
    required this.camera,
  });

  final CameraDescription camera;

  @override
  TakePictureScreenState createState() => TakePictureScreenState();
}

class TakePictureScreenState extends State<TakePictureScreen> {
  late CameraController _controller;
  late Future<void> _initializeControllerFuture;

  @override
  void initState() {
    super.initState();
    // To display the current output from the Camera,
    // create a CameraController.
    _controller = CameraController(
      // Get a specific camera from the list of available cameras.
      widget.camera,
      // Define the resolution to use.
      ResolutionPreset.medium,
    );

    // Next, initialize the controller. This returns a Future.
    _initializeControllerFuture = _controller.initialize();
  }

  @override
  void dispose() {
    // Dispose of the controller when the widget is disposed.
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Take a picture')),
      // You must wait until the controller is initialized before displaying the
      // camera preview. Use a FutureBuilder to display a loading spinner until the
      // controller has finished initializing.
      body: FutureBuilder<void>(
        future: _initializeControllerFuture,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
            // If the Future is complete, display the preview.
            return CameraPreview(_controller);
          } else {
            // Otherwise, display a loading indicator.
            return const Center(child: CircularProgressIndicator());
          }
        },
      ),
      floatingActionButton: FloatingActionButton(
        // Provide an onPressed callback.
        onPressed: () async {
          // Take the Picture in a try / catch block. If anything goes wrong,
          // catch the error.
          try {
            // Ensure that the camera is initialized.
            await _initializeControllerFuture;

            // Attempt to take a picture and get the file `image`
            // where it was saved.
            final image = await _controller.takePicture();

            if (!context.mounted) return;

            // If the picture was taken, display it on a new screen.
            await Navigator.of(context).push(
              MaterialPageRoute(
                builder: (context) => DisplayPictureScreen(
                  // Pass the automatically generated path to
                  // the DisplayPictureScreen widget.
                  imagePath: image.path,
                ),
              ),
            );
          } catch (e) {
            // If an error occurs, log the error to the console.
            print(e);
          }
        },
        child: const Icon(Icons.camera_alt),
      ),
    );
  }
}

// A widget that displays the picture taken by the user.
class DisplayPictureScreen extends StatelessWidget {
  final String imagePath;

  const DisplayPictureScreen({super.key, required this.imagePath});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Display the Picture')),
      // The image is stored as a file on the device. Use the `Image.file`
      // constructor with the given path to display the image.
      body: Image.file(File(imagePath)),
    );
  }
}