环境搭建指引:https://atomgit.com/CPF-Flutter/flutter_samples/blob/master/docs/ohos/getting-started/flutter-oh-env-setup.md

fullscreen_window 是一个只有两个方法的 Flutter 插件:一行调用让窗口进入 / 退出全屏,外加读取屏幕尺寸。它的 1.2.1 版支持 Windows、Linux、Web、Android、iOS,没有 OpenHarmony。

有意思的地方在于:这个库的 Android / iOS 实现一行原生代码都没有——它用 dartPluginClass 直接调 Flutter 自带的 SystemChrome,所以上游仓库里根本不存在 android/ 和 ios/ 目录。到了鸿蒙,这条路接不上:dartPluginClass 只在声明过的平台生效,宿主调用最终落到插件自己那条 fullscreen_window 方法通道上,而通道另一头没有人接。

本文记录把这 1.2.1 版适配到 OpenHarmony 的完整过程:选库前的三道筛、六步适配流程、窗口与系统栏的接口语义、几处只有跑起来才会暴露的坑,以及四条实测数据。

适配对象:上游 fullscreen_window 1.2.1(Apache-2.0);适配产物 TAG 1.2.1-ohos-1.0.0-beta.1。


一、为什么选这个库

1.1 库本身:API 极简,但语义不简单

上游 Dart 对外只有两个入口:

FullScreenWindow.setFullScreen(true);                                  // 进入全屏
FullScreenWindow.setFullScreen(false);                                 // 退出全屏
Size logical  = await FullScreenWindow.getScreenSize(context);         // 逻辑像素(vp)
Size physical = await FullScreenWindow.getScreenSize(null);            // 物理像素(px)

方法少,但跨平台差异全压在原生侧:Android 上"全屏"是一次 SystemChrome.setEnabledSystemUIMode();鸿蒙上它是两件事(布局全屏 + 系统栏显隐),而这两件事的先后顺序、退出时该还原到什么状态,都没有标准答案,必须自己定。

1.2 选库前的三道筛

按"选库三筛"的顺序走一遍,任何一步不过就直接换库:

第一筛:pub.dev 平台列表里有没有 ohos。

$ node -e "...fetch https://pub.dev/api/packages/fullscreen_window..."
[pub.dev] 版本 1.2.1  平台: windows, linux, web, android, ios

没有 ohos,通过。(这一步是一票否决里最快的一条:像 screen_brightness、flutter_volume_controller 这类库,pub.dev 平台列表里已经带 ohos,说明上游自己支持了,不必重复适配。)

第二筛:上游仓库根目录有没有 *_ohos / *_harmony 兄弟包。

git clone https://gh-proxy.com/https://github.com/jakky1/fullscreen_window.git
Get-ChildItem fullscreen_window -Force | Select-Object Name
# .git  .vscode  example  lib  linux  test  windows
# .gitignore  .metadata  analysis_options.yaml  CHANGELOG.md  LICENSE  pubspec.yaml  README.md

没有兄弟包,通过。顺带注意两件事:仓库里没有 android/、ios/ 目录(第一处线索),以及 pub.dev 上也不存在 fullscreen_window_ohos:

$ node -e "...fetch https://pub.dev/api/packages/fullscreen_window_ohos..."
fullscreen_window_ohos -> 404

兄弟包这一步不能只看仓库根目录。此前的经验是:有的库把鸿蒙实现做成独立的 <库名>_ohos 包挂到 pub.dev(例如 plus 系列就有一批 <包名>_ohos),仓库里翻不到、清单里也查不到。所以"pub.dev 查 <库名>_ohos 是否存在"要和"翻仓库根目录"一起做。

第三筛:克隆后 grep Dart 入口有没有"平台门"。

Dart 层一个字都不能改,门一旦关上原生写得再好也没用:

Select-String -Path lib\*.dart -Pattern "Platform\.|TargetPlatform|isSupportedPlatform|MethodChannel"
# lib\fullscreen_window.dart:1:      export 'fullscreen_window_android.dart';
# lib\fullscreen_window_method_channel.dart:12:  final methodChannel = const MethodChannel('fullscreen_window');

lib/ 下只有 4 个文件,没有任何 Platform.isAndroid / TargetPlatform 判断,也没有 isSupportedPlatform 之类的门。通过。

1.3 在线查重:清单会滞后,必须实时核

查重工具(.agents/tools/check-dedup.mjs)依赖的清单数据滞后大约一周,本轮之前已经连续踩过四次。所以这里直接打 AtomGit 的 contents API 实时核验四个组织:

