Skip to content

Commit 16f222b

Browse files
committed
feat: added system haptics
1 parent d2779d8 commit 16f222b

File tree

8 files changed

+450
-4
lines changed

8 files changed

+450
-4
lines changed

README.md

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,27 @@ setHapticsEnabled(false); // Disable all haptics
218218
setHapticsEnabled(true); // Re-enable haptics
219219
```
220220

221+
### System haptics (predefined OS-level feedback)
222+
223+
While the main purpose of this package is to support AHAP-style patterns (transient + continuous haptics with curves), system haptics are also available for completeness. These are simple, predefined OS-level feedback types that don't require pattern definitions. They can be called from the UI thread and don't require the haptic engine to be initialized.
224+
225+
```ts
226+
import {
227+
triggerImpact,
228+
triggerNotification,
229+
triggerSelection,
230+
} from 'react-native-ahaps';
231+
232+
// Impact feedback - simulates a physical collision
233+
triggerImpact('light'); // 'light' | 'medium' | 'heavy' | 'soft' | 'rigid'
234+
235+
// Notification feedback - for alerts and status updates
236+
triggerNotification('success'); // 'success' | 'warning' | 'error'
237+
238+
// Selection feedback - for picker wheels and toggles
239+
triggerSelection();
240+
```
241+
221242
## API
222243

223244
| Function | Purpose |
@@ -234,12 +255,17 @@ setHapticsEnabled(true); // Re-enable haptics
234255
| `startContinuousPlayer(playerId)` / `stopContinuousPlayer(playerId)` | Start/stop continuous playback for player |
235256
| `updateContinuousPlayer(playerId, intensityControl, sharpnessControl)` | Update intensity/sharpness for player |
236257
| `destroyContinuousPlayer(playerId)` | Destroy player and release resources |
258+
| `triggerImpact(style)` | Trigger impact feedback (collision simulation) |
259+
| `triggerNotification(type)` | Trigger notification feedback (alerts/status) |
260+
| `triggerSelection()` | Trigger selection feedback (pickers/toggles) |
237261

238262
## Types (inputs)
239263

240-
| Type | Values |
241-
| --------------------- | ---------------------------- |
242-
| `HapticParameterType` | `'intensity' \| 'sharpness'` |
264+
| Type | Values |
265+
| ------------------------ | ----------------------------------------------------- |
266+
| `HapticParameterType` | `'intensity' \| 'sharpness'` |
267+
| `HapticImpactStyle` | `'light' \| 'medium' \| 'heavy' \| 'soft' \| 'rigid'` |
268+
| `HapticNotificationType` | `'success' \| 'warning' \| 'error'` |
243269

244270
| `HapticEventParameter` | Type |
245271
| ---------------------- | --------------------- |

example/app/_layout.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export default function RootLayout() {
2424
<Stack.Screen name="playground" />
2525
<Stack.Screen name="recorder" />
2626
<Stack.Screen name="composer" />
27+
<Stack.Screen name="system-haptics" />
2728
<Stack.Screen
2829
name="import-modal"
2930
options={{

example/app/system-haptics.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { SystemHaptics } from '../src/screens/SystemHaptics';
2+
3+
export default function SystemHapticsRoute() {
4+
return <SystemHaptics />;
5+
}
6+

example/src/screens/Menu.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,20 @@ export function Menu() {
6666
</Text>
6767
</TouchableOpacity>
6868

69+
<TouchableOpacity
70+
style={[styles.menuItem, { backgroundColor: colors.card }]}
71+
onPress={() => router.push('/system-haptics')}
72+
>
73+
<Text style={[styles.menuText, { color: colors.text }]}>
74+
System Haptics
75+
</Text>
76+
<Text
77+
style={[styles.menuDescription, { color: colors.secondaryText }]}
78+
>
79+
Test predefined OS-level haptic feedback
80+
</Text>
81+
</TouchableOpacity>
82+
6983
<View style={[styles.toggleItem, { backgroundColor: colors.card }]}>
7084
<Text style={[styles.toggleText, { color: colors.text }]}>
7185
Haptics {hapticsEnabled ? 'Enabled' : 'Disabled'}
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import { useState } from 'react';
2+
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
3+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
4+
import { useTheme } from '../contexts/ThemeContext';
5+
import {
6+
triggerImpact,
7+
triggerNotification,
8+
triggerSelection,
9+
type HapticImpactStyle,
10+
type HapticNotificationType,
11+
} from 'react-native-ahaps';
12+
13+
const IMPACT_STYLES: HapticImpactStyle[] = [
14+
'light',
15+
'medium',
16+
'heavy',
17+
'soft',
18+
'rigid',
19+
];
20+
21+
const NOTIFICATION_TYPES: HapticNotificationType[] = [
22+
'success',
23+
'warning',
24+
'error',
25+
];
26+
27+
export const SystemHaptics = () => {
28+
const insets = useSafeAreaInsets();
29+
const { colors } = useTheme();
30+
31+
const [selectedImpactStyle, setSelectedImpactStyle] =
32+
useState<HapticImpactStyle>('medium');
33+
const [selectedNotificationType, setSelectedNotificationType] =
34+
useState<HapticNotificationType>('success');
35+
36+
return (
37+
<View
38+
style={[
39+
styles.container,
40+
{
41+
backgroundColor: colors.background,
42+
paddingTop: insets.top + 16,
43+
paddingBottom: insets.bottom + 16,
44+
},
45+
]}
46+
>
47+
<Text style={[styles.title, { color: colors.text }]}>System Haptics</Text>
48+
<Text style={[styles.subtitle, { color: colors.secondaryText }]}>
49+
Test predefined OS-level haptic feedback
50+
</Text>
51+
52+
{/* Impact Section */}
53+
<View style={styles.section}>
54+
<Text style={[styles.sectionTitle, { color: colors.text }]}>
55+
Impact Feedback
56+
</Text>
57+
<Text style={[styles.sectionDescription, { color: colors.secondaryText }]}>
58+
Simulates a physical collision
59+
</Text>
60+
61+
<View style={styles.pickerContainer}>
62+
{IMPACT_STYLES.map((style) => (
63+
<TouchableOpacity
64+
key={style}
65+
style={[
66+
styles.pickerItem,
67+
{
68+
backgroundColor:
69+
selectedImpactStyle === style
70+
? colors.blue
71+
: colors.card,
72+
},
73+
]}
74+
onPress={() => setSelectedImpactStyle(style)}
75+
>
76+
<Text
77+
style={[
78+
styles.pickerText,
79+
{
80+
color:
81+
selectedImpactStyle === style ? '#FFFFFF' : colors.text,
82+
},
83+
]}
84+
>
85+
{style}
86+
</Text>
87+
</TouchableOpacity>
88+
))}
89+
</View>
90+
91+
<TouchableOpacity
92+
style={[styles.triggerButton, { backgroundColor: colors.blue }]}
93+
onPress={() => triggerImpact(selectedImpactStyle)}
94+
>
95+
<Text style={styles.triggerButtonText}>Trigger Impact</Text>
96+
</TouchableOpacity>
97+
</View>
98+
99+
{/* Notification Section */}
100+
<View style={styles.section}>
101+
<Text style={[styles.sectionTitle, { color: colors.text }]}>
102+
Notification Feedback
103+
</Text>
104+
<Text style={[styles.sectionDescription, { color: colors.secondaryText }]}>
105+
For alerts and status updates
106+
</Text>
107+
108+
<View style={styles.pickerContainer}>
109+
{NOTIFICATION_TYPES.map((type) => (
110+
<TouchableOpacity
111+
key={type}
112+
style={[
113+
styles.pickerItem,
114+
styles.notificationPickerItem,
115+
{
116+
backgroundColor:
117+
selectedNotificationType === type
118+
? type === 'success'
119+
? colors.green
120+
: type === 'warning'
121+
? '#FF9500'
122+
: colors.accent
123+
: colors.card,
124+
},
125+
]}
126+
onPress={() => setSelectedNotificationType(type)}
127+
>
128+
<Text
129+
style={[
130+
styles.pickerText,
131+
{
132+
color:
133+
selectedNotificationType === type
134+
? '#FFFFFF'
135+
: colors.text,
136+
},
137+
]}
138+
>
139+
{type}
140+
</Text>
141+
</TouchableOpacity>
142+
))}
143+
</View>
144+
145+
<TouchableOpacity
146+
style={[
147+
styles.triggerButton,
148+
{
149+
backgroundColor:
150+
selectedNotificationType === 'success'
151+
? colors.green
152+
: selectedNotificationType === 'warning'
153+
? '#FF9500'
154+
: colors.accent,
155+
},
156+
]}
157+
onPress={() => triggerNotification(selectedNotificationType)}
158+
>
159+
<Text style={styles.triggerButtonText}>Trigger Notification</Text>
160+
</TouchableOpacity>
161+
</View>
162+
163+
{/* Selection Section */}
164+
<View style={styles.section}>
165+
<Text style={[styles.sectionTitle, { color: colors.text }]}>
166+
Selection Feedback
167+
</Text>
168+
<Text style={[styles.sectionDescription, { color: colors.secondaryText }]}>
169+
For picker wheels and toggles
170+
</Text>
171+
172+
<TouchableOpacity
173+
style={[styles.triggerButton, { backgroundColor: colors.purple }]}
174+
onPress={() => triggerSelection()}
175+
>
176+
<Text style={styles.triggerButtonText}>Trigger Selection</Text>
177+
</TouchableOpacity>
178+
</View>
179+
</View>
180+
);
181+
};
182+
183+
const styles = StyleSheet.create({
184+
container: {
185+
flex: 1,
186+
paddingHorizontal: 24,
187+
},
188+
title: {
189+
fontSize: 32,
190+
fontWeight: 'bold',
191+
marginBottom: 8,
192+
},
193+
subtitle: {
194+
fontSize: 16,
195+
marginBottom: 32,
196+
},
197+
section: {
198+
marginBottom: 32,
199+
},
200+
sectionTitle: {
201+
fontSize: 20,
202+
fontWeight: '600',
203+
marginBottom: 4,
204+
},
205+
sectionDescription: {
206+
fontSize: 14,
207+
marginBottom: 16,
208+
},
209+
pickerContainer: {
210+
flexDirection: 'row',
211+
flexWrap: 'wrap',
212+
gap: 8,
213+
marginBottom: 16,
214+
},
215+
pickerItem: {
216+
paddingHorizontal: 16,
217+
paddingVertical: 10,
218+
borderRadius: 20,
219+
},
220+
notificationPickerItem: {
221+
flex: 1,
222+
alignItems: 'center',
223+
},
224+
pickerText: {
225+
fontSize: 14,
226+
fontWeight: '500',
227+
textTransform: 'capitalize',
228+
},
229+
triggerButton: {
230+
paddingVertical: 16,
231+
borderRadius: 12,
232+
alignItems: 'center',
233+
},
234+
triggerButtonText: {
235+
color: '#FFFFFF',
236+
fontSize: 16,
237+
fontWeight: '600',
238+
},
239+
});
240+

ios/Ahap.swift

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import CoreHaptics
2+
import UIKit
23

34
struct AnyHapticAnimationState: HapticAnimationState {
45
var hapticEvents: [CHHapticEvent]
@@ -121,4 +122,64 @@ class Ahap: HybridAhapSpec {
121122
haptics.createHapticPlayers(for: [state])
122123
haptics.startHapticPlayer(for: state)
123124
}
125+
126+
// MARK: - System Haptics (Predefined OS-level feedback)
127+
128+
func triggerImpact(style: HapticImpactStyle) throws {
129+
guard haptics.hapticsEnabled else { return }
130+
131+
let feedbackStyle = style.toUIFeedbackStyle()
132+
let generator = UIImpactFeedbackGenerator(style: feedbackStyle)
133+
generator.prepare()
134+
generator.impactOccurred()
135+
}
136+
137+
func triggerNotification(type: HapticNotificationType) throws {
138+
guard haptics.hapticsEnabled else { return }
139+
140+
let feedbackType = type.toUIFeedbackType()
141+
let generator = UINotificationFeedbackGenerator()
142+
generator.prepare()
143+
generator.notificationOccurred(feedbackType)
144+
}
145+
146+
func triggerSelection() throws {
147+
guard haptics.hapticsEnabled else { return }
148+
149+
let generator = UISelectionFeedbackGenerator()
150+
generator.prepare()
151+
generator.selectionChanged()
152+
}
153+
}
154+
155+
// MARK: - Type Conversions
156+
157+
extension HapticImpactStyle {
158+
func toUIFeedbackStyle() -> UIImpactFeedbackGenerator.FeedbackStyle {
159+
switch self {
160+
case .rigid:
161+
return .rigid
162+
case .heavy:
163+
return .heavy
164+
case .medium:
165+
return .medium
166+
case .light:
167+
return .light
168+
case .soft:
169+
return .soft
170+
}
171+
}
172+
}
173+
174+
extension HapticNotificationType {
175+
func toUIFeedbackType() -> UINotificationFeedbackGenerator.FeedbackType {
176+
switch self {
177+
case .error:
178+
return .error
179+
case .success:
180+
return .success
181+
case .warning:
182+
return .warning
183+
}
184+
}
124185
}

0 commit comments

Comments
 (0)