Flutter - 探索相機插件

如果您曾經構建或使用過任何大型移動應用程序,則該應用程序很有可能會使用相機功能。如果您查看PlayStore中的熱門圖表,您會發(fā)現(xiàn)許多應用程序都使用相機執(zhí)行各種任務。Flutter提供了一個相機插件,可以訪問Android和iOS設備上的相機。在本文中,我們將探索Flutter相機插件,并且將構建一個小型相機應用程序以查看該插件可以做什么和不能做什么。

在繼續(xù)前進之前,讓我們看看我們將要構建什么。這個應用程式將可以拍照和錄制影片。您可以在前置和后置攝像頭之間切換。還有一個畫廊,您可以在其中查看捕獲的圖像和錄制的視頻,并與其他應用程序共享它們或從設備中刪除它們。


入門

該應用程序使用以下5個依賴項。您需要將這些依賴項添加到pubspec.yaml

  • camera:提供用于與設備上的攝像頭配合使用的工具。
  • path_provider:查找正確的路徑來存儲媒體。
  • video_player:播放錄制的視頻。
  • esys_flutter_share:用于與其他應用程序共享媒體文件。
  • thumbnails:用于從視頻生成縮略圖。
dependencies:
  camera:
  path_provider:
  thumbnails:
    git:
      url: https://github.com/divyanshub024/Flutter_Thumbnails.git
  video_player:
  esys_flutter_share:

接下來,將文件中的最低Android SDK版本更新為21(或更高)android/app/build.gradle。

將以下幾行添加到您的ios/Runner/Info.plist

<key>NSCameraUsageDescription</key>
<string>Can I use the camera please?</string>
<key>NSMicrophoneUsageDescription</key>
<string>Can I use the mic please?</string>
<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsArbitraryLoads</key>
  <true/>
</dict>

獲取可用相機列表

首先,我們將使用相機插件獲取相機列表。

List<CameraDescription> _cameras;
@override
void initState() {
  _initCamera();
  super.initState();
}
Future<void> _initCamera() async {
  _cameras = await availableCameras();
}

初始化相機控制器

現(xiàn)在,我們有可用相機的列表。接下來,我們將初始化相機控制器。攝像機控制器用于控制設備攝像機。CameraController接受兩個值CameraDescriptionResolutionPreset。最初,我們給出了一個攝像機說明,因為_camera[0]它是我們的后置攝像機。

注意:這里我們ResolutionPreset以介質為準。如果凍結相機,請嘗試避免使用更高的分辨率。請查看此問題以獲取更多詳細信息。

CameraController _controller;

Future<void> _initCamera() async {
  _controller = CameraController(_cameras[0], ResolutionPreset.medium);
  _controller.initialize().then((_) {
    if (!mounted) {
      return;
    }
    setState(() {});
  });
}

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

相機預覽

設置好相機后,我們將使用CameraPreview小部件顯示預覽供稿。在顯示攝像機預覽之前,我們必須等待CameraController初始化。

@override
Widget build(BuildContext context) {
  if (_controller != null) {
    if (!_controller.value.isInitialized) {
      return Container();
    }
  } else {
    return const Center(
      child: SizedBox(
        width: 32,
        height: 32,
        child: CircularProgressIndicator(),
      ),
    );
  }
}

初始化攝像機后,我們將顯示攝像機預覽。

return Scaffold(
  backgroundColor: Theme.of(context).backgroundColor,
  key: _scaffoldKey,
  extendBody: true,
  body: Stack(
    children: <Widget>[
      _buildCameraPreview(),
    ],
  ),
);

在內部,_buildCameraPreview()我們將攝像機預覽縮放到屏幕尺寸,以使其看起來為全屏。

Widget _buildCameraPreview() {
  final size = MediaQuery.of(context).size;
  return ClipRect(
    child: Container(
      child: Transform.scale(
        scale: _controller.value.aspectRatio / size.aspectRatio,
        child: Center(
          child: AspectRatio(
            aspectRatio: _controller.value.aspectRatio,
            child: CameraPreview(_controller),
          ),
        ),
      ),
    ),
  );
}

