FlutterPlugin 实现双屏
2021-03-16 本文已影响0人
李小轰
背景
由于项目需要,团队使用flutter进行开发,实现一款门店点餐的app(android端双屏设备),工作人员使用主屏操作点餐,副屏显示餐单和价格等信息给顾客。
技术方案:
- 整体项目为flutter-application的形式,我们将副屏能力封装成plugin提供给主程序使用;
- android 8.0以下的设备,副屏显示方案为presentation,通过插件唤起原生的presentation;
- 副屏一个维护flutterEngine,主屏维护一个flutterEngine,两个engine间使用channel进行关联,相互传递事件数据;
记录实现步骤:
1. 使用 androidStudio 创建一个 flutterPlugin 项目: flutter_subscreen_plugin(目录结构如下)
image.png2. 第二步,封装原生能力,提供唤起第二屏幕的能力
创建一个类 FlutterSubScreenPresentation 继承自 Presentation,作为副屏的UI载体:
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
class FlutterSubScreenPresentation(outerContext: Context?, display: Display?) : Presentation(outerContext, display) {
lateinit var flutterEngine: FlutterEngine
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val engine = FlutterEngine(context)
flutterEngine = engine
//指定初始化路由
flutterEngine.navigationChannel.setInitialRoute("subMain");
flutterEngine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
"main"))
setContentView(R.layout.flutter_presentation_view)
val flutterView: FlutterView = findViewById(R.id.flutter_presentation_view)
flutterView.attachToFlutterEngine(flutterEngine)
// 一定要调用 不然页面会卡死不更新
flutterEngine.lifecycleChannel.appIsResumed()
}
override fun dismiss() {
flutterEngine.lifecycleChannel.appIsDetached()
super.dismiss()
}
}
flutter_presentation_view 文件如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<io.flutter.embedding.android.FlutterView
android:id="@+id/flutter_presentation_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
技术点:
- 继承 presentation, 重写onCreate,新建一个flutterEngine,用于关联flutterView,将flutterView作为 setContentView 的入参,实现使用flutter层来绘制副屏页面;
- setInitialRoute("subMain") 用于指定main.dart中的初始化路由
- dartExecutor.executeDartEntrypoin 用于指定engine对应的渲染页面的路径:lib/main.dart
- flutterView.attachToFlutterEngine(flutterEngine) 此时进行UI渲染
- flutterEngine.lifecycleChannel.appIsResumed() 生命事件传递
3. 接下来我们看看主副屏间是如何实现交互的:
当我们创建了plugin项目时,自动生成了一个类 FlutterSubscreenPlugin ,主副屏通过这个中间件来进行交互。
我们将结构分为三种颜色来进行标记:(如下)
- 定义两个channel,一个用于主屏与原生交互,一个用于副屏与原生交互(蓝色)
- 本插件(FlutterSubscreenPlugin)与主工程(主屏)进行绑定时,onAttachedToEngine被触发,此时,使用mainChannel来进行绑定监听,在onMethodCall中处理事件监听,将mainChannel接收到的事件传递给subChannel 进行分发(红色)【主 --> 副】
- 提供方法给外部初始化,提供能力将subChannel接收到的事件传递给mainChannel 实现副屏与主屏的数据传递(绿色)【副 --> 主】
4. 新建一个工具类 FlutterSubScreenProvider ,提供给壳用来触发唤起副屏以及绑定事件传递关系
class FlutterSubScreenProvider {
//设置显示副屏幕, 在壳工程的MainActivity初始化
fun configSecondDisplay(plugin: FlutterSubscreenPlugin, context: Context) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
val manager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
val displays = manager.displays
if (displays.size > 1) {
val display = displays[1]
val handler = FlutterSubScreenPresentation(context, display)
handler.show()
plugin.onCreateViceChannel(handler.flutterEngine.dartExecutor)
}
}
} catch (e: Throwable) {
println(e.message)
e.printStackTrace()
}
}
}
5. 在 MainActivity 调用唤起副屏以及绑定事件交互:
class MainActivity: FlutterActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initSubScreen()
}
//初始化副屏
private fun initSubScreen() {
flutterEngine?.plugins?.get(FlutterSubscreenPlugin::class.java)?.let { plugin ->
val subScreenPlugin = plugin as FlutterSubscreenPlugin
val subScreenProvider = FlutterSubScreenProvider()
subScreenProvider.configSecondDisplay(subScreenPlugin, context)
}
}
}
接下来粘贴一下dart文件中UI层需要做的处理:main.dart
void main() {
var defaultRouteName = window.defaultRouteName;
if ("subMain" == defaultRouteName) {
viceScreenMain();
} else {
defaultMain();
}
}
//主屏ui
void defaultMain() {
runApp(MyApp(
isViceScreen: false,
));
}
//副屏ui
void viceScreenMain() {
runApp(MyApp(
isViceScreen: true,
));
}
在main方法中获取initRoute做区分,绑定对应的widget
新建一个工具类用于channel 主副屏交互:
///封装方法用于主副屏交互
class SubScreenPlugin {
static const _mainChannelName = 'screen_plugin_main_channel';
static const _subChannelName = 'screen_plugin_sub_channel';
static MethodChannel _mainChannel = MethodChannel(_mainChannelName)
..setMethodCallHandler(_onMainChannelMethodHandler);
static Future<dynamic> _onMainChannelMethodHandler(MethodCall call) async {
print(call.method);
}
static MethodChannel _subChannel;
// ignore: close_sinks
static StreamController<MethodCall> _subStreamController;
static Stream<MethodCall> get viceStream {
if (_subChannel == null) {
_subChannel = MethodChannel(_subChannelName)
..setMethodCallHandler(_onSubChannelMethodHandler);
}
if (_subStreamController == null) {
_subStreamController = StreamController<MethodCall>.broadcast();
}
return _subStreamController.stream;
}
static Future<dynamic> _onSubChannelMethodHandler(MethodCall call) async {
//副屏channel 没接收到一个事件都放进去流里, 由外部监听
_subStreamController?.sink?.add(call);
return "success";
}
//给主屏幕调用,发送事件体给副屏
static Future<void> sendMsgToViceScreen(
String method, {
Map<String, dynamic> params,
}) async {
await _mainChannel.invokeMethod(method, params);
}
}
通过如下方法,可以拿到主屏传递给副屏的所有事件数据:
//发送数据
SubScreenPlugin.sendMsgToViceScreen("test",params: {"content": "test"});
//获取数据
SubScreenPlugin.viceStream.listen((event) {
val name = event.method;//test
val params = event.arguments;// {"content": "test"}
});
注意:使用android的双屏,需要在清单配置文件,添加如下两个权限:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />