使用Javascript检测抖动事件,适用于所有主要浏览器/设备(iOS、Android)



我读过Javascript。收听iPhone抖动事件?以及检测html5手机中的抖动,这为检测手机提供了一个很好的解决方案;摇动";事件:

<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设备,这个问题表明,应该为最近的iOS版本授予特殊权限。注意,这里的代码在库shake.js.中不容易使用

问题:截至2022年,有哪种方法可以检测";摇动";使用Javascript的活动,在主要浏览器(Firefox、Chrome、Safari(和移动设备(iOS、Android(上工作

如果有一个弹出窗口先请求权限,也没关系(比如弹出窗口请求地理位置请求的权限(。

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

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

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

注意:根据MDN相关文档页面上的兼容性数据,您列出的环境支持模块中使用的方法。(值得注意的是,桌面Safari根本不支持DeviceMotionEvent。(然而,我无法访问您列出的所有环境组合,以便自己进行测试,所以我将留给您。

TS游乐场

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();

我不确定SO代码段环境是否会导致降级问题,但我已经包含了从TS Playground链接编译的JS,以防万一:

"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>

设备抖动检测w/plain JS,无库

通用抖动检测的尝试。

对于非iOS:第一次摇晃会向用户显示允许使用传感器的权限提示。

对于iOS(或任何严格关于requestPermission API的设备(:用户体验需要额外的步骤。用户必须自己调用传感器权限提示,而不是在第一次抖动时自己调用权限提示。这是通过在体验中的某个位置提供一个按钮来实现的,可能是在工具栏或模态中,该按钮调用requestPermissionneneneba API。

除上述内容外,您还需要在HTTPS服务器上托管它(我使用了github-pages(。我也在localhost/本地wifi上工作,但那是另一个线程。特别是对于这个问题,我会避免在在线IDE(如Codepen(中测试它,即使它们是https,requestPermission可能不起作用。

建议:无论你做什么,在你的应用程序(或网站(中,如果你为用户独立存储状态(即他们是否允许权限(,那就太好了。如果他们击中";取消";,那么你就可以可靠地知道这一点,并可能周期性地告诉他们";嘿,你错过了这个很棒的功能"并再次提供权限提示(通过显式UI控件(。

HTML

<button id="btn_reqPermission" style="display: none;padding: 2em">
    Hey! This will be much better with sensors. Allow?
</button>
<div id="output_message"></div>

Javascript:

// PERMISSION BUTTON
var btn_reqPermission = document.getElementById("btn_reqPermission")
btn_reqPermission.addEventListener("click", () => { this.checkMotionPermission() })

// ON PAGE LOAD
this.checkMotionPermission()

// FUNCTIONS
async function checkMotionPermission() {
    // Any browser using requestPermission API
    if (typeof DeviceOrientationEvent.requestPermission === 'function') {
        // If previously granted, user will see no prompts and listeners get setup right away.
        // If error, we show special UI to the user.
        // FYI, "requestPermission" acts more like "check permission" on the device.
        await DeviceOrientationEvent.requestPermission()
        .then(permissionState => {
            if (permissionState == 'granted') {
                // Hide special UI; no longer needed
                btn_reqPermission.style.display = "none"
                this.setMotionListeners()
            }
        })
        .catch( (error) => {
            console.log("Error getting sensor permission: %O", error)
            // Show special UI to user, suggesting they should allow motion sensors. The tap-or-click on the button will invoke the permission dialog.
            btn_reqPermission.style.display = "block"
        })
    // All other browsers
    } else {
        this.setMotionListeners()
    }
}
async function setMotionListeners() {
    // ORIENTATION LISTENER
    await window.addEventListener('orientation', event => {
        console.log('Device orientation event: %O', event)
    })
    // MOTION LISTENER
    await window.addEventListener('devicemotion', event => {
        console.log('Device motion event: %O', event)
        // SHAKE EVENT
        // Using rotationRate, which essentially is velocity,
        // we check each axis (alpha, beta, gamma) whether they cross a threshold (e.g. 256).
        // Lower = more sensitive, higher = less sensitive. 256 works nice, imho.
        if ((event.rotationRate.alpha > 256 || event.rotationRate.beta > 256 || event.rotationRate.gamma > 256)) {
            this.output_message.innerHTML = "SHAKEN!"
            setTimeout(() => {
                this.message.innerHTML = null
            }, "2000")
        }
    })
}

最新更新