Flutter for OpenHarmony三方库适配实战:flutter_keyboard_visibility 键盘可见性监听
键盘可见性监听是移动应用的常见需求,用于处理键盘遮挡、动态调整布局、表单交互等场景。在 Flutter for OpenHarmony 应用开发中,是一个功能完善的键盘状态监听插件,提供了跨平台的键盘可见性检测能力。flutter_keyboard_visibility 库为 Flutter for OpenHarmony 提供了完整的键盘可见性监听能力,支持多种使用方式,适合各种键盘交互场景。核
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
本文基于flutter3.27.5开发

一、flutter_keyboard_visibility 库概述
键盘可见性监听是移动应用的常见需求,用于处理键盘遮挡、动态调整布局、表单交互等场景。在 Flutter for OpenHarmony 应用开发中,flutter_keyboard_visibility 是一个功能完善的键盘状态监听插件,提供了跨平台的键盘可见性检测能力。
flutter_keyboard_visibility 库特点
flutter_keyboard_visibility 库基于 Flutter 平台接口实现,提供了以下核心特性:
实时监听:通过 Stream 实时监听键盘状态变化,响应及时准确。
多种使用方式:提供控制器、Builder、Provider 等多种使用方式,适应不同场景需求。
便捷组件:内置 KeyboardDismissOnTap 组件,点击空白区域自动收起键盘。
测试支持:提供测试工具类,方便单元测试和 Widget 测试。
跨平台支持:统一的 API 设计,支持 Android、iOS、OpenHarmony、Web 等多个平台。
功能支持对比
| 功能 | Android | iOS | OpenHarmony |
|---|---|---|---|
| 键盘可见性监听 | ✅ | ✅ | ✅ |
| 当前状态获取 | ✅ | ✅ | ✅ |
| Builder 组件 | ✅ | ✅ | ✅ |
| Provider 组件 | ✅ | ✅ | ✅ |
| 点击收起键盘 | ✅ | ✅ | ✅ |
使用场景:聊天应用、表单页面、登录注册、搜索页面、评论输入等需要处理键盘交互的场景。
二、安装与配置
2.1 添加依赖
在项目的 pubspec.yaml 文件中添加 flutter_keyboard_visibility 依赖:
dependencies:
flutter_keyboard_visibility:
git:
url: https://atomgit.com/openharmony-sig/flutter_keyboard_visibility.git
path: flutter_keyboard_visibility
然后执行以下命令获取依赖:
flutter pub get
2.2 无需额外权限
键盘可见性监听不需要任何特殊权限配置。
三、核心 API 详解
3.1 KeyboardVisibilityController 控制器
KeyboardVisibilityController 是键盘可见性控制的核心类,采用单例模式设计。
class KeyboardVisibilityController {
factory KeyboardVisibilityController() => _instance;
Stream<bool> get onChange;
bool get isVisible;
}
说明:
- 采用单例模式,全局共享同一实例
- 通过
onChangeStream 监听键盘状态变化 - 通过
isVisible属性获取当前键盘可见状态
3.2 onChange 属性
onChange 是键盘状态变化的 Stream,每次键盘显示或隐藏都会触发。
Stream<bool> get onChange
返回值:返回 Stream<bool>,true 表示键盘可见,false 表示键盘隐藏。
使用示例:
final controller = KeyboardVisibilityController();
controller.onChange.listen((isVisible) {
if (isVisible) {
print('键盘已显示');
} else {
print('键盘已隐藏');
}
});
3.3 isVisible 属性
isVisible 属性用于获取当前键盘是否可见。
bool get isVisible
返回值:返回 true 表示键盘当前可见,false 表示键盘当前隐藏。
使用示例:
final controller = KeyboardVisibilityController();
if (controller.isVisible) {
print('键盘当前可见');
} else {
print('键盘当前隐藏');
}
四、Widget 组件详解
4.1 KeyboardVisibilityBuilder 组件
KeyboardVisibilityBuilder 是一个便捷的 Builder 组件,通过 Builder 函数暴露键盘可见状态。
class KeyboardVisibilityBuilder extends StatelessWidget {
const KeyboardVisibilityBuilder({
Key? key,
required this.builder,
this.controller,
});
final Widget Function(BuildContext, bool isKeyboardVisible) builder;
final KeyboardVisibilityController? controller;
}
参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
| builder | Widget Function(BuildContext, bool) | 必需,构建 Widget 的函数 |
| controller | KeyboardVisibilityController? | 可选,自定义控制器 |
使用示例:
KeyboardVisibilityBuilder(
builder: (context, isKeyboardVisible) {
return Text(
isKeyboardVisible ? '键盘已显示' : '键盘已隐藏',
);
},
)
4.2 KeyboardVisibilityProvider 组件
KeyboardVisibilityProvider 是一个 InheritedWidget,允许子树中的任何 Widget 访问键盘可见状态。
class KeyboardVisibilityProvider extends StatefulWidget {
const KeyboardVisibilityProvider({
Key? key,
required this.child,
this.controller,
});
final Widget child;
final KeyboardVisibilityController? controller;
static bool isKeyboardVisible(BuildContext context);
}
参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
| child | Widget | 必需,子 Widget |
| controller | KeyboardVisibilityController? | 可选,自定义控制器 |
使用示例:
KeyboardVisibilityProvider(
child: Builder(
builder: (context) {
final isVisible = KeyboardVisibilityProvider.isKeyboardVisible(context);
return Text('键盘状态: $isVisible');
},
),
)
4.3 KeyboardDismissOnTap 组件
KeyboardDismissOnTap 是一个点击空白区域自动收起键盘的组件。
class KeyboardDismissOnTap extends StatefulWidget {
const KeyboardDismissOnTap({
Key? key,
required this.child,
this.dismissOnCapturedTaps = false,
});
final Widget child;
final bool dismissOnCapturedTaps;
}
参数说明:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| child | Widget | 必需 | 子 Widget |
| dismissOnCapturedTaps | bool | false | 是否在捕获点击时也收起键盘 |
使用示例:
KeyboardDismissOnTap(
child: Scaffold(
body: Column(
children: [
TextField(),
Expanded(child: Container()),
],
),
),
)
4.4 IgnoreKeyboardDismiss 组件
IgnoreKeyboardDismiss 用于忽略特定区域的键盘收起行为。
class IgnoreKeyboardDismiss extends StatelessWidget {
const IgnoreKeyboardDismiss({
Key? key,
required this.child,
});
final Widget child;
}
使用示例:
KeyboardDismissOnTap(
child: Column(
children: [
TextField(),
IgnoreKeyboardDismiss(
child: ElevatedButton(
onPressed: () {},
child: Text('点击不收起键盘'),
),
),
],
),
)
五、OpenHarmony 平台实现原理
5.1 原生 API 映射
| Flutter API | OpenHarmony API |
|---|---|
| onChange Stream | window.on(“avoidAreaChange”) |
| isVisible | avoidArea.bottomRect.height > 0 |
5.2 键盘可见性监听实现
OpenHarmony 通过监听窗口避让区域变化来检测键盘状态:
export default class FlutterKeyboardVisibilityPlugin
implements FlutterPlugin, StreamHandler, AbilityAware {
private eventSink: EventSink | null = null;
private isVisible: boolean = false;
private window: window.Window | undefined = undefined;
onListen(o: ESObject, eventSink: EventSink): void {
this.eventSink = eventSink;
this.listenForKeyboard();
}
private async listenForKeyboard(): Promise<void> {
try {
if(this.window == undefined) {
const uiAbility = FlutterManager.getInstance().getUIAbility(getContext(this));
const windowStage = FlutterManager.getInstance().getWindowStage(uiAbility);
this.window = windowStage.getMainWindowSync();
}
this.window?.on("avoidAreaChange", (data) => {
if (data.type == 3) {
let newState = data.area.bottomRect.height > 0 ? true : false;
if (newState != this.isVisible) {
this.isVisible = newState;
if (this.eventSink != null) {
this.eventSink.success(this.isVisible ? 1 : 0);
}
}
}
});
} catch (err) {
Log.e(TAG, "Failed to obtain the top window. Cause: " + JSON.stringify(err));
}
}
}
5.3 Android 平台实现对比
Android 通过 ViewTreeObserver 监听布局变化来检测键盘:
public class FlutterKeyboardVisibilityPlugin
implements FlutterPlugin, ActivityAware, EventChannel.StreamHandler,
ViewTreeObserver.OnGlobalLayoutListener {
private EventChannel.EventSink eventSink;
private View mainView;
private boolean isVisible;
@Override
public void onGlobalLayout() {
if (mainView != null) {
Rect r = new Rect();
mainView.getWindowVisibleDisplayFrame(r);
boolean newState = ((double)r.height() / (double)mainView.getRootView().getHeight()) < 0.85;
if (newState != isVisible) {
isVisible = newState;
if (eventSink != null) {
eventSink.success(isVisible ? 1 : 0);
}
}
}
}
private void listenForKeyboard(Activity activity) {
mainView = activity.findViewById(android.R.id.content);
mainView.getViewTreeObserver().addOnGlobalLayoutListener(this);
}
}
5.4 EventChannel 通信
private init(messenger: BinaryMessenger): void {
const eventChannel = new EventChannel(messenger, "flutter_keyboard_visibility");
eventChannel.setStreamHandler(this);
}
六、MethodChannel 通信协议
6.1 EventChannel 名称
const EventChannel eventChannel = EventChannel("flutter_keyboard_visibility");
6.2 事件数据
| 值 | 说明 |
|---|---|
| 1 | 键盘已显示 |
| 0 | 键盘已隐藏 |
七、实战案例
7.1 完整键盘可见性示例
以下示例整合了键盘可见性监听的核心功能,包括:状态监听、布局调整、点击收起键盘等。
import 'package:flutter/material.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
void main() {
runApp(const MaterialApp(home: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return KeyboardDismissOnTap(
dismissOnCapturedTaps: true,
child: const KeyboardDemoPage(),
);
}
}
class KeyboardDemoPage extends StatefulWidget {
const KeyboardDemoPage({super.key});
State<StatefulWidget> createState() => _KeyboardDemoPageState();
}
class _KeyboardDemoPageState extends State<KeyboardDemoPage> {
final KeyboardVisibilityController _controller = KeyboardVisibilityController();
final TextEditingController _textController = TextEditingController();
final FocusNode _focusNode = FocusNode();
bool _isKeyboardVisible = false;
int _keyboardEventCount = 0;
void initState() {
super.initState();
_isKeyboardVisible = _controller.isVisible;
_controller.onChange.listen(_onKeyboardVisibilityChange);
}
void _onKeyboardVisibilityChange(bool isVisible) {
setState(() {
_isKeyboardVisible = isVisible;
_keyboardEventCount++;
});
}
void dispose() {
_textController.dispose();
_focusNode.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('键盘可见性监听')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildStatusCard(),
const SizedBox(height: 16),
_buildInputSection(),
const SizedBox(height: 16),
_buildBuilderDemo(),
const SizedBox(height: 16),
_buildProviderDemo(),
const SizedBox(height: 100),
],
),
),
floatingActionButton: _isKeyboardVisible
? FloatingActionButton(
onPressed: () => _focusNode.unfocus(),
child: const Icon(Icons.keyboard_hide),
)
: null,
);
}
Widget _buildStatusCard() {
return Card(
color: _isKeyboardVisible ? Colors.green.shade50 : Colors.grey.shade100,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Icon(
_isKeyboardVisible ? Icons.keyboard : Icons.keyboard_hide,
size: 48,
color: _isKeyboardVisible ? Colors.green : Colors.grey,
),
const SizedBox(height: 8),
Text(
_isKeyboardVisible ? '键盘已显示' : '键盘已隐藏',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: _isKeyboardVisible ? Colors.green : Colors.grey,
),
),
const SizedBox(height: 8),
Text(
'状态变化次数: $_keyboardEventCount',
style: const TextStyle(color: Colors.grey),
),
],
),
),
);
}
Widget _buildInputSection() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'输入区域',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
TextField(
controller: _textController,
focusNode: _focusNode,
decoration: const InputDecoration(
labelText: '点击此处唤起键盘',
border: OutlineInputBorder(),
suffixIcon: Icon(Icons.edit),
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () => _focusNode.requestFocus(),
icon: const Icon(Icons.keyboard),
label: const Text('显示键盘'),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
onPressed: () => _focusNode.unfocus(),
icon: const Icon(Icons.keyboard_hide),
label: const Text('隐藏键盘'),
),
),
],
),
],
),
),
);
}
Widget _buildBuilderDemo() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'KeyboardVisibilityBuilder 示例',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
KeyboardVisibilityBuilder(
builder: (context, isVisible) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isVisible ? Colors.blue.shade50 : Colors.orange.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
isVisible ? Icons.arrow_upward : Icons.arrow_downward,
color: isVisible ? Colors.blue : Colors.orange,
),
const SizedBox(width: 8),
Expanded(
child: Text(
isVisible
? '键盘弹出时可以调整布局,避免遮挡内容'
: '键盘收起时恢复原始布局',
style: TextStyle(
color: isVisible ? Colors.blue : Colors.orange,
),
),
),
],
),
);
},
),
],
),
),
);
}
Widget _buildProviderDemo() {
return KeyboardVisibilityProvider(
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'KeyboardVisibilityProvider 示例',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
Builder(
builder: (context) {
final isVisible = KeyboardVisibilityProvider.isKeyboardVisible(context);
return Text(
'通过 Provider 获取状态: ${isVisible ? "可见" : "隐藏"}',
style: TextStyle(
color: isVisible ? Colors.green : Colors.red,
fontWeight: FontWeight.w500,
),
);
},
),
],
),
),
),
);
}
}
代码要点说明:
| 功能模块 | 实现方式 |
|---|---|
| 状态监听 | KeyboardVisibilityController.onChange.listen() |
| 当前状态 | KeyboardVisibilityController.isVisible |
| Builder 方式 | KeyboardVisibilityBuilder 组件 |
| Provider 方式 | KeyboardVisibilityProvider + isKeyboardVisible(context) |
| 点击收起 | KeyboardDismissOnTap 包裹整个页面 |
| 手动控制 | FocusNode.unfocus() 收起键盘 |
八、常见问题与解决方案
8.1 键盘状态检测延迟
问题:键盘弹出或收起时,状态更新有延迟。
解决方案:
这是平台原生实现的特性,Android 和 OpenHarmony 都是基于布局变化检测,存在一定延迟。可以通过动画过渡来优化用户体验:
AnimatedContainer(
duration: const Duration(milliseconds: 200),
height: isKeyboardVisible ? 0 : 100,
child: Container(color: Colors.blue),
)
8.2 某些按钮点击会收起键盘
问题:使用 KeyboardDismissOnTap 后,某些按钮点击也会收起键盘。
解决方案:
使用 IgnoreKeyboardDismiss 包裹需要保留键盘的按钮:
KeyboardDismissOnTap(
child: Column(
children: [
TextField(),
IgnoreKeyboardDismiss(
child: ElevatedButton(
onPressed: () {},
child: Text('发送'),
),
),
],
),
)
8.3 Web 平台兼容性
问题:Web 平台键盘检测不准确。
解决方案:
Web 平台基于窗口大小变化检测,可能不够精确。建议在 Web 平台使用 MediaQuery.of(context).viewInsets.bottom 作为补充:
double keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
bool isKeyboardVisible = keyboardHeight > 0;
8.4 多次订阅问题
问题:多次调用 onChange.listen 导致重复触发。
解决方案:
KeyboardVisibilityController 是单例,onChange 是广播 Stream,可以安全地多次订阅。但记得在 dispose 时取消订阅:
class _MyState extends State<MyWidget> {
StreamSubscription? _subscription;
void initState() {
super.initState();
_subscription = KeyboardVisibilityController().onChange.listen((isVisible) {
});
}
void dispose() {
_subscription?.cancel();
super.dispose();
}
}
九、与其他库对比
| 特性 | flutter_keyboard_visibility | flutter_keyboard_utils | keyboard_visibility |
|---|---|---|---|
| 状态监听 | ✅ | ✅ | ✅ |
| Builder 组件 | ✅ | ❌ | ❌ |
| Provider 组件 | ✅ | ❌ | ❌ |
| 点击收起键盘 | ✅ | ✅ | ❌ |
| 测试支持 | ✅ | ❌ | ❌ |
| OpenHarmony 支持 | ✅ | ❌ | ❌ |
| 维护状态 | 活跃开发 | 社区维护 | 停止维护 |
建议:
- 推荐使用
flutter_keyboard_visibility,功能完善且持续维护 - 如果只需要简单的键盘高度获取,可使用
MediaQuery.viewInsets.bottom
十、总结
flutter_keyboard_visibility 库为 Flutter for OpenHarmony 提供了完整的键盘可见性监听能力,支持多种使用方式,适合各种键盘交互场景。
核心要点:
- 使用单例模式的
KeyboardVisibilityController控制器 - 通过
onChangeStream 监听键盘状态变化 - 通过
isVisible属性获取当前键盘状态 - 使用
KeyboardVisibilityBuilder简化状态响应 - 使用
KeyboardVisibilityProvider在子树中共享状态 - 使用
KeyboardDismissOnTap实现点击空白收起键盘
最佳实践:
- 在需要响应键盘状态的地方使用
KeyboardVisibilityBuilder - 在多个 Widget 需要访问状态时使用
KeyboardVisibilityProvider - 使用
KeyboardDismissOnTap提升用户体验 - 记得取消 Stream 订阅,避免内存泄漏
- 结合
MediaQuery.viewInsets.bottom获取键盘高度
更多推荐
所有评论(0)