FlutterFlutter跨平台应用

FlutterPlugin 实现双屏

2021-03-16  本文已影响0人  李小轰

背景

由于项目需要,团队使用flutter进行开发,实现一款门店点餐的app(android端双屏设备),工作人员使用主屏操作点餐,副屏显示餐单和价格等信息给顾客。

技术方案:
  1. 整体项目为flutter-application的形式,我们将副屏能力封装成plugin提供给主程序使用;
  2. android 8.0以下的设备,副屏显示方案为presentation,通过插件唤起原生的presentation;
  3. 副屏一个维护flutterEngine,主屏维护一个flutterEngine,两个engine间使用channel进行关联,相互传递事件数据;

记录实现步骤:

1. 使用 androidStudio 创建一个 flutterPlugin 项目: flutter_subscreen_plugin(目录结构如下)
image.png
2. 第二步,封装原生能力,提供唤起第二屏幕的能力

创建一个类 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>
技术点:
  1. 继承 presentation, 重写onCreate,新建一个flutterEngine,用于关联flutterView,将flutterView作为 setContentView 的入参,实现使用flutter层来绘制副屏页面;
  2. setInitialRoute("subMain") 用于指定main.dart中的初始化路由
  3. dartExecutor.executeDartEntrypoin 用于指定engine对应的渲染页面的路径:lib/main.dart
  4. flutterView.attachToFlutterEngine(flutterEngine) 此时进行UI渲染
  5. flutterEngine.lifecycleChannel.appIsResumed() 生命事件传递
3. 接下来我们看看主副屏间是如何实现交互的:

当我们创建了plugin项目时,自动生成了一个类 FlutterSubscreenPlugin ,主副屏通过这个中间件来进行交互。
我们将结构分为三种颜色来进行标记:(如下)

  1. 定义两个channel,一个用于主屏与原生交互,一个用于副屏与原生交互(蓝色)
  2. 本插件(FlutterSubscreenPlugin)与主工程(主屏)进行绑定时,onAttachedToEngine被触发,此时,使用mainChannel来进行绑定监听,在onMethodCall中处理事件监听,将mainChannel接收到的事件传递给subChannel 进行分发(红色)【主 --> 副】
  3. 提供方法给外部初始化,提供能力将subChannel接收到的事件传递给mainChannel 实现副屏与主屏的数据传递(绿色)【副 --> 主】
image.png
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" />
整个调用关系的结构如下:
image.png
git已上传,项目路径:https://github.com/liyufengrex/flutter_subscreen_plugin
上一篇下一篇

猜你喜欢

热点阅读