Skip to content

Commit 8375b1f

Browse files
authored
feat: native support for haptic feedback (#108)
1 parent 8fcd0e4 commit 8375b1f

File tree

13 files changed

+75
-3
lines changed

13 files changed

+75
-3
lines changed

android/src/main/java/com/rcttabview/RCTTabView.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import android.content.res.ColorStateList
55
import android.graphics.drawable.BitmapDrawable
66
import android.graphics.drawable.ColorDrawable
77
import android.graphics.drawable.Drawable
8+
import android.os.Build
89
import android.util.TypedValue
910
import android.view.Choreographer
11+
import android.view.HapticFeedbackConstants
1012
import android.view.MenuItem
1113
import android.view.View
1214
import androidx.appcompat.content.res.AppCompatResources
@@ -34,6 +36,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
3436
private var inactiveTintColor: Int? = null
3537
private val checkedStateSet = intArrayOf(android.R.attr.state_checked)
3638
private val uncheckedStateSet = intArrayOf(-android.R.attr.state_checked)
39+
private var hapticFeedbackEnabled = true
3740

3841
private val layoutCallback = Choreographer.FrameCallback {
3942
isLayoutEnqueued = false
@@ -59,6 +62,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
5962
putString("key", longPressedItem.key)
6063
}
6164
onTabLongPressedListener?.invoke(event)
65+
emitHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
6266
}
6367
}
6468

@@ -88,6 +92,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
8892
putString("key", selectedItem.key)
8993
}
9094
onTabSelectedListener?.invoke(event)
95+
emitHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
9196
}
9297
}
9398

@@ -200,6 +205,16 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
200205
itemActiveIndicatorColor = color
201206
}
202207

