小程序即时通讯聊天控件(二)

来源:互联网 发布:法国留学知乎 编辑:程序博客网 时间:2024/05/16 12:08

小程序即时通讯——文本、语音输入控件(二)实现原理

集成请看小程序即时通讯聊天控件(一)集成
这个控件的编写主要分为三个部分:文本和语音信息的输入及获取录音时手势操作的处理自定义功能。其中文本信息的输入及获取使用微信官方控件input即可实现,语音输入也是,可以参考录音功能。这部分就是调用个API的事儿,不讲述了。文本和语音状态的切换自定义功能的实现原理也非常简单,这里可以简单讲下。倒是手势操作这块花了一些时间,调试过程中也发现了一些问题,可以跟大家分享下。

先说下输入状态的切换吧

切换输入状态

点击左侧的按钮会切换输入的状态

我把这个控件中用到的所有字段都用inputObj这个对象来管理,在调用chatInput.init(page,opt)方法时就在pagedata对象中初始化了inputObj对象。那么是怎么点击左侧的按钮切换状态的呢?

没错!就是在点击时将inputObj.inputStatus这个字段置为textvoice,然后渲染到布局上,布局使用wx:if来控制对应控件的显示和隐藏。

上代码:

chat-input.wxml布局的代码贴上来排版太乱了,所以我贴代码的时候删除了一些样式。。。把最主要的放了出来

<!--左侧输入状态的图片切换--><image src="../../image/chat/voice/{{inputObj.inputStatus==='voice'?'keyboard':'voice'}}.png"       bindtap="changeInputWayEvent" /><!--控制显示语音输入--><block wx:if="{{inputObj.inputStatus==='voice'}}">    <template is="voice" data="{{voiceObj:inputObj.voiceObj}}" /></block><!--控制显示文本输入--><input wx:if="{{inputObj.inputStatus==='text'}}"       confirm-type="send" value="{{textMessage}}" bindconfirm="chatInputSendTextMessage" />

chat-input.js

//在chat-input.wxml中绑定的changeInputWayEvent事件//该方法在chatInput.init(page,opt)执行时会调用//inputObj.extraObj.chatInputShowExtra这个字段是用于在点击切换按钮时隐藏自定义功能弹窗function initChangeInputWayEvent() {    _page.changeInputWayEvent = function () {        _page.setData({            'inputObj.inputStatus': _page.data.inputObj.inputStatus === 'text' ? 'voice' : 'text',            'inputObj.extraObj.chatInputShowExtra': false        });    }}

小技巧:在调用_page.setData()时,如果传入整个inputObj对象,会刷新布局中与inputObj有关的所有UI,但是如果传入inputObj的某个元素(如inputObj.inputStatus),则只会刷新与该元素有关的所有UI,同时会减少渲染时数据的传入量,从而实现局部刷新,增加渲染速度。

自定义功能与上面的实现方式的思路是一样的。就不再赘述了。

下面重点说下手势操作部分

1. 划分手势操作区域:

首先我们要搭建录音功能的UI,包括底部的录音按钮和录音时显示的弹出窗。按钮的适配很简单,弹出窗的大小和位置也是,但是手势操作向上滑动到一定范围时,需要更新录音的状态为将要取消录音,滑动的区域的限定肯定要通过js来实现,那么就需要引入systemInfo了,引入之后可以在初始化时就配置好这些信息。

具体实现是在chat-input.js,代码如下:
chat-input.js

//这里的windowWidth、windowHeight是在初始化时从systemInfo中获取到的。function initVoiceData() {    let width = windowWidth / 2.6;    _page.setData({        'inputObj.inputStatus': 'text',//初始状态为文本输入模式        'inputObj.windowHeight': windowHeight,//注入可操作区域的总高度        'inputObj.windowWidth': windowWidth,//注入可操作区域的总宽度        'inputObj.voiceObj.status': 'end',//录音为结束状态        'inputObj.voiceObj.startStatus': 0,//录音按钮状态,0:按住说话状态;1:松开结束状态        'inputObj.voiceObj.voicePartWidth': width,//录音时弹出窗的宽度        'inputObj.voiceObj.moveToCancel': false,//是否移动到了取消录音区域        'inputObj.voiceObj.voicePartPositionToBottom': (windowHeight - width / 2.4) / 2,//录音时弹出窗的距离底部的位置,因为弹出窗是正方形的,所以这里是减的width,后面除的2.4是为了调整弹窗在屏幕中的显示位置        'inputObj.voiceObj.voicePartPositionToLeft': (windowWidth - width) / 2//录音时弹出窗的距离左侧位置    });    cancelLineYPosition = windowHeight * 0.12;//向上滑动到距离屏幕底部cancelLineYPosition大小时,进入到了取消录音的区域}

chat-input.wxml中引入了语音模板voice

<import src="voice.wxml" /><template is="voice" data="{{voiceObj:inputObj.voiceObj}}" />

语音功能具体的布局voice.wxml,其实也是通过js中的setData动态的去更新UI,没什么技术含量。
稍微有些技术含量的就是下面代码中button绑定的两个事件(手指在屏幕上移动和离开)的处理。
我们需要在手指移动时判断触摸点的位置是否已经到了取消区域(在距离底部cancelLineYPosition大小以上的位置都是取消区域),在手指离开屏幕时判断录音是否过短。
这里写图片描述

