diff --git a/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt b/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt index 08277a66..19acbef4 100644 --- a/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +++ b/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt @@ -248,6 +248,10 @@ class TrueSheetView(private val reactContext: ThemedReactContext) : viewController.insetAdjustment = insetAdjustment } + fun setScrollable(scrollable: Boolean) { + viewController.scrollable = scrollable + } + // ==================== State Management ==================== /** diff --git a/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt b/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt index 93649190..9af7ef56 100644 --- a/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +++ b/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt @@ -170,6 +170,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) : override var grabberOptions: GrabberOptions? = null override var sheetBackgroundColor: Int? = null var insetAdjustment: String = "automatic" + var scrollable: Boolean = false + set(value) { + field = value + coordinatorLayout?.scrollable = value + } override var sheetCornerRadius: Float = DEFAULT_CORNER_RADIUS.dpToPx() set(value) { @@ -297,6 +302,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) : // Create coordinator layout coordinatorLayout = TrueSheetCoordinatorLayout(reactContext).apply { delegate = this@TrueSheetViewController + scrollable = this@TrueSheetViewController.scrollable } sheetView = TrueSheetBottomSheetView(reactContext).apply { diff --git a/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt b/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt index 58ee6144..8b278467 100644 --- a/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +++ b/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt @@ -190,7 +190,7 @@ class TrueSheetViewManager : @ReactProp(name = "scrollable", defaultBoolean = false) override fun setScrollable(view: TrueSheetView, value: Boolean) { - // iOS-specific prop - no-op on Android + view.setScrollable(value) } @ReactProp(name = "pageSizing", defaultBoolean = true) diff --git a/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt b/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt index 165ec8fe..ff7ea8a4 100644 --- a/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +++ b/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt @@ -2,7 +2,10 @@ package com.lodev09.truesheet.core import android.annotation.SuppressLint import android.content.Context -import android.view.View +import android.view.MotionEvent +import android.view.ViewConfiguration +import android.view.ViewGroup +import android.widget.ScrollView import androidx.coordinatorlayout.widget.CoordinatorLayout import com.facebook.react.uimanager.PointerEvents import com.facebook.react.uimanager.ReactPointerEventsView @@ -22,15 +25,19 @@ class TrueSheetCoordinatorLayout(context: Context) : ReactPointerEventsView { var delegate: TrueSheetCoordinatorLayoutDelegate? = null + var scrollable: Boolean = false + + private val touchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop + private var dragging = false + private var initialY = 0f + private var activePointerId = 0 init { - // Fill the entire screen layoutParams = LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT ) - // Ensure we don't clip the sheet during animations clipChildren = false clipToPadding = false } @@ -46,10 +53,85 @@ class TrueSheetCoordinatorLayout(context: Context) : delegate?.coordinatorLayoutDidLayout(changed) } - /** - * Allow pointer events to pass through to underlying views. - * The DimView and BottomSheetView handle their own touch interception. - */ override val pointerEvents: PointerEvents get() = PointerEvents.BOX_NONE + + /** + * Intercepts touch events for ScrollViews that can't scroll (content < viewport), + * allowing the sheet to be dragged in these cases. + * + * TODO: Remove this workaround once NestedScrollView is merged into react-native core. + * See: https://github.com/facebook/react-native/pull/44099 + */ + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { + if (!scrollable) { + return super.onInterceptTouchEvent(ev) + } + + val scrollView = findScrollView(this) + val cannotScroll = scrollView != null && + scrollView.scrollY == 0 && + !scrollView.canScrollVertically(1) + + if (cannotScroll) { + when (ev.action and MotionEvent.ACTION_MASK) { + MotionEvent.ACTION_DOWN -> { + dragging = false + initialY = ev.y + activePointerId = ev.getPointerId(0) + } + MotionEvent.ACTION_MOVE -> { + val pointerIndex = ev.findPointerIndex(activePointerId) + if (pointerIndex != -1) { + val y = ev.getY(pointerIndex) + val deltaY = initialY - y + if (kotlin.math.abs(deltaY) > touchSlop) { + dragging = true + parent?.requestDisallowInterceptTouchEvent(true) + } + } + } + MotionEvent.ACTION_UP, + MotionEvent.ACTION_CANCEL -> { + dragging = false + } + } + } else { + dragging = false + } + + return dragging || super.onInterceptTouchEvent(ev) + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(ev: MotionEvent): Boolean { + if (dragging) { + when (ev.action and MotionEvent.ACTION_MASK) { + MotionEvent.ACTION_UP, + MotionEvent.ACTION_CANCEL -> { + dragging = false + } + } + // Let parent CoordinatorLayout handle the touch for BottomSheetBehavior + return super.onTouchEvent(ev) + } + return super.onTouchEvent(ev) + } + + private fun findScrollView(view: android.view.View): ScrollView? { + if (view is ScrollView) { + return view + } + + if (view is ViewGroup) { + for (i in 0 until view.childCount) { + val scrollView = findScrollView(view.getChildAt(i)) + if (scrollView != null) { + return scrollView + } + } + } + + return null + } } diff --git a/docs/docs/guides/scrolling.mdx b/docs/docs/guides/scrolling.mdx index 746cdb9c..9dd28587 100644 --- a/docs/docs/guides/scrolling.mdx +++ b/docs/docs/guides/scrolling.mdx @@ -58,10 +58,6 @@ On Android, `scrollable` ensures the scroll view fills the available sheet space 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`. ::: -:::warning -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. -::: - :::warning `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. diff --git a/docs/docs/troubleshooting.mdx b/docs/docs/troubleshooting.mdx index c4db6082..78f21c86 100644 --- a/docs/docs/troubleshooting.mdx +++ b/docs/docs/troubleshooting.mdx @@ -59,33 +59,6 @@ return ( ) ``` -## Unable to Drag on Android - -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. - -Related: [React Native PR #44099](https://github.com/facebook/react-native/pull/44099) - -**Workarounds:** - -1. Ensure your content height exceeds the sheet height, or use a `FlatList` with a minimum number of items. - -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: - -```tsx -const [minHeight, setMinHeight] = useState() - -return ( - setMinHeight(e.nativeEvent.layout.height + 1)} - > - - - - -) -``` - ## Keyboard Covering TextInput on Android with Unistyles 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. diff --git a/example/bare/ios/Podfile.lock b/example/bare/ios/Podfile.lock index a5e607a4..95f50f8f 100644 --- a/example/bare/ios/Podfile.lock +++ b/example/bare/ios/Podfile.lock @@ -2646,7 +2646,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - RNTrueSheet (3.6.6): + - RNTrueSheet (3.6.9): - boost - DoubleConversion - fast_float @@ -3095,7 +3095,7 @@ SPEC CHECKSUMS: RNGestureHandler: e1cf8ef3f11045536eed6bd4f132b003ef5f9a5f RNReanimated: f1868b36f4b2b52a0ed00062cfda69506f75eaee RNScreens: d821082c6dd1cb397cc0c98b026eeafaa68be479 - RNTrueSheet: 5c0b9f0651f07167ceed0cea987a4ec653b4706d + RNTrueSheet: 66d29463562c7ba9d9679f5d7af46b8ca8ec5f46 RNWorklets: d9c050940f140af5d8b611d937eab1cbfce5e9a5 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: 689c8e04277f3ad631e60fe2a08e41d411daf8eb diff --git a/scripts/clean.sh b/scripts/clean.sh index 4d27cddf..2b9db5d7 100755 --- a/scripts/clean.sh +++ b/scripts/clean.sh @@ -63,6 +63,11 @@ step() { rm -f "$error_file" } +install() { + rm -rf node_modules example/bare/node_modules example/expo/node_modules docs/node_modules + yarn +} + clean_watchman() { watchman watch-del-all 2>/dev/null || true rm -rf $TMPDIR/metro-* @@ -76,7 +81,7 @@ clean_bare() { npx pod-install example/bare } -step "Installing dependencies" "Dependencies installed" yarn +step "Installing dependencies" "Dependencies installed" install step "Cleaning watchman" "Watchman cache cleared" clean_watchman step "Cleaning up simulator cache" "Simulator cache cleared" rm -rf ~/Library/Developer/CoreSimulator/Caches step "Cleaning bare example" "Bare example cleaned" clean_bare