标签归档:微信

移动端实现块拖动功能

主要的原理是通过获取手指拖动时的位置坐标,来计算块移动的相对位置(判断是否超出界面)。拖动的同时需要禁止页面滚动,拖动结束后恢复。具体的实现代码如下:

var helper=document.querySelector('#helper');
var maxW=document.body.clientWidth-helper.offsetWidth;//可移动的最大宽度
var maxH=document.body.clientHeight-helper.offsetHeight;//可移动的最大高度

helper.addEventListener('touchstart',function(e){
    var ev = e || window.event;
    var touch = ev.targetTouches[0];
    oL = touch.clientX - helper.offsetLeft;//鼠标所点位置的坐标
    oT = touch.clientY - helper.offsetTop;
    //阻止页面滚动
    document.body.ontouchmove=function(e){
	    e.preventDefault();
	}

	//兼容android微信
	document.body.style.height = '100%';
	document.body.style.overflow = 'hidden';
})

helper.addEventListener('touchmove',function(e){
   var ev = e || window.event;
   var touch = ev.targetTouches[0];
   var oLeft = touch.clientX - oL;
   var oTop = touch.clientY - oT;
   if(oLeft<0){
       oLeft=0;
   }else if (oLeft>=maxW) {
       oLeft=maxW;
   }
   if(oTop<0){
       oTop=0;
   }else if (oTop>=maxH) {
       oTop=maxH;
   }

   helper.style.left = oLeft + 'px';
   helper.style.top = oTop + 'px';
})

helper.addEventListener('touchend',function(){
	//恢复页面滚动
    document.body.ontouchmove=function(e){}

   	//兼容android微信
    document.body.style.height = 'auto';
	document.body.style.overflow = 'auto';
});

Demo:(兼容安卓、iOS及相应微信端)

drap

微信语音识别功能实战

最近在微信上实现一个长按录音并识别语音功能,即在用户按下时调用微信 JS-SDK 录音接口,然后在用户松开时停止录音并识别录下了的语音。

流程如下:

touchstart -> wx.startRecord()
touchend -> wx.stopRecord()-> wx.translateVoice()

嗯,看起来流程很简单,实现起来也很快,但是测试的时候马上发现了问题:

1.短按会出现无法结束录音

如果点击了一下录音按钮, 相当于快速地startRecord然后stopRecord,那么stopRecord是极有可能是无效的,不会执行任何callback。因为微信JSSDK的调用是异步的。你调用startRecord的时间,和startRecord的success的callback被执行的时间可能间隔了若干毫秒甚至秒。这意味着,用户点击按钮可能会造成:虽然是先调用startRecord再调用stopRecord,但是可能stopRecord先于startRecord调用成功。这样就造成无法结束录音的情况。

解决的方法就是记录touchend 的时间点A,然后在startRecord调用成功时判断A是否大于此时的时间点,如果小于,则再次调用结束录音方法。

2.部分安卓机在调用startRecord后无法触发touchend

通过不断的测试,得出以下的异常表现(目前测试到的有vivo手机):

当 touchstart 时调用了 startRecord,触发的事件为: touchstart -> touchcancel(此时还未松开手指)

当 touchstart 时不调用 startRecord,触发的事件为: touchstart -> touchend(松开手指后)

可以看到:通过 touchstart 调用了 startRecord 后会立即触发元素的 touchcancel 事件,继而造成 touchend 事件就不触发了。

目前还没找到好的解决方法,只能针对安卓更好交互方式:点击录音->再次点击结束录音并识别。

相关实现代码:

<div class="voice-remote">
    <span class="cover"></span>
    <span class="icon"></span>
</div>

 

