Day30 Flutter Camera、播放影片

铁人赛完赛了... 很抱歉没有完成一开始订的完成一个旅游App 的目标,最终只能算是完成Flutter 的入门介绍,最後一天才决定要报名再加上中间遇到一些困难,导致有很多想要讲的并没有时间完成,之後我会在Medium新建一个系列来完成内容,也会对之前讲的一些不足的地方做补充,有兴趣的朋友欢迎再去看看,感恩各位
最近应该会回去将前面觉得不足的文章做补充,之後也会把范例完成,欢迎各位再赏脸去看看

今天最後介绍一些小工具

Camera

很多App都会有功能要使用到手机等设备的相机功能拍摄图片和影片,因此,Flutter提供了camera插件,camera插件提供了一系列可用的相机功能,可以使用相机预览、拍照、录影片

添加三个依赖:

  1. camera
    提供使用设备相机模组的工具
  2. path_provider
    寻找储存图片的正确路径
  3. path
    创建适配任何平台的路径
...
dependencies:
  flutter:
    sdk: flutter
  camera: ^0.5.8
  path_provider: ^1.6.21
  path: ^1.7.0
...  

在Android,您必须更新minSdkVersion到21(或更高)

在iOS上,在ios/Runner/Info.plist中添加下面几行才能使用相机

<key>NSCameraUsageDescription</key>
<string>Explanation on why the camera access is needed.</string>

获取可用相机列表

使用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;

创建并初始化 CameraController

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

如果你没有初始化CameraController,你就不能使用相机预览和拍照

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

  1. 创建一个带有State类的StatefulWidget组件
  2. 添加一个变量到State类来存放CameraController
  3. 添加另外一个变量到State类中来存放 CameraController.initialize()返回的Future
  4. initState()方法中创建并初始化控制器
  5. dispose()方法中销毁控制器
// A screen that takes in a list of cameras and the Directory to store images.
class TakePictureScreen extends StatefulWidget {
  final CameraDescription camera;

  const TakePictureScreen({
    Key key,
    @required this.camera,
  }) : super(key: key);

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

class TakePictureScreenState extends State<TakePictureScreen> {
  // Add two variables to the state class to store the CameraController and
  // the Future.
  CameraController _controller;
  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.
  }
}

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

使用camera中的CameraPreview组件来展示相机预览,在使用相机前,请确保控制器已经完成初始化。因此,你一定要等待前一个步骤创建_initializeControllerFuture() 执行完毕才去展示CameraPreview

// 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 Center(child: CircularProgressIndicator());
    }
  },
)

使用CameraController拍照

可以使用CameraControllertakePicture()方法拍照。在这个示例中,创建了一个浮动按钮FloatingActionButton,当使用者点击这个按钮,就能通过CameraController来拍摄图片

保存一张图片,需要经过一下三个步骤:

  1. 确保相机模块已经被初始化完成
  2. 创建图片需要被保存的路径
  3. 使用控制器拍摄一张图片并保存结果到上述路径

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

FloatingActionButton(
  child: Icon(Icons.camera_alt),
  // 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;

      // Construct the path where the image should be saved using the path
      // package.
      final path = join(
        // Store the picture in the temp directory.
        // Find the temp directory using the `path_provider` plugin.
        (await getTemporaryDirectory()).path,
        '${DateTime.now()}.png',
      );

      // Attempt to take a picture and log where it's been saved.
      await _controller.takePicture(path);
    } catch (e) {
      // If an error occurs, log the error to the console.
      print(e);
    }
  },
)

Imagewidget 显示图片

如果你能成功拍摄图片,你就可以使用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';
import 'package:path/path.dart' show join;
import 'package:path_provider/path_provider.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 {
  final CameraDescription camera;

  const TakePictureScreen({
    Key key,
    @required this.camera,
  }) : super(key: key);

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