<template name="voice">    <!--这里是最主要的一部分,button绑定了三个事件:屏幕长按、移动和离开,手势操作部分是后面两个事件来处理的-->    <button bind:longtap="long$click$voice$btn" catch:touchmove="send$voice$move$event"    catch:touchend="send$voice$move$end$event" id="send$voice$btn" hover-class="btn-voice-press">{{voiceObj.startStatus?'松开 结束':'按住 说话'}}    </button>    <view wx:if="{{voiceObj.showCancelSendVoicePart}}"          style="width: {{voiceObj.voicePartWidth}}px;height: {{voiceObj.voicePartWidth}}px;display: flex;position: fixed;left: {{voiceObj.voicePartPositionToLeft}}px;bottom: {{voiceObj.voicePartPositionToBottom}}px;justify-content:center;align-items: center;border-radius: 20rpx;">        <view style="background-color:black;opacity:{{voiceObj.status==='timeDown'?0.6:0}};width: 100%;height: 100%;border-radius: 20rpx;position: absolute"/>        <image src="./../../image/chat/voice/{{voiceObj.status==='start'?(voiceObj.moveToCancel?'recall':'speak'):'attention'}}.png" style="width: 100%;height: 100%;border-radius: 20rpx" wx:if="{{voiceObj.status!=='timeDown'}}"/>        <text style="margin-bottom:30rpx;font-size: 150rpx;text-align: center;color: white;position: relative" wx:if="{{voiceObj.status==='timeDown'}}">{{voiceObj.timeDownNum}}</text>        <view class="voice-record-git-status-style" wx:if="{{!voiceObj.moveToCancel&&voiceObj.status!=='short'}}">            <image src="录音时显示的动态图" class="voice-record-git-size-style"/>        </view>        <text class="voice-status-style" style="background-color: {{voiceObj.moveToCancel?'#ab1900':'transparent'}};">{{voiceObj.status==='start'||voiceObj.status==='timeDown'?(voiceObj.moveToCancel?'松开手指,取消发送':'手指上滑,取消发送'):(voiceObj.status==='short'?'说话时间太短':'说话时间超时')}}</text>    </view></template>

2. 移动事件的处理。

_page.send$voice$move$event = function (e) {    if ('send$voice$btn' === e.currentTarget.id) {        let y = windowHeight + tabbarHeigth - e.touches[0].clientY;        if (y > cancelLineYPosition) {            if (!inputObj.voiceObj.moveToCancel) {                _page.setData({                    'inputObj.voiceObj.moveToCancel': true                });            }        } else {            if (inputObj.voiceObj.moveToCancel) {//如果移出了该区域                _page.setData({                    'inputObj.voiceObj.moveToCancel': false                })            }        }    }};

e.touches[0].clientY是当前触摸点的Y轴坐标,正数。小程序的坐标原点在左上角,所以当从下往上滑动时,该参数的值会越来越小。那么我们由windowHeight + tabbarHeigth - e.touches[0].clientY就可以计算出用户滑动屏幕时,以左下角为原点的Y轴坐标了。
将这个y值与cancelLineYPosition进行比较就可以得到用户是否滑到了取消区域。

那么有人会问,为什么要加个tabbarHeigth呢?这是为了解决微信tabbar的一个bug。开发过程中我发现无论是Android还是iOS手机,当有你的小程序有tabbar时,通过wx.getSystemInfo()wx.getSystemInfoSync()获取到的windowHeight值都偏小,恰好缺少一个tabbar的高度。如果不适配的话,会导致一些手机向上滑动还没滑出button的范围,程序就标识为inputObj.voiceObj.moveToCancel = true的状态了。

3. 当用户手指离开屏幕时该怎么处理呢?

当然是要结束录音啦!在结束之前需要先判断下本次的录音时长是否小于最小的录音时长,然后再将用于记录录音时长的计时器关掉。

_page.send$voice$move$end$event = function (e) {    console.log('离开', e);    if ('send$voice$btn' === e.currentTarget.id) {        console.log('时间短', singleVoiceTimeCount, minVoiceTime);        if (singleVoiceTimeCount < minVoiceTime) {//语音时间太短            _page.setData({                'inputObj.voiceObj.status': 'short'            });            delayDismissCancelView();        } else {//语音时间正常            _page.setData({                'inputObj.voiceObj.showCancelSendVoicePart': false,                'inputObj.voiceObj.status': 'end'            });        }        if (timer) {//关闭计时器            clearInterval(timer);        }        endRecord();    }}

录音文件的获取是在wx.startRecord()方法的success回调函数中接收,我是在long$click$voice$btn(button长按事件)触发时调用的,当录音结束后,就会回调wx.startRecord()success

下面的这段代码是在_page.long$click$voice$btn中执行的,用于录音结束后获取文件临时路径。

wx.startRecord({    success: function (res) {        if (_page.data.inputObj.voiceObj.status === 'short') {//录音时间太短或者移动到了取消录音区域, 则取消录音            typeof startVoiceRecordCbOk === "function" && startVoiceRecordCbOk(status.SHORT);            return;        } else if (_page.data.inputObj.voiceObj.moveToCancel) {            typeof startVoiceRecordCbOk === "function" && startVoiceRecordCbOk(status.CANCEL);            return;        }        console.log('录音成功');        typeof startVoiceRecordCbOk === "function" && startVoiceRecordCbOk(status.SUCCESS);        typeof sendVoiceCbOk === "function" && sendVoiceCbOk(res, singleVoiceTimeCount + '');    },    fail:res=>{        typeof startVoiceRecordCbOk === "function" && startVoiceRecordCbOk(status.FAIL);        typeof sendVoiceCbError === "function" && sendVoiceCbError(res);    }});

总结

这个组件到此讲完了,在开发过程中,我将语音、自定义功能各个模块在UI上分离开了,以便后期的维护。其实大部分时间都花在了UI上。手势操作那部分做过移动开发的同学们相信都会有思路,也没有难度。写的有不好的地方,或者大家有什么问题,可以在评论区留言,我会继续改进。
github下载https://github.com/unmagic/wechat-im

原创粉丝点击