Flutter for OpenHarmony 实战:三方库 fullscreen_window 的鸿蒙化适配指南
环境搭建指引: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 是官方给的"自动补全"命令,它会:
- 生成
ohos/(HAR 工程:index.ets、oh-package.json5、src/main/module.json5、插件模板src/main/ets/components/plugin/FullscreenWindowPlugin.ets); - 生成
example/ohos/(应用工程:AppScope/、entry/、hvigorfile.ts、hvigorconfig.ts); - 改写
.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.ets | HAR 出口,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);
}
}
契约拆开来只有四条:
- 通道名:
fullscreen_window; - 方法名:
setFullScreen(参数是{"isFullScreen": bool})、getScreenSize(无参); setFullScreen无返回值,失败就直接抛异常给 Dart;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.top | MediaQuery.padding.bottom | MediaQuery.size |
|---|---|---|---|
| 只恢复系统栏(最终实现) | 39.0 vp | 28.0 vp | 440 × 744 vp |
| 再关掉布局全屏 | 39.0 vp | 28.0 vp | 440 × 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 SDK | 3.44.9+ohos-0.0.1-canary1(Dart 3.12.2) |
| DevEco Studio | 26.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 / bottom | MediaQuery.size | 插件返回 |
|---|---|---|---|
| 启动 | 39.0 / 28.0 vp | 440 × 744 vp | — |
| 进入全屏(插件) | 0.0 / 0.0 vp | 440 × 744 vp | — |
| 读取屏幕尺寸 | 0.0 / 0.0 vp | 440 × 744 vp | 物理 1320 × 2232 px、逻辑 440 × 744 vp |
| 退出全屏(插件) | 39.0 / 28.0 vp | 440 × 744 vp | — |
点"进入全屏(插件)",状态栏与导航栏一起消失,padding 归零,标题栏顶到屏幕最上方:

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

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

四条数据合起来说明三件事:
padding从 39/28 变 0/0,证明系统栏真的没了、且窗口可用区域真的变了;MediaQuery.size始终是 440 × 744vp,证明鸿蒙的全屏是"覆盖"而不是"变大"——窗口尺寸不变,变的是系统栏是否覆盖在上面(这也解释了为什么退出时不能去关布局全屏);- 插件返回的物理像素 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

八、已知限制
- 只作用于主窗口。取窗口用的是
window.getLastWindow(),自由多窗口 / 2in1 形态未验证。 - 全屏状态不持久化。插件只在自己进程内记住状态(用于焦点重压),应用重启后回到系统默认;上游 Dart 侧本身也不保存状态。
- 系统栏的所有权在系统。用户仍可从屏幕边缘把系统栏拉回来,插件只能在"窗口重新获得焦点"时再压回去;如果系统栏是被手势拉出且窗口一直保持焦点,不会立刻重新隐藏。
- 示例移除了
desktop_multi_window依赖。该包只支持 macOS / Windows / Linux,留着会让鸿蒙示例多一个永远不注册的插件;去掉后example/lib/main.dart换成面向鸿蒙的演示页(进入 / 退出全屏、读尺寸、SystemChrome 对照)。 - 布局全屏的还原语义有限。若宿主应用自己先把布局全屏关掉、再调用
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) |
| 适配 TAG | 1.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 SDK | 3.44.9+ohos-0.0.1-canary1 |
| Dart | 3.12.2 |
| DevEco Studio | 26.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
更多推荐



所有评论(0)