# 12.6 Texture和PlatformView

本节主要介绍原生和Flutter之间如何共享图像,以及如何在Flutter中嵌套原生组件。

# 12.6.1 Texture(示例:使用摄像头)

前面说过Flutter本身只是一个UI系统,对于一些系统能力的调用我们可以通过消息传送机制与原生交互。但是这种消息传送机制并不能覆盖所有的应用场景,比如我们想调用摄像头来拍照或录视频,但在拍照和录视频的过程中我们需要将预览画面显示到我们的Flutter UI中,如果我们要用Flutter定义的消息通道机制来实现这个功能,就需要将摄像头采集的每一帧图片都要从原生传递到Flutter中,这样做代价将会非常大,因为将图像或视频数据通过消息通道实时传输必然会引起内存和CPU的巨大消耗!为此,Flutter提供了一种基于Texture的图片数据共享机制。

Texture可以理解为GPU内保存将要绘制的图像数据的一个对象,Flutter engine会将Texture的数据在内存中直接进行映射(而无需在原生和Flutter之间再进行数据传递),Flutter会给每一个Texture分配一个id,同时Flutter中提供了一个Texture组件,Texture构造函数定义如下:

const Texture({
  Key key,
   this.textureId,
})
1
2
3
4

Texture 组件正是通过textureId与Texture数据关联起来;在Texture组件绘制时,Flutter会自动从内存中找到相应id的Texture数据,然后进行绘制。可以总结一下整个流程:图像数据先在原生部分缓存,然后在Flutter部分再通过textureId和缓存关联起来,最后绘制由Flutter完成。

如果我们作为一个插件开发者,我们在原生代码中分配了textureId,那么在Flutter侧使用Texture组件时要如何获取textureId呢?这又回到了之前的内容了,textureId完全可以通过MethodChannel来传递。

另外,值得注意的是,当原生摄像头捕获的图像发生变化时,Texture 组件会自动重绘,这不需要我们写任何Dart 代码去控制。

# Texture用法

如果我们要手动实现一个相机插件,和前面几节介绍的“获取剩余电量”插件的步骤一样,需要分别实现原生部分和Flutter部分。考虑到大多数读者可能并非同时既了解Android开发,又了解iOS开发,如果我们再花大量篇幅来介绍不同端的实现可能会没什么意义,另外,由于Flutter官方提供的相机(camera)插件和视频播放(video_player)插件都是使用Texture来实现的,它们本身就是Texture非常好的示例,所以在本书中将不会再介绍使用Texture的具体流程了,读者有兴趣查看camera和video_player的实现代码。下面我们重点介绍一下如何使用camera和video_player。

# 相机示例

下面我们看一下camera包自带的一个示例,它包含如下功能:

  1. 可以拍照,也可以拍视频,拍摄完成后可以保存;排号的视频可以播放预览。
  2. 可以切换摄像头(前置摄像头、后置摄像头、其它)
  3. 可以显示已经拍摄内容的预览图。