切換相機

下一步是要能夠在前后攝像頭之間切換或切換。為此,我們首先將圖標按鈕添加到stack widget中。

body: Stack(
  children: <Widget>[
    _buildCameraPreview(),
    Positioned(
      top: 24.0,
      left: 12.0,
      child: IconButton(
        icon: Icon(
          Icons.switch_camera,
          color: Colors.white,
        ),
        onPressed: _onCameraSwitch,
      ),
    ),
  ],
),

_onCameraSwitch按下時,此圖標按鈕調用方法。在此方法中,我們將先處理,CameraController然后使用new初始化CameraController和新的CameraDescription。

Future<void> _onCameraSwitch() async {
  final CameraDescription cameraDescription =
      (_controller.description == _cameras[0]) ? _cameras[1] : _cameras[0];
  if (_controller != null) {
    await _controller.dispose();
  }
  _controller = CameraController(cameraDescription, ResolutionPreset.medium);
  _controller.addListener(() {
    if (mounted) setState(() {});
    if (_controller.value.hasError) {
      showInSnackBar('Camera error ${_controller.value.errorDescription}');
    }
  });

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

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

相機控制視圖

在屏幕底部,我們將有一個控件視圖,該視圖基本上包含3個按鈕。首先去畫廊,其次去捕捉圖像或錄制視頻,第三次在圖像捕捉和視頻錄制之間切換。

return Scaffold(
  backgroundColor: Theme.of(context).backgroundColor,
  key: _scaffoldKey,
  extendBody: true,
  body: ...
  bottomNavigationBar: _buildBottomNavigationBar(),
);

該視圖將顯示在底部導航欄中。不要忘記添加extendBody: true.

Widget _buildBottomNavigationBar() {
  return Container(
    color: Theme.of(context).bottomAppBarColor,
    height: 100.0,
    width: double.infinity,
    child: Row(
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      children: <Widget>[
        FutureBuilder(
          future: getLastImage(),
          builder: (context, snapshot) {
            if (snapshot.data == null) {
              return Container(
                width: 40.0,
                height: 40.0,
              );
            }
            return GestureDetector(
              onTap: () => Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => Gallery(),
                ),
              ),
              child: Container(
                width: 40.0,
                height: 40.0,
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(4.0),
                  child: Image.file(
                    snapshot.data,
                    fit: BoxFit.cover,
                  ),
                ),
              ),
            );
          },
        ),
        CircleAvatar(
          backgroundColor: Colors.white,
          radius: 28.0,
          child: IconButton(
            icon: Icon(
              (_isRecordingMode)
                  ? (_isRecording) ? Icons.stop : Icons.videocam
                  : Icons.camera_alt,
              size: 28.0,
              color: (_isRecording) ? Colors.red : Colors.black,
            ),
            onPressed: () {
              if (!_isRecordingMode) {
                _captureImage();
              } else {
                if (_isRecording) {
                  stopVideoRecording();
                } else {
                  startVideoRecording();
                }
              }
            },
          ),
        ),
        IconButton(
          icon: Icon(
            (_isRecordingMode) ? Icons.camera_alt : Icons.videocam,
            color: Colors.white,
          ),
          onPressed: () {
            setState(() {
              _isRecordingMode = !_isRecordingMode;
            });
          },
        ),
      ],
    ),
  );
}

捕獲圖像

使用相機控制器捕獲圖像非常容易。

  1. 檢查相機控制器是否已初始化。
  2. 構造目錄并定義路徑。
  3. 使用CameraController捕獲圖像并將其保存到給定路徑。
