使用 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)),
    );
  }
}