ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

鸿蒙OS Next上Flutter软键盘避让实战:从踩坑到封装

鸿蒙OS Next上Flutter软键盘避让实战:从踩坑到封装 这几天在鸿蒙OS Next设备上调试一个Flutter页面遇到一个很典型的输入场景底部是一个多行输入框用户点进去键盘一弹输入框直接被盖住大半。在Android上遇到这类问题第一反应是检查AndroidManifest里的windowSoftInputMode或者指望Flutter自带的Scaffold.resizeToAvoidBottomInset帮你把bottom inset扛掉。可在鸿蒙OS Next上这套经验失灵了。折腾了一圈之后还是老老实实用flutter_keyboard_visibility来做软键盘感知再自己处理避让逻辑。这篇文章就是想把这次实战中踩过的坑、查过的源码、以及能直接抄的代码整理出来给正在做Flutter for OpenHarmony适配的同学一点参考。我默认你手头的项目已经能用Flutter跑起OpenHarmony的Hello World如果还没到这个程度文章第2部分会讲环境准备重点提一下VS Code和工具链上容易卡住的报错。整个过程会围绕三个问题展开为什么鸿蒙上不能直接套Android的经验、插件怎么接入、以及接入之后还有哪些磨人的细节。1. 软键盘避让为什么不能照搬Android思路1.1 鸿蒙OS Next的窗口管理模式做Android开发的同学都清楚软键盘出来之后窗口要不要被压缩取决于windowSoftInputMode的取值。adjustResize会压缩Activity的可用高度adjustPan则是把整个窗口往上顶让焦点控件露出来。Flutter在Android上的表现跟窗口模式高度绑定Scaffold之所以能自动避让核心是MediaQuery.viewInsets.bottom携带了被键盘压缩掉的高度Widget树里能根据这个值做自适应。但到了鸿蒙OS Next上这套机制不再等于Android。OpenHarmony的窗口管理是另一套逻辑应用窗口默认情况下对软键盘的处理更接近“覆盖式”而不是“压缩式”。含义就是键盘弹出来窗口本身不会自觉收缩Flutter引擎收到的viewInsets经常是零导致resizeToAvoidBottomInset形同虚设。我在真机上验证过键盘完全弹出时MediaQuery.of(context).viewInsets.bottom始终是0但屏幕底部一块区域肉眼可见地盖住了输入框。在鸿蒙上做输入体验不要依赖默认的窗口压缩行为必须主动感知键盘高度再自己去挪布局。这也是标题里“软键盘感知”这四个字的含义。做法上来讲可以走Flutter社区插件也可以自己写Platform Channel把所有键盘事件桥接给Flutter层。1.2 Flutter引擎在鸿蒙上的View层级差异Flutter for OpenHarmony这个项目本质上是Flutter引擎跑在OpenHarmony平台上由OpenHarmony官方的ArkUI容器承载Flutter渲染Surface。这就带来一个很关键的问题Flutter拿到的键盘信息和ArkUI原生控件的键盘信息很可能不在一个频道上。这里说直白一点你输入框是Flutter渲染的但软键盘是系统窗口弹出来的。系统键盘与Flutter View之间的关系取决于鸿蒙的窗口层级如何挂接。实测中发现当系统键盘弹出时Flutter引擎侧的windowMetrics并不会像Android那样随之刷新onMetricsChanged事件不触发所以UI层完全感知不到键盘动作。这个现象不是插件能解决的插件只是把原生键盘的显示/隐藏事件转发出来Flutter UI层需要自行处理避让。所以正确的姿势是监听键盘状态拿到高度用Padding或AnimatedContainer把底部区域垫起来必要时配合滚动控制器把焦点项滚到可见区域。1.3 插件选型为什么选flutter_keyboard_visibility鸿蒙侧可以用的方案大概有几类自己写HarmonyOS Plugin通过onKeyboardHeightChanged拿到高度走MethodChannel抛给Flutter。优点是可控缺点是得维护一套原生代码。使用OpenHarmony社区适配过的flutter_keyboard_visibility。这个仓库本身是Flutter社区的老牌插件社区有人做过鸿蒙的平台实现能直接用。如果只是参考高度也可以看看flutter_keyboard_visibility这一类的插件是否会被官方能力替代但就目前来看社区插件是最快的路径。我在项目里选了flutter_keyboard_visibility核心原因是它有现成的keyboardVisibilityController既能监听显隐也能拿到键盘高度比纯手写省事很多。另一个原因是它的API设计足够简单不用改太多业务代码。后面第3部分会完整演示怎么用。2. 环境准备与工具链排查2.1 鸿蒙Flutter开发环境速配先把环境说清楚避免后面代码跑不起来。Flutter for OpenHarmony 不能直接拿Google官方Flutter SDK来用需要用OpenHarmony适配过的SDK。常见做法是拉取对应的flutter仓分支然后通过flutter config指定SDK路径。建议的版本搭配组件推荐版本说明OpenHarmony SDK / DevEco Studio5.x及以上鸿蒙OS Next对应API 10Flutter for OpenHarmony SDK仓库匹配版本建议3.7之后与官方Flutter版本同步推进hdc工具随DevEco类似adb用于安装调试JDK17DevEco Studio依赖环境变量方面OHOS_SDK_HOME要指向DevEco自带的SDK目录hdc建议加到PATH里方便命令行安装APK包。配好之后执行flutter doctor -v能看到OpenHarmony相关入口如果某一步是红色的重点检查SDK路径和config.json对不对。如果你是第一次接触建议用一个全新的空项目跑通一遍再动手改不然很容易分不清是环境问题还是代码问题。2.2 VS Code/Android工具链常见报错现场在VS Code里开发Flutter最闹心的一类问题就是工具链依赖出幺蛾子。比如创建完项目直接跑构建有时会弹出类似“unable to find suitable visual studio toolchain”的报错虽然这是在Windows上编译Windows插件时才会遇到的经典错误但很多人会把场景搞混以为自己要装Visual Studio。这里说下我的排查心得先确认你当前激活的设备/编译目标是什么。flutter devices如果看到的是Windows桌面那编译时确实需要VS构建工具如果目标是鸿蒙设备这个报错一般不该来。如果在编译OpenHarmony目标时还冒出这类错误多半是某个原生依赖触发到了host构建逻辑可以检查插件目录里是否混入了纯Flutter插件不支持的平台代码。另一个高频错误是“you are applying flutters main gradle plugin imperatively using the apply script”这是Gradle插件应用方式的问题。遇到时去看android/build.gradle或settings.gradle里插件声明方式把apply script改写成Gradle Plugin DSL通常就能过。这俩问题不解决后面所有软键盘的工作都无从谈起先花十分钟把构建链路跑通是值得的。2.3 fvm管理多版本Flutter做鸿蒙适配最烦的一个点就是版本对齐。项目可能同时依赖官方Flutter SDK开发其他业务又要用OpenHarmony的Flutter SDK跑鸿蒙壳两套SDK来回切很容易乱。我用的是fvmFlutter Version Management它能把不同版本的Flutter SDK隔离到项目级.fvmrc里。核心命令就几个fvm install 3.22.0-ohos fvm use 3.22.0-ohos fvm flutter pub get fvm flutter run -d ohos设备好处是项目成员拉下仓库后执行fvm install就能自动装到.fvmrc指定的版本不会出现一个人能跑另一个人跑不了的情况。如果在切SDK版本后出现依赖解析失败优先清pubspec.lock再重新pub get同时把build目录删掉重编一次。鸿蒙的构建产物和Android的在增量编译时偶尔会互相干扰干净构建可以屏蔽掉很大一部分玄学问题。3. 接入flutter_keyboard_visibility实操3.1 插件依赖与平台注册在pubspec.yaml里添加依赖如果项目走的是OpenHarmony适配版仓库直接用dependencies: flutter_keyboard_visibility: git: url: https://gitee.com/your_mirror/flutter_keyboard_visibility.git ref: main如果你用的是社区已经发布到pub上的版本直接写flutter_keyboard_visibility: ^x.y.z也行。这里提醒一句鸿蒙适配版可能存在主仓库没有的补丁尽量用Gitee上的fork版本别纠结版本号。然后执行fvm flutter pub get插件类型的项目不需要额外注册但如果你是自己写的Platform Channel需要在鸿蒙原生工程里注册对应的Plugin。用现成插件的话一般在flutter_plugins文件里会自动生成注册代码不需要手工改动。3.2 核心API监听键盘状态插件提供的核心类是KeyboardVisibilityController通过FlutterKeyboardVisibilityPlugin获取实例。import package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart; final controller KeyboardVisibilityController();常用的监听方式有两种。第一种监听显隐状态StreamSubscriptionbool? _subscription; void _watchKeyboardVisibility() { _subscription controller.onChange.listen((isVisible) { debugPrint(键盘是否显示: $isVisible); }); }第二种获取实时高度double _keyboardHeight 0; void _watchKeyboardHeight() { controller.onKeyboardHeightChanged.listen((height) { setState(() { _keyboardHeight height; }); }); }这里要重点说一下API层面的差异。onChange只告诉你显隐onKeyboardHeightChanged会给你具体高度。避让场景下一定要拿高度因为不同输入法的高度不一样有一部分高度是给候选词栏的也有一部分是给底部手势条的这些全都得算进来。3.3 避让策略写一个可复用的KeyBoardAwareScaffold直接在每个页面里监听高度再改布局代码会散得到处都是。我的做法是封装一个KeyBoardAwareScaffold把键盘感知和避让逻辑收敛到一处业务页面只管往里塞内容。class KeyBoardAwareScaffold extends StatefulWidget { const KeyBoardAwareScaffold({ super.key, required this.body, this.bottomBar, }); final Widget body; final Widget? bottomBar; override StateKeyBoardAwareScaffold createState() _KeyBoardAwareScaffoldState(); } class _KeyBoardAwareScaffoldState extends StateKeyBoardAwareScaffold { final _controller KeyboardVisibilityController(); double _keyboardHeight 0; StreamSubscriptiondouble? _subscription; override void initState() { super.initState(); _subscription _controller.onKeyboardHeightChanged.listen((height) { if (mounted) { setState(() _keyboardHeight height); } }); } override void dispose() { _subscription?.cancel(); super.dispose(); } override Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: false, body: Padding( padding: EdgeInsets.only(bottom: _keyboardHeight), child: widget.body, ), bottomNavigationBar: widget.bottomBar null ? null : AnimatedPadding( duration: const Duration(milliseconds: 150), padding: EdgeInsets.only(bottom: _keyboardHeight), child: widget.bottomBar, ), ); } }几个设计为什么要这样定的理由resizeToAvoidBottomInset: false关闭Flutter默认的bottom inset机制让避让完全由自己控制。这一步在鸿蒙上不是必须的因为默认可能已经失效但建议显式关掉避免出现双重避让或布局抖动。Padding用EdgeInsets.only(bottom: _keyboardHeight)把整个内容往上垫这是最简单可靠的避让手法。AnimatedPadding只包在底部工具条上避免整个页面因为高度变化出现过度动画造成性能损耗。使用的时候return KeyBoardAwareScaffold( bottomBar: _buildInputBar(), body: _buildMessageList(), );3.4 键盘高度参与动画与滚动联动的细节避让不只是把底部垫高还需要考虑两个联动一是跟滚动联动。比如一个聊天页面键盘弹出来后如果列表底部在最后一条消息避让后的可视区域应立即滚动到最底部。实现思路是监听键盘高度变化的回调在高度从0变成非0时用ScrollController.animateTo滚到maxScrollExtent。void _handleKeyboardHeight(double height) { final isKeyboardOpen height 0; if (isKeyboardOpen _scrollController.hasClients) { WidgetsBinding.instance.addPostFrameCallback((_) { _scrollController.animateTo( _scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 200), curve: Curves.easeOut, ); }); } }这里有个细节animateTo最好放在addPostFrameCallback里因为setState触发重建后滚动视图的高度才会更新到最终值如果直接计算可能在旧约束上发生跳变。二是跟输入法候选栏的高度联动。有些输入法的onKeyboardHeightChanged已经包含了候选栏高度有些是分两次回调。实测中可以打日志观察如果发现在不同的输入法下有高度波动可以做一个平滑过渡动画AnimatedContainer( duration: const Duration(milliseconds: 100), curve: Curves.easeOut, height: _keyboardHeight, )动画时长不要拉太长软键盘本身的弹出动画大约200ms左右如果你自己的垫高动画比它还慢就会出现内容先被盖住后弹出来的劣质感。100ms到150ms是一个比较舒服的区间。4. 鸿蒙OS Next适配中的真实坑与排查4.1 键盘回调不触发的定位思路接入插件后第一个可能会遇到的现象是键盘明明弹出来了onKeyboardHeightChanged就是不触发。先别急着换插件按这个顺序定位确认插件版本是鸿蒙适配版而不是原版。原版没有鸿蒙原生实现MethodChannel调到原生层找不到实现表现为完全没回调。确认工程里有没有遗漏插件注册。打开linux、ohos等平台的生成文件看有没有自动生成插件注册代码。如果缺失手动注册FlutterKeyboardVisibilityPlugin。在鸿蒙原生层看有没有权限或窗口监听相关的限制。部分系统键盘事件需要Window.setWindowKeyboardPreferredMode这类能力配合插件内部如果在初始化时没拿到窗口实例回调就发不出来。如果不想改原生代码还有一种土办法用WidgetsBindingObserver监听didChangeMetrics查一下View.of(context).viewInsets.bottom的变化。虽然不是所有鸿蒙版本都会上报但有些版本在键盘弹出后这个值是会变的可以作为双保险。class KeyboardObserver with WidgetsBindingObserver { override void didChangeMetrics() { super.didChangeMetrics(); final view WidgetsBinding.instance.platformDispatcher.views.first; final bottom view.viewInsets.bottom; debugPrint(didChangeMetrics bottom inset: $bottom); } }如果这个也拿不到那就彻底只能依靠插件原生桥接了。4.2 键盘高度异常或为0另一个高频问题事件确实触发了但高度是0或者高度比实际键盘矮一截。这种情况多半是插件的原生实现拿到的“键盘区域”和实际可视区域之间存在偏差。鸿蒙系统的键盘高度在onKeyboardHeightChanged里通常是相对应用窗口的高度而不是屏幕高度。如果应用处于分屏状态或者窗口高度被系统导航栏挤占拿到的高度就会偏小。我的处理方式是在键盘弹出时用MediaQuery.of(context).size.height减去当前viewInsets或插件拿到的可见高度得到一个修正值。具体修正多少需要真机测试。鸿蒙OS Next在不同输入法下的行为有差异建议在设置里切换一下自带输入法和第三方输入法对比记录实际高度做偏差校准。另外提一句横屏场景下键盘高度会明显缩水甚至只有半屏高度。如果你的应用允许横屏避让逻辑里要把最大垫高限制在屏幕高度的一半左右避免出现诡异的空白区域。4.3 横竖屏切换与折叠屏状态同步鸿蒙OS Next支持折叠屏展开状态下窗口尺寸会变化。如果软件键盘开着的时候用户折叠或旋转了设备插件回调的键盘高度和窗口尺寸会交错到达很容易造成布局瞬间闪烁。我用的策略是给键盘高度监听加一层去抖Timer? _debounce; void _onKeyboardHeightChanged(double height) { _debounce?.cancel(); _debounce Timer(const Duration(milliseconds: 80), () { if (mounted) { setState(() _keyboardHeight height); } }); }去抖不是延迟键盘反应而是避免在短时间内多次回调造成布局抖动。80ms的延迟用户基本感知不到但对稳定性提升很大。同时在didChangeMetrics里监听窗口尺寸变化当尺寸变化时重置键盘高度为0等新的键盘事件重新上报之后再更新。这么做的原因是尺寸变化时键盘是否保持弹出是一个不确定状态先归零再更新避免脏数据残留。4.4 画面渲染异常与内存优化在鸿蒙上跑Flutter另一个弹得很多的坑是画面渲染异常。比如键盘弹出后花屏、黑块、或者Surface没刷新。这个问题跟Flutter引擎与ArkUI的Surface共享方式有关跟插件本身关系不大但键盘弹起会频繁触发Surface重建放大了问题出现概率。经验上可以先做两件事第一避免在键盘高频率回调里做重型计算或setState大Widget树。键盘从弹出到稳定的过程中onKeyboardHeightChanged可能连续回调十几次如果每次都重建整个列表渲染压力会很大。建议用ValueNotifierdouble代替setState只让涉及高度的Widget监听变化。final keyboardHeightNotifier ValueNotifierdouble(0); // 监听 keyboardHeightNotifier.value height; // 在需要避让的地方 ValueListenableBuilderdouble( valueListenable: keyboardHeightNotifier, builder: (context, height, _) { return Padding( padding: EdgeInsets.only(bottom: height), child: content, ); }, )第二开启Flutter的内存优化措施。特别是在列表页用ListView.builder而不是一次性构建所有子项。键盘避让导致的布局抖动很多时候不是键盘逻辑问题而是列表重建太频繁把内存和渲染线程都打满了导致的。适当用RepaintBoundary隔离局部重绘也能减少渲染次数。如果出现持续性的渲染异常可以尝试清理鸿蒙原生侧的缓存Surface或者升级Flutter for OpenHarmony引擎版本。老版本引擎在Surface重建处理上确实存在缺陷升级之后大概率能解决。5. 几个容易被忽略的适配细节5.1 键盘避让与系统导航栏的关系鸿蒙OS Next支持手势导航和传统三键导航不同模式对键盘可见高度的影响不同。手势导航下屏幕底部会有一条横线或手势条这部分区域在键盘弹出后可能仍然可视导致避让高度多算了一段内容整体偏高。我的处理经验是键盘高度回调出来之后减掉底部导航栏高度。导航栏高度可以通过鸿蒙原生的Window获取如果暂时拿不到就用一个常量先垫着等后续版本完善再替换。5.2 多输入框页面焦点切换如果页面上有多个输入框用户从A框切到B框键盘高度一般不变但焦点切换会导致滚动位置错乱。避让逻辑不应该只在键盘高度变化时触发也要监听焦点变化。可以给每个输入框绑定FocusNode在监听里判断当前焦点输入框的位置是否被遮挡如果是就滚动到可见区域。这块写起来比较繁琐但体验差距很大尤其是在表单页。5.3 插件不可用时的降级方案再稳定的插件也架不住系统版本升级带来的兼容性问题。如果哪天flutter_keyboard_visibility在鸿蒙新版本上彻底失灵不用慌降级方案就是自己写一个轻量MethodChannel。原生侧注册一个KeyboardHeightPlugin在ArkUI侧监听键盘事件把高度传给Dart侧Dart侧用EventChannel接收。整体工作量不大效果可控。const EventChannel _channel EventChannel(com.example/keyboard_height); void _listenNativeKeyboardHeight() { _channel.receiveBroadcastStream().listen((event) { final height (event as num).toDouble(); keyboardHeightNotifier.value height; }); }这种方式的优点是完全自己掌控不受第三方插件更新节奏影响。缺点是你得维护原生代码还要处理不同鸿蒙版本的API差异。好在鸿蒙的键盘事件API相对稳定写一次能用很久。我个人在实际操作中的体会是软键盘避让这种问题表面看是“加个插件”就能解决实际上牵扯到窗口体系、输入法生态、Flutter引擎的渲染机制每一项都得花时间摸一遍。如果时间充裕建议先在鸿蒙原生工程里把键盘高度打出来观察几个输入法在不同场景下的行为再把这套逻辑搬到Flutter层这样思路会清晰很多。最后再分享一个小技巧调试键盘高度时别只看固定日志建议把键盘高度实时渲染在屏幕角落改动一下输入法设置、切换一次横竖屏观察它怎么变这比反复打日志高效多了。把这一步做扎实后面写避让逻辑的时候会顺畅很多。
返回列表