Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,10 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
viewController.insetAdjustment = insetAdjustment
}

fun setScrollable(scrollable: Boolean) {
viewController.scrollable = scrollable
}

// ==================== State Management ====================

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -297,6 +302,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
// Create coordinator layout
coordinatorLayout = TrueSheetCoordinatorLayout(reactContext).apply {
delegate = this@TrueSheetViewController
scrollable = [email protected]
}

sheetView = TrueSheetBottomSheetView(reactContext).apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand All @@ -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
}
}
4 changes: 0 additions & 4 deletions docs/docs/guides/scrolling.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
27 changes: 0 additions & 27 deletions docs/docs/troubleshooting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>()

return (
<TrueSheet
detents={[0.5, 1]}
onLayout={(e) => setMinHeight(e.nativeEvent.layout.height + 1)}
>
<ScrollView style={{ minHeight }} nestedScrollEnabled>
<View />
</ScrollView>
</TrueSheet>
)
```

## 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.
Expand Down
4 changes: 2 additions & 2 deletions example/bare/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2646,7 +2646,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- RNTrueSheet (3.6.6):
- RNTrueSheet (3.6.9):
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -3095,7 +3095,7 @@ SPEC CHECKSUMS:
RNGestureHandler: e1cf8ef3f11045536eed6bd4f132b003ef5f9a5f
RNReanimated: f1868b36f4b2b52a0ed00062cfda69506f75eaee
RNScreens: d821082c6dd1cb397cc0c98b026eeafaa68be479
RNTrueSheet: 5c0b9f0651f07167ceed0cea987a4ec653b4706d
RNTrueSheet: 66d29463562c7ba9d9679f5d7af46b8ca8ec5f46
RNWorklets: d9c050940f140af5d8b611d937eab1cbfce5e9a5
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: 689c8e04277f3ad631e60fe2a08e41d411daf8eb
Expand Down
7 changes: 6 additions & 1 deletion scripts/clean.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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-*
Expand All @@ -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
Expand Down