查看: 110|回复: 0

鸿蒙ASCF键盘高度监听与手动收起聊天页踩坑实践

[复制链接]
发表于 半小时前 | 显示全部楼层 |阅读模式
最近在开发鸿蒙应用里的聊天页面时,遇到了键盘交互的一系列问题:键盘弹出时底部输入框需要跟着上推,点击消息列表空白处要能收起键盘,发送消息后也要自动收起。原以为就是简单的监听事件,真正实现时才发现坑比想象中多。这里把基于 ASCF 的键盘 API 使用经验整理出来,希望能帮到同场景的开发者。

先看 ASCF 提供的键盘 API,一共就四个,数量不多,但覆盖了键盘监听、手动收起、获取光标位置和移除监听这几个核心能力。

首先是最常用的键盘高度监听。聊天页面最基本的需求就是键盘弹出时,输入框紧贴键盘,消息列表自动滚动到底部。实现的第一步是获取键盘高度。
  1. has.onKeyboardHeightChange(function(res) {
  2.   console.info('键盘高度:', res.height);
  3.   // height 单位是 px,键盘弹出时 > 0,收起时 = 0
  4. });
复制代码

onKeyboardHeightChange 注册一个回调,每次键盘高度变化都会触发。res.height 是键盘的像素高度,键盘弹出时是正数,收起时是 0。这里有个容易忽略的细节:这个回调不是只在键盘完全弹出后触发一次,而是实时触发的——键盘弹出动画的过程中也会不断回调。这个特性可以用于做跟手动画,但如果不加处理,频繁触发可能会带来性能问题。

在聊天页中的常见用法是:页面 onReady 时注册监听,onUnload 时移除;回调里 setData 更新键盘高度,如果高度大于 0 则滚动消息列表到底部。
  1. Page({
  2.   data: {
  3.     keyboardHeight: 0,
  4.     inputValue: '',
  5.     messageList: [], // 消息列表
  6.   },
  7.   onReady() {
  8.     this.startKeyboardListener();
  9.   },
  10.   onUnload() {
  11.     // 页面销毁时记得移除监听
  12.     has.offKeyboardHeightChange();
  13.   },
  14.   startKeyboardListener() {
  15.     let that = this;
  16.     has.onKeyboardHeightChange(function(res) {
  17.       that.setData({ keyboardHeight: res.height });
  18.       // 键盘弹出时自动滚到底部
  19.       if (res.height > 0) {
  20.         that.scrollToBottom();
  21.       }
  22.     });
  23.   },
  24.   scrollToBottom() {
  25.     // 滚动到消息列表底部的逻辑
  26.     // 实际项目里用 scroll-view 的 scroll-into-view
  27.   },
  28. });
复制代码

模板里底部输入框的容器用键盘高度做偏移:
  1. <view class="input-bar" style="margin-bottom: {{keyboardHeight}}px;">
  2.   <input class="msg-input" placeholder="输入消息" value="{{inputValue}}" bindinput="onInput" />
  3.   <button size="mini" bindtap="sendMessage">发送</button>
  4. </view>
复制代码

这样键盘弹起来时,输入框就能跟着往上推。

然后是手动收起键盘。某些场景需要程序主动收起键盘,比如用户点击消息列表空白区域、点击发送按钮之后。has.hideKeyboard 没有参数,调用就完事,有 success/fail/complete 三个回调。
  1. has.hideKeyboard({
  2.   success: () => {
  3.     console.info('键盘已收起');
  4.   },
  5.   fail: (err) => {
  6.     console.error('收起键盘失败:', err);
  7.   },
  8.   complete: () => {
  9.     console.info('hideKeyboard 调用完成');
  10.   }
  11. });
复制代码

点击空白区域收起键盘的交互很常见:消息列表容器加 bindtap 事件,在事件回调里调 hideKeyboard。
  1. <scroll-view scroll-y="true" class="message-list" bindtap="onTapMessageList">
  2.   <!-- 消息列表 -->
  3. </scroll-view>
复制代码
  1. onTapMessageList() {
  2.   has.hideKeyboard({
  3.     success: () => {
  4.       console.info('点击空白区域,键盘已收起');
  5.     }
  6.   });
  7. }
复制代码

