@lodev09/react-native-true-sheet 4.0.0-beta.3 → 4.0.0-beta.30

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.
Files changed (244) hide show
  1. package/README.md +28 -5
  2. package/android/build.gradle +23 -1
  3. package/android/proguard-rules.pro +3 -0
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +50 -16
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +303 -184
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +5 -1
  7. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +38 -109
  8. package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +13 -0
  9. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +127 -0
  10. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayViewManager.kt +45 -0
  11. package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +2 -1
  12. package/android/src/main/java/com/lodev09/truesheet/TrueSheetStateUpdater.kt +29 -3
  13. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +234 -66
  14. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +491 -186
  15. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +64 -10
  16. package/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt +31 -14
  17. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetBehavior.kt +135 -0
  18. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +23 -23
  19. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +130 -6
  20. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +104 -39
  21. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +14 -17
  22. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetGrabberView.kt +2 -4
  23. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetOverlayRootView.kt +148 -0
  24. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetStackManager.kt +33 -40
  25. package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +15 -0
  26. package/android/src/main/java/com/lodev09/truesheet/utils/ColorUtils.kt +19 -0
  27. package/android/src/main/java/com/lodev09/truesheet/utils/KeyboardUtils.kt +15 -2
  28. package/android/src/main/java/com/lodev09/truesheet/utils/ScreenUtils.kt +0 -12
  29. package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +19 -7
  30. package/android/src/main/jni/TrueSheetSpec.h +1 -0
  31. package/android/src/main/jni/TrueSheetStateUpdater.cpp +37 -6
  32. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +1 -9
  33. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +114 -30
  34. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +30 -7
  35. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +2 -2
  36. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +9 -8
  37. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewComponentDescriptor.h +3 -5
  38. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +14 -3
  39. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.h +5 -6
  40. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.cpp +1 -1
  41. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.h +10 -6
  42. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.cpp +1 -1
  43. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.h +1 -1
  44. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetLayoutUtils.h +43 -0
  45. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewComponentDescriptor.h +21 -0
  46. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.cpp +17 -0
  47. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.h +33 -0
  48. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewComponentDescriptor.h +3 -5
  49. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.cpp +7 -40
  50. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.h +3 -7
  51. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.cpp +3 -5
  52. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.h +4 -5
  53. package/ios/TrueSheetContainerView.h +23 -7
  54. package/ios/TrueSheetContainerView.mm +45 -25
  55. package/ios/TrueSheetContentView.h +44 -15
  56. package/ios/TrueSheetContentView.mm +221 -190
  57. package/ios/TrueSheetFooterView.mm +133 -28
  58. package/ios/TrueSheetModule.h +2 -7
  59. package/ios/TrueSheetModule.mm +18 -18
  60. package/ios/TrueSheetOverlayView.h +42 -0
  61. package/ios/TrueSheetOverlayView.mm +175 -0
  62. package/ios/TrueSheetView.h +2 -0
  63. package/ios/TrueSheetView.mm +268 -52
  64. package/ios/TrueSheetViewController.h +36 -3
  65. package/ios/TrueSheetViewController.mm +797 -102
  66. package/ios/core/RNScreensEventObserver.mm +31 -28
  67. package/ios/core/TrueSheetBlurView.h +0 -2
  68. package/ios/core/TrueSheetBlurView.mm +3 -36
  69. package/ios/core/TrueSheetDetentCalculator.h +18 -1
  70. package/ios/core/TrueSheetDetentCalculator.mm +44 -9
  71. package/ios/core/TrueSheetDimView.h +27 -0
  72. package/ios/core/TrueSheetDimView.mm +41 -0
  73. package/ios/core/TrueSheetGrabberView.mm +13 -3
  74. package/ios/core/TrueSheetKeyboardObserver.h +7 -0
  75. package/ios/core/TrueSheetKeyboardObserver.mm +107 -15
  76. package/ios/core/TrueSheetOverlayContainerView.h +31 -0
  77. package/ios/core/TrueSheetOverlayContainerView.mm +35 -0
  78. package/ios/events/TrueSheetLifecycleEvents.h +2 -0
  79. package/ios/events/TrueSheetLifecycleEvents.mm +8 -0
  80. package/ios/tvos/TrueSheetStubs.mm +12 -0
  81. package/ios/utils/UIView+ScrollEdgeInteraction.h +1 -0
  82. package/ios/utils/UIView+ScrollEdgeInteraction.mm +24 -2
  83. package/lib/module/TrueSheet.js +222 -54
  84. package/lib/module/TrueSheet.js.map +1 -1
  85. package/lib/module/TrueSheet.web.js +364 -275
  86. package/lib/module/TrueSheet.web.js.map +1 -1
  87. package/lib/module/TrueSheetBackground.js +15 -0
  88. package/lib/module/TrueSheetBackground.js.map +1 -0
  89. package/lib/module/TrueSheetOverlay.js +30 -0
  90. package/lib/module/TrueSheetOverlay.js.map +1 -0
  91. package/lib/module/TrueSheetOverlay.web.js +35 -0
  92. package/lib/module/TrueSheetOverlay.web.js.map +1 -0
  93. package/lib/module/TrueSheetPeek.web.js +20 -15
  94. package/lib/module/TrueSheetPeek.web.js.map +1 -1
  95. package/lib/module/TrueSheetProvider.web.js +13 -9
  96. package/lib/module/TrueSheetProvider.web.js.map +1 -1
  97. package/lib/module/detentBackgrounds.js +21 -0
  98. package/lib/module/detentBackgrounds.js.map +1 -0
  99. package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
  100. package/lib/module/fabric/TrueSheetFooterViewNativeComponent.ts +2 -3
  101. package/lib/module/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  102. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +89 -73
  103. package/lib/module/index.js +1 -0
  104. package/lib/module/index.js.map +1 -1
  105. package/lib/module/mocks/index.js +18 -2
  106. package/lib/module/mocks/index.js.map +1 -1
  107. package/lib/module/mocks/navigationExpoRouter.js.map +1 -1
  108. package/lib/module/navigation/base-types.js +2 -0
  109. package/lib/module/navigation/base-types.js.map +1 -0
  110. package/lib/module/navigation/createTrueSheetRouter.js +158 -100
  111. package/lib/module/navigation/createTrueSheetRouter.js.map +1 -1
  112. package/lib/module/navigation/expo-router/base-types.js +2 -0
  113. package/lib/module/navigation/expo-router/base-types.js.map +1 -0
  114. package/lib/module/navigation/expo-router/extendTrueSheetRouter.js +71 -0
  115. package/lib/module/navigation/expo-router/extendTrueSheetRouter.js.map +1 -0
  116. package/lib/module/navigation/expo-router/index.js +18 -7
  117. package/lib/module/navigation/expo-router/index.js.map +1 -1
  118. package/lib/module/navigation/expo-router/types.js +4 -0
  119. package/lib/module/navigation/expo-router/types.js.map +1 -0
  120. package/lib/module/navigation/index.js.map +1 -1
  121. package/lib/module/navigation/screen/useSheetScreenState.js +1 -0
  122. package/lib/module/navigation/screen/useSheetScreenState.js.map +1 -1
  123. package/lib/module/scrollable.js +28 -0
  124. package/lib/module/scrollable.js.map +1 -0
  125. package/lib/module/web/constants.js +1 -1
  126. package/lib/module/web/constants.js.map +1 -1
  127. package/lib/module/web/keyboard.js +69 -0
  128. package/lib/module/web/keyboard.js.map +1 -0
  129. package/lib/module/web/layout.js +168 -0
  130. package/lib/module/web/layout.js.map +1 -0
  131. package/lib/module/web/vaul/constants.js +9 -0
  132. package/lib/module/web/vaul/constants.js.map +1 -1
  133. package/lib/module/web/vaul/context.js +4 -0
  134. package/lib/module/web/vaul/context.js.map +1 -1
  135. package/lib/module/web/vaul/helpers.js +1 -0
  136. package/lib/module/web/vaul/helpers.js.map +1 -1
  137. package/lib/module/web/vaul/index.js +83 -21
  138. package/lib/module/web/vaul/index.js.map +1 -1
  139. package/lib/module/web/vaul/use-snap-points.js +57 -28
  140. package/lib/module/web/vaul/use-snap-points.js.map +1 -1
  141. package/lib/typescript/src/TrueSheet.d.ts +18 -1
  142. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  143. package/lib/typescript/src/TrueSheet.types.d.ts +209 -35
  144. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  145. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  146. package/lib/typescript/src/TrueSheetBackground.d.ts +3 -0
  147. package/lib/typescript/src/TrueSheetBackground.d.ts.map +1 -0
  148. package/lib/typescript/src/TrueSheetOverlay.d.ts +8 -0
  149. package/lib/typescript/src/TrueSheetOverlay.d.ts.map +1 -0
  150. package/lib/typescript/src/TrueSheetOverlay.web.d.ts +8 -0
  151. package/lib/typescript/src/TrueSheetOverlay.web.d.ts.map +1 -0
  152. package/lib/typescript/src/TrueSheetPeek.web.d.ts +8 -4
  153. package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -1
  154. package/lib/typescript/src/TrueSheetProvider.web.d.ts +5 -1
  155. package/lib/typescript/src/TrueSheetProvider.web.d.ts.map +1 -1
  156. package/lib/typescript/src/detentBackgrounds.d.ts +10 -0
  157. package/lib/typescript/src/detentBackgrounds.d.ts.map +1 -0
  158. package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts +2 -3
  159. package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts.map +1 -1
  160. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts +6 -0
  161. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts.map +1 -0
  162. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +76 -65
  163. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  164. package/lib/typescript/src/index.d.ts +1 -0
  165. package/lib/typescript/src/index.d.ts.map +1 -1
  166. package/lib/typescript/src/mocks/index.d.ts +4 -0
  167. package/lib/typescript/src/mocks/index.d.ts.map +1 -1
  168. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts +3 -3
  169. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts.map +1 -1
  170. package/lib/typescript/src/navigation/base-types.d.ts +9 -0
  171. package/lib/typescript/src/navigation/base-types.d.ts.map +1 -0
  172. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts +36 -10
  173. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -1
  174. package/lib/typescript/src/navigation/expo-router/base-types.d.ts +8 -0
  175. package/lib/typescript/src/navigation/expo-router/base-types.d.ts.map +1 -0
  176. package/lib/typescript/src/navigation/expo-router/extendTrueSheetRouter.d.ts +33 -0
  177. package/lib/typescript/src/navigation/expo-router/extendTrueSheetRouter.d.ts.map +1 -0
  178. package/lib/typescript/src/navigation/expo-router/index.d.ts +9 -6
  179. package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -1
  180. package/lib/typescript/src/navigation/expo-router/types.d.ts +44 -0
  181. package/lib/typescript/src/navigation/expo-router/types.d.ts.map +1 -0
  182. package/lib/typescript/src/navigation/index.d.ts +3 -4
  183. package/lib/typescript/src/navigation/index.d.ts.map +1 -1
  184. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts +2 -1
  185. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts.map +1 -1
  186. package/lib/typescript/src/navigation/types.d.ts +43 -6
  187. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  188. package/lib/typescript/src/scrollable.d.ts +2 -0
  189. package/lib/typescript/src/scrollable.d.ts.map +1 -0
  190. package/lib/typescript/src/web/constants.d.ts +1 -1
  191. package/lib/typescript/src/web/constants.d.ts.map +1 -1
  192. package/lib/typescript/src/web/keyboard.d.ts +15 -0
  193. package/lib/typescript/src/web/keyboard.d.ts.map +1 -0
  194. package/lib/typescript/src/web/layout.d.ts +20 -0
  195. package/lib/typescript/src/web/layout.d.ts.map +1 -0
  196. package/lib/typescript/src/web/vaul/constants.d.ts +3 -0
  197. package/lib/typescript/src/web/vaul/constants.d.ts.map +1 -1
  198. package/lib/typescript/src/web/vaul/context.d.ts +5 -1
  199. package/lib/typescript/src/web/vaul/context.d.ts.map +1 -1
  200. package/lib/typescript/src/web/vaul/helpers.d.ts +1 -0
  201. package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -1
  202. package/lib/typescript/src/web/vaul/index.d.ts +20 -1
  203. package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
  204. package/lib/typescript/src/web/vaul/types.d.ts +9 -0
  205. package/lib/typescript/src/web/vaul/types.d.ts.map +1 -1
  206. package/lib/typescript/src/web/vaul/use-snap-points.d.ts +6 -4
  207. package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
  208. package/package.json +18 -8
  209. package/react-native.config.js +1 -0
  210. package/src/TrueSheet.tsx +257 -62
  211. package/src/TrueSheet.types.ts +225 -35
  212. package/src/TrueSheet.web.tsx +445 -333
  213. package/src/TrueSheetBackground.tsx +17 -0
  214. package/src/TrueSheetOverlay.tsx +24 -0
  215. package/src/TrueSheetOverlay.web.tsx +33 -0
  216. package/src/TrueSheetPeek.web.tsx +25 -23
  217. package/src/TrueSheetProvider.web.tsx +22 -13
  218. package/src/detentBackgrounds.ts +24 -0
  219. package/src/fabric/TrueSheetContentViewNativeComponent.ts +1 -1
  220. package/src/fabric/TrueSheetFooterViewNativeComponent.ts +2 -3
  221. package/src/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  222. package/src/fabric/TrueSheetViewNativeComponent.ts +89 -73
  223. package/src/index.ts +1 -0
  224. package/src/mocks/index.ts +17 -2
  225. package/src/mocks/navigationExpoRouter.ts +6 -5
  226. package/src/navigation/base-types.ts +20 -0
  227. package/src/navigation/createTrueSheetRouter.ts +220 -144
  228. package/src/navigation/expo-router/base-types.ts +18 -0
  229. package/src/navigation/expo-router/extendTrueSheetRouter.ts +99 -0
  230. package/src/navigation/expo-router/index.ts +41 -30
  231. package/src/navigation/expo-router/types.ts +116 -0
  232. package/src/navigation/index.ts +6 -7
  233. package/src/navigation/screen/useSheetScreenState.ts +2 -0
  234. package/src/navigation/types.ts +81 -23
  235. package/src/scrollable.ts +25 -0
  236. package/src/web/constants.ts +1 -1
  237. package/src/web/keyboard.ts +79 -0
  238. package/src/web/layout.ts +196 -0
  239. package/src/web/vaul/constants.ts +9 -0
  240. package/src/web/vaul/context.ts +7 -1
  241. package/src/web/vaul/helpers.ts +5 -0
  242. package/src/web/vaul/index.tsx +113 -16
  243. package/src/web/vaul/types.ts +10 -0
  244. package/src/web/vaul/use-snap-points.ts +86 -33
