@lodev09/react-native-true-sheet 4.0.0-beta.24 → 4.0.0-beta.26
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 +2 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +13 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +12 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +120 -19
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +103 -27
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +18 -10
- package/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt +31 -14
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetBehavior.kt +9 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +3 -16
- 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/core/TrueSheetStackManager.kt +29 -38
- package/android/src/main/java/com/lodev09/truesheet/utils/ColorUtils.kt +19 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +6 -2
- package/ios/TrueSheetContainerView.h +1 -0
- package/ios/TrueSheetContainerView.mm +6 -0
- package/ios/TrueSheetContentView.h +1 -0
- package/ios/TrueSheetContentView.mm +2 -0
- package/ios/TrueSheetFooterView.mm +5 -0
- package/ios/TrueSheetModule.mm +11 -2
- package/ios/TrueSheetView.h +2 -0
- package/ios/TrueSheetView.mm +62 -9
- package/ios/TrueSheetViewController.h +14 -0
- package/ios/TrueSheetViewController.mm +490 -33
- package/ios/core/RNScreensEventObserver.mm +18 -1
- package/ios/core/TrueSheetDetentCalculator.h +11 -0
- package/ios/core/TrueSheetDetentCalculator.mm +17 -5
- package/ios/core/TrueSheetDimView.h +27 -0
- package/ios/core/TrueSheetDimView.mm +41 -0
- package/ios/core/TrueSheetKeyboardObserver.mm +98 -15
- 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 +8 -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 +53 -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 +7 -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 +60 -2
- package/src/TrueSheet.web.tsx +170 -41
- package/src/TrueSheetProvider.web.tsx +22 -13
- package/src/fabric/TrueSheetViewNativeComponent.ts +8 -0
- package/src/navigation/types.ts +3 -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,8 @@ 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`
|
|
38
|
+
* 📱 **Foldables** - [Rest the sheet on the hinge](https://sheet.lodev09.com/guides/foldables) of a half-folded iPhone with `foldOptions` (iOS 27.1+)
|
|
37
39
|
|
|
38
40
|
```sh
|
|
39
41
|
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,13 +51,23 @@ 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() {
|
|
59
|
-
|
|
69
|
+
viewTreeObserver.removeOnWindowAttachListener(windowAttachListener)
|
|
70
|
+
rootContainerView?.post { if (!isAttachedToWindow) detachRootView() }
|
|
60
71
|
super.onDetachedFromWindow()
|
|
61
72
|
}
|
|
62
73
|
|
|
@@ -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,27 +50,45 @@ 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
|
|
65
73
|
private var isSheetUpdatePending: Boolean = false
|
|
66
74
|
|
|
75
|
+
// didFocus waiting for the slide-back after a child dismissed
|
|
76
|
+
private var isDidFocusPending: Boolean = false
|
|
77
|
+
|
|
67
78
|
// Root container for the coordinator layout (activity or Modal dialog content view)
|
|
68
79
|
internal var rootContainerView: ViewGroup? = null
|
|
69
80
|
|
|
70
81
|
// Screen event observer for react-native-screens integration
|
|
71
82
|
internal var screensEventObserver: RNScreensEventObserver? = null
|
|
72
83
|
|
|
84
|
+
// Present deferred until the keyboard hides; a dismiss before then cancels it
|
|
85
|
+
private var pendingPresentCallback: (() -> Unit)? = null
|
|
86
|
+
private var pendingPresentGeneration = 0
|
|
87
|
+
|
|
88
|
+
// Sheet the deferred present will stack on — dismissing it drops the present, like iOS
|
|
89
|
+
internal var pendingPresentParent: TrueSheetView? = null
|
|
90
|
+
private set
|
|
91
|
+
|
|
73
92
|
// ==================== Initialization ====================
|
|
74
93
|
|
|
75
94
|
init {
|
|
@@ -189,6 +208,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
189
208
|
|
|
190
209
|
cleanupScreenEventObserver()
|
|
191
210
|
didInitiallyPresent = false
|
|
211
|
+
cancelPendingPresent()
|
|
192
212
|
|
|
193
213
|
viewController.dismissPromise = { viewController.delegate = null }
|
|
194
214
|
|
|
@@ -247,6 +267,24 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
247
267
|
}
|
|
248
268
|
}
|
|
249
269
|
|
|
270
|
+
fun setDimColor(color: Int?) {
|
|
271
|
+
val resolved = color ?: Color.BLACK
|
|
272
|
+
if (viewController.dimColor == resolved) return
|
|
273
|
+
viewController.dimColor = resolved
|
|
274
|
+
if (viewController.isPresented) {
|
|
275
|
+
viewController.updateDimAmount()
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
fun setDimOpacity(opacity: Float) {
|
|
280
|
+
val resolved = if (opacity < 0f) TrueSheetViewController.DEFAULT_DIM_OPACITY else opacity.coerceIn(0f, 1f)
|
|
281
|
+
if (viewController.dimOpacity == resolved) return
|
|
282
|
+
viewController.dimOpacity = resolved
|
|
283
|
+
if (viewController.isPresented) {
|
|
284
|
+
viewController.updateDimAmount()
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
250
288
|
fun setDimmedDetentIndex(index: Int) {
|
|
251
289
|
if (viewController.dimmedDetentIndex == index) return
|
|
252
290
|
viewController.dimmedDetentIndex = index
|
|
@@ -364,10 +402,12 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
364
402
|
|
|
365
403
|
// For stacked sheets on the same screen, inherit parent's presenter screen tag.
|
|
366
404
|
// If parent was hidden by screen navigation, this sheet is on a different screen.
|
|
367
|
-
val
|
|
405
|
+
val parentObserver = viewController.parentSheetView?.screensEventObserver
|
|
406
|
+
val parentScreenTag = parentObserver?.presenterScreenTag ?: 0
|
|
368
407
|
val parentHiddenByScreen = viewController.parentSheetView?.viewController?.wasHiddenByScreen == true
|
|
369
408
|
if (parentScreenTag != 0 && !parentHiddenByScreen) {
|
|
370
409
|
presenterScreenTag = parentScreenTag
|
|
410
|
+
ancestorScreenTags = parentObserver?.ancestorScreenTags ?: emptySet()
|
|
371
411
|
} else {
|
|
372
412
|
capturePresenterScreenFromView(this@TrueSheetView)
|
|
373
413
|
}
|
|
@@ -399,7 +439,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
399
439
|
|
|
400
440
|
@UiThread
|
|
401
441
|
fun present(detentIndex: Int, animated: Boolean = true, promiseCallback: () -> Unit) {
|
|
402
|
-
if (viewController.isPresented) {
|
|
442
|
+
if (viewController.isPresented || pendingPresentCallback != null) {
|
|
403
443
|
RNLog.w(reactContext, "TrueSheet: sheet is already presented. Use resize() to change detent.")
|
|
404
444
|
promiseCallback()
|
|
405
445
|
return
|
|
@@ -414,8 +454,18 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
414
454
|
val shouldDismissKeyboard = isFocusedViewWithinSheet || viewController.isDimmedAtDetentIndex(detentIndex)
|
|
415
455
|
if (shouldDismissKeyboard && KeyboardUtils.isKeyboardVisible(reactContext)) {
|
|
416
456
|
viewController.saveFocusedView()
|
|
457
|
+
pendingPresentCallback = promiseCallback
|
|
458
|
+
pendingPresentParent = parentSheet
|
|
459
|
+
// A cancelled request's callback can still land (a timer below API 30) — only the latest may present
|
|
460
|
+
val generation = ++pendingPresentGeneration
|
|
417
461
|
KeyboardUtils.dismiss(this) {
|
|
418
|
-
post {
|
|
462
|
+
post {
|
|
463
|
+
if (generation != pendingPresentGeneration) return@post
|
|
464
|
+
val callback = pendingPresentCallback ?: return@post
|
|
465
|
+
pendingPresentCallback = null
|
|
466
|
+
pendingPresentParent = null
|
|
467
|
+
presentSheet(detentIndex, animated, callback)
|
|
468
|
+
}
|
|
419
469
|
}
|
|
420
470
|
// Blur like iOS so JS focus state stays in sync until restoreFocusedView() refocuses it
|
|
421
471
|
rootView.findFocus()?.clearFocus()
|
|
@@ -447,8 +497,31 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
447
497
|
viewController.handleBackPress()
|
|
448
498
|
}
|
|
449
499
|
|
|
500
|
+
/**
|
|
501
|
+
* Drops a present still waiting on the keyboard and resolves its caller. Returns whether one was pending.
|
|
502
|
+
* Skip [restoreFocus] when the focused view is going away with its dismissing sheet.
|
|
503
|
+
*/
|
|
504
|
+
fun cancelPendingPresent(restoreFocus: Boolean = true): Boolean {
|
|
505
|
+
val callback = pendingPresentCallback ?: return false
|
|
506
|
+
pendingPresentCallback = null
|
|
507
|
+
pendingPresentParent = null
|
|
508
|
+
if (restoreFocus) {
|
|
509
|
+
viewController.restoreFocusedView()
|
|
510
|
+
} else {
|
|
511
|
+
viewController.clearFocusedView()
|
|
512
|
+
}
|
|
513
|
+
callback()
|
|
514
|
+
return true
|
|
515
|
+
}
|
|
516
|
+
|
|
450
517
|
@UiThread
|
|
451
518
|
fun dismiss(animated: Boolean = true, promiseCallback: () -> Unit) {
|
|
519
|
+
// Dismissing before a deferred present starts cancels it — nothing was presented
|
|
520
|
+
if (cancelPendingPresent()) {
|
|
521
|
+
promiseCallback()
|
|
522
|
+
return
|
|
523
|
+
}
|
|
524
|
+
|
|
452
525
|
if (viewController.isBeingDismissed || !viewController.isPresented) {
|
|
453
526
|
RNLog.w(reactContext, "TrueSheet: sheet is already dismissed. No need to dismiss it again.")
|
|
454
527
|
promiseCallback()
|
|
@@ -471,6 +544,9 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
471
544
|
return
|
|
472
545
|
}
|
|
473
546
|
|
|
547
|
+
// A present waiting on the keyboard would stack above this sheet — drop it with the rest
|
|
548
|
+
TrueSheetModule.cancelPendingPresents(parent = this)
|
|
549
|
+
|
|
474
550
|
val sheetsAbove = TrueSheetStackManager.getSheetsAbove(this)
|
|
475
551
|
// Create snapshot only for topmost sheet (first in reversed list)
|
|
476
552
|
sheetsAbove.firstOrNull()?.viewController?.createSheetSnapshot()
|
|
@@ -537,7 +613,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
537
613
|
|
|
538
614
|
// Propagate any additional translation up the stack
|
|
539
615
|
if (additionalTranslation > 0) {
|
|
540
|
-
|
|
616
|
+
viewController.parentSheetView?.addTranslation(additionalTranslation)
|
|
541
617
|
}
|
|
542
618
|
}
|
|
543
619
|
|
|
@@ -569,7 +645,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
569
645
|
|
|
570
646
|
val additionalTranslation = newTranslation - currentTranslation
|
|
571
647
|
if (additionalTranslation > 0) {
|
|
572
|
-
|
|
648
|
+
viewController.parentSheetView?.addTranslation(additionalTranslation, animated = false)
|
|
573
649
|
}
|
|
574
650
|
}
|
|
575
651
|
|
|
@@ -580,7 +656,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
580
656
|
if (viewController.isExpanded) return
|
|
581
657
|
|
|
582
658
|
viewController.translateSheet(viewController.currentTranslationY + amount, animated)
|
|
583
|
-
|
|
659
|
+
viewController.parentSheetView?.addTranslation(amount, animated)
|
|
584
660
|
}
|
|
585
661
|
|
|
586
662
|
/**
|
|
@@ -593,12 +669,33 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
593
669
|
|
|
594
670
|
// Parent should recalculate its translation based on this sheet's position
|
|
595
671
|
val mySheetTop = viewController.detentCalculator.getSheetTopForDetentIndex(viewController.currentDetentIndex)
|
|
596
|
-
|
|
672
|
+
viewController.parentSheetView?.updateTranslationForChild(mySheetTop)
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Resets translation after a child dismissed and emits didFocus once the sheet settles.
|
|
677
|
+
* A sheet presented on top before then cancels the slide-back, so the next blur flushes it.
|
|
678
|
+
*/
|
|
679
|
+
private fun focusAfterResetTranslation() {
|
|
680
|
+
isDidFocusPending = true
|
|
681
|
+
resetTranslation { emitPendingDidFocus() }
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
private fun emitPendingDidFocus() {
|
|
685
|
+
if (!isDidFocusPending) return
|
|
686
|
+
isDidFocusPending = false
|
|
687
|
+
|
|
688
|
+
if (viewController.isPresented && !viewController.isBeingDismissed) {
|
|
689
|
+
viewControllerDidFocus()
|
|
690
|
+
}
|
|
597
691
|
}
|
|
598
692
|
|
|
599
693
|
// ==================== TrueSheetViewControllerDelegate ====================
|
|
600
694
|
|
|
601
695
|
override fun viewControllerWillPresent(index: Int, position: Float, detent: Float) {
|
|
696
|
+
// Observe from the start so navigating away mid-present still hides the sheet
|
|
697
|
+
setupScreenEventObserver()
|
|
698
|
+
|
|
602
699
|
// Update parent sheet translation now that content is measured
|
|
603
700
|
TrueSheetStackManager.updateParentTranslation(this)
|
|
604
701
|
|
|
@@ -607,13 +704,14 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
607
704
|
}
|
|
608
705
|
|
|
609
706
|
override fun viewControllerDidPresent(index: Int, position: Float, detent: Float) {
|
|
610
|
-
setupScreenEventObserver()
|
|
611
|
-
|
|
612
707
|
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
613
708
|
eventDispatcher?.dispatchEvent(DidPresentEvent(surfaceId, id, index, position, detent))
|
|
614
709
|
}
|
|
615
710
|
|
|
616
711
|
override fun viewControllerWillDismiss() {
|
|
712
|
+
// Covers every dismiss path (swipe, back press, stack); the focused view leaves with this sheet
|
|
713
|
+
TrueSheetModule.cancelPendingPresents(parent = this, restoreFocus = false)
|
|
714
|
+
|
|
617
715
|
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
618
716
|
eventDispatcher?.dispatchEvent(WillDismissEvent(surfaceId, id))
|
|
619
717
|
}
|
|
@@ -624,6 +722,9 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
624
722
|
}
|
|
625
723
|
|
|
626
724
|
override fun viewControllerDidDismiss(parent: TrueSheetView?) {
|
|
725
|
+
// A slide-back cut short by this sheet's own dismissal never emits; don't carry it over
|
|
726
|
+
isDidFocusPending = false
|
|
727
|
+
|
|
627
728
|
// Detach coordinator from the root container view
|
|
628
729
|
viewController.coordinatorLayout?.let { rootContainerView?.removeView(it) }
|
|
629
730
|
rootContainerView = null
|
|
@@ -635,12 +736,10 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
635
736
|
|
|
636
737
|
TrueSheetStackManager.unregisterSheet(this)
|
|
637
738
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
}
|
|
643
|
-
}
|
|
739
|
+
// The sheets below stop blending this sheet's dim
|
|
740
|
+
parent?.viewController?.takeIf { it.isPresented }?.updateDimAmount()
|
|
741
|
+
|
|
742
|
+
parent?.focusAfterResetTranslation()
|
|
644
743
|
}
|
|
645
744
|
|
|
646
745
|
override fun viewControllerDidChangeDetent(index: Int, position: Float, detent: Float) {
|
|
@@ -683,6 +782,8 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
683
782
|
}
|
|
684
783
|
|
|
685
784
|
override fun viewControllerWillBlur() {
|
|
785
|
+
emitPendingDidFocus()
|
|
786
|
+
|
|
686
787
|
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
687
788
|
eventDispatcher?.dispatchEvent(WillBlurEvent(surfaceId, id))
|
|
688
789
|
}
|
|
@@ -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)
|
|
@@ -862,6 +886,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
862
886
|
setupSheetDetents()
|
|
863
887
|
setupDimmedBackground()
|
|
864
888
|
setupKeyboardObserver()
|
|
889
|
+
// Before the slide, so an input focused in onWillPresent insets the scrollable
|
|
890
|
+
containerView?.setupKeyboardHandler()
|
|
865
891
|
|
|
866
892
|
displayedBackgroundIndex = detentIndex
|
|
867
893
|
setupBackground(animated = false)
|
|
@@ -968,6 +994,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
968
994
|
return
|
|
969
995
|
}
|
|
970
996
|
|
|
997
|
+
// Bring the parent back alongside the slide-out (like iOS) instead of after it
|
|
998
|
+
parentSheetView?.resetTranslation()
|
|
999
|
+
|
|
971
1000
|
sheet.animate()
|
|
972
1001
|
.y(realScreenHeight.toFloat())
|
|
973
1002
|
.setDuration(DISMISS_DURATION)
|
|
@@ -981,11 +1010,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
981
1010
|
// Restore isHideable to actual value after present animation
|
|
982
1011
|
behavior?.isHideable = dismissible
|
|
983
1012
|
|
|
984
|
-
containerView?.setupKeyboardHandler()
|
|
985
|
-
|
|
986
1013
|
val (index, position, detent) = getDetentInfoWithValue(currentDetentIndex)
|
|
987
|
-
delegate?.viewControllerDidPresent(index, position, detent)
|
|
988
1014
|
parentSheetView?.viewControllerDidBlur()
|
|
1015
|
+
delegate?.viewControllerDidPresent(index, position, detent)
|
|
989
1016
|
delegate?.viewControllerDidFocus()
|
|
990
1017
|
sheetView?.updateGrabberAccessibilityValue(index, detents.size)
|
|
991
1018
|
|
|
@@ -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() {
|