从DeviceMotion兼容处理到摇一摇功能实现

4,443 阅读9分钟

年前在一个程序中需要用到摇一摇这个功能,忽然发现devicemotion这个事件IOS下不起效果,查了一堆资料终于解决了这个问题。DeviceMotion 是什么?摇一摇这个功能具体怎么实现,在各个设备下需要处理什么样的问题,我们通过一篇文章来好好掰扯掰扯这个事情。

DeviceMotion 到底是何物?

在 window 对象中存在一个专门的事件 devicemotion,这个事件可以用来监听设备的加速度变化等信息. 在该事件的 Event 对象中(DeviceMotionEvent),包含了 acceleration 对象,我们可以同过 acceleration 对象来获取设备的加速度信息。演示代码如下:

    <script>
        window.addEventListener("devicemotion",(e)=>{
            let motion = e.acceleration;
            let {x,y,z} = motion;
            // x,y,z 分别是手机三个方向的加速度 
        });
    </script>

acceleration 对象中 x、y、z 分别代表了手机长、宽、厚,3个方向的加速度,具体如下图:

在这里我们已经了解 acceleration 的具体使用,接下来我们是不是就可以上手应用呢?先别急,接下来才是我们的踩坑之旅。接着往下我们来细聊一下,想要在手机上使用 acceleration 还有哪些坑在等着我们。

DeviceMotion 踩坑之旅

先从最简单的开始说起,在 安卓 下,使用 acceleration 时,在 X,Y,Z 三个方向取值时,要注意和IOS下刚好相反。 什么意思呢?比如 现在我使用的是一部安卓手机,向左移动,x轴得到一个速度为 10,那么相应 IOS 下 我们拿到的就是一个 -10,其他的Y轴和Z轴同理,处理一下兼容,得出下列代码:

<script>
    // 判断是否是 IOS 设备,true 代表 IOS,false 代表非 IOS
    function getIos(){
        let u = window.navigator.userAgent;
        return !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/);
    }
    window.addEventListener("devicemotion",(e)=>{
        let motion = e.acceleration;
        let {x,y,z} = motion;
        if(!getIos()){
            x = -x;
            y = -y;
            z = -z;
        }
    });
</script>

爬过了第一个坑之后,我们继续探坑,上述代码使用IOS的小伙伴可能一直看不到效果,这是为什么呢,一个版本一个版本的来说。

IOS 下的 https 要求

在 IOS 一开始的版本中是可以和安卓一样直接运行的,但是从 IOS 10 之后的某个版本(实在记不起来大家原谅), IOS 做了安全限制,要使用 devicemotion 事件则必须使用 https 协议。好吧,那既然官方要求了,咱也没有办法是吧,配吧。

终于在 IOS 协议配置好之后,问题应该能解决吧,少年你把这个问题想得太简单了。

IOS 12.2 之后的安全限制

在 IOS 12.2 之后,苹果的开发团队又给我们添了一个大乐子,用户可以在手机的设置关闭掉“动作与方向访问”,如下图所示。如果用户关闭了,那么我们就只能“呵呵”了,顺便说一下,目前为止,开发人员有没有有效的办法直接获取到用户是否关闭了“动作与方向访问”。

这个问题怎么处理呢?我这做了一个取巧的办法,分享给大家,代码如下:

<sccript>
    if(!window.DeviceMotionEvent){
        alert("您的设备不支持DeviceMotion");
    } else {
        let timer = setTimeout(function(){
            alert('请开启您设备中"动作和方向的权限",否则您将无法使用本应用');
        },100);
        window.addEventListener("devicemotion",(e)=>{
            clearTimeout(timer);
        },{once:true});
    }
</script>

第一步,我们先通过 window.DeviceMotionEvent 排除掉不支持获取加速度的设备,如电脑,如果该设备支持就会有 window.DeviceMotionEvent 这个属性。

第二步,设备如果支持 window.DeviceMotionEvent,开始一个事件监测(这里注意加速度获取时特别敏感的哪怕我们认为我们的手机静止了也会有一些加速给它,所以devicemotion这个事件会时时触发,触发间隔极小),如果该事件没有执行,说明用户关闭了动作和方向的权限。

到这我们解决了 IOS 12.2 之后用户可以关闭权限的问题,那获取加速度的问题就应该解决了吧。但是,苹果的开发团队会就这样放过我们的吗?不要太天真少年!

IOS 13 的用户权限请求

