React-native-gesture-handler: State.END kept firing

Created on 24 Mar 2020  路  5Comments  路  Source: software-mansion/react-native-gesture-handler

I'm trying to implement drag and drop feature using react-native-reanimated + react-native-gesture-handler. Everything is working fine except an event. The problem is an event (State.END) not stop triggering. Here is my code:

import * as React from "react";
import { RecyclerListView, LayoutProvider, DataProvider } from "recyclerlistview";
import Animated from "react-native-reanimated";
import { LayoutChangeEvent, Dimensions } from "react-native";
import { PanGestureHandler, State } from "react-native-gesture-handler";

const { cond, eq, add, call, Value, event, or } = Animated;

interface Props<T> {
    rowHeight: number;
    data: T[];
    indexToKey: (index: number) => string;
        renderRow: (
        data: T,
        index: number,
        state: "normal" | "dragging" | "placeholder",
        dragHandle: JSX.Element
    ) => JSX.Element | null;
    renderDragHandle: () => JSX.Element;
    onSort: (newData: T[]) => void;
}

interface RState {
    dataProvider: DataProvider;
    dragging: boolean;
    draggingIdx: number;
}

function immutableMove(arr, from, to) {
    return arr.reduce((prev, current, idx, self) => {
        if (from === to) {
            prev.push(current);
        }

        if (idx === from) {
            return prev;
        }

        if (from < to) {
            prev.push(current);
        }

        if (idx === to) {
            prev.push(self[from]);
        }

        if (from > to) {
            prev.push(current);
        }
        return prev;
    }, []);
}

export class SortableList<T> extends React.PureComponent<Props<T>, RState> {
    list = React.createRef<RecyclerListView<any, any>>();
    _layoutProvider: LayoutProvider;
    rowCenterY: Animated.Node<number>;
    absoluteY = new Value(0);
    gestureState = new Value(-1);
    onGestureEvent: any;
    halfRowHeightValue: Animated.Value<number>;
    currIdx = -1;
    scrollOffset = 0;
    flatlistHeight = 0;
    topOffset = 0;
    scrolling = false;

    constructor(props: Props<T>) {
        super(props);

        this.halfRowHeightValue = new Value((-props.rowHeight / 2) - 190);
        const { width } = Dimensions.get("window");

        this.onGestureEvent = event([{
            nativeEvent: {
                absoluteY: this.absoluteY,
                state: this.gestureState
            }
        }]);

        this.rowCenterY = add(this.absoluteY, this.halfRowHeightValue);

        this._layoutProvider = new LayoutProvider(
            index => {
                return 1;
            },
            (type, dim) => {
                dim.width = width;
                dim.height = props.rowHeight;
            }
        );

        const dataProvider = new DataProvider((row1, row2) => {
            return row1.borrower_id !== row2.borrower_id;
        }, props.indexToKey);

        this.state = {
            dataProvider: dataProvider.cloneWithRows(props.data),
            dragging: false,
            draggingIdx: -1
        };
    }

    componentDidUpdate(prevProps) {
        if (prevProps.data !== this.props.data) {
            this.setState({
                dataProvider: this.state.dataProvider.cloneWithRows(this.props.data)
            });
        }
    }

    handleScroll = (_, __, offsetY: number) => {
        this.scrollOffset = offsetY;
    };

    handleLayout = (e: LayoutChangeEvent) => {
        this.flatlistHeight = e.nativeEvent.layout.height;
        this.topOffset = 190;
    };

    yToIndex = (y: number) =>
        Math.min(
            this.state.dataProvider.getSize() - 1,
            Math.max(
                0,
                Math.floor(
                    (y + this.scrollOffset - this.topOffset) / this.props.rowHeight
                )
            )
        );

    moveList = amount => {
        if (!this.scrolling) {
            return;
        }

        this.list.current.scrollToOffset(
            this.scrollOffset + amount,
            this.scrollOffset + amount,
            false
        );

        requestAnimationFrame(() => {
            this.moveList(amount);
        });
    };

    updateOrder = y => {
        const newIdx = this.yToIndex(y);
        if (this.currIdx !== newIdx) {
            this.setState({
                dataProvider: this.state.dataProvider.cloneWithRows(
                    immutableMove(
                        this.state.dataProvider.getAllData(),
                        this.currIdx,
                        newIdx
                    )
                ),
                draggingIdx: this.yToIndex(y)
            });
            this.currIdx = newIdx;
        }
    };

    start = ([y]) => {
        this.currIdx = this.yToIndex(y);
        this.setState({ dragging: true, draggingIdx: this.currIdx });
    };

    reset = () => {
        const newData = this.state.dataProvider.getAllData();
        this.setState({
            dataProvider: this.state.dataProvider.cloneWithRows(newData),
            dragging: false,
            draggingIdx: -1
        });
        this.scrolling = false;
        this.currIdx = -1;
        console.log('entered');
        // this.props.onSort(newData);
    };

