@lodev09/react-native-true-sheet 4.0.0-beta.24 → 4.0.0-beta.25
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 +1 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +13 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +11 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +89 -9
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +100 -24
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +13 -10
- package/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt +31 -6
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +3 -8
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +14 -1
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +11 -17
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetGrabberView.kt +2 -4
- package/android/src/main/java/com/lodev09/truesheet/utils/ColorUtils.kt +19 -0
- package/ios/TrueSheetModule.mm +11 -2
- package/ios/TrueSheetView.h +2 -0
- package/ios/TrueSheetView.mm +53 -7
- package/ios/TrueSheetViewController.h +2 -0
- package/ios/TrueSheetViewController.mm +158 -4
- package/ios/core/RNScreensEventObserver.mm +18 -1
- package/ios/core/TrueSheetDimView.h +27 -0
- package/ios/core/TrueSheetDimView.mm +41 -0
- package/ios/core/TrueSheetKeyboardObserver.mm +11 -8
- package/lib/module/TrueSheet.js +88 -17
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +150 -37
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/TrueSheetProvider.web.js +13 -9
- package/lib/module/TrueSheetProvider.web.js.map +1 -1
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +2 -0
- package/lib/module/scrollable.js +28 -0
- package/lib/module/scrollable.js.map +1 -0
- package/lib/module/web/keyboard.js +69 -0
- package/lib/module/web/keyboard.js.map +1 -0
- package/lib/module/web/layout.js +1 -1
- package/lib/module/web/layout.js.map +1 -1
- package/lib/module/web/vaul/context.js.map +1 -1
- package/lib/module/web/vaul/index.js +18 -8
- package/lib/module/web/vaul/index.js.map +1 -1
- package/lib/module/web/vaul/use-snap-points.js +15 -10
- package/lib/module/web/vaul/use-snap-points.js.map +1 -1
- package/lib/typescript/src/TrueSheet.d.ts +14 -1
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +22 -2
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheetProvider.web.d.ts +5 -1
- package/lib/typescript/src/TrueSheetProvider.web.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +2 -0
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/navigation/types.d.ts +1 -1
- package/lib/typescript/src/navigation/types.d.ts.map +1 -1
- package/lib/typescript/src/scrollable.d.ts +2 -0
- package/lib/typescript/src/scrollable.d.ts.map +1 -0
- package/lib/typescript/src/web/keyboard.d.ts +15 -0
- package/lib/typescript/src/web/keyboard.d.ts.map +1 -0
- package/lib/typescript/src/web/layout.d.ts +1 -0
- package/lib/typescript/src/web/layout.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/context.d.ts +2 -1
- package/lib/typescript/src/web/vaul/context.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/index.d.ts +8 -1
- package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/types.d.ts +9 -0
- package/lib/typescript/src/web/vaul/types.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts +3 -2
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/TrueSheet.tsx +91 -14
- package/src/TrueSheet.types.ts +26 -2
- package/src/TrueSheet.web.tsx +170 -41
- package/src/TrueSheetProvider.web.tsx +22 -13
- package/src/fabric/TrueSheetViewNativeComponent.ts +2 -0
- package/src/navigation/types.ts +2 -0
- package/src/scrollable.ts +25 -0
- package/src/web/keyboard.ts +79 -0
- package/src/web/layout.ts +1 -1
- package/src/web/vaul/context.ts +2 -1
- package/src/web/vaul/index.tsx +20 -5
- package/src/web/vaul/types.ts +10 -0
- package/src/web/vaul/use-snap-points.ts +19 -9
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ The true native bottom sheet experience for your React Native Apps. 💩
|
|
|
34
34
|
* 🧭 **Expo Router** - First-class support via the `Sheet` layout from `/navigation/expo-router`
|
|
35
35
|
* 🍞 **`TrueSheetOverlay`** - Render toasts, dialogs, and other [overlays above your sheets](https://sheet.lodev09.com/guides/overlays) — no more `FullWindowOverlay`/`Modal` workarounds
|
|
36
36
|
* 🔍 **Element inspector** - React Native's dev menu inspector now works inside a presented sheet
|
|
37
|
+
* 🎨 **Dim color and opacity** - [Customize the dim](https://sheet.lodev09.com/guides/dimming#dim-color-and-opacity) behind the sheet with `dimColor` and `dimOpacity`
|
|
37
38
|
|
|
38
39
|
```sh
|
|
39
40
|
npx expo install @lodev09/react-native-true-sheet@beta
|
|
@@ -125,6 +125,9 @@ class TrueSheetModule(reactContext: ReactApplicationContext) :
|
|
|
125
125
|
fun dismissAll(animated: Boolean, promise: Promise) {
|
|
126
126
|
Handler(Looper.getMainLooper()).post {
|
|
127
127
|
try {
|
|
128
|
+
// Drop presents still waiting on the keyboard
|
|
129
|
+
viewRegistry.values.forEach { it.cancelPendingPresent() }
|
|
130
|
+
|
|
128
131
|
val rootSheet = TrueSheetStackManager.getRootSheet()
|
|
129
132
|
if (rootSheet == null) {
|
|
130
133
|
promise.resolve(null)
|
|
@@ -221,5 +224,15 @@ class TrueSheetModule(reactContext: ReactApplicationContext) :
|
|
|
221
224
|
*/
|
|
222
225
|
@JvmStatic
|
|
223
226
|
fun getSheetByTag(tag: Int): TrueSheetView? = viewRegistry[tag]
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Drop presents still waiting on the keyboard that would stack on [parent]
|
|
230
|
+
*/
|
|
231
|
+
@JvmStatic
|
|
232
|
+
fun cancelPendingPresents(parent: TrueSheetView, restoreFocus: Boolean = true) {
|
|
233
|
+
viewRegistry.values.forEach {
|
|
234
|
+
if (it.pendingPresentParent === parent) it.cancelPendingPresent(restoreFocus)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
224
237
|
}
|
|
225
238
|
}
|
|
@@ -3,6 +3,7 @@ package com.lodev09.truesheet
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.view.View
|
|
5
5
|
import android.view.ViewGroup
|
|
6
|
+
import android.view.ViewTreeObserver
|
|
6
7
|
import android.view.accessibility.AccessibilityEvent
|
|
7
8
|
import com.facebook.react.uimanager.StateWrapper
|
|
8
9
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
@@ -50,12 +51,22 @@ class TrueSheetOverlayView(private val reactContext: ThemedReactContext) :
|
|
|
50
51
|
|
|
51
52
|
// ==================== View Hierarchy Management ====================
|
|
52
53
|
|
|
54
|
+
// When the whole window goes away (e.g. activity destroyed), the container is mid-way through
|
|
55
|
+
// detaching its children by then, so removing the root view in onDetachedFromWindow crashes.
|
|
56
|
+
// This fires before that pass begins.
|
|
57
|
+
private val windowAttachListener = object : ViewTreeObserver.OnWindowAttachListener {
|
|
58
|
+
override fun onWindowAttached() {}
|
|
59
|
+
override fun onWindowDetached() = detachRootView()
|
|
60
|
+
}
|
|
61
|
+
|
|
53
62
|
override fun onAttachedToWindow() {
|
|
54
63
|
super.onAttachedToWindow()
|
|
64
|
+
viewTreeObserver.addOnWindowAttachListener(windowAttachListener)
|
|
55
65
|
attachRootView()
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
override fun onDetachedFromWindow() {
|
|
69
|
+
viewTreeObserver.removeOnWindowAttachListener(windowAttachListener)
|
|
59
70
|
detachRootView()
|
|
60
71
|
super.onDetachedFromWindow()
|
|
61
72
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package com.lodev09.truesheet
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
|
+
import android.graphics.Color
|
|
4
5
|
import android.view.View
|
|
5
6
|
import android.view.ViewGroup
|
|
6
7
|
import android.view.accessibility.AccessibilityEvent
|
|
@@ -49,16 +50,23 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
49
50
|
|
|
50
51
|
private var lastContainerWidth: Int = 0
|
|
51
52
|
private var lastContainerHeight: Int = 0
|
|
53
|
+
private var hasCommittedContainerSize: Boolean = false
|
|
52
54
|
|
|
53
55
|
var stateWrapper: StateWrapper? = null
|
|
54
56
|
set(value) {
|
|
55
57
|
field = value
|
|
58
|
+
if (value == null || hasCommittedContainerSize) return
|
|
56
59
|
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
if (value
|
|
60
|
-
|
|
60
|
+
// A preallocated view gets its initial state before the node is committed, so the size
|
|
61
|
+
// update is dropped. Resend until a committed state has it, or Yoga sizes the sheet to its parent.
|
|
62
|
+
if ((value.stateData?.getDouble("containerHeight") ?: 0.0) > 0.0) {
|
|
63
|
+
hasCommittedContainerSize = true
|
|
64
|
+
return
|
|
61
65
|
}
|
|
66
|
+
|
|
67
|
+
lastContainerWidth = 0
|
|
68
|
+
lastContainerHeight = 0
|
|
69
|
+
updateState(viewController.screenWidth, viewController.screenHeight)
|
|
62
70
|
}
|
|
63
71
|
|
|
64
72
|
// Debounce flag to coalesce rapid layout changes into a single sheet update
|
|
@@ -70,6 +78,14 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
70
78
|
// Screen event observer for react-native-screens integration
|
|
71
79
|
internal var screensEventObserver: RNScreensEventObserver? = null
|
|
72
80
|
|
|
81
|
+
// Present deferred until the keyboard hides; a dismiss before then cancels it
|
|
82
|
+
private var pendingPresentCallback: (() -> Unit)? = null
|
|
83
|
+
private var pendingPresentGeneration = 0
|
|
84
|
+
|
|
85
|
+
// Sheet the deferred present will stack on — dismissing it drops the present, like iOS
|
|
86
|
+
internal var pendingPresentParent: TrueSheetView? = null
|
|
87
|
+
private set
|
|
88
|
+
|
|
73
89
|
// ==================== Initialization ====================
|
|
74
90
|
|
|
75
91
|
init {
|
|
@@ -189,6 +205,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
189
205
|
|
|
190
206
|
cleanupScreenEventObserver()
|
|
191
207
|
didInitiallyPresent = false
|
|
208
|
+
cancelPendingPresent()
|
|
192
209
|
|
|
193
210
|
viewController.dismissPromise = { viewController.delegate = null }
|
|
194
211
|
|
|
@@ -247,6 +264,24 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
247
264
|
}
|
|
248
265
|
}
|
|
249
266
|
|
|
267
|
+
fun setDimColor(color: Int?) {
|
|
268
|
+
val resolved = color ?: Color.BLACK
|
|
269
|
+
if (viewController.dimColor == resolved) return
|
|
270
|
+
viewController.dimColor = resolved
|
|
271
|
+
if (viewController.isPresented) {
|
|
272
|
+
viewController.updateDimAmount()
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
fun setDimOpacity(opacity: Float) {
|
|
277
|
+
val resolved = if (opacity < 0f) TrueSheetViewController.DEFAULT_DIM_OPACITY else opacity.coerceIn(0f, 1f)
|
|
278
|
+
if (viewController.dimOpacity == resolved) return
|
|
279
|
+
viewController.dimOpacity = resolved
|
|
280
|
+
if (viewController.isPresented) {
|
|
281
|
+
viewController.updateDimAmount()
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
250
285
|
fun setDimmedDetentIndex(index: Int) {
|
|
251
286
|
if (viewController.dimmedDetentIndex == index) return
|
|
252
287
|
viewController.dimmedDetentIndex = index
|
|
@@ -364,10 +399,12 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
364
399
|
|
|
365
400
|
// For stacked sheets on the same screen, inherit parent's presenter screen tag.
|
|
366
401
|
// If parent was hidden by screen navigation, this sheet is on a different screen.
|
|
367
|
-
val
|
|
402
|
+
val parentObserver = viewController.parentSheetView?.screensEventObserver
|
|
403
|
+
val parentScreenTag = parentObserver?.presenterScreenTag ?: 0
|
|
368
404
|
val parentHiddenByScreen = viewController.parentSheetView?.viewController?.wasHiddenByScreen == true
|
|
369
405
|
if (parentScreenTag != 0 && !parentHiddenByScreen) {
|
|
370
406
|
presenterScreenTag = parentScreenTag
|
|
407
|
+
ancestorScreenTags = parentObserver?.ancestorScreenTags ?: emptySet()
|
|
371
408
|
} else {
|
|
372
409
|
capturePresenterScreenFromView(this@TrueSheetView)
|
|
373
410
|
}
|
|
@@ -399,7 +436,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
399
436
|
|
|
400
437
|
@UiThread
|
|
401
438
|
fun present(detentIndex: Int, animated: Boolean = true, promiseCallback: () -> Unit) {
|
|
402
|
-
if (viewController.isPresented) {
|
|
439
|
+
if (viewController.isPresented || pendingPresentCallback != null) {
|
|
403
440
|
RNLog.w(reactContext, "TrueSheet: sheet is already presented. Use resize() to change detent.")
|
|
404
441
|
promiseCallback()
|
|
405
442
|
return
|
|
@@ -414,8 +451,18 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
414
451
|
val shouldDismissKeyboard = isFocusedViewWithinSheet || viewController.isDimmedAtDetentIndex(detentIndex)
|
|
415
452
|
if (shouldDismissKeyboard && KeyboardUtils.isKeyboardVisible(reactContext)) {
|
|
416
453
|
viewController.saveFocusedView()
|
|
454
|
+
pendingPresentCallback = promiseCallback
|
|
455
|
+
pendingPresentParent = parentSheet
|
|
456
|
+
// A cancelled request's callback can still land (a timer below API 30) — only the latest may present
|
|
457
|
+
val generation = ++pendingPresentGeneration
|
|
417
458
|
KeyboardUtils.dismiss(this) {
|
|
418
|
-
post {
|
|
459
|
+
post {
|
|
460
|
+
if (generation != pendingPresentGeneration) return@post
|
|
461
|
+
val callback = pendingPresentCallback ?: return@post
|
|
462
|
+
pendingPresentCallback = null
|
|
463
|
+
pendingPresentParent = null
|
|
464
|
+
presentSheet(detentIndex, animated, callback)
|
|
465
|
+
}
|
|
419
466
|
}
|
|
420
467
|
// Blur like iOS so JS focus state stays in sync until restoreFocusedView() refocuses it
|
|
421
468
|
rootView.findFocus()?.clearFocus()
|
|
@@ -447,8 +494,31 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
447
494
|
viewController.handleBackPress()
|
|
448
495
|
}
|
|
449
496
|
|
|
497
|
+
/**
|
|
498
|
+
* Drops a present still waiting on the keyboard and resolves its caller. Returns whether one was pending.
|
|
499
|
+
* Skip [restoreFocus] when the focused view is going away with its dismissing sheet.
|
|
500
|
+
*/
|
|
501
|
+
fun cancelPendingPresent(restoreFocus: Boolean = true): Boolean {
|
|
502
|
+
val callback = pendingPresentCallback ?: return false
|
|
503
|
+
pendingPresentCallback = null
|
|
504
|
+
pendingPresentParent = null
|
|
505
|
+
if (restoreFocus) {
|
|
506
|
+
viewController.restoreFocusedView()
|
|
507
|
+
} else {
|
|
508
|
+
viewController.clearFocusedView()
|
|
509
|
+
}
|
|
510
|
+
callback()
|
|
511
|
+
return true
|
|
512
|
+
}
|
|
513
|
+
|
|
450
514
|
@UiThread
|
|
451
515
|
fun dismiss(animated: Boolean = true, promiseCallback: () -> Unit) {
|
|
516
|
+
// Dismissing before a deferred present starts cancels it — nothing was presented
|
|
517
|
+
if (cancelPendingPresent()) {
|
|
518
|
+
promiseCallback()
|
|
519
|
+
return
|
|
520
|
+
}
|
|
521
|
+
|
|
452
522
|
if (viewController.isBeingDismissed || !viewController.isPresented) {
|
|
453
523
|
RNLog.w(reactContext, "TrueSheet: sheet is already dismissed. No need to dismiss it again.")
|
|
454
524
|
promiseCallback()
|
|
@@ -471,6 +541,9 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
471
541
|
return
|
|
472
542
|
}
|
|
473
543
|
|
|
544
|
+
// A present waiting on the keyboard would stack above this sheet — drop it with the rest
|
|
545
|
+
TrueSheetModule.cancelPendingPresents(parent = this)
|
|
546
|
+
|
|
474
547
|
val sheetsAbove = TrueSheetStackManager.getSheetsAbove(this)
|
|
475
548
|
// Create snapshot only for topmost sheet (first in reversed list)
|
|
476
549
|
sheetsAbove.firstOrNull()?.viewController?.createSheetSnapshot()
|
|
@@ -599,6 +672,9 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
599
672
|
// ==================== TrueSheetViewControllerDelegate ====================
|
|
600
673
|
|
|
601
674
|
override fun viewControllerWillPresent(index: Int, position: Float, detent: Float) {
|
|
675
|
+
// Observe from the start so navigating away mid-present still hides the sheet
|
|
676
|
+
setupScreenEventObserver()
|
|
677
|
+
|
|
602
678
|
// Update parent sheet translation now that content is measured
|
|
603
679
|
TrueSheetStackManager.updateParentTranslation(this)
|
|
604
680
|
|
|
@@ -607,13 +683,14 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
607
683
|
}
|
|
608
684
|
|
|
609
685
|
override fun viewControllerDidPresent(index: Int, position: Float, detent: Float) {
|
|
610
|
-
setupScreenEventObserver()
|
|
611
|
-
|
|
612
686
|
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
613
687
|
eventDispatcher?.dispatchEvent(DidPresentEvent(surfaceId, id, index, position, detent))
|
|
614
688
|
}
|
|
615
689
|
|
|
616
690
|
override fun viewControllerWillDismiss() {
|
|
691
|
+
// Covers every dismiss path (swipe, back press, stack); the focused view leaves with this sheet
|
|
692
|
+
TrueSheetModule.cancelPendingPresents(parent = this, restoreFocus = false)
|
|
693
|
+
|
|
617
694
|
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
618
695
|
eventDispatcher?.dispatchEvent(WillDismissEvent(surfaceId, id))
|
|
619
696
|
}
|
|
@@ -635,6 +712,9 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
635
712
|
|
|
636
713
|
TrueSheetStackManager.unregisterSheet(this)
|
|
637
714
|
|
|
715
|
+
// The sheets below stop blending this sheet's dim
|
|
716
|
+
parent?.viewController?.takeIf { it.isPresented }?.updateDimAmount()
|
|
717
|
+
|
|
638
718
|
parent?.resetTranslation {
|
|
639
719
|
val parentController = parent.viewController
|
|
640
720
|
if (parentController.isPresented && !parentController.isBeingDismissed) {
|
|
@@ -3,6 +3,7 @@ package com.lodev09.truesheet
|
|
|
3
3
|
import android.animation.ValueAnimator
|
|
4
4
|
import android.annotation.SuppressLint
|
|
5
5
|
import android.graphics.Canvas
|
|
6
|
+
import android.graphics.Color
|
|
6
7
|
import android.os.Build
|
|
7
8
|
import android.view.MotionEvent
|
|
8
9
|
import android.view.View
|
|
@@ -11,6 +12,7 @@ import android.view.accessibility.AccessibilityEvent
|
|
|
11
12
|
import android.view.accessibility.AccessibilityNodeInfo
|
|
12
13
|
import android.widget.ImageView
|
|
13
14
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
15
|
+
import androidx.core.graphics.ColorUtils
|
|
14
16
|
import androidx.core.graphics.createBitmap
|
|
15
17
|
import androidx.core.view.isNotEmpty
|
|
16
18
|
import com.facebook.react.R
|
|
@@ -43,6 +45,7 @@ import com.lodev09.truesheet.utils.Insets
|
|
|
43
45
|
import com.lodev09.truesheet.utils.KeyboardUtils
|
|
44
46
|
import com.lodev09.truesheet.utils.ScreenUtils
|
|
45
47
|
import com.lodev09.truesheet.utils.TouchEventDeduper
|
|
48
|
+
import kotlin.math.roundToInt
|
|
46
49
|
|
|
47
50
|
// =============================================================================
|
|
48
51
|
// MARK: - Data Types & Delegate Protocol
|
|
@@ -122,6 +125,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
122
125
|
TrueSheetBottomSheetViewDelegate {
|
|
123
126
|
|
|
124
127
|
companion object {
|
|
128
|
+
const val DEFAULT_DIM_OPACITY = 0.5f
|
|
125
129
|
private const val DEFAULT_MAX_WIDTH = 640 // dp
|
|
126
130
|
private const val DEFAULT_CORNER_RADIUS = 16 // dp
|
|
127
131
|
private const val DEFAULT_PLACEMENT_OFFSET = 16 // dp
|
|
@@ -171,6 +175,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
171
175
|
// animation doesn't snap the sheet back to the stale currentDetentIndex.
|
|
172
176
|
private var pendingDetentIndex: Int = -1
|
|
173
177
|
|
|
178
|
+
private val isSettlingInPlace: Boolean
|
|
179
|
+
get() = !isBeingDismissed && pendingDetentIndex >= 0 && pendingDetentIndex == currentDetentIndex
|
|
180
|
+
|
|
174
181
|
private var interactionState: InteractionState = InteractionState.Idle
|
|
175
182
|
|
|
176
183
|
// A content/header/footer size change arrived mid-drag — reconfiguring then
|
|
@@ -190,6 +197,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
190
197
|
private var detentIndexBeforeKeyboard: Int = -1
|
|
191
198
|
private var isKeyboardDismissProgrammatic = false
|
|
192
199
|
|
|
200
|
+
// The keyboard hid under a touch (e.g. keyboardDismissMode="on-drag") — the
|
|
201
|
+
// keyboard stops stay until it lifts. Settling or moving the stops under a
|
|
202
|
+
// scrolling finger makes the behavior's nested pre-scroll snap the sheet and
|
|
203
|
+
// jump the content.
|
|
204
|
+
private var isKeyboardSettleDeferred = false
|
|
205
|
+
|
|
193
206
|
// Sheet top when a keyboard-floor drag was released; -1 once the settle target is known
|
|
194
207
|
private var keyboardSettleStartTop = -1
|
|
195
208
|
private var focusedViewBeforeBlur: View? = null
|
|
@@ -226,6 +239,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
226
239
|
// Appearance Configuration
|
|
227
240
|
var dimmed = true
|
|
228
241
|
var dimmedDetentIndex = 0
|
|
242
|
+
var dimColor: Int = Color.BLACK
|
|
243
|
+
var dimOpacity: Float = DEFAULT_DIM_OPACITY
|
|
244
|
+
private var dimProgress = 0f
|
|
229
245
|
override var grabber: Boolean = true
|
|
230
246
|
override var grabberOptions: GrabberOptions? = null
|
|
231
247
|
override var accessibilityOptions: AccessibilityOptions? = null
|
|
@@ -473,6 +489,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
473
489
|
detentIndexBeforeKeyboard = -1
|
|
474
490
|
pendingDetentIndex = -1
|
|
475
491
|
isKeyboardDismissProgrammatic = false
|
|
492
|
+
isKeyboardSettleDeferred = false
|
|
476
493
|
focusedViewBeforeBlur = null
|
|
477
494
|
shouldAnimatePresent = true
|
|
478
495
|
}
|
|
@@ -552,6 +569,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
552
569
|
delegate?.viewControllerDidAttemptDismiss()
|
|
553
570
|
}
|
|
554
571
|
|
|
572
|
+
override fun coordinatorLayoutDidEndTouch() {
|
|
573
|
+
if (!isKeyboardSettleDeferred) return
|
|
574
|
+
isKeyboardSettleDeferred = false
|
|
575
|
+
setupSheetDetentsForSizeChange()
|
|
576
|
+
}
|
|
577
|
+
|
|
555
578
|
// =============================================================================
|
|
556
579
|
// MARK: - TrueSheetDimViewDelegate
|
|
557
580
|
// =============================================================================
|
|
@@ -640,7 +663,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
640
663
|
return
|
|
641
664
|
}
|
|
642
665
|
|
|
643
|
-
|
|
666
|
+
// The slide-out owns the sheet now — a present settle landing mid-dismiss must not finish the present
|
|
667
|
+
if (!isPresented || isBeingDismissed) return
|
|
644
668
|
|
|
645
669
|
when (newState) {
|
|
646
670
|
BottomSheetBehavior.STATE_DRAGGING -> handleDragBegin(sheetView)
|
|
@@ -968,6 +992,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
968
992
|
return
|
|
969
993
|
}
|
|
970
994
|
|
|
995
|
+
// Bring the parent back alongside the slide-out (like iOS) instead of after it
|
|
996
|
+
parentSheetView?.resetTranslation()
|
|
997
|
+
|
|
971
998
|
sheet.animate()
|
|
972
999
|
.y(realScreenHeight.toFloat())
|
|
973
1000
|
.setDuration(DISMISS_DURATION)
|
|
@@ -1101,8 +1128,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1101
1128
|
|
|
1102
1129
|
fun setupSheetDetentsForSizeChange() {
|
|
1103
1130
|
// Reconfiguring while the container tracks the sheet can interrupt its
|
|
1104
|
-
// motion. Apply content size changes once the drag or resize settles
|
|
1105
|
-
|
|
1131
|
+
// motion. Apply content size changes once the drag or resize settles, or
|
|
1132
|
+
// the touch holding a deferred keyboard settle lifts.
|
|
1133
|
+
if (interactionState is InteractionState.Dragging || isSettling || isKeyboardSettleDeferred) {
|
|
1106
1134
|
hasPendingSizeChange = true
|
|
1107
1135
|
return
|
|
1108
1136
|
}
|
|
@@ -1156,6 +1184,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1156
1184
|
if (dimView == null) {
|
|
1157
1185
|
dimView = TrueSheetDimView(reactContext).apply {
|
|
1158
1186
|
delegate = this@TrueSheetViewController
|
|
1187
|
+
setDimColor(dimColor)
|
|
1159
1188
|
}
|
|
1160
1189
|
}
|
|
1161
1190
|
if (!parentDimVisible) {
|
|
@@ -1169,6 +1198,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1169
1198
|
if (parentDimView == null) {
|
|
1170
1199
|
parentDimView = TrueSheetDimView(reactContext).apply {
|
|
1171
1200
|
delegate = this@TrueSheetViewController
|
|
1201
|
+
setDimColor(dimColor)
|
|
1172
1202
|
}
|
|
1173
1203
|
}
|
|
1174
1204
|
parentDimView?.attach(parentBottomSheet, parentController.sheetCornerRadius)
|
|
@@ -1182,25 +1212,57 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1182
1212
|
}
|
|
1183
1213
|
|
|
1184
1214
|
fun updateDimAmount(sheetTop: Int? = null, animated: Boolean = false) {
|
|
1185
|
-
if (
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1215
|
+
if (dimmed) {
|
|
1216
|
+
// While keyboard is active or transitioning, or settling in place after a content
|
|
1217
|
+
// resize (the detent top moves before the sheet does), use the target detent position
|
|
1218
|
+
val top = if (isKeyboardActive || isSettlingInPlace) {
|
|
1219
|
+
detentCalculator.getSheetTopForDetentIndex(currentDetentIndex)
|
|
1220
|
+
} else {
|
|
1221
|
+
val keyboardOffset = if (isBeingDismissed) 0 else currentKeyboardInset
|
|
1222
|
+
(sheetTop ?: sheetView?.top ?: return) + keyboardOffset
|
|
1223
|
+
}
|
|
1194
1224
|
|
|
1195
|
-
|
|
1196
|
-
val targetAlpha = dimView?.calculateAlpha(
|
|
1225
|
+
dimProgress = dimView?.calculateProgress(
|
|
1197
1226
|
top,
|
|
1198
1227
|
dimmedDetentIndex,
|
|
1199
1228
|
detentCalculator::getSheetTopForDetentIndex
|
|
1200
1229
|
) ?: 0f
|
|
1201
|
-
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1230
|
+
applyDimAmount(animated)
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// The sheets below blend this sheet's dim into theirs
|
|
1234
|
+
var parent = parentSheetView?.viewController
|
|
1235
|
+
while (parent != null) {
|
|
1236
|
+
parent.applyDimAmount(animated)
|
|
1237
|
+
parent = parent.parentSheetView?.viewController
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/**
|
|
1242
|
+
* Dims by this sheet's progress, blending in the dim of the sheets presented on top by theirs,
|
|
1243
|
+
* so everything below the topmost sheet dims with its color and opacity.
|
|
1244
|
+
*/
|
|
1245
|
+
private fun applyDimAmount(animated: Boolean) {
|
|
1246
|
+
if (!dimmed) return
|
|
1247
|
+
|
|
1248
|
+
var color = dimColor
|
|
1249
|
+
var opacity = dimOpacity
|
|
1250
|
+
(delegate as? TrueSheetView)?.let { hostView ->
|
|
1251
|
+
TrueSheetStackManager.getSheetsAbove(hostView).asReversed().forEach {
|
|
1252
|
+
val child = it.viewController
|
|
1253
|
+
if (!child.dimmed) return@forEach
|
|
1254
|
+
val below = (1f - child.dimProgress) * opacity
|
|
1255
|
+
val above = child.dimProgress * child.dimOpacity
|
|
1256
|
+
if (below + above > 0f) color = ColorUtils.blendARGB(color, child.dimColor, above / (below + above))
|
|
1257
|
+
opacity = below + above
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// Keep opacity in the color so a transparent dim still receives touches.
|
|
1262
|
+
color = ColorUtils.setAlphaComponent(color, (Color.alpha(color) * opacity).roundToInt())
|
|
1263
|
+
dimViews.forEach {
|
|
1264
|
+
it.setDimColor(color)
|
|
1265
|
+
if (animated) it.animate().alpha(dimProgress).setDuration(200).start() else it.alpha = dimProgress
|
|
1204
1266
|
}
|
|
1205
1267
|
}
|
|
1206
1268
|
|
|
@@ -1272,6 +1334,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1272
1334
|
?: reactContext.currentActivity?.currentFocus
|
|
1273
1335
|
}
|
|
1274
1336
|
|
|
1337
|
+
fun clearFocusedView() {
|
|
1338
|
+
focusedViewBeforeBlur = null
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1275
1341
|
fun restoreFocusedView() {
|
|
1276
1342
|
val viewToFocus = focusedViewBeforeBlur ?: return
|
|
1277
1343
|
focusedViewBeforeBlur = null
|
|
@@ -1314,13 +1380,16 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1314
1380
|
return
|
|
1315
1381
|
}
|
|
1316
1382
|
val restoring = !isBeingDismissed && detentIndexBeforeKeyboard >= 0
|
|
1383
|
+
if (restoring) {
|
|
1384
|
+
currentDetentIndex = detentIndexBeforeKeyboard
|
|
1385
|
+
}
|
|
1317
1386
|
|
|
1318
|
-
//
|
|
1319
|
-
//
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1387
|
+
// A scroll-committed keyboard detent still holds the keyboard-shifted
|
|
1388
|
+
// top too — reconfigure so the sheet settles with the keyboard, once
|
|
1389
|
+
// any touch on the sheet lifts
|
|
1390
|
+
if (!isBeingDismissed && coordinatorLayout?.isTouchingSheet == true) {
|
|
1391
|
+
isKeyboardSettleDeferred = true
|
|
1392
|
+
} else if (!isBeingDismissed || isKeyboardDismissProgrammatic) {
|
|
1324
1393
|
setupSheetDetents()
|
|
1325
1394
|
}
|
|
1326
1395
|
|
|
@@ -1343,7 +1412,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1343
1412
|
return
|
|
1344
1413
|
}
|
|
1345
1414
|
detentIndexBeforeKeyboard = -1
|
|
1346
|
-
|
|
1415
|
+
if (!isKeyboardSettleDeferred) {
|
|
1416
|
+
setupSheetDetents(applyState = false)
|
|
1417
|
+
}
|
|
1347
1418
|
positionFooter()
|
|
1348
1419
|
updateDimAmount(
|
|
1349
1420
|
sheetTop = detentCalculator.getSheetTopForDetentIndex(currentDetentIndex),
|
|
@@ -1458,6 +1529,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1458
1529
|
delegate?.viewControllerWillBlur()
|
|
1459
1530
|
delegate?.viewControllerWillDismiss()
|
|
1460
1531
|
parentSheetView?.viewControllerWillFocus()
|
|
1532
|
+
|
|
1533
|
+
// Dismissed mid-present: finishPresent() won't run. Settle the pending present well
|
|
1534
|
+
// before didDismiss so JS clears isPresenting and unmounts the lazy native view
|
|
1535
|
+
presentPromise?.invoke()
|
|
1536
|
+
presentPromise = null
|
|
1461
1537
|
}
|
|
1462
1538
|
|
|
1463
1539
|
private fun emitDidDismissEvents() {
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
package com.lodev09.truesheet
|
|
2
2
|
|
|
3
3
|
import android.view.WindowManager
|
|
4
|
-
import com.facebook.react.bridge.ColorPropConverter
|
|
5
4
|
import com.facebook.react.bridge.ReadableArray
|
|
6
5
|
import com.facebook.react.bridge.ReadableMap
|
|
7
|
-
import com.facebook.react.bridge.ReadableType
|
|
8
6
|
import com.facebook.react.module.annotations.ReactModule
|
|
9
7
|
import com.facebook.react.uimanager.PixelUtil.dpToPx
|
|
10
8
|
import com.facebook.react.uimanager.PointerEvents
|
|
@@ -20,6 +18,7 @@ import com.facebook.react.viewmanagers.TrueSheetViewManagerInterface
|
|
|
20
18
|
import com.lodev09.truesheet.core.AccessibilityOptions
|
|
21
19
|
import com.lodev09.truesheet.core.GrabberOptions
|
|
22
20
|
import com.lodev09.truesheet.events.*
|
|
21
|
+
import com.lodev09.truesheet.utils.getColorOrNull
|
|
23
22
|
|
|
24
23
|
/**
|
|
25
24
|
* ViewManager for TrueSheetView - Fabric architecture
|
|
@@ -120,13 +119,7 @@ class TrueSheetViewManager :
|
|
|
120
119
|
override fun setDetentBackgrounds(view: TrueSheetView, value: ReadableArray?) {
|
|
121
120
|
view.setDetentBackgrounds(
|
|
122
121
|
(0 until (value?.size() ?: 0)).map { index ->
|
|
123
|
-
|
|
124
|
-
if (entry != null && entry.hasKey("color") && !entry.isNull("color")) {
|
|
125
|
-
val color = if (entry.getType("color") == ReadableType.Map) entry.getMap("color") else entry.getDouble("color")
|
|
126
|
-
ColorPropConverter.getColor(color, view.context)
|
|
127
|
-
} else {
|
|
128
|
-
null
|
|
129
|
-
}
|
|
122
|
+
value?.getMap(index)?.getColorOrNull("color", view.context)
|
|
130
123
|
}
|
|
131
124
|
)
|
|
132
125
|
}
|
|
@@ -164,7 +157,7 @@ class TrueSheetViewManager :
|
|
|
164
157
|
} else {
|
|
165
158
|
null
|
|
166
159
|
},
|
|
167
|
-
color =
|
|
160
|
+
color = options.getColorOrNull("color", view.context),
|
|
168
161
|
adaptive = if (options.hasKey("adaptive")) options.getBoolean("adaptive") else true
|
|
169
162
|
)
|
|
170
163
|
view.setGrabberOptions(grabberOptions)
|
|
@@ -209,6 +202,16 @@ class TrueSheetViewManager :
|
|
|
209
202
|
view.setDimmed(dimmed)
|
|
210
203
|
}
|
|
211
204
|
|
|
205
|
+
@ReactProp(name = "dimColor", customType = "Color")
|
|
206
|
+
override fun setDimColor(view: TrueSheetView, color: Int?) {
|
|
207
|
+
view.setDimColor(color)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@ReactProp(name = "dimOpacity", defaultDouble = -1.0)
|
|
211
|
+
override fun setDimOpacity(view: TrueSheetView, opacity: Double) {
|
|
212
|
+
view.setDimOpacity(opacity.toFloat())
|
|
213
|
+
}
|
|
214
|
+
|
|
212
215
|
@ReactProp(name = "dimmedDetentIndex", defaultInt = 0)
|
|
213
216
|
override fun setDimmedDetentIndex(view: TrueSheetView, index: Int) {
|
|
214
217
|
view.setDimmedDetentIndex(index)
|
|
@@ -22,6 +22,15 @@ class RNScreensEventObserver : EventDispatcherListener {
|
|
|
22
22
|
private var eventDispatcher: EventDispatcher? = null
|
|
23
23
|
var presenterScreenTag: Int = 0
|
|
24
24
|
|
|
25
|
+
// Screens enclosing the presenter. react-native-screens only dispatches lifecycle
|
|
26
|
+
// events for stack screens, so a presenter inside a plain screen container (e.g. a
|
|
27
|
+
// bottom tab) never receives them and an enclosing stack screen has to stand in.
|
|
28
|
+
var ancestorScreenTags: Set<Int> = emptySet()
|
|
29
|
+
|
|
30
|
+
// Only the screen that hid the sheet brings it back. An enclosing screen reappearing
|
|
31
|
+
// must not reveal a sheet whose own screen is still covered inside it.
|
|
32
|
+
private var disappearedScreenTag: Int = 0
|
|
33
|
+
|
|
25
34
|
fun startObserving(dispatcher: EventDispatcher?) {
|
|
26
35
|
if (eventDispatcher != null || dispatcher == null) return
|
|
27
36
|
|
|
@@ -36,24 +45,40 @@ class RNScreensEventObserver : EventDispatcherListener {
|
|
|
36
45
|
|
|
37
46
|
fun capturePresenterScreenFromView(view: View?) {
|
|
38
47
|
presenterScreenTag = 0
|
|
48
|
+
val ancestors = mutableSetOf<Int>()
|
|
39
49
|
|
|
40
50
|
var current: View? = view
|
|
41
51
|
while (current != null) {
|
|
42
52
|
if (isScreenView(current)) {
|
|
43
|
-
presenterScreenTag
|
|
44
|
-
|
|
53
|
+
if (presenterScreenTag == 0) {
|
|
54
|
+
presenterScreenTag = current.id
|
|
55
|
+
} else {
|
|
56
|
+
ancestors.add(current.id)
|
|
57
|
+
}
|
|
45
58
|
}
|
|
46
59
|
current = (current.parent as? View)
|
|
47
60
|
}
|
|
61
|
+
|
|
62
|
+
ancestorScreenTags = ancestors
|
|
48
63
|
}
|
|
49
64
|
|
|
50
65
|
override fun onEventDispatch(event: Event<*>) {
|
|
51
|
-
// Only process events for the presenter screen
|
|
52
|
-
if (presenterScreenTag == 0
|
|
66
|
+
// Only process events for the presenter screen and the screens enclosing it
|
|
67
|
+
if (presenterScreenTag == 0) return
|
|
68
|
+
if (event.viewTag != presenterScreenTag && event.viewTag !in ancestorScreenTags) return
|
|
53
69
|
|
|
54
70
|
when (event.eventName) {
|
|
55
|
-
"topWillDisappear" ->
|
|
56
|
-
|
|
71
|
+
"topWillDisappear" -> {
|
|
72
|
+
if (disappearedScreenTag != 0) return
|
|
73
|
+
disappearedScreenTag = event.viewTag
|
|
74
|
+
delegate?.presenterScreenWillDisappear()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
"topWillAppear" -> {
|
|
78
|
+
if (event.viewTag != disappearedScreenTag) return
|
|
79
|
+
disappearedScreenTag = 0
|
|
80
|
+
delegate?.presenterScreenWillAppear()
|
|
81
|
+
}
|
|
57
82
|
}
|
|
58
83
|
}
|
|
59
84
|
|