本節(jié)主要介紹原生和 Flutter 之間如何共享圖像,以及如何在 Flutter 中嵌套原生組件。
前面說(shuō)過(guò) Flutter 本身只是一個(gè)UI系統(tǒng),對(duì)于一些系統(tǒng)能力的調(diào)用我們可以通過(guò)消息傳送機(jī)制與原生交互。但是這種消息傳送機(jī)制并不能覆蓋所有的應(yīng)用場(chǎng)景,比如我們想調(diào)用攝像頭來(lái)拍照或錄視頻,但在拍照和錄視頻的過(guò)程中我們需要將預(yù)覽畫(huà)面顯示到我們的 Flutter UI 中,如果我們要用 Flutter 定義的消息通道機(jī)制來(lái)實(shí)現(xiàn)這個(gè)功能,就需要將攝像頭采集的每一幀圖片都要從原生傳遞到 Flutter 中,這樣做代價(jià)將會(huì)非常大,因?yàn)閷D像或視頻數(shù)據(jù)通過(guò)消息通道實(shí)時(shí)傳輸必然會(huì)引起內(nèi)存和 CPU 的巨大消耗!為此,F(xiàn)lutter 提供了一種基于 Texture 的圖片數(shù)據(jù)共享機(jī)制。
Texture 可以理解為 GPU 內(nèi)保存將要繪制的圖像數(shù)據(jù)的一個(gè)對(duì)象,F(xiàn)lutter engine 會(huì)將 Texture 的數(shù)據(jù)在內(nèi)存中直接進(jìn)行映射(而無(wú)需在原生和 Flutter 之間再進(jìn)行數(shù)據(jù)傳遞),F(xiàn)lutter 會(huì)給每一個(gè) Texture 分配一個(gè) id,同時(shí) Flutter 中提供了一個(gè)Texture
組件,Texture
構(gòu)造函數(shù)定義如下:
const Texture({
Key key,
@required this.textureId,
})
Texture
組件正是通過(guò)textureId
與 Texture 數(shù)據(jù)關(guān)聯(lián)起來(lái);在Texture
組件繪制時(shí),F(xiàn)lutter 會(huì)自動(dòng)從內(nèi)存中找到相應(yīng) id 的 Texture 數(shù)據(jù),然后進(jìn)行繪制??梢钥偨Y(jié)一下整個(gè)流程:圖像數(shù)據(jù)先在原生部分緩存,然后在 Flutter 部分再通過(guò)textureId
和緩存關(guān)聯(lián)起來(lái),最后繪制由 Flutter 完成。
如果我們作為一個(gè)插件開(kāi)發(fā)者,我們?cè)谠a中分配了textureId
,那么在 Flutter 側(cè)使用Texture
組件時(shí)要如何獲取textureId
呢?這又回到了之前的內(nèi)容了,textureId
完全可以通過(guò) MethodChannel 來(lái)傳遞。
另外,值得注意的是,當(dāng)原生攝像頭捕獲的圖像發(fā)生變化時(shí),Texture
組件會(huì)自動(dòng)重繪,這不需要我們寫(xiě)任何 Dart 代碼去控制。
如果我們要手動(dòng)實(shí)現(xiàn)一個(gè)相機(jī)插件,和前面幾節(jié)介紹的“獲取剩余電量”插件的步驟一樣,需要分別實(shí)現(xiàn)原生部分和 Flutter 部分。考慮到大多數(shù)讀者可能并非同時(shí)既了解 Android 開(kāi)發(fā),又了解 iOS 開(kāi)發(fā),如果我們?cè)倩ù罅科鶃?lái)介紹不同端的實(shí)現(xiàn)可能會(huì)沒(méi)什么意義,另外,由于 Flutter 官方提供的相機(jī)(camera)插件和視頻播放(video_player)插件都是使用 Texture 來(lái)實(shí)現(xiàn)的,它們本身就是 Texture 非常好的示例,所以在本書(shū)中將不會(huì)再介紹使用 Texture 的具體流程了,讀者有興趣查看 camera和video_player 的實(shí)現(xiàn)代碼。下面我們重點(diǎn)介紹一下如何使用 camera 和 video_player。
下面我們看一下 camera 包自帶的一個(gè)示例,它包含如下功能:
下面我們看一下具體代碼:
dependencies:
... //省略無(wú)關(guān)代碼
camera: ^0.5.2+2
main
方法中獲取可用攝像頭列表。 void main() async {
// 獲取可用攝像頭列表,cameras為全局變量
cameras = await availableCameras();
runApp(MyApp());
}
線面是完整的代碼:
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'; //用于播放錄制的視頻
/// 獲取不同攝像頭的圖標(biāo)(前置、后置、其它)
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');
// 示例頁(yè)面路由
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)聽(tīng)APP狀態(tài)改變,是否在前臺(tái)
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// 如果APP不在在前臺(tái)
if (state == AppLifecycleState.inactive) {
controller?.dispose();
} else if (state == AppLifecycleState.resumed) {
// 在前臺(tái)
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('相機(jī)示例'),
),
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(),
],
),
),
],
),
);
}
/// 展示預(yù)覽窗口
Widget _cameraPreviewWidget() {
if (controller == null || !controller.value.isInitialized) {
return const Text(
'選擇一個(gè)攝像頭',
style: TextStyle(
color: Colors.white,
fontSize: 24.0,
fontWeight: FontWeight.w900,
),
);
} else {
return AspectRatio(
aspectRatio: controller.value.aspectRatio,
child: CameraPreview(controller),
);
}
}
/// 開(kāi)啟或關(guān)閉錄音
Widget _toggleAudioWidget() {
return Padding(
padding: const EdgeInsets.only(left: 25),
child: Row(
children: <Widget>[
const Text('開(kāi)啟錄音:'),
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,
),
],
),
),
);
}
/// 相機(jī)工具欄
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('沒(méi)有檢測(cè)到攝像頭');
} 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ǎn)擊回調(diào)
void onTakePictureButtonPressed() {
takePicture().then((String filePath) {
if (mounted) {
setState(() {
imagePath = filePath;
videoController?.dispose();
videoController = null;
});
if (filePath != null) showInSnackBar('圖片保存在 $filePath');
}
});
}
// 開(kāi)始錄制視頻
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('請(qǐng)先選擇一個(gè)攝像頭');
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('錯(cuò)誤: 請(qǐng)先選擇一個(gè)相機(jī)');
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}');
}
}
如果代碼運(yùn)行遇到困難,請(qǐng)直接查看camera官方文檔 (opens new window)。
如果我們?cè)陂_(kāi)發(fā)過(guò)程中需要使用一個(gè)原生組件,但這個(gè)原生組件在 Flutter 中很難實(shí)現(xiàn)時(shí)怎么辦(如 webview)?這時(shí)一個(gè)簡(jiǎn)單的方法就是將需要使用原生組件的頁(yè)面全部用原生實(shí)現(xiàn),在 flutter 中需要打開(kāi)該頁(yè)面時(shí)通過(guò)消息通道打開(kāi)這個(gè)原生的頁(yè)面。但是這種方法有一個(gè)最大的缺點(diǎn),就是原生組件很難和 Flutter 組件進(jìn)行組合。
在 Flutter 1.0版本中,F(xiàn)lutter SDK 中新增了AndroidView
和UIKitView
兩個(gè)組件,這兩個(gè)組件的主要功能就是將原生的 Android 組件和 iOS 組件嵌入到 Flutter 的組件樹(shù)中,這個(gè)功能是非常重要的,尤其是對(duì)一些實(shí)現(xiàn)非常復(fù)雜的組件,比如 webview,這些組件原生已經(jīng)有了,如果 Flutter 中要用,重新實(shí)現(xiàn)的話成本將非常高,所以如果有一種機(jī)制能讓 Flutter 共享原生組件,這將會(huì)非常有用,也正因如此,F(xiàn)lutter 才提供了這兩個(gè)組件。
由于AndroidView
和UIKitView
是和具體平臺(tái)相關(guān)的,所以稱(chēng)它們?yōu)?PlatformView。需要說(shuō)明的是將來(lái) Flutter 支持的平臺(tái)可能會(huì)增多,則相應(yīng)的 PlatformView 也將會(huì)變多。那么如何使用 Platform View 呢?我們以 Flutter 官方提供的webview_flutter插件 (opens new window)為例:
注意,在本書(shū)寫(xiě)作之時(shí),webview_flutter 仍處于預(yù)覽階段,如您想在項(xiàng)目中使用它,請(qǐng)查看一下 webview_flutter 插件最新版本及動(dòng)態(tài)。
public static void registerWith(Registrar registrar) {
registrar.platformViewRegistry().registerViewFactory("webview",
WebViewFactory(registrar.messenger()));
}
WebViewFactory
的具體實(shí)現(xiàn)請(qǐng)參考 webview_flutter 插件的實(shí)現(xiàn)源碼,在此不再贅述。
class PlatformViewRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return WebView(
initialUrl: "https://flutterchina.club",
javascriptMode: JavascriptMode.unrestricted,
);
}
}
運(yùn)行效果如圖12-5所示:
注意,使用 PlatformView 的開(kāi)銷(xiāo)是非常大的,因此,如果一個(gè)原生組件用 Flutter 實(shí)現(xiàn)的難度不大時(shí),我們應(yīng)該首選 Flutter 實(shí)現(xiàn)。
另外,PlatformView 的相關(guān)功能在作者寫(xiě)作時(shí)還處于預(yù)覽階段,可能還會(huì)發(fā)生變化,因此,讀者如果需要在項(xiàng)目中使用的話,應(yīng)查看一下最新的文檔。
更多建議: