鸿蒙ASCF键盘高度监听与手动收起聊天页踩坑实践
最近在开发鸿蒙应用里的聊天页面时,遇到了键盘交互的一系列问题:键盘弹出时底部输入框需要跟着上推,点击消息列表空白处要能收起键盘,发送消息后也要自动收起。原以为就是简单的监听事件,真正实现时才发现坑比想象中多。这里把基于 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 都摸一遍,聊天、评论、表单这类有输入交互的页面会少踩很多坑。
Re: 鸿蒙ASCF键盘高度监听与手动收起聊天页踩坑实践
感谢分享,这些踩坑细节真的很实用。特别是“先隐藏键盘再清空输入框”那个顺序问题,我也遇到过类似的诡异现象,当时怎么调都不稳定,看了你的解释才明白可能是渲染时序干扰了。另外键盘高度实时回调的提醒也很到位,不加防抖确实容易造成性能浪费。后面如果做插入表情或者@功能,光标位置API也很有参考性。学习了!Re: 鸿蒙ASCF键盘高度监听与手动收起聊天页踩坑实践
感谢分享!最近也刚好在做聊天页的键盘适配,ASCF 这套 API 看着确实简单,但实际用起来细节不少。你提到的键盘高度实时回调那个点很关键,动画过程中不断触发,如果不做节流或判断,确实容易造成频繁 setData 的性能问题。 手动收起这块也很有共鸣,我一开始就是先清空输入框再 hideKeyboard,确实偶尔会出现收不起来的情况,改成先收起再清空就稳了。点击空白处收起键盘基本也是标配交互了。 另外 getSelectedTextRange 我之前没太注意,原来可以做光标定位,回头试试看。期待后续有更多深入的踩坑分享!Re: 鸿蒙ASCF键盘高度监听与手动收起聊天页踩坑实践
感谢楼主分享!正好最近也在折腾鸿蒙的聊天页面,键盘这块确实坑不少。特别是那个“实时回调”的细节,不处理的话动画过程里setData太频繁,列表滚动会有点卡。我后来是加了个简单的节流,只在高度变化超过一定阈值时才更新,感觉会好一些。 另外发送后先收起再清空输入框这个顺序问题,我也遇到过类似情况,不过没细查原因。楼主这个经验很实用,下次我直接按这个顺序写,省得再踩一遍。 光标位置那个API还没用过,回头试试看能不能用在@功能上。感谢整理!
页:
[1]