Flutter實戰(zhàn) Textrue和PlatformView

2021-03-09 14:02 更新

本節(jié)主要介紹原生和 Flutter 之間如何共享圖像,以及如何在 Flutter 中嵌套原生組件。

#12.6.1 Texture(示例:使用攝像頭)

前面說過 Flutter 本身只是一個UI系統(tǒng),對于一些系統(tǒng)能力的調(diào)用我們可以通過消息傳送機制與原生交互。但是這種消息傳送機制并不能覆蓋所有的應用場景,比如我們想調(diào)用攝像頭來拍照或錄視頻,但在拍照和錄視頻的過程中我們需要將預覽畫面顯示到我們的 Flutter UI 中,如果我們要用 Flutter 定義的消息通道機制來實現(xiàn)這個功能,就需要將攝像頭采集的每一幀圖片都要從原生傳遞到 Flutter 中,這樣做代價將會非常大,因為將圖像或視頻數(shù)據(jù)通過消息通道實時傳輸必然會引起內(nèi)存和 CPU 的巨大消耗!為此,F(xiàn)lutter 提供了一種基于 Texture 的圖片數(shù)據(jù)共享機制。

Texture 可以理解為 GPU 內(nèi)保存將要繪制的圖像數(shù)據(jù)的一個對象,F(xiàn)lutter engine 會將 Texture 的數(shù)據(jù)在內(nèi)存中直接進行映射(而無需在原生和 Flutter 之間再進行數(shù)據(jù)傳遞),F(xiàn)lutter 會給每一個 Texture 分配一個 id,同時 Flutter 中提供了一個Texture組件,Texture構(gòu)造函數(shù)定義如下:

const Texture({
  Key key,
  @required this.textureId,
})

Texture 組件正是通過textureId與 Texture 數(shù)據(jù)關(guān)聯(lián)起來;在Texture組件繪制時,F(xiàn)lutter 會自動從內(nèi)存中找到相應 id 的 Texture 數(shù)據(jù),然后進行繪制??梢钥偨Y(jié)一下整個流程:圖像數(shù)據(jù)先在原生部分緩存,然后在 Flutter 部分再通過textureId和緩存關(guān)聯(lián)起來,最后繪制由 Flutter 完成。

如果我們作為一個插件開發(fā)者,我們在原生代碼中分配了textureId,那么在 Flutter 側(cè)使用Texture組件時要如何獲取textureId呢?這又回到了之前的內(nèi)容了,textureId完全可以通過 MethodChannel 來傳遞。

另外,值得注意的是,當原生攝像頭捕獲的圖像發(fā)生變化時,Texture 組件會自動重繪,這不需要我們寫任何 Dart 代碼去控制。

#Texture用法

如果我們要手動實現(xiàn)一個相機插件,和前面幾節(jié)介紹的“獲取剩余電量”插件的步驟一樣,需要分別實現(xiàn)原生部分和 Flutter 部分??紤]到大多數(shù)讀者可能并非同時既了解 Android 開發(fā),又了解 iOS 開發(fā),如果我們再花大量篇幅來介紹不同端的實現(xiàn)可能會沒什么意義,另外,由于 Flutter 官方提供的相機(camera)插件和視頻播放(video_player)插件都是使用 Texture 來實現(xiàn)的,它們本身就是 Texture 非常好的示例,所以在本書中將不會再介紹使用 Texture 的具體流程了,讀者有興趣查看 camera和video_player 的實現(xiàn)代碼。下面我們重點介紹一下如何使用 camera 和 video_player。

#相機示例

下面我們看一下 camera 包自帶的一個示例,它包含如下功能:

  1. 可以拍照,也可以拍視頻,拍攝完成后可以保存;排號的視頻可以播放預覽。
  2. 可以切換攝像頭(前置攝像頭、后置攝像頭、其它)
  3. 可以顯示已經(jīng)拍攝內(nèi)容的預覽圖。