到了 IOS 13 之后苹果开发团队又开始作妖了,尤其到了现在的 13.3 。

具体怎么回事呢?我们看看下列代码:

<script>
    DeviceMotionEvent.requestPermission()
            .then(permissionState => {
                if (permissionState === 'granted') {
                    window.addEventListener('devicemotion', () => {})
                }
            })
            .catch((err)=>{

            });
</script>

这是 IOS 13 之后的,苹果给我新增的一个方法用来获取用户权限,获取到用户权限之后,我们就可以来检测加速度了,但是注意该方法是 IOS 13 之后才有的,在 IOS 13 之前这么写的话,DeviceMotionEvent下并没有requestPermission方法就会报错,另外注意在最新的 IOS 13.3里,该方法需要用户触发获取,所以我们来看看最终版要写成什么样子,代码如下:

<script>
    // 判断是否是 ios 设备    
    function getIos(){
        var u = window.navigator.userAgent;
        return !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/);
    }    
    /*
        setDeviceMotion 添加陀螺仪监控
            cb devicemotion的事件处理函数
            errCb 不支持 devicemotion 时的处理回调
    */    
    function setDeviceMotion(cb,errCb){
        if(!window.DeviceMotionEvent){
                errCb("设备不支持DeviceMotion"); 
            return;
        }
        if (typeof DeviceMotionEvent.requestPermission === 'function') {
            // IOS 13
            DeviceMotionEvent.requestPermission()
                .then(permissionState => {
                    if (permissionState === 'granted') {
                        window.addEventListener('devicemotion',cb);
                    }
                })
                .catch((err)=>{
                    errCb("用户未允许权限");
                });
        } else {
            // 其他支持加速度检测的系统
            let timer = setTimeout(function(){
                errCb("用户未开启权限");
            },1000);
            window.addEventListener("devicemotion",(e)=>{
                clearTimeout(timer);
            },{once:true});
            window.addEventListener("devicemotion",cb);
        }
    }
    document.ontouchstart=function(){
        setDeviceMotion((e)=>{
            let motion = e.acceleration;
            let {x,y,z} = motion;
            if(!getIos()){
                x = -x;
                y = -y;
                z = -z;
            } 
        },(errMessage)=>{
            alert(errMessage);
        }); // 最后这里一定要注意,在IOS 13.3中必须通过用户操作去获取该权限
    }
</script>

到这一步,我们终于有了一个兼容的加速度监听处理,接下来我们就可以看看摇一摇的相关功能实现,这一步就比较简单了。

摇一摇实现

可以获取到手机的加速度之后,摇一摇的功能就比较简单了。当我们在晃动手机时,手机本身会产生一个加速度。我们可以截取手机的两次加速度比较,中间会有一个差值,当差值大于某个幅度时就可以认定用户进行了摇一摇这个操作。

这里要注意一个问题,在检测用户摇动手机的时候,我们也没有办法预知到用户摇动的是哪一个轴,所以咱们三个轴都会进行检测,具体代码如下:

<script>
    function shake(cb){ 
        const maxRange = 60;//当用户的两次加速度差值大于这个幅度,判定用户进行了摇一摇功能
        let lastX=0,
        lastY = 0,
        lastZ = 0;
        window.addEventListener("devicemotion",(e)=>{
            let motion = e.acceleration;
            let {x,y,z} = motion;
            let range = Math.abs(x - lastX) + Math.abs(y - lastY) + Math.abs(z - lastZ);
            if(range > maxRange){//用户进行了摇一摇
                alert("您进行了摇一摇");
            }
            lastX = x;
            lastY = y;
            lastZ = z;
        });
    }
</script>

在这里我们有了一个基本的摇一摇功能,但是还有功能不太完善: 1. 摇一摇这个功能一般我们是在用户停止摇动之后才去做某些事情 2. devicemotion 执行间隔特别的小,可以到达几毫米,但是从性能上来说,我们并不需要这么小的执行间隔,另外用户摇动手机也需要一定时间,基本上没有人可以在几毫米内摇动我们的手机。 3. 前边我们讲的 devicemotion 的兼容处理还没有加进来 我们把这些问题都整体处理一下,代码如下:

