注册

Flutter Android多窗口方案落地(下)

接:Flutter Android多窗口方案落地(上)

  1. 插件层封装。插件层就很简单了,创建好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


0 个评论

要回复文章请先登录或注册