$ node ... (逐个请求 /api/v5/repos/{org}/{repo}/contents)
✗ oh-flutter/fullscreen_window     404
✗ oh-flutter/fluttertpc_fullscreen_window  404
✗ CPF-Flutter/fullscreen_window    404
✗ CPF-Flutter/fluttertpc_fullscreen_window 404
✗ hxa-flutter/fullscreen_window    404
✗ oh-tpc/fullscreen_window         404

四个组织都没有,确认是"没人做过"。

顺带把同主题的库也扫了一遍,避免撞题:

已存在的仓库结论
CPF-Flutter/fluttertpc_status_bar_control是另一个库 status_bar_control(Android 专属,改状态栏颜色 / 可见性)
CPF-Flutter/fluttertpc_flutter_statusbarcolor_ns同为状态栏着色类库
CPF-Flutter/fluttertpc_no_screenshot / fluttertpc_disable_screenshots防截屏,方向相反

fullscreen_window 本身无人适配,且它走的是窗口级接口(window.setWindowLayoutFullScreen / setWindowSystemBarEnable),与上述"改状态栏样式"的库不是一条路。


二、六步适配流程

第一步:把上游同步到 AtomGit

在 oh-flutter 组织下建一个与 pub.dev 包名同名的空仓库(不加 README,避免多一个初始提交)。可以用网页建,也可以直接打 API:

# AtomGit API:注意仓库必须建在组织下(/orgs/{org}/repos)。
# 早期踩过坑:POST /user/repos 会静默建到个人命名空间去。
$repo = "fullscreen_window"
$desc = "Flutter for OpenHarmony 版 fullscreen_window:一行调用让鸿蒙窗口进入/退出全屏(窗口布局全屏 + 系统栏显隐),支持读取屏幕物理尺寸。"
# 请求体必须是 UTF-8 字节,PowerShell 5.1 的 Invoke-RestMethod 会按 ASCII 编码把中文写成问号
$bytes = [System.Text.Encoding]::UTF8.GetBytes((@{ name = $repo; description = $desc } | ConvertTo-Json))

建完回读一遍 description:出现码点 63(?)就说明被 ASCII 编码毁掉了,要重新 PATCH。

第二步:本地克隆

cd E:\flutter3fangku\_probe
git clone https://gh-proxy.com/https://github.com/jakky1/fullscreen_window.git fsw_work
cd fsw_work
git log --oneline -1        # 0984a5e release v1.2.1

克隆大仓库用 https://gh-proxy.com/ 前缀(ghproxy.net 会中途断流)。

第三步:创建分支,并用框架命令补出鸿蒙化目录

git checkout -b feat/ohos_fullscreen_window_1.2.1
flutter create -t plugin --platforms ohos .

flutter create 是官方给的"自动补全"命令,它会:

  1. 生成 ohos/(HAR 工程:index.ets、oh-package.json5、src/main/module.json5、插件模板 src/main/ets/components/plugin/FullscreenWindowPlugin.ets);
  2. 生成 example/ohos/(应用工程:AppScope/、entry/、hvigorfile.ts、hvigorconfig.ts);
  3. 改写 .metadata,加入 platform: ohos。

两个必须注意的点:

  • 不加 -t plugin 会按 app 模板生成,出来的是 AppScope/ + entry/,而且 .metadata 会被写成 project_type: app,之后再补 -t plugin 直接报 The requested template type 'plugin' doesn't match the existing template type of 'app',只能先删 .metadata 重跑;
  • 它会顺手灌进模板垃圾。本次 flutter create 之后 git status 是这样的:
 M .metadata
 M example/pubspec.lock
?? android/            ← 上游本来没有(Android 是纯 Dart 实现),删
?? example/android/    ← 删
?? example/integration_test/   ← 删
?? example/ios/        ← 删
?? example/ohos/       ← 保留
?? ios/                ← 删
?? linux/test/         ← 删
?? ohos/               ← 保留
?? windows/test/       ← 删

只保留 ohos/ 与 example/ohos/,.metadata 里新增的 platform: ohos 是合法的,保留。

第四步:适配过程(新增了什么、为什么)

新增两个文件(外加一处 pubspec 声明):

文件作用
ohos/index.etsHAR 出口,export { default } from './src/main/ets/components/plugin/FullscreenWindowPlugin'
ohos/src/main/ets/components/plugin/FullscreenWindowPlugin.ets插件本体:接 fullscreen_window 通道的两个方法
pubspec.yaml新增 ohos: pluginClass: FullscreenWindowPlugin

