React-native-draggable-flatlist: Draggable Flatlist inside a ScrollView doesn't account for scrolled offset

Created on 6 Sep 2018  路  25Comments  路  Source: computerjazz/react-native-draggable-flatlist

When nesting a DraggableFlatlist inside a ScrollView the proxy components rendered while dragging are not positioned relative to the scrolled position of the parent container (the ScrollView).

This means they either disappear or render offset from the mouse/touch position by the scrolled amount.

In the below test case, I'm offsetting the position of the draggable flatlist using content container padding on the ScrollView. Dragging/re-ordering still works by disabling the scrollview with onMoveBegin

I would guess that you need to position the proxy component absolutely to the touch position within the screen, not just the view.

import React, { PureComponent } from 'react';
import { ScrollView, Text, TouchableOpacity } from 'react-native';
import DraggableFlatList from 'react-native-draggable-flatlist';

export default class Test extends PureComponent {
  state = {
    scrollEnabled: true,
    data: [
      { label: '1', key: '1' },
      { label: '2', key: '2' },
      { label: '3', key: '3' },
      { label: '4', key: '4' },
    ],
  };

  renderItem = ({
    item, isActive, index, move, moveEnd,
  }) => (
    <TouchableOpacity
      activeOpacity={0.9}
      style={{
        flex: 1,
        alignItems: 'center',
        height: 60,
        borderColor: isActive ? 'white' : 'black',
        borderWidth: 2,
      }}
      onLongPress={move}
      onPressOut={moveEnd}
    >
      <Text style={{ fontSize: 30, color: 'white' }}>
        {item.label}
      </Text>
    </TouchableOpacity>
  );

  render() {
    return (
      <ScrollView
        style={{ backgroundColor: '#000' }}
        contentContainerStyle={{ paddingTop: 800, paddingBottom: 100 }}
        scrollEnabled={this.state.scrollEnabled}
      >
        <DraggableFlatList
          scrollPercent={5}
          data={this.state.data}
          renderItem={this.renderItem}
          keyExtractor={item => `draggable-item-${item.key}`}
          onMoveBegin={() => this.setState({ scrollEnabled: false })}
          onMoveEnd={({ data }) => {
            this.setState({ scrollEnabled: true, data });
          }}
        />
      </ScrollView>
    );
  }
}

Most helpful comment

I have a proposed fix that works for me. It adds an optional prop called scrollingContainerOffset that allows us to inject the offset of the surrounding scrollview container. Starting on onStartShouldSetPanResponderCapture line 88:

if (this.props.scrollingContainerOffset !== undefined) {
    this._offset.setValue((this._additionalOffset + this._containerOffset - this.props.scrollingContainerOffset - this._androidStatusBarOffset) * -1)
} else {
    this._offset.setValue((this._additionalOffset + this._containerOffset - this._androidStatusBarOffset) * -1)
}

To calculate it properly you'd need to setup a scrollview with the following setup in the app itself so that your app will pass the prop to Draggable Flatlist.

state = {
    ...
    scrollOffset: 0,
    ...
};

render() {
    return (
        <ScrollView
            onScrollEndDrag={({ nativeEvent }) => { this.setState({ scrollOffset: nativeEvent.contentOffset['y'] }); }}
            onMomentumScrollEnd={({ nativeEvent }) => { this.setState({scrollOffset: nativeEvent.contentOffset['y']}); }}
        >
            <DraggableFlatList
                ...
                scrollingContainerOffset={this.state.scrollOffset}
                ...
            />
       </ScrollView>
    );
}

All 25 comments

hm yeah, nested scrollable items get really tricky. this wasn't a use case I had in mind.

@samjt Did you figure out a solution for this?

No, afraid not, I redesigned to avoid it

What's the suggested approach for this? this issue is breaking our design

Any solution yet?

I have the same issue, and I wonder if this means the component only works if your list is never longer than a screen's height? Or did I misinterpret that? I have a:

 View (flex:1)
   ScrollView
      DraggableFlatList
      something else
      FlatList
   /ScrollView
 /View

I see that exact same issue others have mentioned above. Is there a way we can inject the scroll offset from the parent scroll view somewhere in your code? I am happy to adjust the library, but you probably know best where that offset would/should go, no? Your library improves on react-native-sortable-list because you're using FlatList and performance seems much better.

Thanks!

@samjt could you elaborate on your redesign solution?

I meant I moved the draggable list to a different separate screen

Ahh fair ok.

I have a proposed fix that works for me. It adds an optional prop called scrollingContainerOffset that allows us to inject the offset of the surrounding scrollview container. Starting on onStartShouldSetPanResponderCapture line 88:

