实现移动端摇一摇功能(内含移动端知识)

2,400 阅读4分钟

一、devicemotion事件

在实现移动端摇一摇功能前,需要大家先了解移动端的devicemotion事件,以及防抖节流的知识,这样有助于大家理解

监控横竖屏切换检测

监听orientationchange时间,通过window.orientation属性判断当前屏幕属于横屏(90,-90)还是竖屏(0,180)

function setOrientation() {
    let mask = document.querySelector("#mask");
    switch (window.orientation) {
        case 90:
        case -90:
            mask.style.display = "flex";
            break;
        default:
            mask.style.display = "none";
            break;
    }
}
setOrientation();
window.addEventListener("orientationchange",setOrientation)

手机加速度检测

主要监听devicemotion事件,acceleration就是获取手机的加速度

window.addEventListener("devicemotion",(e)=>{
    const motion = e.acceleration; // 手机加速度
    const {x,y,z} = motion;
    box.innerHTML = `
        x:${x.toFixed(0)}<br/>
        y:${y.toFixed(0)}<br/>
        z:${z.toFixed(0)}
        `;
});

手机重力加速度

手机重力加速度很加速度很相似,就是属性名不同,其为accelerationIncludingGravity

window.addEventListener("devicemotion",(e)=>{
    const motion = e.accelerationIncludingGravity; // 手机加速度 + 重力
    const {x,y,z} = motion;
    box.innerHTML = `
        x:${x.toFixed(0)}<br/>
        y:${y.toFixed(0)}<br/>
        z:${z.toFixed(0)}
        `;
});

手机收到的重力

前面我们讲到accelerationIncludingGravity为手机加速度+重力,acceleration为加速度,则两者相减就能得到重力

window.addEventListener("devicemotion",(e)=>{
    const motion = e.accelerationIncludingGravity; // 手机加速度 + 重力
    const motion2 = e.acceleration; // 加速度
    let {x,y,z} = motion;
    let {x:x2,y:y2,z:z2} = motion2;
    x -= x2;
    y -= y2;
    z -= z2;
    box.innerHTML = `
        x:${x.toFixed(0)}<br/>
        y:${y.toFixed(0)}<br/>
        z:${z.toFixed(0)}
        `;
});

体感操作

利用translateXtranslateY

let translateX = 0;
let translateY = 0;     
window.addEventListener("devicemotion",(e)=>{
    const motion = e.accelerationIncludingGravity; // 手机加速度 + 重力
    const motion2 = e.acceleration; // 加速度
    let {x,y} = motion;
    let {x:x2,y:y2} = motion2;
    x -= x2;
    y -= y2;
    translateX += x;
    translateY -= y;
    box.style.transform = `translate(${translateX}px,${translateY}px)`;
});

踩坑

加速度在检测过程中,会存在一些坑:

  • 安卓下 和 IOS 下,加速度方向取值相反

  • IOS 中,如果要使用加速度 API,当前应用则必须使用 https 协议

  • IOS 12.2 中,用户可以在手机设置中关闭掉,动作与方向的访问权限

  • IOS 13 及之后,当应用中想要使用动作与方向的访问权限时,需要请求用户授权

  • IOS 13.3 及之后,申请授权,必须用户手动触发

针对上述的坑,我们来一步步解决

由于在安卓下 和 IOS 下,加速度方向取值相反,我们可以编写一个方法判断是否为安卓

function isAndroid() {
    const u = window.navigator.userAgent;
    return u.indexOf("Android")>-1||u.indexOf("Adr")>-1;
}

IOS 12.2 中,如果用户在在手机设置中设置关闭了动作与方向的访问权限,那么我们就监听不了devicemotion事件

我们可以在页面一开始渲染的时候就触发一个延时器提示用户设置权限,再通过监听事件,清楚这个延时器

let timer = setTimeout(() => {
    alert("请开启动作与方向的访问权限,否则将无法使用本应用");
}, 200);
window.addEventListener("devicemotion",()=>{
    clearTimeout(timer);
},{once:true});

IOS 13.3 及之后,申请授权,必须用户手动触发。这里的意思大概就是,我们不能把加速度的监听事件,直接在挂载的时候渲染。需要在DOM事件触发后执行

btn.addEventListener("touchend",()=>{
    setMotion((e) => {
        const motion = e.accelerationIncludingGravity; // 手机加速度 + 重力
        const motion2 = e.acceleration;
        let { x, y } = motion;
        let { x: x2, y: y2 } = motion2;
        x -= x2;
        y -= y2;
        translateX += x;
        translateY -= y;
        box.style.transform = `translate(${translateX}px,${translateY}px)`;
    });
});

二、防抖与节流

防抖

简单来讲,就是希望函数只执行一次,哪怕我进行了多次调用

下面给出一个简单版本的防抖函数封装:

其中,fn为需要防抖的函数,delay为防抖时长,start为是否在开始时执行函数,return返回经过近防抖处理的函数

防抖主要需要处理的两点地方:

  • 处理this指向问题
  • 处理参数问题
function debounce(fn,deley = 200,start = false) {
    let timer = 0;
    let isStart = true;
    return function(...arg) { // 经过防抖处理的函数
        const _this = this;
        clearTimeout(timer);
        if(isStart){
            start && fn.apply(_this,arg);
            isStart = false;
        }
        timer = setTimeout(() => {
            (!start)&&fn.apply(_this,arg);
            isStart = true;
        }, deley);
    }
}

节流

