Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions entry/src/main/ets/components/CustomKeyConstants.ets
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* 编辑模式相关的颜色常量、预设色板、辅助接口和振动反馈函数。
*/

import { vibrator } from '@kit.SensorServiceKit';
import { DeviceVibrationCoordinator } from '../service/input/DeviceVibrationCoordinator';

// =============================================================================
// 接口
Expand Down Expand Up @@ -81,9 +81,7 @@ export function doKeyHaptic(level: string): void {
case 'heavy': duration = 80; break;
default: return;
}
try {
vibrator.startVibration({ type: 'time', duration }, { id: 0, usage: 'touch' });
} catch (_e) { /* ignore */ }
DeviceVibrationCoordinator.getInstance().playTouchHaptic(duration);
}

// =============================================================================
Expand Down
14 changes: 2 additions & 12 deletions entry/src/main/ets/components/SettingSlider.ets
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* - 自定义格式化显示
*/
import { AppColors, AppSizes } from '../common/Theme';
import { vibrator } from '@kit.SensorServiceKit';
import { DeviceVibrationCoordinator } from '../service/input/DeviceVibrationCoordinator';

/**
* 对数滑块转换工具类
Expand Down Expand Up @@ -142,17 +142,7 @@ export struct SettingSlider {
* 使用 20ms 短促振动以获得清晰的"咔嗒"手感
*/
private triggerSnapHaptic(): void {
try {
vibrator.startVibration({
type: 'time',
duration: 20
}, {
id: 0,
usage: 'touch'
})
} catch (_e) {
// 忽略振动失败
}
DeviceVibrationCoordinator.getInstance().playTouchHaptic(20);
}

/**
Expand Down
14 changes: 2 additions & 12 deletions entry/src/main/ets/components/virtual/VirtualController.ets
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
*/
import { InputEvent, InputType, ControllerButton, ControllerButtonEvent, ControllerAxis, ControllerAxisEvent } from '../../model/InputEvent';
import { AnalogStick, AnalogStickConfig } from './AnalogStick';
import { vibrator } from '@kit.SensorServiceKit';
import { DeviceVibrationCoordinator } from '../../service/input/DeviceVibrationCoordinator';
import { PreferencesUtil } from '../../utils/PreferencesUtil';
import { display } from '@kit.ArkUI';

Expand Down Expand Up @@ -616,17 +616,7 @@ export struct VirtualController {
}

private triggerVibration(): void {
try {
vibrator.startVibration({
type: 'time',
duration: 30
}, {
id: 0,
usage: 'touch'
});
} catch (e) {
// 忽略震动错误
}
DeviceVibrationCoordinator.getInstance().playTouchHaptic(30);
}

}
Expand Down
6 changes: 2 additions & 4 deletions entry/src/main/ets/components/virtual/VirtualKeyboard.ets
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import { picker } from '@kit.CoreFileKit';
import { fileIo } from '@kit.CoreFileKit';
import { PreferencesUtil } from '../../utils/PreferencesUtil';
import { vibrator } from '@kit.SensorServiceKit';
import { DeviceVibrationCoordinator } from '../../service/input/DeviceVibrationCoordinator';
import { display } from '@kit.ArkUI';

