Skip to content

Commit d45061a

Browse files
authored
fix(android): allow sheet drag when ScrollView cannot scroll (#369)
* chore: update clean script * fix(android): allow sheet drag when ScrollView cannot scroll * docs: remove ScrollView drag workaround from troubleshooting * fix(android): only intercept ScrollView touches when scrollable is enabled * fix(android): apply scrollable when creating coordinator layout
1 parent 837d93c commit d45061a

8 files changed

Lines changed: 108 additions & 42 deletions

File tree

android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,10 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
248248
viewController.insetAdjustment = insetAdjustment
249249
}
250250

251+
fun setScrollable(scrollable: Boolean) {
252+
viewController.scrollable = scrollable
253+
}
254+
251255
// ==================== State Management ====================
252256

253257
/**

android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
170170
override var grabberOptions: GrabberOptions? = null
171171
override var sheetBackgroundColor: Int? = null
172172
var insetAdjustment: String = "automatic"
173+
var scrollable: Boolean = false
174+
set(value) {
175+
field = value
176+
coordinatorLayout?.scrollable = value
177+
}
173178

174179
override var sheetCornerRadius: Float = DEFAULT_CORNER_RADIUS.dpToPx()
175180
set(value) {
@@ -297,6 +302,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
297302
// Create coordinator layout
298303
coordinatorLayout = TrueSheetCoordinatorLayout(reactContext).apply {
299304
delegate = this@TrueSheetViewController
305+
scrollable = this@TrueSheetViewController.scrollable
300306
}
301307

302308
sheetView = TrueSheetBottomSheetView(reactContext).apply {

android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ class TrueSheetViewManager :
190190

191191
@ReactProp(name = "scrollable", defaultBoolean = false)
192192
override fun setScrollable(view: TrueSheetView, value: Boolean) {
193-
// iOS-specific prop - no-op on Android
193+
view.setScrollable(value)
194194
}
195195

196196
@ReactProp(name = "pageSizing", defaultBoolean = true)

android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package com.lodev09.truesheet.core
22

33
import android.annotation.SuppressLint
44
import android.content.Context
5-
import android.view.View
5+
import android.view.MotionEvent
6+
import android.view.ViewConfiguration
7+
import android.view.ViewGroup
8+
import android.widget.ScrollView
69
import androidx.coordinatorlayout.widget.CoordinatorLayout
710
import com.facebook.react.uimanager.PointerEvents
811
import com.facebook.react.uimanager.ReactPointerEventsView
@@ -22,15 +25,19 @@ class TrueSheetCoordinatorLayout(context: Context) :
2225
ReactPointerEventsView {
2326

2427
var delegate: TrueSheetCoordinatorLayoutDelegate? = null
28+
var scrollable: Boolean = false
29+
30+
private val touchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop
31+
private var dragging = false
32+
private var initialY = 0f
33+
private var activePointerId = 0
2534

2635
init {
27-
// Fill the entire screen
2836
layoutParams = LayoutParams(
2937
LayoutParams.MATCH_PARENT,
3038
LayoutParams.MATCH_PARENT
3139
)
3240

33-
// Ensure we don't clip the sheet during animations
3441
clipChildren = false
3542
clipToPadding = false
3643
}
@@ -46,10 +53,85 @@ class TrueSheetCoordinatorLayout(context: Context) :
4653
delegate?.coordinatorLayoutDidLayout(changed)
4754
}
4855

49-
/**
50-
* Allow pointer events to pass through to underlying views.
51-
* The DimView and BottomSheetView handle their own touch interception.
52-
*/
5356
override val pointerEvents: PointerEvents
5457
get() = PointerEvents.BOX_NONE
58+
59+
/**
60+
* Intercepts touch events for ScrollViews that can't scroll (content < viewport),
61+
* allowing the sheet to be dragged in these cases.
62+
*
63+
* TODO: Remove this workaround once NestedScrollView is merged into react-native core.
64+
* See: https://github.com/facebook/react-native/pull/44099
65+
*/
66+
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
67+
if (!scrollable) {
68+
return super.onInterceptTouchEvent(ev)
69+
}
70+
71+
val scrollView = findScrollView(this)
72+
val cannotScroll = scrollView != null &&
73+
scrollView.scrollY == 0 &&
74+
!scrollView.canScrollVertically(1)
75+
76+
if (cannotScroll) {
77+
when (ev.action and MotionEvent.ACTION_MASK) {
78+
MotionEvent.ACTION_DOWN -> {
79+
dragging = false
80+
initialY = ev.y
81+
activePointerId = ev.getPointerId(0)
82+
}
83+
MotionEvent.ACTION_MOVE -> {
84+
val pointerIndex = ev.findPointerIndex(activePointerId)
85+
if (pointerIndex != -1) {
86+
val y = ev.getY(pointerIndex)
87+
val deltaY = initialY - y
88+
if (kotlin.math.abs(deltaY) > touchSlop) {
89+
dragging = true
90+
parent?.requestDisallowInterceptTouchEvent(true)
91+
}
92+
}
93+
}
94+
MotionEvent.ACTION_UP,
95+
MotionEvent.ACTION_CANCEL -> {
96+
dragging = false
97+
}
98+
}
99+
} else {
100+
dragging = false
101+
}
102+
103+
return dragging || super.onInterceptTouchEvent(ev)
104+
}
105+
106+
@SuppressLint("ClickableViewAccessibility")
107+
override fun onTouchEvent(ev: MotionEvent): Boolean {
108+
if (dragging) {
109+
when (ev.action and MotionEvent.ACTION_MASK) {
110+
MotionEvent.ACTION_UP,
111+
MotionEvent.ACTION_CANCEL -> {
112+
dragging = false
113+
}
114+
}
115+
// Let parent CoordinatorLayout handle the touch for BottomSheetBehavior
116+
return super.onTouchEvent(ev)
117+
}
118+
return super.onTouchEvent(ev)
119+
}
120+
121+
private fun findScrollView(view: android.view.View): ScrollView? {
122+
if (view is ScrollView) {
123+
return view
124+
}
125+
126+
if (view is ViewGroup) {
127+
for (i in 0 until view.childCount) {
128+
val scrollView = findScrollView(view.getChildAt(i))
129+
if (scrollView != null) {
130+
return scrollView
131+
}
132+
}
133+
}
134+
135+
return null
136+
}
55137
}

