@lodev09/react-native-true-sheet 4.0.0-beta.3 → 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 +50 -7
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +101 -18
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +24 -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/ios/TrueSheetContainerView.h +13 -5
- package/ios/TrueSheetContainerView.mm +6 -6
- package/ios/TrueSheetContentView.h +31 -10
- package/ios/TrueSheetContentView.mm +137 -162
- package/ios/TrueSheetFooterView.mm +17 -0
- package/ios/TrueSheetView.mm +13 -4
- package/ios/utils/UIView+ScrollEdgeInteraction.h +1 -0
- package/ios/utils/UIView+ScrollEdgeInteraction.mm +24 -2
- package/lib/module/TrueSheet.js +37 -9
- 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 +2 -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 +34 -8
- 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
|
@@ -304,6 +304,12 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
304
304
|
setupScrollable()
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
fun setScrollableHandle(handle: Int) {
|
|
308
|
+
if (viewController.scrollableHandle == handle) return
|
|
309
|
+
viewController.scrollableHandle = handle
|
|
310
|
+
setupScrollable()
|
|
311
|
+
}
|
|
312
|
+
|
|
307
313
|
fun setAbsoluteHeader(absolute: Boolean) {
|
|
308
314
|
viewController.absoluteHeader = absolute
|
|
309
315
|
}
|
|
@@ -319,9 +325,9 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
319
325
|
|
|
320
326
|
private fun setupScrollable() {
|
|
321
327
|
viewController.containerView?.let {
|
|
322
|
-
it.insetAdjustment = viewController.insetAdjustment
|
|
323
|
-
it.scrollViewBottomInset = viewController.contentBottomInset
|
|
324
328
|
it.scrollableOptions = viewController.scrollableOptions
|
|
329
|
+
it.scrollableHandle = viewController.scrollableHandle
|
|
330
|
+
it.scrollableBottomInset = viewController.contentBottomInset
|
|
325
331
|
it.hasAutoDetent = viewController.detents.contains(-1.0)
|
|
326
332
|
it.footerBottomInset = viewController.contentBottomInset
|
|
327
333
|
it.absoluteFooter = viewController.absoluteFooter
|
|
@@ -486,7 +492,12 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
486
492
|
if (viewController.containerView == null) return@post
|
|
487
493
|
|
|
488
494
|
viewController.setupSheetDetentsForSizeChange()
|
|
489
|
-
|
|
495
|
+
// While settling, onSlide drives the parent translation in realtime —
|
|
496
|
+
// an animated update here would race it frame-by-frame, staggering the
|
|
497
|
+
// emitted position
|
|
498
|
+
if (!viewController.isSettling) {
|
|
499
|
+
TrueSheetStackManager.updateParentTranslation(this)
|
|
500
|
+
}
|
|
490
501
|
}
|
|
491
502
|
}
|
|
492
503
|
|
|
@@ -514,14 +525,46 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
514
525
|
}
|
|
515
526
|
}
|
|
516
527
|
|
|
528
|
+
/**
|
|
529
|
+
* Realtime counterpart of updateTranslationForChild, driven by the child's
|
|
530
|
+
* onSlide frames so stacked sheets move in lockstep instead of on
|
|
531
|
+
* independently-timed animations.
|
|
532
|
+
*/
|
|
533
|
+
fun syncTranslationForChild(childSheetTop: Int, childMinSheetTop: Int) {
|
|
534
|
+
if (!viewController.isSheetVisible || viewController.isExpanded) return
|
|
535
|
+
|
|
536
|
+
val mySheetTop = viewController.detentCalculator.getSheetTopForDetentIndex(viewController.currentDetentIndex)
|
|
537
|
+
val targetTranslation = maxOf(0, childMinSheetTop - mySheetTop)
|
|
538
|
+
val actualTranslation = maxOf(0, childSheetTop - mySheetTop)
|
|
539
|
+
val currentTranslation = viewController.currentTranslationY
|
|
540
|
+
|
|
541
|
+
// Ride the child's actual position, but only between the current
|
|
542
|
+
// translation and the settled target — tracks the child frame-by-frame
|
|
543
|
+
// when the alignment changes (auto detent resize) while staying put when
|
|
544
|
+
// the child moves between detents above its minimum. Returning early on
|
|
545
|
+
// no-ops avoids cancelling an in-flight realign animation.
|
|
546
|
+
val newTranslation = actualTranslation.coerceIn(
|
|
547
|
+
minOf(currentTranslation, targetTranslation),
|
|
548
|
+
maxOf(currentTranslation, targetTranslation)
|
|
549
|
+
)
|
|
550
|
+
if (newTranslation == currentTranslation) return
|
|
551
|
+
|
|
552
|
+
viewController.translateSheet(newTranslation, animated = false)
|
|
553
|
+
|
|
554
|
+
val additionalTranslation = newTranslation - currentTranslation
|
|
555
|
+
if (additionalTranslation > 0) {
|
|
556
|
+
TrueSheetStackManager.getParentSheet(this)?.addTranslation(additionalTranslation, animated = false)
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
517
560
|
/**
|
|
518
561
|
* Recursively adds translation to this sheet and all parent sheets.
|
|
519
562
|
*/
|
|
520
|
-
private fun addTranslation(amount: Int) {
|
|
563
|
+
private fun addTranslation(amount: Int, animated: Boolean = true) {
|
|
521
564
|
if (viewController.isExpanded) return
|
|
522
565
|
|
|
523
|
-
viewController.translateSheet(viewController.currentTranslationY + amount)
|
|
524
|
-
TrueSheetStackManager.getParentSheet(this)?.addTranslation(amount)
|
|
566
|
+
viewController.translateSheet(viewController.currentTranslationY + amount, animated)
|
|
567
|
+
TrueSheetStackManager.getParentSheet(this)?.addTranslation(amount, animated)
|
|
525
568
|
}
|
|
526
569
|
|
|
527
570
|
/**
|
|
@@ -643,7 +686,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
643
686
|
viewController.commitKeyboardDetent()
|
|
644
687
|
}
|
|
645
688
|
|
|
646
|
-
// When the ScrollView changes (e.g. conditional remount), re-
|
|
689
|
+
// When the ScrollView changes (e.g. conditional remount), re-detect the new ScrollView
|
|
647
690
|
// and request layout so BottomSheetBehavior re-discovers the nested scrolling child.
|
|
648
691
|
override fun containerViewScrollViewDidChange() {
|
|
649
692
|
setupScrollable()
|
|
@@ -37,6 +37,7 @@ import com.lodev09.truesheet.core.TrueSheetDimViewDelegate
|
|
|
37
37
|
import com.lodev09.truesheet.core.TrueSheetKeyboardObserver
|
|
38
38
|
import com.lodev09.truesheet.core.TrueSheetKeyboardObserverDelegate
|
|
39
39
|
import com.lodev09.truesheet.core.TrueSheetStackManager
|
|
40
|
+
import com.lodev09.truesheet.utils.Insets
|
|
40
41
|
import com.lodev09.truesheet.utils.KeyboardUtils
|
|
41
42
|
import com.lodev09.truesheet.utils.ScreenUtils
|
|
42
43
|
import com.lodev09.truesheet.utils.TouchEventDeduper
|
|
@@ -162,6 +163,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
162
163
|
private var pendingDetentIndex: Int = -1
|
|
163
164
|
|
|
164
165
|
private var interactionState: InteractionState = InteractionState.Idle
|
|
166
|
+
|
|
167
|
+
// A content/header/footer size change arrived mid-drag — reconfiguring then
|
|
168
|
+
// would reset behavior state and kill the gesture, so it's applied on settle.
|
|
169
|
+
private var hasPendingSizeChange = false
|
|
165
170
|
internal var isBeingDismissed = false
|
|
166
171
|
private set
|
|
167
172
|
var wasHiddenByScreen = false
|
|
@@ -227,6 +232,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
227
232
|
behavior?.scrollingExpandsSheet = value?.scrollingExpandsSheet ?: true
|
|
228
233
|
}
|
|
229
234
|
|
|
235
|
+
// React tag of the user-provided scrollable within the content (see the scrollableRef prop)
|
|
236
|
+
var scrollableHandle: Int = -1
|
|
237
|
+
|
|
230
238
|
override var sheetCornerRadius: Float = DEFAULT_CORNER_RADIUS.dpToPx()
|
|
231
239
|
set(value) {
|
|
232
240
|
field = if (value < 0) DEFAULT_CORNER_RADIUS.dpToPx() else value
|
|
@@ -270,9 +278,29 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
270
278
|
val screenWidth: Int
|
|
271
279
|
get() = ScreenUtils.getScreenWidth(reactContext)
|
|
272
280
|
|
|
281
|
+
// Cached per layout/configuration pass — the WindowManager and
|
|
282
|
+
// rootWindowInsets queries behind these allocate, and they're read many
|
|
283
|
+
// times per animation frame (position emission, detent interpolation,
|
|
284
|
+
// stacked sheet sync)
|
|
285
|
+
private var cachedRealScreenHeight: Int = 0
|
|
286
|
+
private var cachedInsets: Insets? = null
|
|
287
|
+
|
|
288
|
+
private fun invalidateScreenMetrics() {
|
|
289
|
+
cachedRealScreenHeight = 0
|
|
290
|
+
cachedInsets = null
|
|
291
|
+
}
|
|
292
|
+
|
|
273
293
|
// Includes system bars for accurate positioning
|
|
274
294
|
override val realScreenHeight: Int
|
|
275
|
-
get()
|
|
295
|
+
get() {
|
|
296
|
+
if (cachedRealScreenHeight == 0) {
|
|
297
|
+
cachedRealScreenHeight = ScreenUtils.getRealScreenHeight(reactContext)
|
|
298
|
+
}
|
|
299
|
+
return cachedRealScreenHeight
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private val insets: Insets
|
|
303
|
+
get() = cachedInsets ?: ScreenUtils.getInsets(reactContext).also { cachedInsets = it }
|
|
276
304
|
|
|
277
305
|
// Content Measurements
|
|
278
306
|
// Cached values used during dismiss when container is unmounted
|
|
@@ -315,10 +343,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
315
343
|
}
|
|
316
344
|
|
|
317
345
|
val bottomInset: Int
|
|
318
|
-
get() = if (edgeToEdgeEnabled)
|
|
346
|
+
get() = if (edgeToEdgeEnabled) insets.bottom else 0
|
|
319
347
|
|
|
320
348
|
override val topInset: Int
|
|
321
|
-
get() = if (edgeToEdgeEnabled)
|
|
349
|
+
get() = if (edgeToEdgeEnabled) insets.top else 0
|
|
322
350
|
|
|
323
351
|
override val contentBottomInset: Int
|
|
324
352
|
get() = if (insetAdjustment == TrueSheetInsetAdjustment.AUTOMATIC) bottomInset else 0
|
|
@@ -331,6 +359,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
331
359
|
}
|
|
332
360
|
|
|
333
361
|
// Sheet State
|
|
362
|
+
val isSettling: Boolean
|
|
363
|
+
get() = behavior?.state == BottomSheetBehavior.STATE_SETTLING
|
|
364
|
+
|
|
334
365
|
val isExpanded: Boolean
|
|
335
366
|
get() {
|
|
336
367
|
val sheetTop = sheetView?.top ?: return false
|
|
@@ -392,6 +423,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
392
423
|
sheetView = null
|
|
393
424
|
|
|
394
425
|
interactionState = InteractionState.Idle
|
|
426
|
+
hasPendingSizeChange = false
|
|
395
427
|
isBeingDismissed = false
|
|
396
428
|
isPresented = false
|
|
397
429
|
isSheetVisible = false
|
|
@@ -440,6 +472,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
440
472
|
// =============================================================================
|
|
441
473
|
|
|
442
474
|
override fun coordinatorLayoutDidLayout(changed: Boolean) {
|
|
475
|
+
if (changed) invalidateScreenMetrics()
|
|
476
|
+
|
|
443
477
|
// Reposition footer when layout changes
|
|
444
478
|
if (isPresented && changed) {
|
|
445
479
|
positionFooter()
|
|
@@ -447,6 +481,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
447
481
|
}
|
|
448
482
|
|
|
449
483
|
override fun coordinatorLayoutDidChangeConfiguration() {
|
|
484
|
+
invalidateScreenMetrics()
|
|
450
485
|
if (!isPresented) return
|
|
451
486
|
|
|
452
487
|
sheetView?.updateGravity()
|
|
@@ -570,6 +605,14 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
570
605
|
|
|
571
606
|
emitChangePositionDelegate(sheetView.top)
|
|
572
607
|
|
|
608
|
+
// Drive the parent's stack translation from our actual frame position —
|
|
609
|
+
// an independently-timed animation lags behind (e.g. auto detent shrink)
|
|
610
|
+
// and reveals the parent above this sheet's top edge. Skipped while
|
|
611
|
+
// dragging so the parent stays put during drag-to-dismiss.
|
|
612
|
+
if (!isPresentAnimating && interactionState !is InteractionState.Dragging) {
|
|
613
|
+
syncParentTranslation(sheetView.top)
|
|
614
|
+
}
|
|
615
|
+
|
|
573
616
|
// Position on every slide frame — during keyboard transitions the IME
|
|
574
617
|
// animation callbacks are sparser than the sheet's settle frames, and the
|
|
575
618
|
// footer rides the sheet between them, jittering against the keyboard.
|
|
@@ -589,7 +632,15 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
589
632
|
// whether settled at the target, interrupted by drag, or superseded.
|
|
590
633
|
pendingDetentIndex = -1
|
|
591
634
|
|
|
592
|
-
val index = detentCalculator.getDetentIndexForState(newState) ?:
|
|
635
|
+
val index = detentCalculator.getDetentIndexForState(newState) ?: run {
|
|
636
|
+
// Unmapped settle — end any drag anyway so deferred size changes aren't
|
|
637
|
+
// dropped forever (a leaked Dragging state skips every reconfigure)
|
|
638
|
+
if (interactionState is InteractionState.Dragging) {
|
|
639
|
+
interactionState = InteractionState.Idle
|
|
640
|
+
}
|
|
641
|
+
flushPendingSizeChange()
|
|
642
|
+
return
|
|
643
|
+
}
|
|
593
644
|
val position = getPositionDpForView(sheetView)
|
|
594
645
|
val detentInfo = DetentInfo(index, position)
|
|
595
646
|
|
|
@@ -637,6 +688,14 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
637
688
|
}
|
|
638
689
|
|
|
639
690
|
TrueSheetStackManager.updateBackgroundAccessibility()
|
|
691
|
+
|
|
692
|
+
flushPendingSizeChange()
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
private fun flushPendingSizeChange() {
|
|
696
|
+
if (!hasPendingSizeChange) return
|
|
697
|
+
hasPendingSizeChange = false
|
|
698
|
+
setupSheetDetentsForSizeChange()
|
|
640
699
|
}
|
|
641
700
|
|
|
642
701
|
// =============================================================================
|
|
@@ -698,6 +757,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
698
757
|
val sheet = this.sheetView ?: return
|
|
699
758
|
if (isPresented) return
|
|
700
759
|
|
|
760
|
+
invalidateScreenMetrics()
|
|
701
761
|
shouldAnimatePresent = animated
|
|
702
762
|
currentDetentIndex = detentIndex
|
|
703
763
|
interactionState = InteractionState.Idle
|
|
@@ -922,10 +982,15 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
922
982
|
}
|
|
923
983
|
|
|
924
984
|
fun setupSheetDetentsForSizeChange() {
|
|
925
|
-
//
|
|
985
|
+
// Defer while dragging — the size change is driven by the drag itself (the
|
|
926
986
|
// container tracks the sheet's visible height), and reconfiguring resets
|
|
927
|
-
// behavior state, killing the gesture.
|
|
928
|
-
|
|
987
|
+
// behavior state, killing the gesture. Applied on settle so a real content
|
|
988
|
+
// change mid-drag (e.g. items removed from a list) isn't lost.
|
|
989
|
+
if (interactionState is InteractionState.Dragging) {
|
|
990
|
+
hasPendingSizeChange = true
|
|
991
|
+
return
|
|
992
|
+
}
|
|
993
|
+
hasPendingSizeChange = false
|
|
929
994
|
|
|
930
995
|
setupSheetDetents()
|
|
931
996
|
positionFooter()
|
|
@@ -1011,8 +1076,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1011
1076
|
val footerView = containerView?.footerView ?: return
|
|
1012
1077
|
val sheet = sheetView ?: return
|
|
1013
1078
|
|
|
1014
|
-
val keyboardShift = if (currentKeyboardInset > 0) maxOf(0, currentKeyboardInset + footerKeyboardOffset) else 0
|
|
1015
|
-
|
|
1016
1079
|
// A relative footer is laid out by Yoga (pinned to the container's bottom
|
|
1017
1080
|
// edge) and stays in the layout flow behind the keyboard — only an
|
|
1018
1081
|
// absolute footer floats above it
|
|
@@ -1021,6 +1084,15 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1021
1084
|
return
|
|
1022
1085
|
}
|
|
1023
1086
|
|
|
1087
|
+
// The footer's absorbed safe-area inset rides below the keyboard's top
|
|
1088
|
+
// edge — its content stays flush with the keyboard without dropping the
|
|
1089
|
+
// inset padding from the layout, which would jump at the transition edges
|
|
1090
|
+
val keyboardShift = if (currentKeyboardInset > 0) {
|
|
1091
|
+
maxOf(0, currentKeyboardInset + footerKeyboardOffset - footerView.bottomInset)
|
|
1092
|
+
} else {
|
|
1093
|
+
0
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1024
1096
|
val footerHeight = footerView.height
|
|
1025
1097
|
val sheetHeight = sheet.height
|
|
1026
1098
|
val sheetTop = sheet.top
|
|
@@ -1079,9 +1151,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1079
1151
|
delegate = object : TrueSheetKeyboardObserverDelegate {
|
|
1080
1152
|
override fun keyboardWillShow(height: Int) {
|
|
1081
1153
|
if (!shouldHandleKeyboard()) return
|
|
1082
|
-
// An absolute footer rises above the keyboard, so it drops the bottom
|
|
1083
|
-
// inset padding — before detents are configured against its height
|
|
1084
|
-
if (absoluteFooter) containerView?.footerView?.keyboardVisible = true
|
|
1085
1154
|
// If a resize is in flight, restore to its target — not the stale current
|
|
1086
1155
|
detentIndexBeforeKeyboard = if (pendingDetentIndex >= 0) pendingDetentIndex else currentDetentIndex
|
|
1087
1156
|
pendingDetentIndex = -1
|
|
@@ -1094,7 +1163,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1094
1163
|
|
|
1095
1164
|
override fun keyboardWillHide() {
|
|
1096
1165
|
if (!shouldHandleKeyboard(checkFocus = false)) return
|
|
1097
|
-
containerView?.footerView?.keyboardVisible = false
|
|
1098
1166
|
val restoring = !isBeingDismissed && detentIndexBeforeKeyboard >= 0
|
|
1099
1167
|
|
|
1100
1168
|
// Skip reconfigure during interactive keyboard dismiss (e.g. keyboardDismissMode="on-drag")
|
|
@@ -1119,7 +1187,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1119
1187
|
|
|
1120
1188
|
override fun keyboardDidHide() {
|
|
1121
1189
|
if (!shouldHandleKeyboard(checkFocus = false)) return
|
|
1122
|
-
containerView?.footerView?.keyboardVisible = false
|
|
1123
1190
|
detentIndexBeforeKeyboard = -1
|
|
1124
1191
|
isKeyboardDismissProgrammatic = false
|
|
1125
1192
|
setupSheetDetents(applyState = false)
|
|
@@ -1141,7 +1208,6 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1141
1208
|
// Handle case where keyboard is already visible and focus moves into the sheet
|
|
1142
1209
|
if (!shouldHandleKeyboard()) return
|
|
1143
1210
|
if (detentIndexBeforeKeyboard < 0 && (keyboardObserver?.currentHeight ?: 0) > 0) {
|
|
1144
|
-
if (absoluteFooter) containerView?.footerView?.keyboardVisible = true
|
|
1145
1211
|
detentIndexBeforeKeyboard = currentDetentIndex
|
|
1146
1212
|
currentDetentIndex = detents.size - 1
|
|
1147
1213
|
setupSheetDetents()
|
|
@@ -1256,8 +1322,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1256
1322
|
lastEmittedPositionPx = currentTop
|
|
1257
1323
|
val visibleHeight = realScreenHeight - currentTop
|
|
1258
1324
|
val position = detentCalculator.getPositionDp(visibleHeight)
|
|
1259
|
-
val interpolatedIndex = detentCalculator.
|
|
1260
|
-
val detent = detentCalculator.getInterpolatedDetentForPosition(currentTop)
|
|
1325
|
+
val (interpolatedIndex, detent) = detentCalculator.getInterpolatedPositionInfo(currentTop)
|
|
1261
1326
|
delegate?.viewControllerDidChangePosition(interpolatedIndex, position, detent, realtime)
|
|
1262
1327
|
}
|
|
1263
1328
|
|
|
@@ -1336,9 +1401,27 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1336
1401
|
delegate?.viewControllerDidChangeSize(width, height)
|
|
1337
1402
|
}
|
|
1338
1403
|
|
|
1339
|
-
|
|
1404
|
+
/**
|
|
1405
|
+
* Realtime (non-animated) counterpart of TrueSheetStackManager.updateParentTranslation,
|
|
1406
|
+
* driven by onSlide so the parent tracks this sheet's actual position.
|
|
1407
|
+
*/
|
|
1408
|
+
private fun syncParentTranslation(sheetTop: Int) {
|
|
1409
|
+
val parent = parentSheetView ?: return
|
|
1410
|
+
val minSheetTop = detentCalculator.getSheetTopForDetentIndex(0)
|
|
1411
|
+
parent.syncTranslationForChild(sheetTop, minSheetTop)
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
fun translateSheet(translationY: Int, animated: Boolean = true, onEnd: (() -> Unit)? = null) {
|
|
1340
1415
|
val sheet = sheetView ?: return
|
|
1341
1416
|
|
|
1417
|
+
if (!animated) {
|
|
1418
|
+
sheet.animate().cancel()
|
|
1419
|
+
sheet.translationY = translationY.toFloat()
|
|
1420
|
+
emitChangePositionDelegate(sheet.top + translationY)
|
|
1421
|
+
onEnd?.invoke()
|
|
1422
|
+
return
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1342
1425
|
sheet.animate()
|
|
1343
1426
|
.translationY(translationY.toFloat())
|
|
1344
1427
|
.setDuration(TRANSLATE_ANIMATION_DURATION)
|
|
@@ -5,6 +5,7 @@ import com.facebook.react.bridge.ReadableArray
|
|
|
5
5
|
import com.facebook.react.bridge.ReadableMap
|
|
6
6
|
import com.facebook.react.module.annotations.ReactModule
|
|
7
7
|
import com.facebook.react.uimanager.PixelUtil.dpToPx
|
|
8
|
+
import com.facebook.react.uimanager.PointerEvents
|
|
8
9
|
import com.facebook.react.uimanager.ReactStylesDiffMap
|
|
9
10
|
import com.facebook.react.uimanager.StateWrapper
|
|
10
11
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
@@ -55,6 +56,20 @@ class TrueSheetViewManager :
|
|
|
55
56
|
|
|
56
57
|
override fun getDelegate(): ViewManagerDelegate<TrueSheetView> = delegate
|
|
57
58
|
|
|
59
|
+
// The codegen delegate drops props outside the spec (pointerEvents comes from the
|
|
60
|
+
// host's style), so apply it here after the delegate pass.
|
|
61
|
+
override fun updateProperties(viewToUpdate: TrueSheetView, props: ReactStylesDiffMap) {
|
|
62
|
+
super.updateProperties(viewToUpdate, props)
|
|
63
|
+
if (props.hasKey("pointerEvents")) {
|
|
64
|
+
setPointerEvents(viewToUpdate, props.getString("pointerEvents"))
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@ReactProp(name = "pointerEvents")
|
|
69
|
+
fun setPointerEvents(view: TrueSheetView, pointerEventsStr: String?) {
|
|
70
|
+
view.pointerEvents = PointerEvents.parsePointerEvents(pointerEventsStr)
|
|
71
|
+
}
|
|
72
|
+
|
|
58
73
|
/**
|
|
59
74
|
* Export custom direct event types for Fabric
|
|
60
75
|
* Uses Kotlin native collections with decoupled event classes
|
|
@@ -232,6 +247,11 @@ class TrueSheetViewManager :
|
|
|
232
247
|
view.setSheetElevation(elevation.toFloat())
|
|
233
248
|
}
|
|
234
249
|
|
|
250
|
+
@ReactProp(name = "scrollableHandle", defaultInt = -1)
|
|
251
|
+
override fun setScrollableHandle(view: TrueSheetView, handle: Int) {
|
|
252
|
+
view.setScrollableHandle(handle)
|
|
253
|
+
}
|
|
254
|
+
|
|
235
255
|
@ReactProp(name = "scrollableOptions")
|
|
236
256
|
override fun setScrollableOptions(view: TrueSheetView, options: ReadableMap?) {
|
|
237
257
|
if (options == null) {
|
|
@@ -241,7 +261,10 @@ class TrueSheetViewManager :
|
|
|
241
261
|
|
|
242
262
|
val scrollableOptions = ScrollableOptions(
|
|
243
263
|
keyboardScrollOffset = if (options.hasKey("keyboardScrollOffset")) options.getDouble("keyboardScrollOffset").toFloat() else 0f,
|
|
244
|
-
|
|
264
|
+
keyboardOffset = if (options.hasKey("keyboardOffset")) options.getDouble("keyboardOffset").toFloat() else 0f,
|
|
265
|
+
scrollingExpandsSheet = if (options.hasKey("scrollingExpandsSheet")) options.getBoolean("scrollingExpandsSheet") else true,
|
|
266
|
+
contentInsetAdjustment = !options.hasKey("contentInsetAdjustmentBehavior") ||
|
|
267
|
+
options.getBoolean("contentInsetAdjustmentBehavior")
|
|
245
268
|
)
|
|
246
269
|
view.setScrollableOptions(scrollableOptions)
|
|
247
270
|
}
|
|
@@ -57,7 +57,7 @@ class TrueSheetCoordinatorLayout(context: Context) :
|
|
|
57
57
|
private var streamGestureClaimed = false
|
|
58
58
|
|
|
59
59
|
// Whether the sheet owns vertical drags for this stream: the touch target is inside
|
|
60
|
-
// the sheet but outside the
|
|
60
|
+
// the sheet but outside the detected scrollable (e.g. a header overlaying it), with
|
|
61
61
|
// nothing vertically scrollable along its chain. Such targets consume the stream
|
|
62
62
|
// natively (no nested scroll), and BottomSheetBehavior won't intercept because the
|
|
63
63
|
// scrollable's bounds still contain the point (touchingScrollingChild) — intercept
|
|
@@ -127,7 +127,7 @@ class TrueSheetCoordinatorLayout(context: Context) :
|
|
|
127
127
|
|
|
128
128
|
/**
|
|
129
129
|
* Resolves the stream's touch target (respecting zIndex and pointerEvents) and
|
|
130
|
-
* decides whether the sheet may drag from it — inside the sheet, outside the
|
|
130
|
+
* decides whether the sheet may drag from it — inside the sheet, outside the detected
|
|
131
131
|
* scrollable, and nothing along the target's chain scrolls vertically.
|
|
132
132
|
*/
|
|
133
133
|
private fun updateStreamTargetFlags(ev: MotionEvent) {
|
|
@@ -139,7 +139,7 @@ class TrueSheetCoordinatorLayout(context: Context) :
|
|
|
139
139
|
.firstNotNullOfOrNull { it.getView() } ?: return
|
|
140
140
|
if (target !== sheet && !target.isDescendantOf(sheet)) return
|
|
141
141
|
|
|
142
|
-
// A
|
|
142
|
+
// A detected scrollable that can scroll (or hosts pull-to-refresh) owns its
|
|
143
143
|
// touches — nested scrolling drags the sheet. When it can't scroll (e.g.
|
|
144
144
|
// the sheet is fully expanded and the content fits) it eats the gesture
|
|
145
145
|
// instead, so fall through and let the sheet drag from it — an EditText
|
|
@@ -169,8 +169,12 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
|
|
|
169
169
|
*/
|
|
170
170
|
private fun getDetentStateMap(): Map<Int, Int>? =
|
|
171
171
|
when (detents.size) {
|
|
172
|
+
// With one detent halfExpandedRatio matches the peek height, so the
|
|
173
|
+
// behavior can settle HALF_EXPANDED at the same position (e.g. an upward
|
|
174
|
+
// drag release) — map it too, or the settle is unmappable
|
|
172
175
|
1 -> mapOf(
|
|
173
176
|
BottomSheetBehavior.STATE_COLLAPSED to 0,
|
|
177
|
+
BottomSheetBehavior.STATE_HALF_EXPANDED to 0,
|
|
174
178
|
BottomSheetBehavior.STATE_EXPANDED to 0
|
|
175
179
|
)
|
|
176
180
|
|
|
@@ -197,7 +201,7 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
|
|
|
197
201
|
* Find which segment the position falls into for interpolation.
|
|
198
202
|
* @return Triple(fromIndex, toIndex, progress) where progress is 0-1, or null if no detents
|
|
199
203
|
*/
|
|
200
|
-
fun findSegmentForPosition(positionPx: Int): Triple<Int, Int, Float>? {
|
|
204
|
+
private fun findSegmentForPosition(positionPx: Int): Triple<Int, Int, Float>? {
|
|
201
205
|
val count = detents.size
|
|
202
206
|
if (count == 0) return null
|
|
203
207
|
|
|
@@ -238,37 +242,23 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
|
|
|
238
242
|
}
|
|
239
243
|
|
|
240
244
|
/**
|
|
241
|
-
* Returns continuous index (e.g., 0.5 = halfway between detent
|
|
245
|
+
* Returns interpolated continuous index (e.g., 0.5 = halfway between detent
|
|
246
|
+
* 0 and 1) and screen fraction for a position, sharing a single segment
|
|
247
|
+
* lookup — this runs on every position frame.
|
|
242
248
|
*/
|
|
243
|
-
fun
|
|
244
|
-
|
|
245
|
-
if (count == 0) return -1f
|
|
246
|
-
|
|
247
|
-
val segment = findSegmentForPosition(positionPx) ?: return 0f
|
|
248
|
-
val (fromIndex, _, progress) = segment
|
|
249
|
-
|
|
250
|
-
if (fromIndex == -1) return -progress
|
|
251
|
-
return fromIndex + progress
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Returns interpolated screen fraction for position.
|
|
256
|
-
*/
|
|
257
|
-
fun getInterpolatedDetentForPosition(positionPx: Int): Float {
|
|
258
|
-
val count = detents.size
|
|
259
|
-
if (count == 0) return 0f
|
|
249
|
+
fun getInterpolatedPositionInfo(positionPx: Int): Pair<Float, Float> {
|
|
250
|
+
if (detents.isEmpty()) return Pair(-1f, 0f)
|
|
260
251
|
|
|
261
|
-
val
|
|
262
|
-
|
|
252
|
+
val (fromIndex, toIndex, progress) = findSegmentForPosition(positionPx)
|
|
253
|
+
?: return Pair(0f, getDetentValueForIndex(0))
|
|
263
254
|
|
|
264
255
|
if (fromIndex == -1) {
|
|
265
|
-
|
|
266
|
-
return maxOf(0f, firstDetent * (1 - progress))
|
|
256
|
+
return Pair(-progress, maxOf(0f, getDetentValueForIndex(0) * (1 - progress)))
|
|
267
257
|
}
|
|
268
258
|
|
|
269
259
|
val fromDetent = getDetentValueForIndex(fromIndex)
|
|
270
260
|
val toDetent = getDetentValueForIndex(toIndex)
|
|
271
|
-
return fromDetent + progress * (toDetent - fromDetent)
|
|
261
|
+
return Pair(fromIndex + progress, fromDetent + progress * (toDetent - fromDetent))
|
|
272
262
|
}
|
|
273
263
|
|
|
274
264
|
companion object {
|
|
@@ -15,13 +15,6 @@ fun View.isDescendantOf(ancestor: View): Boolean {
|
|
|
15
15
|
return false
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
fun ViewGroup.smoothScrollBy(dx: Int, dy: Int) {
|
|
19
|
-
when (this) {
|
|
20
|
-
is ScrollView -> smoothScrollBy(dx, dy)
|
|
21
|
-
is NestedScrollView -> smoothScrollBy(dx, dy)
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
18
|
fun ViewGroup.smoothScrollTo(x: Int, y: Int) {
|
|
26
19
|
when (this) {
|
|
27
20
|
is ScrollView -> smoothScrollTo(x, y)
|
package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
#include "TrueSheetContentViewShadowNode.h"
|
|
2
2
|
|
|
3
|
+
#include <limits>
|
|
4
|
+
|
|
3
5
|
#include <react/renderer/components/view/conversions.h>
|
|
6
|
+
#include <react/renderer/core/LayoutConstraints.h>
|
|
7
|
+
#include <react/renderer/core/LayoutContext.h>
|
|
4
8
|
|
|
5
9
|
namespace facebook::react {
|
|
6
10
|
|
|
@@ -8,6 +12,10 @@ using namespace yoga;
|
|
|
8
12
|
|
|
9
13
|
extern const char TrueSheetContentViewComponentName[] = "TrueSheetContentView";
|
|
10
14
|
|
|
15
|
+
// measure() lays out a clone of this node, which re-enters layout() on the
|
|
16
|
+
// clone — guard so the measurement pass doesn't measure again recursively.
|
|
17
|
+
static thread_local bool gIsMeasuringNaturalHeight = false;
|
|
18
|
+
|
|
11
19
|
void TrueSheetContentViewShadowNode::adjustLayoutWithState() {
|
|
12
20
|
ensureUnsealed();
|
|
13
21
|
|
|
@@ -17,12 +25,12 @@ void TrueSheetContentViewShadowNode::adjustLayoutWithState() {
|
|
|
17
25
|
|
|
18
26
|
auto &props = getConcreteProps();
|
|
19
27
|
|
|
20
|
-
// When a ScrollView is
|
|
28
|
+
// When a ScrollView is detected under an auto detent, fill the container so
|
|
21
29
|
// descendant flex layouts (flex:1 wrappers, the ScrollView itself) resolve
|
|
22
30
|
// against a definite height and the viewport is bounded — natural layout is
|
|
23
|
-
// circular there since the sheet height derives from the
|
|
24
|
-
//
|
|
25
|
-
//
|
|
31
|
+
// circular there since the sheet height derives from the content's natural
|
|
32
|
+
// height instead (see updateNaturalHeightIfNeeded). For fixed detents,
|
|
33
|
+
// content lays out naturally like a regular view.
|
|
26
34
|
auto flexGrow = stateData.scrollableBounded
|
|
27
35
|
? FloatOptional{1.0f}
|
|
28
36
|
: props.yogaStyle.flexGrow();
|
|
@@ -40,4 +48,42 @@ void TrueSheetContentViewShadowNode::adjustLayoutWithState() {
|
|
|
40
48
|
}
|
|
41
49
|
}
|
|
42
50
|
|
|
51
|
+
void TrueSheetContentViewShadowNode::layout(LayoutContext layoutContext) {
|
|
52
|
+
ConcreteViewShadowNode::layout(layoutContext);
|
|
53
|
+
updateNaturalHeightIfNeeded(layoutContext);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// The natural height is the height the content wants when unbounded — the
|
|
57
|
+
// source for the auto detent. Once the container bounds the content
|
|
58
|
+
// (scrollableBounded), the committed layout no longer reflects it, so lay out
|
|
59
|
+
// a clone of the subtree with an unconstrained height. Yoga respects the
|
|
60
|
+
// user's styles: an explicit-height ScrollView keeps its height while a
|
|
61
|
+
// flexible one expands to its content — no ScrollView discovery involved.
|
|
62
|
+
void TrueSheetContentViewShadowNode::updateNaturalHeightIfNeeded(
|
|
63
|
+
const LayoutContext &layoutContext) {
|
|
64
|
+
if (gIsMeasuringNaturalHeight) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
auto stateData = getStateData();
|
|
69
|
+
auto size = getLayoutMetrics().frame.size;
|
|
70
|
+
|
|
71
|
+
Float naturalHeight = size.height;
|
|
72
|
+
if (stateData.scrollableBounded) {
|
|
73
|
+
auto constraints = LayoutConstraints{};
|
|
74
|
+
constraints.minimumSize = {size.width, 0};
|
|
75
|
+
constraints.maximumSize = {size.width, std::numeric_limits<Float>::infinity()};
|
|
76
|
+
constraints.layoutDirection = getLayoutMetrics().layoutDirection;
|
|
77
|
+
|
|
78
|
+
gIsMeasuringNaturalHeight = true;
|
|
79
|
+
naturalHeight = measure(layoutContext, constraints).height;
|
|
80
|
+
gIsMeasuringNaturalHeight = false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (stateData.naturalHeight != naturalHeight) {
|
|
84
|
+
stateData.naturalHeight = naturalHeight;
|
|
85
|
+
setStateData(std::move(stateData));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
43
89
|
} // namespace facebook::react
|
|
@@ -23,6 +23,13 @@ class JSI_EXPORT TrueSheetContentViewShadowNode final
|
|
|
23
23
|
|
|
24
24
|
public:
|
|
25
25
|
void adjustLayoutWithState();
|
|
26
|
+
|
|
27
|
+
#pragma mark - LayoutableShadowNode
|
|
28
|
+
|
|
29
|
+
void layout(LayoutContext layoutContext) override;
|
|
30
|
+
|
|
31
|
+
private:
|
|
32
|
+
void updateNaturalHeightIfNeeded(const LayoutContext &layoutContext);
|
|
26
33
|
};
|
|
27
34
|
|
|
28
35
|
} // namespace facebook::react
|
|
@@ -4,7 +4,8 @@ namespace facebook::react {
|
|
|
4
4
|
|
|
5
5
|
#ifdef ANDROID
|
|
6
6
|
folly::dynamic TrueSheetContentViewState::getDynamic() const {
|
|
7
|
-
return folly::dynamic::object("scrollableBounded", scrollableBounded)
|
|
7
|
+
return folly::dynamic::object("scrollableBounded", scrollableBounded)(
|
|
8
|
+
"naturalHeight", naturalHeight);
|
|
8
9
|
}
|
|
9
10
|
#endif
|
|
10
11
|
|