if (this.props.scrollingContainerOffset !== undefined) {
    this._offset.setValue((this._additionalOffset + this._containerOffset - this.props.scrollingContainerOffset - this._androidStatusBarOffset) * -1)
} else {
    this._offset.setValue((this._additionalOffset + this._containerOffset - this._androidStatusBarOffset) * -1)
}

To calculate it properly you'd need to setup a scrollview with the following setup in the app itself so that your app will pass the prop to Draggable Flatlist.

state = {
    ...
    scrollOffset: 0,
    ...
};

render() {
    return (
        <ScrollView
            onScrollEndDrag={({ nativeEvent }) => { this.setState({ scrollOffset: nativeEvent.contentOffset['y'] }); }}
            onMomentumScrollEnd={({ nativeEvent }) => { this.setState({scrollOffset: nativeEvent.contentOffset['y']}); }}
        >
            <DraggableFlatList
                ...
                scrollingContainerOffset={this.state.scrollOffset}
                ...
            />
       </ScrollView>
    );
}

@spacewaffle wow, it works for me.tnx

@spacewaffle you saved my day 鉂わ笍

@spacewaffle

I have a proposed fix that works for me. It adds an optional prop called scrollingContainerOffset that allows us to inject the offset of the surrounding scrollview container. Starting on onStartShouldSetPanResponderCapture line 88:

if (this.props.scrollingContainerOffset !== undefined) {
    this._offset.setValue((this._additionalOffset + this._containerOffset - this.props.scrollingContainerOffset - this._androidStatusBarOffset) * -1)
} else {
    this._offset.setValue((this._additionalOffset + this._containerOffset - this._androidStatusBarOffset) * -1)
}

To calculate it properly you'd need to setup a scrollview with the following setup in the app itself so that your app will pass the prop to Draggable Flatlist.

state = {
    ...
    scrollOffset: 0,
    ...
};

render() {
    return (
        <ScrollView
            onScrollEndDrag={({ nativeEvent }) => { this.setState({ scrollOffset: nativeEvent.contentOffset['y'] }); }}
            onMomentumScrollEnd={({ nativeEvent }) => { this.setState({scrollOffset: nativeEvent.contentOffset['y']}); }}
        >
            <DraggableFlatList
                ...
                scrollingContainerOffset={this.state.scrollOffset}
                ...
            />
       </ScrollView>
    );
}

Did you tried this on iOS ? Because I fork your repo (I see that there is your modifications), I have added the scrollOffset in my DraggableFlatList but there is still the bug :( !

In my case it is more complex because I have in my scrollview a map of a component who has a draggable flatlist... But it should work right ?

It works on iOS. Using the fix in production for both iOS and android

@LucasLpr I built primarily for iOS so yeah it should work. Did you remember to set a state like scrollOffset with onScrollEndDrag or onMomentumScrollEnd that you can pass to scrollingContainerOffset? My thinking is the nesting might be messing up what you're doing but hard to say without looking at it. Just make sure whatever component is scrolling sets a scrollOffset of some kind, then pass that state through props as many times as you need to get it to the DraggableFlatList so you can set scrollingContainerOffset. Also keep in mind my fix only works for vertical scrolling since it only captures the y axis, but would be pretty simple to capture x too if that were necessary.

@LucasLpr I built primarily for iOS so yeah it should work. Did you remember to set a state like scrollOffset with onScrollEndDrag or onMomentumScrollEnd that you can pass to scrollingContainerOffset? My thinking is the nesting might be messing up what you're doing but hard to say without looking at it. Just make sure whatever component is scrolling sets a scrollOffset of some kind, then pass that state through props as many times as you need to get it to the DraggableFlatList so you can set scrollingContainerOffset. Also keep in mind my fix only works for vertical scrolling since it only captures the y axis, but would be pretty simple to capture x too if that were necessary.

@spacewaffle Hello, yeah of course I did that and it is a vertical scroll as well ;) ! In my case I did something like that

state = {
    ...
    scrollOffset: 0,
    ...
};

