@lodev09/react-native-true-sheet 3.5.1-beta.2 → 3.5.1-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/TrueSheetModule.kt +2 -2
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +32 -28
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +378 -253
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +0 -5
- package/android/src/main/java/com/lodev09/truesheet/core/RNScreensFragmentObserver.kt +36 -14
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +150 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +55 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +18 -12
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +91 -2
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetKeyboardObserver.kt +21 -11
- package/android/src/main/java/com/lodev09/truesheet/core/{TrueSheetDialogObserver.kt → TrueSheetStackManager.kt} +7 -5
- package/ios/TrueSheetViewController.h +2 -3
- package/ios/TrueSheetViewController.mm +11 -4
- package/ios/core/TrueSheetDetentCalculator.h +2 -3
- package/ios/core/TrueSheetDetentCalculator.mm +7 -9
- package/lib/module/TrueSheet.js +1 -16
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +0 -1
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +0 -8
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +0 -1
- 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/package.json +1 -1
- package/src/TrueSheet.tsx +1 -16
- package/src/TrueSheet.types.ts +0 -9
- package/src/fabric/TrueSheetViewNativeComponent.ts +0 -1
- package/src/navigation/types.ts +0 -1
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetAnimator.kt +0 -145
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDialogFragment.kt +0 -320
- package/android/src/main/res/anim/fast_fade_out.xml +0 -6
- package/android/src/main/res/values/styles.xml +0 -21
|
@@ -183,11 +183,6 @@ class TrueSheetViewManager :
|
|
|
183
183
|
// iOS-specific prop - no-op on Android
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
@ReactProp(name = "edgeToEdgeFullScreen", defaultBoolean = false)
|
|
187
|
-
override fun setEdgeToEdgeFullScreen(view: TrueSheetView, edgeToEdgeFullScreen: Boolean) {
|
|
188
|
-
view.setEdgeToEdgeFullScreen(edgeToEdgeFullScreen)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
186
|
@ReactProp(name = "insetAdjustment")
|
|
192
187
|
override fun setInsetAdjustment(view: TrueSheetView, insetAdjustment: String?) {
|
|
193
188
|
view.setInsetAdjustment(insetAdjustment ?: "automatic")
|
|
@@ -4,6 +4,8 @@ import android.content.Context
|
|
|
4
4
|
import androidx.appcompat.app.AppCompatActivity
|
|
5
5
|
import androidx.fragment.app.Fragment
|
|
6
6
|
import androidx.fragment.app.FragmentManager
|
|
7
|
+
import androidx.lifecycle.DefaultLifecycleObserver
|
|
8
|
+
import androidx.lifecycle.LifecycleOwner
|
|
7
9
|
import com.facebook.react.bridge.ReactContext
|
|
8
10
|
|
|
9
11
|
private const val RN_SCREENS_PACKAGE = "com.swmansion.rnscreens"
|
|
@@ -19,7 +21,9 @@ class RNScreensFragmentObserver(
|
|
|
19
21
|
private val onModalDidDismiss: () -> Unit
|
|
20
22
|
) {
|
|
21
23
|
private var fragmentLifecycleCallback: FragmentManager.FragmentLifecycleCallbacks? = null
|
|
24
|
+
private var activityLifecycleObserver: DefaultLifecycleObserver? = null
|
|
22
25
|
private val activeModalFragments: MutableSet<Fragment> = mutableSetOf()
|
|
26
|
+
private var isActivityInForeground = true
|
|
23
27
|
|
|
24
28
|
/**
|
|
25
29
|
* Start observing fragment lifecycle events.
|
|
@@ -28,10 +32,25 @@ class RNScreensFragmentObserver(
|
|
|
28
32
|
val activity = reactContext.currentActivity as? AppCompatActivity ?: return
|
|
29
33
|
val fragmentManager = activity.supportFragmentManager
|
|
30
34
|
|
|
35
|
+
// Track activity foreground state to ignore fragment lifecycle events during background/foreground transitions
|
|
36
|
+
activityLifecycleObserver = object : DefaultLifecycleObserver {
|
|
37
|
+
override fun onResume(owner: LifecycleOwner) {
|
|
38
|
+
isActivityInForeground = true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
override fun onPause(owner: LifecycleOwner) {
|
|
42
|
+
isActivityInForeground = false
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
activity.lifecycle.addObserver(activityLifecycleObserver!!)
|
|
46
|
+
|
|
31
47
|
fragmentLifecycleCallback = object : FragmentManager.FragmentLifecycleCallbacks() {
|
|
32
48
|
override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) {
|
|
33
49
|
super.onFragmentAttached(fm, f, context)
|
|
34
50
|
|
|
51
|
+
// Ignore if app is resuming from background
|
|
52
|
+
if (!isActivityInForeground) return
|
|
53
|
+
|
|
35
54
|
if (isModalFragment(f) && !activeModalFragments.contains(f)) {
|
|
36
55
|
activeModalFragments.add(f)
|
|
37
56
|
|
|
@@ -44,14 +63,14 @@ class RNScreensFragmentObserver(
|
|
|
44
63
|
override fun onFragmentStopped(fm: FragmentManager, f: Fragment) {
|
|
45
64
|
super.onFragmentStopped(fm, f)
|
|
46
65
|
|
|
47
|
-
// Ignore if app is
|
|
48
|
-
|
|
49
|
-
if (!activity.lifecycle.currentState.isAtLeast(androidx.lifecycle.Lifecycle.State.RESUMED)) {
|
|
50
|
-
return
|
|
51
|
-
}
|
|
66
|
+
// Ignore if app is going to background (fragments stop with activity)
|
|
67
|
+
if (!isActivityInForeground) return
|
|
52
68
|
|
|
53
|
-
|
|
54
|
-
|
|
69
|
+
// Only trigger when fragment is being removed (not just stopped for navigation)
|
|
70
|
+
if (activeModalFragments.contains(f) && f.isRemoving) {
|
|
71
|
+
activeModalFragments.remove(f)
|
|
72
|
+
|
|
73
|
+
if (activeModalFragments.isEmpty()) {
|
|
55
74
|
onModalWillDismiss()
|
|
56
75
|
}
|
|
57
76
|
}
|
|
@@ -60,12 +79,8 @@ class RNScreensFragmentObserver(
|
|
|
60
79
|
override fun onFragmentDestroyed(fm: FragmentManager, f: Fragment) {
|
|
61
80
|
super.onFragmentDestroyed(fm, f)
|
|
62
81
|
|
|
63
|
-
if (activeModalFragments.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (activeModalFragments.isEmpty()) {
|
|
67
|
-
onModalDidDismiss()
|
|
68
|
-
}
|
|
82
|
+
if (activeModalFragments.isEmpty()) {
|
|
83
|
+
onModalDidDismiss()
|
|
69
84
|
}
|
|
70
85
|
}
|
|
71
86
|
}
|
|
@@ -77,11 +92,18 @@ class RNScreensFragmentObserver(
|
|
|
77
92
|
* Stop observing and cleanup.
|
|
78
93
|
*/
|
|
79
94
|
fun stop() {
|
|
95
|
+
val activity = reactContext.currentActivity as? AppCompatActivity
|
|
96
|
+
|
|
80
97
|
fragmentLifecycleCallback?.let { callback ->
|
|
81
|
-
val activity = reactContext.currentActivity as? AppCompatActivity
|
|
82
98
|
activity?.supportFragmentManager?.unregisterFragmentLifecycleCallbacks(callback)
|
|
83
99
|
}
|
|
84
100
|
fragmentLifecycleCallback = null
|
|
101
|
+
|
|
102
|
+
activityLifecycleObserver?.let { observer ->
|
|
103
|
+
activity?.lifecycle?.removeObserver(observer)
|
|
104
|
+
}
|
|
105
|
+
activityLifecycleObserver = null
|
|
106
|
+
|
|
85
107
|
activeModalFragments.clear()
|
|
86
108
|
}
|
|
87
109
|
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
package com.lodev09.truesheet.core
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.graphics.Color
|
|
5
|
+
import android.graphics.drawable.ShapeDrawable
|
|
6
|
+
import android.graphics.drawable.shapes.RoundRectShape
|
|
7
|
+
import android.util.TypedValue
|
|
8
|
+
import android.view.Gravity
|
|
9
|
+
import android.view.View
|
|
10
|
+
import android.widget.FrameLayout
|
|
11
|
+
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
12
|
+
import com.facebook.react.uimanager.PixelUtil.dpToPx
|
|
13
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
14
|
+
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
15
|
+
|
|
16
|
+
interface TrueSheetBottomSheetViewDelegate {
|
|
17
|
+
val isTopmostSheet: Boolean
|
|
18
|
+
val sheetCornerRadius: Float
|
|
19
|
+
val sheetBackgroundColor: Int?
|
|
20
|
+
val grabber: Boolean
|
|
21
|
+
val grabberOptions: GrabberOptions?
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The bottom sheet view that holds the content.
|
|
26
|
+
* This view has BottomSheetBehavior attached via CoordinatorLayout.LayoutParams.
|
|
27
|
+
*
|
|
28
|
+
* Touch dispatching to React Native is handled by TrueSheetViewController,
|
|
29
|
+
* which is the actual RootView containing the React content.
|
|
30
|
+
*/
|
|
31
|
+
@SuppressLint("ViewConstructor")
|
|
32
|
+
class TrueSheetBottomSheetView(private val reactContext: ThemedReactContext) : FrameLayout(reactContext) {
|
|
33
|
+
|
|
34
|
+
companion object {
|
|
35
|
+
private const val GRABBER_TAG = "TrueSheetGrabber"
|
|
36
|
+
private const val DEFAULT_CORNER_RADIUS = 16f // dp
|
|
37
|
+
private const val DEFAULT_MAX_WIDTH = 640 // dp
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// MARK: - Properties
|
|
42
|
+
// =============================================================================
|
|
43
|
+
|
|
44
|
+
var delegate: TrueSheetBottomSheetViewDelegate? = null
|
|
45
|
+
|
|
46
|
+
// Behavior reference (set after adding to CoordinatorLayout)
|
|
47
|
+
val behavior: BottomSheetBehavior<TrueSheetBottomSheetView>?
|
|
48
|
+
get() = (layoutParams as? CoordinatorLayout.LayoutParams)
|
|
49
|
+
?.behavior as? BottomSheetBehavior<TrueSheetBottomSheetView>
|
|
50
|
+
|
|
51
|
+
// =============================================================================
|
|
52
|
+
// MARK: - Initialization
|
|
53
|
+
// =============================================================================
|
|
54
|
+
|
|
55
|
+
init {
|
|
56
|
+
// Allow content to extend beyond bounds (for footer positioning)
|
|
57
|
+
clipChildren = false
|
|
58
|
+
clipToPadding = false
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
override fun setTranslationY(translationY: Float) {
|
|
62
|
+
// Skip resetting translation to 0 for parent sheets (non-topmost)
|
|
63
|
+
// This prevents keyboard inset animations from resetting parent sheet translation
|
|
64
|
+
if (delegate?.isTopmostSheet == false && translationY == 0f && this.translationY != 0f) {
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
super.setTranslationY(translationY)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// =============================================================================
|
|
71
|
+
// MARK: - Layout
|
|
72
|
+
// =============================================================================
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Creates layout params with BottomSheetBehavior attached.
|
|
76
|
+
*/
|
|
77
|
+
fun createLayoutParams(): CoordinatorLayout.LayoutParams {
|
|
78
|
+
val behavior = BottomSheetBehavior<TrueSheetBottomSheetView>().apply {
|
|
79
|
+
isHideable = true
|
|
80
|
+
maxWidth = DEFAULT_MAX_WIDTH.dpToPx().toInt()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return CoordinatorLayout.LayoutParams(
|
|
84
|
+
CoordinatorLayout.LayoutParams.MATCH_PARENT,
|
|
85
|
+
CoordinatorLayout.LayoutParams.MATCH_PARENT
|
|
86
|
+
).apply {
|
|
87
|
+
this.behavior = behavior
|
|
88
|
+
this.gravity = Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// =============================================================================
|
|
93
|
+
// MARK: - Background & Styling
|
|
94
|
+
// =============================================================================
|
|
95
|
+
|
|
96
|
+
fun setupBackground() {
|
|
97
|
+
val radius = delegate?.sheetCornerRadius ?: DEFAULT_CORNER_RADIUS.dpToPx()
|
|
98
|
+
val effectiveRadius = if (radius < 0) DEFAULT_CORNER_RADIUS.dpToPx() else radius
|
|
99
|
+
|
|
100
|
+
val outerRadii = floatArrayOf(
|
|
101
|
+
effectiveRadius,
|
|
102
|
+
effectiveRadius, // top-left
|
|
103
|
+
effectiveRadius,
|
|
104
|
+
effectiveRadius, // top-right
|
|
105
|
+
0f,
|
|
106
|
+
0f, // bottom-right
|
|
107
|
+
0f,
|
|
108
|
+
0f // bottom-left
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
val color = delegate?.sheetBackgroundColor ?: getDefaultBackgroundColor()
|
|
112
|
+
|
|
113
|
+
background = ShapeDrawable(RoundRectShape(outerRadii, null, null)).apply {
|
|
114
|
+
paint.color = color
|
|
115
|
+
}
|
|
116
|
+
clipToOutline = true
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private fun getDefaultBackgroundColor(): Int {
|
|
120
|
+
val typedValue = TypedValue()
|
|
121
|
+
return if (reactContext.theme.resolveAttribute(
|
|
122
|
+
com.google.android.material.R.attr.colorSurfaceContainerLow,
|
|
123
|
+
typedValue,
|
|
124
|
+
true
|
|
125
|
+
)
|
|
126
|
+
) {
|
|
127
|
+
typedValue.data
|
|
128
|
+
} else {
|
|
129
|
+
Color.WHITE
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// =============================================================================
|
|
134
|
+
// MARK: - Grabber
|
|
135
|
+
// =============================================================================
|
|
136
|
+
|
|
137
|
+
fun setupGrabber() {
|
|
138
|
+
findViewWithTag<View>(GRABBER_TAG)?.let { removeView(it) }
|
|
139
|
+
|
|
140
|
+
val isEnabled = delegate?.grabber ?: true
|
|
141
|
+
val isDraggable = behavior?.isDraggable ?: true
|
|
142
|
+
if (!isEnabled || !isDraggable) return
|
|
143
|
+
|
|
144
|
+
val grabberView = TrueSheetGrabberView(reactContext, delegate?.grabberOptions).apply {
|
|
145
|
+
tag = GRABBER_TAG
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
addView(grabberView, 0)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
package com.lodev09.truesheet.core
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.view.View
|
|
6
|
+
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
7
|
+
import com.facebook.react.uimanager.PointerEvents
|
|
8
|
+
import com.facebook.react.uimanager.ReactPointerEventsView
|
|
9
|
+
|
|
10
|
+
interface TrueSheetCoordinatorLayoutDelegate {
|
|
11
|
+
fun coordinatorLayoutDidLayout(changed: Boolean)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Custom CoordinatorLayout that hosts the bottom sheet and dim view.
|
|
16
|
+
* Implements ReactPointerEventsView to allow touch events to pass through
|
|
17
|
+
* to underlying React Native views when appropriate.
|
|
18
|
+
*/
|
|
19
|
+
@SuppressLint("ViewConstructor")
|
|
20
|
+
class TrueSheetCoordinatorLayout(context: Context) :
|
|
21
|
+
CoordinatorLayout(context),
|
|
22
|
+
ReactPointerEventsView {
|
|
23
|
+
|
|
24
|
+
var delegate: TrueSheetCoordinatorLayoutDelegate? = null
|
|
25
|
+
|
|
26
|
+
init {
|
|
27
|
+
// Fill the entire screen
|
|
28
|
+
layoutParams = LayoutParams(
|
|
29
|
+
LayoutParams.MATCH_PARENT,
|
|
30
|
+
LayoutParams.MATCH_PARENT
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
// Ensure we don't clip the sheet during animations
|
|
34
|
+
clipChildren = false
|
|
35
|
+
clipToPadding = false
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
override fun onLayout(
|
|
39
|
+
changed: Boolean,
|
|
40
|
+
l: Int,
|
|
41
|
+
t: Int,
|
|
42
|
+
r: Int,
|
|
43
|
+
b: Int
|
|
44
|
+
) {
|
|
45
|
+
super.onLayout(changed, l, t, r, b)
|
|
46
|
+
delegate?.coordinatorLayoutDidLayout(changed)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Allow pointer events to pass through to underlying views.
|
|
51
|
+
* The DimView and BottomSheetView handle their own touch interception.
|
|
52
|
+
*/
|
|
53
|
+
override val pointerEvents: PointerEvents
|
|
54
|
+
get() = PointerEvents.BOX_NONE
|
|
55
|
+
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
package com.lodev09.truesheet.core
|
|
2
2
|
|
|
3
3
|
import com.facebook.react.uimanager.PixelUtil.pxToDp
|
|
4
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
5
|
+
import com.facebook.react.util.RNLog
|
|
4
6
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Provides screen dimensions and content measurements for detent calculations.
|
|
8
10
|
*/
|
|
9
|
-
interface
|
|
11
|
+
interface TrueSheetDetentCalculatorDelegate {
|
|
10
12
|
val screenHeight: Int
|
|
11
13
|
val realScreenHeight: Int
|
|
12
14
|
val detents: MutableList<Double>
|
|
@@ -19,18 +21,19 @@ interface TrueSheetDetentMeasurements {
|
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Handles all detent-related calculations for the bottom sheet.
|
|
22
|
-
* Takes a measurements provider to always read current values.
|
|
23
24
|
*/
|
|
24
|
-
class TrueSheetDetentCalculator(private val
|
|
25
|
+
class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
private val
|
|
29
|
-
private val
|
|
30
|
-
private val
|
|
31
|
-
private val
|
|
32
|
-
private val
|
|
33
|
-
private val
|
|
27
|
+
var delegate: TrueSheetDetentCalculatorDelegate? = null
|
|
28
|
+
|
|
29
|
+
private val screenHeight: Int get() = delegate?.screenHeight ?: 0
|
|
30
|
+
private val realScreenHeight: Int get() = delegate?.realScreenHeight ?: 0
|
|
31
|
+
private val detents: List<Double> get() = delegate?.detents ?: emptyList()
|
|
32
|
+
private val contentHeight: Int get() = delegate?.contentHeight ?: 0
|
|
33
|
+
private val headerHeight: Int get() = delegate?.headerHeight ?: 0
|
|
34
|
+
private val contentBottomInset: Int get() = delegate?.contentBottomInset ?: 0
|
|
35
|
+
private val maxSheetHeight: Int? get() = delegate?.maxSheetHeight
|
|
36
|
+
private val keyboardInset: Int get() = delegate?.keyboardInset ?: 0
|
|
34
37
|
|
|
35
38
|
/**
|
|
36
39
|
* Calculate the height in pixels for a given detent value.
|
|
@@ -55,7 +58,10 @@ class TrueSheetDetentCalculator(private val measurements: TrueSheetDetentMeasure
|
|
|
55
58
|
* Get the expected sheet top position for a detent index.
|
|
56
59
|
*/
|
|
57
60
|
fun getSheetTopForDetentIndex(index: Int): Int {
|
|
58
|
-
if (index < 0 || index >= detents.size)
|
|
61
|
+
if (index < 0 || index >= detents.size) {
|
|
62
|
+
RNLog.w(reactContext, "TrueSheet: Detent index ($index) is out of bounds (0..${detents.size - 1})")
|
|
63
|
+
return realScreenHeight
|
|
64
|
+
}
|
|
59
65
|
return realScreenHeight - getDetentHeight(detents[index])
|
|
60
66
|
}
|
|
61
67
|
|
|
@@ -3,21 +3,51 @@ 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.view.MotionEvent
|
|
6
7
|
import android.view.View
|
|
7
8
|
import android.view.ViewGroup
|
|
8
9
|
import android.view.ViewOutlineProvider
|
|
10
|
+
import com.facebook.react.uimanager.PointerEvents
|
|
11
|
+
import com.facebook.react.uimanager.ReactPointerEventsView
|
|
9
12
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
10
13
|
import com.lodev09.truesheet.utils.ScreenUtils
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Delegate for handling dim view interactions.
|
|
17
|
+
*/
|
|
18
|
+
interface TrueSheetDimViewDelegate {
|
|
19
|
+
fun dimViewDidTap()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Dim view that sits behind the bottom sheet in the CoordinatorLayout.
|
|
24
|
+
*
|
|
25
|
+
* Key behaviors:
|
|
26
|
+
* - When alpha > 0 (dimmed): blocks touches and calls delegate on tap
|
|
27
|
+
* - When alpha == 0 (not dimmed): passes touches through to views below
|
|
28
|
+
*
|
|
29
|
+
* This implements the "dimmedDetentIndex" equivalent functionality:
|
|
30
|
+
* the view only becomes interactive when the sheet is at or above the dimmed detent.
|
|
31
|
+
*/
|
|
32
|
+
@SuppressLint("ViewConstructor", "ClickableViewAccessibility")
|
|
33
|
+
class TrueSheetDimView(private val reactContext: ThemedReactContext) :
|
|
34
|
+
View(reactContext),
|
|
35
|
+
ReactPointerEventsView {
|
|
14
36
|
|
|
15
37
|
companion object {
|
|
16
38
|
private const val MAX_ALPHA = 0.5f
|
|
17
39
|
}
|
|
18
40
|
|
|
41
|
+
var delegate: TrueSheetDimViewDelegate? = null
|
|
42
|
+
|
|
19
43
|
private var targetView: ViewGroup? = null
|
|
20
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Whether this view should block gestures (when dimmed).
|
|
47
|
+
*/
|
|
48
|
+
private val blockGestures: Boolean
|
|
49
|
+
get() = alpha > 0f
|
|
50
|
+
|
|
21
51
|
init {
|
|
22
52
|
layoutParams = ViewGroup.LayoutParams(
|
|
23
53
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
@@ -25,8 +55,22 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) : View(reac
|
|
|
25
55
|
)
|
|
26
56
|
setBackgroundColor(Color.BLACK)
|
|
27
57
|
alpha = 0f
|
|
58
|
+
|
|
59
|
+
// Handle taps on the dim view
|
|
60
|
+
setOnClickListener {
|
|
61
|
+
delegate?.dimViewDidTap()
|
|
62
|
+
}
|
|
28
63
|
}
|
|
29
64
|
|
|
65
|
+
// =============================================================================
|
|
66
|
+
// MARK: - Attachment
|
|
67
|
+
// =============================================================================
|
|
68
|
+
|
|
69
|
+
/**
|
|
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
|
+
* For stacked sheets, pass the parent sheet's bottom sheet view with corner radius.
|
|
73
|
+
*/
|
|
30
74
|
fun attach(view: ViewGroup? = null, cornerRadius: Float = 0f) {
|
|
31
75
|
if (parent != null) return
|
|
32
76
|
targetView = view ?: reactContext.currentActivity?.window?.decorView as? ViewGroup
|
|
@@ -38,16 +82,34 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) : View(reac
|
|
|
38
82
|
}
|
|
39
83
|
}
|
|
40
84
|
clipToOutline = true
|
|
85
|
+
} else {
|
|
86
|
+
outlineProvider = null
|
|
87
|
+
clipToOutline = false
|
|
41
88
|
}
|
|
42
89
|
|
|
43
90
|
targetView?.addView(this)
|
|
44
91
|
}
|
|
45
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Attaches this dim view to a CoordinatorLayout at index 0 (behind the sheet).
|
|
95
|
+
*/
|
|
96
|
+
fun attachToCoordinator(coordinator: TrueSheetCoordinatorLayout) {
|
|
97
|
+
if (parent != null) return
|
|
98
|
+
targetView = coordinator
|
|
99
|
+
outlineProvider = null
|
|
100
|
+
clipToOutline = false
|
|
101
|
+
coordinator.addView(this, 0)
|
|
102
|
+
}
|
|
103
|
+
|
|
46
104
|
fun detach() {
|
|
47
105
|
targetView?.removeView(this)
|
|
48
106
|
targetView = null
|
|
49
107
|
}
|
|
50
108
|
|
|
109
|
+
// =============================================================================
|
|
110
|
+
// MARK: - Alpha Calculation
|
|
111
|
+
// =============================================================================
|
|
112
|
+
|
|
51
113
|
fun calculateAlpha(sheetTop: Int, dimmedDetentIndex: Int, getSheetTopForDetentIndex: (Int) -> Int): Float {
|
|
52
114
|
val realHeight = ScreenUtils.getRealScreenHeight(reactContext)
|
|
53
115
|
val dimmedDetentTop = getSheetTopForDetentIndex(dimmedDetentIndex)
|
|
@@ -68,4 +130,31 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) : View(reac
|
|
|
68
130
|
fun interpolateAlpha(sheetTop: Int, dimmedDetentIndex: Int, getSheetTopForDetentIndex: (Int) -> Int) {
|
|
69
131
|
alpha = calculateAlpha(sheetTop, dimmedDetentIndex, getSheetTopForDetentIndex)
|
|
70
132
|
}
|
|
133
|
+
|
|
134
|
+
// =============================================================================
|
|
135
|
+
// MARK: - Touch Handling
|
|
136
|
+
// =============================================================================
|
|
137
|
+
|
|
138
|
+
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
139
|
+
if (blockGestures) {
|
|
140
|
+
// When dimmed, consume touch and trigger click on ACTION_UP
|
|
141
|
+
if (event.action == MotionEvent.ACTION_UP) {
|
|
142
|
+
performClick()
|
|
143
|
+
}
|
|
144
|
+
return true
|
|
145
|
+
}
|
|
146
|
+
// When not dimmed, let touches pass through
|
|
147
|
+
return false
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// =============================================================================
|
|
151
|
+
// MARK: - ReactPointerEventsView
|
|
152
|
+
// =============================================================================
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* When dimmed (alpha > 0), intercept touches (AUTO).
|
|
156
|
+
* When not dimmed (alpha == 0), pass through (NONE).
|
|
157
|
+
*/
|
|
158
|
+
override val pointerEvents: PointerEvents
|
|
159
|
+
get() = if (blockGestures) PointerEvents.AUTO else PointerEvents.NONE
|
|
71
160
|
}
|
|
@@ -111,6 +111,9 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
private fun setupLegacyListener() {
|
|
114
|
+
// Ensure we don't add duplicate listeners
|
|
115
|
+
if (globalLayoutListener != null) return
|
|
116
|
+
|
|
114
117
|
val rootView = reactContext.currentActivity?.window?.decorView?.rootView ?: return
|
|
115
118
|
activityRootView = rootView
|
|
116
119
|
|
|
@@ -122,19 +125,26 @@ class TrueSheetKeyboardObserver(private val targetView: View, private val reactC
|
|
|
122
125
|
val keyboardHeight = screenHeight - rect.bottom
|
|
123
126
|
|
|
124
127
|
val newHeight = if (keyboardHeight > screenHeight * 0.15) keyboardHeight else 0
|
|
128
|
+
|
|
129
|
+
// Skip if already at this height
|
|
130
|
+
if (targetHeight == newHeight) return@OnGlobalLayoutListener
|
|
131
|
+
|
|
125
132
|
val previousHeight = currentHeight
|
|
133
|
+
targetHeight = newHeight
|
|
134
|
+
isHiding = newHeight < previousHeight
|
|
126
135
|
|
|
127
|
-
if (
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
if (newHeight > previousHeight) {
|
|
137
|
+
delegate?.keyboardWillShow(newHeight)
|
|
138
|
+
} else if (isHiding) {
|
|
139
|
+
delegate?.keyboardWillHide()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// On legacy API, keyboard has already animated - just update immediately
|
|
143
|
+
updateHeight(previousHeight, newHeight, 1f)
|
|
144
|
+
|
|
145
|
+
if (isHiding && newHeight == 0) {
|
|
146
|
+
delegate?.keyboardDidHide()
|
|
147
|
+
isHiding = false
|
|
138
148
|
}
|
|
139
149
|
}
|
|
140
150
|
|
|
@@ -6,7 +6,7 @@ import com.lodev09.truesheet.TrueSheetView
|
|
|
6
6
|
* Manages TrueSheet stacking behavior.
|
|
7
7
|
* Tracks presented sheets and handles visibility when sheets stack on top of each other.
|
|
8
8
|
*/
|
|
9
|
-
object
|
|
9
|
+
object TrueSheetStackManager {
|
|
10
10
|
|
|
11
11
|
private val presentedSheetStack = mutableListOf<TrueSheetView>()
|
|
12
12
|
|
|
@@ -18,9 +18,9 @@ object TrueSheetDialogObserver {
|
|
|
18
18
|
fun onSheetWillPresent(sheetView: TrueSheetView, detentIndex: Int): TrueSheetView? {
|
|
19
19
|
synchronized(presentedSheetStack) {
|
|
20
20
|
val parentSheet = presentedSheetStack.lastOrNull()
|
|
21
|
-
?.takeIf { it.viewController.isPresented && it.viewController.
|
|
21
|
+
?.takeIf { it.viewController.isPresented && it.viewController.isSheetVisible }
|
|
22
22
|
|
|
23
|
-
val childSheetTop = sheetView.viewController.
|
|
23
|
+
val childSheetTop = sheetView.viewController.detentCalculator.getSheetTopForDetentIndex(detentIndex)
|
|
24
24
|
parentSheet?.updateTranslationForChild(childSheetTop)
|
|
25
25
|
|
|
26
26
|
if (!presentedSheetStack.contains(sheetView)) {
|
|
@@ -59,8 +59,10 @@ object TrueSheetDialogObserver {
|
|
|
59
59
|
|
|
60
60
|
// Post to ensure layout is complete before reading position
|
|
61
61
|
sheetView.viewController.post {
|
|
62
|
-
val childMinSheetTop = sheetView.viewController.
|
|
63
|
-
val childCurrentSheetTop = sheetView.viewController.
|
|
62
|
+
val childMinSheetTop = sheetView.viewController.detentCalculator.getSheetTopForDetentIndex(0)
|
|
63
|
+
val childCurrentSheetTop = sheetView.viewController.detentCalculator.getSheetTopForDetentIndex(
|
|
64
|
+
sheetView.viewController.currentDetentIndex
|
|
65
|
+
)
|
|
64
66
|
// Cap to minimum detent position
|
|
65
67
|
val childSheetTop = maxOf(childMinSheetTop, childCurrentSheetTop)
|
|
66
68
|
parentSheet.updateTranslationForChild(childSheetTop)
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
//
|
|
8
8
|
|
|
9
9
|
#import <UIKit/UIKit.h>
|
|
10
|
+
#import "core/TrueSheetDetentCalculator.h"
|
|
10
11
|
|
|
11
12
|
#if __has_include(<RNScreens/RNSDismissibleModalProtocol.h>)
|
|
12
13
|
#import <RNScreens/RNSDismissibleModalProtocol.h>
|
|
@@ -40,10 +41,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
40
41
|
|
|
41
42
|
@end
|
|
42
43
|
|
|
43
|
-
@protocol TrueSheetDetentMeasurements;
|
|
44
|
-
|
|
45
44
|
@interface TrueSheetViewController : UIViewController <UISheetPresentationControllerDelegate,
|
|
46
|
-
|
|
45
|
+
TrueSheetDetentCalculatorDelegate
|
|
47
46
|
#if RNS_DISMISSIBLE_MODAL_PROTOCOL_AVAILABLE
|
|
48
47
|
,
|
|
49
48
|
RNSDismissibleModalProtocol
|
|
@@ -65,7 +65,8 @@
|
|
|
65
65
|
|
|
66
66
|
_blurInteraction = YES;
|
|
67
67
|
_insetAdjustment = @"automatic";
|
|
68
|
-
_detentCalculator = [[TrueSheetDetentCalculator alloc]
|
|
68
|
+
_detentCalculator = [[TrueSheetDetentCalculator alloc] init];
|
|
69
|
+
_detentCalculator.delegate = self;
|
|
69
70
|
}
|
|
70
71
|
return self;
|
|
71
72
|
}
|
|
@@ -503,8 +504,10 @@
|
|
|
503
504
|
|
|
504
505
|
- (void)setupSheetDetents {
|
|
505
506
|
UISheetPresentationController *sheet = self.sheetPresentationController;
|
|
506
|
-
if (!sheet)
|
|
507
|
+
if (!sheet) {
|
|
508
|
+
RCTLogError(@"TrueSheet: sheetPresentationController is nil in setupSheetDetents");
|
|
507
509
|
return;
|
|
510
|
+
}
|
|
508
511
|
|
|
509
512
|
NSMutableArray<UISheetPresentationControllerDetent *> *detents = [NSMutableArray array];
|
|
510
513
|
[_detentCalculator clearResolvedPositions];
|
|
@@ -591,8 +594,10 @@
|
|
|
591
594
|
|
|
592
595
|
- (UISheetPresentationControllerDetentIdentifier)detentIdentifierForIndex:(NSInteger)index {
|
|
593
596
|
UISheetPresentationController *sheet = self.sheetPresentationController;
|
|
594
|
-
if (!sheet)
|
|
597
|
+
if (!sheet) {
|
|
598
|
+
RCTLogError(@"TrueSheet: sheetPresentationController is nil in detentIdentifierForIndex");
|
|
595
599
|
return UISheetPresentationControllerDetentIdentifierMedium;
|
|
600
|
+
}
|
|
596
601
|
|
|
597
602
|
UISheetPresentationControllerDetentIdentifier identifier = UISheetPresentationControllerDetentIdentifierMedium;
|
|
598
603
|
if (index >= 0 && index < (NSInteger)sheet.detents.count) {
|
|
@@ -611,8 +616,10 @@
|
|
|
611
616
|
|
|
612
617
|
- (void)applyActiveDetent {
|
|
613
618
|
UISheetPresentationController *sheet = self.sheetPresentationController;
|
|
614
|
-
if (!sheet)
|
|
619
|
+
if (!sheet) {
|
|
620
|
+
RCTLogError(@"TrueSheet: sheetPresentationController is nil in applyActiveDetent");
|
|
615
621
|
return;
|
|
622
|
+
}
|
|
616
623
|
|
|
617
624
|
NSInteger detentCount = _detents.count;
|
|
618
625
|
if (detentCount == 0)
|