163

Is it possible to get the current scroll position, or the current page of a <ScrollView> component in React Native?

So something like:

<ScrollView
  horizontal={true}
  pagingEnabled={true}
  onScrollAnimationEnd={() => { 
      // get this scrollview's current page or x/y scroll position
  }}>
  this.state.data.map(function(e, i) { 
      <ImageCt key={i}></ImageCt> 
  })
</ScrollView>
Mark Amery
  • 143,130
  • 81
  • 406
  • 459
Sang
  • 1,655
  • 2
  • 11
  • 7
  • Relevant: [a GitHub issue](https://github.com/facebook/react-native/issues/2215) suggesting adding a convenient way of getting this information. – Mark Amery Dec 26 '17 at 20:33

11 Answers11

273

Try.

<ScrollView onScroll={this.handleScroll} />

And then:

handleScroll: function(event: Object) {
 console.log(event.nativeEvent.contentOffset.y);
},

In another context, let's say you also want to implement a pagination indicator, you'll want to go further by doing this:

<ScrollView
     onScroll={Animated.event([{ nativeEvent: { contentOffset: { x: 
     scrollX } } }], {listener: (event) => handleScroll(event)})}
     scrollEventThrottle={16}
>
   ...some content
</ScrollView>

where scrollX would be an animated value you can use for you pagination and your handleScroll function can take the form:

  const handleScroll = (event) => {
    const positionX = event.nativeEvent.contentOffset.x;
    const positionY = event.nativeEvent.contentOffset.y;
  };
Segun
  • 68
  • 5
brad oyler
  • 3,669
  • 1
  • 20
  • 22
  • 8
    This won't always fire when the scroll value changes. If for example scrollview is resized causing it to now be able to display the whole thing on screen at once you don't always seem to get an onScroll event telling you. – Thomas Parslow Aug 12 '15 at 11:14
  • 23
    Or `event.nativeEvent.contentOffset.x` if you've got a horizontal ScrollView ;) Thanks! – Tieme Sep 05 '16 at 16:05
  • 2
    This also we can use in Android. – Janaka Pushpakumara Jun 28 '17 at 07:08
  • 7
    Frustratingly, unless you set [`scrollEventThrottle`](https://facebook.github.io/react-native/docs/scrollview.html#scrolleventthrottle) to 16 or lower (to get an event for every single frame), there's no guarantee that this will report the offset on the *final* frame - meaning that to be sure that you have the right offset after scrolling has finished, you need to set `scrollEventThrottle={16}` and accept the performance impact of that. – Mark Amery Dec 26 '17 at 14:33
  • @bradoyler: is it possible to know the maximum value of `event.nativeEvent.contentOffset.y`? – Isaac Oct 08 '18 at 09:44
  • I am getting an error: Property 'nativeEvent' does not exist on type 'Object' – salvi shahzad Feb 20 '20 at 09:17
  • I use it with `onLayout` listener to make sure that both the page size and the page offset are in my hands so that I can make the scrollview usable. BTW, unless I add the `scrollEventThrottle` property, the RN runtime will warn about it. – Rick Dou Apr 16 '22 at 05:30
80

Disclaimer: what follows is primarily the result of my own experimentation in React Native 0.50. The ScrollView documentation is currently missing a lot of the information covered below; for instance onScrollEndDrag is completely undocumented. Since everything here relies upon undocumented behaviour, I can unfortunately make no promises that this information will remain correct a year or even a month from now.

Also, everything below assumes a purely vertical scrollview whose y offset we are interested in; translating to x offsets, if needed, is hopefully an easy exercise for the reader.


Various event handlers on a ScrollView take an event and let you get the current scroll position via event.nativeEvent.contentOffset.y. Some of these handlers have slightly different behaviour between Android and iOS, as detailed below.

onScroll

On Android

Fires every frame while the user is scrolling, on every frame while the scroll view is gliding after the user releases it, on the final frame when the scroll view comes to rest, and also whenever the scroll view's offset changes as a result of its frame changing (e.g. due to rotation from landscape to portrait).

On iOS

Fires while the user is dragging or while the scroll view is gliding, at some frequency determined by scrollEventThrottle and at most once per frame when scrollEventThrottle={16}. If the user releases the scroll view while it has enough momentum to glide, the onScroll handler will also fire when it comes to rest after gliding. However, if the user drags and then releases the scroll view while it is stationary, onScroll is not guaranteed to fire for the final position unless scrollEventThrottle has been set such that onScroll fires every frame of scrolling.

There is a performance cost to setting scrollEventThrottle={16} that can be reduced by setting it to a larger number. However, this means that onScroll will not fire every frame.

onMomentumScrollEnd

Fires when the scroll view comes to a stop after gliding. Does not fire at all if the user releases the scroll view while it is stationary such that it does not glide.

onScrollEndDrag

Fires when the user stops dragging the scroll view - regardless of whether the scroll view is left stationary or begins to glide.


Given these differences in behaviour, the best way to keep track of the offset depends upon your precise circumstances. In the most complicated case (you need to support Android and iOS, including handling changes in the ScrollView's frame due to rotation, and you don't want to accept the performance penalty on Android from setting scrollEventThrottle to 16), and you need to handle changes to the content in the scroll view too, then it's a right damn mess.

The simplest case is if you only need to handle Android; just use onScroll:

<ScrollView
  onScroll={event => { 
    this.yOffset = event.nativeEvent.contentOffset.y
  }}
>

To additionally support iOS, if you're happy to fire the onScroll handler every frame and accept the performance implications of that, and if you don't need to handle frame changes, then it's only a little bit more complicated:

<ScrollView
  onScroll={event => { 
    this.yOffset = event.nativeEvent.contentOffset.y
  }}
  scrollEventThrottle={16}
>

To reduce the performance overhead on iOS while still guaranteeing that we record any position that the scroll view settles on, we can increase scrollEventThrottle and additionally provide an onScrollEndDrag handler:

<ScrollView
  onScroll={event => { 
    this.yOffset = event.nativeEvent.contentOffset.y
  }}
  onScrollEndDrag={event => { 
    this.yOffset = event.nativeEvent.contentOffset.y
  }}
  scrollEventThrottle={160}
>

But if we want to handle frame changes (e.g. because we allow the device to be rotated, changing the available height for the scroll view's frame) and/or content changes, then we must additionally implement both onContentSizeChange and onLayout to keep track of the height of both the scroll view's frame and its contents, and thereby continually calculate the maximum possible offset and infer when the offset has been automatically reduced due to a frame or content size change:

<ScrollView
  onLayout={event => {
    this.frameHeight = event.nativeEvent.layout.height;
    const maxOffset = this.contentHeight - this.frameHeight;
    if (maxOffset < this.yOffset) {
      this.yOffset = maxOffset;
    }
  }}
  onContentSizeChange={(contentWidth, contentHeight) => {
    this.contentHeight = contentHeight;
    const maxOffset = this.contentHeight - this.frameHeight;
    if (maxOffset < this.yOffset) {
      this.yOffset = maxOffset;
    }
  }}
  onScroll={event => { 
    this.yOffset = event.nativeEvent.contentOffset.y;
  }}
  onScrollEndDrag={event => { 
    this.yOffset = event.nativeEvent.contentOffset.y;
  }}
  scrollEventThrottle={160}
>

Yeah, it's pretty horrifying. I'm also not 100% certain that it'll always work right in cases where you simultaneously change the size of both the frame and content of the scroll view. But it's the best I can come up with, and until this feature gets added within the framework itself, I think this is the best that anyone can do.

David Schumann
  • 13,380
  • 9
  • 75
  • 96
Mark Amery
  • 143,130
  • 81
  • 406
  • 459
  • Things sure would be a lot easier if one could just access state information about the scrollview, instead of having to manually track its changes. Little crazy there. Great writeup by the way, well explained. – Kevin Teman May 11 '22 at 19:35
  • 2
    I've spent hours looking through various react-native tickets and similar SO tickets and this is the most complete solution I've found. IMO It really disappointing that the corresponding react-native ticket (linked above) was closed and locked as the use case and deficiency here is very obvious. – Dave Welling May 19 '22 at 14:18
27

The above answers tell how to get the position using different API, onScroll, onMomentumScrollEnd etc; If you want to know the page index, you can calculate it using the offset value.

 <ScrollView 
    pagingEnabled={true}
    onMomentumScrollEnd={this._onMomentumScrollEnd}>
    {pages}
 </ScrollView> 

  _onMomentumScrollEnd = ({ nativeEvent }: any) => {
   // the current offset, {x: number, y: number} 
   const position = nativeEvent.contentOffset; 
   // page index 
   const index = Math.round(nativeEvent.contentOffset.x / PAGE_WIDTH);

   if (index !== this.state.currentIndex) {
     // onPageDidChanged
   }
 };

enter image description here

In iOS, the relationship between ScrollView and the visible region is as follow: The picture comes from https://www.objc.io/issues/3-views/scroll-view/

ref: https://www.objc.io/issues/3-views/scroll-view/

RY_ Zheng
  • 3,041
  • 29
  • 36
20

Brad Oyler's answer is correct. But you will only receive one event. If you need to get constant updates of the scroll position, you should set the scrollEventThrottle prop, like so:

<ScrollView onScroll={this.handleScroll} scrollEventThrottle={16} >
  <Text>
    Be like water my friend …
  </Text>
</ScrollView>

And the event handler:

handleScroll: function(event: Object) {
  console.log(event.nativeEvent.contentOffset.y);
},

Be aware that you might run into performance issues. Adjust the throttle accordingly. 16 gives you the most updates. 0 only one.

cutemachine
  • 5,520
  • 2
  • 33
  • 30
  • 16
    This is incorrect. For one, scrollEvenThrottle only works for iOS and not for android. Second, it only regulates how often you receive onScroll calls. Default is once per frame, not one event. 1 gives you more updates and 16 gives you less. 0 is default. This answer is completely wrong. https://facebook.github.io/react-native/docs/scrollview.html#scrolleventthrottle – Ted Dec 26 '16 at 21:50
  • 2
    I answered this before Android was supported by React Native, so yes, the answer applys to iOS only. The default value is zero, which results in the scroll event being sent only once each time the view is scrolled. – cutemachine Dec 29 '16 at 20:31
  • 1
    What would be the es6 notation for function(event: Object) {} ? – cbartondock Apr 26 '17 at 22:50
  • 1
    Just `function(event) { }`. – cutemachine May 01 '17 at 16:36
  • 2
    @Teodors While this answer doesn't cover Android, the rest of your criticism is completely confused. `scrollEventThrottle={16}` does *not* give you "less" updates; any number in the range 1-16 gives you the maximum possible rate of updates. The default of 0 gives you (on iOS) one event when the user starts scrolling and then no subsequent updates (which is pretty useless and possibly a bug); the default is *not* "once per frame". Besides those two errors in your comment, your other assertions don't contradict anything that the answer says (though you seem to think that they do). – Mark Amery Jan 02 '18 at 13:05
7

If you wish to simply get the current position (e.g. when some button is pressed) rather than tracking it whenever the user scrolls, then invoking an onScroll listener is going to cause performance issues. Currently the most performant way to simply get current scroll position is using react-native-invoke package. There is an example for this exact thing, but the package does multiple other things.

Read about it here. https://medium.com/@talkol/invoke-any-native-api-directly-from-pure-javascript-in-react-native-1fb6afcdf57d#.68ls1sopd

Mark Amery
  • 143,130
  • 81
  • 406
  • 459
Ted
  • 450
  • 1
  • 5
  • 15
  • 3
    Do you know if a cleaner way (bit less hacky) to request offset exists now, or is this still the best way of doing it you are aware of? (I wonder why this answer isn't upvoted as it is the only one that looks to make sense) – cglacet Dec 26 '17 at 14:52
  • 2
    The package is no longer there, did it move somewhere? Github is showing 404. – Noitidart Apr 13 '20 at 15:07
7

To get the x/y after scroll ended as the original questions was requesting, the easiest way is probably this:

<ScrollView
   horizontal={true}
   pagingEnabled={true}
   onMomentumScrollEnd={(event) => { 
      // scroll animation ended
      console.log(event.nativeEvent.contentOffset.x);
      console.log(event.nativeEvent.contentOffset.y);
   }}>
   ...content
</ScrollView>
Lasitha Lakmal
  • 486
  • 4
  • 17
j0n
  • 127
  • 1
  • 2
  • 1
    -1; `onMomentumScrollEnd` is only triggered if the scroll view has some momentum when the user lets go of it. If they release while the scroll view is not moving, you'll never get any of the momentum events. – Mark Amery Dec 26 '17 at 22:09
  • 1
    I tested onMomentumScrollEnd and it was imposible for me to not trigger it, I tried scrolling in different ways, I tried releasing the touch when the scroll was in the final position and it also was triggered. So this answer is correct as far as I could test it. – fermmm Sep 27 '19 at 06:19
  • Shouldn't it be {(e) instead of {( event ) ? – Vincent Aug 23 '20 at 06:24
1

This is how ended up getting the current scroll position live from the view. All I am doing is using the on scroll event listener and the scrollEventThrottle. I am then passing it as an event to handle scroll function I made and updating my props.

 export default class Anim extends React.Component {
          constructor(props) {
             super(props);
             this.state = {
               yPos: 0,
             };
           }

        handleScroll(event){
            this.setState({
              yPos : event.nativeEvent.contentOffset.y,
            })
        }

        render() {
            return (
          <ScrollView onScroll={this.handleScroll.bind(this)} scrollEventThrottle={16} />
    )
    }
    }
Sabba Keynejad
  • 7,895
  • 2
  • 26
  • 22
0

As for the page, I'm working on a higher order component that uses basically the above methods to do exactly this. It actually takes just a bit of time when you get down to the subtleties like initial layout and content changes. I won't claim to have done it 'correctly', but in some sense I'd consider the correct answer to use component that does this carefully and consistently.

See: react-native-paged-scroll-view. Would love feedback, even if it's that I've done it all wrong!

0

use of onScroll enters infinite loop. onMomentumScrollEnd or onScrollEndDrag can be used instead

Suraj Rao
  • 29,388
  • 11
  • 94
  • 103
korkut
  • 37
  • 5
-2

I believe contentOffset will give you an object containing the top-left scroll offset:

http://facebook.github.io/react-native/docs/scrollview.html#contentoffset

Colin Ramsay
  • 16,086
  • 9
  • 52
  • 57
-3

 <ScrollView style={{ height: hp('70%'), width: '100%' }} showsVerticalScrollIndicator={false}
              ref={myRef}

        // for getting scrollView offset ->

              onScroll={(event => setOffset(event.nativeEvent.contentOffset.y))}

        // for giving scrollView offset ->

              onContentSizeChange={() =>
                myRef.current.scrollTo({
                  x: 0,
                  y: Offset
                })
              }
            // pagingEnabled={true}
            // scrollEventThrottle={16}
            >

<ScrollView

showsVerticalScrollIndicator={false}

ref={myRef}

onScroll={(event => setOffset(event.nativeEvent.contentOffset.y))}

onContentSizeChange={() =>

myRef.current.scrollTo({

x: 0,

y: Offset

})

}

Asad Ali
  • 13
  • 2