当前位置 : 主页 > 手机开发 > android >

Flutter Android多窗口方案落地实战

来源:互联网 收集:自由互联 发布时间:2023-03-22
目录 前言 实现原理 1. 基本原理 2. 具体步骤 3. 原理图 插件实现 实现过程中的坑 使用方式 写在最后 前言 通过此篇文章,你将了解到: Flutter如何在Android上实现多窗口机制; Flutter与
目录
  • 前言
  • 实现原理
    • 1. 基本原理
    • 2. 具体步骤
    • 3. 原理图
  • 插件实现
    • 实现过程中的坑
      • 使用方式
        • 写在最后

          前言

          通过此篇文章,你将了解到:

          Flutter如何在Android上实现多窗口机制;

          Flutter与Android的事件机制和冲突解决;

          Flutter多窗口存在的隐患和展望。

          Flutter在桌面端的多窗口需求,一直是个历史巨坑。随着Flutter的技术在我们windows、android桌面设备落地,我们发现多窗口需求必不可少,突破这个技术壁垒已经刻不容缓。

          实现原理

          1. 基本原理

          对于Android移动设备来说,多窗口的应用大多是用于直播/音视频的悬浮弹窗,让用户离开应用后还能在小窗口中观看内容。实现原理是通过WindowManager创建和管理窗口,包括视图内容、拖拽、事件等操作。
          我们都清楚Flutter只是一个可以做业务逻辑的UI框架,在Flutter中想要实现多窗口,也必须依赖Android的窗口管理机制。基于原生的Window,显示Flutter绘制的UI,从而实现跨平台的视图交互和业务逻辑。

          2. 具体步骤

          • Android端基于Window Manager创建Window,管理窗口的生命周期和拖拽逻辑;
          • 使用FlutterEngineGroup来管理Flutter Engine,通过引擎吸附Flutter的UI,加入到原生的FlutterView;
          • 把FlutterView通过addView的方式加入到Window上。

          3. 原理图

          插件实现

          基于上述原理,可以在Android的窗口显示Flutter的UI。但要真正提供给Flutter层使用,还需要再封装一个插件层。

          • 通过单例管理多个窗口 由于是多窗口,可能项目中多个地方都会调用到,因此需要使用单例来统一管理所有窗口的生命周期,保证准确创建、及时销毁。
          //引擎生命钩子回调,让调用方感知引擎状态
          interface EngineCallback {
              fun onCreate(id:String)
              fun onEngineDestroy(id: String)
          }
          class EngineManager private constructor(context: Context) {
              // 单例对象
              companion object :
                  SingletonHolder<EngineManager, Context>(::EngineManager)
              // 窗口类型;如果是单一类型,那么同名窗口将返回上一次的未销毁的实例。
              private val TYPE_SINGLE: String = "single"
              init {
                  Log.d("EngineManager", "EngineManager init")
              }
              data class Entry(
                  val engine: FlutterEngine,
                  val window: AndroidWindow?
              )
              private var myContext: Context = context
              private var engineGroup: FlutterEngineGroup = FlutterEngineGroup(myContext)
              // 每个窗口对应一个引擎,基于引擎ID和名称存储多窗口的信息,以及查找
              private val engineMap = ConcurrentHashMap<String, Entry>() //搜索引擎,用作消息分发
              private val name2IdMap = ConcurrentHashMap<String, String>() //判断是否存在了任务
              private val id2NameMap = ConcurrentHashMap<String, String>() //根据任务获取name并清除
              private val engineCallback =
                  ConcurrentHashMap<String, EngineCallback>() //通知调用方引擎状态 0-create 1-attach 2-destroy
              fun showWindow(
                  params: HashMap<String, Any>,
                  engineStatusCallback: EngineCallback
              ): String? {
                  val entry: String?
                  if (params.containsKey("entryPoint")) {
                      entry = params["entryPoint"] as String
                  } else {
                      return null
                  }
                  val name: String?
                  if (params.containsKey("name")) {
                      name = params["name"] as String
                  } else {
                      return null
                  }
                  val type = params["type"]
                  if (type == TYPE_SINGLE && name2IdMap[name] != null) {
                      return name2IdMap[name]
                  }
                  val windowUid = UUID.randomUUID().toString()
                  if (type == TYPE_SINGLE) {
                      name2IdMap[name] = windowUid
                      id2NameMap[windowUid] = name
                      engineCallback[windowUid] = engineStatusCallback
                  }
                  val dartEntrypoint = DartExecutor.DartEntrypoint(findAppBundlePath(), entry)
                  val args = mutableListOf(windowUid)
                  var user: List<String>? = null
                  if (params.containsKey("params")) {
                      user = params["params"] as List<String>
                  }
                  if (user != null) {
                      args.addAll(user)
                  }
                  // 把调用方传递的参数回调给Flutter
                  val option =
                      FlutterEngineGroup.Options(myContext).setDartEntrypoint(dartEntrypoint)
                          .setDartEntrypointArgs(
                              args
                          )
                  val engine = engineGroup.createAndRunEngine(option)
                  val draggable = params["draggable"] as Boolean? ?: true
                  val width = params["width"] as Int? ?: 0
                  val height = params["height"] as Int? ?: 0
                  val config = GravityConfig()
                  config.paddingX = params["paddingX"] as Double? ?: 0.0
                  config.paddingY = params["paddingY"] as Double? ?: 0.0
                  config.gravityX = GravityForX.values()[params["gravityX"] as Int? ?: 1]
                  config.gravityY = GravityForY.values()[params["gravityY"] as Int? ?: 1]
                  // 把创建好的引擎传给AndroidWindow,由其去创建窗口
                  val androidWindow =
                      AndroidWindow(myContext, draggable, width, height, config, engine)
                  engineMap[windowUid] = Entry(engine, androidWindow)
                  androidWindow.open()
                  engine.platformViewsController.attach(
                      myContext,
                      engine.renderer,
                      engine.dartExecutor
                  )
                  return windowUid
              }
              fun setPosition(id: String?, x: Int, y: Int): Boolean {
                  id ?: return false
                  val entry = engineMap[id]
                  entry ?: return false
                  entry.window?.setPosition(x, y)
                  return true
              }
              fun setSize(id: String?, width: double, height: double): Boolean {
                  // ......
              }
          }
          

          通过代码我们可以看到,每个窗口都对应一个engine,通过name和生成的UUID做唯一标识,然后把engine传给AndroidWindow,在那里加入WindowManger,以及Flutter UI的获取。

          • AndroidWindow的实现;通过context.getSystemService(Service.WINDOW_SERVICE) as WindowManager获取窗口管理器;同时创建FlutterView和LayoutInfalter,通过engine拿到视图吸附到FlutterView,把FlutterView加到Layout中,最后把Layout通过addView加到WindowManager中显示。
          class AndroidWindow(
              private val context: Context,
              private val draggable: Boolean,
              private val width: Int,
              private val height: Int,
              private val config: GravityConfig,
              private val engine: FlutterEngine
          ) {
              private var startX = 0f
              private var startY = 0f
              private var initialX = 0
              private var initialY = 0
              private var dragging = false
              private lateinit var flutterView: FlutterView
              private var windowManager = context.getSystemService(Service.WINDOW_SERVICE) as WindowManager
              private val inflater =
                  context.getSystemService(Service.LAYOUT_INFLATER_SERVICE) as LayoutInflater
              private val metrics = DisplayMetrics()
              @SuppressLint("InflateParams")
              private var rootView = inflater.inflate(R.layout.floating, null, false) as ViewGroup
              private val layoutParams = WindowManager.LayoutParams(
                  dip2px(context, width.toFloat()),
                  dip2px(context, height.toFloat()),
                  WindowManager.LayoutParams.TYPE_SYSTEM_ALERT, // 系统应用才可使用此类型
                  WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                  PixelFormat.TRANSLUCENT
              )
              fun open() {
                  @Suppress("Deprecation")
                  windowManager.defaultDisplay.getMetrics(metrics)
                  layoutParams.gravity = Gravity.START or Gravity.TOP
                  selectMeasurementMode()
                  // 设置位置
                  val screenWidth = metrics.widthPixels
                  val screenHeight = metrics.heightPixels
                  when (config.gravityX) {
                      GravityForX.Left -> layoutParams.x = config.paddingX!!.toInt()
                      GravityForX.Center -> layoutParams.x =
                          ((screenWidth - layoutParams.width) / 2 + config.paddingX!!).toInt()
                      GravityForX.Right -> layoutParams.x =
                          (screenWidth - layoutParams.width - config.paddingX!!).toInt()
                      null -> {}
                  }
                  when (config.gravityY) {
                      GravityForY.Top -> layoutParams.y = config.paddingY!!.toInt()
                      GravityForY.Center -> layoutParams.y =
                          ((screenHeight - layoutParams.height) / 2 + config.paddingY!!).toInt()
                      GravityForY.Bottom -> layoutParams.y =
                          (screenHeight - layoutParams.height - config.paddingY!!).toInt()
                      null -> {}
                  }
                  windowManager.addView(rootView, layoutParams)
                  flutterView = FlutterView(inflater.context, FlutterSurfaceView(inflater.context, true))
                  flutterView.attachToFlutterEngine(engine)
                  if (draggable) {
                      @Suppress("ClickableViewAccessibility")
                      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
                      }
                  }
                  @Suppress("ClickableViewAccessibility")
                  rootView.setOnTouchListener { _, event ->
                      when (event.action) {
                          MotionEvent.ACTION_DOWN -> {
                              layoutParams.flags =
                                  layoutParams.flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                              windowManager.updateViewLayout(rootView, layoutParams)
                              true
                          }
                          else -> false
                      }
                  }
                  engine.lifecycleChannel.appIsResumed()
                  rootView.findViewById<FrameLayout>(R.id.floating_window)
                      .addView(
                          flutterView,
                          ViewGroup.LayoutParams(
                              ViewGroup.LayoutParams.MATCH_PARENT,
                              ViewGroup.LayoutParams.MATCH_PARENT
                          )
                      )
                  windowManager.updateViewLayout(rootView, layoutParams)
              }
              // .....
          
          • 插件层封装。插件层就很简单了,创建好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的多窗口实现!

          更多关于Flutter Android多窗口的资料请关注自由互联其它相关文章!

          网友评论