注册

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(rootViewlayoutParams)
      }
  }
   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 个评论

要回复文章请先登录注册