iwongu
iwongu

Reputation: 401

How to scroll to bottom of React Native ListView

I'm writing an app using React Native. It has a ListView to show the list of items.

I'm appending new items at the bottom when there are new ones available. I'd like to scroll to bottom in the ListView to show the new items automatically. How can I do this?

Upvotes: 39

Views: 33538

Answers (13)

msahin
msahin

Reputation: 1760

I have combined the simplest solutions which worked for me. With this, ScrollView scrolls to bottom not only with content change but also with height change. Only tested with ios.

<ScrollView
  ref="scrollView"
  onContentSizeChange={(width, height) =>
    this.refs.scrollView.scrollTo({ y: height })
  }
  onLayout={e => {
    const height = e.nativeEvent.layout.height;
    this.refs.scrollView.scrollTo({ y: height });
    }
  }
>
  ......
</ScrollView>

"react-native": "0.59.10",

Upvotes: 0

Somwang Souksavatd
Somwang Souksavatd

Reputation: 5085

extended from @jonasb try this code below, use onContentSizeChange event to execute scrollToEnd method

<ListView
  ref={ ( ref ) => this.scrollView = ref }
  dataSource={yourDataSource}
  renderRow={yourRenderingCallback}
  onContentSizeChange={ () => {        
     this.scrollView.scrollToEnd( { animated: true })
  }} 
</ListView>

Upvotes: 0

David
David

Reputation: 1591

So in order to make an automatic scrolling to the bottom of ListView, you should add prop 'onContentSizeChange' to ListView and take respective content from its argument, like this:

<ListView
              ref='listView'
              onContentSizeChange={(contentWidth, contentHeight) => {
                this.scrollTo(contentHeight);
               }}
              ...
            />

So for my case, I should render my list vertically that is why I used contentHeight, in case of horizontal listing you just had to use contentWeight.

Here scrollTo function should be:

scrollTo = (y) => {
if (y<deviceHeight-120) {
  this.refs.listView.scrollTo({ y: 0, animated: true })
}else{
  let bottomSpacing = 180;
  if (this.state.messages.length > 0) {
    bottomSpacing = 120;
  }

  this.refs.listView.scrollTo({ y: y - deviceHeight + bottomSpacing, animated: true })
}

}

That's it. I hope my this explanation could help someone to save time.

Upvotes: 0

jonasb
jonasb

Reputation: 1875

As of React Native 0.41 both ListView and ScrollView have scrollToEnd() methods. ListView's is documented at https://facebook.github.io/react-native/docs/listview.html#scrolltoend

You'll need to use a ref to store a reference to your ListView when you render it:

<ListView
  dataSource={yourDataSource}
  renderRow={yourRenderingCallback}
  ref={listView => { this.listView = listView; }}
</ListView>

Then, you can just call this.listView.scrollToEnd() to scroll to the bottom of the list. If you'd like to do this every time the content of the ListView changes (for instance, when content is added), then do it from within the ListView's onContentSizeChange prop callback, just like with a ScrollView.

Upvotes: 12

lazywei
lazywei

Reputation: 12465

Please check this out: https://github.com/650Industries/react-native-invertible-scroll-view

This component inverts the original scroll view. So when new items arrive, it will automatically scroll to the bottom.

However, please note two things

  1. Your "data array" need to be reverted too. That is, it should be

    [new_item, old_item]
    

    and any new-arriving item should be inserted to the front-most.

  2. Though they use ListView in their README example, there are still some flaws when using this plugin with ListView. Instead, I'd suggest you just use the ScrollView, which works pretty well.

A example for the inverted scroll view:

var MessageList = React.createClass({
  propTypes: {
    messages: React.PropTypes.array,
  },

  renderRow(message) {
    return <Text>{message.sender.username} : {message.content}</Text>;
  },

  render() {
    return (
      <InvertibleScrollView
        onScroll={(e) => console.log(e.nativeEvent.contentOffset.y)}
        scrollEventThrottle={200}
        renderScrollView={
          (props) => <InvertibleScrollView {...props} inverted />
        }>
        {_.map(this.props.messages, this.renderRow)}
      </InvertibleScrollView>
    );
  }
});

Upvotes: 3

Stan Luo
Stan Luo

Reputation: 3889

