周其仁:停止改革,我们将面临三大麻烦

抛开立场观点不谈,且看周小平写一句话能犯多少语病

罗马尼亚的声明:小事件隐藏着大趋势——黑暗中的风:坚持做对的事相信未来的结果

布林肯突访乌克兰,为何选择去吃麦当劳?

中国不再是美国第一大进口国,贸易战殃及纺织业? 美国进一步延长352项中国商品的关税豁免期

生成图片,分享到微信朋友圈

自由微信安卓APP发布,立即下载! | 提交文章网址
查看原文

前端摇一摇功能基本实现

小龙 冰岩作坊 2022-09-24

这里的 摇一摇 指什么

简单来说,就是通过摇动移动设备去完成一些功能

那么对于程序员而言,主要是监听移动设备摇动的事件

我们如何判断用户是否摇动了移动设备,又如何获得摇动的相关信息

DeviceMotionEvent API

由 HTML5 提供,可以获得设备的物理方向及运动信息

这里我们用到的主要是其中 devicemotion 事件,提供设备的加速度信息,有 4 个只读属性:

  1. accelerationIncludingGravity:重力加速度
  2. acceleration:加速度(需要设备陀螺仪支持)
  3. rotationRate(alpha,beat,gamma):旋转速度
  4. interval:获取的时间间隔

封装事件监听函数

const useEventListener = (
  event: string,
  eventHandler,
  useCapture = false
) => {
  window.addEventListener(event, eventHandler, useCapture);

  return () => {
    window.removeEventListener(event, eventHandler, useCapture);
  };
};

监听 devicemotion 事件

const unRegister = useEventListener(
            "devicemotion",
            onDeviceMotionHandler
          );

具体实现

由上文可知,我们可以获得的是三维加速度信息,所以首先需要由加速度计算得速度

这里用加速度在 x、y、z 三个方向单位时间变化率来表示速度

const calculateSpeed = (
  curA: DeviceMotionEvent["acceleration"],
  lastA: DeviceMotionEvent["acceleration"],
  diff: number
) =>
  (Math.sqrt(
    Math.pow(curA!.x! - lastA!.x!, 2) +
      Math.pow(curA!.y! - lastA!.y!, 2) +
      Math.pow(curA!.z! - lastA!.z!, 2)
  ) /
    diff) *
  10000;

此时我们可以通过实际测试,设置一个临界速度,当实际速度超过临界速度时,即认为发生了一次摇动

const CRITICAL_SPEED = 800;

记录开始时间与开始时间后的摇动次数,则可计算出从开始时间到当前时间的摇动频率

当时间间隔足够短时,可认为获得的是当前时刻的摇动频率

// 这三个变量都对外暴露,使用 ref 可以同步修改
const frequency = ref(0);
const shakeTimes = ref(0);
const startTime = ref(Date.now());

let lastTime = Date.now(); // 上一次时间
let lastA: DeviceMotionEvent["acceleration"] = {
    x: 0,
    y: 0,
    z: 0,
}; // 上一次加速度

const deviceMotionHandler = (e: DeviceMotionEvent) => {
    const now = Date.now();
    const diff = now - lastTime;
    if (diff < 100return;
    
    const totalTime = now === startTime.value ? 1 : now - startTime.value;
    const curA = e.acceleration;
    
    const speed = calculateSpeed(curA, lastA, diff);
    if (speed > CRITICAL_SPEED) shakeTimes.value++;
    frequency.value = (shakeTimes.value / totalTime) * 100;
    
    lastA = curA;
    lastTime = now;
};  

至此,我们已经可以判断用户是否摇动了移动设备,且获得摇动次数与摇动频率

兼容各种情况

核心功能的实现并不复杂,但在实际应用中会碰到各种不兼容的情况需要处理

注意我们的网站需要支持 HTTPS 协议,才能使用 DeviceMotionEvent API

在此之前先封装处理 await

const awaitWraper = (promise) =>
 promise.then((res) => [null, res]).catch((err) => [err, null]);

接着将之前的具体实现包装成 registerOnDeviceMotion 函数,用于注册 devicemotion 事件

interface registerOnDeviceMotionReturnType {
  success: boolean;
  error: string | null;
  unRegister; //移除事件的函数
}

const registerOnDeviceMotion =
 async (): Promise<registerOnDeviceMotionReturnType> => {
    const returnObj: registerOnDeviceMotionReturnType = {
      success: false,
      error: null,
      unRegister: null,
    };

    /* 如果设备不支持 devicemotion 事件 */
    if (!window.DeviceMotionEvent) {
      returnObj.error = "当前浏览器不支持此项功能";
      return returnObj;
    }

    let lastTime = Date.now(); // 上一次时间
    let lastA: DeviceMotionEvent["acceleration"] = {
      x: 0,
      y: 0,
      z: 0,
    }; // 上一次加速度

    /* 事件处理函数 */
    const deviceMotionHandler = (e: DeviceMotionEvent) => {
        ...
    };

    /* IOS 13 */
    if (typeof DeviceMotionEvent.requestPermission === "function") {
      const permissionState = await awaitWraper(
        DeviceMotionEvent.requestPermission()
      );

      if (!permissionState[0]) {
        // 如果没出错
        if (permissionState[1] === "granted") {
          returnObj.success = true;
          returnObj.unRegister = useEventListener(
            "devicemotion",
            onDeviceMotionHandler
          );
          return returnObj;
        } else {
          returnObj.error =
            "权限申请被拒绝,如想继续使用此项功能,请重启应用并再次点击授权";
          return returnObj;
        }
      } else {
        returnObj.error = permissionState[0];
        return returnObj;
      }
    }

    returnObj.success = true;
    returnObj.unRegister = useEventListener(
      "devicemotion",
      onDeviceMotionHandler
    );
    return returnObj;
  };   

在 IOS 中,在使用 DeviceMotionEvent API 时需要用户手动授权

我们可以将用户授权的行为绑定给某个在发生该行为前必须经历的事件,比如点击开始按钮

const getPermission = () => {
  registerOnDeviceMotion().then(res => {
    permissionRes = res;
    if (permissionRes.success)
      /* 如果授权成功,进入后续流程 */
      totalProcess().then(res => {
        ...
      })
    if (permissionRes.error) alert(permissionRes.error) // 这里用 alert 模拟一下展示错误信息
  });
}

const clickHandler = () => { getPermission(); };

最终在某个 DOM 元素上监听 click 事件

<div @click="clickHandler"></div>


文章有问题?点此查看未经处理的缓存