pubspec.yaml 只加两行:

      ohos:
        pluginClass: FullscreenWindowPlugin

注意这里写的是 pluginClass(原生插件类)而不是 dartPluginClass。上游 Android / iOS 声明的是 dartPluginClass: FullScreenWindowAndroid,那是"用 Dart 类替换平台接口实例"的机制;鸿蒙要接的恰恰是平台接口的默认实现——MethodChannelFullscreenWindow(通道 fullscreen_window),所以只能用原生 pluginClass 去响应这条通道。这个区别就是整个适配的立足点。

第五步:补全额外文件

  • README.OpenHarmony.md / README.OpenHarmony_CN.md:安装方式、接口对照、关键点、已知限制、验证数据;
  • CHANGELOG.OpenHarmony.md:本次适配首个版本的变更;
  • 根 README.md:平台支持表补一列 OpenHarmony,并指向上面两份文档。

上游根 README 支持表补列后:

| Windows | Linux | Web | Android | iOS | OpenHarmony |

第六步:推送并打 TAG

git add -A
git commit -F commit-msg.txt
git remote add atomgit https://atomgit.com/oh-flutter/fullscreen_window.git
git push atomgit HEAD:main
git push atomgit feat/ohos_fullscreen_window_1.2.1
git tag -a 1.2.1-ohos-1.0.0-beta.1 -m "fullscreen_window OpenHarmony 适配 1.2.1-ohos-1.0.0-beta.1"
git push atomgit 1.2.1-ohos-1.0.0-beta.1

推送前记得把 example/ohos/build-profile.json5 里的 signingConfigs 清空——devecocli signature generate 会把证书路径与明文密码写进去,直接提交等于把签名材料入库:

{
  app: {
    signingConfigs: [],        // 提交前必须清空
    products: [
      { name: 'default', compatibleSdkVersion: '5.1.0(18)', runtimeOS: 'HarmonyOS' },
    ],
  },
}

推送完再看一眼远端树,确认根 README 是普通文件而不是符号链接(100644):

$ git fetch atomgit main; git ls-tree FETCH_HEAD
100644 blob bb2fb409...  README.md
100644 blob 0c6b698d...  README.OpenHarmony.md
100644 blob fbd18319...  README.OpenHarmony_CN.md
100644 blob 2c20adab...  CHANGELOG.OpenHarmony.md
040000 tree 12ac58f2...  ohos

这一步不是多余的:有的上游仓库把根 README.md 做成指向子包的符号链接(模式 120000),AtomGit 不渲染这类文件,仓库首页会是一片空白。而且 Windows 上 core.symlinks=false,直接覆盖再 git add 仍会记成 120000,必须 git rm --cached + git add,再用 git ls-files -s 确认是 100644。

推完的仓库首页:根目录同时有 ohos/、example/ 与三份 OpenHarmony 文档。

在这里插入图片描述


三、契约分析:这条通道到底要什么

Dart 侧的实现全在 lib/fullscreen_window_method_channel.dart:

class MethodChannelFullscreenWindow extends FullScreenWindowPlatform {
  final methodChannel = const MethodChannel('fullscreen_window');   // 通道名

  Future<void> setFullScreen(bool isFullScreen) async {
    await methodChannel.invokeMethod<void>('setFullScreen', { "isFullScreen": isFullScreen });
  }

  Future<Size> getScreenSize(BuildContext? context) async {
    double devicePixelRatio = 1.0;
    if (context != null) { /* 从 MediaQuery 取 devicePixelRatio */ }
    var map = await methodChannel.invokeMethod<Map>('getScreenSize', {});
    int width  = map!["width"];
    int height = map["height"];
    return Size(width.toDouble() / devicePixelRatio, height.toDouble() / devicePixelRatio);
  }
}

契约拆开来只有四条:

  1. 通道名:fullscreen_window;
  2. 方法名:setFullScreen(参数是 {"isFullScreen": bool})、getScreenSize(无参);
  3. setFullScreen 无返回值,失败就直接抛异常给 Dart;
  4. getScreenSize 返回 {width, height},且 Dart 侧声明为 int,再自己除以 devicePixelRatio。

第 4 条有两个硬约束:原生侧必须返回整数(浮点会直接抛类型错误),而且必须是物理像素——Dart 侧已经准备好再除一次了。

还有一条容易被忽略的:FullScreenWindow 这个顶层变量就是平台接口的默认实例:

