WIP: better haptics API
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import {useMemo} from 'react'
|
||||
import {
|
||||
AndroidHaptics,
|
||||
impactAsync,
|
||||
ImpactFeedbackStyle,
|
||||
notificationAsync,
|
||||
NotificationFeedbackType,
|
||||
performAndroidHapticsAsync,
|
||||
} from 'expo-haptics'
|
||||
|
||||
import {useHapticsDisabled} from '#/state/preferences'
|
||||
import {IS_ANDROID, IS_IOS, IS_WEB} from '#/env'
|
||||
|
||||
export enum IOS_HAPTICS {
|
||||
Light = 'light',
|
||||
Medium = 'medium',
|
||||
Heavy = 'heavy',
|
||||
Rigid = 'rigid',
|
||||
Soft = 'soft',
|
||||
}
|
||||
|
||||
export enum ANDROID_HAPTICS {
|
||||
ClockTick = 'clock-tick',
|
||||
Confirm = 'confirm',
|
||||
ContextClick = 'context-click',
|
||||
DragStart = 'drag-start',
|
||||
GestureEnd = 'gesture-end',
|
||||
GestureStart = 'gesture-start',
|
||||
KeyboardPress = 'keyboard-press',
|
||||
KeyboardRelease = 'keyboard-release',
|
||||
KeyboardTap = 'keyboard-tap',
|
||||
LongPress = 'long-press',
|
||||
Reject = 'reject',
|
||||
SegmentFrequentTick = 'segment-frequent-tick',
|
||||
SegmentTick = 'segment-tick',
|
||||
TextHandleMove = 'text-handle-move',
|
||||
ToggleOff = 'toggle-off',
|
||||
ToggleOn = 'toggle-on',
|
||||
VirtualKey = 'virtual-key',
|
||||
VirtualKeyRelease = 'virtual-key-release',
|
||||
}
|
||||
|
||||
const IOS_TO_IMPACT: Record<IOS_HAPTICS, ImpactFeedbackStyle> = {
|
||||
[IOS_HAPTICS.Light]: ImpactFeedbackStyle.Light,
|
||||
[IOS_HAPTICS.Medium]: ImpactFeedbackStyle.Medium,
|
||||
[IOS_HAPTICS.Heavy]: ImpactFeedbackStyle.Heavy,
|
||||
[IOS_HAPTICS.Rigid]: ImpactFeedbackStyle.Rigid,
|
||||
[IOS_HAPTICS.Soft]: ImpactFeedbackStyle.Soft,
|
||||
}
|
||||
|
||||
const ANDROID_TO_HAPTICS: Record<ANDROID_HAPTICS, AndroidHaptics> = {
|
||||
[ANDROID_HAPTICS.ClockTick]: AndroidHaptics.Clock_Tick,
|
||||
[ANDROID_HAPTICS.Confirm]: AndroidHaptics.Confirm,
|
||||
[ANDROID_HAPTICS.ContextClick]: AndroidHaptics.Context_Click,
|
||||
[ANDROID_HAPTICS.DragStart]: AndroidHaptics.Drag_Start,
|
||||
[ANDROID_HAPTICS.GestureEnd]: AndroidHaptics.Gesture_End,
|
||||
[ANDROID_HAPTICS.GestureStart]: AndroidHaptics.Gesture_Start,
|
||||
[ANDROID_HAPTICS.KeyboardPress]: AndroidHaptics.Keyboard_Press,
|
||||
[ANDROID_HAPTICS.KeyboardRelease]: AndroidHaptics.Keyboard_Release,
|
||||
[ANDROID_HAPTICS.KeyboardTap]: AndroidHaptics.Keyboard_Tap,
|
||||
[ANDROID_HAPTICS.LongPress]: AndroidHaptics.Long_Press,
|
||||
[ANDROID_HAPTICS.Reject]: AndroidHaptics.Reject,
|
||||
[ANDROID_HAPTICS.SegmentFrequentTick]: AndroidHaptics.Segment_Frequent_Tick,
|
||||
[ANDROID_HAPTICS.SegmentTick]: AndroidHaptics.Segment_Tick,
|
||||
[ANDROID_HAPTICS.TextHandleMove]: AndroidHaptics.Text_Handle_Move,
|
||||
[ANDROID_HAPTICS.ToggleOff]: AndroidHaptics.Toggle_Off,
|
||||
[ANDROID_HAPTICS.ToggleOn]: AndroidHaptics.Toggle_On,
|
||||
[ANDROID_HAPTICS.VirtualKey]: AndroidHaptics.Virtual_Key,
|
||||
[ANDROID_HAPTICS.VirtualKeyRelease]: AndroidHaptics.Virtual_Key_Release,
|
||||
}
|
||||
|
||||
async function noop() {}
|
||||
|
||||
export function useHapticFeedback() {
|
||||
const isHapticsDisabled = useHapticsDisabled()
|
||||
|
||||
return useMemo(() => {
|
||||
const disabled = IS_WEB || isHapticsDisabled
|
||||
|
||||
if (disabled) {
|
||||
return {
|
||||
success: noop,
|
||||
error: noop,
|
||||
platform: noop,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: async () => {
|
||||
if (IS_IOS) {
|
||||
await notificationAsync(NotificationFeedbackType.Success)
|
||||
} else {
|
||||
await performAndroidHapticsAsync(AndroidHaptics.Confirm)
|
||||
}
|
||||
},
|
||||
error: async () => {
|
||||
if (IS_IOS) {
|
||||
await notificationAsync(NotificationFeedbackType.Error)
|
||||
} else {
|
||||
await performAndroidHapticsAsync(AndroidHaptics.Reject)
|
||||
}
|
||||
},
|
||||
platform: async (opts: {
|
||||
ios?: IOS_HAPTICS
|
||||
android?: ANDROID_HAPTICS
|
||||
}) => {
|
||||
if (IS_IOS && opts.ios != null) {
|
||||
await impactAsync(IOS_TO_IMPACT[opts.ios])
|
||||
} else if (IS_ANDROID && opts.android != null) {
|
||||
await performAndroidHapticsAsync(ANDROID_TO_HAPTICS[opts.android])
|
||||
}
|
||||
},
|
||||
}
|
||||
}, [isHapticsDisabled])
|
||||
}
|
||||
+6
-3
@@ -1,14 +1,17 @@
|
||||
import React from 'react'
|
||||
import {useCallback} from 'react'
|
||||
import * as Device from 'expo-device'
|
||||
import {impactAsync, ImpactFeedbackStyle} from 'expo-haptics'
|
||||
|
||||
import {useHapticsDisabled} from '#/state/preferences/disable-haptics'
|
||||
import {IS_IOS, IS_WEB} from '#/env'
|
||||
|
||||
/**
|
||||
* @deprecated use `useHapticFeedback` instead
|
||||
*/
|
||||
export function useHaptics() {
|
||||
const isHapticsDisabled = useHapticsDisabled()
|
||||
|
||||
return React.useCallback(
|
||||
return useCallback(
|
||||
(strength: 'Light' | 'Medium' | 'Heavy' = 'Medium') => {
|
||||
if (isHapticsDisabled || IS_WEB) {
|
||||
return
|
||||
@@ -18,7 +21,7 @@ export function useHaptics() {
|
||||
const style = IS_IOS
|
||||
? ImpactFeedbackStyle[strength]
|
||||
: ImpactFeedbackStyle.Light
|
||||
impactAsync(style)
|
||||
void impactAsync(style)
|
||||
|
||||
// DEV ONLY - show a toast when a haptic is meant to fire on simulator
|
||||
if (__DEV__ && !Device.isDevice) {
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {
|
||||
ANDROID_HAPTICS,
|
||||
IOS_HAPTICS,
|
||||
useHapticFeedback,
|
||||
} from '#/lib/haptic-feedback'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function Haptics() {
|
||||
const t = useTheme()
|
||||
const haptics = useHapticFeedback()
|
||||
|
||||
return (
|
||||
<View style={[a.gap_md]}>
|
||||
<Text style={[a.font_bold, a.text_5xl]}>Haptics</Text>
|
||||
|
||||
<Text style={[a.font_bold, a.text_2xl]}>Semantic</Text>
|
||||
<View style={[a.flex_row, a.gap_sm, a.flex_wrap]}>
|
||||
<Button
|
||||
label="Success haptic"
|
||||
size="small"
|
||||
color="primary"
|
||||
onPress={() => haptics.success()}>
|
||||
<ButtonText>success</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
label="Error haptic"
|
||||
size="small"
|
||||
color="negative"
|
||||
onPress={() => haptics.error()}>
|
||||
<ButtonText>error</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<Text style={[a.font_bold, a.text_2xl]}>iOS</Text>
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
UIImpactFeedbackGenerator.FeedbackStyle
|
||||
</Text>
|
||||
<View style={[a.flex_row, a.gap_sm, a.flex_wrap]}>
|
||||
{Object.entries(IOS_HAPTICS).map(([name, value]) => (
|
||||
<Button
|
||||
key={name}
|
||||
label={`${name} haptic`}
|
||||
size="small"
|
||||
color="secondary"
|
||||
onPress={() => haptics.platform({ios: value})}>
|
||||
<ButtonText>{name}</ButtonText>
|
||||
</Button>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<Text style={[a.font_bold, a.text_2xl]}>Android</Text>
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
performHapticFeedback
|
||||
</Text>
|
||||
<View style={[a.flex_row, a.gap_sm, a.flex_wrap]}>
|
||||
{Object.entries(ANDROID_HAPTICS).map(([name, value]) => (
|
||||
<Button
|
||||
key={name}
|
||||
label={`${name} haptic`}
|
||||
size="small"
|
||||
color="secondary"
|
||||
onPress={() => haptics.platform({android: value})}>
|
||||
<ButtonText>{name}</ButtonText>
|
||||
</Button>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import {Breakpoints} from './Breakpoints'
|
||||
import {Buttons} from './Buttons'
|
||||
import {Dialogs} from './Dialogs'
|
||||
import {Forms} from './Forms'
|
||||
import {Haptics} from './Haptics'
|
||||
import {Icons} from './Icons'
|
||||
import {Links} from './Links'
|
||||
import {Menus} from './Menus'
|
||||
@@ -107,6 +108,7 @@ export default function Storybook() {
|
||||
<Theming />
|
||||
</ThemeProvider>
|
||||
|
||||
<Haptics />
|
||||
<Toasts />
|
||||
<Buttons />
|
||||
<Forms />
|
||||
|
||||
Reference in New Issue
Block a user