// =============================================================================
Expand Down Expand Up @@ -611,9 +611,7 @@ export struct VirtualKeyboard {
if (touchType === TouchType.Down) {
this.onVirtualKeyEvent(key.vk, true);
// 触感反馈
try {
vibrator.startVibration({ type: 'time', duration: 20 }, { id: 0, usage: 'touch' });
} catch (_e) {}
DeviceVibrationCoordinator.getInstance().playTouchHaptic(20);
} else if (touchType === TouchType.Up || touchType === TouchType.Cancel) {
this.onVirtualKeyEvent(key.vk, false);

Expand Down
278 changes: 278 additions & 0 deletions entry/src/main/ets/service/input/DeviceVibrationCoordinator.ets
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
/*
* Moonlight for HarmonyOS
* Copyright (C) 2024-2025 Moonlight/AlkaidLab
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*/

import { vibrator } from '@kit.SensorServiceKit';
import { deviceInfo } from '@kit.BasicServicesKit';

/**
* 统一协调所有机身马达写入。
*
* 按键触感只使用本机默认马达,并在脉冲期间临时独占执行器。游戏和音频仍可更新
* 来源状态,脉冲结束后通过回调恢复最新的混合输出。
*/
export class DeviceVibrationCoordinator {
private static readonly NATIVE_OPERATION_TIMEOUT_MS: number = 2000;
private static instance: DeviceVibrationCoordinator;

private hdHapticSupported: boolean | null = null;
private sourceVibratorDeviceId: number | null = null;
private sourceVibratorId: number = 0;

private touchHapticTimer: number = -1;
private touchHapticActive: boolean = false;
private touchHapticEpoch: number = 0;
private sourceEpoch: number = 0;
private vibrationQueue: Promise<void> = Promise.resolve();
private sourceResumeCallback: (() => void) | null = null;

static getInstance(): DeviceVibrationCoordinator {
if (!DeviceVibrationCoordinator.instance) {
DeviceVibrationCoordinator.instance = new DeviceVibrationCoordinator();
}
return DeviceVibrationCoordinator.instance;
}

setSourceResumeCallback(callback: () => void): void {
this.sourceResumeCallback = callback;
}

isHdHapticSupported(): boolean {
this.ensureSourceVibrator();
return this.hdHapticSupported ?? false;
}

isTouchHapticActive(): boolean {
return this.touchHapticActive;
}

playTouchHaptic(durationMs: number): void {
const duration = Math.max(1, Math.min(1000, Math.round(durationMs)));
const touchEpoch = ++this.touchHapticEpoch;
this.sourceEpoch++;
this.touchHapticActive = true;

if (this.touchHapticTimer !== -1) {
clearTimeout(this.touchHapticTimer);
this.touchHapticTimer = -1;
}
this.enqueueVibrationRequest((): Promise<void> =>
this.executeTouchHaptic(duration, touchEpoch));
}

startSourceVibration(effect: vibrator.VibrateEffect, usage: vibrator.Usage,
onRejected?: () => void): boolean {
if (this.isTouchHapticActive()) return false;

const sourceEpoch = ++this.sourceEpoch;
this.enqueueVibrationRequest((): Promise<void> =>
this.executeSourceVibration(effect, usage, sourceEpoch, onRejected));
return true;
}

stopSourceVibration(): void {
const sourceEpoch = ++this.sourceEpoch;
this.enqueueVibrationRequest((): Promise<void> =>
this.executeSourceStop(sourceEpoch));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

stopAll(): void {
this.sourceEpoch++;
this.touchHapticEpoch++;
this.touchHapticActive = false;
if (this.touchHapticTimer !== -1) {
clearTimeout(this.touchHapticTimer);
this.touchHapticTimer = -1;
}
this.enqueueVibrationRequest((): Promise<void> => this.stopLocalVibration());
}

private finishTouchHaptic(touchEpoch: number): void {
if (touchEpoch !== this.touchHapticEpoch || !this.touchHapticActive) return;
if (this.touchHapticTimer !== -1) {
clearTimeout(this.touchHapticTimer);
}
this.touchHapticTimer = -1;
this.touchHapticActive = false;
this.sourceResumeCallback?.();
}

private enqueueVibrationRequest(request: () => Promise<void>): void {
this.vibrationQueue = this.vibrationQueue
.then(request)
.catch((error: Error): void => {
console.warn('[VIBRATION] 机身振动队列异常: ' + error.message);
});
}

private executeTouchHaptic(duration: number, touchEpoch: number): Promise<void> {
if (touchEpoch !== this.touchHapticEpoch || !this.touchHapticActive) {
return Promise.resolve();
}

return this.stopLocalVibration().then((): Promise<void> => {
if (touchEpoch !== this.touchHapticEpoch || !this.touchHapticActive) {
return Promise.resolve();
}

try {
return this.withNativeTimeout(
vibrator.startVibration({
type: 'time',
duration: duration
}, this.createTouchVibrateAttribute()),
'机身按键触感启动'
).then((): void => {
if (touchEpoch !== this.touchHapticEpoch || !this.touchHapticActive) return;
this.touchHapticTimer = setTimeout((): void => {
this.finishTouchHaptic(touchEpoch);
}, duration);
}).catch((error: Error): void => {
if (touchEpoch !== this.touchHapticEpoch || !this.touchHapticActive) return;
console.warn('[VIBRATION] 机身按键触感失败: ' + error.message);
this.finishTouchHaptic(touchEpoch);
});
} catch (error) {
console.warn('[VIBRATION] 机身按键触感异常:', error);
this.finishTouchHaptic(touchEpoch);
return Promise.resolve();
}
});
}

private executeSourceVibration(effect: vibrator.VibrateEffect, usage: vibrator.Usage,
sourceEpoch: number, onRejected?: () => void): Promise<void> {
if (sourceEpoch !== this.sourceEpoch || this.isTouchHapticActive()) {
return Promise.resolve();
}

try {
return this.withNativeTimeout(
vibrator.startVibration(
effect,
this.createSourceVibrateAttribute(usage)
),
'机身来源振动启动'
).then((): void => {
if (sourceEpoch !== this.sourceEpoch || this.isTouchHapticActive()) return;
}).catch((error: Error): void => {
if (sourceEpoch !== this.sourceEpoch || this.isTouchHapticActive()) return;
console.warn('[VIBRATION] 机身振动失败: ' + error.message);
if (onRejected !== undefined) onRejected();
});
} catch (error) {
console.warn('[VIBRATION] 机身振动异常:', error);
if (sourceEpoch === this.sourceEpoch &&
!this.isTouchHapticActive() &&
onRejected !== undefined) {
onRejected();
}
return Promise.resolve();
}
}

private executeSourceStop(sourceEpoch: number): Promise<void> {
if (sourceEpoch !== this.sourceEpoch || this.isTouchHapticActive()) {
return Promise.resolve();
}
return this.stopLocalVibration().then((): void => {
if (sourceEpoch !== this.sourceEpoch || this.isTouchHapticActive()) return;
});
}

private stopLocalVibration(): Promise<void> {
try {
vibrator.stopVibrationSync();
return Promise.resolve();
} catch (_) {
try {
return this.withNativeTimeout(
vibrator.stopVibration(),
'机身振动停止'
).catch((error: Error): void => {
console.warn('[VIBRATION] 停止机身振动失败: ' + error.message);
});
} catch (error) {
console.warn('[VIBRATION] 停止机身振动异常:', error);
return Promise.resolve();
}
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private withNativeTimeout(operation: Promise<void>, operationName: string): Promise<void> {
return new Promise<void>((resolve, reject): void => {
let settled: boolean = false;
const timeoutId: number = setTimeout((): void => {
if (settled) return;
settled = true;
console.warn('[VIBRATION] ' + operationName + '超时,继续处理后续请求');
resolve();
}, DeviceVibrationCoordinator.NATIVE_OPERATION_TIMEOUT_MS);

operation.then((): void => {
if (settled) return;
settled = true;
clearTimeout(timeoutId);
resolve();
}).catch((error: Error): void => {
if (settled) return;
settled = true;
clearTimeout(timeoutId);
reject(error);
});
});
}

private createTouchVibrateAttribute(): vibrator.VibrateAttribute {
// 不设置 deviceId 即表示本机;id 0 与 touch 保持原有按键触感语义。
return {
id: 0,
usage: 'touch'
};
}

private createSourceVibrateAttribute(usage: vibrator.Usage): vibrator.VibrateAttribute {
this.ensureSourceVibrator();
const attribute: vibrator.VibrateAttribute = {
id: this.sourceVibratorId,
usage: usage
};
if (deviceInfo.sdkApiVersion >= 19 && this.sourceVibratorDeviceId !== null) {
attribute.deviceId = this.sourceVibratorDeviceId;
}
return attribute;
}

private ensureSourceVibrator(): void {
if (this.hdHapticSupported !== null) return;
if (deviceInfo.sdkApiVersion < 18) {
this.hdHapticSupported = false;
return;
}

try {
this.hdHapticSupported = vibrator.isHdHapticSupported();
if (deviceInfo.sdkApiVersion >= 19) {
const localVibrators = vibrator.getVibratorInfoSync()
.filter((info: vibrator.VibratorInfo): boolean => info.isLocalVibrator);
const localVibrator = localVibrators
.find((info: vibrator.VibratorInfo): boolean => info.isHdHapticSupported) ?? localVibrators[0];
if (localVibrator !== undefined) {
this.sourceVibratorDeviceId = localVibrator.deviceId;
this.sourceVibratorId = localVibrator.vibratorId;
this.hdHapticSupported = localVibrator.isHdHapticSupported;
}
}
console.info('[VIBRATION] 本地高清振动支持: ' + this.hdHapticSupported);
} catch (error) {
console.warn('[VIBRATION] 查询本地马达失败:', error);
this.hdHapticSupported = false;
}
}
}
Loading
Loading