final FullScreenWindow = FullScreenWindowPlatform.instance;

而 FullScreenWindowPlatform._instance 的初值是 MethodChannelFullscreenWindow()。这就是为什么鸿蒙必须写原生代码:Android / iOS 通过 dartPluginClass 把实例换成了纯 Dart 的 FullScreenWindowAndroid;鸿蒙不在这个列表里,实例仍是方法通道版本,而通道另一头没人接,调用会得到 MissingPluginException。


四、OHOS 实现:窗口与系统栏

4.1 代码写在哪个文件

fullscreen_window/
├── ohos/
│   ├── index.ets                                   # HAR 出口(flutter create 生成)
│   ├── oh-package.json5                            # 包名/版本/协议(flutter create 生成后手改)
│   └── src/main/
│       ├── module.json5                            # HAR 类型声明(flutter create 生成)
│       └── ets/components/plugin/
│           └── FullscreenWindowPlugin.ets           # ★ 插件本体,本次适配新增的实现都在这
├── example/
│   ├── lib/main.dart                               # ★ 鸿蒙演示页(本次重写)
│   └── ohos/                                       # 应用工程(flutter create 生成)
└── pubspec.yaml                                    # ★ 新增 ohos: pluginClass

4.2 关键点一:鸿蒙的"全屏"是两件事

@ohos.window 把"布局铺满"和"系统栏显隐"拆成两个接口(SDK 注释里也写明旧的 setFullScreen() / setSystemBarEnable() 由这两个替代):

setWindowLayoutFullScreen(isLayoutFullScreen: boolean): Promise<void>;
setWindowSystemBarEnable(names: Array<'status' | 'navigation'>): Promise<void>;

两个都得调,缺一个都能看出问题:

  • 只调 setWindowLayoutFullScreen(true):窗口铺到系统栏底下,但状态栏、导航栏还在(内容被压在状态栏下面);
  • 只调 setWindowSystemBarEnable([]):系统栏没了,但窗口若不是布局全屏,内容不会铺满。

setWindowSystemBarEnable 收的是"要显示哪些栏",所以传空数组才是全隐藏:

if (enable) {
  await mainWindow.setWindowLayoutFullScreen(true);
  await mainWindow.setWindowSystemBarEnable([]);
} else {
  await mainWindow.setWindowSystemBarEnable(ALL_SYSTEM_BARS);   // ['status', 'navigation']
}

4.3 关键点二:退出时只恢复系统栏

这是本次适配唯一一处"反直觉但必须这样做"的设计。

鸿蒙应用的默认形态是"布局全屏 + 系统栏可见":窗口铺满整屏,由应用自己按 avoid area 让出状态栏(Flutter 侧表现为 MediaQuery.padding.top)。所以退出全屏时,只要把系统栏恢复出来,就回到了用户原本看到的样子。

如果顺手把 setWindowLayoutFullScreen(false) 也调回去,就会翻车。实测(Pura X View 模拟器,屏幕 1320×2232px、density 3.0):

退出方式MediaQuery.padding.topMediaQuery.padding.bottomMediaQuery.size
只恢复系统栏(最终实现)39.0 vp28.0 vp440 × 744 vp
再关掉布局全屏39.0 vp28.0 vp440 × 677 vp

744 − 677 = 67vp,正好是 39 + 28:窗口被系统栏挤小了,可 MediaQuery 依旧上报 39vp 的顶部内边距,于是内容被顶下来两次——截图里标题栏比正常位置低了一整个状态栏的高度。这不是"更彻底地退出全屏",而是把一个非默认的窗口形态塞给了应用。

最终实现里退出分支只有一行:

} else {
  // 只恢复系统栏。布局全屏是鸿蒙的默认形态,不要关掉:
  // 关掉后窗口会被系统栏挤小(744vp → 677vp),而 MediaQuery 仍报 39vp 顶部 padding,
  // 内容会被二次顶下来(实测见 README.OpenHarmony_CN.md)。
  await mainWindow.setWindowSystemBarEnable(ALL_SYSTEM_BARS);
}

4.4 关键点三:getScreenSize 必须返回物理像素

Dart 侧拿到值之后会自己除 devicePixelRatio,所以原生侧直接透传 display.getDefaultDisplaySync() 的 width / height 即可——它们本来就是物理像素:

const defaultDisplay: display.Display = display.getDefaultDisplaySync();
const size: Record<string, number> = {
  'width': defaultDisplay.width,      // 模拟器:1320
  'height': defaultDisplay.height     // 模拟器:2232
};
result.success(size);

