Flutter HarmonyOS 键盘高度监听插件开发指南

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 应用提供流畅的键盘面板切换体验。

相关推荐
帅气马战的账号12 小时前
OpenHarmony 与 Flutter 跨端融合开发指南:从基础到实战
flutter
爱吃大芒果11 小时前
GitCode口袋工具的部署运行教程
flutter·华为·harmonyos·gitcode
爱吃大芒果11 小时前
Flutter基础入门与核心能力构建——Widget、State与BuildContext核心解析
flutter·华为·harmonyos
灵感菇_13 小时前
Flutter Riverpod 完整教程:从入门到实战
前端·flutter·ui·状态管理
威哥爱编程14 小时前
【鸿蒙开发案例篇】鸿蒙6.0计算器实现详解
harmonyos·arkts·arkui
Zender Han15 小时前
Flutter Gradients 全面指南:原理、类型与实战使用
android·flutter·ios
威哥爱编程15 小时前
【鸿蒙开发案例篇】鸿蒙跨设备实时滤镜同步的完整方案
harmonyos·arkts·arkui
火柴就是我15 小时前
Flutter Path.computeMetrics() 的使用注意点
android·flutter
等你等了那么久16 小时前
Flutter打包APK记录
flutter·dart