下面我們看一下具體代碼:

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

   dependencies:
     ...  //省略無關(guān)代碼
     camera: ^0.5.2+2

  1. main方法中獲取可用攝像頭列表。

   void main() async {
     // 獲取可用攝像頭列表,cameras為全局變量
     cameras = await availableCameras();
     runApp(MyApp());
   }

  1. 構(gòu)建UI。現(xiàn)在我們構(gòu)建如圖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 {
     @override
     _CameraExampleHomeState createState() {
       return _CameraExampleHomeState();
     }
   }

   
   class _CameraExampleHomeState extends State<CameraExampleHome>
       with WidgetsBindingObserver {
     CameraController controller;
     String imagePath; // 圖片保存路徑
     String videoPath; //視頻保存路徑
     VideoPlayerController videoController;
     VoidCallback videoPlayerListener;
     bool enableAudio = true;

   
     @override
     void initState() {
       super.initState();
       // 監(jiān)聽APP狀態(tài)改變,是否在前臺
       WidgetsBinding.instance.addObserver(this);
     }

   
     @override
     void dispose() {
       WidgetsBinding.instance.removeObserver(this);
       super.dispose();
     }

   
     @override
     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>();

   
     @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: _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),
         );
       }
     }

   
     /// 開啟或關(guān)閉錄音
     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)));
     }

   
     // 攝像頭選中回調(diào)
     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(() {});
       }
     }

   
     // 拍照按鈕點擊回調(diào)
     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}');
     }
   }

如果代碼運行遇到困難,請直接查看camera官方文檔 (opens new window)。

#12.6.2 PlatformView (示例:WebView)

如果我們在開發(fā)過程中需要使用一個原生組件,但這個原生組件在 Flutter 中很難實現(xiàn)時怎么辦(如 webview)?這時一個簡單的方法就是將需要使用原生組件的頁面全部用原生實現(xiàn),在 flutter 中需要打開該頁面時通過消息通道打開這個原生的頁面。但是這種方法有一個最大的缺點,就是原生組件很難和 Flutter 組件進行組合。

在 Flutter 1.0版本中,F(xiàn)lutter SDK 中新增了AndroidViewUIKitView 兩個組件,這兩個組件的主要功能就是將原生的 Android 組件和 iOS 組件嵌入到 Flutter 的組件樹中,這個功能是非常重要的,尤其是對一些實現(xiàn)非常復雜的組件,比如 webview,這些組件原生已經(jīng)有了,如果 Flutter 中要用,重新實現(xiàn)的話成本將非常高,所以如果有一種機制能讓 Flutter 共享原生組件,這將會非常有用,也正因如此,F(xiàn)lutter 才提供了這兩個組件。

由于AndroidViewUIKitView 是和具體平臺相關(guān)的,所以稱它們?yōu)?PlatformView。需要說明的是將來 Flutter 支持的平臺可能會增多,則相應的 PlatformView 也將會變多。那么如何使用 Platform View 呢?我們以 Flutter 官方提供的webview_flutter插件 (opens new window)為例:

注意,在本書寫作之時,webview_flutter 仍處于預覽階段,如您想在項目中使用它,請查看一下 webview_flutter 插件最新版本及動態(tài)。

  1. 原生代碼中注冊要被 Flutter 嵌入的組件工廠,如 webview_flutter 插件中 Android 端注冊webview 插件代碼:

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

WebViewFactory的具體實現(xiàn)請參考 webview_flutter 插件的實現(xiàn)源碼,在此不再贅述。

  1. 在 Flutter 中使用;打開 Flutter 中文社區(qū)首頁。

   class PlatformViewRoute extends StatelessWidget {
     @override
     Widget build(BuildContext context) {
       return WebView(
         initialUrl: "https://flutterchina.club",
         javascriptMode: JavascriptMode.unrestricted,
       );
     }
   }

運行效果如圖12-5所示:

注意,使用 PlatformView 的開銷是非常大的,因此,如果一個原生組件用 Flutter 實現(xiàn)的難度不大時,我們應該首選 Flutter 實現(xiàn)。

另外,PlatformView 的相關(guān)功能在作者寫作時還處于預覽階段,可能還會發(fā)生變化,因此,讀者如果需要在項目中使用的話,應查看一下最新的文檔。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號