@lodev09/react-native-true-sheet 4.0.0-beta.2 → 4.0.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/TrueSheetContainerView.kt +28 -4
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +206 -149
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +5 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +6 -17
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +73 -13
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +101 -18
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +25 -1
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +3 -3
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +14 -24
- package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +0 -7
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +50 -4
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +7 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +2 -1
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +11 -3
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +7 -0
- package/ios/TrueSheetContainerView.h +18 -5
- package/ios/TrueSheetContainerView.mm +10 -6
- package/ios/TrueSheetContentView.h +31 -10
- package/ios/TrueSheetContentView.mm +137 -162
- package/ios/TrueSheetFooterView.h +6 -0
- package/ios/TrueSheetFooterView.mm +48 -2
- package/ios/TrueSheetView.mm +95 -21
- package/ios/TrueSheetViewController.h +9 -0
- package/ios/TrueSheetViewController.mm +108 -25
- package/ios/utils/UIView+ScrollEdgeInteraction.h +1 -0
- package/ios/utils/UIView+ScrollEdgeInteraction.mm +24 -2
- package/lib/module/TrueSheet.js +77 -11
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +26 -24
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +4 -0
- package/lib/typescript/src/TrueSheet.d.ts +4 -0
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +50 -1
- 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/TrueSheetViewNativeComponent.d.ts +3 -0
- 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 +78 -9
- package/src/TrueSheet.types.ts +53 -1
- package/src/TrueSheet.web.tsx +37 -23
- package/src/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
- package/src/fabric/TrueSheetViewNativeComponent.ts +4 -0
- package/src/navigation/types.ts +1 -0
|
@@ -61,8 +61,6 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
|
|
|
61
61
|
return bottom
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
var insetAdjustment: TrueSheetInsetAdjustment = TrueSheetInsetAdjustment.AUTOMATIC
|
|
65
|
-
var scrollViewBottomInset: Int = 0
|
|
66
64
|
var absoluteFooter: Boolean = false
|
|
67
65
|
|
|
68
66
|
/**
|
|
@@ -80,6 +78,23 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
|
|
|
80
78
|
contentView?.scrollableOptions = value
|
|
81
79
|
}
|
|
82
80
|
|
|
81
|
+
// React tag of the user-provided scrollable within the content (see the scrollableRef prop)
|
|
82
|
+
var scrollableHandle: Int = -1
|
|
83
|
+
set(value) {
|
|
84
|
+
field = value
|
|
85
|
+
contentView?.scrollableHandle = value
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Bottom safe-area inset the plugged scrollable applies as content padding
|
|
90
|
+
* while it can scroll (see ScrollableOptions.contentInsetAdjustment)
|
|
91
|
+
*/
|
|
92
|
+
var scrollableBottomInset: Int = 0
|
|
93
|
+
set(value) {
|
|
94
|
+
field = value
|
|
95
|
+
contentView?.bottomInset = value
|
|
96
|
+
}
|
|
97
|
+
|
|
83
98
|
var hasAutoDetent = false
|
|
84
99
|
set(value) {
|
|
85
100
|
field = value
|
|
@@ -96,8 +111,7 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
|
|
|
96
111
|
}
|
|
97
112
|
|
|
98
113
|
fun setupScrollable() {
|
|
99
|
-
|
|
100
|
-
contentView?.setupScrollable(bottomInset)
|
|
114
|
+
contentView?.setupScrollable()
|
|
101
115
|
}
|
|
102
116
|
|
|
103
117
|
fun setupKeyboardHandler() {
|
|
@@ -115,9 +129,14 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
|
|
|
115
129
|
is TrueSheetContentView -> {
|
|
116
130
|
child.delegate = this
|
|
117
131
|
child.scrollableOptions = scrollableOptions
|
|
132
|
+
child.scrollableHandle = scrollableHandle
|
|
133
|
+
child.bottomInset = scrollableBottomInset
|
|
118
134
|
child.hasAutoDetent = hasAutoDetent
|
|
119
135
|
contentView = child
|
|
120
136
|
|
|
137
|
+
// State lands before the view is attached, so pull the current value
|
|
138
|
+
contentHeight = child.naturalHeight
|
|
139
|
+
|
|
121
140
|
// Children mount bottom-up, so the content subtree is complete here.
|
|
122
141
|
// Late-mounted peek views attach themselves instead (see TrueSheetPeekView).
|
|
123
142
|
findPeekView(child)?.let { attachPeekView(it) }
|
|
@@ -205,6 +224,9 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
|
|
|
205
224
|
override val footerKeyboardOcclusion: Int
|
|
206
225
|
get() = if (absoluteFooter) footerView?.keyboardOcclusionHeight ?: 0 else -(footerView?.height ?: 0)
|
|
207
226
|
|
|
227
|
+
override val hasRelativeFooter: Boolean
|
|
228
|
+
get() = footerView != null && !absoluteFooter
|
|
229
|
+
|
|
208
230
|
override fun contentViewDidScroll() {
|
|
209
231
|
delegate?.containerViewContentDidScroll()
|
|
210
232
|
}
|
|
@@ -221,6 +243,8 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
|
|
|
221
243
|
override fun footerViewDidChangeSize(width: Int, height: Int) {
|
|
222
244
|
footerHeight = height
|
|
223
245
|
delegate?.containerViewFooterDidChangeSize(width, height)
|
|
246
|
+
// A relative footer takes over the bottom inset — drop the scrollable's
|
|
247
|
+
contentView?.updateContentInset()
|
|
224
248
|
}
|
|
225
249
|
|
|
226
250
|
override fun peekViewDidChangeSize(width: Int, height: Int) {
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
package com.lodev09.truesheet
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
|
+
import android.os.Build
|
|
4
5
|
import android.text.Editable
|
|
5
6
|
import android.text.TextWatcher
|
|
6
7
|
import android.view.View
|
|
7
8
|
import android.view.ViewGroup
|
|
8
9
|
import android.widget.EditText
|
|
9
|
-
import android.widget.ScrollView
|
|
10
|
-
import androidx.core.widget.NestedScrollView
|
|
11
10
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
12
11
|
import com.facebook.react.bridge.WritableNativeMap
|
|
13
12
|
import com.facebook.react.uimanager.PixelUtil.dpToPx
|
|
@@ -17,10 +16,14 @@ import com.facebook.react.views.view.ReactViewGroup
|
|
|
17
16
|
import com.lodev09.truesheet.core.TrueSheetKeyboardObserver
|
|
18
17
|
import com.lodev09.truesheet.core.TrueSheetKeyboardObserverDelegate
|
|
19
18
|
import com.lodev09.truesheet.utils.isDescendantOf
|
|
20
|
-
import com.lodev09.truesheet.utils.smoothScrollBy
|
|
21
19
|
import com.lodev09.truesheet.utils.smoothScrollTo
|
|
22
20
|
|
|
23
|
-
data class ScrollableOptions(
|
|
21
|
+
data class ScrollableOptions(
|
|
22
|
+
val keyboardScrollOffset: Float = 0f,
|
|
23
|
+
val keyboardOffset: Float = 0f,
|
|
24
|
+
val scrollingExpandsSheet: Boolean = true,
|
|
25
|
+
val contentInsetAdjustment: Boolean = true
|
|
26
|
+
)
|
|
24
27
|
|
|
25
28
|
/**
|
|
26
29
|
* Delegate interface for content view size changes
|
|
@@ -32,10 +35,19 @@ interface TrueSheetContentViewDelegate {
|
|
|
32
35
|
|
|
33
36
|
/**
|
|
34
37
|
* Keyboard occlusion adjustment below the content — positive for an absolute
|
|
35
|
-
* footer
|
|
36
|
-
* negative for a relative footer sitting behind the keyboard shielding
|
|
38
|
+
* footer floating over the viewport's bottom edge (extends the caret reveal),
|
|
39
|
+
* negative for a relative footer sitting behind the keyboard shielding that
|
|
40
|
+
* much of its overlap (reduces the keyboard padding).
|
|
37
41
|
*/
|
|
38
42
|
val footerKeyboardOcclusion: Int
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A relative footer sits below the content and absorbs the bottom safe-area
|
|
46
|
+
* inset — the scrollable doesn't reach the sheet's bottom edge, so no
|
|
47
|
+
* content inset is applied (mirrors iOS, where UIKit resolves this
|
|
48
|
+
* geometrically via the scroll view's safe area).
|
|
49
|
+
*/
|
|
50
|
+
val hasRelativeFooter: Boolean
|
|
39
51
|
}
|
|
40
52
|
|
|
41
53
|
/**
|
|
@@ -47,42 +59,43 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
|
|
|
47
59
|
var delegate: TrueSheetContentViewDelegate? = null
|
|
48
60
|
var stateWrapper: StateWrapper? = null
|
|
49
61
|
|
|
50
|
-
private var
|
|
51
|
-
private var lastHeight = 0
|
|
52
|
-
|
|
53
|
-
private var pinnedScrollView: ViewGroup? = null
|
|
54
|
-
private var observedScrollChild: View? = null
|
|
62
|
+
private var detectedScrollView: ViewGroup? = null
|
|
55
63
|
private var originalScrollViewPaddingBottom: Int = 0
|
|
56
|
-
private var bottomInset: Int = 0
|
|
57
64
|
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
65
|
|
|
69
66
|
/**
|
|
70
|
-
* Content height
|
|
71
|
-
*
|
|
72
|
-
* Falls back to the view height
|
|
67
|
+
* Content height measured unconstrained by the shadow node — the height the
|
|
68
|
+
* content wants regardless of container bounds (see
|
|
69
|
+
* TrueSheetContentViewShadowNode). Falls back to the view height before the
|
|
70
|
+
* first state update.
|
|
73
71
|
*/
|
|
74
72
|
val naturalHeight: Int
|
|
75
|
-
get()
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
73
|
+
get() = if (lastNaturalHeight > 0) lastNaturalHeight else height
|
|
74
|
+
|
|
75
|
+
private var lastNaturalHeight = 0
|
|
76
|
+
|
|
77
|
+
fun updateNaturalHeight(heightDp: Double) {
|
|
78
|
+
val heightPx = heightDp.toFloat().dpToPx().toInt()
|
|
79
|
+
if (heightPx == lastNaturalHeight) return
|
|
80
|
+
lastNaturalHeight = heightPx
|
|
81
|
+
delegate?.contentViewDidChangeSize(width, heightPx)
|
|
82
|
+
}
|
|
84
83
|
|
|
85
84
|
private var keyboardScrollOffset: Float = 0f
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Adjustment added to the keyboard bottom inset applied to the detected
|
|
88
|
+
* scrollable — negative values reduce the inset (e.g. to cancel out safe-area
|
|
89
|
+
* padding already baked into the content's paddingBottom).
|
|
90
|
+
*/
|
|
91
|
+
private var keyboardOffset: Float = 0f
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* How much of keyboardOffset actually landed in the applied padding — the
|
|
95
|
+
* caret reveal compensates by this so it still targets the real keyboard edge.
|
|
96
|
+
*/
|
|
97
|
+
private var appliedKeyboardOffset = 0
|
|
98
|
+
|
|
86
99
|
private var keyboardObserver: TrueSheetKeyboardObserver? = null
|
|
87
100
|
|
|
88
101
|
private var watchedEditText: EditText? = null
|
|
@@ -92,18 +105,52 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
|
|
|
92
105
|
set(value) {
|
|
93
106
|
field = value
|
|
94
107
|
keyboardScrollOffset = value?.keyboardScrollOffset?.dpToPx() ?: 0f
|
|
108
|
+
keyboardOffset = value?.keyboardOffset?.dpToPx() ?: 0f
|
|
109
|
+
updateContentInset()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Bottom safe-area inset applied to the scrollable's content padding while
|
|
114
|
+
* the content can scroll — Android counterpart of iOS's
|
|
115
|
+
* `contentInsetAdjustmentBehavior="automatic"`. Already zero when the
|
|
116
|
+
* sheet's `insetAdjustment` is `'never'`.
|
|
117
|
+
*/
|
|
118
|
+
var bottomInset: Int = 0
|
|
119
|
+
set(value) {
|
|
120
|
+
if (field == value) return
|
|
121
|
+
field = value
|
|
122
|
+
updateContentInset()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private var baseBottomInset = 0
|
|
126
|
+
private var keyboardBottomInset = 0
|
|
127
|
+
|
|
128
|
+
private val scrollableLayoutListener =
|
|
129
|
+
View.OnLayoutChangeListener { _, _, top, _, bottom, _, oldTop, _, oldBottom ->
|
|
130
|
+
if (bottom - top != oldBottom - oldTop) updateContentInset()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* React tag of the user-provided scrollable (see the `scrollableRef` prop).
|
|
135
|
+
* Setting a new handle clears the currently resolved ScrollView.
|
|
136
|
+
*/
|
|
137
|
+
var scrollableHandle: Int = -1
|
|
138
|
+
set(value) {
|
|
139
|
+
if (field == value) return
|
|
140
|
+
field = value
|
|
141
|
+
clearScrollable()
|
|
95
142
|
}
|
|
96
143
|
|
|
97
144
|
/**
|
|
98
145
|
* Whether the sheet has an `auto` detent. Deriving the sheet height from the
|
|
99
|
-
* scroll content is circular with natural layout, so the
|
|
146
|
+
* scroll content is circular with natural layout, so the detected ScrollView's
|
|
100
147
|
* viewport is force-bounded to the container only in this case.
|
|
101
148
|
*/
|
|
102
149
|
var hasAutoDetent = false
|
|
103
150
|
set(value) {
|
|
104
151
|
if (field == value) return
|
|
105
152
|
field = value
|
|
106
|
-
if (
|
|
153
|
+
if (detectedScrollView != null) {
|
|
107
154
|
setScrollableBounded(value)
|
|
108
155
|
}
|
|
109
156
|
}
|
|
@@ -119,47 +166,14 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
|
|
|
119
166
|
}
|
|
120
167
|
|
|
121
168
|
private fun checkScrollViewChanged() {
|
|
122
|
-
if (
|
|
169
|
+
if (detectedScrollView == null || detectedScrollView?.isDescendantOf(this) == false) {
|
|
123
170
|
delegate?.contentViewScrollViewDidChange()
|
|
124
171
|
}
|
|
125
172
|
}
|
|
126
173
|
|
|
127
|
-
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
128
|
-
super.onSizeChanged(w, h, oldw, oldh)
|
|
129
|
-
reportSizeIfChanged()
|
|
130
|
-
}
|
|
131
|
-
|
|
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
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
reportSizeNow()
|
|
149
|
-
}
|
|
150
|
-
|
|
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)
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
174
|
/**
|
|
161
175
|
* Tells the shadow node to fill the container (flexGrow/flexShrink) so the
|
|
162
|
-
*
|
|
176
|
+
* detected ScrollView's viewport is bounded to the visible space. Only applied
|
|
163
177
|
* for auto detents — otherwise content lays out naturally like a regular view.
|
|
164
178
|
*/
|
|
165
179
|
private fun setScrollableBounded(bounded: Boolean) {
|
|
@@ -173,44 +187,37 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
|
|
|
173
187
|
}
|
|
174
188
|
}
|
|
175
189
|
|
|
176
|
-
fun setupScrollable(
|
|
177
|
-
// Check if
|
|
178
|
-
if (
|
|
190
|
+
fun setupScrollable() {
|
|
191
|
+
// Check if the detected scroll view is still valid (still in view hierarchy)
|
|
192
|
+
if (detectedScrollView != null && detectedScrollView?.isDescendantOf(this) == false) {
|
|
179
193
|
clearScrollable()
|
|
180
194
|
}
|
|
181
195
|
|
|
182
|
-
|
|
183
|
-
if (pinnedScrollView != null && this.bottomInset == bottomInset) {
|
|
196
|
+
if (detectedScrollView != null) {
|
|
184
197
|
return
|
|
185
198
|
}
|
|
186
199
|
|
|
187
|
-
val scrollView = findScrollView(
|
|
188
|
-
|
|
189
|
-
// Only capture originals on first pin
|
|
190
|
-
if (pinnedScrollView == null) {
|
|
191
|
-
originalScrollViewPaddingBottom = scrollView.paddingBottom
|
|
192
|
-
pinnedScrollView = scrollView
|
|
200
|
+
val scrollView = findScrollView() ?: return
|
|
193
201
|
|
|
194
|
-
|
|
195
|
-
|
|
202
|
+
originalScrollViewPaddingBottom = scrollView.paddingBottom
|
|
203
|
+
detectedScrollView = scrollView
|
|
196
204
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
delegate?.contentViewDidScroll()
|
|
200
|
-
}
|
|
201
|
-
}
|
|
205
|
+
scrollView.isNestedScrollingEnabled = true
|
|
206
|
+
(scrollView.parent as? SwipeRefreshLayout)?.isNestedScrollingEnabled = false
|
|
202
207
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
208
|
+
scrollView.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
|
|
209
|
+
if (scrollY != oldScrollY) {
|
|
210
|
+
delegate?.contentViewDidScroll()
|
|
206
211
|
}
|
|
207
|
-
setScrollableBounded(hasAutoDetent)
|
|
208
|
-
reportSizeIfChanged()
|
|
209
212
|
}
|
|
210
213
|
|
|
211
|
-
|
|
214
|
+
// Track viewport and content size changes — the content inset only applies
|
|
215
|
+
// while the content can scroll (mirrors iOS's automatic behavior)
|
|
216
|
+
scrollView.addOnLayoutChangeListener(scrollableLayoutListener)
|
|
217
|
+
scrollView.getChildAt(0)?.addOnLayoutChangeListener(scrollableLayoutListener)
|
|
212
218
|
|
|
213
|
-
|
|
219
|
+
setScrollableBounded(hasAutoDetent)
|
|
220
|
+
updateContentInset()
|
|
214
221
|
|
|
215
222
|
// If keyboard is currently showing, re-apply the keyboard inset to the new ScrollView
|
|
216
223
|
val keyboardHeight = keyboardObserver?.currentHeight ?: 0
|
|
@@ -219,8 +226,38 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
|
|
|
219
226
|
}
|
|
220
227
|
}
|
|
221
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Re-applies the base content inset — the bottom safe-area inset while the
|
|
231
|
+
* content can scroll, no relative footer absorbs it, and
|
|
232
|
+
* `contentInsetAdjustmentBehavior` isn't disabled.
|
|
233
|
+
*/
|
|
234
|
+
fun updateContentInset() {
|
|
235
|
+
val scrollView = detectedScrollView ?: return
|
|
236
|
+
|
|
237
|
+
val enabled = scrollableOptions?.contentInsetAdjustment ?: true
|
|
238
|
+
baseBottomInset = if (enabled && delegate?.hasRelativeFooter != true && canScrollContent(scrollView)) {
|
|
239
|
+
bottomInset
|
|
240
|
+
} else {
|
|
241
|
+
0
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
applyBottomPadding()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Measured against the original padding so the applied inset doesn't feed
|
|
248
|
+
// back into the check
|
|
249
|
+
private fun canScrollContent(scrollView: ViewGroup): Boolean {
|
|
250
|
+
val child = scrollView.getChildAt(0) ?: return false
|
|
251
|
+
return child.height > scrollView.height - scrollView.paddingTop - originalScrollViewPaddingBottom
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// The keyboard covers the safe-area region, so the larger inset wins over a sum
|
|
255
|
+
private fun applyBottomPadding() {
|
|
256
|
+
setScrollViewPaddingBottom(originalScrollViewPaddingBottom + maxOf(baseBottomInset, keyboardBottomInset))
|
|
257
|
+
}
|
|
258
|
+
|
|
222
259
|
private fun setScrollViewPaddingBottom(paddingBottom: Int) {
|
|
223
|
-
val scrollView =
|
|
260
|
+
val scrollView = detectedScrollView ?: return
|
|
224
261
|
scrollView.clipToPadding = false
|
|
225
262
|
scrollView.setPadding(
|
|
226
263
|
scrollView.paddingLeft,
|
|
@@ -231,39 +268,28 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
|
|
|
231
268
|
}
|
|
232
269
|
|
|
233
270
|
fun clearScrollable() {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
(
|
|
271
|
+
detectedScrollView?.setOnScrollChangeListener(null as View.OnScrollChangeListener?)
|
|
272
|
+
detectedScrollView?.removeOnLayoutChangeListener(scrollableLayoutListener)
|
|
273
|
+
detectedScrollView?.getChildAt(0)?.removeOnLayoutChangeListener(scrollableLayoutListener)
|
|
274
|
+
detectedScrollView?.isNestedScrollingEnabled = false
|
|
275
|
+
(detectedScrollView?.parent as? SwipeRefreshLayout)?.isNestedScrollingEnabled = true
|
|
237
276
|
setScrollViewPaddingBottom(originalScrollViewPaddingBottom)
|
|
238
|
-
observedScrollChild?.removeOnLayoutChangeListener(scrollChildLayoutListener)
|
|
239
|
-
observedScrollChild = null
|
|
240
277
|
setScrollableBounded(false)
|
|
241
|
-
|
|
278
|
+
detectedScrollView = null
|
|
242
279
|
originalScrollViewPaddingBottom = 0
|
|
243
|
-
|
|
244
|
-
|
|
280
|
+
baseBottomInset = 0
|
|
281
|
+
keyboardBottomInset = 0
|
|
282
|
+
appliedKeyboardOffset = 0
|
|
245
283
|
}
|
|
246
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Resolves the user-provided `scrollableHandle` within the content subtree —
|
|
287
|
+
* React tags are view ids on Android.
|
|
288
|
+
*/
|
|
247
289
|
fun findScrollView(): ViewGroup? {
|
|
248
|
-
if (
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
private fun findScrollView(view: View): ViewGroup? {
|
|
253
|
-
if (view is ScrollView || view is NestedScrollView) {
|
|
254
|
-
return view as ViewGroup
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (view is ViewGroup) {
|
|
258
|
-
for (i in 0 until view.childCount) {
|
|
259
|
-
val scrollView = findScrollView(view.getChildAt(i))
|
|
260
|
-
if (scrollView != null) {
|
|
261
|
-
return scrollView
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
return null
|
|
290
|
+
if (detectedScrollView != null) return detectedScrollView
|
|
291
|
+
if (scrollableHandle <= 0) return null
|
|
292
|
+
return findViewById<View>(scrollableHandle) as? ViewGroup
|
|
267
293
|
}
|
|
268
294
|
|
|
269
295
|
// ==================== Keyboard Handling ====================
|
|
@@ -273,8 +299,19 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
|
|
|
273
299
|
|
|
274
300
|
keyboardObserver = TrueSheetKeyboardObserver(this, reactContext).apply {
|
|
275
301
|
delegate = object : TrueSheetKeyboardObserverDelegate {
|
|
276
|
-
|
|
302
|
+
// Drive the inset per keyboard animation frame so the content tracks
|
|
303
|
+
// the keyboard edge instead of jumping to the final inset up front.
|
|
304
|
+
// On hide, the shrinking inset gradually clamps the scroll position
|
|
305
|
+
// back in sync with the keyboard.
|
|
306
|
+
override fun keyboardDidChangeHeight(height: Int) {
|
|
277
307
|
updateScrollViewInsetForKeyboard(height)
|
|
308
|
+
|
|
309
|
+
// Follow the caret frame-by-frame — only API 30+ delivers
|
|
310
|
+
// incremental heights; legacy fires once after the keyboard is
|
|
311
|
+
// already up, where keyboardDidShow's smooth reveal takes over
|
|
312
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !isHiding) {
|
|
313
|
+
scrollToFocusedInput(smooth = false)
|
|
314
|
+
}
|
|
278
315
|
}
|
|
279
316
|
|
|
280
317
|
override fun keyboardDidShow(height: Int) {
|
|
@@ -284,7 +321,6 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
|
|
|
284
321
|
|
|
285
322
|
override fun keyboardWillHide() {
|
|
286
323
|
detachCaretListener()
|
|
287
|
-
updateScrollViewInsetForKeyboard(0)
|
|
288
324
|
}
|
|
289
325
|
|
|
290
326
|
override fun focusDidChange(newFocus: View) {
|
|
@@ -329,30 +365,39 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
|
|
|
329
365
|
}
|
|
330
366
|
|
|
331
367
|
private fun updateScrollViewInsetForKeyboard(keyboardHeight: Int) {
|
|
332
|
-
val scrollView =
|
|
333
|
-
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
|
|
339
|
-
maxOf(0, keyboardHeight + (delegate?.footerKeyboardOcclusion ?: 0))
|
|
368
|
+
val scrollView = detectedScrollView ?: return
|
|
369
|
+
|
|
370
|
+
// A relative footer stays behind the keyboard below the content, so its
|
|
371
|
+
// height shields that much of the keyboard's overlap. An absolute footer
|
|
372
|
+
// floats within the viewport — its clearance is the content padding's job
|
|
373
|
+
// (the caret reveal accounts for it, see scrollToFocusedInput).
|
|
374
|
+
keyboardBottomInset = if (keyboardHeight > 0) {
|
|
375
|
+
val baseInset = maxOf(0, keyboardHeight + minOf(0, delegate?.footerKeyboardOcclusion ?: 0))
|
|
376
|
+
val adjustedInset = maxOf(0, baseInset + keyboardOffset.toInt())
|
|
377
|
+
appliedKeyboardOffset = adjustedInset - baseInset
|
|
378
|
+
adjustedInset
|
|
340
379
|
} else {
|
|
341
|
-
|
|
380
|
+
appliedKeyboardOffset = 0
|
|
381
|
+
0
|
|
342
382
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
scrollView.post { nudgeScrollView() }
|
|
383
|
+
applyBottomPadding()
|
|
384
|
+
clampScrollPosition()
|
|
346
385
|
}
|
|
347
386
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
387
|
+
// Padding changes don't re-clamp the scroll position until the next layout
|
|
388
|
+
// pass — clamp synchronously so the content follows the shrinking inset on
|
|
389
|
+
// every keyboard frame instead of snapping afterwards
|
|
390
|
+
private fun clampScrollPosition() {
|
|
391
|
+
val scrollView = detectedScrollView ?: return
|
|
392
|
+
val child = scrollView.getChildAt(0) ?: return
|
|
393
|
+
val maxScrollY = maxOf(0, child.height - (scrollView.height - scrollView.paddingTop - scrollView.paddingBottom))
|
|
394
|
+
if (scrollView.scrollY > maxScrollY) {
|
|
395
|
+
scrollView.scrollTo(scrollView.scrollX, maxScrollY)
|
|
396
|
+
}
|
|
352
397
|
}
|
|
353
398
|
|
|
354
|
-
private fun scrollToFocusedInput() {
|
|
355
|
-
val scrollView =
|
|
399
|
+
private fun scrollToFocusedInput(smooth: Boolean = true) {
|
|
400
|
+
val scrollView = detectedScrollView ?: findScrollView() ?: return
|
|
356
401
|
val focusedView = findFocus() ?: return
|
|
357
402
|
|
|
358
403
|
val focusedLocation = IntArray(2)
|
|
@@ -379,17 +424,29 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
|
|
|
379
424
|
caretBottom = viewTop + focusedView.height
|
|
380
425
|
}
|
|
381
426
|
|
|
427
|
+
// An absolute footer floats over the viewport's bottom edge — extend the
|
|
428
|
+
// caret target so it clears the footer, not just the keyboard.
|
|
429
|
+
val footerOcclusion = maxOf(0, delegate?.footerKeyboardOcclusion ?: 0)
|
|
430
|
+
|
|
382
431
|
val offset = keyboardScrollOffset.toInt()
|
|
383
|
-
val visibleHeight = scrollView.height - scrollView.paddingBottom
|
|
432
|
+
val visibleHeight = scrollView.height - scrollView.paddingBottom + appliedKeyboardOffset
|
|
384
433
|
val visibleTop = scrollView.scrollY
|
|
385
434
|
val visibleBottom = scrollView.scrollY + visibleHeight
|
|
386
435
|
|
|
387
|
-
when {
|
|
388
|
-
caretBottom + offset > visibleBottom ->
|
|
389
|
-
|
|
436
|
+
val targetY = when {
|
|
437
|
+
caretBottom + offset + footerOcclusion > visibleBottom ->
|
|
438
|
+
caretBottom + offset + footerOcclusion - visibleHeight
|
|
390
439
|
|
|
391
440
|
caretTop - offset < visibleTop ->
|
|
392
|
-
|
|
441
|
+
(caretTop - offset).coerceAtLeast(0)
|
|
442
|
+
|
|
443
|
+
else -> return
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (smooth) {
|
|
447
|
+
scrollView.smoothScrollTo(0, targetY)
|
|
448
|
+
} else {
|
|
449
|
+
scrollView.scrollTo(0, targetY)
|
|
393
450
|
}
|
|
394
451
|
}
|
|
395
452
|
|
|
@@ -21,6 +21,11 @@ class TrueSheetContentViewManager : ViewGroupManager<TrueSheetContentView>() {
|
|
|
21
21
|
|
|
22
22
|
override fun updateState(view: TrueSheetContentView, props: ReactStylesDiffMap?, stateWrapper: StateWrapper?): Any? {
|
|
23
23
|
view.stateWrapper = stateWrapper
|
|
24
|
+
stateWrapper?.stateData?.let { data ->
|
|
25
|
+
if (data.hasKey("naturalHeight")) {
|
|
26
|
+
view.updateNaturalHeight(data.getDouble("naturalHeight"))
|
|
27
|
+
}
|
|
28
|
+
}
|
|
24
29
|
return null
|
|
25
30
|
}
|
|
26
31
|
|
|
@@ -45,25 +45,17 @@ class TrueSheetFooterView(private val reactContext: ThemedReactContext) :
|
|
|
45
45
|
|
|
46
46
|
private var lastWidth = 0
|
|
47
47
|
private var lastHeight = 0
|
|
48
|
-
private var bottomInset = 0
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
* above the keyboard, so the inset would leave a gap.
|
|
53
|
-
*/
|
|
54
|
-
var keyboardVisible = false
|
|
55
|
-
set(value) {
|
|
56
|
-
if (field == value) return
|
|
57
|
-
field = value
|
|
58
|
-
pushBottomInsetState()
|
|
59
|
-
}
|
|
49
|
+
var bottomInset = 0
|
|
50
|
+
private set
|
|
60
51
|
|
|
61
52
|
/**
|
|
62
53
|
* Height the footer occupies above the keyboard — its layout height minus
|
|
63
|
-
* the safe-area inset
|
|
54
|
+
* the safe-area inset, which rides below the keyboard's top edge while the
|
|
55
|
+
* keyboard is open (see positionFooter).
|
|
64
56
|
*/
|
|
65
57
|
val keyboardOcclusionHeight: Int
|
|
66
|
-
get() = maxOf(0, height -
|
|
58
|
+
get() = maxOf(0, height - bottomInset)
|
|
67
59
|
|
|
68
60
|
/**
|
|
69
61
|
* Tells the shadow node to pad the footer's bottom edge with the sheet's
|
|
@@ -73,12 +65,9 @@ class TrueSheetFooterView(private val reactContext: ThemedReactContext) :
|
|
|
73
65
|
fun setBottomInset(inset: Int) {
|
|
74
66
|
if (bottomInset == inset) return
|
|
75
67
|
bottomInset = inset
|
|
76
|
-
pushBottomInsetState()
|
|
77
|
-
}
|
|
78
68
|
|
|
79
|
-
private fun pushBottomInsetState() {
|
|
80
69
|
val sw = stateWrapper ?: return
|
|
81
|
-
val insetDp =
|
|
70
|
+
val insetDp = bottomInset.toFloat().pxToDp()
|
|
82
71
|
|
|
83
72
|
// Synchronous update — the footer must be padded before detents are
|
|
84
73
|
// configured, otherwise the auto detent is set up an inset short
|