    move = ([y]) => {
        if (y + 100 > this.flatlistHeight) {
            if (!this.scrolling) {
                this.scrolling = true;
                this.moveList(20);
            }
        } else if (y < 100) {
            if (!this.scrolling) {
                this.scrolling = true;
                this.moveList(-20);
            }
        } else {
            this.scrolling = false;
        }
        this.updateOrder(y);
    };

    _rowRenderer = (type, data, index) => {
        return this.props.renderRow(
            data,
            index,
            this.state.draggingIdx === index ? "placeholder" : "normal",
            <PanGestureHandler
                maxPointers={1}
                onGestureEvent={this.onGestureEvent}
                onHandlerStateChange={this.onGestureEvent} >
                <Animated.View>{this.props.renderDragHandle()}</Animated.View>
            </PanGestureHandler>
        );
    };

    render() {
        const { dataProvider, dragging, draggingIdx } = this.state;

        return (
            <>
                <Animated.Code>
                    {() =>
                        cond(
                            eq(this.gestureState, State.BEGAN),
                            call([this.absoluteY], this.start)
                        )
                    }
                </Animated.Code>
                <Animated.Code>
                    {() =>
                        cond(
                            or(
                                eq(this.gestureState, State.END),
                                eq(this.gestureState, State.CANCELLED),
                                eq(this.gestureState, State.FAILED),
                                eq(this.gestureState, State.UNDETERMINED)
                            ),
                            call([], this.reset)
                        )
                    }
                </Animated.Code>
                <Animated.Code>
                    {() =>
                        cond(
                            eq(this.gestureState, State.ACTIVE),
                            call([this.absoluteY], this.move)
                        )
                    }
                </Animated.Code>
                {dragging ? (
                    <Animated.View
                        style={{
                            top: this.rowCenterY,
                            position: "absolute",
                            width: "100%",
                            zIndex: 99,
                            elevation: 99
                        }} >
                        {this.props.renderRow(
                            dataProvider.getDataForIndex(draggingIdx),
                            draggingIdx,
                            "dragging",
                            this.props.renderDragHandle()
                        )}
                    </Animated.View>
                ) : null}

                <RecyclerListView
                    ref={this.list}
                    style={{ flex: 1 }}
                    onScroll={this.handleScroll}
                    onLayout={this.handleLayout}
                    layoutProvider={this._layoutProvider}
                    dataProvider={dataProvider}
                    rowRenderer={this._rowRenderer}
                    extendedState={{ dragging: true }} />
            </>
        );
    }
}

and here is my dependencies:

  "dependencies": {
    "@react-native-community/async-storage": "^1.7.1",
    "@react-native-community/datetimepicker": "^2.1.0",
    "@react-native-community/netinfo": "^5.5.0",
    "@sendgrid/mail": "^6.4.0",
    "native-base": "^2.13.8",
    "react": "16.9.0",
    "react-native": "0.61.2",
    "react-native-gesture-handler": "^1.4.1",
    "react-native-linear-gradient": "^2.5.6",
    "react-native-loading-spinner-overlay": "^1.0.1",
    "react-native-material-dropdown": "^0.11.1",
    "react-native-material-menu": "^1.0.0",
    "react-native-reanimated": "^1.7.0",
    "react-native-screens": "^2.4.0",
    "react-native-searchbar": "^1.16.0",
    "react-native-vector-icons": "^6.6.0",
    "react-navigation": "^4.0.10",
    "react-navigation-stack": "^1.9.4",
    "realm": "^4.0.0-beta.0",
    "recyclerlistview": "^3.0.0"
  },

The State.END keep firing the this.reset . Can anyone help me out! Thank you.

Note: (this code working fine in Expo. Seeing this problem on RN Cli, And i'm testing on real android device)

Most helpful comment

The same here. It seems that after END handler should switch to UNDETERMINED, but actually he doesn't.

All 5 comments

The same here. It seems that after END handler should switch to UNDETERMINED, but actually he doesn't.

I am facing the same situation here. UNDETERMINED state is only fired at my block when I start the app. After that, it is always END.

@rassemdev have you resolved this issue? Im also facing this problem

@chuanhd no i did not. it did not effect that much to my functionality. I think it do no trigger in production.

I am facing this issue too, the handler state stuck on END and does not reset to UNDETERMINED. Guess I am 5 months late. Wonder if anyone managed to solve this?

Running on RN, not expo, in version 1.6.

Edit:
Hijacking this issue to share what I did to solve this. Context, I needed the state END to fire to call something once, assuming it will only fire once. I ended up using an Animated.Value that holds a 0 or 1, signifying true and false. It will only be toggled to be 1 when the state enters ACTIVE. It will be reset to 0 when the once only method runs its course. This is sort of a state machine in its own right.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

neiker picture neiker  路  3Comments

enahum picture enahum  路  4Comments

brunolemos picture brunolemos  路  3Comments

Agoujil2saad picture Agoujil2saad  路  3Comments

brentvatne picture brentvatne  路  4Comments