@lodev09/react-native-true-sheet 3.7.0 → 3.7.2
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/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +25 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +40 -15
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +84 -14
- package/android/src/main/java/com/lodev09/truesheet/core/RNScreensFragmentObserver.kt +26 -1
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +32 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetGrabberView.kt +26 -10
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetKeyboardObserver.kt +2 -2
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetStackManager.kt +23 -0
- package/ios/TrueSheetModule.mm +39 -0
- package/ios/TrueSheetView.h +4 -0
- package/ios/TrueSheetView.mm +49 -0
- package/ios/TrueSheetViewController.h +16 -0
- package/ios/TrueSheetViewController.mm +16 -0
- package/lib/module/TrueSheet.js +10 -0
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +34 -7
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/TrueSheetProvider.js +2 -1
- package/lib/module/TrueSheetProvider.js.map +1 -1
- package/lib/module/TrueSheetProvider.web.js +24 -1
- package/lib/module/TrueSheetProvider.web.js.map +1 -1
- package/lib/module/mocks/index.js +3 -1
- package/lib/module/mocks/index.js.map +1 -1
- package/lib/module/specs/NativeTrueSheetModule.js.map +1 -1
- package/lib/typescript/src/TrueSheet.d.ts +7 -0
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +4 -0
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.web.d.ts +9 -1
- package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheetProvider.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheetProvider.web.d.ts +5 -0
- package/lib/typescript/src/TrueSheetProvider.web.d.ts.map +1 -1
- package/lib/typescript/src/mocks/index.d.ts +1 -0
- package/lib/typescript/src/mocks/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts +6 -0
- package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/TrueSheet.tsx +10 -0
- package/src/TrueSheet.types.ts +4 -0
- package/src/TrueSheet.web.tsx +50 -6
- package/src/TrueSheetProvider.tsx +1 -0
- package/src/TrueSheetProvider.web.tsx +26 -0
- package/src/mocks/index.ts +2 -0
- package/src/specs/NativeTrueSheetModule.ts +7 -0
|
@@ -95,6 +95,31 @@ class TrueSheetModule(reactContext: ReactApplicationContext) :
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Dismiss all presented sheets by dismissing from the bottom of the stack
|
|
100
|
+
*
|
|
101
|
+
* @param animated Whether to animate the dismissals
|
|
102
|
+
* @param promise Promise that resolves when all sheets are dismissed
|
|
103
|
+
*/
|
|
104
|
+
@ReactMethod
|
|
105
|
+
fun dismissAll(animated: Boolean, promise: Promise) {
|
|
106
|
+
Handler(Looper.getMainLooper()).post {
|
|
107
|
+
try {
|
|
108
|
+
val rootSheet = TrueSheetStackManager.getRootSheet()
|
|
109
|
+
if (rootSheet == null) {
|
|
110
|
+
promise.resolve(null)
|
|
111
|
+
return@post
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
rootSheet.dismissAll(animated) {
|
|
115
|
+
promise.resolve(null)
|
|
116
|
+
}
|
|
117
|
+
} catch (e: Exception) {
|
|
118
|
+
promise.reject("OPERATION_FAILED", "Failed to dismiss all sheets: ${e.message}", e)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
98
123
|
/**
|
|
99
124
|
* Helper method to get TrueSheetView by tag and execute closure
|
|
100
125
|
*/
|
|
@@ -3,7 +3,6 @@ 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.ViewStructure
|
|
7
6
|
import android.view.accessibility.AccessibilityEvent
|
|
8
7
|
import androidx.annotation.UiThread
|
|
9
8
|
import com.facebook.react.bridge.LifecycleEventListener
|
|
@@ -32,10 +31,6 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
32
31
|
TrueSheetViewControllerDelegate,
|
|
33
32
|
TrueSheetContainerViewDelegate {
|
|
34
33
|
|
|
35
|
-
companion object {
|
|
36
|
-
const val TAG_NAME = "TrueSheet"
|
|
37
|
-
}
|
|
38
|
-
|
|
39
34
|
// ==================== Properties ====================
|
|
40
35
|
|
|
41
36
|
internal val viewController: TrueSheetViewController = TrueSheetViewController(reactContext)
|
|
@@ -109,6 +104,10 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
109
104
|
}
|
|
110
105
|
|
|
111
106
|
override fun addView(child: View?, index: Int) {
|
|
107
|
+
if (child is TrueSheetContainerView) {
|
|
108
|
+
viewController.removeSheetSnapshot()
|
|
109
|
+
}
|
|
110
|
+
|
|
112
111
|
viewController.addView(child, index)
|
|
113
112
|
|
|
114
113
|
if (child is TrueSheetContainerView) {
|
|
@@ -128,10 +127,11 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
128
127
|
val child = getChildAt(index)
|
|
129
128
|
if (child is TrueSheetContainerView) {
|
|
130
129
|
child.delegate = null
|
|
130
|
+
viewController.createSheetSnapshot()
|
|
131
131
|
|
|
132
|
-
// Dismiss
|
|
132
|
+
// Dismiss when container is removed
|
|
133
133
|
if (viewController.isPresented) {
|
|
134
|
-
|
|
134
|
+
dismissAll(true) {}
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
viewController.removeView(child)
|
|
@@ -157,13 +157,19 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
157
157
|
fun onDropInstance() {
|
|
158
158
|
reactContext.removeLifecycleEventListener(this)
|
|
159
159
|
|
|
160
|
-
viewController.dismiss()
|
|
161
|
-
viewController.delegate = null
|
|
162
|
-
|
|
163
160
|
TrueSheetModule.unregisterView(id)
|
|
164
161
|
TrueSheetStackManager.removeSheet(this)
|
|
165
162
|
|
|
166
163
|
didInitiallyPresent = false
|
|
164
|
+
|
|
165
|
+
if (viewController.isPresented) {
|
|
166
|
+
viewController.dismissPromise = {
|
|
167
|
+
viewController.delegate = null
|
|
168
|
+
}
|
|
169
|
+
viewController.dismiss()
|
|
170
|
+
} else {
|
|
171
|
+
viewController.delegate = null
|
|
172
|
+
}
|
|
167
173
|
}
|
|
168
174
|
|
|
169
175
|
/**
|
|
@@ -189,7 +195,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
189
195
|
if (viewController.dimmed == dimmed) return
|
|
190
196
|
viewController.dimmed = dimmed
|
|
191
197
|
if (viewController.isPresented) {
|
|
192
|
-
viewController.setupDimmedBackground(
|
|
198
|
+
viewController.setupDimmedBackground()
|
|
193
199
|
viewController.updateDimAmount()
|
|
194
200
|
}
|
|
195
201
|
}
|
|
@@ -198,7 +204,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
198
204
|
if (viewController.dimmedDetentIndex == index) return
|
|
199
205
|
viewController.dimmedDetentIndex = index
|
|
200
206
|
if (viewController.isPresented) {
|
|
201
|
-
viewController.setupDimmedBackground(
|
|
207
|
+
viewController.setupDimmedBackground()
|
|
202
208
|
viewController.updateDimAmount()
|
|
203
209
|
}
|
|
204
210
|
}
|
|
@@ -310,6 +316,16 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
310
316
|
viewController.dismiss(animated)
|
|
311
317
|
}
|
|
312
318
|
|
|
319
|
+
@UiThread
|
|
320
|
+
fun dismissAll(animated: Boolean = true, promiseCallback: () -> Unit) {
|
|
321
|
+
// Dismiss all sheets above first
|
|
322
|
+
dismiss(animated) {}
|
|
323
|
+
|
|
324
|
+
// Then dismiss itself
|
|
325
|
+
viewController.dismissPromise = promiseCallback
|
|
326
|
+
viewController.dismiss(animated)
|
|
327
|
+
}
|
|
328
|
+
|
|
313
329
|
@UiThread
|
|
314
330
|
fun resize(detentIndex: Int, promiseCallback: () -> Unit) {
|
|
315
331
|
if (!viewController.isPresented) {
|
|
@@ -326,12 +342,13 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
326
342
|
* Uses post() to ensure all layout passes complete before reconfiguring.
|
|
327
343
|
*/
|
|
328
344
|
fun updateSheetIfNeeded() {
|
|
329
|
-
if (!viewController.isPresented) return
|
|
330
|
-
if (isSheetUpdatePending) return
|
|
345
|
+
if (!viewController.isPresented || isSheetUpdatePending) return
|
|
331
346
|
|
|
332
347
|
isSheetUpdatePending = true
|
|
333
348
|
viewController.post {
|
|
334
349
|
isSheetUpdatePending = false
|
|
350
|
+
if (viewController.containerView == null) return@post
|
|
351
|
+
|
|
335
352
|
viewController.setupSheetDetentsForSizeChange()
|
|
336
353
|
TrueSheetStackManager.onSheetSizeChanged(this)
|
|
337
354
|
}
|
|
@@ -466,6 +483,14 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
466
483
|
eventDispatcher?.dispatchEvent(BackPressEvent(surfaceId, id))
|
|
467
484
|
}
|
|
468
485
|
|
|
486
|
+
override fun viewControllerDidDetectScreenDisappear() {
|
|
487
|
+
dismissAll(animated = true) {}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
override fun viewControllerDidDetectScreenDismiss() {
|
|
491
|
+
resetTranslation()
|
|
492
|
+
}
|
|
493
|
+
|
|
469
494
|
// ==================== TrueSheetContainerViewDelegate ====================
|
|
470
495
|
|
|
471
496
|
override fun containerViewContentDidChangeSize(width: Int, height: Int) {
|
|
@@ -489,7 +514,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
489
514
|
* of whichever window this view is in - whether that's the activity's window or a
|
|
490
515
|
* Modal's dialog window.
|
|
491
516
|
*/
|
|
492
|
-
|
|
517
|
+
override fun findRootContainerView(): ViewGroup? {
|
|
493
518
|
var current: android.view.ViewParent? = parent
|
|
494
519
|
|
|
495
520
|
while (current != null) {
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
package com.lodev09.truesheet
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
|
+
import android.graphics.Canvas
|
|
4
5
|
import android.os.Build
|
|
5
6
|
import android.view.MotionEvent
|
|
6
7
|
import android.view.View
|
|
7
8
|
import android.view.ViewGroup
|
|
8
9
|
import android.view.accessibility.AccessibilityNodeInfo
|
|
10
|
+
import android.widget.ImageView
|
|
9
11
|
import androidx.activity.OnBackPressedCallback
|
|
10
12
|
import androidx.appcompat.app.AppCompatActivity
|
|
11
|
-
import androidx.
|
|
13
|
+
import androidx.core.graphics.createBitmap
|
|
12
14
|
import androidx.core.view.isNotEmpty
|
|
13
|
-
import androidx.core.view.isVisible
|
|
14
15
|
import com.facebook.react.R
|
|
15
16
|
import com.facebook.react.uimanager.JSPointerDispatcher
|
|
16
17
|
import com.facebook.react.uimanager.JSTouchDispatcher
|
|
@@ -46,6 +47,7 @@ data class DetentInfo(val index: Int, val position: Float)
|
|
|
46
47
|
|
|
47
48
|
interface TrueSheetViewControllerDelegate {
|
|
48
49
|
val eventDispatcher: EventDispatcher?
|
|
50
|
+
fun findRootContainerView(): ViewGroup?
|
|
49
51
|
fun viewControllerWillPresent(index: Int, position: Float, detent: Float)
|
|
50
52
|
fun viewControllerDidPresent(index: Int, position: Float, detent: Float)
|
|
51
53
|
fun viewControllerWillDismiss()
|
|
@@ -61,6 +63,8 @@ interface TrueSheetViewControllerDelegate {
|
|
|
61
63
|
fun viewControllerWillBlur()
|
|
62
64
|
fun viewControllerDidBlur()
|
|
63
65
|
fun viewControllerDidBackPress()
|
|
66
|
+
fun viewControllerDidDetectScreenDisappear()
|
|
67
|
+
fun viewControllerDidDetectScreenDismiss()
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
// =============================================================================
|
|
@@ -84,8 +88,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
84
88
|
TrueSheetBottomSheetViewDelegate {
|
|
85
89
|
|
|
86
90
|
companion object {
|
|
87
|
-
const val TAG_NAME = "TrueSheet"
|
|
88
|
-
|
|
89
91
|
private const val DEFAULT_MAX_WIDTH = 640 // dp
|
|
90
92
|
private const val DEFAULT_CORNER_RADIUS = 16 // dp
|
|
91
93
|
private const val TRANSLATE_ANIMATION_DURATION = 200L
|
|
@@ -130,7 +132,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
130
132
|
|
|
131
133
|
private var interactionState: InteractionState = InteractionState.Idle
|
|
132
134
|
private var isDismissing = false
|
|
133
|
-
|
|
135
|
+
internal var wasHiddenByModal = false
|
|
134
136
|
private var shouldAnimatePresent = false
|
|
135
137
|
private var isPresentAnimating = false
|
|
136
138
|
|
|
@@ -213,7 +215,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
213
215
|
private val behavior: BottomSheetBehavior<TrueSheetBottomSheetView>?
|
|
214
216
|
get() = sheetView?.behavior
|
|
215
217
|
|
|
216
|
-
|
|
218
|
+
internal val containerView: TrueSheetContainerView?
|
|
217
219
|
get() = if (this.isNotEmpty()) getChildAt(0) as? TrueSheetContainerView else null
|
|
218
220
|
|
|
219
221
|
// Screen Measurements
|
|
@@ -228,11 +230,15 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
228
230
|
get() = ScreenUtils.getRealScreenHeight(reactContext)
|
|
229
231
|
|
|
230
232
|
// Content Measurements
|
|
233
|
+
// Cached values used during dismiss when container is unmounted
|
|
234
|
+
private var cachedContentHeight: Int = 0
|
|
235
|
+
private var cachedHeaderHeight: Int = 0
|
|
236
|
+
|
|
231
237
|
override val contentHeight: Int
|
|
232
|
-
get() = containerView?.contentHeight ?:
|
|
238
|
+
get() = containerView?.contentHeight ?: cachedContentHeight
|
|
233
239
|
|
|
234
240
|
override val headerHeight: Int
|
|
235
|
-
get() = containerView?.headerHeight ?:
|
|
241
|
+
get() = containerView?.headerHeight ?: cachedHeaderHeight
|
|
236
242
|
|
|
237
243
|
// Insets
|
|
238
244
|
// Target keyboard height used for detent calculations
|
|
@@ -260,9 +266,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
260
266
|
override val contentBottomInset: Int
|
|
261
267
|
get() = if (insetAdjustment == "automatic") bottomInset else 0
|
|
262
268
|
|
|
269
|
+
@Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
|
|
263
270
|
private val edgeToEdgeEnabled: Boolean
|
|
264
271
|
get() {
|
|
265
|
-
val defaultEnabled =
|
|
272
|
+
val defaultEnabled = Build.VERSION.SDK_INT >= 36
|
|
266
273
|
return BuildConfig.EDGE_TO_EDGE_ENABLED || defaultEnabled
|
|
267
274
|
}
|
|
268
275
|
|
|
@@ -320,6 +327,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
320
327
|
parentDimView?.detach()
|
|
321
328
|
parentDimView = null
|
|
322
329
|
|
|
330
|
+
// Cleanup snapshot
|
|
331
|
+
removeSheetSnapshot()
|
|
332
|
+
|
|
323
333
|
// Detach content from sheet
|
|
324
334
|
sheetView?.removeView(this)
|
|
325
335
|
|
|
@@ -331,6 +341,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
331
341
|
isPresented = false
|
|
332
342
|
isSheetVisible = false
|
|
333
343
|
wasHiddenByModal = false
|
|
344
|
+
cachedContentHeight = 0
|
|
345
|
+
cachedHeaderHeight = 0
|
|
334
346
|
isPresentAnimating = false
|
|
335
347
|
lastEmittedPositionPx = -1
|
|
336
348
|
detentIndexBeforeKeyboard = -1
|
|
@@ -338,6 +350,34 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
338
350
|
shouldAnimatePresent = true
|
|
339
351
|
}
|
|
340
352
|
|
|
353
|
+
private var snapshotView: View? = null
|
|
354
|
+
|
|
355
|
+
fun createSheetSnapshot() {
|
|
356
|
+
if (!isPresented) return
|
|
357
|
+
val sheet = sheetView ?: return
|
|
358
|
+
if (sheet.width <= 0 || sheet.height <= 0) return
|
|
359
|
+
|
|
360
|
+
val bitmap = createBitmap(sheet.width, sheet.height)
|
|
361
|
+
val canvas = Canvas(bitmap)
|
|
362
|
+
sheet.draw(canvas)
|
|
363
|
+
|
|
364
|
+
val snapshot = ImageView(reactContext).apply {
|
|
365
|
+
setImageBitmap(bitmap)
|
|
366
|
+
layoutParams = LayoutParams(sheet.width, sheet.height)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
sheet.addView(snapshot, 0)
|
|
370
|
+
snapshotView = snapshot
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
fun removeSheetSnapshot() {
|
|
374
|
+
snapshotView?.let {
|
|
375
|
+
(it as? ImageView)?.setImageDrawable(null)
|
|
376
|
+
sheetView?.removeView(it)
|
|
377
|
+
}
|
|
378
|
+
snapshotView = null
|
|
379
|
+
}
|
|
380
|
+
|
|
341
381
|
// =============================================================================
|
|
342
382
|
// MARK: - Back Button Handling
|
|
343
383
|
// =============================================================================
|
|
@@ -405,6 +445,19 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
405
445
|
dismissOrCollapseToLowest()
|
|
406
446
|
}
|
|
407
447
|
|
|
448
|
+
// =============================================================================
|
|
449
|
+
// MARK: - TrueSheetBottomSheetViewDelegate - Grabber Tap
|
|
450
|
+
// =============================================================================
|
|
451
|
+
|
|
452
|
+
override fun bottomSheetViewDidTapGrabber() {
|
|
453
|
+
val nextIndex = (currentDetentIndex + 1) % detents.size
|
|
454
|
+
if (nextIndex == 0 && detents.size == 1 && dismissible) {
|
|
455
|
+
dismiss(animated = true)
|
|
456
|
+
} else {
|
|
457
|
+
setStateForDetentIndex(nextIndex)
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
408
461
|
// =============================================================================
|
|
409
462
|
// MARK: - BottomSheetCallback
|
|
410
463
|
// =============================================================================
|
|
@@ -489,7 +542,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
489
542
|
|
|
490
543
|
if (detentInfo.index != currentDetentIndex) {
|
|
491
544
|
currentDetentIndex = detentInfo.index
|
|
492
|
-
setupDimmedBackground(
|
|
545
|
+
setupDimmedBackground()
|
|
493
546
|
delegate?.viewControllerDidChangeDetent(detentInfo.index, detentInfo.position, detent)
|
|
494
547
|
}
|
|
495
548
|
|
|
@@ -524,6 +577,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
524
577
|
onModalWillDismiss = {
|
|
525
578
|
if (isPresented && wasHiddenByModal && isTopmostSheet) {
|
|
526
579
|
showAfterModal()
|
|
580
|
+
delegate?.viewControllerDidDetectScreenDismiss()
|
|
527
581
|
}
|
|
528
582
|
},
|
|
529
583
|
onModalDidDismiss = {
|
|
@@ -534,6 +588,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
534
588
|
post { parent.showAfterModal() }
|
|
535
589
|
}
|
|
536
590
|
}
|
|
591
|
+
},
|
|
592
|
+
onNonModalScreenPushed = {
|
|
593
|
+
// Only handle on root sheet (no parent) to trigger dismissAll
|
|
594
|
+
if (isPresented && isSheetVisible && parentSheetView == null) {
|
|
595
|
+
delegate?.viewControllerDidDetectScreenDisappear()
|
|
596
|
+
}
|
|
537
597
|
}
|
|
538
598
|
)
|
|
539
599
|
rnScreensObserver?.start()
|
|
@@ -603,7 +663,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
603
663
|
}
|
|
604
664
|
|
|
605
665
|
if (isPresented) {
|
|
606
|
-
setupDimmedBackground(
|
|
666
|
+
setupDimmedBackground()
|
|
607
667
|
setStateForDetentIndex(detentIndex)
|
|
608
668
|
} else {
|
|
609
669
|
shouldAnimatePresent = animated
|
|
@@ -616,7 +676,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
616
676
|
emitWillPresentEvents()
|
|
617
677
|
|
|
618
678
|
setupSheetDetents()
|
|
619
|
-
setupDimmedBackground(
|
|
679
|
+
setupDimmedBackground()
|
|
620
680
|
setupKeyboardObserver()
|
|
621
681
|
setupModalObserver()
|
|
622
682
|
setupBackCallback()
|
|
@@ -647,6 +707,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
647
707
|
|
|
648
708
|
// Create layout params with behavior
|
|
649
709
|
val params = sheet.createLayoutParams()
|
|
710
|
+
|
|
711
|
+
@Suppress("UNCHECKED_CAST")
|
|
650
712
|
val behavior = params.behavior as BottomSheetBehavior<TrueSheetBottomSheetView>
|
|
651
713
|
|
|
652
714
|
// Configure behavior
|
|
@@ -730,6 +792,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
730
792
|
return
|
|
731
793
|
}
|
|
732
794
|
|
|
795
|
+
containerView?.let {
|
|
796
|
+
cachedContentHeight = it.contentHeight
|
|
797
|
+
cachedHeaderHeight = it.headerHeight
|
|
798
|
+
}
|
|
799
|
+
|
|
733
800
|
interactionState = InteractionState.Reconfiguring
|
|
734
801
|
|
|
735
802
|
behavior.isFitToContents = false
|
|
@@ -801,7 +868,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
801
868
|
// MARK: - Dimmed Background
|
|
802
869
|
// =============================================================================
|
|
803
870
|
|
|
804
|
-
fun setupDimmedBackground(
|
|
871
|
+
fun setupDimmedBackground() {
|
|
805
872
|
val coordinator = this.coordinatorLayout ?: run {
|
|
806
873
|
RNLog.e(reactContext, "TrueSheet: coordinatorLayout is null in setupDimmedBackground")
|
|
807
874
|
return
|
|
@@ -840,6 +907,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
840
907
|
|
|
841
908
|
fun updateDimAmount(sheetTop: Int? = null, animated: Boolean = false) {
|
|
842
909
|
if (!dimmed) return
|
|
910
|
+
if (contentHeight == 0) return
|
|
911
|
+
|
|
843
912
|
val keyboardOffset = if (isDismissing) 0 else currentKeyboardInset
|
|
844
913
|
val top = (sheetTop ?: sheetView?.top ?: return) + keyboardOffset
|
|
845
914
|
|
|
@@ -892,7 +961,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
892
961
|
}
|
|
893
962
|
|
|
894
963
|
fun saveFocusedView() {
|
|
895
|
-
focusedViewBeforeBlur =
|
|
964
|
+
focusedViewBeforeBlur = delegate?.findRootContainerView()?.findFocus()
|
|
965
|
+
?: reactContext.currentActivity?.currentFocus
|
|
896
966
|
}
|
|
897
967
|
|
|
898
968
|
fun restoreFocusedView() {
|
|
@@ -18,7 +18,8 @@ class RNScreensFragmentObserver(
|
|
|
18
18
|
private val reactContext: ReactContext,
|
|
19
19
|
private val onModalPresented: () -> Unit,
|
|
20
20
|
private val onModalWillDismiss: () -> Unit,
|
|
21
|
-
private val onModalDidDismiss: () -> Unit
|
|
21
|
+
private val onModalDidDismiss: () -> Unit,
|
|
22
|
+
private val onNonModalScreenPushed: () -> Unit
|
|
22
23
|
) {
|
|
23
24
|
private var fragmentLifecycleCallback: FragmentManager.FragmentLifecycleCallbacks? = null
|
|
24
25
|
private var activityLifecycleObserver: DefaultLifecycleObserver? = null
|
|
@@ -61,6 +62,9 @@ class RNScreensFragmentObserver(
|
|
|
61
62
|
if (activeModalFragments.size == 1) {
|
|
62
63
|
onModalPresented()
|
|
63
64
|
}
|
|
65
|
+
} else if (activeModalFragments.isEmpty() && isNonModalScreenFragment(f)) {
|
|
66
|
+
// Only trigger non-modal push when no modals are active
|
|
67
|
+
onNonModalScreenPushed()
|
|
64
68
|
}
|
|
65
69
|
}
|
|
66
70
|
|
|
@@ -145,6 +149,27 @@ class RNScreensFragmentObserver(
|
|
|
145
149
|
*/
|
|
146
150
|
private fun isScreensFragment(fragment: Fragment): Boolean = fragment.javaClass.name.startsWith(RN_SCREENS_PACKAGE)
|
|
147
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Check if fragment is a non-modal screen (regular push presentation).
|
|
154
|
+
*/
|
|
155
|
+
private fun isNonModalScreenFragment(fragment: Fragment): Boolean {
|
|
156
|
+
if (!isScreensFragment(fragment)) return false
|
|
157
|
+
// ScreenModalFragment is always a modal
|
|
158
|
+
if (fragment.javaClass.name.contains("ScreenModalFragment")) return false
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
val getScreenMethod = fragment.javaClass.getMethod("getScreen")
|
|
162
|
+
val screen = getScreenMethod.invoke(fragment) ?: return false
|
|
163
|
+
|
|
164
|
+
val getStackPresentationMethod = screen.javaClass.getMethod("getStackPresentation")
|
|
165
|
+
val stackPresentation = getStackPresentationMethod.invoke(screen) ?: return false
|
|
166
|
+
|
|
167
|
+
return stackPresentation.toString() == "PUSH"
|
|
168
|
+
} catch (e: Exception) {
|
|
169
|
+
return false
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
148
173
|
/**
|
|
149
174
|
* Check if fragment is a react-native-screens modal (fullScreenModal, transparentModal, or formSheet).
|
|
150
175
|
* Uses reflection to check the fragment's screen.stackPresentation property.
|
|
@@ -3,10 +3,13 @@ package com.lodev09.truesheet.core
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.graphics.Color
|
|
5
5
|
import android.graphics.Outline
|
|
6
|
+
import android.graphics.Rect
|
|
6
7
|
import android.graphics.drawable.ShapeDrawable
|
|
7
8
|
import android.graphics.drawable.shapes.RoundRectShape
|
|
8
9
|
import android.util.TypedValue
|
|
10
|
+
import android.view.GestureDetector
|
|
9
11
|
import android.view.Gravity
|
|
12
|
+
import android.view.MotionEvent
|
|
10
13
|
import android.view.View
|
|
11
14
|
import android.view.ViewOutlineProvider
|
|
12
15
|
import android.widget.FrameLayout
|
|
@@ -22,6 +25,7 @@ interface TrueSheetBottomSheetViewDelegate {
|
|
|
22
25
|
val sheetBackgroundColor: Int?
|
|
23
26
|
val grabber: Boolean
|
|
24
27
|
val grabberOptions: GrabberOptions?
|
|
28
|
+
fun bottomSheetViewDidTapGrabber()
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
/**
|
|
@@ -162,4 +166,32 @@ class TrueSheetBottomSheetView(private val reactContext: ThemedReactContext) : F
|
|
|
162
166
|
|
|
163
167
|
addView(grabberView, 0)
|
|
164
168
|
}
|
|
169
|
+
|
|
170
|
+
// =============================================================================
|
|
171
|
+
// MARK: - Grabber Tap Detection
|
|
172
|
+
// =============================================================================
|
|
173
|
+
|
|
174
|
+
private val grabberTapDetector = GestureDetector(
|
|
175
|
+
context,
|
|
176
|
+
object : GestureDetector.SimpleOnGestureListener() {
|
|
177
|
+
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
|
178
|
+
delegate?.bottomSheetViewDidTapGrabber()
|
|
179
|
+
return true
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
|
|
185
|
+
val grabber = findViewWithTag<View>(GRABBER_TAG)
|
|
186
|
+
if (grabber != null) {
|
|
187
|
+
val rect = Rect()
|
|
188
|
+
grabber.getHitRect(rect)
|
|
189
|
+
if (rect.contains(ev.x.toInt(), ev.y.toInt())) {
|
|
190
|
+
if (grabberTapDetector.onTouchEvent(ev)) {
|
|
191
|
+
return true
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return super.dispatchTouchEvent(ev)
|
|
196
|
+
}
|
|
165
197
|
}
|
|
@@ -25,16 +25,18 @@ data class GrabberOptions(
|
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Native grabber (drag handle) view for the bottom sheet.
|
|
28
|
-
* Displays a small pill-shaped indicator at the top of the sheet.
|
|
28
|
+
* Displays a small pill-shaped indicator at the top of the sheet with a tappable hitbox.
|
|
29
29
|
*/
|
|
30
30
|
@SuppressLint("ViewConstructor")
|
|
31
|
-
class TrueSheetGrabberView(context: Context, private val options: GrabberOptions? = null) :
|
|
31
|
+
class TrueSheetGrabberView(context: Context, private val options: GrabberOptions? = null) : FrameLayout(context) {
|
|
32
32
|
|
|
33
33
|
companion object {
|
|
34
34
|
private const val DEFAULT_WIDTH = 32f // dp
|
|
35
35
|
private const val DEFAULT_HEIGHT = 4f // dp
|
|
36
36
|
private const val DEFAULT_TOP_MARGIN = 16f // dp
|
|
37
37
|
private const val DEFAULT_ALPHA = 0.4f
|
|
38
|
+
private const val HITBOX_PADDING_HORIZONTAL = 16f // dp
|
|
39
|
+
private const val HITBOX_PADDING_VERTICAL = 16f // dp
|
|
38
40
|
private val DEFAULT_COLOR = Color.argb((DEFAULT_ALPHA * 255).toInt(), 73, 69, 79) // #49454F @ 40%
|
|
39
41
|
}
|
|
40
42
|
|
|
@@ -57,19 +59,33 @@ class TrueSheetGrabberView(context: Context, private val options: GrabberOptions
|
|
|
57
59
|
get() = if (isAdaptive) getAdaptiveColor(options?.color) else options?.color ?: DEFAULT_COLOR
|
|
58
60
|
|
|
59
61
|
init {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
val hitboxWidth = grabberWidth + (HITBOX_PADDING_HORIZONTAL * 2)
|
|
63
|
+
val hitboxHeight = grabberHeight + (HITBOX_PADDING_VERTICAL * 2)
|
|
64
|
+
|
|
65
|
+
layoutParams = LayoutParams(
|
|
66
|
+
hitboxWidth.dpToPx().toInt(),
|
|
67
|
+
hitboxHeight.dpToPx().toInt()
|
|
63
68
|
).apply {
|
|
64
69
|
gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP
|
|
65
|
-
topMargin = grabberTopMargin.dpToPx().toInt()
|
|
70
|
+
topMargin = (grabberTopMargin - HITBOX_PADDING_VERTICAL).dpToPx().toInt()
|
|
66
71
|
}
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
// The visible pill centered inside the hitbox
|
|
74
|
+
val pillView = View(context).apply {
|
|
75
|
+
layoutParams = LayoutParams(
|
|
76
|
+
grabberWidth.dpToPx().toInt(),
|
|
77
|
+
grabberHeight.dpToPx().toInt()
|
|
78
|
+
).apply {
|
|
79
|
+
gravity = Gravity.CENTER
|
|
80
|
+
}
|
|
81
|
+
background = GradientDrawable().apply {
|
|
82
|
+
shape = GradientDrawable.RECTANGLE
|
|
83
|
+
cornerRadius = grabberCornerRadius.dpToPx()
|
|
84
|
+
setColor(grabberColor)
|
|
85
|
+
}
|
|
72
86
|
}
|
|
87
|
+
|
|
88
|
+
addView(pillView)
|
|
73
89
|
}
|
|
74
90
|
|
|
75
91
|
private fun getAdaptiveColor(baseColor: Int? = null): Int {
|
|
@@ -35,7 +35,7 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
|
|
|
35
35
|
private set
|
|
36
36
|
|
|
37
37
|
fun isFocusedViewWithinSheet(sheetView: View): Boolean {
|
|
38
|
-
val focusedView =
|
|
38
|
+
val focusedView = targetView.rootView?.findFocus() ?: return false
|
|
39
39
|
var current: View? = focusedView
|
|
40
40
|
while (current != null && current !== targetView) {
|
|
41
41
|
if (current === sheetView) return true
|
|
@@ -131,7 +131,7 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
|
|
|
131
131
|
// Ensure we don't add duplicate listeners
|
|
132
132
|
if (globalLayoutListener != null) return
|
|
133
133
|
|
|
134
|
-
val rootView =
|
|
134
|
+
val rootView = targetView.rootView ?: return
|
|
135
135
|
activityRootView = rootView
|
|
136
136
|
|
|
137
137
|
globalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
|
|
@@ -151,4 +151,27 @@ object TrueSheetStackManager {
|
|
|
151
151
|
return findTopmostSheet()
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Returns the root presented sheet for dismissAll.
|
|
157
|
+
* Starts from the topmost sheet and walks up to find the root of its stack.
|
|
158
|
+
* Stops at modal boundary (parent hidden by modal) or when there's no parent.
|
|
159
|
+
*/
|
|
160
|
+
@JvmStatic
|
|
161
|
+
fun getRootSheet(): TrueSheetView? {
|
|
162
|
+
synchronized(presentedSheetStack) {
|
|
163
|
+
val topmost = presentedSheetStack.lastOrNull { it.viewController.isPresented } ?: return null
|
|
164
|
+
|
|
165
|
+
var current: TrueSheetView = topmost
|
|
166
|
+
while (true) {
|
|
167
|
+
val parent = current.viewController.parentSheetView ?: return current
|
|
168
|
+
|
|
169
|
+
if (parent.viewController.wasHiddenByModal) {
|
|
170
|
+
return current
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
current = parent
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
154
177
|
}
|