Skip to content
Closed
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
2 changes: 2 additions & 0 deletions apps/common-app/src/new_api/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import PinchExample from './simple/pinch';
import RotationExample from './simple/rotation';
import TapExample from './simple/tap';
import KeyboardShouldPersistTapsExample from './tests/keyboardShouldPersistTaps';
import MovedDetectorExample from './tests/movedDetector';
import NativeInterceptCancellationExample from './tests/nativeInterceptCancellation';
import NestedPressablesExample from './tests/nestedPressables';
import NestedRootViewExample from './tests/nestedRootView';
Expand Down Expand Up @@ -128,6 +129,7 @@ export const NEW_EXAMPLES: ExamplesSection[] = [
},
{ name: 'Pointer Type', component: PointerTypeExample },
{ name: 'Reattaching', component: ReattachingExample },
{ name: 'Moved detector', component: MovedDetectorExample },
{ name: 'Modal with Nested Root View', component: NestedRootViewExample },
{ name: 'Nested pressables', component: NestedPressablesExample },
{ name: 'Nested touchables', component: NestedTouchablesExample },
Expand Down
115 changes: 115 additions & 0 deletions apps/common-app/src/new_api/tests/movedDetector/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React, { useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { GestureDetector, usePanGesture } from 'react-native-gesture-handler';

import { COLORS, useIndexedLogger } from '../../../common';

// Grabbing a row moves it to the top while its pan is active, the way a
// drag-to-reorder list moves a held row. Fabric moves the row's view by
// removing it and adding it back, so the detector is off the window for a
// moment. The pan must stay active through that and end only on release: the
// status reads "ended", not "cancelled".

type RowData = { id: string; color: string };

const INITIAL_ROWS: RowData[] = [
{ id: 'A', color: COLORS.NAVY },
{ id: 'B', color: COLORS.RED },
{ id: 'C', color: COLORS.GREEN },
{ id: 'D', color: COLORS.PURPLE },
];

type RowProps = RowData & {
onGrab: (id: string) => void;
onRelease: (canceled: boolean) => void;
log: (message: string) => void;
};

function Row({ id, color, onGrab, onRelease, log }: RowProps) {
const pan = usePanGesture({
runOnJS: true,
onActivate: () => {
log(`${id}: onActivate`);
onGrab(id);
},
onDeactivate: (event) => {
log(`${id}: onDeactivate, canceled: ${event.canceled}`);
onRelease(event.canceled);
},
});

return (
<GestureDetector gesture={pan}>
<View
testID={`row-${id}`}
style={[styles.row, { backgroundColor: color }]}>
<Text style={styles.rowText}>Drag row {id}</Text>
</View>
</GestureDetector>
);
}

export default function MovedDetectorExample() {
const [rows, setRows] = useState(INITIAL_ROWS);
const [status, setStatus] = useState('idle');
const log = useIndexedLogger();

const onGrab = (id: string) => {
setStatus('active');
setRows((current) => [
...current.filter((row) => row.id === id),
...current.filter((row) => row.id !== id),
]);
};

const onRelease = (canceled: boolean) => {
setStatus(canceled ? 'cancelled' : 'ended');
};

return (
<View style={styles.container}>
<Text style={styles.hint}>
Drag any row but the top one and keep moving. It moves to the top as the
pan starts. When you let go, the status must read &quot;ended&quot;, not
&quot;cancelled&quot;.
</Text>
<Text testID="status" style={styles.status}>
Pan: {status}
</Text>
{rows.map((row) => (
<Row
key={row.id}
{...row}
onGrab={onGrab}
onRelease={onRelease}
log={log}
/>
))}
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
hint: {
marginBottom: 10,
},
status: {
fontSize: 18,
marginBottom: 20,
},
row: {
height: 80,
borderRadius: 12,
marginBottom: 12,
justifyContent: 'center',
alignItems: 'center',
},
rowText: {
color: 'white',
fontSize: 16,
},
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.swmansion.gesturehandler.react

import android.content.Context
import android.os.Handler
import android.os.Looper
import android.view.MotionEvent
import android.view.View
import com.facebook.react.bridge.ReadableArray
Expand Down Expand Up @@ -31,6 +33,17 @@ class RNGestureHandlerDetectorView(context: Context) : ReactViewGroup(context) {
// intercepting ancestor reuses the intercepted event, so it matches that event's time.
private var lastNativeCancelTime = -1L

// Fabric moves a view (a reorder, or reparenting when an ancestor stops being flattened) by
// removing it and adding it back within one mount, so the view is off the window only for that
// moment. Detaching right away would cancel a gesture in progress, so the detach waits until
// after the mount and is skipped if the view is back on the window by then.
private val mainHandler = Handler(Looper.getMainLooper())
private val detachIfStillDetached = Runnable {
if (!isAttachedToWindow) {
detachAllHandlers()
}
}

data class VirtualChildren(val handlerTags: List<Int>, val viewTag: Int)

fun setHandlerTags(handlerTags: ReadableArray?) {
Expand All @@ -42,14 +55,15 @@ class RNGestureHandlerDetectorView(context: Context) : ReactViewGroup(context) {

override fun onAttachedToWindow() {
super.onAttachedToWindow()
mainHandler.removeCallbacks(detachIfStillDetached)
if (moduleId != -1) {
attachHandlers(handlersToAttach)
attachVirtualChildren(virtualChildrenToAttach)
}
}

override fun onDetachedFromWindow() {
detachAllHandlers()
mainHandler.post(detachIfStillDetached)
super.onDetachedFromWindow()
}

Expand Down Expand Up @@ -273,6 +287,8 @@ class RNGestureHandlerDetectorView(context: Context) : ReactViewGroup(context) {
}

fun detachAllHandlers() {
// A view dropped with a detach pending must not lose the handlers it gets when it's reused.
mainHandler.removeCallbacks(detachIfStillDetached)
RNGestureHandlerModule.registries[moduleId]?.let { registry ->
registry.cancelAllObservationsForOwner(this)
for (tag in attachedHandlers) {
Expand Down