<button id="startBtn">开启摇一摇</button>
<button id="closeBtn">关闭摇一摇</button>
<script>   
    /*
        setDeviceMotion 添加陀螺仪监控
            参数:
                cb devicemotion的事件处理函数
                errCb 不支持 devicemotion 时的处理回调
    */    
    function setDeviceMotion(cb,errCb){
        if(!window.DeviceMotionEvent){
                errCb("设备不支持DeviceMotion"); 
            return;
        }
        if (typeof DeviceMotionEvent.requestPermission === 'function') {
            // IOS 13
            DeviceMotionEvent.requestPermission()
                .then(permissionState => {
                    if (permissionState === 'granted') {
                        window.addEventListener('devicemotion',cb);
                    }
                })
                .catch((err)=>{
                    errCb("用户未允许权限");
                });
        } else {
            // 其他支持加速度检测的系统
            let timer = setTimeout(function(){
                errCb("用户未开启权限");
            },1000);
            window.addEventListener("devicemotion",(e)=>{
                clearTimeout(timer);
            },{once:true});
            window.addEventListener("devicemotion",cb);
        }
    } 
    /*
        throttle 节流函数
            参数:
                fn 要节流的函数
                interval 节流间隔时间
                start 是否在节流开始时执行 (true在开始时执行,false在结束时执行)
            返回值:
                经过节流处理的函数
    */
    function throttle(fn,interval=200,start = true){
        if(typeof fn !== "function"){
            return console.error("请传入一个函数");
        }
        let timer = 0;
        return function(...arg){
            let _this = this;
            if(timer){
                return ;
            }
            start&&fn.apply(_this,arg); 
            timer = setTimeout(() => {
                (!start)&&fn.apply(_this,arg); 
                timer = 0;
            }, interval);
        }
    }      
    /*
        addShake 添加摇一摇功能
        参数: 
                cbShake 类型 fn 当用户进行了摇一摇之后要做的事情
        返回值:
                shakeIndex 开启的第几个摇一摇功能的索引,用来删除监听     
    */ 
    function addShake(cbShake){ 
        const maxRange = 60; //当用户的两次加速度差值大于这个幅度,判定用户进行了摇一摇功能
        const minRange = 10; //当用户的两次加速度差值小于这个幅度,判定用户停止摇动手机
        let isShake = false; //记录用户是否摇动手机
        let lastX=0,
        lastY = 0,
        lastZ = 0;
        function toShake(e){
            let motion = e.acceleration;
            let {x,y,z} = motion;
            let range = Math.abs(x - lastX) + Math.abs(y - lastY) + Math.abs(z - lastZ);
            if(range > maxRange){//用户进行了摇一摇
                isShake = true;
            }
            if(range < minRange&&isShake){ // 停止摇一摇
                cbShake(e);
                isShake = false;
            }
            lastX = x;
            lastY = y;
            lastZ = z;
        }
        if(!window.shakeEvent){ //建立 shakeEvent 存储所有的摇一摇的处理函数,方便一会取消
            window.shakeEvent = [];
        }
        toShake = throttle(toShake);
        window.shakeEvent.push(toShake);
        setDeviceMotion(toShake,(errMessage)=>{
            alert(errMessage)
        })
        return  window.shakeEvent.length - 1;//返回该次摇一摇处理的索引
    }
    /*
        删除摇一摇监听
            cbShake 类型 fn 当用户进行了摇一摇之后要做的事情
    */ 
    function remveShake(shakeIndex){
        window.removeEventListener("devicemotion",window.shakeEvent[shakeIndex]);
    }

    // 调用摇一摇
    {
        let startBtn = document.querySelector("#startBtn");
        let closeBtn = document.querySelector("#closeBtn");
        let isStartShake = false;
        let shakeIndex;
        // 再次强调 IOS 13.3 需要用户触发,再能开启摇一摇 
        startBtn.addEventListener("touchend",()=>{
            if(isStartShake)return;
            isStartShake = true;
            shakeIndex = addShake(()=>{
                alert("您进行了摇一摇")
            })
        });
        closeBtn.addEventListener("touchend",()=>{
            if(!isStartShake)return;
            isStartShake = false;
            remveShake(shakeIndex);
        });

    }
</script>

到这一步,我们终于就有了一个比较完整的摇一摇的功能处理函数,当然代码还可以继续处理,比如我们可以把 shake 定义成一个事件等等,不过这些和我们今天要说的主题无关,我就不再复述了。


往期推荐:

想用Vuejs突破20K必备的热门面试题

使用vue+node搭建前端异常监控系统


动动小手点个赞,与小编一起勇闯前端圈儿,欢迎留言评论,一起讨论哟~