render() {
    return (
        <ScrollView
            onScrollEndDrag={({ nativeEvent }) => { this.setState({ scrollOffset: nativeEvent.contentOffset['y'] }); }}
            onMomentumScrollEnd={({ nativeEvent }) => { this.setState({scrollOffset: nativeEvent.contentOffset['y']}); }}
        >

Object.entries(myArray).map(([key, value], index) => {
    return (
        <MyComponentLooped  value={value}
            scrollOffset={this.state.scrollOffset}
            ...SomeOthersProps
                />
    )
}
       </ScrollView>
    );
}

And In that component (MyComponentLooped) :

render() {
    return (
        <Container>
            <SomeStuff>
            </SomeStuff>
            <DraggableContainer>
                <DraggableFlatList ...
                    scrollingContainerOffset={this.props.scrollOffset}/>
            </DraggableContainer>
        </Container>
    )
}

I am having the same problem, implemented the recommended fix (thanks @spacewaffle), but still have an issue with the offset. It works correctly until I scroll down, then the rows appear offset by about 3 rows above where the drag and drop is actually occuring. Here is an excerpt of my code, maybe someone can see where i've gone wrong. Cheers!

constructor(props) {
    super(props);
    this.state = {
        scrollOffset: 0,
        scrollEnabled: true
    }
}

renderRow = ({ item, index, move, moveEnd, isActive }) => {
    return <QuestionRow question={item} key={index} move={move} moveEnd={moveEnd} isActive={isActive} />
}

render() {
    return (
        <ScrollView
            onScrollEndDrag={({ nativeEvent }) => { this.setState({ scrollOffset: nativeEvent.contentOffset.y }) }}
            onMomentumScrollEnd={({ nativeEvent }) => { this.setState({ scrollOffset: nativeEvent.contentOffset.y }) }}
            scrollEnabled={this.state.scrollEnabled}
        >
            <DraggableFlatList
                scrollEnabled={false}
                scrollingContainerOffset={this.state.scrollOffset}
                data={this.props.inspection.sections[this.props.section].questions}
                renderItem={this.renderRow}
                keyExtractor={(item, index) => 'draggable-item-${index}'}
                scrollPercent={5}
                onMoveBegin={() => this.setState({ scrollEnabled: false })}
                onMoveEnd={({ data }) => this.updateQuestions(data)}
            />
            <TouchableOpacity onPress={this.addNewQuestion.bind(this)}>
                <AddQuestion>
                    <AddQuestionButton>Add New Question</AddQuestionButton>
                </AddQuestion>
            </TouchableOpacity>
        </ScrollView>
    )
}

I have a proposed fix that works for me. It adds an optional prop called scrollingContainerOffset that allows us to inject the offset of the surrounding scrollview container. Starting on onStartShouldSetPanResponderCapture line 88:

if (this.props.scrollingContainerOffset !== undefined) {
    this._offset.setValue((this._additionalOffset + this._containerOffset - this.props.scrollingContainerOffset - this._androidStatusBarOffset) * -1)
} else {
    this._offset.setValue((this._additionalOffset + this._containerOffset - this._androidStatusBarOffset) * -1)
}

I can't find this spot (onStartShouldSetPanResponderCapture ) anywhere in the code. Is this still a relevant fix or has another fix been proposed ?

I'd be open to adding a container offset prop, but it should be a reanimated Animated.Value, not a number for optimal performance

Is there a reason that this has not been done as a pull request? I can create the pull request if needed... just not clear on whether there's a reason this hasn't actually been implemented, given how old it is... the suggested work around works, seems to have virtually no chance of causing other conflicts, etc.

I'm not working on react native anymore and don't have the time to implement this properly as a pr, but anyone else can feel free to work from my fork and polish it here:
https://github.com/spacewaffle/react-native-draggable-flatlist

It's not up to date with master but the only commit I've made to it is for the fix. I didn't account for horizontal scrolling, only vertical scrolling, so you might need to give that a once over, but otherwise feel free to grab it and do what you will with it.

Hello, has the solution been implemented?

I have the same issue, and I wonder if this means the component only works if your list is never longer than a screen's height? Or did I misinterpret that? I have a:

 View (flex:1)
   ScrollView
      DraggableFlatList
      something else
      FlatList
   /ScrollView
 /View

I see that exact same issue others have mentioned above. Is there a way we can inject the scroll offset from the parent scroll view somewhere in your code? I am happy to adjust the library, but you probably know best where that offset would/should go, no? Your library improves on react-native-sortable-list because you're using FlatList and performance seems much better.

Thanks!

Did you get any solution, I am having the same structure

Was there any solution yet?
@spacewaffle Where is onStartShouldSetPanResponderCapture line 88:

I would like to know also a solution for this. :slightly_smiling_face:

Was this page helpful?
0 / 5 - 0 ratings

Related issues

exentrich picture exentrich  路  6Comments

ARDcode picture ARDcode  路  9Comments

ethanloh21 picture ethanloh21  路  7Comments

ButuzGOL picture ButuzGOL  路  8Comments

gazedash picture gazedash  路  5Comments