Reputation: 401
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
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
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
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
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
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
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.
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
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
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
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
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
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
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
Reputation: 753
Here's what I use with React Native 0.14. This ListView wrapper scrolls to the bottom whenever:
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
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