如果你想用Flutter技术在桌面端落地,从技术上来讲,你必须解决这三大难题: 1. 应用窗口化,提供窗口操作的能力;
2. 实现多窗口;
3. 对外设的支持。
前言
首先给个结论,Flutter在桌面端落地,完全是可行的
;但生态远没有官方所说的那么完善,我甚至认为其达不到stable的标准
。
目前我们的桌面设备主要有Windows、Android系统
,系统不同但UI一致,我们将在这两个平台上解决以上问题,并落地Flutter。
一、窗口化和窗口操作存在的问题
- 实现应用窗口化:即应用是窗口化展示的,同时可拖拽、可以点击应用外的地方。
Flutter Windows本身是窗口化的;
而Android默认是全屏应用,需要让普通应用支持窗口化;若是小工具性质的应用,还需要支持可拖拽、可点击应用外的地方,这些在Flutter上都是需要我们在原生实现的。 - 实现应用窗口化后,一般开发过程中,肯定会需要以下对窗口的操作:
- 应用窗体圆形、阴影效果;
- 配置应用初始的显示位置;(很多小工具可能不是居中展示)
- 从窗口变为全屏、从全屏变为窗口;
- …
二、支持多窗口
目前Flutter是明确不支持多窗口的
。官方好像对多窗口不太感兴趣,一直没有把优先级提上来,还是停留在p4级别,具体见issue。
但是作为桌面应用,多窗口的需求是非常普遍的,因此这个技术壁垒是必须打破的。
三、窗口化实现方案
1. Windows端
Windows端Flutter默认支持窗口化,交互方式基本符合习惯,因此无需再做开发。
2. Android端
- Android普通应用实现窗口化,是把整个应用展示成窗口的效果,但是点击外部窗口外的地方其实是不响应。
同一时间只能显示一个应用进程,这是安卓的机制,也保证了其安全性。
要实现窗口化,需要把应用Theme设置成Dialog的样式;同时设置窗口全屏,但是背景色为透明,设置点击外部Dialog不消失,即可实现应用的窗口化展示。
- 设置主题
<style name="Theme.DialogApp" parent="Theme.AppCompat.Light.Dialog">
<item name="android:windowBackground">@drawable/launch_application</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowContentOverlay">@null</item>
<!-- 不显示遮罩层 -->
<item name="android:backgroundDimEnabled">false</item>
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<activity
android:name=".MainActivity"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:theme="@style/Theme.DialogApp"
android:windowSoftInputMode="adjustResize"> <meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/Theme.DialogApp" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
- 设置窗口全屏,但是背景色为透明,点击外部Dialog不消失
class MainActivity : FlutterActivity() {
// 设置窗口背景透明
override fun getTransparencyMode(): TransparencyMode {
return TransparencyMode.transparent
}
override fun onResume() {
super.onResume()
// 点击外部,dialog不消失
setFinishOnTouchOutside(false)
// 设置窗口全屏
var lp = window.attributes
lp.width = -1
lp.height = -1
window.attributes = lp
}
}
- 到这里
原生提供给Flutte一个全屏的透明窗体
,那么Flutter的视图想长成啥样都可以
。
- 若是小工具之类的,需要实现应用可拖拽,可点击应用区域外,这在android的实现相对复杂。我们利用
原生的窗口管理,弹出一个悬浮框,然后通过entry-point 找到Flutter层的UI
。这其实就是我们实现多窗口的思路,这里就不单纯讲解,跟着后面一起讲了。
窗口化操作
实现窗口化后,需要做很多相关的操作,我们分两个系统讲。
1. Windows端
-
应用窗体圆形、阴影效果
:通过window_manager插件,让应用背景色透明;然后我们在MaterialApp外面套一层Container可以设置圆角和阴影,再在外面加一次Container,加入padding以展示内层容器的阴影; -
小工具配置初始位置
:通过window_manager插件的setPosition可以设置位置; -
从窗口变为全屏、从全屏变为窗口
:通过window_manager插件可以实现全屏和退出全屏,在切换的过程中页面会闪烁,解决思路是:把透明度设置为0 → 全屏 → 透明度恢复为1
。设置透明度的方法也由window_manager插件提供。
2. Android端
对于普通应用,我们上面实现窗口化后,原生就已经为Flutter提供了一个透明的全屏窗口,因此任何窗体的操作都是Flutter层去实现的,没啥技术难度。
-
应用窗体圆形、阴影效果
:上面我们实现应用窗口后,其实整个应用窗体的背景色就是透明的了,因此我们比Windows少做了背景色透明这一步,然后后面的Container都是通用的,代码达到多平台复用; -
小工具配置初始位置
:直接通过Stack和Positioned来配置就行了。但这种场景一般使用悬浮弹框做,设置定位见后面多窗口; -
从窗口变为全屏、从全屏变为窗口
:Android依然很简单,只需要在全屏的时候把整个Flutter窗口的padding去除,恢复的时候加上就可以了。
多窗口的实现
首先明确一个观点,Flutter应用是基于Flutter engine,由原生提供的一个Surface画布,在这个画布上面用Skia 2绘制Flutter Widget。
也就是说本身这个应用就是一个窗口,它绝对没有能力为自己再创建一个窗口。 所以多窗口的实现,需要依赖于原生的窗口管理。下面是Android端的实现原理图,这个原理适用于任何平台。 [图片上传失败…(image-618d7f-1666938691902)]
原生新建一个Flutter engine,通过dart执行器DartExecutor执行方法executeDartEntrypoint,根据传入的字符串找到对应的方法入口点Entrypoint,从而拿到Flutter widget;
Flutter在方法上声明@pragma('vm:entry-point') 后,此方法即便在Flutter项目没有被调用到,也能编译进去,因此原生新的engine就能找到这个切入点,拿到方法返回的widget;
这是非常典型的Flutter玩法,诸如混合开发都是如此。带来的影响是存在多引擎(engine),增加一些内存,但是这个不可避免,除非你定制Flutter引擎. 目前pub上支持多窗口的库也都是这个原理,但是库的质量其实不高,大家还是自己写吧。
实现步骤
- Plugin与原生通信,由于操作都是异步的,所以务必使用
双向通信机制BasicMessageChannel
,而且需要两个通道:主应用与子窗口通道
。 - 定义接口协议,一般
至少需提供以下能力
:
// 主应用打开子窗口
void open(String entryPoint, Size size, GravityConfig? gravityConfig,
bool draggable);
// 主应用关闭子窗口
void close();
// 主应用设置大小
void resize(int width, int height);
// 主应用设置位置
void setPosition(int x, int y);
// 子窗口启动app,需要支持后台唤起以及命令行启动
void launchApp();
// 子窗口自行关闭
void closeByWindows();
// 子窗口设置大小
void resizeByWindows(int width, int height);
// 子窗口设置位置
void setPositionByWindows(int x, int y);
- 各端实现,下面贴下Android端的关键代码
- 新建Flutter engine,找到Dart中的方法,此时engine就拿到了Flutter的widget实例;
engine = FlutterEngine(application)
val entry = intent.getStringExtra("entryPoint") ?: "multiWindow"
val entryPoint = DartExecutor.DartEntrypoint(findAppBundlePath(), entry)
engine.dartExecutor.executeDartEntrypoint(entryPoint)
- 新建窗口管理类,通过FlutterViewe吸附engin,然后渲染到悬浮框的view上
///......
private var windowManager = service.getSystemService(Service.WINDOW_SERVICE) as WindowManager
///......
windowManager.addView(rootView, layoutParams)
///......///......
flutterView = FlutterView(inflater.context, FlutterSurfaceView(inflater.context, true))
flutterView.attachToFlutterEngine(engine)
///......
engine.lifecycleChannel.appIsResumed()
///......
rootView.findViewById<LinearLayout>(R.id.floating_window)
.addView(
flutterView,
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
)
- 实现悬浮框后,Android平台上的桌面小工具,也就顺利成章的实现了,只是在小工具这个项目的MainAcitivy上,就不需要去加载FlutterActivity了,直接启动悬浮框即可。
外设支持
usb设备在Flutter上,支持也是非常若的。具体可见我上一篇文章:Flutter桌面端实践之识别外接媒体设备
写在最后
以上是我在桌面端预研Flutter的一些经验和思路分享,如果你想在桌面端落地Flutter,我想这边文章对你是很有帮助的。
以上问题,我们遇到了,也解决了。但转念一想这么多基础的操作Flutter都不支持,这真的可以称得上Stable版本了吗?
Flutter桌面端的生态,急需我们共同建设,文中多次提起的window_manager插件就是国内出色的组织:LeanFlutter 提供的,期待Flutter桌面端越来越好!
作者:Karl_wei