欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Flutter?Android多窗口方案落地實(shí)戰(zhàn)

 更新時間:2023年02月12日 09:26:17   作者:Karl_wei  
這篇文章主要為大家介紹了Flutter?Android多窗口方案落地實(shí)戰(zhàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

前言

通過此篇文章,你將了解到:

Flutter如何在Android上實(shí)現(xiàn)多窗口機(jī)制;

Flutter與Android的事件機(jī)制和沖突解決;

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

Flutter在桌面端的多窗口需求,一直是個歷史巨坑。隨著Flutter的技術(shù)在我們windows、android桌面設(shè)備落地,我們發(fā)現(xiàn)多窗口需求必不可少,突破這個技術(shù)壁壘已經(jīng)刻不容緩。

實(shí)現(xiàn)原理

1. 基本原理

對于Android移動設(shè)備來說,多窗口的應(yīng)用大多是用于直播/音視頻的懸浮彈窗,讓用戶離開應(yīng)用后還能在小窗口中觀看內(nèi)容。實(shí)現(xiàn)原理是通過WindowManager創(chuàng)建和管理窗口,包括視圖內(nèi)容、拖拽、事件等操作。
我們都清楚Flutter只是一個可以做業(yè)務(wù)邏輯的UI框架,在Flutter中想要實(shí)現(xiàn)多窗口,也必須依賴Android的窗口管理機(jī)制?;谠腤indow,顯示Flutter繪制的UI,從而實(shí)現(xiàn)跨平臺的視圖交互和業(yè)務(wù)邏輯。

2. 具體步驟

  • Android端基于Window Manager創(chuàng)建Window,管理窗口的生命周期和拖拽邏輯;
  • 使用FlutterEngineGroup來管理Flutter Engine,通過引擎吸附Flutter的UI,加入到原生的FlutterView;
  • 把FlutterView通過addView的方式加入到Window上。

3. 原理圖

插件實(shí)現(xiàn)

基于上述原理,可以在Android的窗口顯示Flutter的UI。但要真正提供給Flutter層使用,還需要再封裝一個插件層。

  • 通過單例管理多個窗口 由于是多窗口,可能項(xiàng)目中多個地方都會調(diào)用到,因此需要使用單例來統(tǒng)一管理所有窗口的生命周期,保證準(zhǔn)確創(chuàng)建、及時銷毀。
//引擎生命鉤子回調(diào),讓調(diào)用方感知引擎狀態(tài)
interface EngineCallback {
    fun onCreate(id:String)
    fun onEngineDestroy(id: String)
}
class EngineManager private constructor(context: Context) {
    // 單例對象
    companion object :
        SingletonHolder<EngineManager, Context>(::EngineManager)
    // 窗口類型;如果是單一類型,那么同名窗口將返回上一次的未銷毀的實(shí)例。
    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)
    // 每個窗口對應(yīng)一個引擎,基于引擎ID和名稱存儲多窗口的信息,以及查找
    private val engineMap = ConcurrentHashMap<String, Entry>() //搜索引擎,用作消息分發(fā)
    private val name2IdMap = ConcurrentHashMap<String, String>() //判斷是否存在了任務(wù)
    private val id2NameMap = ConcurrentHashMap<String, String>() //根據(jù)任務(wù)獲取name并清除
    private val engineCallback =
        ConcurrentHashMap<String, EngineCallback>() //通知調(diào)用方引擎狀態(tài) 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)
        }
        // 把調(diào)用方傳遞的參數(shù)回調(diào)給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]
        // 把創(chuàng)建好的引擎?zhèn)鹘oAndroidWindow,由其去創(chuàng)建窗口
        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 {
        // ......
    }
}

通過代碼我們可以看到,每個窗口都對應(yīng)一個engine,通過name和生成的UUID做唯一標(biāo)識,然后把engine傳給AndroidWindow,在那里加入WindowManger,以及Flutter UI的獲取。

  • AndroidWindow的實(shí)現(xiàn);通過context.getSystemService(Service.WINDOW_SERVICE) as WindowManager獲取窗口管理器;同時創(chuàng)建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, // 系統(tǒng)應(yīng)用才可使用此類型
        WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
        PixelFormat.TRANSLUCENT
    )
    fun open() {
        @Suppress("Deprecation")
        windowManager.defaultDisplay.getMetrics(metrics)
        layoutParams.gravity = Gravity.START or Gravity.TOP
        selectMeasurementMode()
        // 設(shè)置位置
        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)
    }
    // .....
  • 插件層封裝。插件層就很簡單了,創(chuàng)建好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
        // 必須單例調(diào)用
        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層中的方法入口點(diǎn),在入口點(diǎn)中runApp即可。

實(shí)現(xiàn)過程中的坑

在實(shí)現(xiàn)過程中我們遇到的值得分享的坑,就是Flutter GestureDetector和Window滑動事件的沖突。 由于懸浮窗是需要可滑動的,因此在原生層需要監(jiān)聽對應(yīng)的事件;而Flutter的事件,是Android層分發(fā)給FlutterView的,兩者形成沖突,導(dǎo)致Flutter內(nèi)部滑動的時候,原生層也會捕獲到,最終造成沖突。

如何解決?從需求上來看,懸浮窗是否需要滑動,應(yīng)該交給調(diào)用方?jīng)Q定,也就是由Flutter層來決定是否Android是否要對Flutter的滑動事件進(jìn)行監(jiān)聽,即flutterView.setOnTouchListener。這里我們使用一種更輕量級的操作,F(xiàn)lutterView的監(jiān)聽默認(rèn)加上,然后在事件處理中,我們通過變量來做處理;而Flutter通過MethodChannel改變這個變量,加快了通信速度,避免了事件來回監(jiān)聽和銷毀。

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層去驅(qū)動的:FlutterMultiWindowsPlugin().dragStart();

private fun dragStart() {
    dragging = true
}
private fun dragEnd() {
    dragging = false
}

使用方式

目前我們內(nèi)部已在4個應(yīng)用落地了這個方案。應(yīng)用方式有兩種:一種是Flutter通過插件調(diào)用,也可以直接通過后臺Service打開。效果尚佳,目的都是為了讓Flutter的UI跨端使用。

另外,F(xiàn)lutter的方法入口點(diǎn)必須聲明@pragma('vm:entry-point')。

寫在最后

目前來看這種方式可以完美支持Flutter在Android上開啟多窗口,且能精準(zhǔn)控制。但由于一個engine對應(yīng)一個窗口,過多engine帶來的內(nèi)存隱患還是不可忽視的。我們希望Flutter官方能盡快的支持engine對應(yīng)多個入口點(diǎn),并且共享內(nèi)存,只不過目前來看還是有點(diǎn)天方夜譚~~

這篇文章,需要有一定原生基礎(chǔ)的同學(xué)才能看懂。只講基礎(chǔ)原理,代碼不全,僅供參考! 另外多窗口的需求,不知道大家需求量如何,熱度可以的話我再出個windows的多窗口實(shí)現(xiàn)!

更多關(guān)于Flutter Android多窗口的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論