收口移动触觉反馈边界

将 Expo 触觉反馈调用收口到 haptics 模块

让 HostBridge dispatch 只负责触觉反馈 payload 分发

同步原生壳结构门禁和架构文档清单
This commit is contained in:
2026-06-19 21:12:41 +08:00
parent 2dcc2fa634
commit 9eeaa6a4e0
7 changed files with 62 additions and 18 deletions
@@ -16,6 +16,8 @@ const clipboardPath = new URL('../src/host-bridge/clipboard.ts', import.meta.url
const clipboardSource = fs.readFileSync(clipboardPath, 'utf8');
const dispatchPath = new URL('../src/host-bridge/dispatch.ts', import.meta.url);
const dispatchSource = fs.readFileSync(dispatchPath, 'utf8');
const hapticsPath = new URL('../src/host-bridge/haptics.ts', import.meta.url);
const hapticsSource = fs.readFileSync(hapticsPath, 'utf8');
const notificationsPath = new URL('../src/host-bridge/notifications.ts', import.meta.url);
const notificationsSource = fs.readFileSync(notificationsPath, 'utf8');
const protocolPath = new URL('../src/host-bridge/protocol.ts', import.meta.url);
@@ -78,6 +80,7 @@ const requiredMobileShellSourceModules = [
'host-bridge/clipboard.ts',
'host-bridge/dispatch.ts',
'host-bridge/files.ts',
'host-bridge/haptics.ts',
'host-bridge/notifications.ts',
'host-bridge/protocol.ts',
'host-bridge/scanner.ts',
@@ -1645,6 +1648,29 @@ if (
);
}
if (
!hapticsSource.includes('normalizeHostBridgeHapticsImpactStyle(rawStyle)') ||
!hapticsSource.includes('Haptics.ImpactFeedbackStyle.Heavy') ||
!hapticsSource.includes('Haptics.ImpactFeedbackStyle.Medium') ||
!hapticsSource.includes('Haptics.ImpactFeedbackStyle.Light') ||
!hapticsSource.includes('await Haptics.impactAsync(toExpoImpactStyle(style))')
) {
throw new Error(
'mobile shell haptics bridge must normalize impact style with the shared HostBridge boundary',
);
}
if (
dispatchSource.includes("from 'expo-haptics'") ||
dispatchSource.includes('Haptics.impactAsync') ||
dispatchSource.includes('Haptics.ImpactFeedbackStyle') ||
!dispatchSource.includes('runMobileHapticsImpact(')
) {
throw new Error(
'mobile shell dispatch must delegate haptics IO to haptics.ts',
);
}
for (const snippet of [
'CameraView',
'Camera.requestCameraPermissionsAsync',
+6 -14
View File
@@ -1,4 +1,3 @@
import * as Haptics from 'expo-haptics';
import * as Linking from 'expo-linking';
import {
Appearance,
@@ -17,7 +16,6 @@ import {
normalizeHostBridgeBadgeCount,
normalizeHostBridgeColorScheme,
normalizeHostBridgeExternalUrlPayload,
normalizeHostBridgeHapticsImpactStyle,
normalizeHostBridgeLocalNotification,
type OpenExternalUrlPayload,
type SetBadgeCountPayload,
@@ -54,6 +52,7 @@ import {
readMobileClipboardText,
writeMobileClipboardText,
} from './clipboard';
import { runMobileHapticsImpact } from './haptics';
let currentShareTarget: unknown = null;
let navigation: MobileHostBridgeNavigation | null = null;
@@ -95,21 +94,14 @@ async function writeClipboard(payload: unknown) {
}
async function runHaptics(payload: unknown) {
const style = normalizeHostBridgeHapticsImpactStyle(
(payload as HapticsImpactPayload | undefined)?.style,
);
if (!style) {
if (
!(await runMobileHapticsImpact(
(payload as HapticsImpactPayload | undefined)?.style,
))
) {
throw invalidRequest('haptics impact style must be light, medium, or heavy');
}
const impactStyle =
style === 'heavy'
? Haptics.ImpactFeedbackStyle.Heavy
: style === 'medium'
? Haptics.ImpactFeedbackStyle.Medium
: Haptics.ImpactFeedbackStyle.Light;
await Haptics.impactAsync(impactStyle);
return true;
}
@@ -0,0 +1,24 @@
import * as Haptics from 'expo-haptics';
import {
normalizeHostBridgeHapticsImpactStyle,
type HostBridgeHapticsImpactStyle,
} from '../../../../packages/shared/src/contracts/hostBridge';
function toExpoImpactStyle(style: HostBridgeHapticsImpactStyle) {
return style === 'heavy'
? Haptics.ImpactFeedbackStyle.Heavy
: style === 'medium'
? Haptics.ImpactFeedbackStyle.Medium
: Haptics.ImpactFeedbackStyle.Light;
}
export async function runMobileHapticsImpact(rawStyle: unknown) {
const style = normalizeHostBridgeHapticsImpactStyle(rawStyle);
if (!style) {
return false;
}
await Haptics.impactAsync(toExpoImpactStyle(style));
return true;
}