若在原生侧先换算成 vp(除以 3),Dart 再除一次,会得到 146 × 248 这种明显不对的尺寸。

顺带确认一下编解码:鸿蒙引擎的 StandardMessageCodec.writeValue() 对 Number.isInteger(value) 为 true 的 number 一律编码成 INT32 / INT64(源码 plugin/common/StandardMessageCodec.ets 第 119 行起)。display.width / height 都是整数,到 Dart 侧就是 int,与 int width = map!["width"] 完全对得上。反过来说,如果往通道里塞一个整数值的 double,Dart 侧会拿到 int——这是鸿蒙引擎上所有通道的通病,本插件因为只传整数尺寸而没有受影响。

4.5 关键点四:拿窗口要靠 AbilityAware

插件需要窗口对象,而 UIAbility 上并没有公开的 windowStage 属性(那是 Ability 内部才有的,我第一次编译就是死在这里,见第七节)。正确做法是拿到 UIAbilityContext 之后,用窗口模块自己的接口取当前应用最上层的窗口:

export default class FullscreenWindowPlugin implements FlutterPlugin, MethodCallHandler, AbilityAware {
  private ability: UIAbility | null = null;

  onAttachedToAbility(binding: AbilityPluginBinding): void {
    this.ability = binding.getAbility();
  }

  private async getMainWindow(): Promise<window.Window> {
    const ability = this.ability;
    if (ability === null) {
      throw new Error('FullscreenWindowPlugin is not attached to a UIAbility');
    }
    return await window.getLastWindow(ability.context);
  }
}

window.getLastWindow(ctx: BaseContext) 返回的是当前应用最上层的窗口,单窗口应用里就是主窗口,且只需要一个 BaseContext。插件在 onDetachedFromAbility() 里把 ability 置空,避免持有已销毁的 Ability。

4.6 关键点五:窗口重新获得焦点时把系统栏压回去

系统栏的可见性依附于窗口,切后台、被系统还原之后可能被系统重置。要复刻 Android immersiveSticky 的"自动重新进入"语义,可以在 Ability 绑定上挂一个焦点监听:

onAttachedToAbility(binding: AbilityPluginBinding): void {
  this.ability = binding.getAbility();
  this.abilityBinding = binding;
  binding.addOnWindowFocusChangedListener(this.onWindowFocusChanged);   // 分离时必须注销
}

private handleWindowFocusChanged(hasFocus: boolean): void {
  if (!hasFocus || !this.fullScreen) {
    return;
  }
  this.applyFullScreen(true).catch(/* 只记日志,不影响主流程 */);
}

onDetachedFromAbility() 里对应 removeOnWindowFocusChangedListener()。这一条属于"锦上添花":加不上也不影响进入 / 退出全屏本身,所以实现里用 try/catch 包住,只打警告不抛错。


五、和 Flutter 引擎自带 SystemChrome 的对照实验

既然 Android 侧是拿 SystemChrome 实现的,那鸿蒙上 SystemChrome.setEnabledSystemUIMode() 到底能不能用?我先去引擎里翻了一遍,再做了实验。

5.1 引擎里确实有实现

OHOS 引擎的 PlatformPlugin.ets 里接着 SystemChrome.setEnabledSystemUIMode 这条消息:

} else if (mode == SystemUiMode.IMMERSIVE_STICKY) {
  FlutterManager.getInstance().setUseFullScreen(true, null);   // → setWindowLayoutFullScreen(true)
}
this.showBarOrNavigation = uiConfig;                           // IMMERSIVE_STICKY 时是 []
this.platform?.updateSystemUiOverlays();                        // → setWindowSystemBarEnable(uiConfig)

FlutterManager.setUseFullScreen() 内部正是 currentWindow.getMainWindowSync().setWindowLayoutFullScreen(useFullScreen)。也就是说:引擎做的是和我一样的两步。

5.2 实测结论

操作结果
点"SystemChrome 进入沉浸"系统栏消失,padding 变 0——能用
点"SystemChrome 退出沉浸"系统栏回来,padding 恢复 39.0 / 28.0vp,size 回到 440 × 744vp——与初始状态一致

所以"鸿蒙上 SystemChrome 不管用"这个假设不成立。这个库在鸿蒙上仍然必须写原生代码,原因不在引擎,而在通道没人接:宿主应用调用的是插件包里的 FullScreenWindow.setFullScreen(),它落到 MethodChannelFullscreenWindow 这条通道上;SystemChrome 是另一条通道(flutter/platform),跟插件的通道没有任何关系。Dart 层不允许改动,所以唯一的解法就是在原生侧把 fullscreen_window 这条通道接上。