@@ -10,9 +10,11 @@ import android.view.ViewGroup
10
10
  import android.widget.EditText
11
11
  import androidx.coordinatorlayout.widget.CoordinatorLayout
12
12
  import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
13
+ import com.facebook.react.uimanager.PixelUtil.dpToPx
13
14
  import com.facebook.react.uimanager.PointerEvents
14
15
  import com.facebook.react.uimanager.ReactPointerEventsView
15
16
  import com.facebook.react.uimanager.TouchTargetHelper
17
+ import com.lodev09.truesheet.utils.KeyboardUtils
16
18
  import com.lodev09.truesheet.utils.isDescendantOf
17
19
 
18
20
  interface TrueSheetCoordinatorLayoutDelegate {
@@ -20,6 +22,15 @@ interface TrueSheetCoordinatorLayoutDelegate {
20
22
  fun coordinatorLayoutDidChangeConfiguration()
21
23
  fun findScrollView(): ViewGroup?
22
24
  fun findSheetView(): TrueSheetBottomSheetView?
25
+
26
+ /**
27
+ * A downward pull the sheet couldn't follow ended; `overdragPx` is the travel it
28
+ * didn't follow, `keyboardWasVisible` whether the keyboard was up when it began.
29
+ */
30
+ fun coordinatorLayoutDidPullDown(overdragPx: Int, keyboardWasVisible: Boolean)
31
+
32
+ // A touch that began inside the sheet ended, after the behavior handled the release
33
+ fun coordinatorLayoutDidEndTouch()
23
34
  }
24
35
 
25
36
  /**
@@ -57,13 +68,40 @@ class TrueSheetCoordinatorLayout(context: Context) :
57
68
  private var streamGestureClaimed = false
58
69
 
59
70
  // Whether the sheet owns vertical drags for this stream: the touch target is inside
60
- // the sheet but outside the pinned scrollable (e.g. a header overlaying it), with
71
+ // the sheet but outside the detected scrollable (e.g. a header overlaying it), with
61
72
  // nothing vertically scrollable along its chain. Such targets consume the stream
62
73
  // natively (no nested scroll), and BottomSheetBehavior won't intercept because the
63
74
  // scrollable's bounds still contain the point (touchingScrollingChild) — intercept
64
75
  // manually.
65
76
  private var streamSheetDraggable = false
66
77
  private var streamDraggableEditText = false
78
+ private var streamScrollsIndependently = false
79
+
80
+ // Pull-down tracking. BottomSheetBehavior gives no callback when a downward drag
81
+ // is clamped by `isHideable = false` — the sheet stays put (or the scrollable
82
+ // overscrolls) and no state change fires. Accumulate the downward finger travel
83
+ // neither the sheet nor a scroller under the finger followed and report it on
84
+ // release so the controller can surface a blocked dismiss attempt. Like iOS, a
85
+ // pull that began on a scroller that could scroll up stays a scroll, even past
86
+ // its top.
87
+ private var pullTracking = false
88
+ private var pullStartX = 0f
89
+ private var pullStartY = 0f
90
+ private var pullLastY = 0f
91
+ private var pullOverdrag = 0f
92
+ private var pullKeyboardVisible = false
93
+ private var pullScrollers: List<View> = emptyList()
94
+ private var pullBeganScrolled = false
95
+
96
+ // A touch that began inside the sheet is still down
97
+ val isTouchingSheet: Boolean
98
+ get() = pullTracking
99
+
100
+ // Set while the sheet presents, from present() (keyboard wait included) until the
101
+ // present finishes or a dismiss starts. A stream that begins meanwhile is swallowed
102
+ // whole so nothing below takes it, like the iOS presentation transition.
103
+ var blocksTouches = false
104
+ private var streamBlocked = false
67
105
 
68
106
  init {
69
107
  layoutParams = LayoutParams(
@@ -94,6 +132,68 @@ class TrueSheetCoordinatorLayout(context: Context) :
94
132
  override val pointerEvents: PointerEvents
95
133
  get() = PointerEvents.BOX_NONE
96
134
 
135
+ override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
136
+ if (ev.actionMasked == MotionEvent.ACTION_DOWN) streamBlocked = blocksTouches
137
+ if (streamBlocked) {
138
+ if (ev.actionMasked == MotionEvent.ACTION_UP || ev.actionMasked == MotionEvent.ACTION_CANCEL) {
139
+ streamBlocked = false
140
+ }
141
+ return true
142
+ }
143
+
144
+ when (ev.actionMasked) {
145
+ MotionEvent.ACTION_DOWN -> {
146
+ val sheet = delegate?.findSheetView()
147
+ pullTracking = sheet != null && isTouchInside(sheet, ev)
148
+ pullStartX = ev.rawX
149
+ pullStartY = ev.rawY
150
+ pullLastY = ev.rawY
151
+ pullOverdrag = 0f
152
+ pullKeyboardVisible = pullTracking && KeyboardUtils.isKeyboardVisible(this)
153
+ }
154
+
155
+ MotionEvent.ACTION_MOVE -> if (pullTracking) {
156
+ val sheet = delegate?.findSheetView()
157
+ val topBefore = sheet?.top
158
+ val handled = super.dispatchTouchEvent(ev)
159
+ val step = ev.rawY - pullLastY
160
+ pullLastY = ev.rawY
161
+ // Finger moved down but the sheet stayed put and nothing under the finger
162
+ // scrolled — the pull was clamped
163
+ if (!pullBeganScrolled &&
164
+ step > 0 &&
165
+ sheet != null &&
166
+ sheet.top == topBefore &&
167
+ pullScrollers.none { it.canScrollVertically(-1) }
168
+ ) {
169
+ pullOverdrag += step
170
+ }
171
+ return handled
172
+ }
173
+
174
+ MotionEvent.ACTION_UP -> if (pullTracking) {
175
+ pullTracking = false
176
+ val dx = kotlin.math.abs(ev.rawX - pullStartX)
177
+ val dy = ev.rawY - pullStartY
178
+ // Let the behavior settle first so the controller sees the resting position.
179
+ val handled = super.dispatchTouchEvent(ev)
180
+ if (dy > dx && pullOverdrag > PULL_DOWN_THRESHOLD_DP.dpToPx()) {
181
+ delegate?.coordinatorLayoutDidPullDown(pullOverdrag.toInt(), pullKeyboardVisible)
182
+ }
183
+ delegate?.coordinatorLayoutDidEndTouch()
184
+ return handled
185
+ }
186
+
187
+ MotionEvent.ACTION_CANCEL -> if (pullTracking) {
188
+ pullTracking = false
189
+ val handled = super.dispatchTouchEvent(ev)
190
+ delegate?.coordinatorLayoutDidEndTouch()
191
+ return handled
192
+ }
193
+ }
194
+ return super.dispatchTouchEvent(ev)
195
+ }
196
+
97
197
  /**
98
198
  * Clears stale `nestedScrollingChildRef` from BottomSheetBehavior.
99
199
  *
@@ -127,19 +227,29 @@ class TrueSheetCoordinatorLayout(context: Context) :
127
227
 
128
228
  /**
129
229
  * 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 pinned
131
- * scrollable, and nothing along the target's chain scrolls vertically.
230
+ * decides whether the sheet may drag from it — inside the sheet, outside the detected
231
+ * scrollable, and nothing along the target's chain scrolls vertically. Also collects
232
+ * the chain's vertical scrollers for pull-down tracking.
132
233
  */
133
234
  private fun updateStreamTargetFlags(ev: MotionEvent) {
134
235
  streamSheetDraggable = false
135
236
  streamDraggableEditText = false
237
+ streamScrollsIndependently = false
238
+ pullScrollers = emptyList()
239
+ pullBeganScrolled = false
136
240
 
137
241
  val sheet = delegate?.findSheetView() ?: return
138
242
  val target = TouchTargetHelper.findTargetPathAndCoordinatesForTouch(ev.x, ev.y, this, FloatArray(2))
139
243
  .firstNotNullOfOrNull { it.getView() } ?: return
140
244
  if (target !== sheet && !target.isDescendantOf(sheet)) return
141
245
 
142
- // A pinned scrollable that can scroll (or hosts pull-to-refresh) owns its
246
+ pullScrollers = generateSequence(target) { it.parent as? View }
247
+ .takeWhile { it !== this }
248
+ .filter { it.canScrollVertically(1) || it.canScrollVertically(-1) }
249
+ .toList()
250
+ pullBeganScrolled = pullScrollers.any { it.canScrollVertically(-1) }
251
+
252
+ // A detected scrollable that can scroll (or hosts pull-to-refresh) owns its
143
253
  // touches — nested scrolling drags the sheet. When it can't scroll (e.g.
144
254
  // the sheet is fully expanded and the content fits) it eats the gesture
145
255
  // instead, so fall through and let the sheet drag from it — an EditText
@@ -152,7 +262,10 @@ class TrueSheetCoordinatorLayout(context: Context) :
152
262
 
153
263
  var view: View? = target
154
264
  while (view != null && view !== this) {
155
- if (view.canScrollVertically(1) || view.canScrollVertically(-1)) return
265
+ if (view.canScrollVertically(1) || view.canScrollVertically(-1)) {
266
+ streamScrollsIndependently = !view.isNestedScrollingEnabled
267
+ return
268
+ }
156
269
  view = view.parent as? View
157
270
  }
158
271
 
@@ -160,6 +273,14 @@ class TrueSheetCoordinatorLayout(context: Context) :
160
273
  streamDraggableEditText = target is EditText
161
274
  }
162
275
 
276
+ private fun isTouchInside(view: View, ev: MotionEvent): Boolean {
277
+ val loc = IntArray(2)
278
+ view.getLocationOnScreen(loc)
279
+ val x = ev.rawX.toInt()
280
+ val y = ev.rawY.toInt()
281
+ return x >= loc[0] && x <= loc[0] + view.width && y >= loc[1] && y <= loc[1] + view.height
282
+ }
283
+
163
284
  private fun findDescendantViewAt(view: View, rawX: Int, rawY: Int, predicate: (View) -> Boolean): View? {
164
285
  val loc = IntArray(2)
165
286
  view.getLocationOnScreen(loc)
@@ -174,7 +295,7 @@ class TrueSheetCoordinatorLayout(context: Context) :
174
295
  }
175
296
 
176
297
  /**
177
- * Called by descendant RootView implementations (controller, footer) when a child
298
+ * Called by the descendant RootView (controller) when a child
178
299
  * starts a native gesture — e.g. an RNGH handler activated or a scrollable began
179
300
  * scrolling. Yields sheet interception for the rest of the touch stream.
180
301
  */
@@ -226,6 +347,8 @@ class TrueSheetCoordinatorLayout(context: Context) :
226
347
  return false
227
348
  }