下面我们看一下具体代码:

  1. 首先,依赖camera插件的最新版,并下载依赖。

    dependencies:
      ...  //省略无关代码
      camera: ^0.5.2+2
    
    1
    2
    3
  2. main方法中获取可用摄像头列表。

    void main() async {
      // 获取可用摄像头列表,cameras为全局变量
      cameras = await availableCameras();
      runApp(MyApp());
    }
    
    1
    2
    3
    4
    5
  3. 构建UI。现在我们构建如图12-4的测试界面:

    12-4 线面是完整的代码:

    import 'package:camera/camera.dart';
    import 'package:flutter/material.dart';
    import '../common.dart';
    import 'dart:async';
    import 'dart:io';
    import 'package:path_provider/path_provider.dart';
    import 'package:video_player/video_player.dart'; //用于播放录制的视频
    
    /// 获取不同摄像头的图标(前置、后置、其它)
    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 CameraExampleHome extends StatefulWidget {
      
      _CameraExampleHomeState createState() {
        return _CameraExampleHomeState();
      }
    }
    
    class _CameraExampleHomeState extends State<CameraExampleHome>
        with WidgetsBindingObserver {
      CameraController controller;
      String imagePath; // 图片保存路径
      String videoPath; //视频保存路径
      VideoPlayerController videoController;
      VoidCallback videoPlayerListener;
      bool enableAudio = true;
    
      
      void initState() {
        super.initState();
        // 监听APP状态改变,是否在前台
        WidgetsBinding.instance.addObserver(this);
      }
    
      
      void dispose() {
        WidgetsBinding.instance.removeObserver(this);
        super.dispose();
      }
    
      
      void didChangeAppLifecycleState(AppLifecycleState state) {
        // 如果APP不在在前台
        if (state == AppLifecycleState.inactive) {
          controller?.dispose();
        } else if (state == AppLifecycleState.resumed) {
          // 在前台
          if (controller != null) {
            onNewCameraSelected(controller.description);
          }
        }
      }
    
      final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
    
      
      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: _cameraPreviewWidget(),
                    ),
                  ),
                  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: const Icon(Icons.stop),
              color: Colors.red,
              onPressed: controller != null &&
                  controller.value.isInitialized &&
                  controller.value.isRecordingVideo
                  ? onStopButtonPressed
                  : null,
            )
          ],
        );
      }
    
      /// 展示所有摄像头
      Widget _cameraTogglesRowWidget() {
        final List<Widget> toggles = <Widget>[];
    
        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.high,
          enableAudio: enableAudio,
        );
    
        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 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');
        });
      }
    
      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> _startVideoPlayer() async {
        final VideoPlayerController vcontroller =
        VideoPlayerController.file(File(videoPath));
        videoPlayerListener = () {
          if (videoController != null && videoController.value.size != null) {
            // Refreshing the state to update video player with the correct ratio.
            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) {
          // A capture is already pending, do nothing.
          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('Error: ${e.code}\n${e.description}');
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411

如果代码运行遇到困难,请直接查看camera官方文档 (opens new window)

# 12.6.2 PlatformView (示例:WebView)

如果我们在开发过程中需要使用一个原生组件,但这个原生组件在Flutter中很难实现时怎么办(如webview)?这时一个简单的方法就是将需要使用原生组件的页面全部用原生实现,在flutter中需要打开该页面时通过消息通道打开这个原生的页面。但是这种方法有一个最大的缺点,就是原生组件很难和Flutter组件进行组合。

在 Flutter 1.0版本中,Flutter SDK中新增了AndroidViewUIKitView 两个组件,这两个组件的主要功能就是将原生的Android组件和iOS组件嵌入到Flutter的组件树中,这个功能是非常重要的,尤其是对一些实现非常复杂的组件,比如webview,这些组件原生已经有了,如果Flutter中要用,重新实现的话成本将非常高,所以如果有一种机制能让Flutter共享原生组件,这将会非常有用,也正因如此,Flutter才提供了这两个组件。

由于AndroidViewUIKitView 是和具体平台相关的,所以称它们为PlatformView。需要说明的是将来Flutter支持的平台可能会增多,则相应的PlatformView也将会变多。那么如何使用Platform View呢?我们以Flutter官方提供的webview_flutter插件 (opens new window)为例:

注意,在本书写作之时,webview_flutter仍处于预览阶段,如您想在项目中使用它,请查看一下webview_flutter插件最新版本及动态。

  1. 原生代码中注册要被Flutter嵌入的组件工厂,如webview_flutter插件中Android端注册webview插件代码:

    public static void registerWith(Registrar registrar) {
       registrar.platformViewRegistry().registerViewFactory("webview", 
       WebViewFactory(registrar.messenger()));
    }
    
    1
    2
    3
    4

    WebViewFactory的具体实现请参考webview_flutter插件的实现源码,在此不再赘述。

  2. 在Flutter中使用;打开Flutter中文社区首页。

    class PlatformViewRoute extends StatelessWidget {
      
      Widget build(BuildContext context) {
        return WebView(
          initialUrl: "https://flutterchina.club",
          javascriptMode: JavascriptMode.unrestricted,
        );
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    运行效果如图12-5所示:

    12-5

注意,使用PlatformView的开销是非常大的,因此,如果一个原生组件用Flutter实现的难度不大时,我们应该首选Flutter实现。

另外,PlatformView的相关功能在作者写作时还处于预览阶段,可能还会发生变化,因此,读者如果需要在项目中使用的话,应查看一下最新的文档。

请作者喝杯咖啡