1. 效果
iOS app
2. 背景
公司产品最近提了个bug需求,聊天界面在发送一次消息后,键盘会收起,期望是:点击发送消息后,键盘不收起。得到这个需求时,组长就跟我说过这个需求不好做,真的做了后才发现到处是坑,断断续续做了4天,网上资料也找了并试了一堆,都是按下葫芦起了瓢,就Android端的APP符合要求,第4天准备下班了,突然就找到思路了,最终四个场景都符合要求,至于不同机型不同系统版本是否还存在什么问题,就没那条件去试了。
我们这个uniapp项目是app、微信小程序两端的,聊天界面用的是某IM产品提供uniapp版本的demo代码,不知道是技术问题没做好,还是太久没去更新了,修改时问题不断,iOS原生版本是挺符合需求的。(最坑的一点是:消息列表组件用的是view元素,但元素里却挂了一堆scroll-view元素的属性,界面跳转至最新一条消息也是用scroll-view的属性实现。属实把我整迷糊了,官方文档也翻了好几遍也没找到对应属性,搞得我还在怀疑是不是view的某些隐藏属性,但官方没列出来。让我多进了几个坑,后面的解决方案也是从这方面入手)
3. 主要遇到问题及尝试失败的解决
(1)小程序端使用设置focus属性的方式去处理,出现闪动。
这个方案每发送一次消息,都会有键盘收起再弹起的情况,这种方案被否定。
(2)iOS键盘遮挡输入框、消息列表最新几条消息。
ios键盘弹起时是符合要求的(输入框跟随弹起、输入框上显示的也是消息列表的最新一条数据),发送一条消息后,输入框(用的是fixed)、消息列表都掉下去被键盘遮住,多发几条消息后,输入框会移上来回到正确位置,消息列表的消息虽然会往上滚动,但最新几条被键盘遮住。
主要就是在输入框位置显示有问题、消息列表最新数据被键盘遮住(显示在手机屏幕底部)、发送消息时消息列表中的数据未向上滚动这三个问题中反复摇摆。
列出部分尝试:
- 将adjust-position设置为false,通过uni.onKeyboardHeightChange监听键盘弹出得到键盘高度,从而动态给输入框设置bottom属性的值,给消息列表设置底部内边距。(结果:输入框位置能正确显示,但键盘弹起时向上滑动消息列表,输入框会跟着界面向上滑动;消息列表顶不上去,只会往下增加空白页面)
- 为处理第1步中输入框会滑动的问题,尝试通过监听最外层元素的touchmove、touchstart事件,在事件触发时调用uni.hideKeyboard()将键盘收起。(结果:键盘弹出后的第1次滑动、点击时,事件没有触发,好像有层膜存在一样,输入框继续跟着上滑;再次滑动、点击,事件触发,键盘关闭)
4. 最终解决
主要是使用scroll-view元素来实现消息列表界面,输入框使用fixed定位。之前虽然在demo代码的view上看到一堆scroll-view元素的属性,也怀疑对方之前用的是scroll-view,但为了通过onPullDownRefresh实现下拉加载历史消息功能,而改成了view。这我也能理解,毕竟使用scroll-view,onPullDownRefresh就无法被触发,也没去细研究scroll-view中的相关属性,毕竟公司的主要项目是在PC端,app、微信小程序能用就行,会uniapp的基础语法就够用了(吐槽一下,被这些问题折磨的不轻)。
言归正传,主要要实现的是:
1、小程序发送消息后,键盘不收起,也不接收键盘收起再弹起。
2、iOS键盘弹起时输入框不被遮住、消息列表能看到最新一条。
目标1解决:
在小程序中将textarea的hold-keyboard设置为true。
此时能实现目标1要求,但还需要保留点击输入框、发送按钮之外的地方,键盘收起功能。
给最外层元素、输入框、发送按钮分别绑定touchend事件,在点击界面中时,最外层的事件会触发(handleTouchEnd),此时调用uni.hideKeyboard()将键盘收起。
如果点击的是输入框、发送按钮,它两的事件的触发会先于最外层元素的事件的触发,在它两事件触发时,通过一个标记holdKeyboardFlag变量,让uni.hideKeyboard()不执行即可。
为什么用touchend事件,而不用touchstart等其他事件,这是因为@touchend.prevent="sendMessage"可以使输入框在点击发送后,输入框重新获得焦点并且不会出现闪动(虽然只对app、h5有用,对小程序无效)。为了不再增加额外的处理,就统一用touchend。
目标2解决:
view元素替换成scroll-view元素后就能实现目标要求。但这样会出现无法下拉加载历史消息的问题,所以要再去实现下拉加载功能。实现代码,如下图:
还有界面滚动至最新一条消息时的一个小坑就不写了,直接看示例代码,项目的代码是分了好几个组件,示例代码主要展示的是核心实现。样式什么的能用就行。
以下是重新整理后的示例代码:
{{item.msg}} 发送 export default { name: 'ChatDemo', data() { return { isFresh:false, // 设置当前下拉刷新状态,true 表示下拉刷新已经被触发,false 表示下拉刷新未被触发 freshing:false, intoViewId: "", chatMsgList: [], inputMessage: "", holdKeyboard: false, // focus时,点击页面的时候不收起键盘 holdKeyboardFlag: true, // 是否在键盘弹出,点击界面时关闭键盘 } }, created() { // 针对小程序键盘收起问题处理 // #ifdef MP-WEIXIN this.holdKeyboard = true // #endif }, mounted() { this.init() }, methods: { init() { for (let i = 0; i { // 键盘弹出时点击界面则关闭键盘 if (this.holdKeyboardFlag) { uni.hideKeyboard() } this.holdKeyboardFlag = true }, 50) // #endif }, // 自定义下拉刷新被触发 scrollRefresh(){ if (this.freshing) return; this.freshing = true; this.isFresh = true; this.$emit('refresh'); this.getHistoryMsg() }, // 自定义下拉刷新被复位 onRestore(){ this.isFresh = false }, // 获取历史数据 getHistoryMsg() { const mid = '' this.handleScrollIntoView(mid) }, // 刷新界面数据 async refreshMsg() { const mid = this.chatMsgList[this.chatMsgList.length - 1].mid // 目标元素id // 跳到最后一条 this.handleScrollIntoView(mid) }, sleep(num = 1, step = 50) { return new Promise((resolve) => { setTimeout(() => { resolve() }, num * step) }) }, // 查找目标元素 querySelectEl(markers) { const that = this return new Promise((resolve) => { uni.createSelectorQuery().in(that).select(markers).boundingClientRect((container) => { console.log(container, 888); const flag = container ? true : false resolve(flag) }).exec() }) }, // 滚动至目标元素位置 async handleScrollIntoView(intoViewId) { let flag = false for (let i = 0; i { this.intoViewId = intoViewId }) }, // 点击输入框、发送按钮时,不收键盘 handleNoHideKeyboard() { // #ifdef MP-WEIXIN // this.$emit('noHideKeyboard') this.holdKeyboardFlag = false // #endif }, // 发送消息 sendMessage() { this.handleNoHideKeyboard() // 发送消息代码 this.chatMsgList.push({ mid: `msg${this.chatMsgList.length}`, msg: `新消息${this.inputMessage}` }) this.inputMessage = '' this.refreshMsg() }, } } .page { width: 100%; height: 100%; } .area-msglist { width: 100vw; height: 100vh; .msglist { background-color: #FAFAFA; height: calc(100vh - 80rpx); } } .message { padding: 20rpx; margin-top: 40rpx; line-height: 3; text-align: end; } /* 解决小程序和app当前界面滚动条不出现的问题 */ ::v-deep ::-webkit-scrollbar { /*滚动条整体样式*/ width: 5px !important; height: 1px !important; overflow: auto !important; background: #ccc !important; -webkit-appearance: auto !important; display: block; } ::v-deep ::-webkit-scrollbar-thumb { /*滚动条里面小方块*/ border-radius: 10px !important; box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2) !important; background: #ccc !important; } ::v-deep ::-webkit-scrollbar-track { /*滚动条里面轨道*/ background: #FFFFFF !important; } .area-input { width: 100%; height: auto; position: fixed; // 用的是fixed布局 bottom: 0; right: 0; z-index: 1; padding: 0; display:flex; align-items:center; background-color: #f2f2f2; } .icon-mic{ width: 22px; height: 22px; padding: 5px 10px; position: relative; top: 2px; } .textarea { width: 100%; font-size: 14px; padding: 0 10px; display: inline-block; margin: 10rpx; line-height: 48rpx; position:relative; top: 0; background-color: #fff; border-radius: 16px; flex: 1; max-height: 200rpx; min-height: 60rpx; } .send-btn { font-size: 10px; background-color: #2196F3; color: #fff; margin-right: 10px; }