发送后收起键盘也有顺序问题。原实现里先 setData 清空输入框再 hideKeyboard,发现有时候键盘收不起来;改成先 hideKeyboard 再 setData 清空,就稳定了。具体原因没有深究,推测是 setData 触发了输入框重新渲染,干扰了收起操作。
  1. sendMessage() {
  2.   let that = this;
  3.   if (!that.data.inputValue.trim()) return;
  4.   // 发消息的逻辑...
  5.   let msg = that.data.inputValue;
  6.   // 发完收起键盘
  7.   has.hideKeyboard({
  8.     success: () => {
  9.       that.setData({ inputValue: '' });
  10.       console.info('消息已发送,键盘已收起');
  11.     }
  12.   });
  13. }
复制代码

除了高度和收起,还有一个经常被忽略的 API:getSelectedTextRange。它可以获取输入框里光标的当前位置,返回起始和结束位置。做文本编辑、插入表情、@某人这些功能时会用到。
  1. has.getSelectedTextRange({
  2.   success: (res) => {
  3.     console.info('光标起始位置:', res.start);
  4.     console.info('光标结束位置:', res.end);
  5.     // start == end 表示没有选中文本,光标在 start 位置
  6.     // start != end 表示选中了 start 到 end 之间的文本
  7.   },
  8.   fail: (err) => {
  9.     console.error('获取光标位置失败:', err);
  10.   }
  11. });
复制代码

这个 API 有个前提条件:输入框必须处于 focus 状态。如果输入框没有获取焦点,调用会直接走 fail 回调。实践中有开发者两次踩坑:一次是在页面 onReady 直接调,一次是输入框还没 focus 就调,结果都是 fail。api 名字里有 "Selected",暗示必须有选中态(focus 也是一种选中态)。正确做法是在输入框 focus 事件后再调用,必要时用 setTimeout 延迟 100ms 确保 focus 已完成。
  1. Page({
  2.   data: {
  3.     cursorStart: 0,
  4.     cursorEnd: 0,
  5.   },
  6.   onInputFocus() {
  7.     // 输入框获取焦点后获取光标位置
  8.     let that = this;
  9.     setTimeout(() => {
  10.       has.getSelectedTextRange({
  11.         success: (res) => {
  12.           that.setData({
  13.             cursorStart: res.start,
  14.             cursorEnd: res.end
  15.           });
  16.           console.info('光标位置:', res.start, '-', res.end);
  17.         }
  18.       });
  19.     }, 100); // 延迟一下确保 focus 完成
  20.   },
  21. });
复制代码

有了光标位置,就能实现在当前光标处插入文本,比如聊天输入框里插入表情。
  1. insertEmoji(emoji) {
  2.   let that = this;
  3.   has.getSelectedTextRange({
  4.     success: (res) => {
  5.       let input = that.data.inputValue;
  6.       let pos = res.start;
  7.       // 在光标位置插入表情
  8.       let newInput = input.slice(0, pos) + emoji + input.slice(pos);
  9.       that.setData({ inputValue: newInput });
  10.     },
  11.     fail: () => {
  12.       // 光标位置获取失败,直接追加到末尾
  13.       that.setData({ inputValue: that.data.inputValue + emoji });
  14.     }
  15.   });
  16. }
复制代码

同时也可以利用 start === end 判断是否选中了文本:相等表示没有选中,不相等表示选中了一段。
  1. has.getSelectedTextRange({
  2.   success: (res) => {
  3.     if (res.start === res.end) {
  4.       console.info('没有选中文本,光标在位置:', res.start);
  5.     } else {
  6.       console.info('选中了位置', res.start, '到', res.end, '的文本');
  7.     }
  8.   }
  9. });
复制代码

调试键盘交互时,写一个可视化 Demo 很有帮助。实时显示键盘弹出的高度、触发次数和事件日志,能直观看到键盘的变化频率。
  1. Page({
  2.   data: {
  3.     keyboardHeight: 0,
  4.     maxHeight: 0,
  5.     eventCount: 0,
  6.     logList: [],
  7.   },
  8.   onReady() {
  9.     this.startKeyboardListener();
  10.   },
  11.   onUnload() {
  12.     has.offKeyboardHeightChange();
  13.   },
  14.   startKeyboardListener() {
  15.     let that = this;
  16.     has.onKeyboardHeightChange(function(res) {
  17.       let count = that.data.eventCount + 1;
  18.       let maxH = Math.max(that.data.maxHeight, res.height);
  19.       that.setData({
  20.         keyboardHeight: res.height,
  21.         maxHeight: maxH,
  22.         eventCount: count,
  23.       });
  24.       // 记录日志
  25.       let log = '第' + count + '次: 高度=' + res.height + 'px';
  26.       if (res.height > 0 && that.data.keyboardHeight === 0) {
  27.         log += ' [键盘弹出]';
  28.       } else if (res.height === 0) {
  29.         log += ' [键盘收起]';
  30.       }
  31.       that.addLog(log);
  32.     });
  33.   },
  34.   addLog(msg) {
  35.     const time = new Date().toLocaleTimeString();
  36.     const logList = this.data.logList;
  37.     logList.unshift('[' + time + '] ' + msg);
  38.     if (logList.length > 20) {
  39.       logList.pop();
  40.     }
  41.     this.setData({ logList: logList });
  42.   },
  43. });