228
349
 
350
+ if (streamScrollsIndependently) return false
351
+
229
352
  if (streamGestureClaimed) {
230
353
  if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
231
354
  streamGestureClaimed = false
@@ -299,5 +422,6 @@ class TrueSheetCoordinatorLayout(context: Context) :
299
422
  companion object {
300
423
  private const val GESTURE_HANDLER_ROOT_VIEW_CLASS = "com.swmansion.gesturehandler.react.RNGestureHandlerRootView"
301
424
  private const val GESTURE_CLAIM_SLOP_FACTOR = 3
425
+ private const val PULL_DOWN_THRESHOLD_DP = 24f
302
426
  }
303
427
  }
@@ -18,11 +18,27 @@ interface TrueSheetDetentCalculatorDelegate {
18
18
  val absoluteHeader: Boolean
19
19
  val footerHeight: Int
20
20
  val absoluteFooter: Boolean
21
+
22
+ /**
23
+ * True while the plugged scrollable is padded by the absolute footer's
24
+ * height (see ScrollableOptions.footerInsetAdjustment) — the footer then
25
+ * counts toward the auto detent like a relative one, so the content ends
26
+ * above it.
27
+ */
28
+ val footerInsetAdjustment: Boolean
29
+ val footerAvoidsKeyboard: Boolean
21
30
  val peekContentHeight: Int
22
31
  val contentBottomInset: Int
23
32
  val maxContentHeight: Int?
24
33
  val keyboardInset: Int
25
34
  val topInset: Int
35
+
36
+ /**
37
+ * True while the keyboard holds the sheet up: the collapsed stop then sits at
38
+ * the keyboard-free first detent (the "floor") so a drag-down dismisses the
39
+ * keyboard before it can dismiss the sheet.
40
+ */
41
+ val hasKeyboardFloor: Boolean
26
42
  }
