Javascript检测抖动事件,所有主要browsers/devices(iOS,Android)

Detect shake event with Javascript, with all major browsers/devices (iOS, Android)

我已阅读 Javascript. Listen for iPhone shake event? and Detecting shaking in html5 mobile,它提供了一个很好的解决方案来检测移动 phone“摇动”事件:

<script src="shake.js"></script>
<script>
var myShakeEvent = new Shake({threshold: 15, timeout: 1000});
myShakeEvent.start(); 
window.addEventListener('shake', function() { alert('shake!'); }, false); 
</script>

不幸的是,这似乎不适用于最近的 iOS 设备,并且 this issue shows that special permission should be granted for recent iOS versions. Note that the code from here 在 shake.js.

库中不容易使用

问题:截至 2022 年,哪种方法可用于检测 Javascript 的“摇动”事件,适用于主要浏览器(Firefox、Chrome、Safari ) 和移动设备 (iOS, Android)?

如果有一个弹出窗口先请求权限就可以了(比如弹出窗口请求地理定位请求的权限)。

查看这些链接:

Detect a shake in iOS Safari with Javascript?

https://github.com/lhagan/jquery.ios-shake

第二个库主要用作:

<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, minimal-ui">
<title>Detect shake in phone using Jquery</title>
<script type="text/javascript" src="jquery-1.11.2.min.js"></script>
<script type="text/javascript" src="jquery.ios-shake.js"></script>
<script type="text/javascript">
$(document).ready(function() {
$.shake({
callback: function() {
alert("Please upvote my answer!");
}
});
});
</script>
</head>
<body>
<div id="content">
<h1>Detecting Phone shake using jQuery</h1>
</div>
<div id="welcome">Shake your phone to get the alert</div>
</body>
</html>

没有 shake 事件:最接近的事件是 devicemotion

根据你的问题内容,​​我推断你只是想订阅当设备加速度超过某个阈值时触发的事件,在可能的触发之间有一个去抖延迟(超时)。

使用您链接到的“shake.js”库作为参考,我编写了一个 TypeScript 模块,您可以使用它来完成基本相同的事情。它包括在开始时获得用户许可批准,但请记住,您必须调用 ShakeInstance.start() 方法来响应用户启动的事件(例如单击按钮)。

Note: The methods used in the module are supported by the environments you listed according to the compatibility data on their related documentation pages at MDN. (Remarkably, desktop Safari simply does not support the DeviceMotionEvent whatsoever.) However, I don't have access to all of those combinations of environments you listed in order to perform the testing myself, so I'll leave that to you.

TS Playground

function createEvent <Type extends string, Detail>(
  type: Type,
  detail: Detail,
): CustomEvent<Detail> & {type: Type} {
  return new CustomEvent(type, {detail}) as CustomEvent<Detail> & {type: Type};
}

function getMaxAcceleration (event: DeviceMotionEvent): number {
  let max = 0;
  if (event.acceleration) {
    for (const key of ['x', 'y', 'z'] as const) {
      const value = Math.abs(event.acceleration[key] ?? 0);
      if (value > max) max = value;
    }
  }
  return max;
}

export type ShakeEventData = DeviceMotionEvent;
export type ShakeEvent = CustomEvent<ShakeEventData> & {type: 'shake'};
export type ShakeEventListener = (event: ShakeEvent) => void;

export type ShakeOptions = {
  /**
   * Minimum acceleration needed to dispatch an event:
   * meters per second squared (m/s²).
   *
   * https://developer.mozilla.org/en-US/docs/Web/API/DeviceMotionEvent/acceleration
   */
  threshold: number;
  /**
   * After a shake event is dispatched, subsequent events will not be dispatched
   * until after a duration greater than or equal to this value (milliseconds).
   */
  timeout: number;
};

export class Shake extends EventTarget {
  #approved?: boolean;
  #threshold: ShakeOptions['threshold'];
  #timeout: ShakeOptions['timeout'];
  #timeStamp: number;

  constructor (options?: Partial<ShakeOptions>) {
    super();
    const {
      threshold = 15,
      timeout = 1000,
    } = options ?? {};
    this.#threshold = threshold;
    this.#timeout = timeout;
    this.#timeStamp = timeout * -1;
  }
  
  // @ts-ignore
  addEventListener (
    type: 'shake',
    listener: ShakeEventListener | null,
    options?: boolean | AddEventListenerOptions
  ): void {
    type Arg1 = Parameters<EventTarget['addEventListener']>[1];
    super.addEventListener(type, listener as Arg1, options);
  }

  dispatchEvent (event: ShakeEvent): boolean {
    return super.dispatchEvent(event);
  }

