@lodev09/react-native-true-sheet 3.6.10 → 3.7.0-beta.0
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 +29 -15
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +53 -149
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +7 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +15 -4
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetStackManager.kt +22 -7
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.h +6 -0
- package/package.json +1 -1
- package/android/src/main/java/com/lodev09/truesheet/core/RNScreensFragmentObserver.kt +0 -181
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
|
|
@@ -66,7 +66,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
66
66
|
private var isSheetUpdatePending: Boolean = false
|
|
67
67
|
|
|
68
68
|
// Root container for the coordinator layout (activity or Modal dialog content view)
|
|
69
|
-
|
|
69
|
+
internal var rootContainerView: ViewGroup? = null
|
|
70
70
|
|
|
71
71
|
// ==================== Initialization ====================
|
|
72
72
|
|
|
@@ -151,7 +151,6 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
151
151
|
// ==================== Lifecycle ====================
|
|
152
152
|
|
|
153
153
|
override fun onHostResume() {
|
|
154
|
-
viewController.reapplyHiddenState()
|
|
155
154
|
finalizeUpdates()
|
|
156
155
|
}
|
|
157
156
|
|
|
@@ -278,7 +277,10 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
278
277
|
if (!viewController.isPresented) {
|
|
279
278
|
// Attach coordinator to the root container
|
|
280
279
|
rootContainerView = findRootContainerView()
|
|
281
|
-
viewController.coordinatorLayout?.let {
|
|
280
|
+
viewController.coordinatorLayout?.let { coordinator ->
|
|
281
|
+
rootContainerView?.addView(coordinator)
|
|
282
|
+
coordinator.post { measureCoordinatorLayout() }
|
|
283
|
+
}
|
|
282
284
|
|
|
283
285
|
// Register with observer to track sheet stack hierarchy
|
|
284
286
|
viewController.parentSheetView = TrueSheetStackManager.onSheetWillPresent(this, detentIndex)
|
|
@@ -384,10 +386,6 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
384
386
|
override fun viewControllerWillPresent(index: Int, position: Float, detent: Float) {
|
|
385
387
|
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
386
388
|
eventDispatcher?.dispatchEvent(WillPresentEvent(surfaceId, id, index, position, detent))
|
|
387
|
-
|
|
388
|
-
// Enable touch event dispatching to React Native while sheet is visible
|
|
389
|
-
viewController.eventDispatcher = eventDispatcher
|
|
390
|
-
containerView?.footerView?.eventDispatcher = eventDispatcher
|
|
391
389
|
}
|
|
392
390
|
|
|
393
391
|
override fun viewControllerDidPresent(index: Int, position: Float, detent: Float) {
|
|
@@ -398,10 +396,6 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
398
396
|
override fun viewControllerWillDismiss() {
|
|
399
397
|
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
400
398
|
eventDispatcher?.dispatchEvent(WillDismissEvent(surfaceId, id))
|
|
401
|
-
|
|
402
|
-
// Disable touch event dispatching when sheet is dismissing
|
|
403
|
-
viewController.eventDispatcher = null
|
|
404
|
-
containerView?.footerView?.eventDispatcher = null
|
|
405
399
|
}
|
|
406
400
|
|
|
407
401
|
override fun viewControllerDidDismiss(hadParent: Boolean) {
|
|
@@ -442,6 +436,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
442
436
|
|
|
443
437
|
override fun viewControllerDidChangeSize(width: Int, height: Int) {
|
|
444
438
|
updateState(width, height)
|
|
439
|
+
measureCoordinatorLayout()
|
|
445
440
|
}
|
|
446
441
|
|
|
447
442
|
override fun viewControllerWillFocus() {
|
|
@@ -486,6 +481,19 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
486
481
|
|
|
487
482
|
// ==================== Private Helpers ====================
|
|
488
483
|
|
|
484
|
+
private fun measureCoordinatorLayout() {
|
|
485
|
+
val coordinator = viewController.coordinatorLayout ?: return
|
|
486
|
+
val width = viewController.screenWidth
|
|
487
|
+
val height = viewController.realScreenHeight
|
|
488
|
+
|
|
489
|
+
if (width > 0 && height > 0) {
|
|
490
|
+
val widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY)
|
|
491
|
+
val heightSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
|
|
492
|
+
coordinator.measure(widthSpec, heightSpec)
|
|
493
|
+
coordinator.layout(0, 0, width, height)
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
489
497
|
/**
|
|
490
498
|
* Find the root container view for presenting the sheet.
|
|
491
499
|
* This traverses up the view hierarchy to find the content view (android.R.id.content)
|
|
@@ -494,14 +502,20 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
494
502
|
*/
|
|
495
503
|
private fun findRootContainerView(): ViewGroup? {
|
|
496
504
|
var current: android.view.ViewParent? = parent
|
|
505
|
+
var contentView: ViewGroup? = null
|
|
497
506
|
|
|
498
507
|
while (current != null) {
|
|
499
|
-
if (current is ViewGroup
|
|
500
|
-
|
|
508
|
+
if (current is ViewGroup) {
|
|
509
|
+
if (current.javaClass.name == "com.swmansion.rnscreens.Screen") {
|
|
510
|
+
return current
|
|
511
|
+
}
|
|
512
|
+
if (contentView == null && current.id == android.R.id.content) {
|
|
513
|
+
contentView = current
|
|
514
|
+
}
|
|
501
515
|
}
|
|
502
516
|
current = current.parent
|
|
503
517
|
}
|
|
504
518
|
|
|
505
|
-
return reactContext.currentActivity?.findViewById(android.R.id.content)
|
|
519
|
+
return contentView ?: reactContext.currentActivity?.findViewById(android.R.id.content)
|
|
506
520
|
}
|
|
507
521
|
}
|
|
@@ -23,7 +23,6 @@ import com.facebook.react.util.RNLog
|
|
|
23
23
|
import com.facebook.react.views.view.ReactViewGroup
|
|
24
24
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
25
25
|
import com.lodev09.truesheet.core.GrabberOptions
|
|
26
|
-
import com.lodev09.truesheet.core.RNScreensFragmentObserver
|
|
27
26
|
import com.lodev09.truesheet.core.TrueSheetBottomSheetView
|
|
28
27
|
import com.lodev09.truesheet.core.TrueSheetBottomSheetViewDelegate
|
|
29
28
|
import com.lodev09.truesheet.core.TrueSheetCoordinatorLayout
|
|
@@ -45,6 +44,7 @@ import com.lodev09.truesheet.utils.ScreenUtils
|
|
|
45
44
|
data class DetentInfo(val index: Int, val position: Float)
|
|
46
45
|
|
|
47
46
|
interface TrueSheetViewControllerDelegate {
|
|
47
|
+
val eventDispatcher: EventDispatcher?
|
|
48
48
|
fun viewControllerWillPresent(index: Int, position: Float, detent: Float)
|
|
49
49
|
fun viewControllerDidPresent(index: Int, position: Float, detent: Float)
|
|
50
50
|
fun viewControllerWillDismiss()
|
|
@@ -89,7 +89,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
89
89
|
private const val DEFAULT_CORNER_RADIUS = 16 // dp
|
|
90
90
|
private const val TRANSLATE_ANIMATION_DURATION = 200L
|
|
91
91
|
private const val DISMISS_DURATION = 200L
|
|
92
|
-
private const val MODAL_FADE_DURATION = 150L
|
|
93
92
|
}
|
|
94
93
|
|
|
95
94
|
// =============================================================================
|
|
@@ -129,7 +128,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
129
128
|
|
|
130
129
|
private var interactionState: InteractionState = InteractionState.Idle
|
|
131
130
|
private var isDismissing = false
|
|
132
|
-
private var wasHiddenByModal = false
|
|
133
131
|
private var shouldAnimatePresent = false
|
|
134
132
|
private var isPresentAnimating = false
|
|
135
133
|
|
|
@@ -149,15 +147,16 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
149
147
|
|
|
150
148
|
// Helper Objects
|
|
151
149
|
private var keyboardObserver: TrueSheetKeyboardObserver? = null
|
|
152
|
-
private var rnScreensObserver: RNScreensFragmentObserver? = null
|
|
153
150
|
internal val detentCalculator = TrueSheetDetentCalculator(reactContext).apply {
|
|
154
151
|
delegate = this@TrueSheetViewController
|
|
155
152
|
}
|
|
156
153
|
|
|
157
154
|
// Touch Dispatchers
|
|
158
|
-
internal var eventDispatcher: EventDispatcher? = null
|
|
159
155
|
private val jsTouchDispatcher = JSTouchDispatcher(this)
|
|
160
|
-
private
|
|
156
|
+
private val jsPointerDispatcher = JSPointerDispatcher(this)
|
|
157
|
+
|
|
158
|
+
private val eventDispatcher
|
|
159
|
+
get() = delegate?.eventDispatcher
|
|
161
160
|
|
|
162
161
|
// Detent Configuration
|
|
163
162
|
override var maxSheetHeight: Int? = null
|
|
@@ -284,14 +283,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
284
283
|
private val dimViews: List<TrueSheetDimView>
|
|
285
284
|
get() = listOfNotNull(dimView, parentDimView)
|
|
286
285
|
|
|
287
|
-
// =============================================================================
|
|
288
|
-
// MARK: - Initialization
|
|
289
|
-
// =============================================================================
|
|
290
|
-
|
|
291
|
-
init {
|
|
292
|
-
jsPointerDispatcher = JSPointerDispatcher(this)
|
|
293
|
-
}
|
|
294
|
-
|
|
295
286
|
// =============================================================================
|
|
296
287
|
// MARK: - Sheet Creation & Cleanup
|
|
297
288
|
// =============================================================================
|
|
@@ -312,7 +303,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
312
303
|
|
|
313
304
|
private fun cleanupSheet() {
|
|
314
305
|
cleanupKeyboardObserver()
|
|
315
|
-
cleanupModalObserver()
|
|
316
306
|
cleanupBackCallback()
|
|
317
307
|
sheetView?.animate()?.cancel()
|
|
318
308
|
|
|
@@ -332,7 +322,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
332
322
|
isDismissing = false
|
|
333
323
|
isPresented = false
|
|
334
324
|
isSheetVisible = false
|
|
335
|
-
wasHiddenByModal = false
|
|
336
325
|
isPresentAnimating = false
|
|
337
326
|
lastEmittedPositionPx = -1
|
|
338
327
|
detentIndexBeforeKeyboard = -1
|
|
@@ -372,6 +361,13 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
372
361
|
}
|
|
373
362
|
}
|
|
374
363
|
|
|
364
|
+
override fun coordinatorLayoutDidChangeConfiguration() {
|
|
365
|
+
if (!isPresented) return
|
|
366
|
+
|
|
367
|
+
updateStateDimensions()
|
|
368
|
+
sheetView?.let { emitChangePositionDelegate(it.top, realtime = false) }
|
|
369
|
+
}
|
|
370
|
+
|
|
375
371
|
// =============================================================================
|
|
376
372
|
// MARK: - TrueSheetDimViewDelegate
|
|
377
373
|
// =============================================================================
|
|
@@ -502,84 +498,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
502
498
|
}
|
|
503
499
|
}
|
|
504
500
|
|
|
505
|
-
// =============================================================================
|
|
506
|
-
// MARK: - Modal Observer (react-native-screens)
|
|
507
|
-
// =============================================================================
|
|
508
|
-
|
|
509
|
-
private fun setupModalObserver() {
|
|
510
|
-
rnScreensObserver = RNScreensFragmentObserver(
|
|
511
|
-
reactContext = reactContext,
|
|
512
|
-
onModalPresented = {
|
|
513
|
-
if (isPresented && isSheetVisible && isTopmostSheet) {
|
|
514
|
-
hideForModal()
|
|
515
|
-
}
|
|
516
|
-
},
|
|
517
|
-
onModalWillDismiss = {
|
|
518
|
-
if (isPresented && wasHiddenByModal && isTopmostSheet) {
|
|
519
|
-
showAfterModal()
|
|
520
|
-
}
|
|
521
|
-
},
|
|
522
|
-
onModalDidDismiss = {
|
|
523
|
-
if (isPresented && wasHiddenByModal) {
|
|
524
|
-
wasHiddenByModal = false
|
|
525
|
-
// Restore parent sheet after this sheet is restored
|
|
526
|
-
parentSheetView?.viewController?.let { parent ->
|
|
527
|
-
post { parent.showAfterModal() }
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
)
|
|
532
|
-
rnScreensObserver?.start()
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
private fun cleanupModalObserver() {
|
|
536
|
-
rnScreensObserver?.stop()
|
|
537
|
-
rnScreensObserver = null
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
private fun setSheetVisibility(visible: Boolean) {
|
|
541
|
-
coordinatorLayout?.visibility = if (visible) VISIBLE else GONE
|
|
542
|
-
dimViews.forEach { it.visibility = if (visible) VISIBLE else INVISIBLE }
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
private fun hideForModal() {
|
|
546
|
-
val sheet = sheetView ?: run {
|
|
547
|
-
RNLog.e(reactContext, "TrueSheet: sheetView is null in hideForModal")
|
|
548
|
-
return
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
isSheetVisible = false
|
|
552
|
-
wasHiddenByModal = true
|
|
553
|
-
|
|
554
|
-
dimViews.forEach { it.animate().alpha(0f).setDuration(MODAL_FADE_DURATION).start() }
|
|
555
|
-
sheet.animate()
|
|
556
|
-
.alpha(0f)
|
|
557
|
-
.setDuration(MODAL_FADE_DURATION)
|
|
558
|
-
.withEndAction {
|
|
559
|
-
setSheetVisibility(false)
|
|
560
|
-
}
|
|
561
|
-
.start()
|
|
562
|
-
|
|
563
|
-
// This will hide parent sheets first
|
|
564
|
-
parentSheetView?.viewController?.hideForModal()
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
private fun showAfterModal() {
|
|
568
|
-
isSheetVisible = true
|
|
569
|
-
setSheetVisibility(true)
|
|
570
|
-
sheetView?.alpha = 1f
|
|
571
|
-
updateDimAmount(animated = true)
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
/**
|
|
575
|
-
* Re-applies hidden state after returning from background.
|
|
576
|
-
* Android may restore visibility on activity resume, so we need to hide it again.
|
|
577
|
-
*/
|
|
578
|
-
fun reapplyHiddenState() {
|
|
579
|
-
if (!wasHiddenByModal) return
|
|
580
|
-
setSheetVisibility(false)
|
|
581
|
-
}
|
|
582
|
-
|
|
583
501
|
// =============================================================================
|
|
584
502
|
// MARK: - Presentation
|
|
585
503
|
// =============================================================================
|
|
@@ -611,7 +529,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
611
529
|
setupSheetDetents()
|
|
612
530
|
setupDimmedBackground(currentDetentIndex)
|
|
613
531
|
setupKeyboardObserver()
|
|
614
|
-
setupModalObserver()
|
|
615
532
|
setupBackCallback()
|
|
616
533
|
|
|
617
534
|
sheet.setupBackground()
|
|
@@ -754,15 +671,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
754
671
|
animate = isPresented
|
|
755
672
|
)
|
|
756
673
|
|
|
757
|
-
|
|
758
|
-
val newHeight = realScreenHeight - expandedOffset - offset
|
|
759
|
-
val newWidth = minOf(screenWidth, DEFAULT_MAX_WIDTH.dpToPx().toInt())
|
|
760
|
-
|
|
761
|
-
if (lastStateWidth != newWidth || lastStateHeight != newHeight) {
|
|
762
|
-
lastStateWidth = newWidth
|
|
763
|
-
lastStateHeight = newHeight
|
|
764
|
-
delegate?.viewControllerDidChangeSize(newWidth, newHeight)
|
|
765
|
-
}
|
|
674
|
+
updateStateDimensions(expandedOffset)
|
|
766
675
|
|
|
767
676
|
if (isPresented) {
|
|
768
677
|
setStateForDetentIndex(currentDetentIndex)
|
|
@@ -885,7 +794,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
885
794
|
// =============================================================================
|
|
886
795
|
|
|
887
796
|
private fun shouldHandleKeyboard(checkFocus: Boolean = true): Boolean {
|
|
888
|
-
if (wasHiddenByModal) return false
|
|
889
797
|
if (!isTopmostSheet) return false
|
|
890
798
|
if (checkFocus && !isFocusedViewWithinSheet()) return false
|
|
891
799
|
return true
|
|
@@ -912,14 +820,19 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
912
820
|
setupSheetDetents()
|
|
913
821
|
if (!isDismissing && detentIndexBeforeKeyboard >= 0) {
|
|
914
822
|
setStateForDetentIndex(detentIndexBeforeKeyboard)
|
|
915
|
-
detentIndexBeforeKeyboard = -1
|
|
916
823
|
}
|
|
917
824
|
}
|
|
918
825
|
|
|
919
|
-
override fun keyboardDidHide() {
|
|
826
|
+
override fun keyboardDidHide() {
|
|
827
|
+
if (!shouldHandleKeyboard(checkFocus = false)) return
|
|
828
|
+
detentIndexBeforeKeyboard = -1
|
|
829
|
+
positionFooter()
|
|
830
|
+
}
|
|
920
831
|
|
|
921
832
|
override fun keyboardDidChangeHeight(height: Int) {
|
|
922
|
-
if (
|
|
833
|
+
// Skip focus check if already handling keyboard (focus may be lost during hide)
|
|
834
|
+
val isHandlingKeyboard = detentIndexBeforeKeyboard >= 0
|
|
835
|
+
if (!shouldHandleKeyboard(checkFocus = !isHandlingKeyboard)) return
|
|
923
836
|
positionFooter()
|
|
924
837
|
}
|
|
925
838
|
}
|
|
@@ -1008,6 +921,19 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1008
921
|
// MARK: - Detent Helpers
|
|
1009
922
|
// =============================================================================
|
|
1010
923
|
|
|
924
|
+
private fun updateStateDimensions(expandedOffset: Int? = null) {
|
|
925
|
+
val offset = expandedOffset ?: (realScreenHeight - detentCalculator.getDetentHeight(detents.last()))
|
|
926
|
+
val topOffset = if (offset == 0) topInset else 0
|
|
927
|
+
val newHeight = realScreenHeight - offset - topOffset
|
|
928
|
+
val newWidth = minOf(screenWidth, DEFAULT_MAX_WIDTH.dpToPx().toInt())
|
|
929
|
+
|
|
930
|
+
if (lastStateWidth != newWidth || lastStateHeight != newHeight) {
|
|
931
|
+
lastStateWidth = newWidth
|
|
932
|
+
lastStateHeight = newHeight
|
|
933
|
+
delegate?.viewControllerDidChangeSize(newWidth, newHeight)
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
1011
937
|
fun translateSheet(translationY: Int) {
|
|
1012
938
|
val sheet = sheetView ?: return
|
|
1013
939
|
|
|
@@ -1052,48 +978,26 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1052
978
|
(getTag(R.id.react_test_id) as? String)?.let { info.viewIdResourceName = it }
|
|
1053
979
|
}
|
|
1054
980
|
|
|
1055
|
-
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
1056
|
-
super.onSizeChanged(w, h, oldw, oldh)
|
|
1057
|
-
|
|
1058
|
-
if (w == oldw && h == oldh) return
|
|
1059
|
-
if (!isPresented) return
|
|
1060
|
-
|
|
1061
|
-
// Skip reconfiguration if expanded and only height changed (e.g., keyboard)
|
|
1062
|
-
if (h + topInset >= screenHeight && isExpanded && oldw == w) return
|
|
1063
|
-
|
|
1064
|
-
post {
|
|
1065
|
-
setupSheetDetents()
|
|
1066
|
-
positionFooter()
|
|
1067
|
-
sheetView?.let { emitChangePositionDelegate(it.top, realtime = false) }
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
override fun handleException(t: Throwable) {
|
|
1072
|
-
reactContext.reactApplicationContext.handleException(RuntimeException(t))
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
981
|
// =============================================================================
|
|
1076
|
-
// MARK: - Touch
|
|
982
|
+
// MARK: - RootView Touch Handling
|
|
1077
983
|
// =============================================================================
|
|
1078
984
|
|
|
1079
985
|
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
1080
|
-
// Footer needs special handling since it's positioned absolutely
|
|
1081
986
|
val footer = containerView?.footerView
|
|
1082
|
-
if (footer != null && footer.
|
|
987
|
+
if (footer != null && footer.isShown) {
|
|
1083
988
|
val footerLocation = ScreenUtils.getScreenLocation(footer)
|
|
1084
|
-
val
|
|
1085
|
-
val
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
touchScreenY <= footerLocation[1] + footer.height
|
|
989
|
+
val touchX = event.rawX.toInt()
|
|
990
|
+
val touchY = event.rawY.toInt()
|
|
991
|
+
|
|
992
|
+
if (touchX >= footerLocation[0] &&
|
|
993
|
+
touchX <= footerLocation[0] + footer.width &&
|
|
994
|
+
touchY >= footerLocation[1] &&
|
|
995
|
+
touchY <= footerLocation[1] + footer.height
|
|
1092
996
|
) {
|
|
1093
997
|
val localEvent = MotionEvent.obtain(event)
|
|
1094
998
|
localEvent.setLocation(
|
|
1095
|
-
(
|
|
1096
|
-
(
|
|
999
|
+
(touchX - footerLocation[0]).toFloat(),
|
|
1000
|
+
(touchY - footerLocation[1]).toFloat()
|
|
1097
1001
|
)
|
|
1098
1002
|
val handled = footer.dispatchTouchEvent(localEvent)
|
|
1099
1003
|
localEvent.recycle()
|
|
@@ -1106,7 +1010,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1106
1010
|
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
1107
1011
|
eventDispatcher?.let {
|
|
1108
1012
|
jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
|
|
1109
|
-
jsPointerDispatcher
|
|
1013
|
+
jsPointerDispatcher.handleMotionEvent(event, it, true)
|
|
1110
1014
|
}
|
|
1111
1015
|
return super.onInterceptTouchEvent(event)
|
|
1112
1016
|
}
|
|
@@ -1114,35 +1018,35 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1114
1018
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
1115
1019
|
eventDispatcher?.let {
|
|
1116
1020
|
jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
|
|
1117
|
-
jsPointerDispatcher
|
|
1021
|
+
jsPointerDispatcher.handleMotionEvent(event, it, false)
|
|
1118
1022
|
}
|
|
1119
1023
|
super.onTouchEvent(event)
|
|
1120
1024
|
return true
|
|
1121
1025
|
}
|
|
1122
1026
|
|
|
1123
1027
|
override fun onInterceptHoverEvent(event: MotionEvent): Boolean {
|
|
1124
|
-
eventDispatcher?.let { jsPointerDispatcher
|
|
1028
|
+
eventDispatcher?.let { jsPointerDispatcher.handleMotionEvent(event, it, true) }
|
|
1125
1029
|
return super.onHoverEvent(event)
|
|
1126
1030
|
}
|
|
1127
1031
|
|
|
1128
1032
|
override fun onHoverEvent(event: MotionEvent): Boolean {
|
|
1129
|
-
eventDispatcher?.let { jsPointerDispatcher
|
|
1033
|
+
eventDispatcher?.let { jsPointerDispatcher.handleMotionEvent(event, it, false) }
|
|
1130
1034
|
return super.onHoverEvent(event)
|
|
1131
1035
|
}
|
|
1132
1036
|
|
|
1133
1037
|
override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
|
|
1134
1038
|
eventDispatcher?.let {
|
|
1135
1039
|
jsTouchDispatcher.onChildStartedNativeGesture(ev, it)
|
|
1136
|
-
jsPointerDispatcher
|
|
1040
|
+
jsPointerDispatcher.onChildStartedNativeGesture(childView, ev, it)
|
|
1137
1041
|
}
|
|
1138
1042
|
}
|
|
1139
1043
|
|
|
1140
1044
|
override fun onChildEndedNativeGesture(childView: View, ev: MotionEvent) {
|
|
1141
1045
|
eventDispatcher?.let { jsTouchDispatcher.onChildEndedNativeGesture(ev, it) }
|
|
1142
|
-
jsPointerDispatcher
|
|
1046
|
+
jsPointerDispatcher.onChildEndedNativeGesture()
|
|
1143
1047
|
}
|
|
1144
1048
|
|
|
1145
|
-
override fun
|
|
1146
|
-
|
|
1049
|
+
override fun handleException(t: Throwable) {
|
|
1050
|
+
reactContext.reactApplicationContext.handleException(RuntimeException(t))
|
|
1147
1051
|
}
|
|
1148
1052
|
}
|
|
@@ -2,6 +2,7 @@ package com.lodev09.truesheet.core
|
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.content.Context
|
|
5
|
+
import android.content.res.Configuration
|
|
5
6
|
import android.view.MotionEvent
|
|
6
7
|
import android.view.ViewConfiguration
|
|
7
8
|
import android.view.ViewGroup
|
|
@@ -12,6 +13,7 @@ import com.facebook.react.uimanager.ReactPointerEventsView
|
|
|
12
13
|
|
|
13
14
|
interface TrueSheetCoordinatorLayoutDelegate {
|
|
14
15
|
fun coordinatorLayoutDidLayout(changed: Boolean)
|
|
16
|
+
fun coordinatorLayoutDidChangeConfiguration()
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -53,6 +55,11 @@ class TrueSheetCoordinatorLayout(context: Context) :
|
|
|
53
55
|
delegate?.coordinatorLayoutDidLayout(changed)
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
override fun onConfigurationChanged(newConfig: Configuration?) {
|
|
59
|
+
super.onConfigurationChanged(newConfig)
|
|
60
|
+
delegate?.coordinatorLayoutDidChangeConfiguration()
|
|
61
|
+
}
|
|
62
|
+
|
|
56
63
|
override val pointerEvents: PointerEvents
|
|
57
64
|
get() = PointerEvents.BOX_NONE
|
|
58
65
|
|
|
@@ -68,12 +68,11 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) :
|
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* Attaches this dim view to a target view group.
|
|
71
|
-
* For CoordinatorLayout usage, pass null to use the default (activity's decor view).
|
|
72
71
|
* For stacked sheets, pass the parent sheet's bottom sheet view with corner radius.
|
|
73
72
|
*/
|
|
74
|
-
fun attach(view: ViewGroup
|
|
73
|
+
fun attach(view: ViewGroup, cornerRadius: Float = 0f) {
|
|
75
74
|
if (parent != null) return
|
|
76
|
-
targetView = view
|
|
75
|
+
targetView = view
|
|
77
76
|
|
|
78
77
|
if (cornerRadius > 0f) {
|
|
79
78
|
outlineProvider = object : ViewOutlineProvider() {
|
|
@@ -87,7 +86,19 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) :
|
|
|
87
86
|
clipToOutline = false
|
|
88
87
|
}
|
|
89
88
|
|
|
90
|
-
|
|
89
|
+
view.addView(this)
|
|
90
|
+
|
|
91
|
+
// Manually measure and layout for React Native views (they don't layout native children)
|
|
92
|
+
view.post {
|
|
93
|
+
val width = view.width
|
|
94
|
+
val height = view.height
|
|
95
|
+
if (width > 0 && height > 0) {
|
|
96
|
+
val widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
|
|
97
|
+
val heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
|
98
|
+
measure(widthSpec, heightSpec)
|
|
99
|
+
layout(0, 0, width, height)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
91
102
|
}
|
|
92
103
|
|
|
93
104
|
/**
|
|
@@ -13,12 +13,18 @@ object TrueSheetStackManager {
|
|
|
13
13
|
/**
|
|
14
14
|
* Called when a sheet is about to be presented.
|
|
15
15
|
* Returns the visible parent sheet to stack on, or null if none.
|
|
16
|
+
* Only returns a parent if it's in the same container (e.g., same Screen).
|
|
16
17
|
*/
|
|
17
18
|
@JvmStatic
|
|
18
19
|
fun onSheetWillPresent(sheetView: TrueSheetView, detentIndex: Int): TrueSheetView? {
|
|
19
20
|
synchronized(presentedSheetStack) {
|
|
21
|
+
val rootContainer = sheetView.rootContainerView
|
|
20
22
|
val parentSheet = presentedSheetStack.lastOrNull()
|
|
21
|
-
?.takeIf {
|
|
23
|
+
?.takeIf {
|
|
24
|
+
it.viewController.isPresented &&
|
|
25
|
+
it.viewController.isSheetVisible &&
|
|
26
|
+
it.rootContainerView == rootContainer
|
|
27
|
+
}
|
|
22
28
|
|
|
23
29
|
val childSheetTop = sheetView.viewController.detentCalculator.getSheetTopForDetentIndex(detentIndex)
|
|
24
30
|
parentSheet?.updateTranslationForChild(childSheetTop)
|
|
@@ -48,6 +54,7 @@ object TrueSheetStackManager {
|
|
|
48
54
|
/**
|
|
49
55
|
* Called when a presented sheet's size changes (e.g., after setupSheetDetents).
|
|
50
56
|
* Updates parent sheet translations to match the new sheet position.
|
|
57
|
+
* Only affects parent sheets in the same container.
|
|
51
58
|
*/
|
|
52
59
|
@JvmStatic
|
|
53
60
|
fun onSheetSizeChanged(sheetView: TrueSheetView) {
|
|
@@ -55,7 +62,9 @@ object TrueSheetStackManager {
|
|
|
55
62
|
val index = presentedSheetStack.indexOf(sheetView)
|
|
56
63
|
if (index <= 0) return
|
|
57
64
|
|
|
65
|
+
val rootContainer = sheetView.rootContainerView
|
|
58
66
|
val parentSheet = presentedSheetStack[index - 1]
|
|
67
|
+
.takeIf { it.rootContainerView == rootContainer } ?: return
|
|
59
68
|
|
|
60
69
|
// Post to ensure layout is complete before reading position
|
|
61
70
|
sheetView.viewController.post {
|
|
@@ -71,15 +80,18 @@ object TrueSheetStackManager {
|
|
|
71
80
|
}
|
|
72
81
|
|
|
73
82
|
/**
|
|
74
|
-
* Returns all sheets presented on top of the given sheet (children/descendants)
|
|
75
|
-
* Returns them in reverse order (top-most first) for proper dismissal.
|
|
83
|
+
* Returns all sheets presented on top of the given sheet (children/descendants)
|
|
84
|
+
* that are in the same container. Returns them in reverse order (top-most first) for proper dismissal.
|
|
76
85
|
*/
|
|
77
86
|
@JvmStatic
|
|
78
87
|
fun getSheetsAbove(sheetView: TrueSheetView): List<TrueSheetView> {
|
|
79
88
|
synchronized(presentedSheetStack) {
|
|
80
89
|
val index = presentedSheetStack.indexOf(sheetView)
|
|
81
90
|
if (index < 0 || index >= presentedSheetStack.size - 1) return emptyList()
|
|
82
|
-
|
|
91
|
+
val rootContainer = sheetView.rootContainerView
|
|
92
|
+
return presentedSheetStack.subList(index + 1, presentedSheetStack.size)
|
|
93
|
+
.filter { it.rootContainerView == rootContainer }
|
|
94
|
+
.reversed()
|
|
83
95
|
}
|
|
84
96
|
}
|
|
85
97
|
|
|
@@ -99,23 +111,26 @@ object TrueSheetStackManager {
|
|
|
99
111
|
|
|
100
112
|
/**
|
|
101
113
|
* Gets the parent sheet of the given sheet, if any.
|
|
114
|
+
* Only returns a parent if it's in the same container.
|
|
102
115
|
*/
|
|
103
116
|
@JvmStatic
|
|
104
117
|
fun getParentSheet(sheetView: TrueSheetView): TrueSheetView? {
|
|
105
118
|
synchronized(presentedSheetStack) {
|
|
106
119
|
val index = presentedSheetStack.indexOf(sheetView)
|
|
107
120
|
if (index <= 0) return null
|
|
108
|
-
|
|
121
|
+
val rootContainer = sheetView.rootContainerView
|
|
122
|
+
return presentedSheetStack[index - 1].takeIf { it.rootContainerView == rootContainer }
|
|
109
123
|
}
|
|
110
124
|
}
|
|
111
125
|
|
|
112
126
|
/**
|
|
113
|
-
* Returns true if the given sheet is the topmost presented sheet.
|
|
127
|
+
* Returns true if the given sheet is the topmost presented sheet in its container.
|
|
114
128
|
*/
|
|
115
129
|
@JvmStatic
|
|
116
130
|
fun isTopmostSheet(sheetView: TrueSheetView): Boolean {
|
|
117
131
|
synchronized(presentedSheetStack) {
|
|
118
|
-
|
|
132
|
+
val rootContainer = sheetView.rootContainerView
|
|
133
|
+
return presentedSheetStack.lastOrNull { it.rootContainerView == rootContainer } == sheetView
|
|
119
134
|
}
|
|
120
135
|
}
|
|
121
136
|
}
|
|
@@ -22,6 +22,12 @@ class JSI_EXPORT TrueSheetViewShadowNode final
|
|
|
22
22
|
using ConcreteViewShadowNode::ConcreteViewShadowNode;
|
|
23
23
|
|
|
24
24
|
public:
|
|
25
|
+
static ShadowNodeTraits BaseTraits() {
|
|
26
|
+
auto traits = ConcreteViewShadowNode::BaseTraits();
|
|
27
|
+
traits.set(ShadowNodeTraits::Trait::RootNodeKind);
|
|
28
|
+
return traits;
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
void adjustLayoutWithState();
|
|
26
32
|
};
|
|
27
33
|
|
package/package.json
CHANGED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
package com.lodev09.truesheet.core
|
|
2
|
-
|
|
3
|
-
import android.content.Context
|
|
4
|
-
import androidx.appcompat.app.AppCompatActivity
|
|
5
|
-
import androidx.fragment.app.Fragment
|
|
6
|
-
import androidx.fragment.app.FragmentManager
|
|
7
|
-
import androidx.lifecycle.DefaultLifecycleObserver
|
|
8
|
-
import androidx.lifecycle.LifecycleOwner
|
|
9
|
-
import com.facebook.react.bridge.ReactContext
|
|
10
|
-
|
|
11
|
-
private const val RN_SCREENS_PACKAGE = "com.swmansion.rnscreens"
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Observes fragment lifecycle to detect react-native-screens modal presentation.
|
|
15
|
-
* Automatically notifies when modals are presented/dismissed.
|
|
16
|
-
*/
|
|
17
|
-
class RNScreensFragmentObserver(
|
|
18
|
-
private val reactContext: ReactContext,
|
|
19
|
-
private val onModalPresented: () -> Unit,
|
|
20
|
-
private val onModalWillDismiss: () -> Unit,
|
|
21
|
-
private val onModalDidDismiss: () -> Unit
|
|
22
|
-
) {
|
|
23
|
-
private var fragmentLifecycleCallback: FragmentManager.FragmentLifecycleCallbacks? = null
|
|
24
|
-
private var activityLifecycleObserver: DefaultLifecycleObserver? = null
|
|
25
|
-
private val activeModalFragments: MutableSet<Fragment> = mutableSetOf()
|
|
26
|
-
private var isActivityInForeground = true
|
|
27
|
-
private var pendingDismissRunnable: Runnable? = null
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Start observing fragment lifecycle events.
|
|
31
|
-
*/
|
|
32
|
-
fun start() {
|
|
33
|
-
val activity = reactContext.currentActivity as? AppCompatActivity ?: return
|
|
34
|
-
val fragmentManager = activity.supportFragmentManager
|
|
35
|
-
|
|
36
|
-
// Track activity foreground state to ignore fragment lifecycle events during background/foreground transitions
|
|
37
|
-
activityLifecycleObserver = object : DefaultLifecycleObserver {
|
|
38
|
-
override fun onResume(owner: LifecycleOwner) {
|
|
39
|
-
isActivityInForeground = true
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
override fun onPause(owner: LifecycleOwner) {
|
|
43
|
-
isActivityInForeground = false
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
activity.lifecycle.addObserver(activityLifecycleObserver!!)
|
|
47
|
-
|
|
48
|
-
fragmentLifecycleCallback = object : FragmentManager.FragmentLifecycleCallbacks() {
|
|
49
|
-
override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) {
|
|
50
|
-
super.onFragmentAttached(fm, f, context)
|
|
51
|
-
|
|
52
|
-
// Ignore if app is resuming from background
|
|
53
|
-
if (!isActivityInForeground) return
|
|
54
|
-
|
|
55
|
-
if (isModalFragment(f) && !activeModalFragments.contains(f)) {
|
|
56
|
-
// Cancel any pending dismiss since a modal is being presented
|
|
57
|
-
cancelPendingDismiss()
|
|
58
|
-
|
|
59
|
-
activeModalFragments.add(f)
|
|
60
|
-
|
|
61
|
-
if (activeModalFragments.size == 1) {
|
|
62
|
-
onModalPresented()
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
override fun onFragmentStopped(fm: FragmentManager, f: Fragment) {
|
|
68
|
-
super.onFragmentStopped(fm, f)
|
|
69
|
-
|
|
70
|
-
// Ignore if app is going to background (fragments stop with activity)
|
|
71
|
-
if (!isActivityInForeground) return
|
|
72
|
-
|
|
73
|
-
// Only trigger when fragment is being removed (not just stopped for navigation)
|
|
74
|
-
if (activeModalFragments.contains(f) && f.isRemoving) {
|
|
75
|
-
activeModalFragments.remove(f)
|
|
76
|
-
|
|
77
|
-
if (activeModalFragments.isEmpty()) {
|
|
78
|
-
// Post dismiss to allow fragment attach to cancel if navigation is happening
|
|
79
|
-
schedulePendingDismiss()
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
override fun onFragmentDestroyed(fm: FragmentManager, f: Fragment) {
|
|
85
|
-
super.onFragmentDestroyed(fm, f)
|
|
86
|
-
|
|
87
|
-
if (activeModalFragments.isEmpty() && pendingDismissRunnable == null) {
|
|
88
|
-
onModalDidDismiss()
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallback!!, true)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Stop observing and cleanup.
|
|
98
|
-
*/
|
|
99
|
-
fun stop() {
|
|
100
|
-
val activity = reactContext.currentActivity as? AppCompatActivity
|
|
101
|
-
|
|
102
|
-
cancelPendingDismiss()
|
|
103
|
-
|
|
104
|
-
fragmentLifecycleCallback?.let { callback ->
|
|
105
|
-
activity?.supportFragmentManager?.unregisterFragmentLifecycleCallbacks(callback)
|
|
106
|
-
}
|
|
107
|
-
fragmentLifecycleCallback = null
|
|
108
|
-
|
|
109
|
-
activityLifecycleObserver?.let { observer ->
|
|
110
|
-
activity?.lifecycle?.removeObserver(observer)
|
|
111
|
-
}
|
|
112
|
-
activityLifecycleObserver = null
|
|
113
|
-
|
|
114
|
-
activeModalFragments.clear()
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
private fun schedulePendingDismiss() {
|
|
118
|
-
val activity = reactContext.currentActivity ?: return
|
|
119
|
-
val decorView = activity.window?.decorView ?: return
|
|
120
|
-
|
|
121
|
-
cancelPendingDismiss()
|
|
122
|
-
|
|
123
|
-
pendingDismissRunnable = Runnable {
|
|
124
|
-
pendingDismissRunnable = null
|
|
125
|
-
if (activeModalFragments.isEmpty()) {
|
|
126
|
-
onModalWillDismiss()
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
decorView.post(pendingDismissRunnable)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
private fun cancelPendingDismiss() {
|
|
133
|
-
val activity = reactContext.currentActivity ?: return
|
|
134
|
-
val decorView = activity.window?.decorView ?: return
|
|
135
|
-
|
|
136
|
-
pendingDismissRunnable?.let {
|
|
137
|
-
decorView.removeCallbacks(it)
|
|
138
|
-
pendingDismissRunnable = null
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
companion object {
|
|
143
|
-
/**
|
|
144
|
-
* Check if fragment is from react-native-screens.
|
|
145
|
-
*/
|
|
146
|
-
private fun isScreensFragment(fragment: Fragment): Boolean = fragment.javaClass.name.startsWith(RN_SCREENS_PACKAGE)
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Check if fragment is a react-native-screens modal (fullScreenModal, transparentModal, or formSheet).
|
|
150
|
-
* Uses reflection to check the fragment's screen.stackPresentation property.
|
|
151
|
-
*/
|
|
152
|
-
private fun isModalFragment(fragment: Fragment): Boolean {
|
|
153
|
-
val className = fragment.javaClass.name
|
|
154
|
-
|
|
155
|
-
if (!isScreensFragment(fragment)) {
|
|
156
|
-
return false
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// ScreenModalFragment is always a modal (used for formSheet with BottomSheetDialog)
|
|
160
|
-
if (className.contains("ScreenModalFragment")) {
|
|
161
|
-
return true
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// For ScreenStackFragment, check the screen's stackPresentation via reflection
|
|
165
|
-
try {
|
|
166
|
-
val getScreenMethod = fragment.javaClass.getMethod("getScreen")
|
|
167
|
-
val screen = getScreenMethod.invoke(fragment) ?: return false
|
|
168
|
-
|
|
169
|
-
val getStackPresentationMethod = screen.javaClass.getMethod("getStackPresentation")
|
|
170
|
-
val stackPresentation = getStackPresentationMethod.invoke(screen) ?: return false
|
|
171
|
-
|
|
172
|
-
val presentationName = stackPresentation.toString()
|
|
173
|
-
return presentationName == "MODAL" ||
|
|
174
|
-
presentationName == "TRANSPARENT_MODAL" ||
|
|
175
|
-
presentationName == "FORM_SHEET"
|
|
176
|
-
} catch (e: Exception) {
|
|
177
|
-
return false
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|