27
43
 
28
44
  /**
@@ -44,6 +60,15 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
44
60
  private val maxContentHeight: Int? get() = delegate?.maxContentHeight
45
61
  private val keyboardInset: Int get() = delegate?.keyboardInset ?: 0
46
62
  private val topInset: Int get() = delegate?.topInset ?: 0
63
+ private val hasKeyboardFloor: Boolean get() = delegate?.hasKeyboardFloor == true
64
+
65
+ /**
66
+ * An absolute footer floats over the content and contributes no height to
67
+ * the auto detent — unless it pads the scrollable, then the content ends
68
+ * above it like a relative footer.
69
+ */
70
+ private val footerInAuto: Boolean
71
+ get() = delegate?.absoluteFooter == false || delegate?.footerInsetAdjustment == true
47
72
 
48
73
  /**
49
74
  * Height for auto (-1.0) detents: content + header + footer height.
@@ -52,14 +77,14 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
52
77
  private val autoDetentHeight: Int
53
78
  get() = contentHeight +
54
79
  (if (delegate?.absoluteHeader == true) 0 else headerHeight) +
55
- (if (delegate?.absoluteFooter == true) 0 else footerHeight)
80
+ (if (footerInAuto) footerHeight else 0)
56
81
 
57
82
  /**
58
- * Bottom inset for auto detents. A relative footer owns the sheet's bottom
59
- * edge, so it absorbs the inset instead of adding it to the auto height.
83
+ * Bottom inset for auto detents. A footer counted in the auto height owns
84
+ * the sheet's bottom edge, so it absorbs the inset instead of adding it.
60
85
  */
