小程序即时通讯

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335