复制代码

模板里用一个色块表示键盘高度,高度按比例缩小方便展示:
  1. <view class="height-bar" style="height: {{keyboardHeight / 4}}px;">
  2.   <text class="height-text">{{keyboardHeight}}px</text>
  3. </view>
复制代码

事件日志对调试特别有用。键盘什么时候弹出、什么时候收起、高度变化的频率都能从日志里看到。日志结构可以保持最新的在前面,最多保留 20 条。
  1. // 事件日志的结构
  2. {
  3.   logList: [], // 日志数组,最新的在前面
  4.   addLog(msg) {
  5.     const time = new Date().toLocaleTimeString();
  6.     const logList = this.data.logList;
  7.     logList.unshift('[' + time + '] ' + msg);
  8.     if (logList.length > 20) {
  9.       logList.pop();
  10.     }
  11.     this.setData({ logList: logList });
  12.   },
  13.   clearLog() {
  14.     this.setData({ logList: [] });
  15.   },
  16. }
复制代码

模板里渲染日志列表:
  1. <view class="log-list" if="{{logList.length > 0}}">
  2.   <text class="log-item" for="{{logList}}">{{$item}}</text>
  3. </view>
  4. <text class="log-empty" if="{{logList.length === 0}}">暂无日志</text>
复制代码

最后是移除监听。has.offKeyboardHeightChange 有两种用法:不传参数时移除所有键盘高度监听;传函数时移除指定的监听。
  1. // 移除所有键盘高度监听
  2. has.offKeyboardHeightChange();
  3. // 移除指定的监听函数
  4. function myCallback(res) {
  5.   console.info('键盘高度:', res.height);
  6. }
  7. has.onKeyboardHeightChange(myCallback);
  8. // 之后移除这个特定的监听
  9. has.offKeyboardHeightChange(myCallback);
复制代码

这个 API 的起始版本是 1.0.21,比 onKeyboardHeightChange 晚不少。早期版本只能注册不能移除,所以在老版本里页面切换时可能残留监听。兼容方案是利用标志位控制回调逻辑:
  1. let isListenerActive = true;
  2. has.onKeyboardHeightChange(function(res) {
  3.   if (!isListenerActive) return;
  4.   // 处理键盘高度变化
  5. });
  6. // 需要"移除"监听时
  7. isListenerActive = false;
复制代码

踩坑总结一下,比较常见的有这几个:

一是键盘高度回调触发太频繁。键盘弹出动画过程中,onKeyboardHeightChange 会连续触发很多次,如果每次触发都做重渲染(比如 setData 整个列表),性能会很差。建议只在最终高度时做滚动,中间的变化用 CSS transition 来平滑过渡。

getSelectedTextRange 必须在 focus 时调用。这个坑容易反复踩,记住入框没有获取焦点时调用会直接走 fail 回调。

hideKeyboard 和 setData 的顺序要注意。先 hideKeyboard 再 setData 清空输入框,顺序反了可能键盘收不起来。虽然没法 100% 确认是框架问题还是写法问题,但调整顺序后确实稳定了。

输入框类型对键盘高度也有影响。普通 input 和 textarea 弹出的键盘高度可能不一样,尤其是不同设备上。textarea 因为可以换行,键盘布局可能有差异。实测时一定要在真机上测,模拟器的数据不一定准。

顺便提醒几个容易忽略的细节:键盘高度的单位是 px 不是 rpx,做布局适配要换算;onKeyboardHeightChange 在模拟器上可能数据不准,有些模拟器压根不触发回调,真机上测才靠谱,而且不同品牌的手机键盘高度差异挺大。后续可以试试配合 animation API 做键盘弹出的平滑过渡动画,效果会比直接设 margin 好很多。

键盘交互看着简单,真正做起来有不少细节。把这些 API 都摸一遍,聊天、评论、表单这类有输入交互的页面会少踩很多坑。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-9-2 11:38 , Processed in 0.022832 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部