Skip to content
Open
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
24 changes: 23 additions & 1 deletion crates/nsh-platform-android/java/com/longbridge/nsh/Box.java
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,28 @@ private boolean panStarts(float dx, float dy, float slop) {
return true;
}

/**
* Whether a scroll view inside {@code v} that took this touch's DOWN (at {@code downTime}) can move for this
* drag: the drag is its scroll, and the pan takes only what it cannot (a sheet's list scrolls; at its top, a drag
* down moves the sheet). A scroll view under a button that took the touch does not count. iOS shares the touch
* the same way, with the scroll views between the touched view and the pan (`pan_touch`, `pan_scroll_handoff`).
*/
private static boolean scrollTakes(View v, long downTime, float dx, float dy) {
if (!(v instanceof ViewGroup)) return false;
ViewGroup g = (ViewGroup) v;
for (int i = g.getChildCount() - 1; i >= 0; i--) {
View c = g.getChildAt(i);
if (c.getVisibility() != VISIBLE) continue;
if (c instanceof ScrollBox) {
// A scroll view the touch did not go to: nor did it go to anything inside.
if (!((ScrollBox) c).took(downTime)) continue;
if (((ScrollBox) c).scrolls(dx, dy)) return true;
}
if (scrollTakes(c, downTime, dx, dy)) return true;
}
return false;
}

@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
if (pointerEvents == 3) return true;
Expand All @@ -487,7 +509,7 @@ public boolean onInterceptTouchEvent(MotionEvent e) {
if (pannable() && action == MotionEvent.ACTION_MOVE && !panning) {
float slop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
float dx = e.getRawX() - downRawX, dy = e.getRawY() - downRawY;
if (panStarts(dx, dy, slop)) {
if (panStarts(dx, dy, slop) && !scrollTakes(this, e.getDownTime(), dx, dy)) {
// The DOWN went to the child: the pan starts with this move
// (which onTouchEvent never sees), so a quick flick still
// pans and ends.
Expand Down
17 changes: 17 additions & 0 deletions crates/nsh-platform-android/java/com/longbridge/nsh/ScrollBox.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ final class ScrollBox extends FrameLayout implements Node {
private boolean pulling;
private float startY;
private boolean touching;
/** The down time of the touch whose DOWN this view took (-1: none yet). */
private long downTime = -1;
private final Runnable settled = this::settled;

// `ScrollBehavior`; offsets and lengths in dp.
Expand Down Expand Up @@ -287,6 +289,18 @@ private boolean fling(int velocity) {
return true;
}

/** Whether this view took the DOWN of the touch that went down at {@code downTime}. */
boolean took(long downTime) {
return this.downTime == downTime;
}

/** Whether a drag by ({@code dx}, {@code dy}) px along this view's axis has content to move. */
boolean scrolls(float dx, float dy) {
if (scroller == null || !enabled) return false;
if (horizontal) return Math.abs(dx) > Math.abs(dy) && scroller.canScrollHorizontally(dx < 0 ? 1 : -1);
return Math.abs(dy) > Math.abs(dx) && scroller.canScrollVertically(dy < 0 ? 1 : -1);
}

/** Glides to the snap point for a release at {@code velocity} (px/s). */
private void snap(int velocity) {
if (scroller instanceof ZScroll) return;
Expand Down Expand Up @@ -1008,6 +1022,9 @@ public boolean dispatchTouchEvent(MotionEvent e) {
} else {
handled = super.dispatchTouchEvent(e);
}
// For a pan around this view (Box.scrollTakes): a touch that a view
// above took never comes here, and leaves the time of an older one.
if (action == MotionEvent.ACTION_DOWN) downTime = handled ? e.getDownTime() : -1;
if (action == MotionEvent.ACTION_UP && tapMode >= 0 && !dragging
&& (tapMode == 0 || !handlesTap(this, e.getX(), e.getY()))) {
hideKeyboard();
Expand Down
35 changes: 35 additions & 0 deletions examples/gallery/tests/fixtures/sheet-scroll.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { mount, platform, Scroll, signal, Text, View } from "nsh";
import { Sheet } from "nsh/base";

// A swipe-to-close sheet holding a list taller than the sheet, and a button
// placed over the list.
function App() {
const open = signal(true);
const offset = signal(0);
return (
<View style={{ flex: 1, paddingTop: 80, paddingHorizontal: 24 }}>
<Text>{open() ? "sheet: open" : "sheet: closed"}</Text>
<Text>{offset() > 0 ? "list: scrolled" : "list: at top"}</Text>
<Text>{`Platform: ${platform.os}`}</Text>
<Sheet open={open()} onOpenChange={({ open: o }) => open.set(o)} closeOnSwipe>
<View style={{ maxHeight: 400 }}>
<Scroll testID="rows" onScroll={(e) => offset.set(e.offset.y)} style={{ flexShrink: 1 }}>
{Array.from({ length: 40 }, (_, i) => (
<View style={{ height: 44, justifyContent: "center", paddingHorizontal: 16 }}>
<Text>{`Row ${i + 1}`}</Text>
</View>
))}
</Scroll>
<View
onPress={() => {}}
style={{ position: "absolute", left: 16, right: 16, bottom: 16, height: 48, justifyContent: "center", alignItems: "center", backgroundColor: "#dddddd" }}
>
<Text>Button over the list</Text>
</View>
</View>
</Sheet>
</View>
);
}

mount(App);
30 changes: 30 additions & 0 deletions examples/gallery/tests/sheet-scroll.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// A list in a swipe-to-close sheet scrolls; at its top, a drag down still
// closes the sheet, as does a drag on a button over the list.
import { expect, open, test } from "nsh/test";

test("a drag up on a sheet's list scrolls the list", async () => {
const app = await open("./fixtures/sheet-scroll.tsx");
await app.drag("Row 4", { x: 0, y: -200 });
await app.waitFor("text:list: scrolled");
expect(app.texts()).toContain("sheet: open");
});

test("a drag down on a sheet's list at its top closes the sheet", async () => {
const app = await open("./fixtures/sheet-scroll.tsx");
// Android's for now (a headless run does not close the sheet on this drag).
if (!app.shows("Platform: android")) return;
await app.drag("Row 4", { x: 0, y: 300 });
await app.waitFor("text:sheet: closed");
});

test("a drag down on a button over a scrolled list moves the sheet", async () => {
const app = await open("./fixtures/sheet-scroll.tsx");
// Android's for now, as the case above.
if (!app.shows("Platform: android")) return;
await app.drag("Row 4", { x: 0, y: -200 });
await app.waitFor("text:list: scrolled");
// The button takes the touch, not the list under it: the list does not
// scroll, so the drag moves the sheet.
await app.drag("Button over the list", { x: 0, y: 300 });
await app.waitFor("text:sheet: closed");
});
Loading