注册

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

前言

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. 原理图

e76939f456234236874b2b7b5d67d248~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp?

插件实现

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

  1. 通过单例管理多个窗口 由于是多窗口,可能项目中多个地方都会调用到,因此需要使用单例来统一管理所有窗口的生命周期,保证准确创建、及时销毁。

//引擎生命钩子回调,让调用方感知引擎状态
interface EngineCallback {
   fun onCreate(id:String)
   fun onEngineDestroy(idString)
}

class EngineManager private constructor(contextContext) {

   // 单例对象
   companion object :
       SingletonHolder<EngineManagerContext>(::EngineManager)

   // 窗口类型;如果是单一类型,那么同名窗口将返回上一次的未销毁的实例。
   private val TYPE_SINGLEString = "single"

   init {
       Log.d("EngineManager""EngineManager init")
  }

   data class Entry(
       val engineFlutterEngine,
       val windowAndroidWindow?
  )

   private var myContextContext = context

   private var engineGroupFlutterEngineGroup = FlutterEngineGroup(myContext)

   // 每个窗口对应一个引擎,基于引擎ID和名称存储多窗口的信息,以及查找
   private val engineMap = ConcurrentHashMap<StringEntry>() //搜索引擎,用作消息分发
   private val name2IdMap = ConcurrentHashMap<StringString>() //判断是否存在了任务
   private val id2NameMap = ConcurrentHashMap<StringString>() //根据任务获取name并清除
   private val engineCallback =
       ConcurrentHashMap<StringEngineCallback>() //通知调用方引擎状态 0-create 1-attach 2-destroy

   fun showWindow(
       paramsHashMap<StringAny>,
       engineStatusCallbackEngineCallback
  ): String? {
       val entryString?
       if (params.containsKey("entryPoint")) {
           entry = params["entryPoint"as String
      } else {
           return null
      }

       val nameString?
       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 userList<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(myContextdraggablewidthheightconfigengine)
       engineMap[windowUid= Entry(engineandroidWindow)
       androidWindow.open()
       engine.platformViewsController.attach(
           myContext,
           engine.renderer,
           engine.dartExecutor
      )
       return windowUid
  }

   fun setPosition(idString?xIntyInt): Boolean {
       id ?return false
       val entry = engineMap[id]
       entry ?return false
       entry.window?.setPosition(xy)
       return true
  }
   
   fun setSize(idString?widthdoubleheightdouble): Boolean {
       // ......
  }
}

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

  1. AndroidWindow的实现;通过context.getSystemService(Service.WINDOW_SERVICE) as WindowManager获取窗口管理器;同时创建FlutterView和LayoutInfalter,通过engine拿到视图吸附到FlutterView,把FlutterView加到Layout中,最后把Layout通过addView加到WindowManager中显示。

class AndroidWindow(
   private val contextContext,
   private val draggableBoolean,
   private val widthInt,
   private val heightInt,
   private val configGravityConfig,
   private val engineFlutterEngine
) {
   private var startX = 0f
   private var startY = 0f
   private var initialX = 0
   private var initialY = 0
   private var dragging = false
   private lateinit var flutterViewFlutterView
   private var windowManager = context.getSystemService(Service.WINDOW_SERVICEas WindowManager
   private val inflater =
       context.getSystemService(Service.LAYOUT_INFLATER_SERVICEas LayoutInflater
   private val metrics = DisplayMetrics()

   @SuppressLint("InflateParams")
   private var rootView = inflater.inflate(R.layout.floatingnullfalseas ViewGroup
   private val layoutParams = WindowManager.LayoutParams(
       dip2px(contextwidth.toFloat()),
       dip2px(contextheight.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(rootViewlayoutParams)
       flutterView = FlutterView(inflater.contextFlutterSurfaceView(inflater.contexttrue))
       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(rootViewlayoutParams)
                  }
              }
               false
          }
      }
       @Suppress("ClickableViewAccessibility")
       rootView.setOnTouchListener { _event ->
           when (event.action) {
               MotionEvent.ACTION_DOWN -> {
                   layoutParams.flags =
                       layoutParams.flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                   windowManager.updateViewLayout(rootViewlayoutParams)
                   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(rootViewlayoutParams)
  }
   // .....

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

作者:Karl_wei
来源:juejin.cn/post/7198824926722949179

0 个评论

要回复文章请先登录注册