欢迎加入开源鸿蒙跨平台社区: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;
}

说明

  • 采用单例模式,全局共享同一实例
  • 通过 onChange Stream 监听键盘状态变化
  • 通过 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 提供了完整的键盘可见性监听能力,支持多种使用方式,适合各种键盘交互场景。

核心要点

  1. 使用单例模式的 KeyboardVisibilityController 控制器
  2. 通过 onChange Stream 监听键盘状态变化
  3. 通过 isVisible 属性获取当前键盘状态
  4. 使用 KeyboardVisibilityBuilder 简化状态响应
  5. 使用 KeyboardVisibilityProvider 在子树中共享状态
  6. 使用 KeyboardDismissOnTap 实现点击空白收起键盘

最佳实践

  • 在需要响应键盘状态的地方使用 KeyboardVisibilityBuilder
  • 在多个 Widget 需要访问状态时使用 KeyboardVisibilityProvider
  • 使用 KeyboardDismissOnTap 提升用户体验
  • 记得取消 Stream 订阅,避免内存泄漏
  • 结合 MediaQuery.viewInsets.bottom 获取键盘高度
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