class TakePictureScreenState extends State<TakePictureScreen> {
  CameraController _controller;
  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: Text('Take a picture')),
      // 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 Center(child: CircularProgressIndicator());
          }
        },
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.camera_alt),
        // 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;

            // Construct the path where the image should be saved using the
            // pattern package.
            final path = join(
              // Store the picture in the temp directory.
              // Find the temp directory using the `path_provider` plugin.
              (await getTemporaryDirectory()).path,
              '${DateTime.now()}.png',
            );

            // Attempt to take a picture and log where it's been saved.
            await _controller.takePicture(path);

            // If the picture was taken, display it on a new screen.
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (context) => DisplayPictureScreen(imagePath: path),
              ),
            );
          } catch (e) {
            // If an error occurs, log the error to the console.
            print(e);
          }
        },
      ),
    );
  }
}

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: 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)),
    );
  }
}

影片的widget

为了支持影片播放,Flutter团队提供了video_player插件。你可以使用video_player插件播放存在本地文件系统中的影片或者网路影片,在iOS上,video_player使用AVPlayer进行播放控制。在Android上,使用的是ExoPlayer

接下来介绍我们是如何借助video_player包接收网路影片流,并加入基本的播放、暂停操作

添加video_player依赖

...
dependencies:
  flutter:
    sdk: flutter
  video_player: ^0.11.1
...  

添加权限

  • Android 配置:

    AndroidManifest.xml文件中的<application>配置项下加入如下权限。 AndroidManifest.xml文件的路径是 <project root>/android/app/src/main/AndroidManifest.xml

    <manifest xmlns:android="http://schemas.android.com/apk/res/android">
        <application ...>
    
        </application>
    
        <uses-permission android:name="android.permission.INTERNET"/>
    </manifest>
    
  • iOS 配置:

    针对iOS,你需要在<project root>/ios/Runner/Info.plist 路径下的Info.plist文件中加入如下配置

    <key>NSAppTransportSecurity</key>
    <dict>
      <key>NSAllowsArbitraryLoads</key>
      <true/>
    </dict>
    

    video_player 插件在iOS 模拟器上不能使用,必须要在iOS 真机上进行测试

创建并初始化 VideoPlayerController

video_player插件成功安装且权限设置完成後,需要创建一个VideoPlayerControllerVideoPlayerController类允许你播放不同类型的影片并进行播放控制,在播放影片前,需要对播放控制器进行初始化。初始化过程主要是与影片源建立连接和播放控制的准备

  1. 创建一个StatefulWidget组件和State

  2. State类中增加一个变量来存放VideoPlayerController

  3. State类中增加另外一个变量来存放VideoPlayerController.initialize返回的Future

  4. initState方法里创建和初始化控制器

  5. dispose方法里销毁控制器

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

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

class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
  VideoPlayerController _controller;
  Future<void> _initializeVideoPlayerFuture;

  @override
  void initState() {
    // Create an store the VideoPlayerController. The VideoPlayerController
    // offers several different constructors to play videos from assets, files,
    // or the internet.
    _controller = VideoPlayerController.network(
      'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
    );

    _initializeVideoPlayerFuture = _controller.initialize();

    super.initState();
  }

  @override
  void dispose() {
    // Ensure disposing of the VideoPlayerController to free up resources.
    _controller.dispose();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // Complete the code in the next step.
  }
}

影片播放器

video_player插件提供了VideoPlayer组件来展示已经被VideoPlayerController初始化完成的影片。默认情况下,VideoPlayer组件会尽可能撑满整个空间。但是这通常不会太理想,因为很多时候影片需要在特定的宽高比下展示,比如16x9或者4x3

因此,你可以把VideoPlayer组件嵌进一个 AspectRatio组件中,保证影片播放保持正确的比例,此外,你必须在_initializeVideoPlayerFuture完成後才展示VideoPlayer组件。你可以使用FutureBuilder来展示一个旋转的加载图标直到初始化完成。请注意:控制器初始化完成并不会立即开始播放