这段实验还顺带帮我定了 4.3 的策略:引擎的 setEnabledSystemUIOverlays 路径同样不会去关布局全屏,退出时只恢复系统栏。本插件的实现与引擎行为一致,只是多提供了状态记忆与焦点重压。


六、编译与构建踩坑

6.1 ArkTS 编译报错:UIAbility 上没有 windowStage

第一版实现想当然地这样取窗口:

const stage = ability.windowStage;            // ❌
return stage.getMainWindowSync();

flutter build hap 直接失败:

Error Message: Property 'windowStage' does not exist on type 'UIAbility'.
At File: .../FullscreenWindowPlugin.ets:181:59
COMPILE RESULT:FAIL {ERROR:2 WARN:335}

windowStage 只在 Ability 自身的实现里可用,插件拿不到。改用 window.getLastWindow(ability.context) 后编译通过。

6.2 release 构建看不到 Log.i

flutter build hap 默认是 release,而这个构建的第一版验证截图里,界面上一切正常,hilog 里却只有引擎那一条 Adding plugin: FullscreenWindowPlugin,插件自己的日志一条不剩。原因是引擎的 Log 类把默认级别钉在 WARN 上:

export default class Log {
  private static _logLevel = HiLog.LogLevel.WARN;
  private static isLoggable(level: HiLog.LogLevel): boolean {
    let buildModeName: string = BuildProfile.BUILD_MODE_NAME.toLowerCase();
    if (buildModeName == 'release' || buildModeName == 'profile') {
      return level >= Log._logLevel && HiLog.isLoggable(DOMAIN, TAG, level);
    }
    return HiLog.isLoggable(DOMAIN, TAG, level);
  }
}

release / profile 下 INFO < WARN,Log.i 直接被吞。验证时用 --debug 构建:

flutter build hap --debug --target-platform ohos-x64

日志里同时也能看到引擎自己的提示语,便于对照:

W A000ff/Flutter: FlutterEngineCxnRegistry --> Adding plugin: FullscreenWindowPlugin
I A000ff/Flutter: FullscreenWindowPlugin --> fullscreen_window channel registered

注意 hilog 的 tag 固定是 Flutter(domain 0x00FF),插件名只是消息前缀,所以过滤要用消息内容而不是 tag。

6.3 其它

  • ABI 必须对齐模拟器:构建要带 --target-platform ohos-x64,否则安装时 ABI 不匹配(code:9568347);
  • PUB_CACHE 必须与工程同盘:$env:PUB_CACHE = "E:\pub-cache",否则报 The srcPath is not a relative path;
  • 构建前先唤醒设备:hdc shell power-shell wakeup,锁屏状态下安装 / 启动会报 code:10106102;
  • 签名:flutter build hap 只产出 unsigned hap,先在 example/ohos 下跑 devecocli signature generate,再构建才会得到 entry-default-signed.hap;产物路径是 <example>/build/ohos/hap/entry-default-signed.hap。

七、真机(模拟器)验证

7.1 验证环境

项值
Flutter for OpenHarmony SDK3.44.9+ohos-0.0.1-canary1(Dart 3.12.2)
DevEco Studio26.0.0.621(OpenHarmony SDK API 26)
设备Pura X View 模拟器,HarmonyOS 7.0.0(26.0.0) Beta2,ohos-x64
屏幕1320 × 2232 px,density 3.0(440 × 744 vp)
构建flutter build hap --debug --target-platform ohos-x64

示例工程里加了一组只读的"证据面板":MediaQuery 的 padding.top / padding.bottom / size / devicePixelRatio,加上插件返回的屏幕尺寸。全屏是看不见摸不着的状态,用这些数字才能证明它真的生效了。启动后是这样:状态栏正常占位,padding.top 为 39.0vp。

在这里插入图片描述

7.2 实测数据

操作padding.top / bottomMediaQuery.size插件返回
启动39.0 / 28.0 vp440 × 744 vp—
进入全屏(插件)0.0 / 0.0 vp440 × 744 vp—
读取屏幕尺寸0.0 / 0.0 vp440 × 744 vp物理 1320 × 2232 px、逻辑 440 × 744 vp
退出全屏(插件)39.0 / 28.0 vp440 × 744 vp—

点"进入全屏(插件)",状态栏与导航栏一起消失,padding 归零,标题栏顶到屏幕最上方:

