@lodev09/react-native-true-sheet 3.6.9 → 3.6.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +5 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +5 -2
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +5 -9
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +30 -35
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +1 -1
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +91 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,6 +31,7 @@ The true native bottom sheet experience for your React Native Apps. 💩
|
|
|
31
31
|
|
|
32
32
|
- React Native >= 0.76 (Expo SDK 52+)
|
|
33
33
|
- New Architecture enabled (default in RN 0.76+)
|
|
34
|
+
- Xcode 26.2 (strongly recommended for better library functionality)
|
|
34
35
|
|
|
35
36
|
### Expo
|
|
36
37
|
|
|
@@ -45,6 +46,27 @@ yarn add @lodev09/react-native-true-sheet
|
|
|
45
46
|
cd ios && pod install
|
|
46
47
|
```
|
|
47
48
|
|
|
49
|
+
### EAS Build (iOS)
|
|
50
|
+
|
|
51
|
+
When using [EAS Build](https://docs.expo.dev/build/introduction/) to build your iOS app, you must configure your `eas.json` to use a build image that includes Xcode 26.2. Use `"image": "latest"` or choose from the [available build images](https://docs.expo.dev/build-reference/infrastructure/#ios-server-images):
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"build": {
|
|
56
|
+
"production": {
|
|
57
|
+
"ios": {
|
|
58
|
+
"image": "latest"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"development": {
|
|
62
|
+
"ios": {
|
|
63
|
+
"image": "latest"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
48
70
|
## Documentation
|
|
49
71
|
|
|
50
72
|
- [Example](example)
|
|
@@ -3,9 +3,11 @@ package com.lodev09.truesheet
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.view.View
|
|
5
5
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
6
|
+
import com.facebook.react.uimanager.events.EventDispatcher
|
|
6
7
|
import com.facebook.react.views.view.ReactViewGroup
|
|
7
8
|
|
|
8
9
|
interface TrueSheetContainerViewDelegate {
|
|
10
|
+
val eventDispatcher: EventDispatcher?
|
|
9
11
|
fun containerViewContentDidChangeSize(width: Int, height: Int)
|
|
10
12
|
fun containerViewHeaderDidChangeSize(width: Int, height: Int)
|
|
11
13
|
fun containerViewFooterDidChangeSize(width: Int, height: Int)
|
|
@@ -32,6 +34,9 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
|
|
|
32
34
|
var headerHeight: Int = 0
|
|
33
35
|
var footerHeight: Int = 0
|
|
34
36
|
|
|
37
|
+
override val eventDispatcher: EventDispatcher?
|
|
38
|
+
get() = delegate?.eventDispatcher
|
|
39
|
+
|
|
35
40
|
init {
|
|
36
41
|
// Allow footer to position outside container bounds
|
|
37
42
|
clipChildren = false
|
|
@@ -11,10 +11,11 @@ import com.facebook.react.uimanager.events.EventDispatcher
|
|
|
11
11
|
import com.facebook.react.views.view.ReactViewGroup
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Delegate interface for footer view size changes
|
|
14
|
+
* Delegate interface for footer view size changes and event dispatching
|
|
15
15
|
*/
|
|
16
16
|
interface TrueSheetFooterViewDelegate {
|
|
17
17
|
fun footerViewDidChangeSize(width: Int, height: Int)
|
|
18
|
+
val eventDispatcher: EventDispatcher?
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -30,7 +31,9 @@ class TrueSheetFooterView(private val reactContext: ThemedReactContext) :
|
|
|
30
31
|
RootView {
|
|
31
32
|
|
|
32
33
|
var delegate: TrueSheetFooterViewDelegate? = null
|
|
33
|
-
|
|
34
|
+
|
|
35
|
+
private val eventDispatcher: EventDispatcher?
|
|
36
|
+
get() = delegate?.eventDispatcher
|
|
34
37
|
|
|
35
38
|
private var lastWidth = 0
|
|
36
39
|
private var lastHeight = 0
|
|
@@ -42,7 +42,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
42
42
|
private val containerView: TrueSheetContainerView?
|
|
43
43
|
get() = viewController.getChildAt(0) as? TrueSheetContainerView
|
|
44
44
|
|
|
45
|
-
var eventDispatcher: EventDispatcher? = null
|
|
45
|
+
override var eventDispatcher: EventDispatcher? = null
|
|
46
46
|
|
|
47
47
|
// Initial present configuration (set by ViewManager before mount)
|
|
48
48
|
var initialDetentIndex: Int = -1
|
|
@@ -248,6 +248,10 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
248
248
|
viewController.insetAdjustment = insetAdjustment
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
fun setScrollable(scrollable: Boolean) {
|
|
252
|
+
viewController.scrollable = scrollable
|
|
253
|
+
}
|
|
254
|
+
|
|
251
255
|
// ==================== State Management ====================
|
|
252
256
|
|
|
253
257
|
/**
|
|
@@ -380,10 +384,6 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
380
384
|
override fun viewControllerWillPresent(index: Int, position: Float, detent: Float) {
|
|
381
385
|
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
382
386
|
eventDispatcher?.dispatchEvent(WillPresentEvent(surfaceId, id, index, position, detent))
|
|
383
|
-
|
|
384
|
-
// Enable touch event dispatching to React Native while sheet is visible
|
|
385
|
-
viewController.eventDispatcher = eventDispatcher
|
|
386
|
-
containerView?.footerView?.eventDispatcher = eventDispatcher
|
|
387
387
|
}
|
|
388
388
|
|
|
389
389
|
override fun viewControllerDidPresent(index: Int, position: Float, detent: Float) {
|
|
@@ -394,10 +394,6 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
394
394
|
override fun viewControllerWillDismiss() {
|
|
395
395
|
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
396
396
|
eventDispatcher?.dispatchEvent(WillDismissEvent(surfaceId, id))
|
|
397
|
-
|
|
398
|
-
// Disable touch event dispatching when sheet is dismissing
|
|
399
|
-
viewController.eventDispatcher = null
|
|
400
|
-
containerView?.footerView?.eventDispatcher = null
|
|
401
397
|
}
|
|
402
398
|
|
|
403
399
|
override fun viewControllerDidDismiss(hadParent: Boolean) {
|
|
@@ -45,6 +45,7 @@ import com.lodev09.truesheet.utils.ScreenUtils
|
|
|
45
45
|
data class DetentInfo(val index: Int, val position: Float)
|
|
46
46
|
|
|
47
47
|
interface TrueSheetViewControllerDelegate {
|
|
48
|
+
val eventDispatcher: EventDispatcher?
|
|
48
49
|
fun viewControllerWillPresent(index: Int, position: Float, detent: Float)
|
|
49
50
|
fun viewControllerDidPresent(index: Int, position: Float, detent: Float)
|
|
50
51
|
fun viewControllerWillDismiss()
|
|
@@ -155,9 +156,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
// Touch Dispatchers
|
|
158
|
-
internal var eventDispatcher: EventDispatcher? = null
|
|
159
159
|
private val jsTouchDispatcher = JSTouchDispatcher(this)
|
|
160
|
-
private
|
|
160
|
+
private val jsPointerDispatcher = JSPointerDispatcher(this)
|
|
161
|
+
|
|
162
|
+
private val eventDispatcher
|
|
163
|
+
get() = delegate?.eventDispatcher
|
|
161
164
|
|
|
162
165
|
// Detent Configuration
|
|
163
166
|
override var maxSheetHeight: Int? = null
|
|
@@ -170,6 +173,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
170
173
|
override var grabberOptions: GrabberOptions? = null
|
|
171
174
|
override var sheetBackgroundColor: Int? = null
|
|
172
175
|
var insetAdjustment: String = "automatic"
|
|
176
|
+
var scrollable: Boolean = false
|
|
177
|
+
set(value) {
|
|
178
|
+
field = value
|
|
179
|
+
coordinatorLayout?.scrollable = value
|
|
180
|
+
}
|
|
173
181
|
|
|
174
182
|
override var sheetCornerRadius: Float = DEFAULT_CORNER_RADIUS.dpToPx()
|
|
175
183
|
set(value) {
|
|
@@ -279,14 +287,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
279
287
|
private val dimViews: List<TrueSheetDimView>
|
|
280
288
|
get() = listOfNotNull(dimView, parentDimView)
|
|
281
289
|
|
|
282
|
-
// =============================================================================
|
|
283
|
-
// MARK: - Initialization
|
|
284
|
-
// =============================================================================
|
|
285
|
-
|
|
286
|
-
init {
|
|
287
|
-
jsPointerDispatcher = JSPointerDispatcher(this)
|
|
288
|
-
}
|
|
289
|
-
|
|
290
290
|
// =============================================================================
|
|
291
291
|
// MARK: - Sheet Creation & Cleanup
|
|
292
292
|
// =============================================================================
|
|
@@ -297,6 +297,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
297
297
|
// Create coordinator layout
|
|
298
298
|
coordinatorLayout = TrueSheetCoordinatorLayout(reactContext).apply {
|
|
299
299
|
delegate = this@TrueSheetViewController
|
|
300
|
+
scrollable = this@TrueSheetViewController.scrollable
|
|
300
301
|
}
|
|
301
302
|
|
|
302
303
|
sheetView = TrueSheetBottomSheetView(reactContext).apply {
|
|
@@ -1062,32 +1063,26 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1062
1063
|
}
|
|
1063
1064
|
}
|
|
1064
1065
|
|
|
1065
|
-
override fun handleException(t: Throwable) {
|
|
1066
|
-
reactContext.reactApplicationContext.handleException(RuntimeException(t))
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
1066
|
// =============================================================================
|
|
1070
|
-
// MARK: - Touch
|
|
1067
|
+
// MARK: - RootView Touch Handling
|
|
1071
1068
|
// =============================================================================
|
|
1072
1069
|
|
|
1073
1070
|
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
1074
|
-
// Footer needs special handling since it's positioned absolutely
|
|
1075
1071
|
val footer = containerView?.footerView
|
|
1076
|
-
if (footer != null && footer.
|
|
1072
|
+
if (footer != null && footer.isShown) {
|
|
1077
1073
|
val footerLocation = ScreenUtils.getScreenLocation(footer)
|
|
1078
|
-
val
|
|
1079
|
-
val
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
touchScreenY <= footerLocation[1] + footer.height
|
|
1074
|
+
val touchX = event.rawX.toInt()
|
|
1075
|
+
val touchY = event.rawY.toInt()
|
|
1076
|
+
|
|
1077
|
+
if (touchX >= footerLocation[0] &&
|
|
1078
|
+
touchX <= footerLocation[0] + footer.width &&
|
|
1079
|
+
touchY >= footerLocation[1] &&
|
|
1080
|
+
touchY <= footerLocation[1] + footer.height
|
|
1086
1081
|
) {
|
|
1087
1082
|
val localEvent = MotionEvent.obtain(event)
|
|
1088
1083
|
localEvent.setLocation(
|
|
1089
|
-
(
|
|
1090
|
-
(
|
|
1084
|
+
(touchX - footerLocation[0]).toFloat(),
|
|
1085
|
+
(touchY - footerLocation[1]).toFloat()
|
|
1091
1086
|
)
|
|
1092
1087
|
val handled = footer.dispatchTouchEvent(localEvent)
|
|
1093
1088
|
localEvent.recycle()
|
|
@@ -1100,7 +1095,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1100
1095
|
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
1101
1096
|
eventDispatcher?.let {
|
|
1102
1097
|
jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
|
|
1103
|
-
jsPointerDispatcher
|
|
1098
|
+
jsPointerDispatcher.handleMotionEvent(event, it, true)
|
|
1104
1099
|
}
|
|
1105
1100
|
return super.onInterceptTouchEvent(event)
|
|
1106
1101
|
}
|
|
@@ -1108,35 +1103,35 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1108
1103
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
1109
1104
|
eventDispatcher?.let {
|
|
1110
1105
|
jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
|
|
1111
|
-
jsPointerDispatcher
|
|
1106
|
+
jsPointerDispatcher.handleMotionEvent(event, it, false)
|
|
1112
1107
|
}
|
|
1113
1108
|
super.onTouchEvent(event)
|
|
1114
1109
|
return true
|
|
1115
1110
|
}
|
|
1116
1111
|
|
|
1117
1112
|
override fun onInterceptHoverEvent(event: MotionEvent): Boolean {
|
|
1118
|
-
eventDispatcher?.let { jsPointerDispatcher
|
|
1113
|
+
eventDispatcher?.let { jsPointerDispatcher.handleMotionEvent(event, it, true) }
|
|
1119
1114
|
return super.onHoverEvent(event)
|
|
1120
1115
|
}
|
|
1121
1116
|
|
|
1122
1117
|
override fun onHoverEvent(event: MotionEvent): Boolean {
|
|
1123
|
-
eventDispatcher?.let { jsPointerDispatcher
|
|
1118
|
+
eventDispatcher?.let { jsPointerDispatcher.handleMotionEvent(event, it, false) }
|
|
1124
1119
|
return super.onHoverEvent(event)
|
|
1125
1120
|
}
|
|
1126
1121
|
|
|
1127
1122
|
override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
|
|
1128
1123
|
eventDispatcher?.let {
|
|
1129
1124
|
jsTouchDispatcher.onChildStartedNativeGesture(ev, it)
|
|
1130
|
-
jsPointerDispatcher
|
|
1125
|
+
jsPointerDispatcher.onChildStartedNativeGesture(childView, ev, it)
|
|
1131
1126
|
}
|
|
1132
1127
|
}
|
|
1133
1128
|
|
|
1134
1129
|
override fun onChildEndedNativeGesture(childView: View, ev: MotionEvent) {
|
|
1135
1130
|
eventDispatcher?.let { jsTouchDispatcher.onChildEndedNativeGesture(ev, it) }
|
|
1136
|
-
jsPointerDispatcher
|
|
1131
|
+
jsPointerDispatcher.onChildEndedNativeGesture()
|
|
1137
1132
|
}
|
|
1138
1133
|
|
|
1139
|
-
override fun
|
|
1140
|
-
|
|
1134
|
+
override fun handleException(t: Throwable) {
|
|
1135
|
+
reactContext.reactApplicationContext.handleException(RuntimeException(t))
|
|
1141
1136
|
}
|
|
1142
1137
|
}
|
|
@@ -190,7 +190,7 @@ class TrueSheetViewManager :
|
|
|
190
190
|
|
|
191
191
|
@ReactProp(name = "scrollable", defaultBoolean = false)
|
|
192
192
|
override fun setScrollable(view: TrueSheetView, value: Boolean) {
|
|
193
|
-
|
|
193
|
+
view.setScrollable(value)
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
@ReactProp(name = "pageSizing", defaultBoolean = true)
|
|
@@ -2,7 +2,10 @@ package com.lodev09.truesheet.core
|
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.content.Context
|
|
5
|
-
import android.view.
|
|
5
|
+
import android.view.MotionEvent
|
|
6
|
+
import android.view.ViewConfiguration
|
|
7
|
+
import android.view.ViewGroup
|
|
8
|
+
import android.widget.ScrollView
|
|
6
9
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
7
10
|
import com.facebook.react.uimanager.PointerEvents
|
|
8
11
|
import com.facebook.react.uimanager.ReactPointerEventsView
|
|
@@ -22,15 +25,19 @@ class TrueSheetCoordinatorLayout(context: Context) :
|
|
|
22
25
|
ReactPointerEventsView {
|
|
23
26
|
|
|
24
27
|
var delegate: TrueSheetCoordinatorLayoutDelegate? = null
|
|
28
|
+
var scrollable: Boolean = false
|
|
29
|
+
|
|
30
|
+
private val touchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop
|
|
31
|
+
private var dragging = false
|
|
32
|
+
private var initialY = 0f
|
|
33
|
+
private var activePointerId = 0
|
|
25
34
|
|
|
26
35
|
init {
|
|
27
|
-
// Fill the entire screen
|
|
28
36
|
layoutParams = LayoutParams(
|
|
29
37
|
LayoutParams.MATCH_PARENT,
|
|
30
38
|
LayoutParams.MATCH_PARENT
|
|
31
39
|
)
|
|
32
40
|
|
|
33
|
-
// Ensure we don't clip the sheet during animations
|
|
34
41
|
clipChildren = false
|
|
35
42
|
clipToPadding = false
|
|
36
43
|
}
|
|
@@ -46,10 +53,87 @@ class TrueSheetCoordinatorLayout(context: Context) :
|
|
|
46
53
|
delegate?.coordinatorLayoutDidLayout(changed)
|
|
47
54
|
}
|
|
48
55
|
|
|
49
|
-
/**
|
|
50
|
-
* Allow pointer events to pass through to underlying views.
|
|
51
|
-
* The DimView and BottomSheetView handle their own touch interception.
|
|
52
|
-
*/
|
|
53
56
|
override val pointerEvents: PointerEvents
|
|
54
57
|
get() = PointerEvents.BOX_NONE
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Intercepts touch events for ScrollViews that can't scroll (content < viewport),
|
|
61
|
+
* allowing the sheet to be dragged in these cases.
|
|
62
|
+
*
|
|
63
|
+
* TODO: Remove this workaround once NestedScrollView is merged into react-native core.
|
|
64
|
+
* See: https://github.com/facebook/react-native/pull/44099
|
|
65
|
+
*/
|
|
66
|
+
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
|
|
67
|
+
if (!scrollable) {
|
|
68
|
+
return super.onInterceptTouchEvent(ev)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
val scrollView = findScrollView(this)
|
|
72
|
+
val cannotScroll = scrollView != null &&
|
|
73
|
+
scrollView.scrollY == 0 &&
|
|
74
|
+
!scrollView.canScrollVertically(1)
|
|
75
|
+
|
|
76
|
+
if (cannotScroll) {
|
|
77
|
+
when (ev.action and MotionEvent.ACTION_MASK) {
|
|
78
|
+
MotionEvent.ACTION_DOWN -> {
|
|
79
|
+
dragging = false
|
|
80
|
+
initialY = ev.y
|
|
81
|
+
activePointerId = ev.getPointerId(0)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
MotionEvent.ACTION_MOVE -> {
|
|
85
|
+
val pointerIndex = ev.findPointerIndex(activePointerId)
|
|
86
|
+
if (pointerIndex != -1) {
|
|
87
|
+
val y = ev.getY(pointerIndex)
|
|
88
|
+
val deltaY = initialY - y
|
|
89
|
+
if (kotlin.math.abs(deltaY) > touchSlop) {
|
|
90
|
+
dragging = true
|
|
91
|
+
parent?.requestDisallowInterceptTouchEvent(true)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
MotionEvent.ACTION_UP,
|
|
97
|
+
MotionEvent.ACTION_CANCEL -> {
|
|
98
|
+
dragging = false
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
dragging = false
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return dragging || super.onInterceptTouchEvent(ev)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@SuppressLint("ClickableViewAccessibility")
|
|
109
|
+
override fun onTouchEvent(ev: MotionEvent): Boolean {
|
|
110
|
+
if (dragging) {
|
|
111
|
+
when (ev.action and MotionEvent.ACTION_MASK) {
|
|
112
|
+
MotionEvent.ACTION_UP,
|
|
113
|
+
MotionEvent.ACTION_CANCEL -> {
|
|
114
|
+
dragging = false
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Let parent CoordinatorLayout handle the touch for BottomSheetBehavior
|
|
118
|
+
return super.onTouchEvent(ev)
|
|
119
|
+
}
|
|
120
|
+
return super.onTouchEvent(ev)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private fun findScrollView(view: android.view.View): ScrollView? {
|
|
124
|
+
if (view is ScrollView) {
|
|
125
|
+
return view
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (view is ViewGroup) {
|
|
129
|
+
for (i in 0 until view.childCount) {
|
|
130
|
+
val scrollView = findScrollView(view.getChildAt(i))
|
|
131
|
+
if (scrollView != null) {
|
|
132
|
+
return scrollView
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
55
139
|
}
|
package/package.json
CHANGED