.voice-remote{border-radius:100%;width:1.8rem;height:1.8rem;position:absolute;background:#f6f6f6;bottom:1.5rem;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);-webkit-transition:all .2s;transition:all .2s}
.voice-remote.active{width:2.8rem;height:2.8rem;bottom:1rem;border:1px solid #e7e7e7}
.voice-remote:before{content:"";width:100%;height:100%;position:absolute;z-index:2;top:0;left:0;border-radius:100%;background-image:-webkit-linear-gradient(-90deg,transparent 50%,#f6720e 50%);background-image:linear-gradient(-90deg,transparent 50%,#f6720e 50%)}
.voice-remote:after{content:"";width:100%;height:100%;position:absolute;z-index:3;bottom:0;left:0;border-radius:100%;-webkit-background-image:linear-gradient(-90deg,transparent 50%,#f6720e 50%);background-image:linear-gradient(-90deg,transparent 50%,#f6720e 50%)}
.voice-remote .cover{position:absolute;border-radius:100%;width:100%;height:100%;z-index:4;top:0;left:0;-webkit-background-image:linear-gradient(-90deg,transparent 50%,#f6f6f6 50%);background-image:linear-gradient(-90deg,transparent 50%,#f6f6f6 50%)}
.voice-remote .icon{position:absolute;width:100%;height:100%;top:0;left:0;background:#f6f6f6 url(/public/wap/app/images/voice.png) no-repeat center center;background-size:100%;border-radius:100%;z-index:5}
.voice-remote .icon:before{content:'长按说话';position:absolute;top:-.65rem;left:0;color:#f6720e;width:100%;text-align:center;font-size:.32rem}
.voice-remote.android .icon:before{content:"点击说话"}
.voice-remote.active .icon:before{content:'松开完成';top:-.85rem}
.voice-remote.android.active .icon:before{content:"点击完成"}
.voice-remote.active .icon{width:90%;height:90%;top:5%;left:5%;background-size:60%}
.voice-remote.active:before{-webkit-animation:scoll linear 2.5s;animation:scoll linear 2.5s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}
.voice-remote.active:after{-webkit-animation:xscoll linear 5s;animation:xscoll linear 5s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}
.voice-remote.active .cover{-webkit-animation:hide linear 5s;animation:hide linear 5s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}
@-webkit-keyframes scoll{0%{-webkit-transform:rotate(0)}
100%{-webkit-transform:rotate(180deg)}
}
@keyframes scoll{0%{transform:rotate(0)}
100%{transform:rotate(180deg)}
}
@-webkit-keyframes xscoll{0%{-webkit-transform:rotate(0)}
100%{-webkit-transform:rotate(360deg)}
}
@keyframes xscoll{0%{transform:rotate(0)}
100%{transform:rotate(360deg)}
}
@-webkit-keyframes hide{0%{opacity:1}
49.9%{opacity:1}
50%{opacity:0}
100%{opacity:0}
}
@keyframes hide{0%{opacity:1}
49.9%{opacity:1}
50%{opacity:0}
100%{opacity:0}
}

 

var VR = {
	options: {
		point: 0,
		tpoint: 0,
		epoint: 0,
		timer: 0
	},
	keyword: [
		{
			data: '声音大、音乐大、音乐调大、音乐调高、声音调大',
			type: 1,
			cmdid: '203',
			reply: '小主,已为你调了音乐声音~',
			error: '音乐没调成功,再试一次喔~'
		},
		{
			data: '声音小、音乐小、音乐调小、音乐调低、声音调小',
			type: 1,
			cmdid: '204',
			reply: '小主,已为你调了音乐声音~',
			error: '音乐没调成功,再试一次喔~'
		},
		{
			data: '评分开、开评分、开启评分、评分开启',
			type: 1,
			cmdid: '541',
			reply: '小主,已为你开启评分~',
			error: '评分开启失败,再试一次喔~'
		},
		{
			data: '评分关、关评分、关掉评分、评分关掉',
			type: 1,
			cmdid: '541',
			reply: '小主,已为你关闭评分~',
			error: '评分关闭失败,再试一次喔~'
		}
	],
	recode: function(){//定时最长5s后结束录音
		VR.options.timer = setInterval(function(){
			var time = +new Date() - VR.options.point;
			if(time >= 5000){
				if(VR.getPlatformType() == 2){
					$('.voice-remote').removeClass('active');
				}
				clearInterval(VR.options.timer);
				setTimeout(function(){
	        		VR.translate();
	        	},100);
			}
		},1000);
	},
	cmdMatch: function(words){//根据语音识别处理的内容匹配关键词
		for(i in VR.keyword){
			var key = VR.keyword[i]['data'].split('、');
			for(n in key){
				//console.log(key[n]);
				if(words.indexOf(key[n]) > -1){
					VR.runCmd(i);//匹配到关键词执行相应方法
					return true;
				}else {
					if(i == VR.keyword.length-1 && n == key.length - 1){
						window.location.href = '/Wap/app?searchby=voice&searchkey='+words+'#pageSearchList';
					}
				}
			}
		}
	},
	runCmd: function(index){
		var cmd = VR.keyword[index];
		//执行方法...
	},
	translate: function(){//结束录音并识别语音
		wx.stopRecord({
            success: function(res) {
                localId = res.localId;
                wx.translateVoice({
		            localId: localId,
		            complete: function(res) {
		                if (res.hasOwnProperty('translateResult')) {
		                    //alert('识别结果:' + res.translateResult);
		                    VR.cmdMatch(res.translateResult.split('。')[0]);
		                } else {
		                    //alert('无法识别');
		                }
		            }
		        });
            },
            fail: function(res) {
            	alert(JSON.stringify(res));
            }
        });
	},
	getPlatformType: function(){ //1-IOS微信 2-安卓微信 0-未知
        var ua = navigator.userAgent.toLowerCase();
        if(/micromessenger/.test(ua)){
            return /android/.test(ua)?'2':'1';
        }
        return '0';
    },
	init: function(){
		if(VR.getPlatformType() == 2){//android采用点击方式录音,ios默认采用长按方式录音
			$('.voice-remote').addClass('android');
		}

        wx.config({
            debug: false,
            appId: sign_pkg.appId,
            timestamp: sign_pkg.timestamp,
            nonceStr: sign_pkg.nonceStr,
            signature: sign_pkg.signature,
            jsApiList: [
                'checkJsApi',
                'startRecord',
                'stopRecord',
                'translateVoice'
            ]
        });

		wx.ready(function(){
			$('.voice-remote').on('touchstart',function(){

				VR.options.tpoint = +new Date();//记录touchstart时间点

				if(VR.getPlatformType() == 2){//如果是android端,touchstart后判断当前是否处于录音状态,如果是则结束录音并识别语音
					if($('.voice-remote').hasClass('active')){
						$('.voice-remote').removeClass('active');//切换录音按钮状态

						if(time < 5000){
							clearInterval(VR.options.timer);//清除定时结束录音定时器

							setTimeout(function(){//如果录音按钮有动画效果,需延迟结束录音,否则调用结束录音接口时,会导致动画卡顿,延时时长为动画效果时长
				        		VR.translate();
				        	},200);
						}
						return false;
					}
				}

				wx.startRecord({
					success: function(){
						$('.voice-remote').addClass('active');
						VR.options.point = +new Date();//记录开始录音成功时间点
		        		VR.recode();//启用定时结束录音定时器
		        		if(VR.options.point > VR.options.epoint && VR.options.epoint > VR.options.tpoint){//处理因为短按,startRecord还未初始成功,导致无法正常停止录音
		        			clearInterval(VR.options.timer);

				        	$('.voice-remote').removeClass('active');

				        	setTimeout(function(){
				        		VR.translate();
				        	},200);
		        		}
					},
					fail: function(res) {
						alert(JSON.stringify(res));
		            },
		            cancel: function() {
		                alert('您拒绝了授权录音');
		            }
		        });
			});

			if(VR.getPlatformType() == 1){//ios端才监听touchend事件
				$('.voice-remote').on('touchend',function(){
					VR.options.epoint = +new Date();//记录touchend时间点
					$(this).removeClass('active');

					var time = +new Date() - VR.options.point;

					if(time < 5000){//当录音间隔时间小于5s,touchend后清除定时结束录音定时器,并调用结束录音方法
						clearInterval(VR.options.timer);

			        	setTimeout(function(){
			        		VR.translate();
			        	},200);
					}
			    });
			}

		});
	}
}

VR.init();

 

PS:语音识别操作按钮,请看demo