Flutter HarmonyOS 键盘高度监听插件开发指南
一、背景介绍
在开发聊天类 App 时,我们经常需要实现一个"键盘面板切换"功能------当用户点击表情按钮时,键盘收起,表情面板以相同高度弹出,实现平滑过渡。这就需要准确获取键盘高度。
在 HarmonyOS (OHOS) 平台上,Flutter 插件需要通过原生代码监听键盘高度变化,再通过 MethodChannel 通知 Flutter 端。本文将详细介绍如何实现这一功能。
二、技术难点与解决方案
2.1 窗口获取时机问题
问题描述:
最初的实现方案是在 onAttachedToEngine 中直接获取窗口:
typescript
// ❌ 错误方案
onAttachedToEngine(binding: FlutterPluginBinding): void {
const win = await window.getLastWindow(binding.getApplicationContext());
// 报错:{"code":1300002} - 窗口状态异常
}
错误码 1300002 表示窗口状态异常,因为此时窗口尚未创建完成。
解决方案:
实现 AbilityAware 接口,通过 WindowFocusChangedListener 监听窗口焦点变化,在窗口获得焦点后再设置监听:
typescript
// ✅ 正确方案
onAttachedToAbility(binding: AbilityPluginBinding): void {
this.ability = binding.getAbility();
binding.addOnWindowFocusChangedListener(this.windowFocusChangedListener);
}
private windowFocusChangedListener: WindowFocusChangedListener = {
onWindowFocusChanged: (hasFocus: boolean) => {
if (hasFocus && !this.isKeyboardListenerSetup) {
this.setupKeyboardListener();
}
}
};
2.2 安全区域获取
问题描述:
使用 TYPE_SYSTEM 获取底部安全区域始终返回 0。
解决方案:
优先使用 TYPE_NAVIGATION_INDICATOR 获取导航指示器(底部小白条)区域:
typescript
private getSafeAreaBottom(win: window.Window): number {
const uiContext = win.getUIContext();
// 优先获取导航指示器区域
try {
const navigationArea = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
if (navigationArea.bottomRect.height > 0) {
return uiContext.px2vp(navigationArea.bottomRect.height);
}
} catch (e) {}
// 降级获取系统避让区域
try {
const systemArea = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
if (systemArea.bottomRect.height > 0) {
return uiContext.px2vp(systemArea.bottomRect.height);
}
} catch (e) {}
return 0;
}
三、完整实现
3.1 插件架构
chat_keyboard_panel/
├── lib/
│ └── src/
│ └── keyboard_height.dart # Flutter 端实现
├── ohos/
│ ├── index.ets # 插件导出
│ └── src/main/ets/components/plugin/
│ └── ChatKeyboardPanelPlugin.ets # 原生实现
3.2 原生端实现 (ArkTS)
typescript
import {
AbilityAware,
AbilityPluginBinding,
FlutterPlugin,
FlutterPluginBinding,
MethodCall,
MethodCallHandler,
MethodChannel,
MethodResult,
WindowFocusChangedListener,
} from '@ohos/flutter_ohos';
import { window } from '@kit.ArkUI';
import UIAbility from '@ohos.app.ability.UIAbility';
export default class ChatKeyboardPanelPlugin implements FlutterPlugin, MethodCallHandler, AbilityAware {
private channel: MethodChannel | null = null;
private abilityBinding: AbilityPluginBinding | null = null;
private ability: UIAbility | null = null;
private mainWindow: window.Window | null = null;
private safeAreaBottom: number = 0;
private isKeyboardListenerSetup: boolean = false;
getUniqueClassName(): string {
return "ChatKeyboardPanelPlugin"
}
// FlutterPlugin 生命周期
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new MethodChannel(binding.getBinaryMessenger(), "chat_keyboard_panel");
this.channel.setMethodCallHandler(this);
}
onDetachedFromEngine(_: FlutterPluginBinding): void {
if (this.channel != null) {
this.channel.setMethodCallHandler(null);
this.channel = null;
}
}
// AbilityAware 生命周期
onAttachedToAbility(binding: AbilityPluginBinding): void {
this.abilityBinding = binding;
this.ability = binding.getAbility();
binding.addOnWindowFocusChangedListener(this.windowFocusChangedListener);
}
onDetachedFromAbility(): void {
if (this.abilityBinding != null) {
this.abilityBinding.removeOnWindowFocusChangedListener(this.windowFocusChangedListener);
this.abilityBinding = null;
}
this.removeKeyboardListener();
this.ability = null;
this.mainWindow = null;
this.isKeyboardListenerSetup = false;
}
onMethodCall(call: MethodCall, result: MethodResult): void {
if (call.method == "getPlatformVersion") {
result.success("Harmony");
} else {
result.notImplemented();
}
}
// 窗口焦点监听器
private windowFocusChangedListener: WindowFocusChangedListener = {
onWindowFocusChanged: (hasFocus: boolean) => {
if (hasFocus && !this.isKeyboardListenerSetup) {
this.setupKeyboardListener();
}
}
};
// 设置键盘监听
private async setupKeyboardListener(): Promise<void> {
if (this.isKeyboardListenerSetup || this.ability == null) return;
try {
const win = await window.getLastWindow(this.ability.context);
if (win == null) return;
this.mainWindow = win;
const uiContext = win.getUIContext();
this.safeAreaBottom = this.getSafeAreaBottom(win);
// 监听键盘高度变化
win.on('keyboardHeightChange', (height: number) => {
const keyboardHeight = uiContext.px2vp(height);
this.notifyKeyboardHeight(keyboardHeight);
});
this.isKeyboardListenerSetup = true;
// 移除窗口焦点监听器
if (this.abilityBinding != null) {
this.abilityBinding.removeOnWindowFocusChangedListener(this.windowFocusChangedListener);
}
} catch (error) {
console.error(`setupKeyboardListener error: ${JSON.stringify(error)}`);
}
}
// 获取底部安全区域
private getSafeAreaBottom(win: window.Window): number {
const uiContext = win.getUIContext();
try {
const area = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
if (area.bottomRect.height > 0) {
return uiContext.px2vp(area.bottomRect.height);
}
} catch (e) {}
return 0;
}
// 移除键盘监听
private removeKeyboardListener(): void {
if (this.mainWindow != null) {
try {
this.mainWindow.off('keyboardHeightChange');
} catch (error) {}
}
}
// 通知 Flutter 端
private notifyKeyboardHeight(keyboardHeight: number): void {
if (this.channel != null) {
const args: Record<string, number> = {
'height': keyboardHeight,
'safeArea': this.safeAreaBottom
};
this.channel.invokeMethod('height', args);
}
}
}
3.3 Flutter 端实现
dart
import 'package:flutter/services.dart';
class ChatKeyboardHeight {
MethodChannel channel = const MethodChannel('chat_keyboard_panel');
static ChatKeyboardHeight? _instance;
static ChatKeyboardHeight get instance =>
_instance ??= ChatKeyboardHeight._();
final List<KeyboardHeightCallback> _keyboardHeightCallbacks = [];
ChatKeyboardHeight._() {
channel.setMethodCallHandler((call) async {
if (call.method == 'height') {
Map map = call.arguments;
double height = (map["height"] ?? 0).toDouble();
double safeArea = (map["safeArea"] ?? 0).toDouble();
for (var element in _keyboardHeightCallbacks) {
element(height, safeArea);
}
}
});
}
void addKeyboardHeightCallback(KeyboardHeightCallback callback) {
_keyboardHeightCallbacks.add(callback);
}
void removeKeyboardHeightCallback(KeyboardHeightCallback callback) {
_keyboardHeightCallbacks.remove(callback);
}
}
typedef KeyboardHeightCallback = void Function(double height, double safeArea);
四、使用方式
4.1 Flutter 端使用
dart
@override
void initState() {
super.initState();
ChatKeyboardHeight.instance.addKeyboardHeightCallback(_onKeyboardHeight);
}
@override
void dispose() {
ChatKeyboardHeight.instance.removeKeyboardHeightCallback(_onKeyboardHeight);
super.dispose();
}
void _onKeyboardHeight(double height, double safeArea) {
setState(() {
_keyboardHeight = height;
_safeAreaBottom = safeArea;
});
}
4.2 宿主应用配置
使用此插件的应用只需保持标准的 EntryAbility 配置即可,无需额外代码:
typescript
import { FlutterAbility, FlutterEngine } from '@ohos/flutter_ohos';
import { GeneratedPluginRegistrant } from '../plugins/GeneratedPluginRegistrant';
export default class EntryAbility extends FlutterAbility {
configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine);
GeneratedPluginRegistrant.registerWith(flutterEngine);
}
}
五、关键点总结
| 问题 | 解决方案 |
|---|---|
| 窗口获取时机 | 实现 AbilityAware 接口,在窗口获得焦点后获取 |
| 窗口状态异常 (1300002) | 使用 WindowFocusChangedListener 延迟初始化 |
| 安全区域为 0 | 优先使用 TYPE_NAVIGATION_INDICATOR |
| 单位转换 | 使用 uiContext.px2vp() 将 px 转为 vp |
| 资源清理 | 在 onDetachedFromAbility 中移除所有监听器 |
六、生命周期流程图
FlutterEngine 创建
↓
onAttachedToEngine (创建 MethodChannel)
↓
onAttachedToAbility (注册 WindowFocusChangedListener)
↓
窗口获得焦点 (onWindowFocusChanged: true)
↓
setupKeyboardListener (获取窗口、设置键盘监听)
↓
键盘弹出/收起 (keyboardHeightChange)
↓
notifyKeyboardHeight → Flutter 端回调
↓
onDetachedFromAbility (清理资源)
↓
onDetachedFromEngine (释放 MethodChannel)
通过以上实现,插件能够在 HarmonyOS 平台上准确监听键盘高度变化,为 Flutter 应用提供流畅的键盘面板切换体验。