注册

你真的敢落地Flutter桌面端吗?

如果你想用Flutter技术在桌面端落地,从技术上来讲,你必须解决这三大难题:
1. 应用窗口化,提供窗口操作的能力;
2. 实现多窗口;
3. 对外设的支持。

前言

首先给个结论,Flutter在桌面端落地,完全是可行的;但生态远没有官方所说的那么完善,我甚至认为其达不到stable的标准
目前我们的桌面设备主要有Windows、Android系统,系统不同但UI一致,我们将在这两个平台上解决以上问题,并落地Flutter。

一、窗口化和窗口操作存在的问题

  1. 实现应用窗口化:即应用是窗口化展示的,同时可拖拽、可以点击应用外的地方
    Flutter Windows本身是窗口化的;
    而Android默认是全屏应用,需要让普通应用支持窗口化;若是小工具性质的应用,还需要支持可拖拽、可点击应用外的地方,这些在Flutter上都是需要我们在原生实现的。
  2. 实现应用窗口化后,一般开发过程中,肯定会需要以下对窗口的操作:
    • 应用窗体圆形、阴影效果;
    • 配置应用初始的显示位置;(很多小工具可能不是居中展示)
    • 从窗口变为全屏、从全屏变为窗口;
    • ......

二、支持多窗口

目前Flutter是明确不支持多窗口的。官方好像对多窗口不太感兴趣,一直没有把优先级提上来,还是停留在p4级别,具体见issue
但是作为桌面应用,多窗口的需求是非常普遍的,因此这个技术壁垒是必须打破的。

三、窗口化实现方案

1. Windows端

Windows端Flutter默认支持窗口化,交互方式基本符合习惯,因此无需再做开发。

2. Android端

  • Android普通应用实现窗口化,是把整个应用展示成窗口的效果,但是点击外部窗口外的地方其实是不响应。 同一时间只能显示一个应用进程,这是安卓的机制,也保证了其安全性。要实现窗口化,需要把应用Theme设置成Dialog的样式;同时设置窗口全屏,但是背景色为透明,设置点击外部Dialog不消失,即可实现应用的窗口化展示。

    1. 设置主题

      <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>
    2. 设置窗口全屏,但是背景色为透明,点击外部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
      }
      }
    3. 到这里原生提供给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端的实现原理图,这个原理适用于任何平台。 

  • 原生新建一个Flutter engine,通过dart执行器DartExecutor执行方法executeDartEntrypoint,根据传入的字符串找到对应的方法入口点Entrypoint,从而拿到Flutter widget;
  • Flutter在方法上声明@pragma('vm:entry-point') 后,此方法即便在Flutter项目没有被调用到,也能编译进去,因此原生新的engine就能找到这个切入点,拿到方法返回的widget;

这是非常典型的Flutter玩法,诸如混合开发都是如此。带来的影响是存在多引擎(engine),增加一些内存,但是这个不可避免,除非你定制Flutter引擎. 目前pub上支持多窗口的库也都是这个原理,但是库的质量其实不高,大家还是自己写吧。

实现步骤

  1. Plugin与原生通信,由于操作都是异步的,所以务必使用双向通信机制BasicMessageChannel,而且需要两个通道:主应用与子窗口通道
  2. 定义接口协议,一般至少需提供以下能力
// 主应用打开子窗口
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);
  1. 各端实现,下面贴下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
    )
    )
  1. 实现悬浮框后,Android平台上的桌面小工具,也就顺利成章的实现了,只是在小工具这个项目的MainAcitivy上,就不需要去加载FlutterActivity了,直接启动悬浮框即可。

外设支持

usb设备在Flutter上,支持也是非常若的。具体可见我上一篇文章:Flutter桌面端实践之识别外接媒体设备

写在最后

以上是我在桌面端预研Flutter的一些经验和思路分享,如果你想在桌面端落地Flutter,我想这边文章对你是很有帮助的。
以上问题,我们遇到了,也解决了。但转念一想这么多基础的操作Flutter都不支持,这真的可以称得上Stable版本了吗?
Flutter桌面端的生态,急需我们共同建设,文中多次提起的window_manager插件就是国内出色的组织:LeanFlutter 提供的,期待Flutter桌面端越来越好!


作者:Karl_wei
链接:https://juejin.cn/post/7120944715419090957
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册