小程序即时通讯

作者: layjoy | 来源:发表于2018-08-01 15:19 被阅读106次

    小程序即时通讯——文本、语音输入控件(一)集成

    2018-07-31 作者公告:现在拥有聊天列表UI的项目已经在当前的github仓库中更新了!!!目前还需要一段时间来将多个模块从业务上拆分,现在可以在stage-1.0分支预览。点击前往:聊天列表地址 集成方式文档还没写完。。。

    聊天输入组件

    近期一直在做微信小程序,业务上要求在小程序里实现即时通讯的功能。这部分功能需要用到文本和语音输入及一些语音相关的手势操作。所以我写了一个控件来处理这些操作。

    控件样式

    image.png

    我们先来看下效果 (现在新增了右下角发送按钮!!在输入框获取到焦点后,右下角会显示发送按钮!!只不过没有更新图片。。。)


    小程序.gif

    目前的功能就是动态图中展示的,我们可以使用这个控件来切换输入方式(文本或语音)、获取到输入的信息、取消语音输入、语音消息录制过短过长的判断(该接口暂时还未开放),支持发送图片和其他自定义拓展内容。(语音和图片发送失败是因为小程序新版模拟器的问题,真机上没事)。

    注意:本文所讲的SDK中不包含列表的展示部分及发送状态部分,SDK只测试过微信基础库1.4.0及以上版本。

    这部分内容我会从集成、编写控件两个部分来讲解。毕竟大部分人都是想尽快集成来着,所以先说说集成部分。


    集成

    一、导入SDK相关文件

    导入SDK时一定要用图中所示的路径,不然的话你就自己挨个修改wxml和wxss里面的文件路径哈。

    图片文件总共大概是45kb,chat-input文件夹总共大概41kb。这就是SDK所有的文件,一共在90kb左右。


    二、集成到聊天页面

    1. 在聊天页面中导入chat-input文件
    • 在聊天页的js文件中导入 let chatInput = require('../../modules/chat-input/chat-input');
    • 在聊天页的wxml文件中引入布局
    <import src="../../modules/chat-input/chat-input.wxml"/> 
    <template is="chat-input" data="{{inputObj:inputObj,textMessage:textMessage,showVoicePart:true}}"/>
    
    • 在聊天页的wxss文件中引入样式表@import "../../modules/chat-input/chat-input.wxss";
      根据你的路径来导入这些内容
    2. 初始化chatInput
    chatInput.init(page, {
                systemInfo: wx.getSystemInfoSync(),
                minVoiceTime: 1,//秒,最小录音时长,小于该值则弹出‘说话时间太短’
                maxVoiceTime: 60,//秒,最大录音时长,大于该值则按60秒处理
                startTimeDown: 56,//秒,开始倒计时时间,录音时长达到该值时弹窗界面更新为倒计时弹窗
                format:'mp3',//录音格式,有效值:mp3或aac,仅在基础库1.6.0及以上生效,低版本不生效
                sendButtonBgColor: 'mediumseagreen',//发送按钮的背景色
                sendButtonTextColor: 'white',//发送按钮的文本颜色
                extraArr: [{
                    picName: 'choose_picture',
                    description: '照片'
                }, {
                    picName: 'take_photos',
                    description: '拍摄'
                }, {
                    picName: 'close_chat',
                    description: '自定义功能'
                }],
                tabbarHeigth: 48
            });
    
    • page:这个是指当前的page。
    • systemInfo:必填。手机的系统信息,用于控件的适配。
      -minVoiceTime: 最小录音时长,秒,小于该值则弹出‘说话时间太短’
      -maxVoiceTime: 最大录音时长,秒,填写的数值大于该值则按60秒处理。录音时长如果超过该值,则会保存最大时长的录音,并弹出‘说话时间超时’并终止录音
      -startTimeDown: 开始倒计时时间,秒,录音时长达到该值时弹窗界面更新为倒计时弹窗
    • extraArr:非必填。点击右侧加号时,显示的自定义功能。picName元素的名字就是对应image/chat/extra文件夹下的png格式的图片名称,用于展示自定义功能的图片。description元素用于展示自定义功能的文字说明。
    • tabbarHeight:非必填。这个也是用于适配。如果你的小程序有tabbar,那么需要填写这个字段,填48就行。如果你的小程序没有tabbar,那么就不要填写这个字段。原因我会在第二篇讲到。
    • format: 录音格式,有效值:mp3或aac,仅在基础库1.6.0及以上生效,低版本不生效
    • sendButtonBgColor: 发送按钮的背景色
    • sendButtonTextColor: 发送按钮的文本颜色
    3. 监听获取输入的信息

    在初始化控件之后,监听信息的输入,即可获取到指定类型的信息

    文本信息:
    //文本信息的输入监听
    chatInput.setTextMessageListener(function (e) {
                let content = e.detail.value;//输入的文本信息
    
            });  
    
    语音信息:
    //获取录音之后的音频临时文件
    chatInput.recordVoiceListener(function (res, duration) {
             let tempFilePath = res.tempFilePath;//语音临时文件的路径
             let vDuration = duration;//录音时长
         });
    //监听录音状态
     chatInput.setVoiceRecordStatusListener(function (status) {
                switch (status) {
                    case chatInput.VRStatus.START://开始录音
    
                        break;
                    case chatInput.VRStatus.SUCCESS://录音成功
    
                        break;
                    case chatInput.VRStatus.CANCEL://取消录音
    
                        break;
                    case chatInput.VRStatus.SHORT://录音时长太短
    
                        break;
                    case chatInput.VRStatus.UNAUTH://未授权录音功能
    
                        break;
                    case chatInput.VRStatus.FAIL://录音失败(已经授权了)
    
                        break;
                }
            })
    
    自定义功能:
    //收起自定义功能窗口
    chatInput.closeExtraView();
    
    //自定义功能点击事件
    chatInput.clickExtraListener(function (e) {
                let itemIndex = parseInt(e.currentTarget.dataset.index);//点击的自定义功能索引
                if (itemIndex === 2) {
                    that.myFun();//其他的自定义功能
                    return;
                }
                //选择图片或拍照
                wx.chooseImage({
                    count: 1, // 默认9
                    sizeType: ['compressed'],
                    sourceType: itemIndex === 0 ? ['album'] : ['camera'],
                    success: function (res) {
                        let tempFilePath = res.tempFilePaths[0];
                    }
                });
            });
    //新增右下角加号button点击事件
    chatInput.setExtraButtonClickListener(function (dismiss) {
                console.log('Extra弹窗是否消失', dismiss);
            })
    
    

    至此,输入组件SDK的集成就完成了!

    github地址https://github.com/unmagic/wechat-im

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

    转载请注明出处:https://blog.csdn.net/sinat_27612147/article/details/78476241

    2018-07-30 作者公告:关于大家非常关心的列表UI部分,我近期会更新一期列表UI的控件和教程。我会使用ES6语法来实现,该控件也会进行模块化。最快是一周时间,最慢是两周,集成篇和进阶篇会分别更新在这两篇文章中。使用时开启微信开发工具的ES6转ES5即可。请大家耐心等待。

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

    先说下输入状态的切换吧

    image.png

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

    我把这个控件中用到的所有字段都用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大小以上的位置都是取消区域),在手指离开屏幕时判断录音是否过短。

    image.png
    <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

    出处:https://blog.csdn.net/sinat_27612147/article/details/78456363

    相关文章

      网友评论

        本文标题:小程序即时通讯

        本文链接:https://www.haomeiwen.com/subject/hvhrvftx.html