在这里插入图片描述

再点"读取屏幕尺寸",插件返回的是整块屏幕的物理像素(1320 × 2232 px);Dart 侧除以 devicePixelRatio = 3.0 后得到逻辑尺寸 440 × 744vp:

在这里插入图片描述

点"退出全屏(插件)",系统栏恢复,padding 回到 39.0 / 28.0vp,标题栏位置与启动时完全一致——这也是"退出时只恢复系统栏、不动布局全屏"这条策略的直接结果:

在这里插入图片描述

四条数据合起来说明三件事:

  1. padding 从 39/28 变 0/0,证明系统栏真的没了、且窗口可用区域真的变了;
  2. MediaQuery.size 始终是 440 × 744vp,证明鸿蒙的全屏是"覆盖"而不是"变大"——窗口尺寸不变,变的是系统栏是否覆盖在上面(这也解释了为什么退出时不能去关布局全屏);
  3. 插件返回的物理像素 1320 × 2232 与 devicePixelRatio = 3.0 完全自洽(1320 / 3 = 440)。

7.3 原生日志

FullscreenWindowPlugin --> fullscreen_window channel registered
FullscreenWindowPlugin --> window full screen -> true
FullscreenWindowPlugin --> getScreenSize -> 1320x2232 (px)
FullscreenWindowPlugin --> getScreenSize -> 1320x2232 (px)   ← 逻辑/物理各调一次
FullscreenWindowPlugin --> window full screen -> false

在这里插入图片描述


八、已知限制

  1. 只作用于主窗口。取窗口用的是 window.getLastWindow(),自由多窗口 / 2in1 形态未验证。
  2. 全屏状态不持久化。插件只在自己进程内记住状态(用于焦点重压),应用重启后回到系统默认;上游 Dart 侧本身也不保存状态。
  3. 系统栏的所有权在系统。用户仍可从屏幕边缘把系统栏拉回来,插件只能在"窗口重新获得焦点"时再压回去;如果系统栏是被手势拉出且窗口一直保持焦点,不会立刻重新隐藏。
  4. 示例移除了 desktop_multi_window 依赖。该包只支持 macOS / Windows / Linux,留着会让鸿蒙示例多一个永远不注册的插件;去掉后 example/lib/main.dart 换成面向鸿蒙的演示页(进入 / 退出全屏、读尺寸、SystemChrome 对照)。
  5. 布局全屏的还原语义有限。若宿主应用自己先把布局全屏关掉、再调用 setFullScreen(false),插件不会把它恢复成"关"的状态——我们的策略是把系统栏恢复出来、布局保持鸿蒙默认。这样做的理由见 4.3。

九、常见问题

Q1:为什么 Android / iOS 一行原生代码都不用写,鸿蒙却要写原生?

因为上游的跨平台策略是"平台上有的接口就用 Flutter 自带的"。pubspec.yaml 里 Android / iOS 声明的是 dartPluginClass: FullScreenWindowAndroid,这个 Dart 类内部只调 SystemChrome.setEnabledSystemUIMode()——对 Android 来说足够了,所以仓库里没有 android/、ios/ 目录。而 dartPluginClass 只在声明过的平台上生效,鸿蒙不在其中,FullScreenWindowPlatform.instance 会保持默认的 MethodChannelFullscreenWindow,通道另一头没人接,调用即 MissingPluginException。要么改 Dart(不允许),要么在原生侧接住这条通道——只有后者可行。

Q2:setFullScreen(false) 之后,为什么 MediaQuery.size 还是 744vp,而不是变小?

因为鸿蒙的全屏是"系统栏覆盖"而不是"窗口变大":进入全屏时窗口尺寸没变(一直铺满整屏),变的是系统栏有没有盖在上面。退出全屏时只把系统栏恢复出来,尺寸自然也是 744vp。真正会改变尺寸的是 setWindowLayoutFullScreen(false)——那会让窗口被系统栏挤小到 677vp,同时 MediaQuery 还报着 39vp 的顶部内边距,内容被二次顶下来。所以本插件故意不调它。744 与 677 这组数字在第四节有完整表格。

Q3:getScreenSize 为什么一定要返回物理像素?

Dart 侧的实现是 Size(width / devicePixelRatio, height / devicePixelRatio),它假定原生返回的是物理像素。display.getDefaultDisplaySync() 返回的正好是物理像素(模拟器 1320 × 2232),直接透传即可。若原生侧先除以 density 变成 vp,Dart 再除一次会得到 146 × 248 这种明显错误的尺寸。另外 Dart 侧把 width / height 声明成 int,原生侧必须传整数,否则抛类型错误。

