Haptics
expo-haptics' API on UIKit's feedback generators and Android's Vibrator, shipped as a Lucent package.
- iOS: UIKit's feedback generators may only run on the main thread, so each call is inside
main(). The compiler rejects them anywhere else. - Android:
available("android", 31)picksVibratorManagerorVibrator. Calling an API newer than the app's minimum without such a check is a compile error. - It ships as an npm package,
lucent-haptics, with the same API asexpo-haptics.
import { PLATFORM } from "lucent:platform";import { UIImpactFeedbackGenerator, UIImpactFeedbackGenerator_FeedbackStyle as FeedbackStyle, UINotificationFeedbackGenerator, UINotificationFeedbackGenerator_FeedbackType as FeedbackType, UISelectionFeedbackGenerator,} from "lucent:ios/UIKit";import { VibrationEffect, Vibrator, VibratorManager } from "lucent:android/android.os";import { appContext, available } from "lucent:android";import { main } from "lucent:thread";import { error } from "lucent:core";
export enum ImpactFeedbackStyle { Light = "light", Medium = "medium", Heavy = "heavy", Soft = "soft", Rigid = "rigid",}
export enum NotificationFeedbackType { Success = "success", Warning = "warning", Error = "error",}
// --- iOS ---------------------------------------------------------------------
function feedbackStyle(style: ImpactFeedbackStyle): FeedbackStyle { switch (style) { case ImpactFeedbackStyle.Light: return FeedbackStyle.light; case ImpactFeedbackStyle.Medium: return FeedbackStyle.medium; case ImpactFeedbackStyle.Heavy: return FeedbackStyle.heavy; case ImpactFeedbackStyle.Soft: return FeedbackStyle.soft; case ImpactFeedbackStyle.Rigid: return FeedbackStyle.rigid; }}
function feedbackType(type: NotificationFeedbackType): FeedbackType { switch (type) { case NotificationFeedbackType.Success: return FeedbackType.success; case NotificationFeedbackType.Warning: return FeedbackType.warning; case NotificationFeedbackType.Error: return FeedbackType.error; }}
// --- Android -----------------------------------------------------------------
type VibrationType = { timings: number[]; amplitudes: number[]; oldSDKPattern: number[] };
function impactType(style: ImpactFeedbackStyle): VibrationType { switch (style) { case ImpactFeedbackStyle.Light: case ImpactFeedbackStyle.Soft: return { timings: [0, 50], amplitudes: [0, 30], oldSDKPattern: [0, 20] }; case ImpactFeedbackStyle.Medium: case ImpactFeedbackStyle.Rigid: return { timings: [0, 43], amplitudes: [0, 50], oldSDKPattern: [0, 43] }; case ImpactFeedbackStyle.Heavy: return { timings: [0, 60], amplitudes: [0, 70], oldSDKPattern: [0, 61] }; }}
function notificationType(type: NotificationFeedbackType): VibrationType { switch (type) { case NotificationFeedbackType.Success: return { timings: [0, 40, 100, 40], amplitudes: [0, 50, 0, 60], oldSDKPattern: [0, 40, 100, 40], }; case NotificationFeedbackType.Warning: return { timings: [0, 40, 120, 60], amplitudes: [0, 40, 0, 60], oldSDKPattern: [0, 40, 120, 60], }; case NotificationFeedbackType.Error: return { timings: [0, 60, 100, 40, 80, 50], amplitudes: [0, 50, 0, 40, 0, 50], oldSDKPattern: [0, 60, 100, 40, 80, 50], }; }}
const selectionType: VibrationType = { timings: [0, 50], amplitudes: [0, 30], oldSDKPattern: [0, 70],};
function vibrator(): Vibrator { const context = appContext(); const v = available("android", 31) ? context.getSystemService(VibratorManager)?.defaultVibrator : context.getSystemService(Vibrator); if (!v) throw error("E_HAPTICS_NOT_SUPPORTED", "A haptics engine is not available on this device"); return v;}
/** Android takes durations as `long[]`: bigints, from the milliseconds the patterns hold. */function millis(durations: number[]): bigint[] { return durations.map((ms) => BigInt(ms));}
function vibrate(type: VibrationType): void { if (available("android", 26)) { vibrator().vibrate(VibrationEffect.createWaveform(millis(type.timings), type.amplitudes, -1)); } else { vibrator().vibrate(millis(type.oldSDKPattern), -1); }}
// --- The module --------------------------------------------------------------
export async function notificationAsync( type: NotificationFeedbackType = NotificationFeedbackType.Success,): Promise<void> { if (PLATFORM === "ios") { return main(() => { const generator = new UINotificationFeedbackGenerator(); generator.prepare(); generator.notificationOccurred(feedbackType(type)); }); } else { vibrate(notificationType(type)); }}
export async function impactAsync( style: ImpactFeedbackStyle = ImpactFeedbackStyle.Medium,): Promise<void> { if (PLATFORM === "ios") { return main(() => { const generator = new UIImpactFeedbackGenerator(feedbackStyle(style)); generator.prepare(); generator.impactOccurred(); }); } else { vibrate(impactType(style)); }}
export async function selectionAsync(): Promise<void> { if (PLATFORM === "ios") { return main(() => { const generator = new UISelectionFeedbackGenerator(); generator.prepare(); generator.selectionChanged(); }); } else { vibrate(selectionType); }}import { ImpactFeedbackStyle, impactAsync, selectionAsync } from "lucent-haptics";
await impactAsync(ImpactFeedbackStyle.Heavy);await selectionAsync();Source: examples/lucent-haptics.