docs/docs/guides/scrolling.mdx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,6 @@ On Android, `scrollable` ensures the scroll view fills the available sheet space
5858
The `auto` detent does not work well with `scrollable` on Android. Use fixed fractional detents (e.g., `0.5`, `0.8`, `1`) instead when using `scrollable`.
5959
:::
6060

61-
:::warning
62-
If your `ScrollView` content height is smaller than the sheet height, scrolling may not work properly. See [Troubleshooting](/troubleshooting#unable-to-drag-on-android) for more details.
63-
:::
64-
6561
:::warning
6662
`RefreshControl` does not work with `nestedScrollEnabled` on Android due to how `SwipeRefreshLayout` interferes with the `BottomSheetBehavior`'s nested scrolling coordination. This is a known limitation of the Android platform.
6763

docs/docs/troubleshooting.mdx

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -59,33 +59,6 @@ return (
5959
)
6060
```
6161

62-
## Unable to Drag on Android
63-
64-
If sheet contains `ScrollView` and the content height is smaller than the sheet height, scrolling may not work properly due to a React Native framework limitation. This is because the `ScrollView` won't be scrollable when there's no overflow.
65-
66-
Related: [React Native PR #44099](https://github.com/facebook/react-native/pull/44099)
67-
68-
**Workarounds:**
69-
70-
1. Ensure your content height exceeds the sheet height, or use a `FlatList` with a minimum number of items.
71-
72-
2. Set the `ScrollView`'s `minHeight` to the sheet height + 1 on layout. The extra pixel ensures the content overflows, enabling the scroll behavior. Note that this does not work with `'auto'` detent:
73-
74-
```tsx
75-
const [minHeight, setMinHeight] = useState<number>()
76-
77-
return (
78-
<TrueSheet
79-
detents={[0.5, 1]}
80-
onLayout={(e) => setMinHeight(e.nativeEvent.layout.height + 1)}
81-
>
82-
<ScrollView style={{ minHeight }} nestedScrollEnabled>
83-
<View />
84-
</ScrollView>
85-
</TrueSheet>
86-
)
87-
```
88-
8962
## Keyboard Covering TextInput on Android with Unistyles
9063

9164
When using [`react-native-unistyles`](https://github.com/jpudysz/react-native-unistyles) alongside TrueSheet, the keyboard may cover the TextInput instead of the sheet repositioning itself. This is caused by Unistyles preventing TrueSheet from observing keyboard animation events on Android.

example/bare/ios/Podfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2646,7 +2646,7 @@ PODS:
26462646
- ReactCommon/turbomodule/core
26472647
- SocketRocket
26482648
- Yoga
2649-
- RNTrueSheet (3.6.6):
2649+
- RNTrueSheet (3.6.9):
26502650
- boost
26512651
- DoubleConversion
26522652
- fast_float
@@ -3095,7 +3095,7 @@ SPEC CHECKSUMS:
30953095
RNGestureHandler: e1cf8ef3f11045536eed6bd4f132b003ef5f9a5f
30963096
RNReanimated: f1868b36f4b2b52a0ed00062cfda69506f75eaee
30973097
RNScreens: d821082c6dd1cb397cc0c98b026eeafaa68be479
3098-
RNTrueSheet: 5c0b9f0651f07167ceed0cea987a4ec653b4706d
3098+
RNTrueSheet: 66d29463562c7ba9d9679f5d7af46b8ca8ec5f46
30993099
RNWorklets: d9c050940f140af5d8b611d937eab1cbfce5e9a5
31003100
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
31013101
Yoga: 689c8e04277f3ad631e60fe2a08e41d411daf8eb

scripts/clean.sh

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ step() {
6363
rm -f "$error_file"
6464
}
6565

66+
install() {
67+
rm -rf node_modules example/bare/node_modules example/expo/node_modules docs/node_modules
68+
yarn
69+
}
70+
6671
clean_watchman() {
6772
watchman watch-del-all 2>/dev/null || true
6873
rm -rf $TMPDIR/metro-*
@@ -76,7 +81,7 @@ clean_bare() {
7681
npx pod-install example/bare
7782
}
7883

79-
step "Installing dependencies" "Dependencies installed" yarn
84+
step "Installing dependencies" "Dependencies installed" install
8085
step "Cleaning watchman" "Watchman cache cleared" clean_watchman
8186
step "Cleaning up simulator cache" "Simulator cache cleared" rm -rf ~/Library/Developer/CoreSimulator/Caches
8287
step "Cleaning bare example" "Bare example cleaned" clean_bare

0 commit comments

Comments
 (0)