@lodev09/react-native-true-sheet 3.11.3 → 3.11.5
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/RNTrueSheet.podspec +6 -2
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +69 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +62 -4
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +2 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetPeekView.kt +72 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetPeekViewManager.kt +33 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +7 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +10 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +24 -1
- package/ios/TrueSheetContainerView.h +18 -0
- package/ios/TrueSheetContainerView.mm +78 -1
- package/ios/TrueSheetContentView.mm +82 -4
- package/ios/TrueSheetPeekView.h +32 -0
- package/ios/TrueSheetPeekView.mm +99 -0
- package/ios/TrueSheetView.mm +19 -0
- package/ios/TrueSheetViewController.h +2 -0
- package/ios/TrueSheetViewController.mm +10 -0
- package/ios/core/TrueSheetDetentCalculator.h +10 -0
- package/ios/core/TrueSheetDetentCalculator.mm +9 -0
- package/ios/tvos/TrueSheetStubs.mm +152 -0
- package/lib/module/TrueSheet.js +2 -1
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +104 -69
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/TrueSheetPeek.js +15 -0
- package/lib/module/TrueSheetPeek.js.map +1 -0
- package/lib/module/TrueSheetPeek.web.js +46 -0
- package/lib/module/TrueSheetPeek.web.js.map +1 -0
- package/lib/module/fabric/TrueSheetPeekViewNativeComponent.ts +8 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/mocks/index.js +10 -0
- package/lib/module/mocks/index.js.map +1 -1
- package/lib/module/web/vaul/constants.js +1 -0
- package/lib/module/web/vaul/constants.js.map +1 -1
- package/lib/module/web/vaul/index.js +2 -0
- package/lib/module/web/vaul/index.js.map +1 -1
- package/lib/module/web/vaul/use-snap-points.js +6 -4
- package/lib/module/web/vaul/use-snap-points.js.map +1 -1
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +10 -0
- 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/TrueSheetPeek.d.ts +9 -0
- package/lib/typescript/src/TrueSheetPeek.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheetPeek.web.d.ts +19 -0
- package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -0
- package/lib/typescript/src/fabric/TrueSheetPeekViewNativeComponent.d.ts +6 -0
- package/lib/typescript/src/fabric/TrueSheetPeekViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/mocks/index.d.ts +6 -2
- package/lib/typescript/src/mocks/index.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/constants.d.ts +1 -0
- package/lib/typescript/src/web/vaul/constants.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/index.d.ts +6 -1
- package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts +2 -1
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/TrueSheet.tsx +2 -1
- package/src/TrueSheet.types.ts +11 -0
- package/src/TrueSheet.web.tsx +110 -71
- package/src/TrueSheetPeek.tsx +11 -0
- package/src/TrueSheetPeek.web.tsx +48 -0
- package/src/fabric/TrueSheetPeekViewNativeComponent.ts +8 -0
- package/src/index.ts +1 -0
- package/src/mocks/index.ts +8 -1
- package/src/web/vaul/constants.ts +2 -0
- package/src/web/vaul/index.tsx +7 -0
- package/src/web/vaul/use-snap-points.ts +17 -4
package/RNTrueSheet.podspec
CHANGED
|
@@ -10,10 +10,14 @@ Pod::Spec.new do |s|
|
|
|
10
10
|
s.license = package["license"]
|
|
11
11
|
s.authors = package["author"]
|
|
12
12
|
|
|
13
|
-
s.platforms = { :ios => min_ios_version_supported }
|
|
13
|
+
s.platforms = { :ios => min_ios_version_supported, :tvos => min_ios_version_supported }
|
|
14
14
|
s.source = { :git => "https://github.com/lodev09/react-native-true-sheet.git", :tag => "#{s.version}" }
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
# tvOS compiles stubs only — sheets are not supported there, but codegen
|
|
17
|
+
# still registers the components so the classes must exist (#722)
|
|
18
|
+
s.ios.source_files = "ios/**/*.{h,m,mm,swift,cpp}", "common/cpp/**/*.{cpp,h}"
|
|
19
|
+
s.ios.exclude_files = "ios/tvos/**/*"
|
|
20
|
+
s.tvos.source_files = "ios/tvos/**/*.mm", "common/cpp/**/*.{cpp,h}"
|
|
17
21
|
s.private_header_files = "ios/**/*.h"
|
|
18
22
|
|
|
19
23
|
s.pod_target_xcconfig = {
|
|
@@ -2,8 +2,10 @@ package com.lodev09.truesheet
|
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.view.View
|
|
5
|
+
import android.view.ViewGroup
|
|
5
6
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
6
7
|
import com.facebook.react.uimanager.events.EventDispatcher
|
|
8
|
+
import com.facebook.react.util.RNLog
|
|
7
9
|
import com.facebook.react.views.view.ReactViewGroup
|
|
8
10
|
|
|
9
11
|
interface TrueSheetContainerViewDelegate {
|
|
@@ -13,6 +15,7 @@ interface TrueSheetContainerViewDelegate {
|
|
|
13
15
|
fun containerViewScrollViewDidChange()
|
|
14
16
|
fun containerViewHeaderDidChangeSize(width: Int, height: Int)
|
|
15
17
|
fun containerViewFooterDidChangeSize(width: Int, height: Int)
|
|
18
|
+
fun containerViewPeekDidChangeSize(width: Int, height: Int)
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
/**
|
|
@@ -24,18 +27,40 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
|
|
|
24
27
|
ReactViewGroup(reactContext),
|
|
25
28
|
TrueSheetContentViewDelegate,
|
|
26
29
|
TrueSheetHeaderViewDelegate,
|
|
27
|
-
TrueSheetFooterViewDelegate
|
|
30
|
+
TrueSheetFooterViewDelegate,
|
|
31
|
+
TrueSheetPeekViewDelegate {
|
|
28
32
|
|
|
29
33
|
var delegate: TrueSheetContainerViewDelegate? = null
|
|
30
34
|
|
|
31
35
|
var contentView: TrueSheetContentView? = null
|
|
32
36
|
var headerView: TrueSheetHeaderView? = null
|
|
33
37
|
var footerView: TrueSheetFooterView? = null
|
|
38
|
+
var peekView: TrueSheetPeekView? = null
|
|
34
39
|
|
|
35
40
|
var contentHeight: Int = 0
|
|
36
41
|
var headerHeight: Int = 0
|
|
37
42
|
var footerHeight: Int = 0
|
|
38
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Distance from the top of the content view to the bottom of the peek view.
|
|
46
|
+
* Includes the peek view's offset within the content (padding, views above it)
|
|
47
|
+
* so the peek detent reveals everything down to the peek content's bottom edge.
|
|
48
|
+
*/
|
|
49
|
+
val peekContentHeight: Int
|
|
50
|
+
get() {
|
|
51
|
+
val peek = peekView ?: return 0
|
|
52
|
+
val content = contentView ?: return peek.height
|
|
53
|
+
|
|
54
|
+
var bottom = peek.height
|
|
55
|
+
var view: View = peek
|
|
56
|
+
while (view !== content) {
|
|
57
|
+
bottom += view.top
|
|
58
|
+
view = view.parent as? View ?: return peek.height
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return bottom
|
|
62
|
+
}
|
|
63
|
+
|
|
39
64
|
var insetAdjustment: TrueSheetInsetAdjustment = TrueSheetInsetAdjustment.AUTOMATIC
|
|
40
65
|
var scrollViewBottomInset: Int = 0
|
|
41
66
|
var scrollableEnabled: Boolean = false
|
|
@@ -75,6 +100,10 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
|
|
|
75
100
|
child.delegate = this
|
|
76
101
|
child.scrollableOptions = scrollableOptions
|
|
77
102
|
contentView = child
|
|
103
|
+
|
|
104
|
+
// Children mount bottom-up, so the content subtree is complete here.
|
|
105
|
+
// Late-mounted peek views attach themselves instead (see TrueSheetPeekView).
|
|
106
|
+
findPeekView(child)?.let { attachPeekView(it) }
|
|
78
107
|
}
|
|
79
108
|
|
|
80
109
|
is TrueSheetHeaderView -> {
|
|
@@ -113,6 +142,41 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
|
|
|
113
142
|
super.removeViewAt(index)
|
|
114
143
|
}
|
|
115
144
|
|
|
145
|
+
// ==================== Peek View ====================
|
|
146
|
+
|
|
147
|
+
private fun findPeekView(view: View): TrueSheetPeekView? {
|
|
148
|
+
if (view is TrueSheetPeekView) return view
|
|
149
|
+
|
|
150
|
+
if (view is ViewGroup) {
|
|
151
|
+
for (i in 0 until view.childCount) {
|
|
152
|
+
findPeekView(view.getChildAt(i))?.let { return it }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
fun attachPeekView(view: TrueSheetPeekView) {
|
|
160
|
+
if (peekView === view) return
|
|
161
|
+
|
|
162
|
+
if (peekView != null) {
|
|
163
|
+
RNLog.w(context as ThemedReactContext, "TrueSheet: Sheet can only have one peek component.")
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
peekView = view
|
|
168
|
+
view.delegate = this
|
|
169
|
+
peekViewDidChangeSize(view.width, view.height)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fun detachPeekView(view: TrueSheetPeekView) {
|
|
173
|
+
if (peekView !== view) return
|
|
174
|
+
|
|
175
|
+
view.delegate = null
|
|
176
|
+
peekView = null
|
|
177
|
+
peekViewDidChangeSize(0, 0)
|
|
178
|
+
}
|
|
179
|
+
|
|
116
180
|
// ==================== Delegate Implementations ====================
|
|
117
181
|
|
|
118
182
|
override fun contentViewDidChangeSize(width: Int, height: Int) {
|
|
@@ -138,6 +202,10 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
|
|
|
138
202
|
delegate?.containerViewFooterDidChangeSize(width, height)
|
|
139
203
|
}
|
|
140
204
|
|
|
205
|
+
override fun peekViewDidChangeSize(width: Int, height: Int) {
|
|
206
|
+
delegate?.containerViewPeekDidChangeSize(width, height)
|
|
207
|
+
}
|
|
208
|
+
|
|
141
209
|
companion object {
|
|
142
210
|
const val TAG_NAME = "TrueSheet"
|
|
143
211
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
package com.lodev09.truesheet
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
|
+
import android.text.Editable
|
|
5
|
+
import android.text.TextWatcher
|
|
4
6
|
import android.view.View
|
|
5
7
|
import android.view.ViewGroup
|
|
8
|
+
import android.widget.EditText
|
|
6
9
|
import android.widget.ScrollView
|
|
7
10
|
import androidx.core.widget.NestedScrollView
|
|
8
11
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
@@ -45,6 +48,9 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
|
|
|
45
48
|
private var keyboardScrollOffset: Float = 0f
|
|
46
49
|
private var keyboardObserver: TrueSheetKeyboardObserver? = null
|
|
47
50
|
|
|
51
|
+
private var watchedEditText: EditText? = null
|
|
52
|
+
private var caretTextWatcher: TextWatcher? = null
|
|
53
|
+
|
|
48
54
|
var scrollableOptions: ScrollableOptions? = null
|
|
49
55
|
set(value) {
|
|
50
56
|
field = value
|
|
@@ -190,14 +196,17 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
|
|
|
190
196
|
}
|
|
191
197
|
|
|
192
198
|
override fun keyboardDidShow(height: Int) {
|
|
199
|
+
attachCaretListener()
|
|
193
200
|
scrollToFocusedInput()
|
|
194
201
|
}
|
|
195
202
|
|
|
196
203
|
override fun keyboardWillHide() {
|
|
204
|
+
detachCaretListener()
|
|
197
205
|
updateScrollViewInsetForKeyboard(0)
|
|
198
206
|
}
|
|
199
207
|
|
|
200
208
|
override fun focusDidChange(newFocus: View) {
|
|
209
|
+
attachCaretListener()
|
|
201
210
|
scrollToFocusedInput()
|
|
202
211
|
}
|
|
203
212
|
}
|
|
@@ -206,10 +215,37 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
|
|
|
206
215
|
}
|
|
207
216
|
|
|
208
217
|
fun cleanupKeyboardHandler() {
|
|
218
|
+
detachCaretListener()
|
|
209
219
|
keyboardObserver?.stop()
|
|
210
220
|
keyboardObserver = null
|
|
211
221
|
}
|
|
212
222
|
|
|
223
|
+
private fun attachCaretListener() {
|
|
224
|
+
val focused = findFocus() as? EditText
|
|
225
|
+
if (focused === watchedEditText) return
|
|
226
|
+
|
|
227
|
+
detachCaretListener()
|
|
228
|
+
if (focused == null) return
|
|
229
|
+
|
|
230
|
+
val watcher = object : TextWatcher {
|
|
231
|
+
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
|
232
|
+
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
|
233
|
+
override fun afterTextChanged(s: Editable?) {
|
|
234
|
+
focused.post { scrollToFocusedInput() }
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
focused.addTextChangedListener(watcher)
|
|
239
|
+
watchedEditText = focused
|
|
240
|
+
caretTextWatcher = watcher
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private fun detachCaretListener() {
|
|
244
|
+
caretTextWatcher?.let { watchedEditText?.removeTextChangedListener(it) }
|
|
245
|
+
caretTextWatcher = null
|
|
246
|
+
watchedEditText = null
|
|
247
|
+
}
|
|
248
|
+
|
|
213
249
|
private fun updateScrollViewInsetForKeyboard(keyboardHeight: Int) {
|
|
214
250
|
val scrollView = pinnedScrollView ?: return
|
|
215
251
|
|
|
@@ -234,14 +270,36 @@ class TrueSheetContentView(private val reactContext: ThemedReactContext) : React
|
|
|
234
270
|
focusedView.getLocationOnScreen(focusedLocation)
|
|
235
271
|
scrollView.getLocationOnScreen(scrollViewLocation)
|
|
236
272
|
|
|
237
|
-
val
|
|
238
|
-
|
|
273
|
+
val viewTop = focusedLocation[1] - scrollViewLocation[1] + scrollView.scrollY
|
|
274
|
+
|
|
275
|
+
// Resolve the caret's line within the focused input so we follow the cursor
|
|
276
|
+
// rather than the input's full bounds (which over-scrolls tall inputs).
|
|
277
|
+
val editText = focusedView as? EditText
|
|
278
|
+
val layout = editText?.layout
|
|
279
|
+
|
|
280
|
+
val caretTop: Int
|
|
281
|
+
val caretBottom: Int
|
|
282
|
+
if (editText != null && layout != null) {
|
|
283
|
+
val line = layout.getLineForOffset(editText.selectionEnd.coerceAtLeast(0))
|
|
284
|
+
val textTop = viewTop + editText.totalPaddingTop - editText.scrollY
|
|
285
|
+
caretTop = textTop + layout.getLineTop(line)
|
|
286
|
+
caretBottom = textTop + layout.getLineBottom(line)
|
|
287
|
+
} else {
|
|
288
|
+
caretTop = viewTop
|
|
289
|
+
caretBottom = viewTop + focusedView.height
|
|
290
|
+
}
|
|
239
291
|
|
|
292
|
+
val offset = keyboardScrollOffset.toInt()
|
|
240
293
|
val visibleHeight = scrollView.height - scrollView.paddingBottom
|
|
294
|
+
val visibleTop = scrollView.scrollY
|
|
241
295
|
val visibleBottom = scrollView.scrollY + visibleHeight
|
|
242
296
|
|
|
243
|
-
|
|
244
|
-
|
|
297
|
+
when {
|
|
298
|
+
caretBottom + offset > visibleBottom ->
|
|
299
|
+
scrollView.smoothScrollTo(0, caretBottom + offset - visibleHeight)
|
|
300
|
+
|
|
301
|
+
caretTop - offset < visibleTop ->
|
|
302
|
+
scrollView.smoothScrollTo(0, (caretTop - offset).coerceAtLeast(0))
|
|
245
303
|
}
|
|
246
304
|
}
|
|
247
305
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
package com.lodev09.truesheet
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.view.ViewParent
|
|
5
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
6
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Delegate interface for peek view size changes
|
|
10
|
+
*/
|
|
11
|
+
interface TrueSheetPeekViewDelegate {
|
|
12
|
+
fun peekViewDidChangeSize(width: Int, height: Int)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Peek view that marks part of the content as the sheet's peek content.
|
|
17
|
+
* The peek detent covers the content through this view's bottom edge.
|
|
18
|
+
*/
|
|
19
|
+
@SuppressLint("ViewConstructor")
|
|
20
|
+
class TrueSheetPeekView(context: ThemedReactContext) : ReactViewGroup(context) {
|
|
21
|
+
var delegate: TrueSheetPeekViewDelegate? = null
|
|
22
|
+
|
|
23
|
+
override fun onLayout(
|
|
24
|
+
changed: Boolean,
|
|
25
|
+
left: Int,
|
|
26
|
+
top: Int,
|
|
27
|
+
right: Int,
|
|
28
|
+
bottom: Int
|
|
29
|
+
) {
|
|
30
|
+
super.onLayout(changed, left, top, right, bottom)
|
|
31
|
+
|
|
32
|
+
attachToContainerView()
|
|
33
|
+
|
|
34
|
+
// Position changes matter too — the peek's offset within the content
|
|
35
|
+
// affects the peek detent.
|
|
36
|
+
if (changed) {
|
|
37
|
+
delegate?.peekViewDidChangeSize(width, height)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
override fun onAttachedToWindow() {
|
|
42
|
+
super.onAttachedToWindow()
|
|
43
|
+
attachToContainerView()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Peek can be nested anywhere within the content, so it attaches itself to the
|
|
48
|
+
* nearest container instead of being mounted by it. The container also searches
|
|
49
|
+
* for it when the content is added to cover the initial (bottom-up) mount order.
|
|
50
|
+
*/
|
|
51
|
+
private fun attachToContainerView() {
|
|
52
|
+
if (delegate != null) return
|
|
53
|
+
|
|
54
|
+
var parent: ViewParent? = parent
|
|
55
|
+
while (parent != null && parent !is TrueSheetContainerView) {
|
|
56
|
+
parent = parent.parent
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
(parent as? TrueSheetContainerView)?.attachPeekView(this)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Called by the ViewManager when the view is dropped (Fabric unmount)
|
|
64
|
+
*/
|
|
65
|
+
fun detachFromContainerView() {
|
|
66
|
+
(delegate as? TrueSheetContainerView)?.detachPeekView(this)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
companion object {
|
|
70
|
+
const val TAG_NAME = "TrueSheet"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package com.lodev09.truesheet
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
4
|
+
import com.facebook.react.uimanager.PointerEvents
|
|
5
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewGroupManager
|
|
7
|
+
import com.facebook.react.uimanager.annotations.ReactProp
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* ViewManager for TrueSheetPeekView
|
|
11
|
+
* Marks part of the content as the sheet's peek content
|
|
12
|
+
*/
|
|
13
|
+
@ReactModule(name = TrueSheetPeekViewManager.REACT_CLASS)
|
|
14
|
+
class TrueSheetPeekViewManager : ViewGroupManager<TrueSheetPeekView>() {
|
|
15
|
+
|
|
16
|
+
override fun getName(): String = REACT_CLASS
|
|
17
|
+
|
|
18
|
+
override fun createViewInstance(reactContext: ThemedReactContext): TrueSheetPeekView = TrueSheetPeekView(reactContext)
|
|
19
|
+
|
|
20
|
+
override fun onDropViewInstance(view: TrueSheetPeekView) {
|
|
21
|
+
super.onDropViewInstance(view)
|
|
22
|
+
view.detachFromContainerView()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@ReactProp(name = "pointerEvents")
|
|
26
|
+
fun setPointerEvents(view: TrueSheetPeekView, pointerEventsStr: String?) {
|
|
27
|
+
view.pointerEvents = PointerEvents.parsePointerEvents(pointerEventsStr)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
companion object {
|
|
31
|
+
const val REACT_CLASS = "TrueSheetPeekView"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -615,10 +615,16 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
615
615
|
}
|
|
616
616
|
|
|
617
617
|
override fun containerViewFooterDidChangeSize(width: Int, height: Int) {
|
|
618
|
-
// Footer
|
|
618
|
+
// Footer height affects peek detents
|
|
619
|
+
updateSheetIfNeeded()
|
|
619
620
|
viewController.positionFooter()
|
|
620
621
|
}
|
|
621
622
|
|
|
623
|
+
override fun containerViewPeekDidChangeSize(width: Int, height: Int) {
|
|
624
|
+
// Peek content height affects peek detents
|
|
625
|
+
updateSheetIfNeeded()
|
|
626
|
+
}
|
|
627
|
+
|
|
622
628
|
// ==================== RNScreensEventObserverDelegate ====================
|
|
623
629
|
|
|
624
630
|
override fun presenterScreenWillDisappear() {
|
|
@@ -275,6 +275,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
275
275
|
// Cached values used during dismiss when container is unmounted
|
|
276
276
|
private var cachedContentHeight: Int = 0
|
|
277
277
|
private var cachedHeaderHeight: Int = 0
|
|
278
|
+
private var cachedFooterHeight: Int = 0
|
|
279
|
+
private var cachedPeekContentHeight: Int = 0
|
|
278
280
|
|
|
279
281
|
override val contentHeight: Int
|
|
280
282
|
get() = containerView?.contentHeight ?: cachedContentHeight
|
|
@@ -282,6 +284,12 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
282
284
|
override val headerHeight: Int
|
|
283
285
|
get() = containerView?.headerHeight ?: cachedHeaderHeight
|
|
284
286
|
|
|
287
|
+
override val footerHeight: Int
|
|
288
|
+
get() = containerView?.footerHeight ?: cachedFooterHeight
|
|
289
|
+
|
|
290
|
+
override val peekContentHeight: Int
|
|
291
|
+
get() = containerView?.peekContentHeight ?: cachedPeekContentHeight
|
|
292
|
+
|
|
285
293
|
// Insets
|
|
286
294
|
// Target keyboard height used for detent calculations
|
|
287
295
|
override val keyboardInset: Int
|
|
@@ -836,6 +844,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
836
844
|
containerView?.let {
|
|
837
845
|
cachedContentHeight = it.contentHeight
|
|
838
846
|
cachedHeaderHeight = it.headerHeight
|
|
847
|
+
cachedFooterHeight = it.footerHeight
|
|
848
|
+
cachedPeekContentHeight = it.peekContentHeight
|
|
839
849
|
}
|
|
840
850
|
|
|
841
851
|
interactionState = InteractionState.Reconfiguring
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.lodev09.truesheet.core
|
|
2
2
|
|
|
3
|
+
import com.facebook.react.uimanager.PixelUtil.dpToPx
|
|
3
4
|
import com.facebook.react.uimanager.PixelUtil.pxToDp
|
|
4
5
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
5
6
|
import com.facebook.react.util.RNLog
|
|
@@ -14,6 +15,8 @@ interface TrueSheetDetentCalculatorDelegate {
|
|
|
14
15
|
val detents: MutableList<Double>
|
|
15
16
|
val contentHeight: Int
|
|
16
17
|
val headerHeight: Int
|
|
18
|
+
val footerHeight: Int
|
|
19
|
+
val peekContentHeight: Int
|
|
17
20
|
val contentBottomInset: Int
|
|
18
21
|
val maxContentHeight: Int?
|
|
19
22
|
val keyboardInset: Int
|
|
@@ -31,17 +34,31 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
|
|
|
31
34
|
private val detents: List<Double> get() = delegate?.detents ?: emptyList()
|
|
32
35
|
private val contentHeight: Int get() = delegate?.contentHeight ?: 0
|
|
33
36
|
private val headerHeight: Int get() = delegate?.headerHeight ?: 0
|
|
37
|
+
private val footerHeight: Int get() = delegate?.footerHeight ?: 0
|
|
38
|
+
private val peekContentHeight: Int get() = delegate?.peekContentHeight ?: 0
|
|
34
39
|
private val contentBottomInset: Int get() = delegate?.contentBottomInset ?: 0
|
|
35
40
|
private val maxContentHeight: Int? get() = delegate?.maxContentHeight
|
|
36
41
|
private val keyboardInset: Int get() = delegate?.keyboardInset ?: 0
|
|
37
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Height for peek (-2.0) detents: header + footer + peek content height.
|
|
45
|
+
* Falls back to 150dp when none is present.
|
|
46
|
+
*/
|
|
47
|
+
private val peekDetentHeight: Int
|
|
48
|
+
get() {
|
|
49
|
+
val height = headerHeight + footerHeight + peekContentHeight
|
|
50
|
+
return if (height > 0) height else DEFAULT_PEEK_HEIGHT.dpToPx().toInt()
|
|
51
|
+
}
|
|
52
|
+
|
|
38
53
|
/**
|
|
39
54
|
* Calculate the height in pixels for a given detent value.
|
|
40
|
-
* @param detent The detent value: -1.0 for content-fit, or 0.0-1.0 for screen fraction
|
|
55
|
+
* @param detent The detent value: -1.0 for content-fit, -2.0 for peek, or 0.0-1.0 for screen fraction
|
|
41
56
|
*/
|
|
42
57
|
fun getDetentHeight(detent: Double, includeKeyboard: Boolean = true): Int {
|
|
43
58
|
val baseHeight = if (detent == -1.0) {
|
|
44
59
|
contentHeight + headerHeight + contentBottomInset
|
|
60
|
+
} else if (detent == -2.0) {
|
|
61
|
+
peekDetentHeight + contentBottomInset
|
|
45
62
|
} else {
|
|
46
63
|
if (detent <= 0.0 || detent > 1.0) {
|
|
47
64
|
throw IllegalArgumentException("TrueSheet: detent fraction ($detent) must be between 0 and 1")
|
|
@@ -83,6 +100,8 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
|
|
|
83
100
|
val value = detents[index]
|
|
84
101
|
return if (value == -1.0) {
|
|
85
102
|
(contentHeight + headerHeight).toFloat() / screenHeight.toFloat()
|
|
103
|
+
} else if (value == -2.0) {
|
|
104
|
+
peekDetentHeight.toFloat() / screenHeight.toFloat()
|
|
86
105
|
} else {
|
|
87
106
|
value.toFloat()
|
|
88
107
|
}
|
|
@@ -211,4 +230,8 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
|
|
|
211
230
|
val toDetent = getDetentValueForIndex(toIndex)
|
|
212
231
|
return fromDetent + progress * (toDetent - fromDetent)
|
|
213
232
|
}
|
|
233
|
+
|
|
234
|
+
companion object {
|
|
235
|
+
private const val DEFAULT_PEEK_HEIGHT = 150f
|
|
236
|
+
}
|
|
214
237
|
}
|
|
@@ -23,6 +23,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
23
23
|
|
|
24
24
|
@end
|
|
25
25
|
|
|
26
|
+
@class TrueSheetPeekView;
|
|
27
|
+
|
|
26
28
|
@protocol TrueSheetContainerViewDelegate <NSObject>
|
|
27
29
|
|
|
28
30
|
- (void)containerViewContentDidChangeSize:(CGSize)newSize;
|
|
@@ -32,6 +34,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
32
34
|
|
|
33
35
|
- (void)containerViewHeaderDidChangeSize:(CGSize)newSize;
|
|
34
36
|
- (void)containerViewFooterDidChangeSize:(CGSize)newSize;
|
|
37
|
+
- (void)containerViewPeekDidChangeSize:(CGSize)newSize;
|
|
35
38
|
|
|
36
39
|
@end
|
|
37
40
|
|
|
@@ -67,6 +70,21 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
67
70
|
*/
|
|
68
71
|
- (CGFloat)headerHeight;
|
|
69
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Returns the current footer height
|
|
75
|
+
*/
|
|
76
|
+
- (CGFloat)footerHeight;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Returns the current height of the peek view within the content
|
|
80
|
+
*/
|
|
81
|
+
- (CGFloat)peekContentHeight;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Registers a peek view found within the content subtree
|
|
85
|
+
*/
|
|
86
|
+
- (void)attachPeekView:(TrueSheetPeekView *)peekView;
|
|
87
|
+
|
|
70
88
|
/**
|
|
71
89
|
* Updates footer layout constraints if needed
|
|
72
90
|
*/
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
#import "TrueSheetContentView.h"
|
|
14
14
|
#import "TrueSheetFooterView.h"
|
|
15
15
|
#import "TrueSheetHeaderView.h"
|
|
16
|
+
#import "TrueSheetPeekView.h"
|
|
16
17
|
#import "TrueSheetViewController.h"
|
|
17
18
|
#import "core/TrueSheetKeyboardObserver.h"
|
|
18
19
|
#import "utils/UIView+ScrollEdgeInteraction.h"
|
|
@@ -45,13 +46,15 @@ using namespace facebook::react;
|
|
|
45
46
|
|
|
46
47
|
@interface TrueSheetContainerView () <TrueSheetContentViewDelegate,
|
|
47
48
|
TrueSheetHeaderViewDelegate,
|
|
48
|
-
TrueSheetFooterViewDelegate
|
|
49
|
+
TrueSheetFooterViewDelegate,
|
|
50
|
+
TrueSheetPeekViewDelegate>
|
|
49
51
|
@end
|
|
50
52
|
|
|
51
53
|
@implementation TrueSheetContainerView {
|
|
52
54
|
TrueSheetContentView *_contentView;
|
|
53
55
|
TrueSheetHeaderView *_headerView;
|
|
54
56
|
TrueSheetFooterView *_footerView;
|
|
57
|
+
TrueSheetPeekView *__weak _peekView;
|
|
55
58
|
TrueSheetKeyboardObserver *_keyboardObserver;
|
|
56
59
|
BOOL _scrollableSet;
|
|
57
60
|
}
|
|
@@ -119,6 +122,25 @@ using namespace facebook::react;
|
|
|
119
122
|
return _headerView ? _headerView.frame.size.height : 0;
|
|
120
123
|
}
|
|
121
124
|
|
|
125
|
+
- (CGFloat)footerHeight {
|
|
126
|
+
return _footerView ? _footerView.frame.size.height : 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Distance from the top of the content view to the bottom of the peek view.
|
|
130
|
+
// Includes the peek view's offset within the content (padding, views above it)
|
|
131
|
+
// so the peek detent reveals everything down to the peek content's bottom edge.
|
|
132
|
+
- (CGFloat)peekContentHeight {
|
|
133
|
+
if (!_peekView) {
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!_contentView || ![_peekView isDescendantOfView:_contentView]) {
|
|
138
|
+
return _peekView.frame.size.height;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return CGRectGetMaxY([_peekView convertRect:_peekView.bounds toView:_contentView]);
|
|
142
|
+
}
|
|
143
|
+
|
|
122
144
|
- (void)layoutFooter {
|
|
123
145
|
if (_footerView) {
|
|
124
146
|
CGFloat height = _footerView.frame.size.height;
|
|
@@ -192,6 +214,13 @@ using namespace facebook::react;
|
|
|
192
214
|
}
|
|
193
215
|
_contentView = (TrueSheetContentView *)childComponentView;
|
|
194
216
|
_contentView.delegate = self;
|
|
217
|
+
|
|
218
|
+
// Children mount bottom-up, so the content subtree is complete here.
|
|
219
|
+
// Late-mounted peek views attach themselves instead (see TrueSheetPeekView).
|
|
220
|
+
TrueSheetPeekView *peekView = [self findPeekViewIn:_contentView];
|
|
221
|
+
if (peekView) {
|
|
222
|
+
[self attachPeekView:peekView];
|
|
223
|
+
}
|
|
195
224
|
}
|
|
196
225
|
|
|
197
226
|
if ([childComponentView isKindOfClass:[TrueSheetHeaderView class]]) {
|
|
@@ -211,6 +240,7 @@ using namespace facebook::react;
|
|
|
211
240
|
}
|
|
212
241
|
_footerView = (TrueSheetFooterView *)childComponentView;
|
|
213
242
|
_footerView.delegate = self;
|
|
243
|
+
[self footerViewDidChangeSize:_footerView.frame.size];
|
|
214
244
|
}
|
|
215
245
|
}
|
|
216
246
|
|
|
@@ -229,6 +259,7 @@ using namespace facebook::react;
|
|
|
229
259
|
if ([childComponentView isKindOfClass:[TrueSheetFooterView class]]) {
|
|
230
260
|
_footerView.delegate = nil;
|
|
231
261
|
_footerView = nil;
|
|
262
|
+
[self footerViewDidChangeSize:CGSizeZero];
|
|
232
263
|
}
|
|
233
264
|
|
|
234
265
|
[super unmountChildComponentView:childComponentView index:index];
|
|
@@ -272,6 +303,52 @@ using namespace facebook::react;
|
|
|
272
303
|
}
|
|
273
304
|
}
|
|
274
305
|
|
|
306
|
+
#pragma mark - TrueSheetPeekViewDelegate
|
|
307
|
+
|
|
308
|
+
- (nullable TrueSheetPeekView *)findPeekViewIn:(UIView *)view {
|
|
309
|
+
if ([view isKindOfClass:[TrueSheetPeekView class]]) {
|
|
310
|
+
return (TrueSheetPeekView *)view;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for (UIView *subview in view.subviews) {
|
|
314
|
+
TrueSheetPeekView *found = [self findPeekViewIn:subview];
|
|
315
|
+
if (found) {
|
|
316
|
+
return found;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return nil;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
- (void)attachPeekView:(TrueSheetPeekView *)peekView {
|
|
324
|
+
if (_peekView == peekView) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (_peekView != nil) {
|
|
329
|
+
RCTLogWarn(@"TrueSheet: Sheet can only have one peek component.");
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
_peekView = peekView;
|
|
334
|
+
peekView.delegate = self;
|
|
335
|
+
[self peekViewDidChangeSize:peekView.frame.size];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
- (void)peekViewDidChangeSize:(CGSize)newSize {
|
|
339
|
+
[self.delegate containerViewPeekDidChangeSize:newSize];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
- (void)peekViewWillDetach:(TrueSheetPeekView *)peekView {
|
|
343
|
+
if (_peekView != peekView) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
_peekView.delegate = nil;
|
|
348
|
+
_peekView = nil;
|
|
349
|
+
[self peekViewDidChangeSize:CGSizeZero];
|
|
350
|
+
}
|
|
351
|
+
|
|
275
352
|
#pragma mark - Keyboard Observer
|
|
276
353
|
|
|
277
354
|
- (void)setupKeyboardObserverWithViewController:(UIViewController *)viewController {
|