Flutter Android多窗口方案落地(下)
插件层封装。插件层就很简单了,创建好
MethodCallHandler
之后,直接持有单例的EngineManager
就可以了。
class FlutterMultiWindowsPlugin : FlutterPlugin, MethodCallHandler {
companion object {
private const val TAG = "MultiWindowsPlugin"
}
• @SuppressLint("LongLogTag")
• override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
• Log.i(TAG, "onMessage: onAttachedToEngine")
• Log.i(TAG, "onAttachedToEngine: ${Thread.currentThread().name}")
• MessageHandle.init(flutterPluginBinding.applicationContext)
• MethodChannel(
• flutterPluginBinding.binaryMessenger,
• "flutter_multi_windows.messageChannel",
• ).setMethodCallHandler(this)
• }
• override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
• Log.i(TAG, "onDetachedFromEngine: ${Thread.currentThread().name}")
• }
• override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
• Log.i(TAG, "onMethodCall: thread : ${Thread.currentThread().name}")
• MessageHandle.onMessage(call, result)
• }
}
@SuppressLint("StaticFieldLeak")
internal object MessageHandle {
private const val TAG = "MessageHandle"
• private var context: Context? = null
• private var manager: EngineManager? = null
• fun init(context: Context) {
• this.context = context
• if (manager != null)
• return
• // 必须单例调用
• manager = EngineManager.getInstance(this.context!!)
• }
• // 处理消息,所有管道通用。需要共享Flutter Activity
• fun onMessage(
• call: MethodCall, result: MethodChannel.Result
• ) {
• val params = call.arguments as Map<*, *>
• when (call.method) {
• "open" -> {
• Log.i(TAG, "onMessage: open")
• val map: HashMap<String, Any> = HashMap()
• map["needShowWindow"] = true
• map["name"] = params["name"] as String
• map["entryPoint"] = params["entryPoint"] as String
• map["width"] = (params["width"] as Double).toInt()
• map["height"] = (params["height"] as Double).toInt()
• map["gravityX"] = params["gravityX"] as Int
• map["gravityY"] = params["gravityY"] as Int
• map["paddingX"] = params["paddingX"] as Double
• map["paddingY"] = params["paddingY"] as Double
• map["draggable"] = params["draggable"] as Boolean
• map["type"] = params["type"] as String
• if (params["params"] != null) {
• map["params"] = params["params"] as ArrayList<String>
• }
• result.success(manager?.showWindow(map, object : EngineCallback {
• override fun onEngineDestroy(id: String) {
• }
• }))
• }
• "close" -> {
• val windowId = params["windowId"] as String
• manager?.dismissWindow(windowId)
• }
• "executeTask" -> {
• Log.i(TAG, "onMessage: executeTask")
• val map: HashMap<String, Any> = HashMap()
• map["name"] = params["name"] as String
• map["entryPoint"] = params["entryPoint"] as String
• map["type"] = params["type"] as String
• result.success(manager?.executeTask(map))
• }
• "finishTask" -> {
• manager?.finishTask(params["taskId"] as String)
• }
• "setPosition" -> {
• val res = manager?.setPosition(
• params["windowId"] as String,
• params["x"] as Int,
• params["y"] as Int
• )
• result.success(res)
• }
• "setAlpha" -> {
• val res = manager?.setAlpha(
• params["windowId"] as String,
• (params["alpha"] as Double).toFloat(),
• )
• result.success(res)
• }
• "resize" -> {
• val res = manager?.resetWindowSize(
• params["windowId"] as String,
• params["width"] as Int,
• params["height"] as Int
• )
• result.success(res)
• }
• else -> {
• }
• }
• }
}
同时需要清楚,Engine通过传入的entryPoint
,就可以找到Flutter层中的方法入口点,在入口点中runApp即可。
实现过程中的坑
在实现过程中我们遇到的值得分享的坑,就是Flutter GestureDetector
和Window滑动事件的冲突。 由于悬浮窗是需要可滑动的,因此在原生层需要监听对应的事件;而Flutter的事件,是Android层分发给FlutterView的,两者形成冲突,导致Flutter内部滑动的时候,原生层也会捕获到,最终造成冲突。
如何解决?
从需求上来看,悬浮窗是否需要滑动,应该交给调用方决定,也就是由Flutter层来决定是否Android是否要对Flutter的滑动事件进行监听,即flutterView.setOnTouchListener
。这里我们使用一种更轻量级的操作,FlutterView的监听默认加上,然后在事件处理中,我们通过变量来做处理;而Flutter通过MethodChannel改变这个变量,加快了通信速度,避免了事件来回监听和销毁。
flutterView.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_MOVE -> {
if (dragging) {
setPosition(
initialX + (event.rawX - startX).roundToInt(),
initialY + (event.rawY - startY).roundToInt()
)
}
}
MotionEvent.ACTION_UP -> {
dragEnd()
}
MotionEvent.ACTION_DOWN -> {
startX = event.rawX
startY = event.rawY
initialX = layoutParams.x
initialY = layoutParams.y
dragStart()
windowManager.updateViewLayout(rootView, layoutParams)
}
}
false
}
dragging则是通过Flutter层去驱动的:FlutterMultiWindowsPlugin().dragStart();
private fun dragStart() {
dragging = true
}
private fun dragEnd() {
dragging = false
}
使用方式
目前我们内部已在4个应用落地了这个方案。应用方式有两种:一种是Flutter通过插件调用,也可以直接通过后台Service打开。效果尚佳,目的都是为了让Flutter的UI跨端使用。
另外,Flutter的方法入口点必须声明@pragma('vm:entry-point')
。
写在最后
目前来看这种方式可以完美支持Flutter在Android上开启多窗口,且能精准控制。但由于一个engine对应一个窗口,过多engine带来的内存隐患还是不可忽视的。我们希望Flutter官方能尽快的支持engine对应多个入口点,并且共享内存,只不过目前来看还是有点天方夜谭~~
这篇文章,需要有一定原生基础的同学才能看懂。只讲基础原理,代码不全,仅供参考! 另外多窗口的需求,不知道大家需求量如何,热度可以的话我再出个windows的多窗口实现!
作者:Karl_wei
来源:juejin.cn/post/7198824926722949179