@lodev09/react-native-true-sheet 3.11.10 → 4.0.0-beta.1
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 +6 -5
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +23 -2
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +116 -26
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +7 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +48 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterViewManager.kt +7 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetStateUpdater.kt +45 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +28 -12
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +99 -34
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +9 -5
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +12 -3
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +33 -4
- package/android/src/main/jni/TrueSheetSpec.h +2 -0
- package/android/src/main/jni/TrueSheetStateUpdater.cpp +96 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +24 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +43 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +28 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +11 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +37 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewComponentDescriptor.h +24 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +56 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.h +28 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.cpp +11 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.h +41 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.cpp +18 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.h +16 -0
- package/ios/TrueSheetContainerView.h +12 -10
- package/ios/TrueSheetContainerView.mm +11 -29
- package/ios/TrueSheetContentView.h +22 -7
- package/ios/TrueSheetContentView.mm +143 -41
- package/ios/TrueSheetFooterView.h +12 -2
- package/ios/TrueSheetFooterView.mm +74 -71
- package/ios/TrueSheetHeaderView.mm +0 -1
- package/ios/TrueSheetView.mm +60 -27
- package/ios/TrueSheetViewController.h +9 -0
- package/ios/TrueSheetViewController.mm +288 -122
- package/ios/core/TrueSheetDetentCalculator.h +9 -0
- package/ios/core/TrueSheetDetentCalculator.mm +13 -4
- package/ios/tvos/TrueSheetStubs.mm +2 -0
- package/lib/module/TrueSheet.js +32 -13
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +262 -38
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +5 -1
- package/lib/module/fabric/TrueSheetFooterViewNativeComponent.ts +10 -2
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +8 -1
- package/lib/module/web/vaul/index.js +17 -6
- package/lib/module/web/vaul/index.js.map +1 -1
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +35 -11
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetContentViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts +2 -0
- package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +5 -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/lib/typescript/src/web/vaul/index.d.ts +8 -1
- package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
- package/package.json +10 -10
- package/react-native.config.js +5 -1
- package/src/TrueSheet.tsx +45 -14
- package/src/TrueSheet.types.ts +38 -12
- package/src/TrueSheet.web.tsx +314 -44
- package/src/fabric/TrueSheetContentViewNativeComponent.ts +5 -1
- package/src/fabric/TrueSheetFooterViewNativeComponent.ts +10 -2
- package/src/fabric/TrueSheetViewNativeComponent.ts +8 -1
- package/src/navigation/types.ts +3 -1
- package/src/web/vaul/index.tsx +23 -5
- package/ios/utils/LayoutUtil.h +0 -77
- package/ios/utils/LayoutUtil.mm +0 -106
package/README.md
CHANGED
|
@@ -30,16 +30,17 @@ The true native bottom sheet experience for your React Native Apps. 💩
|
|
|
30
30
|
|
|
31
31
|
### Prerequisites
|
|
32
32
|
|
|
33
|
-
- React Native 0.
|
|
33
|
+
- React Native 0.82+
|
|
34
34
|
- New Architecture enabled
|
|
35
35
|
- Xcode 26.1+
|
|
36
36
|
|
|
37
37
|
### Compatibility
|
|
38
38
|
|
|
39
|
-
| TrueSheet
|
|
40
|
-
|
|
41
|
-
|
|
|
42
|
-
| 3.
|
|
39
|
+
| TrueSheet | React Native | Expo SDK |
|
|
40
|
+
|------------|--------------|----------|
|
|
41
|
+
| 4.0+ | 0.82+ | 55+ |
|
|
42
|
+
| 3.7 - 3.11 | 0.81+ | 54+ |
|
|
43
|
+
| 3.6 | 0.80 | 52-53 |
|
|
43
44
|
|
|
44
45
|
### Expo
|
|
45
46
|
|
|
@@ -63,13 +63,29 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
|
|
|
63
63
|
|
|
64
64
|
var insetAdjustment: TrueSheetInsetAdjustment = TrueSheetInsetAdjustment.AUTOMATIC
|
|
65
65
|
var scrollViewBottomInset: Int = 0
|
|
66
|
-
var
|
|
66
|
+
var absoluteFooter: Boolean = false
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Bottom safe-area inset the footer absorbs as padding — the footer
|
|
70
|
+
* owns the sheet's bottom edge, so its background fills the inset.
|
|
71
|
+
*/
|
|
72
|
+
var footerBottomInset: Int = 0
|
|
73
|
+
set(value) {
|
|
74
|
+
field = value
|
|
75
|
+
footerView?.setBottomInset(value)
|
|
76
|
+
}
|
|
67
77
|
var scrollableOptions: ScrollableOptions? = null
|
|
68
78
|
set(value) {
|
|
69
79
|
field = value
|
|
70
80
|
contentView?.scrollableOptions = value
|
|
71
81
|
}
|
|
72
82
|
|
|
83
|
+
var hasAutoDetent = false
|
|
84
|
+
set(value) {
|
|
85
|
+
field = value
|
|
86
|
+
contentView?.hasAutoDetent = value
|
|
87
|
+
}
|
|
88
|
+
|
|
73
89
|
override val eventDispatcher: EventDispatcher?
|
|
74
90
|
get() = delegate?.eventDispatcher
|
|
75
91
|
|
|
@@ -81,7 +97,7 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
|
|
|
81
97
|
|
|
82
98
|
fun setupScrollable() {
|
|
83
99
|
val bottomInset = if (insetAdjustment == TrueSheetInsetAdjustment.AUTOMATIC) scrollViewBottomInset else 0
|
|
84
|
-
contentView?.setupScrollable(
|
|
100
|
+
contentView?.setupScrollable(bottomInset)
|
|
85
101
|
}
|
|
86
102
|
|
|
87
103
|
fun setupKeyboardHandler() {
|
|
@@ -99,6 +115,7 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
|
|
|
99
115
|
is TrueSheetContentView -> {
|
|
100
116
|
child.delegate = this
|
|
101
117
|
child.scrollableOptions = scrollableOptions
|
|
118
|
+
child.hasAutoDetent = hasAutoDetent
|
|
102
119
|
contentView = child
|
|
103
120
|
|
|
104
121
|
// Children mount bottom-up, so the content subtree is complete here.
|
|
@@ -113,6 +130,7 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
|
|
|
113
130
|
|
|
114
131
|
is TrueSheetFooterView -> {
|
|
115
132
|
child.delegate = this
|
|
133
|
+
child.setBottomInset(footerBottomInset)
|
|
116
134
|
footerView = child
|
|
117
135
|
}
|
|
118
136
|
}
|
|
@@ -184,6 +202,9 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
|
|
|
184
202
|
delegate?.containerViewContentDidChangeSize(width, height)
|
|
185
203
|
}
|
|
186
204
|
|
|
205
|
+
override val footerKeyboardOcclusion: Int
|
|
206
|
+
get() = if (absoluteFooter) footerView?.keyboardOcclusionHeight ?: 0 else -(footerView?.height ?: 0)
|
|
207
|
+
|
|
187
208
|
override fun contentViewDidScroll() {
|
|
188
209
|
delegate?.containerViewContentDidScroll()
|
|
189
210
|
}
|
|
@@ -9,7 +9,9 @@ import android.widget.EditText
|
|
|
9
9
|
import android.widget.ScrollView
|
|
10
10
|
import androidx.core.widget.NestedScrollView
|
|
11
11
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
12
|
+
import com.facebook.react.bridge.WritableNativeMap
|
|
12
13
|
import com.facebook.react.uimanager.PixelUtil.dpToPx
|
|
14
|
+
import com.facebook.react.uimanager.StateWrapper
|
|
13
15
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
14
16
|
import com.facebook.react.views.view.ReactViewGroup
|
|
15
17
|
import com.lodev09.truesheet.core.TrueSheetKeyboardObserver
|
|
@@ -27,6 +29,13 @@ interface TrueSheetContentViewDelegate {
|
|
|
27
29
|
fun contentViewDidChangeSize(width: Int, height: Int)
|
|
28
30
|
fun contentViewDidScroll()
|
|
29
31
|
fun contentViewScrollViewDidChange()
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Keyboard occlusion adjustment below the content — positive for an absolute
|
|
35
|
+
* footer risen above the keyboard covering the content's bottom edge,
|
|
36
|
+
* negative for a relative footer sitting behind the keyboard shielding it.
|
|
37
|
+
*/
|
|
38
|
+
val footerKeyboardOcclusion: Int
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
/**
|
|
@@ -36,14 +45,42 @@ interface TrueSheetContentViewDelegate {
|
|
|
36
45
|
@SuppressLint("ViewConstructor")
|
|
37
46
|
class TrueSheetContentView(private val reactContext: ThemedReactContext) : ReactViewGroup(reactContext) {
|
|
38
47
|
var delegate: TrueSheetContentViewDelegate? = null
|
|
48
|
+
var stateWrapper: StateWrapper? = null
|
|
39
49
|
|
|
40
50
|
private var lastWidth = 0
|
|
41
51
|
private var lastHeight = 0
|
|
42
52
|
|
|
43
53
|
private var pinnedScrollView: ViewGroup? = null
|
|
54
|
+
private var observedScrollChild: View? = null
|
|
44
55
|
private var originalScrollViewPaddingBottom: Int = 0
|
|
45
56
|
private var bottomInset: Int = 0
|
|
46
|
-
private var
|
|
57
|
+
private var scrollableBounded = false
|
|
58
|
+
private var isReportPending = false
|
|
59
|
+
|
|
60
|
+
// Content growth is invisible to layout once the viewport is bounded, so track
|
|
61
|
+
// the scroll content size directly to keep the auto detent height in sync.
|
|
62
|
+
private val scrollChildLayoutListener =
|
|
63
|
+
View.OnLayoutChangeListener { _, _, top, _, bottom, _, oldTop, _, oldBottom ->
|
|
64
|
+
if (bottom - top != oldBottom - oldTop) {
|
|
65
|
+
reportSizeIfChanged()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Content height with the pinned ScrollView's viewport replaced by its content
|
|
71
|
+
* size — the height the content wants regardless of container bounds.
|
|
72
|
+
* Falls back to the view height when no ScrollView is pinned.
|
|
73
|
+
*/
|
|
74
|
+
val naturalHeight: Int
|
|
75
|
+
get() {
|
|
76
|
+
var naturalHeight = height
|
|
77
|
+
pinnedScrollView?.let { scrollView ->
|
|
78
|
+
scrollView.getChildAt(0)?.let { child ->
|
|
79
|
+
naturalHeight += child.height - scrollView.height
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return maxOf(0, naturalHeight)
|
|
83
|
+
}
|
|
47
84
|
|
|
48
85
|
private var keyboardScrollOffset: Float = 0f
|
|
49
86
|
private var keyboardObserver: TrueSheetKeyboardObserver? = null
|
|
@@ -57,6 +94,20 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
|
|
|
57
94
|
keyboardScrollOffset = value?.keyboardScrollOffset?.dpToPx() ?: 0f
|
|
58
95
|
}
|
|
59
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Whether the sheet has an `auto` detent. Deriving the sheet height from the
|
|
99
|
+
* scroll content is circular with natural layout, so the pinned ScrollView's
|
|
100
|
+
* viewport is force-bounded to the container only in this case.
|
|
101
|
+
*/
|
|
102
|
+
var hasAutoDetent = false
|
|
103
|
+
set(value) {
|
|
104
|
+
if (field == value) return
|
|
105
|
+
field = value
|
|
106
|
+
if (pinnedScrollView != null) {
|
|
107
|
+
setScrollableBounded(value)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
60
111
|
override fun addView(child: View?, index: Int) {
|
|
61
112
|
super.addView(child, index)
|
|
62
113
|
checkScrollViewChanged()
|
|
@@ -75,20 +126,54 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
|
|
|
75
126
|
|
|
76
127
|
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
77
128
|
super.onSizeChanged(w, h, oldw, oldh)
|
|
129
|
+
reportSizeIfChanged()
|
|
130
|
+
}
|
|
78
131
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
132
|
+
private fun reportSizeIfChanged() {
|
|
133
|
+
// naturalHeight mixes sizes from different views; mid-layout they're
|
|
134
|
+
// momentarily inconsistent (parent resizes before its children), which
|
|
135
|
+
// feeds back into the auto detent and oscillates. Coalesce to the next
|
|
136
|
+
// frame so sizes are settled before measuring.
|
|
137
|
+
if (pinnedScrollView != null) {
|
|
138
|
+
if (isReportPending) return
|
|
139
|
+
isReportPending = true
|
|
140
|
+
|
|
141
|
+
post {
|
|
142
|
+
isReportPending = false
|
|
143
|
+
reportSizeNow()
|
|
144
|
+
}
|
|
145
|
+
return
|
|
83
146
|
}
|
|
147
|
+
|
|
148
|
+
reportSizeNow()
|
|
84
149
|
}
|
|
85
150
|
|
|
86
|
-
fun
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
151
|
+
private fun reportSizeNow() {
|
|
152
|
+
val newHeight = naturalHeight
|
|
153
|
+
if (width != lastWidth || newHeight != lastHeight) {
|
|
154
|
+
lastWidth = width
|
|
155
|
+
lastHeight = newHeight
|
|
156
|
+
delegate?.contentViewDidChangeSize(width, newHeight)
|
|
90
157
|
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Tells the shadow node to fill the container (flexGrow/flexShrink) so the
|
|
162
|
+
* pinned ScrollView's viewport is bounded to the visible space. Only applied
|
|
163
|
+
* for auto detents — otherwise content lays out naturally like a regular view.
|
|
164
|
+
*/
|
|
165
|
+
private fun setScrollableBounded(bounded: Boolean) {
|
|
166
|
+
if (scrollableBounded == bounded) return
|
|
167
|
+
scrollableBounded = bounded
|
|
168
|
+
|
|
169
|
+
stateWrapper?.let {
|
|
170
|
+
val newState = WritableNativeMap()
|
|
171
|
+
newState.putBoolean("scrollableBounded", bounded)
|
|
172
|
+
it.updateState(newState)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
91
175
|
|
|
176
|
+
fun setupScrollable(bottomInset: Int) {
|
|
92
177
|
// Check if pinned scroll view is still valid (still in view hierarchy)
|
|
93
178
|
if (pinnedScrollView != null && pinnedScrollView?.isDescendantOf(this) == false) {
|
|
94
179
|
clearScrollable()
|
|
@@ -114,6 +199,13 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
|
|
|
114
199
|
delegate?.contentViewDidScroll()
|
|
115
200
|
}
|
|
116
201
|
}
|
|
202
|
+
|
|
203
|
+
scrollView.getChildAt(0)?.let { child ->
|
|
204
|
+
observedScrollChild = child
|
|
205
|
+
child.addOnLayoutChangeListener(scrollChildLayoutListener)
|
|
206
|
+
}
|
|
207
|
+
setScrollableBounded(hasAutoDetent)
|
|
208
|
+
reportSizeIfChanged()
|
|
117
209
|
}
|
|
118
210
|
|
|
119
211
|
this.bottomInset = bottomInset
|
|
@@ -123,23 +215,10 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
|
|
|
123
215
|
// If keyboard is currently showing, re-apply the keyboard inset to the new ScrollView
|
|
124
216
|
val keyboardHeight = keyboardObserver?.currentHeight ?: 0
|
|
125
217
|
if (keyboardHeight > 0) {
|
|
126
|
-
|
|
218
|
+
updateScrollViewInsetForKeyboard(keyboardHeight)
|
|
127
219
|
}
|
|
128
220
|
}
|
|
129
221
|
|
|
130
|
-
// TODO: Replace this workaround with synchronous state layout updates on every sheet resize.
|
|
131
|
-
// The container is currently sized to the largest detent, so at smaller detents the ScrollView
|
|
132
|
-
// viewport extends beyond the visible area, reducing the effective scroll range. This padding
|
|
133
|
-
// compensates for that difference until we can resize the container per-detent synchronously.
|
|
134
|
-
fun updateScrollExpansionPadding(padding: Int) {
|
|
135
|
-
if (scrollExpansionPadding == padding) return
|
|
136
|
-
scrollExpansionPadding = padding
|
|
137
|
-
val keyboardHeight = keyboardObserver?.currentHeight ?: 0
|
|
138
|
-
val basePadding = if (keyboardHeight > 0) keyboardHeight else bottomInset
|
|
139
|
-
setScrollViewPaddingBottom(originalScrollViewPaddingBottom + basePadding)
|
|
140
|
-
nudgeScrollView()
|
|
141
|
-
}
|
|
142
|
-
|
|
143
222
|
private fun setScrollViewPaddingBottom(paddingBottom: Int) {
|
|
144
223
|
val scrollView = pinnedScrollView ?: return
|
|
145
224
|
scrollView.clipToPadding = false
|
|
@@ -147,7 +226,7 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
|
|
|
147
226
|
scrollView.paddingLeft,
|
|
148
227
|
scrollView.paddingTop,
|
|
149
228
|
scrollView.paddingRight,
|
|
150
|
-
paddingBottom
|
|
229
|
+
paddingBottom
|
|
151
230
|
)
|
|
152
231
|
}
|
|
153
232
|
|
|
@@ -155,11 +234,14 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
|
|
|
155
234
|
pinnedScrollView?.setOnScrollChangeListener(null as View.OnScrollChangeListener?)
|
|
156
235
|
pinnedScrollView?.isNestedScrollingEnabled = false
|
|
157
236
|
(pinnedScrollView?.parent as? SwipeRefreshLayout)?.isNestedScrollingEnabled = true
|
|
158
|
-
scrollExpansionPadding = 0
|
|
159
237
|
setScrollViewPaddingBottom(originalScrollViewPaddingBottom)
|
|
238
|
+
observedScrollChild?.removeOnLayoutChangeListener(scrollChildLayoutListener)
|
|
239
|
+
observedScrollChild = null
|
|
240
|
+
setScrollableBounded(false)
|
|
160
241
|
pinnedScrollView = null
|
|
161
242
|
originalScrollViewPaddingBottom = 0
|
|
162
243
|
bottomInset = 0
|
|
244
|
+
reportSizeIfChanged()
|
|
163
245
|
}
|
|
164
246
|
|
|
165
247
|
fun findScrollView(): ViewGroup? {
|
|
@@ -249,7 +331,15 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
|
|
|
249
331
|
private fun updateScrollViewInsetForKeyboard(keyboardHeight: Int) {
|
|
250
332
|
val scrollView = pinnedScrollView ?: return
|
|
251
333
|
|
|
252
|
-
|
|
334
|
+
// An absolute footer rises above the keyboard and covers the content's
|
|
335
|
+
// bottom edge — include it so the caret clears the footer, not just the
|
|
336
|
+
// keyboard. A relative footer stays behind the keyboard below the content,
|
|
337
|
+
// so its height shields that much of the keyboard's overlap.
|
|
338
|
+
val totalBottomInset = if (keyboardHeight > 0) {
|
|
339
|
+
maxOf(0, keyboardHeight + (delegate?.footerKeyboardOcclusion ?: 0))
|
|
340
|
+
} else {
|
|
341
|
+
bottomInset
|
|
342
|
+
}
|
|
253
343
|
setScrollViewPaddingBottom(originalScrollViewPaddingBottom + totalBottomInset)
|
|
254
344
|
|
|
255
345
|
scrollView.post { nudgeScrollView() }
|
|
@@ -2,6 +2,8 @@ package com.lodev09.truesheet
|
|
|
2
2
|
|
|
3
3
|
import com.facebook.react.module.annotations.ReactModule
|
|
4
4
|
import com.facebook.react.uimanager.PointerEvents
|
|
5
|
+
import com.facebook.react.uimanager.ReactStylesDiffMap
|
|
6
|
+
import com.facebook.react.uimanager.StateWrapper
|
|
5
7
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
6
8
|
import com.facebook.react.uimanager.ViewGroupManager
|
|
7
9
|
import com.facebook.react.uimanager.annotations.ReactProp
|
|
@@ -17,6 +19,11 @@ class TrueSheetContentViewManager : ViewGroupManager<TrueSheetContentView>() {
|
|
|
17
19
|
|
|
18
20
|
override fun createViewInstance(reactContext: ThemedReactContext): TrueSheetContentView = TrueSheetContentView(reactContext)
|
|
19
21
|
|
|
22
|
+
override fun updateState(view: TrueSheetContentView, props: ReactStylesDiffMap?, stateWrapper: StateWrapper?): Any? {
|
|
23
|
+
view.stateWrapper = stateWrapper
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
@ReactProp(name = "pointerEvents")
|
|
21
28
|
fun setPointerEvents(view: TrueSheetContentView, pointerEventsStr: String?) {
|
|
22
29
|
view.pointerEvents = PointerEvents.parsePointerEvents(pointerEventsStr)
|
|
@@ -4,10 +4,13 @@ import android.annotation.SuppressLint
|
|
|
4
4
|
import android.view.MotionEvent
|
|
5
5
|
import android.view.View
|
|
6
6
|
import android.view.ViewParent
|
|
7
|
+
import com.facebook.react.bridge.WritableNativeMap
|
|
7
8
|
import com.facebook.react.uimanager.JSPointerDispatcher
|
|
8
9
|
import com.facebook.react.uimanager.JSTouchDispatcher
|
|
10
|
+
import com.facebook.react.uimanager.PixelUtil.pxToDp
|
|
9
11
|
import com.facebook.react.uimanager.PointerEvents
|
|
10
12
|
import com.facebook.react.uimanager.RootView
|
|
13
|
+
import com.facebook.react.uimanager.StateWrapper
|
|
11
14
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
12
15
|
import com.facebook.react.uimanager.events.EventDispatcher
|
|
13
16
|
import com.facebook.react.views.view.ReactViewGroup
|
|
@@ -34,12 +37,57 @@ class TrueSheetFooterView(private val reactContext: ThemedReactContext) :
|
|
|
34
37
|
RootView {
|
|
35
38
|
|
|
36
39
|
var delegate: TrueSheetFooterViewDelegate? = null
|
|
40
|
+
var stateWrapper: StateWrapper? = null
|
|
37
41
|
|
|
38
42
|
private val eventDispatcher: EventDispatcher?
|
|
39
43
|
get() = delegate?.eventDispatcher
|
|
40
44
|
|
|
41
45
|
private var lastWidth = 0
|
|
42
46
|
private var lastHeight = 0
|
|
47
|
+
private var bottomInset = 0
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Skips the inset while the keyboard is open — an absolute footer rises
|
|
51
|
+
* above the keyboard, so the inset would leave a gap.
|
|
52
|
+
*/
|
|
53
|
+
var keyboardVisible = false
|
|
54
|
+
set(value) {
|
|
55
|
+
if (field == value) return
|
|
56
|
+
field = value
|
|
57
|
+
pushBottomInsetState()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Height the footer occupies above the keyboard — its layout height minus
|
|
62
|
+
* the safe-area inset it drops while the keyboard is open.
|
|
63
|
+
*/
|
|
64
|
+
val keyboardOcclusionHeight: Int
|
|
65
|
+
get() = maxOf(0, height - if (keyboardVisible) 0 else bottomInset)
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Tells the shadow node to pad the footer's bottom edge with the sheet's
|
|
69
|
+
* bottom safe-area inset — the footer owns the sheet's bottom edge, so it
|
|
70
|
+
* absorbs the inset and its background fills it.
|
|
71
|
+
*/
|
|
72
|
+
fun setBottomInset(inset: Int) {
|
|
73
|
+
if (bottomInset == inset) return
|
|
74
|
+
bottomInset = inset
|
|
75
|
+
pushBottomInsetState()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private fun pushBottomInsetState() {
|
|
79
|
+
val sw = stateWrapper ?: return
|
|
80
|
+
val insetDp = (if (keyboardVisible) 0 else bottomInset).toFloat().pxToDp()
|
|
81
|
+
|
|
82
|
+
// Synchronous update — the footer must be padded before detents are
|
|
83
|
+
// configured, otherwise the auto detent is set up an inset short
|
|
84
|
+
if (TrueSheetStateUpdater.updateFooterState(sw, insetDp)) return
|
|
85
|
+
|
|
86
|
+
// Fallback: async state update
|
|
87
|
+
val newState = WritableNativeMap()
|
|
88
|
+
newState.putDouble("bottomInset", insetDp.toDouble())
|
|
89
|
+
sw.updateState(newState)
|
|
90
|
+
}
|
|
43
91
|
|
|
44
92
|
private val jsTouchDispatcher = JSTouchDispatcher(this)
|
|
45
93
|
private var jsPointerDispatcher: JSPointerDispatcher? = null
|
|
@@ -2,6 +2,8 @@ package com.lodev09.truesheet
|
|
|
2
2
|
|
|
3
3
|
import com.facebook.react.module.annotations.ReactModule
|
|
4
4
|
import com.facebook.react.uimanager.PointerEvents
|
|
5
|
+
import com.facebook.react.uimanager.ReactStylesDiffMap
|
|
6
|
+
import com.facebook.react.uimanager.StateWrapper
|
|
5
7
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
6
8
|
import com.facebook.react.uimanager.ViewGroupManager
|
|
7
9
|
import com.facebook.react.uimanager.annotations.ReactProp
|
|
@@ -17,6 +19,11 @@ class TrueSheetFooterViewManager : ViewGroupManager<TrueSheetFooterView>() {
|
|
|
17
19
|
|
|
18
20
|
override fun createViewInstance(reactContext: ThemedReactContext): TrueSheetFooterView = TrueSheetFooterView(reactContext)
|
|
19
21
|
|
|
22
|
+
override fun updateState(view: TrueSheetFooterView, props: ReactStylesDiffMap?, stateWrapper: StateWrapper?): Any? {
|
|
23
|
+
view.stateWrapper = stateWrapper
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
@ReactProp(name = "pointerEvents")
|
|
21
28
|
fun setPointerEvents(view: TrueSheetFooterView, pointerEventsStr: String?) {
|
|
22
29
|
view.pointerEvents = PointerEvents.parsePointerEvents(pointerEventsStr)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
package com.lodev09.truesheet
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.uimanager.StateWrapper
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Synchronous Fabric state updates via JNI (see TrueSheetStateUpdater.cpp).
|
|
7
|
+
*
|
|
8
|
+
* StateWrapper.updateState commits asynchronously, so the container's Yoga
|
|
9
|
+
* layout lags the sheet by a frame or two during drags. The native bridge
|
|
10
|
+
* commits with `unstable_Immediate`, which mounts synchronously when called
|
|
11
|
+
* from the UI thread — same-frame layout, like iOS.
|
|
12
|
+
*/
|
|
13
|
+
object TrueSheetStateUpdater {
|
|
14
|
+
private val isAvailable: Boolean = try {
|
|
15
|
+
System.loadLibrary("react_codegen_TrueSheetSpec")
|
|
16
|
+
true
|
|
17
|
+
} catch (e: UnsatisfiedLinkError) {
|
|
18
|
+
false
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Returns false when unavailable — caller should fall back to async updateState. */
|
|
22
|
+
fun updateState(stateWrapper: StateWrapper, widthDp: Float, heightDp: Float): Boolean =
|
|
23
|
+
isAvailable && nativeUpdateState(stateWrapper, widthDp, heightDp)
|
|
24
|
+
|
|
25
|
+
/** Returns false when unavailable — caller should fall back to async updateState. */
|
|
26
|
+
fun updateFooterState(stateWrapper: StateWrapper, bottomInsetDp: Float): Boolean =
|
|
27
|
+
isAvailable && nativeUpdateFooterState(stateWrapper, bottomInsetDp)
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Publishes the precalculated bottom safe-area inset so a late-mounted
|
|
31
|
+
* footer's first layout is already padded (see TrueSheetFooterViewShadowNode).
|
|
32
|
+
*/
|
|
33
|
+
fun setBottomSafeArea(insetDp: Float) {
|
|
34
|
+
if (isAvailable) nativeSetBottomSafeArea(insetDp)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@JvmStatic
|
|
38
|
+
private external fun nativeUpdateState(stateWrapper: Any, width: Float, height: Float): Boolean
|
|
39
|
+
|
|
40
|
+
@JvmStatic
|
|
41
|
+
private external fun nativeUpdateFooterState(stateWrapper: Any, bottomInset: Float): Boolean
|
|
42
|
+
|
|
43
|
+
@JvmStatic
|
|
44
|
+
private external fun nativeSetBottomSafeArea(insetDp: Float)
|
|
45
|
+
}
|
|
@@ -274,6 +274,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
274
274
|
|
|
275
275
|
fun setDetents(newDetents: MutableList<Double>) {
|
|
276
276
|
viewController.detents = newDetents
|
|
277
|
+
setupScrollable()
|
|
277
278
|
}
|
|
278
279
|
|
|
279
280
|
fun setInsetAdjustment(insetAdjustment: String) {
|
|
@@ -281,16 +282,19 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
281
282
|
setupScrollable()
|
|
282
283
|
}
|
|
283
284
|
|
|
284
|
-
fun setScrollable(scrollable: Boolean) {
|
|
285
|
-
viewController.scrollable = scrollable
|
|
286
|
-
setupScrollable()
|
|
287
|
-
}
|
|
288
|
-
|
|
289
285
|
fun setScrollableOptions(options: ScrollableOptions?) {
|
|
290
286
|
viewController.scrollableOptions = options
|
|
291
287
|
setupScrollable()
|
|
292
288
|
}
|
|
293
289
|
|
|
290
|
+
fun setAbsoluteHeader(absolute: Boolean) {
|
|
291
|
+
viewController.absoluteHeader = absolute
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
fun setAbsoluteFooter(absolute: Boolean) {
|
|
295
|
+
viewController.absoluteFooter = absolute
|
|
296
|
+
}
|
|
297
|
+
|
|
294
298
|
fun setFooterKeyboardOffset(offset: Float) {
|
|
295
299
|
viewController.footerKeyboardOffset = offset.dpToPx().toInt()
|
|
296
300
|
viewController.positionFooter()
|
|
@@ -299,11 +303,19 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
299
303
|
private fun setupScrollable() {
|
|
300
304
|
viewController.containerView?.let {
|
|
301
305
|
it.insetAdjustment = viewController.insetAdjustment
|
|
302
|
-
it.scrollableEnabled = viewController.scrollable
|
|
303
306
|
it.scrollViewBottomInset = viewController.contentBottomInset
|
|
304
307
|
it.scrollableOptions = viewController.scrollableOptions
|
|
308
|
+
it.hasAutoDetent = viewController.detents.contains(-1.0)
|
|
309
|
+
it.footerBottomInset = viewController.contentBottomInset
|
|
310
|
+
it.absoluteFooter = viewController.absoluteFooter
|
|
305
311
|
it.setupScrollable()
|
|
306
312
|
}
|
|
313
|
+
|
|
314
|
+
// Publish the inset so a footer set later (e.g. navigation setOptions) is
|
|
315
|
+
// padded on its very first layout instead of resizing the sheet twice.
|
|
316
|
+
if (viewController.insetAdjustment == TrueSheetInsetAdjustment.AUTOMATIC) {
|
|
317
|
+
TrueSheetStateUpdater.setBottomSafeArea(viewController.contentBottomInset.toFloat().pxToDp())
|
|
318
|
+
}
|
|
307
319
|
}
|
|
308
320
|
|
|
309
321
|
// ==================== Screen Event Observer ====================
|
|
@@ -343,9 +355,16 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
343
355
|
lastContainerHeight = height
|
|
344
356
|
|
|
345
357
|
val sw = stateWrapper ?: return
|
|
358
|
+
val widthDp = width.toFloat().pxToDp()
|
|
359
|
+
val heightDp = height.toFloat().pxToDp()
|
|
360
|
+
|
|
361
|
+
// Synchronous update — commits and mounts within the same UI-thread frame
|
|
362
|
+
if (TrueSheetStateUpdater.updateState(sw, widthDp, heightDp)) return
|
|
363
|
+
|
|
364
|
+
// Fallback: async state update
|
|
346
365
|
val newStateData = WritableNativeMap()
|
|
347
|
-
newStateData.putDouble("containerWidth",
|
|
348
|
-
newStateData.putDouble("containerHeight",
|
|
366
|
+
newStateData.putDouble("containerWidth", widthDp.toDouble())
|
|
367
|
+
newStateData.putDouble("containerHeight", heightDp.toDouble())
|
|
349
368
|
sw.updateState(newStateData)
|
|
350
369
|
}
|
|
351
370
|
|
|
@@ -569,10 +588,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
569
588
|
}
|
|
570
589
|
|
|
571
590
|
override fun viewControllerDidChangeSize(width: Int, height: Int) {
|
|
572
|
-
|
|
573
|
-
// Unlike IOS where ScrollView is pinned to the container.
|
|
574
|
-
val effectiveHeight = if (viewController.scrollable) height else viewController.screenHeight
|
|
575
|
-
updateState(width, effectiveHeight)
|
|
591
|
+
updateState(width, height)
|
|
576
592
|
}
|
|
577
593
|
|
|
578
594
|
override fun viewControllerWillFocus() {
|