61
86
  private val autoDetentBottomInset: Int
62
- get() = if (footerHeight > 0 && delegate?.absoluteFooter == false) 0 else contentBottomInset
87
+ get() = if (footerHeight > 0 && footerInAuto) 0 else contentBottomInset
63
88
 
64
89
  /**
65
90
  * Bottom inset for peek detents. An absolute footer counts toward the peek
@@ -97,11 +122,37 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
97
122
  (detent * screenHeight).toInt() + contentBottomInset
98
123
  }
99
124
 
100
- val height = if (includeKeyboard) baseHeight + keyboardInset else baseHeight
125
+ val keyboardHeight = if (includeKeyboard) keyboardInset else 0
126
+ val footerKeyboardOverlap = if (detent == -1.0 &&
127
+ delegate?.absoluteFooter == true &&
128
+ footerInAuto &&
129
+ delegate?.footerAvoidsKeyboard == false
130
+ ) {
131
+ minOf(keyboardHeight, maxOf(0, footerHeight - contentBottomInset))
132
+ } else {
133
+ 0
134
+ }
135
+ val height = baseHeight + keyboardHeight - footerKeyboardOverlap
101
136
  val maxAllowedHeight = screenHeight + contentBottomInset
102
137
  return maxContentHeight?.let { minOf(height, it, maxAllowedHeight) } ?: minOf(height, maxAllowedHeight)
103
138
  }
104
139
 
140
+ fun getBackgroundIndex(sheetTop: Int, displayedIndex: Int): Int {
141
+ if (detents.isEmpty()) return -1
142
+
143
+ val height = getVisibleSheetHeight(sheetTop)
144
+ var nextIndex = displayedIndex.coerceIn(0, detents.size - 1)
145
+ var distance = kotlin.math.abs(height - getVisibleSheetHeight(getSheetTopForDetentIndex(nextIndex)))
146
+ for (index in detents.indices) {
147
+ val nextDistance = kotlin.math.abs(height - getVisibleSheetHeight(getSheetTopForDetentIndex(index)))
148
+ if (nextDistance < distance) {
149
+ nextIndex = index
150
+ distance = nextDistance
151
+ }
152
+ }
153
+ return nextIndex
154
+ }
155
+
105
156
  /**
106
157
  * Get the expected sheet top position for a detent index.
107
158
  */