Liked @ComethTheNerd 's solution best, and here's a more regular(passed all airbnb's ESlinting rules) version:

  state={
    listHeight: 0,
    footerY: 0,
  }

  // dummy footer to ascertain the Y offset of list bottom
  renderFooter = () => (
    <View
      onLayout={(event) => {
        this.setState({ footerY: event.nativeEvent.layout.y });
      }}
    />
  );

...

  <ListView
    ref={(listView) => { this.msgListView = listView; }}
    renderFooter={this.renderFooter}
    onLayout={(event) => {
      this.setState({ listHeight: event.nativeEvent.layout.height });
    }}
  />

And invoke the scrollToBottom method like this:

  componentDidUpdate(prevProps, prevState) {
    if (this.state.listHeight && this.state.footerY &&
        this.state.footerY > this.state.listHeight) {
      // if content is longer than list, scroll to bottom
      this.scrollToBottom();
    }
  }

  scrollToBottom = () => {
    const scrollDistance = this.state.footerY - this.state.listHeight;
    this.msgListView.scrollTo({ y: scrollDistance, animated: true });
  }

Upvotes: -1

Paul Rada
Paul Rada

Reputation: 19521

There’s a pretty simple solution to this problem. You can wrap your ListView inside a Scrollview component. This will provide all the necessary methods to determine the bottom position of your list.

First wrap your listView

<ScrollView> 
  <MyListViewElement />
</ScrollView>

Then use the onLayout method that returns the component's (scrollView) height. and save it to the state.

// add this method to the scrollView component
onLayout={ (e) => {

  // get the component measurements from the callbacks event
  const height = e.nativeEvent.layout.height

  // save the height of the scrollView component to the state
  this.setState({scrollViewHeight: height })
}}

Then use the onContentSizeChange method that returns the inner components (listView) height. and save it to the state. This will happen every time you add or remove an element from the list, or change the height. Essentially every time somebody adds a new message to your list.

onContentSizeChange={ (contentWidth, contentHeight) => {

  // save the height of the content to the state when there’s a change to the list
  // this will trigger both on layout and any general change in height
  this.setState({listHeight: contentHeight })

}}

In order to scroll you will need the scrollTo method found inside the ScrollView. You can access this by saving it to the ref in the state like so.

<ScrollView
  ref={ (component) => this._scrollView = component } 
  …
  >
</ScrollView>

Now you have everything you need to calculate and trigger your scroll to the bottom of the list. You can choose to do this anywhere in your component, I’ll add it to the componentDidUpdate() so whenever the component is rendered it will scrollTo the bottom.

  componentDidUpdate(){
    // calculate the bottom
    const bottomOfList =  this.state.listHeight - this.state.scrollViewHeight

    // tell the scrollView component to scroll to it
    this.scrollView.scrollTo({ y: bottomOfList })   
 }

And that’s it. This is how your ScrollView should look like in the end

<ScrollView
  ref={ (component) => this._scrollView = component }

  onContentSizeChange={ (contentWidth, contentHeight) => {
    this.setState({listHeight: contentHeight })
  }}    

  onLayout={ (e) => {
    const height = e.nativeEvent.layout.heigh
    this.setState({scrollViewHeight: height })
  }}
  > 
  <MyListViewElement />
</ScrollView>

A SIMPLER WAY I was using a ListView and found it way easier to do this by using a scrollView, for simplicities sake, I recommend it. Here’s a direct copy of my Messages module for the scroll to bottom function. Hope it helps.

class Messages extends Component {
  constructor(props){
    super(props)
    this.state = {
      listHeight: 0,
      scrollViewHeight: 0
    }
  }
  componentDidUpdate(){
    this.scrollToBottom()
  }
  scrollToBottom(){
    const bottomOfList =  this.state.listHeight - this.state.scrollViewHeight
    console.log('scrollToBottom');
    this.scrollView.scrollTo({ y: bottomOfList })
  }
  renderRow(message, index){
    return (
        <Message
          key={message.id}
          {...message}
        />
    );
  }
  render(){
    return(
      <ScrollView
        keyboardDismissMode="on-drag"
        onContentSizeChange={ (contentWidth, contentHeight) => {
          this.setState({listHeight: contentHeight })
        }}
        onLayout={ (e) => {
          const height = e.nativeEvent.layout.height
          this.setState({scrollViewHeight: height })
        }}
        ref={ (ref) => this.scrollView = ref }>
        {this.props.messages.map(  message =>  this.renderRow(message) )}
      </ScrollView>
    )
  }
}

export default Messages

Upvotes: 8

Lu Sun
Lu Sun

Reputation: 119

For those who are interested in implementing it with merely ListView

// initialize necessary variables
componentWillMount() {
  // initialize variables for ListView bottom focus
  this._content_height = 0;
  this._view_height = 0;
}
render() {
  return (
    <ListView
      ...other props
      onLayout = {ev => this._scrollViewHeight = ev.nativeEvent.layout.height}
      onContentSizeChange = {(width, height)=>{
        this._scrollContentHeight = height;
        this._focusOnBottom();
      }}
      renderScrollComponent = {(props) =>
        <ScrollView
          ref = {component => this._scroll_view = component}
          onLayout = {props.onLayout}
          onContentSizeChange = {props.onContentSizeChange} /> } />
  );
}
_focusOnLastMessage() {
  const bottom_offset = this._content_height - this._view_height;
  if (bottom_offset > 0 &&)
    this._scroll_view.scrollTo({x:0, y:scrollBottomOffsetY, false});
}

You can use _focusOnLastMessage function wherever you want, for instance, I use it whenever the content size changes. I tested the codes with [email protected]

Upvotes: 0

SudoPlz
SudoPlz

Reputation: 23173

I'm afraid theres not an ultra clean way to do this. Therefore we will be doing it manually, don't worry its easy and its not messy.

Step 1: Declare those variables in your state

constructor(props) {
  super(props);
  this.state={
    lastRowY:0,
  }
}

Step 2: Create the scrollToBottom function like so:

scrollToBottom(){
  if(!!this.state.lastRowY){
    let scrollResponder = this.refs.commentList.getScrollResponder();
    scrollResponder.scrollResponderScrollTo({x: 0, y: this.state.lastRowY, animated: true});
  }
}

Step 3: Add the following properties to your ListView:

  • A ref to be able to access it (in this example commentList)

     ref="commentList"
    
  • The following onLayout function within the renderRow, on your row element:

     onLayout={(event) => {
            var {y} = event.nativeEvent.layout;
            this.setState({
                lastRowY : y
            });
    

Your ListView should look something like this:

 <ListView
     ref="commentList"
     style={styles.commentsContainerList}
     dataSource={this.state.commentDataSource}
     renderRow={()=>(
         <View 
             onLayout={(event)=>{
                 let {y} = event.nativeEvent.layout;
                 this.setState({
                     lastRowY : y
                 });
             }}
         />
         </View>
     )}
 />

Step 4: Then anywhere in your code, just call this.scrollToBottom();.

Enjoy..

Upvotes: 1

Luke Rhodes
Luke Rhodes

Reputation: 323

Here's another variation. I'm personally using this to scroll to the bottom of a list view when someone comments. I prefer it to the other examples as it's more concise.

listViewHeight can be determined through various means but I'm personally getting it from an animation that's used to animate the list view height to get out of the way of the keyboard.

render() {
  let initialListSize = 5

  if (this.state.shouldScrollToBottom) {
    initialListSize = 100000 // A random number that's going to be more rows than we're ever going to see in the list.
  }

  return (<ListView
            ref="listView"
            initialListSize={ initialListSize }
            otherProps...
            renderFooter={() => {
              return <View onLayout={(event)=>{

                if (this.state.shouldScrollToBottom) {
                  const listViewHeight = this.state.height._value
                  this.refs.listView.scrollTo({y: event.nativeEvent.layout.y - listViewHeight + 64}) // 64 for iOS to offset the nav bar height

                  this.setState({shouldScrollToBottom: false})
                }
              }}></View>
            }}
          />)
}

scrollToBottom() {
  self.setState({shouldScrollToBottom: true})
}

Upvotes: -1

Darius
Darius

Reputation: 5259

I had the same problem, and came up with this solution:

render()
{
    if("listHeight" in this.state && 
           "footerY" in this.state && 
               this.state.footerY > this.state.listHeight)
    {
        var scrollDistance = this.state.listHeight - this.state.footerY;
        this.refs.list.getScrollResponder().scrollTo(-scrollDistance);
    }

    return (
            <ListView ref="list"

                onLayout={(event) => {

                    var layout = event.nativeEvent.layout;

                    this.setState({
                        listHeight : layout.height
                    });

                }}
                renderFooter={() => {

                    return <View onLayout={(event)=>{

                        var layout = event.nativeEvent.layout;

                        this.setState({
                            footerY : layout.y
                        });

                    }}></View>
                }}
            />
    )
}

Basically, I render an empty footer in order to ascertain the Y offset of the list bottom. From this I can derive the scroll offset to the bottom, based on the list container height.

NOTE: The last if condition checks whether the content length overflows the list height, and only scrolls if it does. Whether you need this or not depends on your design!

Hope this solution aids others in the same position.

FWIW I did not fancy the InvertibleScrollView plugin discussed in another answer because it does a scale transform on the whole list, and each list item.. which sounds expensive!

Upvotes: 7

TwinHabit
TwinHabit

Reputation: 753

Here's what I use with React Native 0.14. This ListView wrapper scrolls to the bottom whenever:

  • The content's height changes
  • The container's height changes
  • The keyboard becomes visible or invisible

It mirrors standard behavior of most chat applications.

This implementation depends on RN0.14 ListView's implementation details and therefore might need adjustments to be compatible with future React Native versions



    var React = require('react-native');
    var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter');
    var RCTUIManager = require('NativeModules').UIManager;

    var {
      ListView,
      } = React;

    export default class AutoScrollListView extends React.Component {
      componentWillMount() {
        this._subscribableSubscriptions = [];
      }

      componentDidMount() {
        this._addListenerOn(RCTDeviceEventEmitter, 'keyboardWillShow', this._onKeyboardWillShow);
        this._addListenerOn(RCTDeviceEventEmitter, 'keyboardWillHide', this._onKeyboardWillHide);

        var originalSetScrollContentLength = this.refs.listView._setScrollContentLength;
        var originalSetScrollVisibleLength = this.refs.listView._setScrollVisibleLength;

        this.refs.listView._setScrollContentLength = (left, top, width, height) => {
          originalSetScrollContentLength(left, top, width, height);
          this._scrollToBottomIfContentHasChanged();
        };

        this.refs.listView._setScrollVisibleLength = (left, top, width, height) => {
          originalSetScrollVisibleLength(left, top, width, height);
          this._scrollToBottomIfContentHasChanged();
        };
      }

      componentWillUnmount() {
        this._subscribableSubscriptions.forEach(
          (subscription) => subscription.remove()
        );
        this._subscribableSubscriptions = null;
      }

      render() {
        return 
      }

      _addListenerOn = (eventEmitter, eventType, listener, context) => {
        this._subscribableSubscriptions.push(
          eventEmitter.addListener(eventType, listener, context)
        );
      }

      _onKeyboardWillShow = (e) => {
        var animationDuration = e.duration;
        setTimeout(this._forceRecalculationOfLayout, animationDuration);
      }

      _onKeyboardWillHide = (e) => {
        var animationDuration = e.duration;
        setTimeout(this._forceRecalculationOfLayout, animationDuration);
      }

      _forceRecalculationOfLayout = () => {
        requestAnimationFrame(() => {
          var scrollComponent = this.refs.listView.getScrollResponder();
          if (!scrollComponent || !scrollComponent.getInnerViewNode) {
            return;
          }
          RCTUIManager.measureLayout(
            scrollComponent.getInnerViewNode(),
            React.findNodeHandle(scrollComponent),
            () => {}, //Swallow error
            this.refs.listView._setScrollContentLength
          );
          RCTUIManager.measureLayoutRelativeToParent(
            React.findNodeHandle(scrollComponent),
            () => {}, //Swallow error
            this.refs.listView._setScrollVisibleLength
          );
        });
      }

      _scrollToBottomIfContentHasChanged = () => {
        var scrollProperties = this.refs.listView.scrollProperties;
        var hasContentLengthChanged = scrollProperties.contentLength !== this.previousContentLength;
        var hasVisibleLengthChanged = scrollProperties.visibleLength !== this.previousVisibleLength;

        this.previousContentLength = scrollProperties.contentLength;
        this.previousVisibleLength = scrollProperties.visibleLength;

        if(!hasContentLengthChanged && !hasVisibleLengthChanged) {
          return;
        }

        this.scrollToBottom();
      }

      scrollToBottom = () => {
        var scrollProperties = this.refs.listView.scrollProperties;
        var scrollOffset = scrollProperties.contentLength - scrollProperties.visibleLength;
        requestAnimationFrame(() => {
          this.refs.listView.getScrollResponder().scrollTo(scrollOffset);
        });
      }
    }


Upvotes: -1

Kohver
Kohver

Reputation: 356

I solve this for ScrollView. Here is simple example:

class MessageList extends Component {
    componentDidUpdate() {
        let innerScrollView = this._scrollView.refs.InnerScrollView;
        let scrollView = this._scrollView.refs.ScrollView;

        requestAnimationFrame(() => {
            innerScrollView.measure((innerScrollViewX, innerScrollViewY, innerScrollViewWidth, innerScrollViewHeight) => {
                scrollView.measure((scrollViewX, scrollViewY, scrollViewWidth, scrollViewHeight) => {
                    var scrollTo = innerScrollViewHeight - scrollViewHeight + innerScrollViewY;

                    if (innerScrollViewHeight < scrollViewHeight) {
                        return;
                    }

                    this._scrollView.scrollTo(scrollTo);
                });
            });
        });
    }

    render() {
        return (
            <ScrollView ref={component => this._scrollView = component}>
                {this.props.messages.map((message, i) => {
                    return <Text key={i}>{message}</Text>;
                })}
            </ScrollView>
        );
    }
}

Thanks for @ccheever

Upvotes: 8

Related Questions