208+
fun setHapticFeedback(enabled: Boolean) {
209+
hapticFeedbackEnabled = enabled
210+
}
211+
212+
fun emitHapticFeedback(feedbackConstants: Int) {
213+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && hapticFeedbackEnabled) {
214+
this.performHapticFeedback(feedbackConstants)
215+
}
216+
}
217+
203218
private fun updateTintColors(item: MenuItem? = null) {
204219
// First let's check current item color.
205220
val currentItemTintColor = items?.find { it.title == item?.title }?.activeTintColor

android/src/main/java/com/rcttabview/RCTTabViewImpl.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ class RCTTabViewImpl {
7777
view.setInactiveTintColor(color)
7878
}
7979

80+
fun setHapticFeedbackEnabled(view: ReactBottomNavigationView, enabled: Boolean) {
81+
view.setHapticFeedback(enabled)
82+
}
83+
8084
fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any>? {
8185
return MapBuilder.of(
8286
PageSelectedEvent.EVENT_NAME,

android/src/newarch/RCTTabViewManager.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ class RCTTabViewManager(context: ReactApplicationContext) :
9898
return delegate
9999
}
100100

101+
override fun setHapticFeedbackEnabled(view: ReactBottomNavigationView?, value: Boolean) {
102+
if (view != null)
103+
tabViewImpl.setHapticFeedbackEnabled(view, value)
104+
}
105+
101106
public override fun measure(
102107
context: Context?,
103108
localData: ReadableMap?,

android/src/oldarch/RCTTabViewManager.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ class RCTTabViewManager(context: ReactApplicationContext) : SimpleViewManager<Re
109109
fun setDisablePageAnimations(view: ReactBottomNavigationView, flag: Boolean) {
110110
}
111111

112+
@ReactProp(name = "hapticFeedbackEnabled")
113+
fun setHapticFeedbackEnabled(view: ReactBottomNavigationView, value: Boolean) {
114+
tabViewImpl.setHapticFeedbackEnabled(view, value)
115+
}
116+
112117
class TabViewShadowNode() : LayoutShadowNode(),
113118
YogaMeasureFunction {
114119
private var mWidth = 0

docs/docs/docs/getting-started/how-is-it-different.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ TabView can turn in to a side bar on tvOS, iPadOS and macOS. This is controlled
4545

4646
SwiftUI's TabView offer built-in smooth animations between tabs.
4747

48+
### Out of the box support for Haptic Feedback
49+
50+
Using one prop you can add haptic feedback support to your tab bar on both Android and iOS. This can significantly enhance users experience.
51+
4852
## When to use JS Bottom Tabs
4953

5054
Using native components enforce certain constraints that we need to adapt to.

docs/docs/docs/guides/usage-with-react-navigation.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ Tab views using the sidebar adaptable style have an appearance
118118
- macOS and tvOS always show a sidebar.
119119
- visionOS shows an ornament and also shows a sidebar for secondary tabs within a `TabSection`.
120120

121+
#### `hapticFeedbackEnabled`
122+
123+
Whether to enable haptic feedback on tab press. Defaults to true.
124+
121125
### Options
122126

123127
The following options can be used to configure the screens in the navigator. These can be specified under `screenOptions` prop of `Tab.navigator` or `options` prop of `Tab.Screen`.

example/src/Examples/NativeBottomTabs.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const Tab = createNativeBottomTabNavigator();
1111
function NativeBottomTabs() {
1212
return (
1313
<Tab.Navigator
14+
hapticFeedbackEnabled={false}
1415
tabBarInactiveTintColor="#C57B57"
1516
tabBarActiveTintColor="#F7DBA7"
1617
barTintColor="#1E2D2F"

ios/Fabric/RCTTabViewComponentView.mm

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,11 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
140140
}
141141

142142
if (oldViewProps.inactiveTintColor != newViewProps.inactiveTintColor) {
143-
_tabViewProvider.inactiveTintColor = RCTUIColorFromSharedColor(newViewProps.inactiveTintColor);
143+
_tabViewProvider.inactiveTintColor = RCTUIColorFromSharedColor(newViewProps.inactiveTintColor);
144+
}
145+
146+
if (oldViewProps.hapticFeedbackEnabled != newViewProps.hapticFeedbackEnabled) {
147+
_tabViewProvider.hapticFeedbackEnabled = newViewProps.hapticFeedbackEnabled;
144148
}
145149

146150
[super updateProps:props oldProps:oldProps];

ios/RCTTabViewViewManager.mm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ - (instancetype)init
4242
RCT_EXPORT_VIEW_PROPERTY(barTintColor, UIColor)
4343
RCT_EXPORT_VIEW_PROPERTY(activeTintColor, UIColor)
4444
RCT_EXPORT_VIEW_PROPERTY(inactiveTintColor, UIColor)
45+
RCT_EXPORT_VIEW_PROPERTY(hapticFeedbackEnabled, BOOL)
4546

4647
// MARK: TabViewProviderDelegate
4748

ios/TabViewImpl.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class TabViewProps: ObservableObject {
1919
@Published var barTintColor: UIColor?
2020
@Published var activeTintColor: UIColor?
2121
@Published var inactiveTintColor: UIColor?
22+
@Published var hapticFeedbackEnabled: Bool = true
2223

2324
var selectedActiveTintColor: UIColor? {
2425
if let selectedPage = selectedPage,
@@ -80,6 +81,7 @@ struct TabViewImpl: View {
8081
.onTabItemLongPress({ index in
8182
if let key = props.items[safe: index]?.key {
8283
onLongPress(key)
84+
emitHapticFeedback(longPress: true)
8385
}
8486
})
8587
.tintColor(props.selectedActiveTintColor)
@@ -94,8 +96,23 @@ struct TabViewImpl: View {
9496
}
9597

9698
onSelect(newValue)
99+
emitHapticFeedback()
97100
}
98101
}
102+
103+
func emitHapticFeedback(longPress: Bool = false) {
104+
#if os(iOS)
105+
if !props.hapticFeedbackEnabled {
106+
return
107+
}
108+
109+
if longPress {
110+
UINotificationFeedbackGenerator().notificationOccurred(.success)
111+
} else {
112+
UISelectionFeedbackGenerator().selectionChanged()
113+
}
114+
#endif
115+
}
99116
}
100117

101118
private func configureAppearance(for appearanceType: String, appearance: UITabBarAppearance) -> UITabBarAppearance {

0 commit comments

Comments
 (0)