@@ -110,12 +161,31 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
110
161
  RNLog.w(reactContext, "TrueSheet: Detent index ($index) is out of bounds (0..${detents.size - 1})")
111
162
  return realScreenHeight
112
163
  }
113
- // Clamp to the space the sheet can actually occupy — matching
114
- // setupSheetDetents. A keyboard-inflated detent height can exceed it,
115
- // placing the expected top above the screen while the real sheet stops
116
- // at the top inset.
164
+ // With a keyboard floor only the last detent stays keyboard-shifted — it's
165
+ // the keyboard-expanded resting position; the stops below it are keyboard-free
166
+ val isKeyboardFreeStop = hasKeyboardFloor && index != detents.size - 1
167
+ return getSheetTop(detents[index], includeKeyboard = !isKeyboardFreeStop)
168
+ }
169
+
170
+ /**
171
+ * Top of the lowest stop the sheet can rest at: the keyboard floor while it's
172
+ * active, else the first detent.
173
+ */
174
+ fun getLowestSheetTop(): Int = if (hasKeyboardFloor) getSheetTop(detents[0], includeKeyboard = false) else getSheetTopForDetentIndex(0)
175
+
176
+ /**
177
+ * Top of the half stop while the keyboard floor is active: the keyboard-free
178
+ * second detent, or the floor itself for a single detent.
179
+ */
180
+ fun getKeyboardHalfStopTop(): Int = getSheetTop(detents[minOf(1, detents.size - 1)], includeKeyboard = false)
181
+
182
+ // Clamp to the space the sheet can actually occupy — matching
183
+ // setupSheetDetents. A keyboard-inflated detent height can exceed it,
184
+ // placing the expected top above the screen while the real sheet stops
185
+ // at the top inset.
186
+ private fun getSheetTop(detent: Double, includeKeyboard: Boolean = true): Int {
117
187
  val maxAvailableHeight = realScreenHeight - topInset
118
- return realScreenHeight - minOf(getDetentHeight(detents[index]), maxAvailableHeight)
188
+ return realScreenHeight - minOf(getDetentHeight(detent, includeKeyboard), maxAvailableHeight)
119
189
  }