简单来讲,让函数保持在一个可接受的固定频率执行

节流跟防抖十分的相似,无非就是判断的时候处理方式不同

function throttle(fn,deley = 200,start = true) {
    let timer = 0;
    return function(...arg) { // 经过防抖处理的函数
        const _this = this;
        if(timer){
            return;
        }
        start&&fn.apply(_this,arg);
        timer = setTimeout(() => {
            (!start)&&fn.apply(_this,arg);
            timer = 0;
        }, deley);
    }
}

三、实现过程

实现摇一摇的本质就是:当前次的加速度和上一次的加速之间有了一个比较大的差值

下面定义两个属性来判断

const maxRange = 50; // 当加速度的差值大于该值时认定用户进行了摇一摇
const minRange = 5; // 当加速度的差值小于于该值时认定用户停止了摇一摇

获取手机加速度

const { x, y, z } = e.acceleration;
const range = Math.abs(x - lastX) + Math.abs(y - lastY) + Math.abs(z - lastZ);

后面的功能就没什么难点,如果看到不懂的地方,涉及的知识点文章开头就已经涉及,这里是做了兼容 IOS(多个版本) 与 Anroid 的处理,再加上防抖功能,让摇一摇功能更加高效

下面给出完整代码

<body>
    <button id="btn">开启摇一摇</button>
    <button id="stopBtn">关闭摇一摇</button>
    <div id="info"></div>
    <script src="motion.js"></script>
    <script>
        let btn = document.querySelector("#btn");
        let stopBtn  = document.querySelector("#stopBtn");
        /*
     setShake 摇一摇
       ops : {
         start:fn // 开始摇一摇时要做的事情
         shake:fn // 摇一摇中要做得事情
         end: fn// 摇一摇结束后要做的事情
       }
    */
        function setShake(ops) {
            const { start = () => { }, shake = () => { }, end = () => { } } = ops;
            let lastX = 0,
                lastY = 0,
                lastZ = 0;
            const maxRange = 50;
            const minRange = 5;
            let isShake = false;
            const unMotion = setMotion(throttle((e) => {
                const { x, y, z } = e.acceleration;
                const range = Math.abs(x - lastX) + Math.abs(y - lastY) + Math.abs(z - lastZ);
                if (range > maxRange && (!isShake)) {
                    start(e);
                    isShake = true;
                } else if (range > maxRange && isShake) {
                    shake(e);
                } else if (range < minRange && isShake) {
                    end(e);
                    isShake = false;
                }
                lastX = x;
                lastY = y;
                lastZ = z;
            }));
            return unMotion; //取消摇一摇监听
        }
        let unShake;
        btn.addEventListener("touchend", () => { 
            unShake = setShake({
                start:()=>{
                    info.innerHTML += "开始摇一摇<br/>";
                },
                shake:()=>{
                    info.innerHTML += "摇一摇中<br/>";
                },
                end: ()=>{
                    info.innerHTML += "摇一摇结束<br/>";
                }
            })
        });
        stopBtn.addEventListener("touchend",()=>{
            if(unShake){
                unShake();
            }
        })


    </script>
</body>

motion.js

// 在 IOS 12 中,判断用户是否关闭了动作与方向的访问权限
{
  let timer = setTimeout(() => {
    alert("请开启动作与方向的访问权限,否则将无法使用本应用");
  }, 200);
  window.addEventListener("devicemotion", () => {
    clearTimeout(timer);
  }, { once: true });
}
// 判断当前是否是安卓系统
function isAndroid() {
  const u = window.navigator.userAgent;
  return u.indexOf("Android") > -1 || u.indexOf("Adr") > -1;
}
/*
  setMotion 设置监听加速变化要处理的事情
  cb 加速度变化后要做的处理函数
  return 取消事件注册
*/
function setMotion(cb) {
  let fn = (e) => {
    if (isAndroid()) { // 处理安卓取反问题
      e.acceleration.x = -e.acceleration.x;
      e.acceleration.y = -e.acceleration.y;
      e.acceleration.z = -e.acceleration.z;
      e.accelerationIncludingGravity.x = -e.accelerationIncludingGravity.x;
      e.accelerationIncludingGravity.y = -e.accelerationIncludingGravity.y;
      e.accelerationIncludingGravity.z = -e.accelerationIncludingGravity.z;
    }
    cb(e);
  };
  // 区分 IOS 13 及之前
  if (typeof DeviceMotionEvent.requestPermission === "function") { // IOS 13 及之后
    DeviceMotionEvent.requestPermission()
      .then(permissionState => {
        if (permissionState === 'granted') {
          // 权限允许
          window.addEventListener("devicemotion", fn);
        }
      }).catch(() => {
        alert("请开启授权否则无法使用本应用");
      })
  } else { //安卓及IOS 13之前
    window.addEventListener("devicemotion", fn)
  }
  return ()=>{
    window.removeEventListener("devicemotion",fn);
  }
}

四、结束语

如果觉得这篇文章对你有帮助,可以伸出你的小手,为这篇文章点个赞

我是前端路上一位新晋的萌新,怀着学习的态度,怀着认识各位同伴的心态,把自己的知识分享出来,除了让自己对知识认知更加巩固,也希望大家能够通过我写的文章学到一点微薄的知识,如果知识内容有误,可以在评论区或者下面公众号告诉我,我会立刻更改

最后,我也创建了一个 【前端收割机】的公众号,希望大家可以关注一波,里面的文章都是掉头发之作