void _captureImage() async {
  if (_controller.value.isInitialized) {
    final Directory extDir = await getApplicationDocumentsDirectory();
    final String dirPath = '${extDir.path}/media';
    await Directory(dirPath).create(recursive: true);
    final String filePath = '$dirPath/${_timestamp()}.jpeg';
    await _controller.takePicture(filePath);
    setState(() {});
  }
}

錄制視頻

我們可以將錄制視頻過程分為兩個部分:

開始錄像:

  1. 檢查相機控制器是否已初始化。
  2. 啟動計時器以顯示記錄的視頻時間。(可選的)
  3. 構造目錄并定義路徑。
  4. 使用攝像機控制器開始錄制并將視頻保存在定義的路徑上。
Future<String> startVideoRecording() async {
  print('startVideoRecording');
  if (!_controller.value.isInitialized) {
    return null;
  }
  setState(() {
    _isRecording = true;
  });
  _timerKey.currentState.startTimer();

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

  if (_controller.value.isRecordingVideo) {
    // A recording is already started, do nothing.
    return null;
  }

  try {
    await _controller.startVideoRecording(filePath);
  } on CameraException catch (e) {
    _showCameraException(e);
    return null;
  }
  return filePath;
}

停止錄像:

  1. 檢查相機控制器是否已初始化。
  2. 停止計時器。
  3. 使用相機控制器停止視頻錄制。
