Flutter了解之包(Package)
目录
1. 使用包
2. 开发Dart包(创建共享的模块化代码)
3. 开发插件包
4. 平台通道(platform channel)
5. Texture和PlatformView
一些公共的库或SDK会被很多项目用到,将这些代码单独抽到一个独立模块,需要时直接集成,会大大提高开发效率。
很多编程语言都支持“模块共享”,如Java的jar包,Android的aar包,Web的npm包等。
最小的包包括:
1. 一个pubspec.yaml文件。声明了package的名称、版本、作者等的元数据文件。
2. 一个lib文件夹。公开的代码,最少应有一个<package-name>.dart文件
Flutter包分为两类:
1. Dart包:使用到了Flutter的特定功能,因此对Flutter框架具有依赖性,这种包仅用于Flutter。如:fluro包。
2. 插件包(包含原生代码):一种专用的Dart包,其中包含用Dart代码编写的API,以及针对Android(使用Java或Kotlin)和针对iOS(使用OC或Swift)平台的特定实现。如:battery插件包。
虽然Flutter的Dart运行时和Dart VM运行时不是完全相同,但是如果包中没有涉及这些存在差异的部分,那么这样的包可以同时支持Flutter和Dart VM。如Dart http网络库dio。
一个APP在实际开发中往往会依赖很多包,而这些包通常都有交叉依赖关系、版本依赖等,如果由开发者手动来管理应用中的依赖包将会非常麻烦。
因此,各种开发生态或编程语言官方通常都会提供一些包管理工具,比如在Android提供了Gradle来管理依赖,iOS用Cocoapods或Carthage来管理依赖,Node中通过npm等。
pubspec.yaml
Flutter项目默认的配置文件是pubspec.yaml(位于项目根目录下)来管理第三方依赖包。
YAML是一种直观、可读性高并且容易被人类阅读的文件格式,它和xml或Json相比,它语法简单并非常容易解析,所以YAML常用于配置文件。
例(pubspec.yaml)
name: hello
description: First Flutter application.
version: 1.0.0+1
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
说明:
name:应用名/包名。
description: 应用/包的描述、简介。
version:应用/包的版本号。
1. 忽略时为最新版本
2. 指定范围:>=0.1.2 <0.2.0
3. 0.1.x(x大于2):^0.1.2
4. 指定版本:0.1.1
dependencies:应用/包依赖的其它包或插件。
dev_dependencies:开发环境依赖的工具包(而不是flutter应用本身依赖的包)。
flutter:flutter相关的配置选项。
包的依赖方式:
1. Pub仓库
2. 本地包
3. Git
1. 使用包
- 依赖Pub仓库 Pub
Pub是Google官方的Dart Packages仓库,类似于node中的npm仓库,android中的jcenter。在Pub上可以查找或发布 包和插件。
例(一个显示随机字符串的widget)
首先在pub上找到english_words包(包含数千个常用的英文单词以及一些实用功能),确定其最新的版本号和是否支持Flutter。
1. pubspec.yaml文件将“english_words”(3.1.5版本)添加到依赖项列表,如下:
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.0
# 新添加的依赖
english_words: ^3.1.5
2. 下载包。在Android Studio的编辑器视图中查看pubspec.yaml时,单击右上角的 Get dependencies 。这会将依赖包安装到项目,包的版本会保存在pubspec.lock。
命令行对应的命令:flutter packages get
/*
更新包:flutter packages upgrade
*/
3. 引入english_words包。
import 'package:english_words/english_words.dart';
4. 使用english_words包来生成随机字符串。
class RandomWordsWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 生成随机字符串
final wordPair = new WordPair.random();
return Padding(
padding: const EdgeInsets.all(8.0),
child: new Text(wordPair.toString()),
);
}
}
_MyHomePageState.build 的Column中
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
... //省略无关代码
RandomWordsWidget(),
],
)
Pub搜索english_words
pubspec.yaml添加依赖包
下载包
引入包
运行,每次保存字符串都会变化
css_colors包
提供颜色相关功能,如:CSSColors.orange
url_launcher包
打开浏览器,launch('https://wwww.baidu.com')
date_format包
日期格式
- 依赖本地包
dependencies:
pkg1:
path: ../../code/pkg1
路径可以是相对的,也可以是绝对的。
- 依赖Git
包位于Git存储库的根目录中
dependencies:
pkg1:
git:
url: git://github.com/xxx/pkg1.git
不在根目录中
dependencies:
package1:
git:
url: git://github.com/flutter/packages.git
path: packages/package1
发生冲突
// 都声明了Person类时会冲突
import ‘lib/person1.dart’;
import ‘lib/person2.dart’ as lib;
main(){
Person person1=new Person();
lib.Person person2=new lib,Person();
}
导入部分
方式一:导入需要的部分(使用show)
import ‘lib/person1.dart’ show Person;
方式二:隐藏不需要的部分(使用hide)
import ‘lib/person1.dart’ hide run;
延迟加载
需要的时候才去加载(可用来减少app启动时间)
import ‘lib/person1.dart’ deferred as hello;
func() async{
// 加载库
await hello.loadLibrary();
hello.run();
}
分片
part 'personPart1.dart'
part 'personPart2.dart'
2. 开发Dart包(创建共享的模块化代码)
第一步:创建包
通过Android Studio:File>New>New Flutter Project 来创建一个Package工程
或终端命令使用 flutter create --template=package hello
包含 :
lib/hello.dart:Package的Dart代码
test/hello_test.dart:Package的单元测试代码。
第二步:实现包
对于纯Dart包,只需在主lib/<package name>.dart文件内或lib目录中的文件中添加功能即可 。
要测试软件包,在test目录中添加unit tests。
例(shelf Package的目录结构)
Package中主要的功能的源码都在src目录下。shelf Package也导出了一个迷你库: shelf_io,它主要是处理HttpRequest的。
在lib根目录下的“shelf.dart”中,导出了多个“lib/src”目录下的dart文件:
export 'src/cascade.dart';
export 'src/handler.dart';
export 'src/handlers/logger.dart';
export 'src/hijack_exception.dart';
export 'src/middleware.dart';
export 'src/pipeline.dart';
export 'src/request.dart';
export 'src/response.dart';
export 'src/server.dart';
export 'src/server_handler.dart';
shelf Package的目录结构
第三步:生成文档
1. README.md
介绍包
2. CHANGELOG.md
记录每个版本中的更改
3. LICENSE
许可条款
4. 所有公共API的API文档
在发布软件包时,API文档会自动生成并发布到dartdocs.org
可使用 dartdoc 工具来为Package生成文档。开发者只需要遵守文档注释语法,在代码中添加文档注释,最后使用dartdoc生成API文档(一个静态网站)。文档注释是使用三斜线"///"开始,如:
/// The event handler responsible for updating the badge in the UI.
void updateBadge() {
...
}
第四步:发布Package(到Pub上)
1. 检查pubspec.yaml、README.md以及CHANGELOG.md文件,以确保其内容的完整性和正确性。
2. 运行 dry-run 命令(查看是否都准备OK):
flutter packages pub publish --dry-run
3. 验证无误后,发布
flutter packages pub publish
如果遇到包发布失败的情况,先检查是否因为众所周知的网络原因,如果是网络问题,可以使用VPN,这里需要注意的是一些代理只会代理部分APP的网络请求,如浏览器的,它们可能并不能代理dart的网络请求,所以在这种情况下,即使开了代理也依然无法连接到Pub,因此,在发布Pub包时使用全局代理或全局VPN会保险些。如果网络没有问题,以管理员权限(sudo)运行发布命令重试。
很多时候开启全局代理也不会让terminal中的流量打代理服务器走,以socks5为例,应该在终端下输入以下指令:
export all_proxy=socks5://127.0.0.1:1080
此时终端中的http和https流量会打代理服务器走,可以通过curl -i https://ip.cn指令查看代理设置是否成功。
最后一步:使用包
1. 添加依赖包
dependencies:
hello:^1.1.0
2. 通过"package:"指令来指定包的入口文件:
import 'package:hello/hello.dart';
处理包的相互依赖
如果正在开发一个hello包,它依赖于另一个包,则需要在hello/pubspec.yaml的dependencies中添加该依赖包:
dependencies:
url_launcher: ^0.4.2
现在可以在hello中import 'package:url_launcher/url_launcher.dart' 然后调用 launch()方法了。
但是,如果hello碰巧是一个插件包,其平台特定的代码需要访问url_launcher公开的特定于平台的API,那么还需要为特定于平台的构建文件添加合适的依赖声明:
1. Android
在 hello/android/build.gradle:
android {
// lines skipped
dependencies {
provided rootProject.findProject(":url_launcher")
}
}
现在可以在hello/android/src源码中import io.flutter.plugins.urllauncher.UrlLauncherPlugin访问UrlLauncherPlugin类。
2. iOS
在hello/ios/hello.podspec:
Pod::Spec.new do |s|
# lines skipped
s.dependency 'url_launcher'
现在可以在hello/ios/Classes源码中 #import "UrlLauncherPlugin.h" 然后访问 UrlLauncherPlugin类。
解决依赖冲突
假设在hello包中使用some_package和other_package,并且这两个包都依赖url_launcher,但是依赖的是url_launcher的不同的版本,这就有潜在的冲突了。
方法1(最好)
在指定依赖关系时,使用版本范围而不是特定版本,pub将能够自动解决问题。
dependencies:
url_launcher: ^0.4.2 # 较好, 任何0.4.x(x >= 2)都可.
image_picker: '0.1.1' # 不好,只有0.1.1版本.
方法2
添加依赖覆盖声明,从而强制使用特定版本
dependencies:
some_package:
dependency_overrides:
url_launcher: '0.4.3'
如果冲突的依赖不是一个包,而是一个特定于Android的库,比如guava,那么必须将依赖重写声明添加到Gradle构建逻辑中。
强制使用23.0版本的guava库,在hello/android/build.gradle中:
configurations.all {
resolutionStrategy {
force 'com.google.guava:guava:23.0-android'
}
}
Cocoapods目前不提供依赖覆盖功能。
3. 开发Flutter插件包
创建包
flutter create --org com.baidu.www --template=plugin -i objc -a java flutter_package_app
说明:
1. --org选项指定组织(反向域名),Android和iOS的标识符
2. -a指定android语言 java、kotlin(默认)
3. -i指定iOS语言 objc、swift(默认)
创建包
4. 平台通道(platform channel)
“平台特定”或“特定平台”中的平台指的就是Flutter应用程序运行的平台,如Android或IOS。
一个完整的Flutter应用程序实际上包括原生代码和Flutter代码两部分。由于Flutter本身只是一个UI系统,它本身是无法提供一些系统能力,比如使用蓝牙、相机、GPS等,因此要在Flutter APP中调用这些能力就必须和原生平台进行通信。为此,Flutter中提供了一个平台通道,用于Flutter和原生平台的通信。
Flutter使用了一个灵活的系统,允许开发人员调用特定平台的API(无论在Android上的Java或Kotlin代码中,还是iOS上的ObjectiveC或Swift代码中均可用)。
Flutter与原生之间的通信依赖灵活的消息传递方式:
应用的Flutter部分通过平台通道将消息发送到其应用程序的所在的原生宿主(iOS或Android)应用。
宿主监听平台通道,并接收该消息。然后它会调用该平台的API,并将响应发送回客户端,即应用程序的Flutter部分。
消息传递是异步的,确保用户界面在消息传递时不会被挂起(卡顿)。
当在Flutter中调用原生方法时,调用信息通过平台通道传递到原生,原生收到调用信息后方可执行指定的操作,如需返回数据,则原生会将数据再通过平台通道传递给Flutter。
在客户端,MethodChannel API 可以发送与方法调用相对应的消息。 在宿主平台上,MethodChannel Android API 和 FlutterMethodChannel iOS API可以接收方法调用并返回结果。这些类可以用很少的代码就能开发平台插件。
方法调用(消息传递)可以是反向的,即宿主作为客户端调用Dart中实现的API。如: quick_actions插件。
除了上面提到的MethodChannel,还可以使用BasicMessageChannel,它支持使用自定义消息编解码器进行基本的异步消息传递。 此外,可以使用专门的BinaryCodec、StringCodec和 JSONMessageCodec类,或创建自己的编解码器。
使用平台通道在Flutter(client)和原生(host)之间传递消息
平台通道数据类型支持
平台通道使用标准消息编/解码器对消息进行编解码,它可以高效的对消息进行二进制序列化与反序列化。
当在发送和接收值时,这些值在消息中的序列化和反序列化会自动进行。
平台通道数据类型支持
如何获取平台信息
Flutter 中提供了一个全局变量defaultTargetPlatform来获取当前应用的平台信息,defaultTargetPlatform定义在"platform.dart"中,它的类型是TargetPlatform,这是一个枚举类,定义如下:
enum TargetPlatform {
android,
fuchsia,
iOS,
}
目前Flutter只支持这三个平台
判断平台:if(defaultTargetPlatform==TargetPlatform.android){}
由于不同平台有它们各自的交互规范,Flutter Material库中的一些组件都针对相应的平台做了一些适配,比如路由组件MaterialPageRoute,它在android和ios中会应用各自平台规范的切换动画。那如果想让APP在所有平台都表现一致,比如希望在所有平台路由切换动画都按照ios平台一致的左右滑动切换风格该怎么做?Flutter中提供了一种覆盖默认平台的机制,可以通过显式指定debugDefaultTargetPlatformOverride全局变量的值来指定应用平台。比如:
debugDefaultTargetPlatformOverride=TargetPlatform.iOS;
print(defaultTargetPlatform); // 会输出TargetPlatform.iOS
上面代码即在Android中运行后,Flutter APP就会认为是当前系统是iOS,Material组件库中所有组件交互方式都会和iOS平台对齐,defaultTargetPlatform的值也会变为TargetPlatform.iOS。
例(获取电池电量)
在Dart中通过getBatteryLevel 调用Android BatteryManager API和iOS device.batteryLevel API
第一步:创建一个新的应用程序项目
// 默认情况下,模板支持使用Java编写Android代码,或使用Objective-C编写iOS代码。要使用Kotlin或Swift,请使用-i和/或-a标志: flutter create -i swift -a kotlin batterylevel
在终端中运行:flutter create batterylevel
第二步:创建Flutter平台客户端
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
。。。
// 该State类拥有当前的应用状态。需要延长这一点以保持当前的电量
class _MyHomePageState extends State<MyHomePage> {
// 首先,构建通道。使用MethodChannel,调用一个方法来返回电池电量。
// 通道的客户端和宿主通过通道构造函数中传递的通道名称进行连接。单个应用中使用的所有通道名称必须是唯一的; 建议在通道名称前加一个唯一的“域名前缀”,例如samples.flutter.io/battery。
static const platform = const MethodChannel('samples.flutter.io/battery');
// Get battery level.
String _batteryLevel = 'Unknown battery level.';
Future<Null> _getBatteryLevel() async {
String batteryLevel;
try {
// 调用通道上的方法,指定通过字符串标识符调用方法getBatteryLevel。 该调用可能失败(平台不支持平台API,例如在模拟器中运行时),所以将invokeMethod调用包装在try-catch语句中。
final int result = await platform.invokeMethod('getBatteryLevel');
batteryLevel = 'Battery level at $result % .';
} on PlatformException catch (e) {
batteryLevel = "Failed to get battery level: '${e.message}'.";
}
// 使用返回的结果,在setState中来更新用户界面状态batteryLevel。
setState(() {
_batteryLevel = batteryLevel;
});
}
@override
// 在build创建包含一个小字体显示电池状态和一个用于刷新值的按钮的用户界面。
Widget build(BuildContext context) {
return new Material(
child: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
new RaisedButton(
child: new Text('Get Battery Level'),
onPressed: _getBatteryLevel,
),
new Text(_batteryLevel),
],
),
),
);
}
}
Android端API实现(android/src/.../MainActivity.java文件)
import io.flutter.app.FlutterActivity;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
// 添加需要导入的依赖
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
public class MainActivity extends FlutterActivity {
private static final String CHANNEL = "samples.flutter.io/battery";
@Override
// 在onCreate里创建MethodChannel并设置一个MethodCallHandler。确保使用和Flutter客户端中使用的通道名称相同的名称。
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
new MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, Result result) {
// 在call参数中进行检测是否为getBatteryLevel
if (call.method.equals("getBatteryLevel")) {
int batteryLevel = getBatteryLevel();
if (batteryLevel != -1) {
result.success(batteryLevel);
} else {
result.error("UNAVAILABLE", "Battery level not available.", null);
}
} else {
result.notImplemented();
}
}
});
}
private int getBatteryLevel() {
int batteryLevel = -1;
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
BatteryManager batteryManager = (BatteryManager) getSystemService(BATTERY_SERVICE);
batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
} else {
Intent intent = new ContextWrapper(getApplicationContext()).
registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
batteryLevel = (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100) /
intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
}
return batteryLevel;
}
}
iOS端API实现(AppDelegate.m文件)
#import <Flutter/Flutter.h>
@implementation AppDelegate
// 在application didFinishLaunchingWithOptions:方法内部创建一个FlutterMethodChannel,并添加一个处理方法。 确保与在Flutter客户端使用的通道名称相同。
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;
FlutterMethodChannel* batteryChannel = [FlutterMethodChannel
methodChannelWithName:@"samples.flutter.io/battery"
binaryMessenger:controller];
[batteryChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
// call参数中进行检测是否为getBatteryLevel
if ([@"getBatteryLevel" isEqualToString:call.method]) {
int batteryLevel = [self getBatteryLevel];
if (batteryLevel == -1) {
result([FlutterError errorWithCode:@"UNAVAILABLE"
message:@"电池信息不可用"
details:nil]);
} else {
result(@(batteryLevel));
}
} else {
result(FlutterMethodNotImplemented);
}
}];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
- (int)getBatteryLevel {
UIDevice* device = UIDevice.currentDevice;
device.batteryMonitoringEnabled = YES;
if (device.batteryState == UIDeviceBatteryStateUnknown) {
return -1;
} else {
return (int)(device.batteryLevel * 100);
}
}
5. Texture和PlatformView
Flutter本身只是一个UI系统,对于一些系统能力的调用可以通过消息传送机制与原生交互。但是这种消息传送机制并不能覆盖所有的应用场景,比如想调用摄像头来拍照或录视频,但在拍照和录视频的过程中需要将预览画面显示到Flutter UI中,如果要用Flutter定义的消息通道机制来实现这个功能,就需要将摄像头采集的每一帧图片都要从原生传递到Flutter中,这样做代价将会非常大,因为将图像或视频数据通过消息通道实时传输必然会引起内存和CPU的巨大消耗!为此,Flutter提供了一种基于Texture的图片数据共享机制。
Texture(使用摄像头)
Texture可以理解为GPU内保存将要绘制的图像数据的一个对象,Flutter engine会将Texture的数据在内存中直接进行映射(而无需在原生和Flutter之间再进行数据传递),Flutter会给每一个Texture分配一个id,同时Flutter中提供了一个Texture组件,Texture构造函数定义如下:
const Texture({
Key key,
@required this.textureId,
})
Texture 组件正是通过textureId与Texture数据关联起来;在Texture组件绘制时,Flutter会自动从内存中找到相应id的Texture数据,然后进行绘制。可以总结一下整个流程:图像数据先在原生部分缓存,然后在Flutter部分再通过textureId和缓存关联起来,最后绘制由Flutter完成。
如果我们作为一个插件开发者,我们在原生代码中分配了textureId,那么通过MethodChannel来传递textureId在Flutter侧获取。
当原生摄像头捕获的图像发生变化时,Texture 组件会自动重绘,这不需要我们写任何Dart 代码去控制。
如果我们要手动实现一个相机插件,需要分别实现原生部分和Flutter部分。
Flutter官方提供的相机(camera)插件和视频播放(video_player)插件都是使用Texture来实现的
camera包自带的一个示例,它包含如下功能:
可以拍照,也可以拍视频,拍摄完成后可以保存;排号的视频可以播放预览。
可以切换摄像头(前置摄像头、后置摄像头、其它)
可以显示已经拍摄内容的预览图。
1. 首先,依赖camera插件的最新版,并下载依赖。
dependencies:
... //省略无关代码
camera: ^0.5.2+2
2. 在main方法中获取可用摄像头列表。
void main() async {
// 获取可用摄像头列表,cameras为全局变量
cameras = await availableCameras();
runApp(MyApp());
}
3. 构建UI
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();
// 监听APP状态改变,是否在前台
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),
);
}
}
/// 开启或关闭录音
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}');
}
}
PlatformView
如果在开发过程中需要使用一个原生组件,但这个原生组件在Flutter中很难实现时怎么办(如webview)?这时一个简单的方法就是将需要使用原生组件的页面全部用原生实现,在flutter中需要打开该页面时通过消息通道打开这个原生的页面。
缺点:
1. 原生组件很难和Flutter组件进行组合。
2. 开销是非常大的。
如果一个原生组件用Flutter实现的难度不大时,应该首选Flutter实现。
在 Flutter 1.0版本中,Flutter SDK中新增了AndroidView和UIKitView 两个组件,这两个组件的主要功能就是将原生的Android组件和iOS组件嵌入到Flutter的组件树中,这个功能是非常重要的,尤其是对一些实现非常复杂的组件,比如webview,这些组件原生已经有了,如果Flutter中要用,重新实现的话成本将非常高,所以如果有一种机制能让Flutter共享原生组件,这将会非常有用,也正因如此,Flutter才提供了这两个组件。
由于AndroidView和UIKitView 是和具体平台相关的,所以称它们为PlatformView。需要说明的是将来Flutter支持的平台可能会增多,则相应的PlatformView也将会变多。
1. 原生代码中注册要被Flutter嵌入的组件工厂,如webview_flutter插件中Android端注册webview插件代码:
public static void registerWith(Registrar registrar) {
registrar.platformViewRegistry().registerViewFactory("webview",
WebViewFactory(registrar.messenger()));
}
WebViewFactory的具体实现请参考webview_flutter插件的实现源码。
2. 在Flutter中使用;打开Flutter中文社区首页。
class PlatformViewRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return WebView(
initialUrl: "https://flutterchina.club",
javascriptMode: JavascriptMode.unrestricted,
);
}
}