最近在开发鸿蒙应用里的聊天页面时,遇到了键盘交互的一系列问题:键盘弹出时底部输入框需要跟着上推,点击消息列表空白处要能收起键盘,发送消息后也要自动收起。原以为就是简单的监听事件,真正实现时才发现坑比想象中多。这里把基于 ASCF 的键盘 API 使用经验整理出来,希望能帮到同场景的开发者。
先看 ASCF 提供的键盘 API,一共就四个,数量不多,但覆盖了键盘监听、手动收起、获取光标位置和移除监听这几个核心能力。
首先是最常用的键盘高度监听。聊天页面最基本的需求就是键盘弹出时,输入框紧贴键盘,消息列表自动滚动到底部。实现的第一步是获取键盘高度。
- has.onKeyboardHeightChange(function(res) {
- console.info('键盘高度:', res.height);
- // height 单位是 px,键盘弹出时 > 0,收起时 = 0
- });
复制代码
onKeyboardHeightChange 注册一个回调,每次键盘高度变化都会触发。res.height 是键盘的像素高度,键盘弹出时是正数,收起时是 0。这里有个容易忽略的细节:这个回调不是只在键盘完全弹出后触发一次,而是实时触发的——键盘弹出动画的过程中也会不断回调。这个特性可以用于做跟手动画,但如果不加处理,频繁触发可能会带来性能问题。
在聊天页中的常见用法是:页面 onReady 时注册监听,onUnload 时移除;回调里 setData 更新键盘高度,如果高度大于 0 则滚动消息列表到底部。
- Page({
- data: {
- keyboardHeight: 0,
- inputValue: '',
- messageList: [], // 消息列表
- },
- onReady() {
- this.startKeyboardListener();
- },
- onUnload() {
- // 页面销毁时记得移除监听
- has.offKeyboardHeightChange();
- },
- startKeyboardListener() {
- let that = this;
- has.onKeyboardHeightChange(function(res) {
- that.setData({ keyboardHeight: res.height });
- // 键盘弹出时自动滚到底部
- if (res.height > 0) {
- that.scrollToBottom();
- }
- });
- },
- scrollToBottom() {
- // 滚动到消息列表底部的逻辑
- // 实际项目里用 scroll-view 的 scroll-into-view
- },
- });
复制代码
模板里底部输入框的容器用键盘高度做偏移:
- <view class="input-bar" style="margin-bottom: {{keyboardHeight}}px;">
- <input class="msg-input" placeholder="输入消息" value="{{inputValue}}" bindinput="onInput" />
- <button size="mini" bindtap="sendMessage">发送</button>
- </view>
复制代码
这样键盘弹起来时,输入框就能跟着往上推。
然后是手动收起键盘。某些场景需要程序主动收起键盘,比如用户点击消息列表空白区域、点击发送按钮之后。has.hideKeyboard 没有参数,调用就完事,有 success/fail/complete 三个回调。
- has.hideKeyboard({
- success: () => {
- console.info('键盘已收起');
- },
- fail: (err) => {
- console.error('收起键盘失败:', err);
- },
- complete: () => {
- console.info('hideKeyboard 调用完成');
- }
- });
复制代码
点击空白区域收起键盘的交互很常见:消息列表容器加 bindtap 事件,在事件回调里调 hideKeyboard。
- <scroll-view scroll-y="true" class="message-list" bindtap="onTapMessageList">
- <!-- 消息列表 -->
- </scroll-view>
复制代码- onTapMessageList() {
- has.hideKeyboard({
- success: () => {
- console.info('点击空白区域,键盘已收起');
- }
- });
- }
复制代码
发送后收起键盘也有顺序问题。原实现里先 setData 清空输入框再 hideKeyboard,发现有时候键盘收不起来;改成先 hideKeyboard 再 setData 清空,就稳定了。具体原因没有深究,推测是 setData 触发了输入框重新渲染,干扰了收起操作。
- sendMessage() {
- let that = this;
- if (!that.data.inputValue.trim()) return;
- // 发消息的逻辑...
- let msg = that.data.inputValue;
- // 发完收起键盘
- has.hideKeyboard({
- success: () => {
- that.setData({ inputValue: '' });
- console.info('消息已发送,键盘已收起');
- }
- });
- }
复制代码
除了高度和收起,还有一个经常被忽略的 API:getSelectedTextRange。它可以获取输入框里光标的当前位置,返回起始和结束位置。做文本编辑、插入表情、@某人这些功能时会用到。
- has.getSelectedTextRange({
- success: (res) => {
- console.info('光标起始位置:', res.start);
- console.info('光标结束位置:', res.end);
- // start == end 表示没有选中文本,光标在 start 位置
- // start != end 表示选中了 start 到 end 之间的文本
- },
- fail: (err) => {
- console.error('获取光标位置失败:', err);
- }
- });
复制代码
这个 API 有个前提条件:输入框必须处于 focus 状态。如果输入框没有获取焦点,调用会直接走 fail 回调。实践中有开发者两次踩坑:一次是在页面 onReady 直接调,一次是输入框还没 focus 就调,结果都是 fail。api 名字里有 "Selected",暗示必须有选中态(focus 也是一种选中态)。正确做法是在输入框 focus 事件后再调用,必要时用 setTimeout 延迟 100ms 确保 focus 已完成。
- Page({
- data: {
- cursorStart: 0,
- cursorEnd: 0,
- },
- onInputFocus() {
- // 输入框获取焦点后获取光标位置
- let that = this;
- setTimeout(() => {
- has.getSelectedTextRange({
- success: (res) => {
- that.setData({
- cursorStart: res.start,
- cursorEnd: res.end
- });
- console.info('光标位置:', res.start, '-', res.end);
- }
- });
- }, 100); // 延迟一下确保 focus 完成
- },
- });
复制代码
有了光标位置,就能实现在当前光标处插入文本,比如聊天输入框里插入表情。
- insertEmoji(emoji) {
- let that = this;
- has.getSelectedTextRange({
- success: (res) => {
- let input = that.data.inputValue;
- let pos = res.start;
- // 在光标位置插入表情
- let newInput = input.slice(0, pos) + emoji + input.slice(pos);
- that.setData({ inputValue: newInput });
- },
- fail: () => {
- // 光标位置获取失败,直接追加到末尾
- that.setData({ inputValue: that.data.inputValue + emoji });
- }
- });
- }
复制代码
同时也可以利用 start === end 判断是否选中了文本:相等表示没有选中,不相等表示选中了一段。
- has.getSelectedTextRange({
- success: (res) => {
- if (res.start === res.end) {
- console.info('没有选中文本,光标在位置:', res.start);
- } else {
- console.info('选中了位置', res.start, '到', res.end, '的文本');
- }
- }
- });
复制代码
调试键盘交互时,写一个可视化 Demo 很有帮助。实时显示键盘弹出的高度、触发次数和事件日志,能直观看到键盘的变化频率。
- Page({
- data: {
- keyboardHeight: 0,
- maxHeight: 0,
- eventCount: 0,
- logList: [],
- },
- onReady() {
- this.startKeyboardListener();
- },
- onUnload() {
- has.offKeyboardHeightChange();
- },
- startKeyboardListener() {
- let that = this;
- has.onKeyboardHeightChange(function(res) {
- let count = that.data.eventCount + 1;
- let maxH = Math.max(that.data.maxHeight, res.height);
- that.setData({
- keyboardHeight: res.height,
- maxHeight: maxH,
- eventCount: count,
- });
- // 记录日志
- let log = '第' + count + '次: 高度=' + res.height + 'px';
- if (res.height > 0 && that.data.keyboardHeight === 0) {
- log += ' [键盘弹出]';
- } else if (res.height === 0) {
- log += ' [键盘收起]';
- }
- that.addLog(log);
- });
- },
- addLog(msg) {
- const time = new Date().toLocaleTimeString();
- const logList = this.data.logList;
- logList.unshift('[' + time + '] ' + msg);
- if (logList.length > 20) {
- logList.pop();
- }
- this.setData({ logList: logList });
- },
- });
复制代码
模板里用一个色块表示键盘高度,高度按比例缩小方便展示:
- <view class="height-bar" style="height: {{keyboardHeight / 4}}px;">
- <text class="height-text">{{keyboardHeight}}px</text>
- </view>
复制代码
事件日志对调试特别有用。键盘什么时候弹出、什么时候收起、高度变化的频率都能从日志里看到。日志结构可以保持最新的在前面,最多保留 20 条。
- // 事件日志的结构
- {
- logList: [], // 日志数组,最新的在前面
- addLog(msg) {
- const time = new Date().toLocaleTimeString();
- const logList = this.data.logList;
- logList.unshift('[' + time + '] ' + msg);
- if (logList.length > 20) {
- logList.pop();
- }
- this.setData({ logList: logList });
- },
- clearLog() {
- this.setData({ logList: [] });
- },
- }
复制代码
模板里渲染日志列表:
- <view class="log-list" if="{{logList.length > 0}}">
- <text class="log-item" for="{{logList}}">{{$item}}</text>
- </view>
- <text class="log-empty" if="{{logList.length === 0}}">暂无日志</text>
复制代码
最后是移除监听。has.offKeyboardHeightChange 有两种用法:不传参数时移除所有键盘高度监听;传函数时移除指定的监听。
- // 移除所有键盘高度监听
- has.offKeyboardHeightChange();
- // 移除指定的监听函数
- function myCallback(res) {
- console.info('键盘高度:', res.height);
- }
- has.onKeyboardHeightChange(myCallback);
- // 之后移除这个特定的监听
- has.offKeyboardHeightChange(myCallback);
复制代码
这个 API 的起始版本是 1.0.21,比 onKeyboardHeightChange 晚不少。早期版本只能注册不能移除,所以在老版本里页面切换时可能残留监听。兼容方案是利用标志位控制回调逻辑:
- let isListenerActive = true;
- has.onKeyboardHeightChange(function(res) {
- if (!isListenerActive) return;
- // 处理键盘高度变化
- });
- // 需要"移除"监听时
- isListenerActive = false;
复制代码
踩坑总结一下,比较常见的有这几个:
一是键盘高度回调触发太频繁。键盘弹出动画过程中,onKeyboardHeightChange 会连续触发很多次,如果每次触发都做重渲染(比如 setData 整个列表),性能会很差。建议只在最终高度时做滚动,中间的变化用 CSS transition 来平滑过渡。
getSelectedTextRange 必须在 focus 时调用。这个坑容易反复踩,记住入框没有获取焦点时调用会直接走 fail 回调。
hideKeyboard 和 setData 的顺序要注意。先 hideKeyboard 再 setData 清空输入框,顺序反了可能键盘收不起来。虽然没法 100% 确认是框架问题还是写法问题,但调整顺序后确实稳定了。
输入框类型对键盘高度也有影响。普通 input 和 textarea 弹出的键盘高度可能不一样,尤其是不同设备上。textarea 因为可以换行,键盘布局可能有差异。实测时一定要在真机上测,模拟器的数据不一定准。
顺便提醒几个容易忽略的细节:键盘高度的单位是 px 不是 rpx,做布局适配要换算;onKeyboardHeightChange 在模拟器上可能数据不准,有些模拟器压根不触发回调,真机上测才靠谱,而且不同品牌的手机键盘高度差异挺大。后续可以试试配合 animation API 做键盘弹出的平滑过渡动画,效果会比直接设 margin 好很多。
键盘交互看着简单,真正做起来有不少细节。把这些 API 都摸一遍,聊天、评论、表单这类有输入交互的页面会少踩很多坑。 |