@swmansion/react-native-bottom-sheet 0.7.1 → 0.7.3
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
CHANGED
|
@@ -12,6 +12,7 @@ React Native.
|
|
|
12
12
|
- Native implementation for optimal performance.
|
|
13
13
|
- Bring your own sheet surface.
|
|
14
14
|
- Dynamic, content‍-‍based sizing out of the box.
|
|
15
|
+
- Automatic handling of vertically scrollable children.
|
|
15
16
|
- Position tracking for driving UI tied to sheets.
|
|
16
17
|
- Programmatic‍-‍only detents for snap points unreachable
|
|
17
18
|
by dragging.
|
|
@@ -178,7 +179,7 @@ const position = useSharedValue(0);
|
|
|
178
179
|
}}
|
|
179
180
|
>
|
|
180
181
|
{/* ... */}
|
|
181
|
-
</BottomSheet
|
|
182
|
+
</BottomSheet>
|
|
182
183
|
```
|
|
183
184
|
|
|
184
185
|
## By [Software Mansion](https://swmansion.com)
|
|
@@ -59,6 +59,10 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
59
59
|
init {
|
|
60
60
|
clipChildren = false
|
|
61
61
|
clipToPadding = false
|
|
62
|
+
// Set directly rather than via the JSX prop because Fabric doesn't forward
|
|
63
|
+
// pointerEvents to the native view on Android. Without BOX_NONE the view
|
|
64
|
+
// itself becomes a touch target and its onTouchEvent would claim gestures
|
|
65
|
+
// that should go to children.
|
|
62
66
|
pointerEvents = PointerEvents.BOX_NONE
|
|
63
67
|
sheetContainer.clipChildren = false
|
|
64
68
|
sheetContainer.clipToPadding = false
|
|
@@ -327,6 +331,10 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
327
331
|
if (!isAtMaxDraggable) {
|
|
328
332
|
lastTouchY = y
|
|
329
333
|
requestDisallowInterceptTouchEvent(false)
|
|
334
|
+
// Cancel in-flight JS touches. React Native's JSTouchDispatcher
|
|
335
|
+
// processes events at the root view level before onInterceptTouchEvent
|
|
336
|
+
// runs, so without this the JS side never sees a cancel and Pressable
|
|
337
|
+
// would still fire onPress.
|
|
330
338
|
NativeGestureUtil.notifyNativeGestureStarted(this, ev)
|
|
331
339
|
return true
|
|
332
340
|
}
|
|
@@ -408,10 +416,28 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
408
416
|
}
|
|
409
417
|
|
|
410
418
|
// MARK: - Scroll view helpers
|
|
419
|
+
//
|
|
420
|
+
// Explicit scroll-view detection is required because Android's touch dispatch
|
|
421
|
+
// doesn't support mid-gesture handoff. Once a ScrollView claims a gesture it
|
|
422
|
+
// keeps it for the entire sequence, and it always claims (returns true from
|
|
423
|
+
// onTouchEvent) even when at the scroll boundary. Without this check the sheet
|
|
424
|
+
// could never collapse by dragging down when a ScrollView is at the top.
|
|
411
425
|
|
|
412
426
|
private fun isScrollViewAtTop(): Boolean {
|
|
413
427
|
val scrollView = findScrollView(sheetContainer) ?: return true
|
|
414
|
-
|
|
428
|
+
if (!isTouchInsideView(scrollView)) return true
|
|
429
|
+
val inverted = isViewInverted(scrollView)
|
|
430
|
+
return if (inverted) !scrollView.canScrollVertically(1) else !scrollView.canScrollVertically(-1)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private fun isTouchInsideView(target: View): Boolean {
|
|
434
|
+
val rect = android.graphics.Rect()
|
|
435
|
+
if (!target.getGlobalVisibleRect(rect)) return false
|
|
436
|
+
val myLocation = IntArray(2)
|
|
437
|
+
getLocationOnScreen(myLocation)
|
|
438
|
+
val touchScreenX = (myLocation[0] + initialTouchX).toInt()
|
|
439
|
+
val touchScreenY = (myLocation[1] + initialTouchY).toInt()
|
|
440
|
+
return rect.contains(touchScreenX, touchScreenY)
|
|
415
441
|
}
|
|
416
442
|
|
|
417
443
|
private fun findScrollView(view: View): View? {
|
|
@@ -424,6 +450,19 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
424
450
|
return null
|
|
425
451
|
}
|
|
426
452
|
|
|
453
|
+
private fun isViewInverted(view: View): Boolean {
|
|
454
|
+
val values = FloatArray(9)
|
|
455
|
+
var current: View? = view
|
|
456
|
+
while (current != null && current !== sheetContainer) {
|
|
457
|
+
if (!current.matrix.isIdentity) {
|
|
458
|
+
current.matrix.getValues(values)
|
|
459
|
+
if (values[android.graphics.Matrix.MSCALE_Y] < 0) return true
|
|
460
|
+
}
|
|
461
|
+
current = current.parent as? View
|
|
462
|
+
}
|
|
463
|
+
return false
|
|
464
|
+
}
|
|
465
|
+
|
|
427
466
|
// MARK: - Cleanup
|
|
428
467
|
|
|
429
468
|
fun destroy() {
|
|
@@ -42,6 +42,8 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
42
42
|
panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
|
|
43
43
|
panGesture.delegate = self
|
|
44
44
|
panGesture.cancelsTouchesInView = true
|
|
45
|
+
// Delay touch delivery to views so that Pressable doesn't flash its pressed
|
|
46
|
+
// state while the pan gesture is still being disambiguated.
|
|
45
47
|
panGesture.delaysTouchesBegan = true
|
|
46
48
|
panGesture.delaysTouchesEnded = false
|
|
47
49
|
sheetContainer.addGestureRecognizer(panGesture)
|
|
@@ -51,6 +53,30 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
51
53
|
fatalError("init(coder:) has not been implemented")
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
// RCTSurfaceTouchHandler dispatches touch events to JS independently of the
|
|
57
|
+
// pan gesture (it fires in touchesBegan: regardless of its recognizer state).
|
|
58
|
+
// We cache it here and toggle isEnabled in handlePan(.began) to force a
|
|
59
|
+
// touchesCancelled dispatch to JS, preventing Pressable from firing onPress
|
|
60
|
+
// during a sheet drag. This is the iOS equivalent of Android's
|
|
61
|
+
// NativeGestureUtil.notifyNativeGestureStarted.
|
|
62
|
+
private weak var surfaceTouchHandler: UIGestureRecognizer?
|
|
63
|
+
|
|
64
|
+
public override func didMoveToWindow() {
|
|
65
|
+
super.didMoveToWindow()
|
|
66
|
+
surfaceTouchHandler = nil
|
|
67
|
+
guard window != nil else { return }
|
|
68
|
+
var current: UIView? = superview
|
|
69
|
+
while let view = current {
|
|
70
|
+
for gr in view.gestureRecognizers ?? [] {
|
|
71
|
+
if NSStringFromClass(type(of: gr)).contains("TouchHandler") {
|
|
72
|
+
surfaceTouchHandler = gr
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
current = view.superview
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
54
80
|
public override func layoutSubviews() {
|
|
55
81
|
super.layoutSubviews()
|
|
56
82
|
guard bounds.width > 0, bounds.height > 0 else { return }
|
|
@@ -240,6 +266,10 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
240
266
|
case .began:
|
|
241
267
|
isPanning = true
|
|
242
268
|
setContentInteractionEnabled(false)
|
|
269
|
+
if let handler = surfaceTouchHandler {
|
|
270
|
+
handler.isEnabled = false
|
|
271
|
+
handler.isEnabled = true
|
|
272
|
+
}
|
|
243
273
|
gesture.setTranslation(.zero, in: self)
|
|
244
274
|
if let animator = activeAnimator {
|
|
245
275
|
stopDisplayLink()
|
|
@@ -314,6 +344,15 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
314
344
|
return nil
|
|
315
345
|
}
|
|
316
346
|
|
|
347
|
+
private func isViewInverted(_ view: UIView) -> Bool {
|
|
348
|
+
var current: UIView? = view
|
|
349
|
+
while let v = current, v !== sheetContainer {
|
|
350
|
+
if v.transform.d < 0 { return true }
|
|
351
|
+
current = v.superview
|
|
352
|
+
}
|
|
353
|
+
return false
|
|
354
|
+
}
|
|
355
|
+
|
|
317
356
|
public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
318
357
|
guard gestureRecognizer === panGesture else { return true }
|
|
319
358
|
|
|
@@ -324,14 +363,23 @@ public final class RNSBottomSheetHostingView: UIView {
|
|
|
324
363
|
guard draggable.count > 1 else { return false }
|
|
325
364
|
|
|
326
365
|
let maxDraggableIndex = draggable.last?.offset ?? 0
|
|
366
|
+
// Below max: allow drag in either direction to reach other detents.
|
|
327
367
|
guard targetIndex >= maxDraggableIndex else { return true }
|
|
328
|
-
|
|
368
|
+
// At max: only allow downward drag, and only when the scroll view (if any)
|
|
369
|
+
// is at its top edge — otherwise the scroll view should handle the gesture.
|
|
329
370
|
if velocity.y < 0 {
|
|
330
371
|
return false
|
|
331
372
|
}
|
|
332
373
|
|
|
333
|
-
let
|
|
334
|
-
|
|
374
|
+
guard let scrollView = firstScrollView(in: sheetContainer) else { return true }
|
|
375
|
+
let locationInScroll = panGesture.location(in: scrollView)
|
|
376
|
+
guard scrollView.bounds.contains(locationInScroll) else { return true }
|
|
377
|
+
let inverted = isViewInverted(scrollView)
|
|
378
|
+
if inverted {
|
|
379
|
+
let maxOffsetY = scrollView.contentSize.height - scrollView.bounds.height + scrollView.adjustedContentInset.bottom
|
|
380
|
+
return scrollView.contentOffset.y >= maxOffsetY
|
|
381
|
+
}
|
|
382
|
+
return scrollView.contentOffset.y <= 0
|
|
335
383
|
}
|
|
336
384
|
}
|
|
337
385
|
|
package/package.json
CHANGED