  // @ts-ignore
  removeEventListener (
    type: 'shake',
    callback: ShakeEventListener | null,
    options?: EventListenerOptions | boolean
  ): void {
    type Arg1 = Parameters<EventTarget['removeEventListener']>[1];
    super.removeEventListener(type, callback as Arg1, options);
  }

  async approve (): Promise<boolean> {
    if (typeof this.#approved === 'undefined') {
      if (!('DeviceMotionEvent' in window)) return this.#approved = false;
      try {
        type PermissionRequestFn = () => Promise<PermissionState>;
        type DME = typeof DeviceMotionEvent & { requestPermission: PermissionRequestFn };
        if (typeof (DeviceMotionEvent as DME).requestPermission === 'function') {
          const permissionState = await (DeviceMotionEvent as DME).requestPermission();
          this.#approved = permissionState === 'granted';
        }
        else this.#approved = true;
      }
      catch {
        this.#approved = false;
      }
    }
    return this.#approved;
  }

  #handleDeviceMotion = (event: DeviceMotionEvent): void => {
    const diff = event.timeStamp - this.#timeStamp;
    if (diff < this.#timeout) return;
    const accel = getMaxAcceleration(event);
    if (accel < this.#threshold) return;
    this.#timeStamp = event.timeStamp;
    this.dispatchEvent(createEvent('shake', event));
  };

  async start (): Promise<boolean> {
    const approved = await this.approve();
    if (!approved) return false;
    window.addEventListener('devicemotion', this.#handleDeviceMotion);
    return true;
  }

  stop (): void {
    window.removeEventListener('devicemotion', this.#handleDeviceMotion);
  }
}

这样使用:

const shake = new Shake({threshold: 15, timeout: 1000});

shake.addEventListener('shake', ev => {
  console.log('Shake!', ev.detail.timeStamp, ev.detail.acceleration);
});

// Then, in response to a user-initiated event:
const approved = await shake.start();

I'm not sure whether the SO snippet environment will cause a problem for demoing this or not, but I've included the compiled JS from the TS Playground link just in case:

"use strict";
function createEvent(type, detail) {
    return new CustomEvent(type, { detail });
}
function getMaxAcceleration(event) {
    let max = 0;
    if (event.acceleration) {
        for (const key of ['x', 'y', 'z']) {
            const value = Math.abs(event.acceleration[key] ?? 0);
            if (value > max)
                max = value;
        }
    }
    return max;
}
class Shake extends EventTarget {
    constructor(options) {
        super();
        this.#handleDeviceMotion = (event) => {
            const diff = event.timeStamp - this.#timeStamp;
            if (diff < this.#timeout)
                return;
            const accel = getMaxAcceleration(event);
            if (accel < this.#threshold)
                return;
            this.#timeStamp = event.timeStamp;
            this.dispatchEvent(createEvent('shake', event));
        };
        const { threshold = 15, timeout = 1000, } = options ?? {};
        this.#threshold = threshold;
        this.#timeout = timeout;
        this.#timeStamp = timeout * -1;
    }
    #approved;
    #threshold;
    #timeout;
    #timeStamp;
    // @ts-ignore
    addEventListener(type, listener, options) {
        super.addEventListener(type, listener, options);
    }
    dispatchEvent(event) {
        return super.dispatchEvent(event);
    }
    // @ts-ignore
    removeEventListener(type, callback, options) {
        super.removeEventListener(type, callback, options);
    }
    async approve() {
        if (typeof this.#approved === 'undefined') {
            if (!('DeviceMotionEvent' in window))
                return this.#approved = false;
            try {
                if (typeof DeviceMotionEvent.requestPermission === 'function') {
                    const permissionState = await DeviceMotionEvent.requestPermission();
                    this.#approved = permissionState === 'granted';
                }
                else
                    this.#approved = true;
            }
            catch {
                this.#approved = false;
            }
        }
        return this.#approved;
    }
    #handleDeviceMotion;
    async start() {
        const approved = await this.approve();
        if (!approved)
            return false;
        window.addEventListener('devicemotion', this.#handleDeviceMotion);
        return true;
    }
    stop() {
        window.removeEventListener('devicemotion', this.#handleDeviceMotion);
    }
}
////////////////////////////////////////////////////////////////////////////////
// Use:
const shake = new Shake({ threshold: 15, timeout: 1000 });
shake.addEventListener('shake', ev => {
    console.log('Shake!', ev.detail.timeStamp, ev.detail.acceleration);
});
const button = document.getElementById('start');
if (button) {
    button.addEventListener('click', async () => {
        const approved = await shake.start();
        const div = document.body.appendChild(document.createElement('div'));
        div.textContent = `Approved: ${String(approved)}`;
        button.remove();
    }, { once: true });
}
<button id="start">Approve</button>