Flutter高性能相机功能插件flutter_better_camera的使用

Flutter高性能相机功能插件flutter_better_camera的使用

Better Camera Plugin

(注意这是一款官方相机插件的分叉版本,您可以自由使用直到它支持缺失的功能。这里是官方插件 这里

(请注意,这款插件目前尚不稳定,但我们正在积极开发此插件)

一个允许访问设备摄像头的 Flutter 插件。

Dart 包

https://pub.flutter-io.cn/packages/flutter_better_camera

特性

  • 在小部件中显示实时相机预览。
  • 可以捕获快照并保存到文件。
  • 录制视频。
  • 从 Dart 访问图像流。
  • 闪光灯控制。
  • 放大缩小控制。
  • 自动曝光开关。
  • 自动对焦开关。

安装

克隆此仓库并在 flutterpubspec.yaml 文件中添加依赖项。

iOS

ios/Runner/Info.plist 中添加两行:

  • 一行带有键 Privacy - Camera Usage Description 和描述。
  • 一行带有键 Privacy - Microphone Usage Description 和描述。

或者以文本格式添加以下键:

<key>NSCameraUsageDescription</key>
<string>Can I use the camera please?</string>
<key>NSMicrophoneUsageDescription</key>
<string>Can I use the mic please?</string>

Android

android/app/build.gradle 文件中将最小 Android SDK 版本更改为 21 或更高版本。

修改 AndroidManifest.xml

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FLASHLIGHT" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.front" />
<uses-feature android:name="android.hardware.camera2" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
<uses-feature android:name="android.hardware.camera.flash" android:required="false" />
minSdkVersion 21

此仓库中有一个用于相机的示例。

下一步

  • 点击聚焦
  • 连拍模式
  • 控制相机预览比例
  • 优化图像质量
  • 控制白平衡

请随意添加问题并贡献您的力量。


示例代码

以下是完整的示例代码:

// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// ignore_for_file: public_member_api_docs

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

import 'package:flutter_better_camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:video_player/video_player.dart';

class CameraExampleHome extends StatefulWidget {
  [@override](/user/override)
  _CameraExampleHomeState createState() {
    return _CameraExampleHomeState();
  }
}

/// 返回一个适合镜头方向的相机图标。
IconData getCameraLensIcon(CameraLensDirection? direction) {
  switch (direction) {
    case CameraLensDirection.back:
      return Icons.camera_rear;
    case CameraLensDirection.front:
      return Icons.camera_front;
    case CameraLensDirection.external:
      return Icons.camera;
  }
  throw ArgumentError('Unknown lens direction');
}

void logError(String code, String? message) => 
    print('Error: $code\nError Message: $message');

class _CameraExampleHomeState extends State<CameraExampleHome>
    with WidgetsBindingObserver {
  CameraController? controller;
  String? imagePath;
  late String videoPath;
  VideoPlayerController? videoController;
  late VoidCallback videoPlayerListener;
  bool enableAudio = true;
  FlashMode flashMode = FlashMode.off;

  [@override](/user/override)
  void initState() {
    super.initState();
    WidgetsBinding.instance!.addObserver(this);
  }

  [@override](/user/override)
  void dispose() {
    WidgetsBinding.instance!.removeObserver(this);
    super.dispose();
  }

  [@override](/user/override)
  void didChangeAppLifecycleState(AppLifecycleState state) {
    // 应用状态发生变化时,如果控制器尚未初始化,则返回。
    if (controller == null || !controller!.value.isInitialized!) {
      return;
    }
    if (state == AppLifecycleState.inactive) {
      controller?.dispose();
    } else if (state == AppLifecycleState.resumed) {
      if (controller != null) {
        onNewCameraSelected(controller!.description);
      }
    }
  }

  final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      appBar: AppBar(
        title: const Text('相机示例'),
      ),
      body: Column(
        children: <Widget>[
          Expanded(
            child: Container(
              child: Padding(
                padding: const EdgeInsets.all(1.0),
                child: Center(
                    child: ZoomableWidget(
                        child: _cameraPreviewWidget(),
                        onTapUp: (scaledPoint) {
                          //controller.setPointOfInterest(scaledPoint);
                        },
                        onZoom: (zoom) {
                          print('zoom');
                          if (zoom < 11) {
                            controller!.zoom(zoom);
                          }
                        })),
              ),
              decoration: BoxDecoration(
                color: Colors.black,
                border: Border.all(
                  color: controller != null && controller!.value.isRecordingVideo!
                      ? Colors.redAccent
                      : Colors.grey,
                  width: 3.0,
                ),
              ),
            ),
          ),
          _captureControlRowWidget(),
          _toggleAudioWidget(),
          Padding(
            padding: const EdgeInsets.all(5.0),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.start,
              children: <Widget>[
                _cameraTogglesRowWidget(),
                _thumbnailWidget(),
              ],
            ),
          ),
        ],
      ),
    );
  }

  /// 显示相机预览或一条消息(如果预览不可用)。
  Widget _cameraPreviewWidget() {
    if (controller == null || !controller!.value.isInitialized!) {
      return const Text(
        '点击选择相机',
        style: TextStyle(
          color: Colors.white,
          fontSize: 24.0,
          fontWeight: FontWeight.w900,
        ),
      );
    } else {
      return AspectRatio(
        aspectRatio: controller!.value.aspectRatio,
        child: CameraPreview(controller!),
      );
    }
  }

  /// 切换录制音频
  Widget _toggleAudioWidget() {
    return Padding(
      padding: const EdgeInsets.only(left: 25),
      child: Row(
        children: <Widget>[
          const Text('启用音频:'),
          Switch(
            value: enableAudio,
            onChanged: (bool value) {
              enableAudio = value;
              if (controller != null) {
                onNewCameraSelected(controller!.description);
              }
            },
          ),
        ],
      ),
    );
  }

  /// 显示捕获的图像或视频缩略图。
  Widget _thumbnailWidget() {
    return Expanded(
      child: Align(
        alignment: Alignment.centerRight,
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            videoController == null && imagePath == null
                ? Container()
                : SizedBox(
                    child: (videoController == null)
                        ? Image.file(File(imagePath!))
                        : Container(
                            child: Center(
                              child: AspectRatio(
                                  aspectRatio:
                                      videoController!.value.size != null
                                          ? videoController!.value.aspectRatio
                                          : 1.0,
                                  child: VideoPlayer(videoController!)),
                            ),
                            decoration: BoxDecoration(
                                border: Border.all(color: Colors.pink)),
                          ),
                    width: 64.0,
                    height: 64.0,
                  ),
          ],
        ),
      ),
    );
  }

  /// 显示控制栏,包含拍摄照片和录制视频的按钮。
  Widget _captureControlRowWidget() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      mainAxisSize: MainAxisSize.max,
      children: <Widget>[
        IconButton(
          icon: const Icon(Icons.camera_alt),
          color: Colors.blue,
          onPressed: controller != null && 
                  controller!.value.isInitialized! && 
                  !controller!.value.isRecordingVideo!
              ? onTakePictureButtonPressed
              : null,
        ),
        IconButton(
          icon: const Icon(Icons.videocam),
          color: Colors.blue,
          onPressed: controller != null && 
                  controller!.value.isInitialized! && 
                  !controller!.value.isRecordingVideo!
              ? onVideoRecordButtonPressed
              : null,
        ),
        IconButton(
          icon: controller != null && controller!.value.isRecordingPaused
              ? Icon(Icons.play_arrow)
              : Icon(Icons.pause),
          color: Colors.blue,
          onPressed: controller != null && 
                  controller!.value.isInitialized! && 
                  controller!.value.isRecordingVideo!
              ? (controller != null && controller!.value.isRecordingPaused
                  ? onResumeButtonPressed
                  : onPauseButtonPressed)
              : null,
        ),
        IconButton(
          icon: controller != null && controller!.value.autoFocusEnabled!
              ? Icon(Icons.access_alarm)
              : Icon(Icons.access_alarms),
          color: Colors.blue,
          onPressed: (controller != null && controller!.value.isInitialized!)
              ? toogleAutoFocus
              : null,
        ),
        _flashButton(),
        IconButton(
          icon: const Icon(Icons.stop),
          color: Colors.red,
          onPressed: controller != null && 
                  controller!.value.isInitialized! && 
                  controller!.value.isRecordingVideo!
              ? onStopButtonPressed
              : null,
        ),
      ],
    );
  }

  /// 闪光灯切换按钮
  Widget _flashButton() {
    IconData iconData = Icons.flash_off;
    Color color = Colors.black;
    if (flashMode == FlashMode.alwaysFlash) {
      iconData = Icons.flash_on;
      color = Colors.blue;
    } else if (flashMode == FlashMode.autoFlash) {
      iconData = Icons.flash_auto;
      color = Colors.red;
    }
    return IconButton(
      icon: Icon(iconData),
      color: color,
      onPressed: controller != null && controller!.value.isInitialized!
          ? _onFlashButtonPressed
          : null,
    );
  }

  /// 切换闪光灯
  Future<void> _onFlashButtonPressed() async {
    bool hasFlash = false;
    if (flashMode == FlashMode.off || flashMode == FlashMode.torch) {
      // 打开闪光灯
      flashMode = FlashMode.alwaysFlash;
    } else if (flashMode == FlashMode.alwaysFlash) {
      // 如果需要打开闪光灯
      flashMode = FlashMode.autoFlash;
    } else {
      // 关闭闪光灯
      flashMode = FlashMode.off;
    }
    // 应用新模式
    await controller!.setFlashMode(flashMode);

    // 更改UI状态
    setState(() {});
  }

  /// 显示可以选择的相机的行控件。
  Widget _cameraTogglesRowWidget() {
    final List<Widget> toggles = [];

    if (cameras.isEmpty) {
      return const Text('未找到相机');
    } else {
      for (CameraDescription cameraDescription in cameras) {
        toggles.add(
          SizedBox(
            width: 90.0,
            child: RadioListTile<CameraDescription>(
              title: Icon(getCameraLensIcon(cameraDescription.lensDirection)),
              groupValue: controller?.description,
              value: cameraDescription,
              onChanged: controller != null && controller!.value.isRecordingVideo!
                  ? null
                  : onNewCameraSelected,
            ),
          ),
        );
      }
    }

    return Row(children: toggles);
  }

  String timestamp() => DateTime.now().millisecondsSinceEpoch.toString();

  void showInSnackBar(String message) {
    _scaffoldKey.currentState!.showSnackBar(SnackBar(content: Text(message)));
  }

  void onNewCameraSelected(CameraDescription? cameraDescription) async {
    if (controller != null) {
      await controller!.dispose();
    }
    controller = CameraController(
      cameraDescription!,
      ResolutionPreset.medium,
      enableAudio: enableAudio,
    );

    // 如果控制器被更新,则更新UI。
    controller!.addListener(() {
      if (mounted) setState(() {});
      if (controller!.value.hasError) {
        showInSnackBar('相机错误 ${controller!.value.errorDescription}');
      }
    });

    try {
      await controller!.initialize();
    } on CameraException catch (e) {
      _showCameraException(e);
    }

    if (mounted) {
      setState(() {});
    }
  }

  void onTakePictureButtonPressed() {
    takePicture().then((String? filePath) {
      if (mounted) {
        setState(() {
          imagePath = filePath;
          videoController?.dispose();
          videoController = null;
        });
        if (filePath != null) showInSnackBar('照片已保存到 $filePath');
      }
    });
  }

  void onVideoRecordButtonPressed() {
    startVideoRecording().then((String? filePath) {
      if (mounted) setState(() {});
      if (filePath != null) showInSnackBar('正在保存视频到 $filePath');
    });
  }

  void onStopButtonPressed() {
    stopVideoRecording().then((_) {
      if (mounted) setState(() {});
      showInSnackBar('视频已记录到: $videoPath');
    });
  }

  void onPauseButtonPressed() {
    pauseVideoRecording().then((_) {
      if (mounted) setState(() {});
      showInSnackBar('视频录制已暂停');
    });
  }

  void onResumeButtonPressed() {
    resumeVideoRecording().then((_) {
      if (mounted) setState(() {});
      showInSnackBar('视频录制已恢复');
    });
  }

  void toogleAutoFocus() {
    controller!.setAutoFocus(!controller!.value.autoFocusEnabled!);
    showInSnackBar('切换自动对焦');
  }

  Future<String?> startVideoRecording() async {
    if (!controller!.value.isInitialized!) {
      showInSnackBar('错误:请选择相机');
      return null;
    }

    final Directory extDir = await getApplicationDocumentsDirectory();
    final String dirPath = '${extDir.path}/Movies/flutter_test';
    await Directory(dirPath).create(recursive: true);
    final String filePath = '$dirPath/${timestamp()}.mp4';

    if (controller!.value.isRecordingVideo!) {
      // 正在录制视频,无需操作。
      return null;
    }

    try {
      videoPath = filePath;
      await controller!.startVideoRecording(filePath);
    } on CameraException catch (e) {
      _showCameraException(e);
      return null;
    }
    return filePath;
  }

  Future<void> stopVideoRecording() async {
    if (!controller!.value.isRecordingVideo!) {
      return null;
    }

    try {
      await controller!.stopVideoRecording();
    } on CameraException catch (e) {
      _showCameraException(e);
      return null;
    }

    await _startVideoPlayer();
  }

  Future<void> pauseVideoRecording() async {
    if (!controller!.value.isRecordingVideo!) {
      return null;
    }

    try {
      await controller!.pauseVideoRecording();
    } on CameraException catch (e) {
      _showCameraException(e);
      rethrow;
    }
  }

  Future<void> resumeVideoRecording() async {
    if (!controller!.value.isRecordingVideo!) {
      return null;
    }

    try {
      await controller!.resumeVideoRecording();
    } on CameraException catch (e) {
      _showCameraException(e);
      rethrow;
    }
  }

  Future<void> _startVideoPlayer() async {
    final VideoPlayerController vcontroller =
        VideoPlayerController.file(File(videoPath));
    videoPlayerListener = () {
      if (videoController != null && videoController!.value.size != null) {
        // 刷新状态以更新视频播放器。
        if (mounted) setState(() {});
        videoController!.removeListener(videoPlayerListener);
      }
    };
    vcontroller.addListener(videoPlayerListener);
    await vcontroller.setLooping(true);
    await vcontroller.initialize();
    await videoController?.dispose();
    if (mounted) {
      setState(() {
        imagePath = null;
        videoController = vcontroller;
      });
    }
    await vcontroller.play();
  }

  Future<String?> takePicture() async {
    if (!controller!.value.isInitialized!) {
      showInSnackBar('错误:请选择相机');
      return null;
    }
    final Directory extDir = await getApplicationDocumentsDirectory();
    final String dirPath = '${extDir.path}/Pictures/flutter_test';
    await Directory(dirPath).create(recursive: true);
    final String filePath = '$dirPath/${timestamp()}.jpg';

    if (controller!.value.isTakingPicture!) {
      // 拍摄正在进行,无需操作。
      return null;
    }

    try {
      await controller!.takePicture(filePath);
    } on CameraException catch (e) {
      _showCameraException(e);
      return null;
    }
    return filePath;
  }

  void _showCameraException(CameraException e) {
    logError(e.code, e.description);
    showInSnackBar('错误:${e.code}\n${e.description}');
  }
}

