uniapp之app、微信小程序实现输入框发送消息键盘不收起、解决iOS端键盘弹起 ...

立山  金牌会员 | 2024-6-19 19:13:08 | 来自手机 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 839|帖子 839|积分 2517

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元素后就能实现目的要求。但这样会出现无法下拉加载历史消息的问题,所以要再去实现下拉加载功能。实当代码,如下图:


 还有界面滚动至最新一条消息时的一个小坑就不写了,直接看示例代码,项目的代码是分了好几个组件,示例代码主要展示的是核心实现。样式什么的能用就行。
以下是重新整理后的示例代码:
  1. <template>
  2.   <view class="page" @touchend="handleTouchEnd">
  3.     <!-- 消息列表区域 -->
  4.     <view class="area-msglist">
  5.       <!--
  6.         用于替代实现onPullDownRefresh()效果
  7.         :refresher-enabled="true"
  8.         :refresher-threshold="50"
  9.         refresher-default-style="white"
  10.         :refresher-triggered="isFresh"
  11.         refresher-background="#FAFAFA"
  12.         @refresherrestore="onRestore"
  13.         @refresherrefresh="scrollRefresh"
  14.         :enable-back-to-top="true"
  15.       -->
  16.       <scroll-view
  17.         class="msglist"
  18.         :refresher-enabled="true"
  19.         :refresher-threshold="50"
  20.         refresher-default-style="white"
  21.         :refresher-triggered="isFresh"
  22.         refresher-background="#FAFAFA"
  23.         @refresherrestore="onRestore"
  24.         @refresherrefresh="scrollRefresh"
  25.         :enable-back-to-top="true"
  26.         :scroll-y="true"
  27.         upper-threshold="-50"
  28.         :scroll-into-view="intoViewId"
  29.         :scroll-with-animation="true">
  30.         <!-- 具体信息代码 -->
  31.         <!-- mid示例:msg1234545 -->
  32.         <view class="message" v-for="item in chatMsgList" :key="item.mid" :id="item.mid">
  33.           <view>{{item.msg}}</view>
  34.         </view>
  35.       </scroll-view>
  36.     </view>
  37.     <!-- 输入框区域 -->
  38.     <view class="area-input">
  39.       <!-- 发送语音 -->
  40.       <view>
  41.         <image class="icon-mic" src="/package-user/static/voice.png" />
  42.       </view>
  43.       <!-- 输入框 -->
  44.       <textarea
  45.         class="textarea"
  46.         type="text"
  47.         cursor-spacing="65"
  48.         confirm-type='done'
  49.         v-model="inputMessage"
  50.         @confirm="sendMessage"
  51.         @touchend="handleNoHideKeyboard"
  52.         :confirm-hold="true"
  53.         auto-height
  54.         :show-confirm-bar='false'
  55.         :hold-keyboard="holdKeyboard"
  56.         maxlength="300"
  57.       />
  58.       <!-- 表情 -->
  59.       <view>
  60.         <image class="icon-mic" src="/package-user/static/Emoji.png" />
  61.       </view>
  62.       <!-- 更多 -->
  63.       <view v-show="!inputMessage">
  64.         <image class="icon-mic" src="/package-user/static/ad.png" />
  65.       </view>
  66.       <button
  67.         v-show="inputMessage"
  68.         class="send-btn"
  69.         hover-class='hover'
  70.         @touchend.prevent="sendMessage"
  71.       >发送</button>
  72.     </view>
  73.   </view>
  74. </template>
  75. <script>
  76. export default {
  77.   name: 'ChatDemo',
  78.   data() {
  79.     return {
  80.       isFresh:false, // 设置当前下拉刷新状态,true 表示下拉刷新已经被触发,false 表示下拉刷新未被触发
  81.       freshing:false,
  82.       intoViewId: "",
  83.       chatMsgList: [],
  84.       inputMessage: "",
  85.       holdKeyboard: false, // focus时,点击页面的时候不收起键盘
  86.       holdKeyboardFlag: true, // 是否在键盘弹出,点击界面时关闭键盘
  87.     }
  88.   },
  89.   created() {
  90.     // 针对小程序键盘收起问题处理
  91.     // #ifdef MP-WEIXIN
  92.     this.holdKeyboard = true
  93.                 // #endif
  94.   },
  95.   mounted() {
  96.     this.init()
  97.   },
  98.   methods: {
  99.     init() {
  100.       for (let i = 0; i < 12; i++) {
  101.         this.chatMsgList.push({
  102.           mid: `msg${i}`,
  103.           msg: `消息${i}`
  104.         })
  105.       }
  106.       this.refreshMsg()
  107.     },
  108.     // 针对小程序键盘收起问题处理
  109.                 handleTouchEnd() {
  110.                         // #ifdef MP-WEIXIN
  111.                         clearTimeout(this.timer)
  112.                         this.timer = setTimeout(() => {
  113.                                 // 键盘弹出时点击界面则关闭键盘
  114.                                 if (this.holdKeyboardFlag) {
  115.                                         uni.hideKeyboard()
  116.                                 }
  117.                                 this.holdKeyboardFlag = true
  118.                         }, 50)
  119.                         // #endif
  120.                 },
  121.     // 自定义下拉刷新被触发
  122.     scrollRefresh(){
  123.       if (this.freshing) return;
  124.       this.freshing = true;
  125.       this.isFresh = true;
  126.       this.$emit('refresh');
  127.       this.getHistoryMsg()
  128.                 },
  129.     // 自定义下拉刷新被复位
  130.                 onRestore(){
  131.                         this.isFresh = false
  132.                 },
  133.     // 获取历史数据
  134.     getHistoryMsg() {
  135.       const mid = ''
  136.       this.handleScrollIntoView(mid)
  137.     },
  138.     // 刷新界面数据
  139.     async refreshMsg() {
  140.       const mid = this.chatMsgList[this.chatMsgList.length - 1].mid // 目标元素id
  141.       // 跳到最后一条
  142.       this.handleScrollIntoView(mid)
  143.     },
  144.     sleep(num = 1, step = 50) {
  145.       return new Promise((resolve) => {
  146.         setTimeout(() => {
  147.           resolve()
  148.         }, num * step)
  149.       })
  150.     },
  151.     // 查找目标元素
  152.     querySelectEl(markers) {
  153.       const that = this
  154.       return new Promise((resolve) => {
  155.         uni.createSelectorQuery().in(that).select(markers).boundingClientRect((container) => {
  156.           console.log(container, 888);
  157.           const flag = container ? true : false
  158.           resolve(flag)
  159.         }).exec()
  160.       })
  161.     },
  162.     // 滚动至目标元素位置
  163.     async handleScrollIntoView(intoViewId) {
  164.       let flag = false
  165.       for (let i = 0; i < 20; i++) {
  166.         flag = await this.querySelectEl(`#${intoViewId}`)
  167.         if(flag) {
  168.           break
  169.         }
  170.         await this.sleep()
  171.       }
  172.       // 在查询到目标元素存在后,再进行滚动操作
  173.       // 直接用nextTick在iOS端是可以的;但在android端界面重新渲染时,首次可能找不到符合id的元素,导致未能正确滚动
  174.       if (!flag) return
  175.       this.$nextTick(() => {
  176.         this.intoViewId = intoViewId
  177.       })
  178.     },
  179.     // 点击输入框、发送按钮时,不收键盘
  180.     handleNoHideKeyboard() {
  181.       // #ifdef MP-WEIXIN
  182.       // this.$emit('noHideKeyboard')
  183.       this.holdKeyboardFlag = false
  184.       // #endif
  185.     },
  186.     // 发送消息
  187.     sendMessage() {
  188.       this.handleNoHideKeyboard()
  189.       // 发送消息代码
  190.       this.chatMsgList.push({
  191.         mid: `msg${this.chatMsgList.length}`,
  192.         msg: `新消息${this.inputMessage}`
  193.       })
  194.       this.inputMessage = ''
  195.       this.refreshMsg()
  196.     },
  197.   }
  198. }
  199. </script>
  200. <style lang="scss" scoped>
  201. .page {
  202.   width: 100%;
  203.         height: 100%;
  204. }
  205. .area-msglist {
  206.   width: 100vw;
  207.         height: 100vh;
  208.   .msglist {
  209.     background-color: #FAFAFA;
  210.     height: calc(100vh - 80rpx);
  211.   }
  212. }
  213. .message {
  214.         padding: 20rpx;
  215.   margin-top: 40rpx;
  216.   line-height: 3;
  217.   text-align: end;
  218. }
  219. /* 解决小程序和app当前界面滚动条不出现的问题 */
  220. ::v-deep ::-webkit-scrollbar {
  221.   /*滚动条整体样式*/
  222.   width: 5px !important;
  223.   height: 1px !important;
  224.   overflow: auto !important;
  225.   background: #ccc !important;
  226.   -webkit-appearance: auto !important;
  227.   display: block;
  228. }
  229. ::v-deep ::-webkit-scrollbar-thumb {
  230.   /*滚动条里面小方块*/
  231.   border-radius: 10px !important;
  232.   box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2) !important;
  233.   background: #ccc !important;
  234. }
  235. ::v-deep ::-webkit-scrollbar-track {
  236.   /*滚动条里面轨道*/
  237.   background: #FFFFFF !important;
  238. }
  239. .area-input {
  240.   width: 100%;
  241.   height: auto;
  242.         position: fixed; // 用的是fixed布局
  243.         bottom: 0;
  244.         right: 0;
  245.         z-index: 1;
  246.   
  247.   padding: 0;
  248.         display:flex;
  249.         align-items:center;
  250.         background-color: #f2f2f2;
  251. }
  252. .icon-mic{
  253.         width: 22px;
  254.   height: 22px;
  255.         padding: 5px 10px;
  256.         position: relative;
  257.         top: 2px;
  258. }
  259. .textarea {
  260.         width: 100%;
  261.         font-size: 14px;
  262.         padding: 0 10px;
  263.         display: inline-block;
  264.         margin: 10rpx;
  265.         line-height: 48rpx;
  266.         position:relative;
  267.         top: 0;
  268.         background-color: #fff;
  269.         border-radius: 16px;
  270.         flex: 1;
  271.         max-height: 200rpx;
  272.         min-height: 60rpx;
  273. }
  274. .send-btn {
  275.         font-size: 10px;
  276.         background-color: #2196F3;
  277.         color: #fff;
  278.   margin-right: 10px;
  279. }
  280. </style>
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

立山

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表