@lodev09/react-native-true-sheet 3.3.0-beta.2 → 3.3.0-beta.4
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/TrueSheetViewController.kt +47 -24
- package/android/src/main/java/com/lodev09/truesheet/core/RNScreensFragmentObserver.kt +2 -2
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDialogObserver.kt +9 -6
- package/android/src/main/java/com/lodev09/truesheet/core/{TrueSheetKeyboardHandler.kt → TrueSheetKeyboardObserver.kt} +29 -39
- package/android/src/main/res/anim/true_sheet_fade_in.xml +6 -0
- package/android/src/main/res/anim/true_sheet_fade_out.xml +6 -0
- package/android/src/main/res/values/styles.xml +6 -0
- package/package.json +1 -1
|
@@ -29,7 +29,8 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
|
|
|
29
29
|
import com.lodev09.truesheet.core.GrabberOptions
|
|
30
30
|
import com.lodev09.truesheet.core.RNScreensFragmentObserver
|
|
31
31
|
import com.lodev09.truesheet.core.TrueSheetGrabberView
|
|
32
|
-
import com.lodev09.truesheet.core.
|
|
32
|
+
import com.lodev09.truesheet.core.TrueSheetKeyboardObserver
|
|
33
|
+
import com.lodev09.truesheet.core.TrueSheetKeyboardObserverDelegate
|
|
33
34
|
import com.lodev09.truesheet.utils.ScreenUtils
|
|
34
35
|
|
|
35
36
|
data class DetentInfo(val index: Int, val position: Float)
|
|
@@ -126,6 +127,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
126
127
|
private var windowAnimation: Int = 0
|
|
127
128
|
private var lastEmittedPositionPx: Int = -1
|
|
128
129
|
|
|
130
|
+
/** Tracks if this sheet was hidden due to a RN Screens modal (vs sheet stacking) */
|
|
131
|
+
private var wasHiddenByModal = false
|
|
132
|
+
|
|
129
133
|
var presentPromise: (() -> Unit)? = null
|
|
130
134
|
var dismissPromise: (() -> Unit)? = null
|
|
131
135
|
|
|
@@ -224,7 +228,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
224
228
|
|
|
225
229
|
window?.apply {
|
|
226
230
|
windowAnimation = attributes.windowAnimations
|
|
227
|
-
// Disable default keyboard avoidance - sheet handles it via
|
|
231
|
+
// Disable default keyboard avoidance - sheet handles it via setupKeyboardObserver
|
|
228
232
|
setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
|
|
229
233
|
}
|
|
230
234
|
|
|
@@ -255,7 +259,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
255
259
|
setOnDismissListener(null)
|
|
256
260
|
}
|
|
257
261
|
|
|
258
|
-
|
|
262
|
+
cleanupKeyboardObserver()
|
|
259
263
|
cleanupModalObserver()
|
|
260
264
|
sheetContainer?.removeView(this)
|
|
261
265
|
|
|
@@ -264,6 +268,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
264
268
|
isDismissing = false
|
|
265
269
|
isPresented = false
|
|
266
270
|
isDialogVisible = false
|
|
271
|
+
wasHiddenByModal = false
|
|
267
272
|
lastEmittedPositionPx = -1
|
|
268
273
|
}
|
|
269
274
|
|
|
@@ -274,7 +279,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
274
279
|
resetAnimation()
|
|
275
280
|
setupBackground()
|
|
276
281
|
setupGrabber()
|
|
277
|
-
|
|
282
|
+
setupKeyboardObserver()
|
|
278
283
|
|
|
279
284
|
sheetContainer?.post {
|
|
280
285
|
bottomSheetView?.let { emitChangePositionDelegate(it, realtime = false) }
|
|
@@ -380,13 +385,16 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
380
385
|
rnScreensObserver = RNScreensFragmentObserver(
|
|
381
386
|
reactContext = reactContext,
|
|
382
387
|
onModalPresented = {
|
|
383
|
-
if (isPresented) {
|
|
384
|
-
hideDialog()
|
|
388
|
+
if (isPresented && isDialogVisible) {
|
|
389
|
+
hideDialog(animated = true)
|
|
390
|
+
wasHiddenByModal = true
|
|
385
391
|
}
|
|
386
392
|
},
|
|
387
393
|
onModalDismissed = {
|
|
388
|
-
if
|
|
389
|
-
|
|
394
|
+
// Only show if we were the one hidden by modal, not by sheet stacking
|
|
395
|
+
if (isPresented && wasHiddenByModal) {
|
|
396
|
+
showDialog(animated = true)
|
|
397
|
+
wasHiddenByModal = false
|
|
390
398
|
}
|
|
391
399
|
}
|
|
392
400
|
)
|
|
@@ -436,8 +444,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
436
444
|
}
|
|
437
445
|
|
|
438
446
|
/** Hides without dismissing. Used for sheet stacking and RN Screens modals. */
|
|
439
|
-
fun hideDialog(emitPosition: Boolean = false) {
|
|
447
|
+
fun hideDialog(emitPosition: Boolean = false, animated: Boolean = false) {
|
|
440
448
|
isDialogVisible = false
|
|
449
|
+
if (animated) {
|
|
450
|
+
dialog?.window?.setWindowAnimations(com.lodev09.truesheet.R.style.TrueSheetFadeAnimation)
|
|
451
|
+
}
|
|
441
452
|
dialog?.window?.decorView?.visibility = INVISIBLE
|
|
442
453
|
if (emitPosition) {
|
|
443
454
|
emitDismissedPosition()
|
|
@@ -445,12 +456,18 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
445
456
|
}
|
|
446
457
|
|
|
447
458
|
/** Shows a previously hidden dialog. */
|
|
448
|
-
fun showDialog(emitPosition: Boolean = false) {
|
|
459
|
+
fun showDialog(emitPosition: Boolean = false, animated: Boolean = false) {
|
|
449
460
|
isDialogVisible = true
|
|
450
461
|
dialog?.window?.decorView?.visibility = VISIBLE
|
|
451
462
|
if (emitPosition) {
|
|
452
463
|
bottomSheetView?.let { emitChangePositionDelegate(it, realtime = false) }
|
|
453
464
|
}
|
|
465
|
+
if (animated) {
|
|
466
|
+
// Restore original animation after fade-in completes (100ms)
|
|
467
|
+
sheetContainer?.postDelayed({
|
|
468
|
+
dialog?.window?.setWindowAnimations(windowAnimation)
|
|
469
|
+
}, 100)
|
|
470
|
+
}
|
|
454
471
|
}
|
|
455
472
|
|
|
456
473
|
// ====================================================================
|
|
@@ -568,18 +585,23 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
568
585
|
bottomSheet.addView(grabberView)
|
|
569
586
|
}
|
|
570
587
|
|
|
571
|
-
private var
|
|
588
|
+
private var keyboardObserver: TrueSheetKeyboardObserver? = null
|
|
572
589
|
|
|
573
|
-
|
|
574
|
-
fun setupKeyboardHandler() {
|
|
590
|
+
fun setupKeyboardObserver() {
|
|
575
591
|
val bottomSheet = bottomSheetView ?: return
|
|
576
|
-
|
|
577
|
-
|
|
592
|
+
keyboardObserver = TrueSheetKeyboardObserver(bottomSheet, reactContext).apply {
|
|
593
|
+
delegate = object : TrueSheetKeyboardObserverDelegate {
|
|
594
|
+
override fun keyboardHeightDidChange(height: Int) {
|
|
595
|
+
setupSheetDetents()
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
start()
|
|
599
|
+
}
|
|
578
600
|
}
|
|
579
601
|
|
|
580
|
-
fun
|
|
581
|
-
|
|
582
|
-
|
|
602
|
+
fun cleanupKeyboardObserver() {
|
|
603
|
+
keyboardObserver?.stop()
|
|
604
|
+
keyboardObserver = null
|
|
583
605
|
}
|
|
584
606
|
|
|
585
607
|
fun setupBackground() {
|
|
@@ -632,7 +654,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
632
654
|
val sheetTop = bottomSheet.top
|
|
633
655
|
|
|
634
656
|
// Footer Y relative to sheet: place at bottom of sheet container minus footer height
|
|
635
|
-
var footerY = (sheetHeight - sheetTop - footerHeight).toFloat()
|
|
657
|
+
var footerY = (sheetHeight - sheetTop - footerHeight - keyboardHeight).toFloat()
|
|
636
658
|
|
|
637
659
|
if (slideOffset != null && slideOffset < 0) {
|
|
638
660
|
footerY -= (footerHeight * slideOffset)
|
|
@@ -806,16 +828,17 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
806
828
|
// MARK: - Detent Calculations
|
|
807
829
|
// ====================================================================
|
|
808
830
|
|
|
831
|
+
private val keyboardHeight: Int
|
|
832
|
+
get() = keyboardObserver?.currentHeight ?: 0
|
|
833
|
+
|
|
809
834
|
private fun getDetentHeight(detent: Double): Int {
|
|
810
|
-
val height
|
|
811
|
-
|
|
812
|
-
contentHeight + headerHeight + contentBottomInset
|
|
835
|
+
val height = if (detent == -1.0) {
|
|
836
|
+
contentHeight + headerHeight + contentBottomInset + keyboardHeight
|
|
813
837
|
} else {
|
|
814
838
|
if (detent <= 0.0 || detent > 1.0) {
|
|
815
839
|
throw IllegalArgumentException("TrueSheet: detent fraction ($detent) must be between 0 and 1")
|
|
816
840
|
}
|
|
817
|
-
|
|
818
|
-
(detent * screenHeight).toInt() + contentBottomInset
|
|
841
|
+
(detent * screenHeight).toInt() + contentBottomInset + keyboardHeight
|
|
819
842
|
}
|
|
820
843
|
|
|
821
844
|
val maxAllowedHeight = screenHeight + contentBottomInset
|
|
@@ -27,8 +27,8 @@ class RNScreensFragmentObserver(
|
|
|
27
27
|
val fragmentManager = activity.supportFragmentManager
|
|
28
28
|
|
|
29
29
|
fragmentLifecycleCallback = object : FragmentManager.FragmentLifecycleCallbacks() {
|
|
30
|
-
override fun
|
|
31
|
-
super.
|
|
30
|
+
override fun onFragmentPreAttached(fm: FragmentManager, fragment: Fragment, context: android.content.Context) {
|
|
31
|
+
super.onFragmentPreAttached(fm, fragment, context)
|
|
32
32
|
|
|
33
33
|
if (isModalFragment(fragment) && !activeModalFragments.contains(fragment)) {
|
|
34
34
|
activeModalFragments.add(fragment)
|
|
@@ -20,12 +20,15 @@ object TrueSheetDialogObserver {
|
|
|
20
20
|
val parentSheet = presentedSheetStack.lastOrNull()
|
|
21
21
|
?.takeIf { it.viewController.isPresented && it.viewController.isDialogVisible }
|
|
22
22
|
|
|
23
|
-
// Hide parent
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (
|
|
28
|
-
|
|
23
|
+
// Hide any parent sheets that would be visible behind the new sheet
|
|
24
|
+
val newSheetTop = sheetView.viewController.getExpectedSheetTop(detentIndex)
|
|
25
|
+
for (sheet in presentedSheetStack) {
|
|
26
|
+
if (!sheet.viewController.isDialogVisible) continue
|
|
27
|
+
if (sheet.viewController.isExpanded) continue
|
|
28
|
+
|
|
29
|
+
val sheetTop = sheet.viewController.currentSheetTop
|
|
30
|
+
if (sheetTop < newSheetTop) {
|
|
31
|
+
sheet.viewController.hideDialog(emitPosition = true)
|
|
29
32
|
}
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -9,25 +9,25 @@ import androidx.core.view.WindowInsetsAnimationCompat
|
|
|
9
9
|
import androidx.core.view.WindowInsetsCompat
|
|
10
10
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
11
11
|
|
|
12
|
+
interface TrueSheetKeyboardObserverDelegate {
|
|
13
|
+
fun keyboardHeightDidChange(height: Int)
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
/**
|
|
13
|
-
*
|
|
14
|
-
* Uses WindowInsetsAnimationCompat
|
|
15
|
-
* falls back to ViewTreeObserver on Activity's decor view for API 29 and below.
|
|
16
|
-
*
|
|
17
|
-
* @param targetView The view to translate (typically the bottom sheet)
|
|
18
|
-
* @param reactContext The React context to get the current activity
|
|
19
|
-
* @param topInset The top safe area inset to respect
|
|
17
|
+
* Tracks keyboard height and notifies delegate on changes.
|
|
18
|
+
* Uses WindowInsetsAnimationCompat on API 30+, ViewTreeObserver fallback on older versions.
|
|
20
19
|
*/
|
|
21
|
-
class
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
class TrueSheetKeyboardObserver(private val targetView: View, private val reactContext: ThemedReactContext) {
|
|
21
|
+
|
|
22
|
+
var delegate: TrueSheetKeyboardObserverDelegate? = null
|
|
23
|
+
|
|
24
|
+
var currentHeight: Int = 0
|
|
25
|
+
private set
|
|
26
26
|
|
|
27
27
|
private var globalLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
|
|
28
28
|
private var activityRootView: View? = null
|
|
29
29
|
|
|
30
|
-
fun
|
|
30
|
+
fun start() {
|
|
31
31
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
32
32
|
setupAnimationCallback()
|
|
33
33
|
} else {
|
|
@@ -35,7 +35,7 @@ class TrueSheetKeyboardHandler(
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
fun
|
|
38
|
+
fun stop() {
|
|
39
39
|
globalLayoutListener?.let { listener ->
|
|
40
40
|
activityRootView?.viewTreeObserver?.removeOnGlobalLayoutListener(listener)
|
|
41
41
|
globalLayoutListener = null
|
|
@@ -44,35 +44,31 @@ class TrueSheetKeyboardHandler(
|
|
|
44
44
|
ViewCompat.setWindowInsetsAnimationCallback(targetView, null)
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
private fun
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
private fun updateHeight(height: Int) {
|
|
48
|
+
if (currentHeight != height) {
|
|
49
|
+
currentHeight = height
|
|
50
|
+
delegate?.keyboardHeightDidChange(height)
|
|
51
|
+
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
private fun getKeyboardHeight(insets: WindowInsetsCompat?): Int = insets?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0
|
|
55
|
+
|
|
55
56
|
private fun setupAnimationCallback() {
|
|
56
57
|
ViewCompat.setWindowInsetsAnimationCallback(
|
|
57
58
|
targetView,
|
|
58
59
|
object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
|
|
59
|
-
private var
|
|
60
|
-
private var
|
|
61
|
-
|
|
62
|
-
private fun getKeyboardHeight(rootInsets: WindowInsetsCompat?): Int {
|
|
63
|
-
if (rootInsets == null) return 0
|
|
64
|
-
return rootInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
|
65
|
-
}
|
|
60
|
+
private var startHeight = 0
|
|
61
|
+
private var endHeight = 0
|
|
66
62
|
|
|
67
63
|
override fun onPrepare(animation: WindowInsetsAnimationCompat) {
|
|
68
|
-
|
|
64
|
+
startHeight = getKeyboardHeight(ViewCompat.getRootWindowInsets(targetView))
|
|
69
65
|
}
|
|
70
66
|
|
|
71
67
|
override fun onStart(
|
|
72
68
|
animation: WindowInsetsAnimationCompat,
|
|
73
69
|
bounds: WindowInsetsAnimationCompat.BoundsCompat
|
|
74
70
|
): WindowInsetsAnimationCompat.BoundsCompat {
|
|
75
|
-
|
|
71
|
+
endHeight = getKeyboardHeight(ViewCompat.getRootWindowInsets(targetView))
|
|
76
72
|
return bounds
|
|
77
73
|
}
|
|
78
74
|
|
|
@@ -82,23 +78,21 @@ class TrueSheetKeyboardHandler(
|
|
|
82
78
|
} ?: return insets
|
|
83
79
|
|
|
84
80
|
val fraction = imeAnimation.interpolatedFraction
|
|
85
|
-
val
|
|
86
|
-
|
|
81
|
+
val currentHeight = (startHeight + (endHeight - startHeight) * fraction).toInt()
|
|
82
|
+
updateHeight(currentHeight)
|
|
87
83
|
|
|
88
84
|
return insets
|
|
89
85
|
}
|
|
90
86
|
|
|
91
87
|
override fun onEnd(animation: WindowInsetsAnimationCompat) {
|
|
92
|
-
|
|
88
|
+
updateHeight(getKeyboardHeight(ViewCompat.getRootWindowInsets(targetView)))
|
|
93
89
|
}
|
|
94
90
|
}
|
|
95
91
|
)
|
|
96
92
|
}
|
|
97
93
|
|
|
98
|
-
/** API 29 and below fallback using ViewTreeObserver on Activity's root view */
|
|
99
94
|
private fun setupLegacyListener() {
|
|
100
95
|
val rootView = reactContext.currentActivity?.window?.decorView?.rootView ?: return
|
|
101
|
-
|
|
102
96
|
activityRootView = rootView
|
|
103
97
|
|
|
104
98
|
globalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
|
|
@@ -108,11 +102,7 @@ class TrueSheetKeyboardHandler(
|
|
|
108
102
|
val screenHeight = rootView.height
|
|
109
103
|
val keyboardHeight = screenHeight - rect.bottom
|
|
110
104
|
|
|
111
|
-
if (keyboardHeight > screenHeight * 0.15)
|
|
112
|
-
applyTranslation(keyboardHeight)
|
|
113
|
-
} else {
|
|
114
|
-
applyTranslation(0)
|
|
115
|
-
}
|
|
105
|
+
updateHeight(if (keyboardHeight > screenHeight * 0.15) keyboardHeight else 0)
|
|
116
106
|
}
|
|
117
107
|
|
|
118
108
|
rootView.viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener)
|
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
<item name="android:windowExitAnimation">@anim/true_sheet_slide_out</item>
|
|
7
7
|
</style>
|
|
8
8
|
|
|
9
|
+
<!-- Fast fade animation - used for hide/show when modal is presented -->
|
|
10
|
+
<style name="TrueSheetFadeAnimation" parent="Animation.AppCompat.Dialog">
|
|
11
|
+
<item name="android:windowEnterAnimation">@anim/true_sheet_fade_in</item>
|
|
12
|
+
<item name="android:windowExitAnimation">@anim/true_sheet_fade_out</item>
|
|
13
|
+
</style>
|
|
14
|
+
|
|
9
15
|
<!-- Default BottomSheetDialog style with smooth animations -->
|
|
10
16
|
<style name="TrueSheetDialog" parent="Theme.Design.Light.BottomSheetDialog">
|
|
11
17
|
<item name="android:windowAnimationStyle">@style/TrueSheetAnimation</item>
|
package/package.json
CHANGED