class CameraApp extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return
      MaterialApp(
        theme: ThemeData(
          accentTextTheme: TextTheme(body2: TextStyle(color: Colors.white)),
        ),
      home: CameraExampleHome(),
    );
  }
}

List<CameraDescription> cameras = [];

Future<void> main() async {
  // 初始化应用之前获取可用的摄像头。
  try {
    WidgetsFlutterBinding.ensureInitialized();
    cameras = await availableCameras();
  } on CameraException catch (e) {
    logError(e.code, e.description);
  }
  runApp(CameraApp());
}

// 缩放控件,将是一个单独的组件
class ZoomableWidget extends StatefulWidget {
  final Widget? child;
  final Function? onZoom;
  final Function? onTapUp;

  const ZoomableWidget({Key? key, this.child, this.onZoom, this.onTapUp})
      : super(key: key);

  [@override](/user/override)
  _ZoomableWidgetState createState() => _ZoomableWidgetState();
}

class _ZoomableWidgetState extends State<ZoomableWidget> {
  Matrix4 matrix = Matrix4.identity();
  double zoom = 1;
  double prevZoom = 1;
  bool showZoom = false;
  Timer? t1;

  bool handleZoom(newZoom){
    if (newZoom >= 1) {
      if (newZoom > 10) {
        return false;
      }
      setState(() {
        showZoom = true;
        zoom = newZoom;
      });

      if (t1 != null) {
        t1!.cancel();
      }

      t1 = Timer(Duration(milliseconds: 2000), () {
        setState(() {
          showZoom = false;
        });
      });
    }
    widget.onZoom!(zoom);
    return true;

  }
  [@override](/user/override)
  Widget build(BuildContext context) {

    return GestureDetector(
        onScaleStart: (scaleDetails) {
          print('scalStart');
          setState(() => prevZoom = zoom);
          //print(scaleDetails);
        },
        onScaleUpdate: (ScaleUpdateDetails scaleDetails) {
          var newZoom = (prevZoom * scaleDetails.scale);

          handleZoom(newZoom);
        },
        onScaleEnd: (scaleDetails) {
          print('end');
          //print(scaleDetails);
        },
        onTapUp: (TapUpDetails det) {
          final RenderBox box = context.findRenderObject() as RenderBox;
          final Offset localPoint = box.globalToLocal(det.globalPosition);
          final Offset scaledPoint =
              localPoint.scale(1 / box.size.width, 1 / box.size.height);
          // TODO 实现
          // widget.onTapUp(scaledPoint);
        },
        child: Stack(children: [
          Column(
            children: <Widget>[
              Container(
                child: Expanded(
                  child: widget.child!,
                ),
              ),
            ],
          ),
          Visibility(
            visible: showZoom, //默认为true
            child: Positioned.fill(
              child: Container(
                  child: Column(
                crossAxisAlignment: CrossAxisAlignment.center,
                mainAxisSize: MainAxisSize.max,
                mainAxisAlignment: MainAxisAlignment.end,
                children: <Widget>[
                  Align(
                      alignment: Alignment.bottomCenter,
                      child:
                      SliderTheme(
                        data: SliderTheme.of(context).copyWith(
                          valueIndicatorTextStyle: TextStyle(
                              color: Colors.amber, letterSpacing: 2.0, fontSize: 30),
                          valueIndicatorColor: Colors.blue,
                          // 这是你需要的
                          inactiveTrackColor: Color(0xFF8D8E98),
                          // 自定义灰色
                          activeTrackColor: Colors.white,
                          thumbColor: Colors.red,
                          overlayColor: Color(0x29EB1555),
                          // 自定义拇指覆盖颜色
                          thumbShape:
                          RoundSliderThumbShape(enabledThumbRadius: 12.0),
                          overlayShape:
                          RoundSliderOverlayShape(overlayRadius: 20.0),

                        ),
                        child: Slider(
                          value: zoom,
                          onChanged: (double newValue) {
                            handleZoom(newValue);
                          },
                          label: "$zoom",
                          min: 1,
                          max: 10,
                        ),
                      ),
                  ),
                ],
              )),
            ),
            //maintainSize: bool. 当为真时,相当于invisible;
            //replacement: Widget. 默认为Sizedbox.shrink,0x0
          )
        ]));
  }
}

