经过此篇文章,你将了解到:
- 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的多窗口完成!
这是我参加「日新计划 2 月更文应战」的第 1 天,点击查看活动概况”