Q4:为什么不直接用 windowStage.getMainWindowSync()?

因为插件拿不到 windowStage:UIAbility 上并没有公开这个属性(第一次编译就是被 Property 'windowStage' does not exist on type 'UIAbility' 拦下的)。引擎内部是通过自己的 FlutterManager 拿到 windowStage 的,但那是引擎内部类;插件用公开的 window.getLastWindow(context) 更稳,而且只需要一个 BaseContext,语义就是"当前应用最上层窗口"。

Q5:引擎自带的 SystemChrome 在鸿蒙上能用,那这个插件还有必要吗?

SystemChrome 确实能用(第五节的对照实验),但它和这个插件的通道是两回事。宿主应用调用的是 FullScreenWindow.setFullScreen(),走的是 fullscreen_window 通道;SystemChrome 走的是引擎的 flutter/platform 通道。Dart 层不允许改,所以插件在鸿蒙上必须自己把通道接住。真正的结论不是"引擎不行",而是"这条通道没人接"。

Q6:为什么验证一定要用 --debug 构建?

因为引擎的 Log 类在 release / profile 构建下把默认级别钉在 WARN,Log.i 会被 isLoggable 过滤掉,hilog 里一条插件日志都看不到(界面行为仍然正常,容易误判成"代码没执行")。debug 构建下 Log.i 正常输出。如果一定要在 release 里留证据,把关键日志改成 Log.w 也可以。

Q7:全屏状态会在应用重启后保持吗?

不会。系统栏显隐属于窗口状态,窗口重建就回到系统默认(系统栏可见)。插件只在进程内记住"当前是否全屏",用于窗口重新获得焦点时补压一次;上游 Dart 侧也不做持久化。如果需要"重启后仍全屏",得由应用自己在启动时再调一次 setFullScreen(true)。

Q8:需要申请权限吗?

不需要。setWindowLayoutFullScreen、setWindowSystemBarEnable、display.getDefaultDisplaySync() 都是普通应用可以直接调用的窗口 / 显示接口,既不用在 module.json5 里声明权限,也没有运行时弹窗。示例工程 module.json5 里那条 ohos.permission.INTERNET 是 flutter create 模板带的,本插件用不到。


十、本篇用到的库

项值
适配仓库https://atomgit.com/oh-flutter/fullscreen_window
上游仓库https://github.com/jakky1/fullscreen_window
上游版本1.2.1(Apache-2.0)
适配 TAG1.2.1-ohos-1.0.0-beta.1
适配分支feat/ohos_fullscreen_window_1.2.1
平台目录ohos/(插件 HAR)、example/ohos/(示例工程)
接口FullScreenWindow.setFullScreen(bool)、FullScreenWindow.getScreenSize(context | null)

依赖写法(写死 TAG,不跟分支):

dependencies:
  fullscreen_window:
    git:
      url: https://atomgit.com/oh-flutter/fullscreen_window.git
      ref: 1.2.1-ohos-1.0.0-beta.1

示例工程里引用适配库的方式就是这一条 git 依赖(example/pubspec.yaml 里用的是 path: ../,方便随插件一起改)。

验证环境

项值
Flutter for OpenHarmony SDK3.44.9+ohos-0.0.1-canary1
Dart3.12.2
DevEco Studio26.0.0.621(OpenHarmony SDK API 26)
设备Pura X View 模拟器,HarmonyOS 7.0.0(26.0.0) Beta2,ohos-x64
构建产物example/build/ohos/hap/entry-default-signed.hap

复现命令

# 1. 构建(PUB_CACHE 必须与工程同盘;模拟器是 ohos-x64)
$env:PUB_CACHE = "E:\pub-cache"
cd example
flutter pub get
flutter build hap --debug --target-platform ohos-x64

# 2. 安装并启动
hdc shell power-shell wakeup
hdc install -r build/ohos/hap/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.example.fullscreen_window_example

# 3. 依次点击:进入全屏(插件)→ 读取屏幕尺寸 → 退出全屏(插件)
#    截图
hdc shell snapshot_display -f /data/local/tmp/v1.jpeg
hdc file recv /data/local/tmp/v1.jpeg .

# 4. 取原生日志
hdc shell hilog -x | Select-String "FullscreenWindowPlugin"

欢迎加入 CPF-Flutter 鸿蒙社区:https://atomgit.com/CPF-Flutter

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