更多关于Flutter高性能相机功能插件flutter_better_camera的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter高性能相机功能插件flutter_better_camera的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,下面是一个关于如何使用Flutter高性能相机功能插件flutter_better_camera的代码示例。这个插件提供了对相机功能的更高级和更灵活的访问。

首先,确保你已经在pubspec.yaml文件中添加了flutter_better_camera依赖项:

dependencies:
  flutter:
    sdk: flutter
  flutter_better_camera: ^x.y.z  # 替换为最新版本号

然后,运行flutter pub get来获取依赖项。

接下来,在你的Flutter应用中实现相机功能。下面是一个基本的实现示例:

main.dart

import 'package:flutter/material.dart';
import 'package:flutter_better_camera/flutter_better_camera.dart';
import 'package:flutter_better_camera_example/camera_screen.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Better Camera Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: CameraScreen(),
    );
  }
}

camera_screen.dart

import 'package:flutter/material.dart';
import 'package:flutter_better_camera/flutter_better_camera.dart';
import 'package:flutter_better_camera/flutter_better_camera_controller.dart';

class CameraScreen extends StatefulWidget {
  @override
  _CameraScreenState createState() => _CameraScreenState();
}

class _CameraScreenState extends State<CameraScreen> {
  late FlutterBetterCameraController _controller;

