我读过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的设备(:用户体验需要额外的步骤。用户必须自己调用传感器权限提示,而不是在第一次抖动时自己调用权限提示。这是通过在体验中的某个位置提供一个按钮来实现的,可能是在工具栏或模态中,该按钮调用requestPermission
neneneba 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")
}
})
}