120
190
 
121
191
  /**
@@ -152,6 +222,9 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
152
222
  */
153
223
  fun getStateForDetentIndex(index: Int): Int {
154
224
  val stateMap = getDetentStateMap() ?: return BottomSheetBehavior.STATE_HIDDEN
225
+ // With a keyboard floor the keyboard-expanded last detent lives at the
226
+ // expanded stop — the half stop (which may share its index) is keyboard-free
227
+ if (hasKeyboardFloor && index == detents.size - 1) return BottomSheetBehavior.STATE_EXPANDED
155
228
  return stateMap.entries.find { it.value == index }?.key ?: BottomSheetBehavior.STATE_HIDDEN
156
229
  }
157
230
 
@@ -169,8 +242,12 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
169
242
  */
170
243
  private fun getDetentStateMap(): Map<Int, Int>? =
171
244
  when (detents.size) {
245
+ // With one detent halfExpandedRatio matches the peek height, so the
246
+ // behavior can settle HALF_EXPANDED at the same position (e.g. an upward
247
+ // drag release) — map it too, or the settle is unmappable
172
248
  1 -> mapOf(
173
249
  BottomSheetBehavior.STATE_COLLAPSED to 0,
250
+ BottomSheetBehavior.STATE_HALF_EXPANDED to 0,
174
251
  BottomSheetBehavior.STATE_EXPANDED to 0
175
252
  )
176
253
 
@@ -197,16 +274,18 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
197
274
  * Find which segment the position falls into for interpolation.
198
275
  * @return Triple(fromIndex, toIndex, progress) where progress is 0-1, or null if no detents
199
276
  */
200
- fun findSegmentForPosition(positionPx: Int): Triple<Int, Int, Float>? {
277
+ private fun findSegmentForPosition(positionPx: Int): Triple<Int, Int, Float>? {
201
278
  val count = detents.size
202
279
  if (count == 0) return null
203
280
 
204
- val firstPos = getSheetTopForDetentIndex(0)
281
+ // A single keyboard-shifted detent can rest down at its keyboard floor —
282
+ // that range is still index 0, not a dismissal
283
+ val floorPos = getLowestSheetTop()
205
284
 
206
- // Position is below first detent (sheet is being dragged down to dismiss)
207
- if (positionPx > firstPos) {
208
- val range = realScreenHeight - firstPos
209
- val progress = if (range > 0) (positionPx - firstPos).toFloat() / range else 0f
285
+ // Position is below the lowest stop (sheet is being dragged down to dismiss)
286
+ if (positionPx > floorPos) {
287
+ val range = realScreenHeight - floorPos
288
+ val progress = if (range > 0) (positionPx - floorPos).toFloat() / range else 0f
210
289
  return Triple(-1, 0, progress)
211
290
  }
212
291
 
@@ -238,37 +317,23 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
238
317
  }
239
318
 
240
319
  /**
241
- * Returns continuous index (e.g., 0.5 = halfway between detent 0 and 1).
320
+ * Returns interpolated continuous index (e.g., 0.5 = halfway between detent
321
+ * 0 and 1) and screen fraction for a position, sharing a single segment
322
+ * lookup — this runs on every position frame.
242
323
  */
243
- fun getInterpolatedIndexForPosition(positionPx: Int): Float {
244
- val count = detents.size
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
324
+ fun getInterpolatedPositionInfo(positionPx: Int): Pair<Float, Float> {
325
+ if (detents.isEmpty()) return Pair(-1f, 0f)
260
326
 
261
- val segment = findSegmentForPosition(positionPx) ?: return getDetentValueForIndex(0)
262
- val (fromIndex, toIndex, progress) = segment
327
+ val (fromIndex, toIndex, progress) = findSegmentForPosition(positionPx)
328
+ ?: return Pair(0f, getDetentValueForIndex(0))
263
329
 
264
330
  if (fromIndex == -1) {
265
- val firstDetent = getDetentValueForIndex(0)
266
- return maxOf(0f, firstDetent * (1 - progress))
331
+ return Pair(-progress, maxOf(0f, getDetentValueForIndex(0) * (1 - progress)))
267
332
  }
268
333
 
269
334
  val fromDetent = getDetentValueForIndex(fromIndex)
270
335
  val toDetent = getDetentValueForIndex(toIndex)
271
- return fromDetent + progress * (toDetent - fromDetent)
336
+ return Pair(fromIndex + progress, fromDetent + progress * (toDetent - fromDetent))
272
337
  }
273
338
 
274
339
  companion object {
@@ -34,10 +34,6 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) :
34
34
  View(reactContext),
35
35
  ReactPointerEventsView {
36
36
 
37
- companion object {
38
- private const val MAX_ALPHA = 0.5f
39
- }
40
-
41
37
  var delegate: TrueSheetDimViewDelegate? = null
42
38
 
43
39
  private var targetView: ViewGroup? = null
@@ -48,6 +44,10 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) :
48
44
  private val blockGestures: Boolean
49
45
  get() = alpha > 0f
50
46
 
47
+ fun setDimColor(color: Int) {
48
+ setBackgroundColor(color)
49
+ }
50
+
51
51
  init {
52
52
  layoutParams = ViewGroup.LayoutParams(
53
53
  ViewGroup.LayoutParams.MATCH_PARENT,
@@ -109,36 +109,33 @@ class TrueSheetDimView(private val reactContext: ThemedReactContext) :
109
109
  }
110
110
 
111
111
  // =============================================================================
112
- // MARK: - Alpha Calculation
112
+ // MARK: - Progress Calculation
113
113
  // =============================================================================
114
114
 
115
- fun calculateAlpha(sheetTop: Int, dimmedDetentIndex: Int, getSheetTopForDetentIndex: (Int) -> Int): Float {
115
+ /**
116
+ * How far the sheet is into its dimmed range, from 0 (undimmed) to 1 (fully dimmed).
117
+ */
118
+ fun calculateProgress(sheetTop: Int, dimmedDetentIndex: Int, getSheetTopForDetentIndex: (Int) -> Int): Float {
116
119
  val realHeight = ScreenUtils.getRealScreenHeight(reactContext)
117
120
  val dimmedDetentTop = getSheetTopForDetentIndex(dimmedDetentIndex)
118
121
  val belowDimmedTop = if (dimmedDetentIndex > 0) getSheetTopForDetentIndex(dimmedDetentIndex - 1) else realHeight
119
122
 
120
123
  return when {
121
- sheetTop <= dimmedDetentTop -> MAX_ALPHA
122
-
124
+ sheetTop <= dimmedDetentTop -> 1f
123
125
  sheetTop >= belowDimmedTop -> 0f
124
-
125
- else -> {
126
- val progress = 1f - (sheetTop - dimmedDetentTop).toFloat() / (belowDimmedTop - dimmedDetentTop)
127
- (progress * MAX_ALPHA).coerceIn(0f, MAX_ALPHA)
128
- }
126
+ else -> (1f - (sheetTop - dimmedDetentTop).toFloat() / (belowDimmedTop - dimmedDetentTop)).coerceIn(0f, 1f)
129
127
  }
130
128
  }
131
129
 
132
- fun interpolateAlpha(sheetTop: Int, dimmedDetentIndex: Int, getSheetTopForDetentIndex: (Int) -> Int) {
133
- alpha = calculateAlpha(sheetTop, dimmedDetentIndex, getSheetTopForDetentIndex)
134
- }
135
-
136
130
  // =============================================================================
137
131
  // MARK: - Touch Handling
138
132
  // =============================================================================
139
133
 
140
134
  override fun onTouchEvent(event: MotionEvent): Boolean {
141
135
  if (blockGestures) {
136
+ if (event.actionMasked == MotionEvent.ACTION_DOWN) {
137
+ parent?.requestDisallowInterceptTouchEvent(true)
138
+ }
142
139
  // When dimmed, consume touch and trigger click on ACTION_UP
143
140
  if (event.action == MotionEvent.ACTION_UP) {
144
141
  performClick()
@@ -2,7 +2,6 @@ package com.lodev09.truesheet.core
2
2
 
3
3
  import android.annotation.SuppressLint
4
4
  import android.content.Context
5
- import android.content.res.Configuration
6
5
  import android.graphics.Color
7
6
  import android.graphics.drawable.GradientDrawable
8
7
  import android.os.Bundle
@@ -13,6 +12,7 @@ import android.widget.FrameLayout
13
12
  import androidx.core.graphics.ColorUtils
14
13
  import androidx.core.view.ViewCompat
15
14
  import com.facebook.react.uimanager.PixelUtil.dpToPx
15
+ import com.lodev09.truesheet.utils.isDarkMode
16
16
 
17
17
  /**
18
18
  * Options for customizing the grabber appearance.
@@ -165,9 +165,7 @@ class TrueSheetGrabberView(
165
165
  }
166
166
 
167
167
  private fun getAdaptiveColor(baseColor: Int? = null): Int {
168
- val nightMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
169
- val isDarkMode = nightMode == Configuration.UI_MODE_NIGHT_YES
170
- val modeColor = if (isDarkMode) Color.WHITE else Color.BLACK
168
+ val modeColor = if (context.isDarkMode) Color.WHITE else Color.BLACK
171
169
 
172
170
  return if (baseColor != null) {
173
171
  // Blend user color with mode color for adaptive effect