  @override
  void initState() {
    super.initState();
    _controller = FlutterBetterCameraController(
      cameraId: 0, // 默认使用后置摄像头,0为后置,1为前置
      enableAudio: true,
      enableVideoCapture: true,
      enablePhotoCapture: true,
      enablePreview: true,
      flashMode: FlashMode.off,
      orientation: CameraOrientation.portrait,
      previewAspectRatio: 16 / 9,
      previewResolution: PreviewResolution.high,
      videoResolution: VideoResolution.high,
      photoResolution: PhotoResolution.high,
      photoFormat: PhotoFormat.jpeg,
    );

    _controller.initialize().then((_) {
      if (mounted) {
        setState(() {});
      }
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Better Camera'),
      ),
      body: Stack(
        children: [
          if (_controller.isPreviewActive)
            FlutterBetterCameraPreview(
              controller: _controller,
            ),
          Column(
            mainAxisAlignment: MainAxisAlignment.end,
            children: [
              SizedBox(height: 20),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  ElevatedButton(
                    onPressed: () async {
                      final result = await _controller.capturePhoto();
                      if (result != null) {
                        // 处理拍照结果
                        print('Photo path: ${result.path}');
                      }
                    },
                    child: Text('Capture Photo'),
                  ),
                  SizedBox(width: 20),
                  ElevatedButton(
                    onPressed: () async {
                      final result = await _controller.startVideoCapture();
                      if (result != null) {
                        // 处理录像结果
                        print('Video path: ${result.path}');
                      }
                    },
                    child: Text('Start Video Capture'),
                  ),
                ],
              ),
            ],
          ),
        ],
      ),
    );
  }
}

说明

  1. 初始化相机控制器:在initState方法中,我们初始化了FlutterBetterCameraController并设置了相机参数。
  2. 预览相机画面:使用FlutterBetterCameraPreview小部件来显示相机预览。
  3. 拍照和录像:提供两个按钮,分别用于拍照和录像。拍照按钮调用capturePhoto方法,录像按钮调用startVideoCapture方法。

注意事项

  • 请确保在AndroidManifest.xmlInfo.plist文件中添加必要的相机和麦克风权限。
  • 根据需要调整相机参数,例如分辨率、闪光灯模式等。
  • 处理拍照和录像结果时,注意文件路径的访问和权限管理。

希望这个示例能帮助你更好地理解和使用flutter_better_camera插件。

回到顶部