// Use a FutureBuilder to display a loading spinner while waiting for the
// VideoPlayerController to finish initializing.
FutureBuilder(
  future: _initializeVideoPlayerFuture,
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.done) {
      // If the VideoPlayerController has finished initialization, use
      // the data it provides to limit the aspect ratio of the VideoPlayer.
      return AspectRatio(
        aspectRatio: _controller.value.aspectRatio,
        // Use the VideoPlayer widget to display the video.
        child: VideoPlayer(_controller),
      );
    } else {
      // If the VideoPlayerController is still initializing, show a
      // loading spinner.
      return Center(child: CircularProgressIndicator());
    }
  },
)

播放视频和暂停视频

默认情况下,播放器启动时会处於暂停状态。需要调用VideoPlayerController提供的play()方法来开始播放,然後需要调用pause()方法来停止播放

此范例中加入了一个FloatingActionButton,这个按钮会根据播放状态展示播放或者暂停的图标。当使用者点击按钮,会切换播放状态,如果当前是暂停状态,就开始播放。如果当前是播放状态,就暂停播放

FloatingActionButton(
  onPressed: () {
    // Wrap the play or pause in a call to `setState`. This ensures the
    // correct icon is shown
    setState(() {
      // If the video is playing, pause it.
      if (_controller.value.isPlaying) {
        _controller.pause();
      } else {
        // If the video is paused, play it.
        _controller.play();
      }
    });
  },
  // Display the correct icon depending on the state of the player.
  child: Icon(
    _controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
  ),
)

完整程序码范例:

import 'dart:async';

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

void main() => runApp(VideoPlayerApp());

class VideoPlayerApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Video Player Demo',
      home: VideoPlayerScreen(),
    );
  }
}

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

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

class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
  VideoPlayerController _controller;
  Future<void> _initializeVideoPlayerFuture;

  @override
  void initState() {
    // Create and store the VideoPlayerController. The VideoPlayerController
    // offers several different constructors to play videos from assets, files,
    // or the internet.
    _controller = VideoPlayerController.network(
      'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
    );

    // Initialize the controller and store the Future for later use.
    _initializeVideoPlayerFuture = _controller.initialize();

    // Use the controller to loop the video.
    _controller.setLooping(true);

    super.initState();
  }

  @override
  void dispose() {
    // Ensure disposing of the VideoPlayerController to free up resources.
    _controller.dispose();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Butterfly Video'),
      ),
      // Use a FutureBuilder to display a loading spinner while waiting for the
      // VideoPlayerController to finish initializing.
      body: FutureBuilder(
        future: _initializeVideoPlayerFuture,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
            // If the VideoPlayerController has finished initialization, use
            // the data it provides to limit the aspect ratio of the video.
            return AspectRatio(
              aspectRatio: _controller.value.aspectRatio,
              // Use the VideoPlayer widget to display the video.
              child: VideoPlayer(_controller),
            );
          } else {
            // If the VideoPlayerController is still initializing, show a
            // loading spinner.
            return Center(child: CircularProgressIndicator());
          }
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // Wrap the play or pause in a call to `setState`. This ensures the
          // correct icon is shown.
          setState(() {
            // If the video is playing, pause it.
            if (_controller.value.isPlaying) {
              _controller.pause();
            } else {
              // If the video is paused, play it.
              _controller.play();
            }
          });
        },
        // Display the correct icon depending on the state of the player.
        child: Icon(
          _controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
        ),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

<<:  VSCode 套件推荐系列 - 下

>>:  Day[0] <- "Day End" 今天我想来点Kibana的Controls & Dashboard

Extra07 - Jest - 单元测试框架

此篇为番外,为选入本篇的原因为 Jest 的功能与单元测试的方式多元且复杂,此篇仅能做初步的介绍,...

Day8 React State(编辑中)

State概要 React Component 只能透过资料状态的改变来更新UI,资料来源来自於以下...

有关fb的商业应用

请问现在要开发有关fb的商业应用,是否都局限在粉丝团的上面,个人专页的资料商业应用有局限的吗? ...

Day 14 : 程序码日志与品质

今天来探讨怎麽留下程序码纪录和提升自己的程序码品质。(终於快写到一半了XDDD) 程序码日志 程序设...

Day23 - 使用 Kamiflex 生成 Flex Message

LINE Developers:https://developers.line.biz/zh-ha...