Future<void> stopVideoRecording() async {
  if (!_controller.value.isRecordingVideo) {
    return null;
  }
  _timerKey.currentState.stopTimer();
  setState(() {
    _isRecording = false;
  });

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

這是相機屏幕的完整代碼。

import 'dart:io';

import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_camera/gallery.dart';
import 'package:flutter_camera/video_timer.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:thumbnails/thumbnails.dart';

class CameraScreen extends StatefulWidget {
  const CameraScreen({Key key}) : super(key: key);

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

class CameraScreenState extends State<CameraScreen>
    with AutomaticKeepAliveClientMixin {
  CameraController _controller;
  List<CameraDescription> _cameras;
  final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
  bool _isRecordingMode = false;
  bool _isRecording = false;
  final _timerKey = GlobalKey<VideoTimerState>();

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

  Future<void> _initCamera() async {
    _cameras = await availableCameras();
    _controller = CameraController(_cameras[0], ResolutionPreset.medium);
    _controller.initialize().then((_) {
      if (!mounted) {
        return;
      }
      setState(() {});
    });
  }

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

  @override
  Widget build(BuildContext context) {
    super.build(context);
    if (_controller != null) {
      if (!_controller.value.isInitialized) {
        return Container();
      }
    } else {
      return const Center(
        child: SizedBox(
          width: 32,
          height: 32,
          child: CircularProgressIndicator(),
        ),
      );
    }

    if (!_controller.value.isInitialized) {
      return Container();
    }
    return Scaffold(
      backgroundColor: Theme.of(context).backgroundColor,
      key: _scaffoldKey,
      extendBody: true,
      body: Stack(
        children: <Widget>[
          _buildCameraPreview(),
          Positioned(
            top: 24.0,
            left: 12.0,
            child: IconButton(
              icon: Icon(
                Icons.switch_camera,
                color: Colors.white,
              ),
              onPressed: () {
                _onCameraSwitch();
              },
            ),
          ),
          if (_isRecordingMode)
            Positioned(
              left: 0,
              right: 0,
              top: 32.0,
              child: VideoTimer(
                key: _timerKey,
              ),
            )
        ],
      ),
      bottomNavigationBar: _buildBottomNavigationBar(),
    );
  }

  Widget _buildCameraPreview() {
    final size = MediaQuery.of(context).size;
    return ClipRect(
      child: Container(
        child: Transform.scale(
          scale: _controller.value.aspectRatio / size.aspectRatio,
          child: Center(
            child: AspectRatio(
              aspectRatio: _controller.value.aspectRatio,
              child: CameraPreview(_controller),
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildBottomNavigationBar() {
    return Container(
      color: Theme.of(context).bottomAppBarColor,
      height: 100.0,
      width: double.infinity,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: <Widget>[
          FutureBuilder(
            future: getLastImage(),
            builder: (context, snapshot) {
              if (snapshot.data == null) {
                return Container(
                  width: 40.0,
                  height: 40.0,
                );
              }
              return GestureDetector(
                onTap: () => Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => Gallery(),
                  ),
                ),
                child: Container(
                  width: 40.0,
                  height: 40.0,
                  child: ClipRRect(
                    borderRadius: BorderRadius.circular(4.0),
                    child: Image.file(
                      snapshot.data,
                      fit: BoxFit.cover,
                    ),
                  ),
                ),
              );
            },
          ),
          CircleAvatar(
            backgroundColor: Colors.white,
            radius: 28.0,
            child: IconButton(
              icon: Icon(
                (_isRecordingMode)
                    ? (_isRecording) ? Icons.stop : Icons.videocam
                    : Icons.camera_alt,
                size: 28.0,
                color: (_isRecording) ? Colors.red : Colors.black,
              ),
              onPressed: () {
                if (!_isRecordingMode) {
                  _captureImage();
                } else {
                  if (_isRecording) {
                    stopVideoRecording();
                  } else {
                    startVideoRecording();
                  }
                }
              },
            ),
          ),
          IconButton(
            icon: Icon(
              (_isRecordingMode) ? Icons.camera_alt : Icons.videocam,
              color: Colors.white,
            ),
            onPressed: () {
              setState(() {
                _isRecordingMode = !_isRecordingMode;
              });
            },
          ),
        ],
      ),
    );
  }

  Future<FileSystemEntity> getLastImage() async {
    final Directory extDir = await getApplicationDocumentsDirectory();
    final String dirPath = '${extDir.path}/media';
    final myDir = Directory(dirPath);
    List<FileSystemEntity> _images;
    _images = myDir.listSync(recursive: true, followLinks: false);
    _images.sort((a, b) {
      return b.path.compareTo(a.path);
    });
    var lastFile = _images[0];
    var extension = path.extension(lastFile.path);
    if (extension == '.jpeg') {
      return lastFile;
    } else {
      String thumb = await Thumbnails.getThumbnail(
          videoFile: lastFile.path, imageType: ThumbFormat.PNG, quality: 30);
      return File(thumb);
    }
  }

  Future<void> _onCameraSwitch() async {
    final CameraDescription cameraDescription =
        (_controller.description == _cameras[0]) ? _cameras[1] : _cameras[0];
    if (_controller != null) {
      await _controller.dispose();
    }
    _controller = CameraController(cameraDescription, ResolutionPreset.medium);
    _controller.addListener(() {
      if (mounted) setState(() {});
      if (_controller.value.hasError) {
        showInSnackBar('Camera error ${_controller.value.errorDescription}');
      }
    });

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

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

  void _captureImage() async {
    print('_captureImage');
    if (_controller.value.isInitialized) {
      SystemSound.play(SystemSoundType.click);
      final Directory extDir = await getApplicationDocumentsDirectory();
      final String dirPath = '${extDir.path}/media';
      await Directory(dirPath).create(recursive: true);
      final String filePath = '$dirPath/${_timestamp()}.jpeg';
      print('path: $filePath');
      await _controller.takePicture(filePath);
      setState(() {});
    }
  }

  Future<String> startVideoRecording() async {
    print('startVideoRecording');
    if (!_controller.value.isInitialized) {
      return null;
    }
    setState(() {
      _isRecording = true;
    });
    _timerKey.currentState.startTimer();

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

    if (_controller.value.isRecordingVideo) {
      // A recording is already started, do nothing.
      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;
    }
    _timerKey.currentState.stopTimer();
    setState(() {
      _isRecording = false;
    });

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

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

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

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

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

  @override
  bool get wantKeepAlive => true;
}

圖庫視圖

我們的相機已經準備就緒,可以使用了。但是,我們如何查看捕獲的圖像和錄制的視頻?我們將創(chuàng)建一個畫廊視圖。它將由一個水平的網頁瀏覽和一個底部的應用欄以及一個共享按鈕和一個刪除按鈕組成。

在內部,PageView.builder我們正在檢查文件的擴展名。如果文件擴展名為jpeg,則將其顯示為圖像,否則,將使用VideoPreview小部件顯示視頻。

String currentFilePath;
@override
Widget build(BuildContext context) {
  return Scaffold(
    backgroundColor: Theme.of(context).backgroundColor,
    appBar: AppBar(
      backgroundColor: Colors.black,
    ),
    body: FutureBuilder(
      future: _getAllImages(),
      builder: (context, AsyncSnapshot<List<FileSystemEntity>> snapshot) {
        if (!snapshot.hasData || snapshot.data.isEmpty) {
          return Container();
        }
        print('${snapshot.data.length} ${snapshot.data}');
        if (snapshot.data.length == 0) {
          return Center(
            child: Text('No images found.'),
          );
        }

        return PageView.builder(
          itemCount: snapshot.data.length,
          itemBuilder: (context, index) {
            currentFilePath = snapshot.data[index].path;
            var extension = path.extension(snapshot.data[index].path);
            if (extension == '.jpeg') {
              return Container(
                height: 300,
                padding: const EdgeInsets.only(bottom: 8.0),
                child: Image.file(
                  File(snapshot.data[index].path),
                ),
              );
            } else {
              return VideoPreview(
                videoPath: snapshot.data[index].path,
              );
            }
          },
        );
      },
    ),
    bottomNavigationBar: BottomAppBar(
      child: Container(
        height: 56.0,
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: <Widget>[
            IconButton(
              icon: Icon(Icons.share),
              onPressed: () => _shareFile(),
            ),
            IconButton(
              icon: Icon(Icons.delete),
              onPressed: _deleteFile,
            ),
          ],
        ),
      ),
    ),
  );
}

從設備獲取媒體文件

Future<List<FileSystemEntity>> _getAllImages() async {
  final Directory extDir = await getApplicationDocumentsDirectory();
  final String dirPath = '${extDir.path}/media';
  final myDir = Directory(dirPath);
  List<FileSystemEntity> _images;
  _images = myDir.listSync(recursive: true, followLinks: false);
  _images.sort((a, b) {
    return b.path.compareTo(a.path);
  });
  return _images;
}

刪除媒體文件

刪除文件非常容易。只需將目錄指向文件路徑,然后使用deleteSync函數(shù)將其刪除。

_deleteFile() {
  final dir = Directory(currentFilePath);
  dir.deleteSync(recursive: true);
  setState(() {});
}

共享媒體文件

為了共享文件,我們使用esys_flutter_share插件。您可以使用Share.file()將String title,String name,List < int >bytes,String mimeType作為強制參數(shù)的方法輕松共享文件。您可以使用readAsBytesSync方法從文件中獲取字節(jié)。

_shareFile() async {
  var extension = path.extension(currentFilePath);
  await Share.file(
    'image',
    (extension == '.jpeg') ? 'image.jpeg' : '  video.mp4',
    File(currentFilePath).readAsBytesSync(),
    (extension == '.jpeg') ? 'image/jpeg' : '  video/mp4',
  );
}

我對相機插件的看法

在得出結論之前,我們應該知道Flutter Camera插件仍在開發(fā)中。該插件非常適合制作任何像樣的相機應用程序,但是它有一些小問題,并且缺少許多高級功能,例如自動曝光和閃光燈支持。如果您想了解有關相機插件即將發(fā)生的變化的最新信息,請關注“相機插件的未來”問題。本期將討論相機插件中即將提供的一些很酷的功能。


您可以在此處查看該項目的完整源代碼。

翻譯自:https://levelup.gitconnected.com/exploring-flutter-camera-plugin-d2c54ac95f05

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容