react-native: [Android] ScrollView is missing initial scroll position for Android

ScrollView has a contentOffset prop for iOS, which sets the initial scroll offset. But it lacks on Android, so you can not set the initial scroll position.

For anyone who can make this, these are the attributes in Android’s native ScrollView.

android:scrollX     The initial horizontal scroll offset, in pixels. 
android:scrollY     The initial vertical scroll offset, in pixels. 

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 63
  • Comments: 82 (31 by maintainers)

Commits related to this issue

Most upvoted comments

When I try to call scrollTo from componentDidMount method it does not scroll. I have to use workaround with setTimeout to make it work:

componentDidMount() {
  setTimeout(() => this.refs._scrollView.scrollTo({ x: 100, y: 0 }) , 0);
}

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Maybe the issue has been fixed in a recent release, or perhaps it is not affecting a lot of people. If you think this issue should definitely remain open, please let us know why. Thank you for your contributions.

@sphairo That is not a solution, that is the problem itself, because It won’t work if the position you want to scroll is not rendered yet, as I said several times before.

Hi there! This issue is being closed because it has been inactive for a while.

But don’t worry, it will live on with ProductPains! Check out its new home: https://productpains.com/post/react-native/android-scrollview-is-missing-initial-scroll-position-for-android

Product Pains has been very useful in highlighting the top bugs and feature requests: https://productpains.com/product/react-native?tab=top

Also, if this issue is a bug, please consider sending a pull request with a fix.

Guys, I solve this this by using onContentSizeChange prop:

<ScrollView
            ref={scrollView => this.scrollView = scrollView}
            onContentSizeChange={() => {
                this._onContentSizeChange();
            }}
        ></ScrollView>

And my method:

_onContentSizeChange() {
     let initialYScroll = 200;
       this.scrollView.scrollTo({x: 0, y: initialYScroll, animated: false});
    };

such an important feature still not solved for andorid 😦 its almost 2020

this is not stale. it still happens 2 years after the initial posting

If someone is willing to send a PR, please do

Has there been any further discussion on this issue? I would really love to set: pagingEnabled={true} horizontal={true} and have the ScrollView set the x position to the 2nd page initially without the flash of (0, 0). A workaround is fine if there is a solution…

I can’t believe this issue has been open for 3 years and there is still no better solution to the problem… For me, I am using scrollTo() in componentDidMount() with setTimeout at 0ms, but flickering still occurs once every 3 refreshes or so… Will there every be a solution for this? Because it seems like a pretty common feature that a lot of people need…

I didn’t want to use setTimeout, since that doesn’t seem guaranteed (depends on how long render takes to run). Since all of my content is loaded at once, I used onContentSizeChange with an instance variable to negate it after it’s run,

constructor(props) {
  super(props);
  this.isLoadingContent = true;
}

handleContentSizeChange = () =>  {
  if (this.isLoadingContent) {
    this.isLoadingContent = false;
    this.scrollView.scrollToEnd({ animated: false });
  }
}

render() {
  return (
    <ScrollView
        ref={ref => this.scrollView = ref}
        onContentSizeChange={this.handleContentSizeChange}
    >
  )
}

Alternatively, I guess you could also just set handleContentSizeChange to null in order to avoid the needless if checks. In this way it’s only called once,

handleContentSizeChange = () =>  {
  this.handleContentSizeChange = null;
  this.scrollView.scrollToEnd();
}

Still not a perfect solution, and I do notice some flashing of the top before the scroll happens, but I needed something right now.

@madox2 Because the scroll position you want to scroll is not rendered yet, you have to wait for all rows to be rendered before you scroll to that position. So setTimeout is not a solution, which may not work.

iOS ListView does’t have this issue, because it has a initial scroll position property, so it can start rendering from that position, not from zero.

The solution a this problem is:

componentDidUpdate() {
        setTimeout(
            () => {
                this._scrollView.scrollTo({x: 100, y:0, animated:false});
            }, 150);
    }

I am closing this issue because according to the comment by @shergin from the past, we have decided that setting offset by a prop is not optimal and method scrollTo should be used instead.

That said, we will not be adding support for the (to be deprecated) prop on iOS.

Maybe contentOffset could be a native animated value. That way we could easily control position with setValue, and render with an initial value provided by JS:

class Test extends Component {
  _scrollY = new Animated.Value(startPos);
  render() {
    return (
      <ScrollView positionY={this._scrollY}>
...

Nevermind, I got it to work by making sure the Navigator animation had finished before using scrollTo using the InteractionManager.runAfterInteractions() method

This is still an issue, using scrollTo causes a flash which most people here don’t want but rather want to just start at that possition

These simple things just make me disappointed to use RN for my projects…

+1

I was able to scroll doing it on oncontentsizechange.

Take into account the callback attached is called twice, first with the height, and second with the width and height. I’m calling scrollTo in the second call, this is when width is passed.

Anyway the behaviour is not ideal, as the user is able to see the initial page as the call takes a lot to run.

One possible solution to this, would be to render views instead of the children if initialPage hasn’t been set yet or the children otherwise, the problem is that views size should be the same as their corresponding child which is the case when render horizontally and 1 page at a time.

I am afraid, I think that the right decision would be do not implement it on Android and abandon this feature on iOS. Why? TL;DR: This is nonconceptual because this value cannot be continuously synchronized with real offset. We have to use scrollTo() instead.

Some reasoning was discussed here: https://github.com/facebook/react-native/pull/15395#discussion_r132614518

Any other consideration?

+1

+1

+1

@alvaromb It won’t work when you try to scroll to position that is not rendered yet. initial scroll position lets your app to start rendering from your initial scroll position, not from zero. You you won’t have to wait for all of the rows to be rendered, which is a massive performance impact.

@tangkunyin No. What if you need a to use it on an infinite scrolling page?

Think about the instagram app. Go to the explore tab, you will see the grids of posts, then click on an any post, the new page is an infinite scrolling listview, but started from an initial scroll position.

So because of this issue, you can’t make an instagram clone for android using React Native.

@hufeng That will cause flickering after render, first you will see the 0,0 position, then it will scroll.

Also if you are using a ListView, then you will have to render all the data until the scroll position you want, then you can scroll there… Which will slow your app dramatically. But in iOS, you can just set initialListSize={1} and it can just start rendering where-ever you want by setting contentOffset.

Is it big deal to add it as style property? Then I’ll be able to animate it with native support with rn-reanimated.

@grabbou what do you mean by “not optimal”? Currently, it is using scrollTo which feels not optimal, as user sees initial position of the list before it is scrolled to the desired position. There is still no way for me to implement e.g. the instagram example from @bcalik using react native.

@Jacse Yeah specially on Android you have to do lot of weird hacks to init scroll view to specific position

Thanks @ryankask. It’d be great to start a discussion on implementing this feature. From a conceptual point of view, setting the scroll offset on a ScrollView on Android should be as simple as doing so on iOS. Can anyone shed any light on the technical feasibility of this and whether there are any potential issues that could be problematic in providing an implementation? @janicduplessis @satya164 @sahrens, I’m not sure if you guys might be able to shed any light on the technical feasibility? Thanks a lot.

edit: for anyone wishing to upvote the feature request on canny.io, here is link: https://react-native.canny.io/feature-requests/p/androidscrollview-is-missing-initial-scroll-position-for-android

@cdimitroulas Oooh right, I had to put it inside a setTimeout to make it work.

Try

componentDidMount() {
	setTimeout(() => {
		this.scrollView.scrollTo({x: 100});
	}, 0);
}

Keep in mind that InteractionManager.runAfterInteractions() won’t dispatch your function if there are no interactions taking place.

Kind of surprising that this is still not fixed upstream.

Based on the suggestions mentioned, I developed this drop-in ScrollViewOffset replacement component that adds contentOffset support for Android: https://github.com/dsernst/react-native-scrollview-offset/blob/master/ScrollViewOffset.tsx

It starts with opacity: 0, waits until after render to call scrollTo(props.contentOffset), then sets opacity: 1.

It also adds a new prop option — startAtEnd (boolean, default: false) — to set the initial scroll position to the end instead of needing to manually calculate it for contentOffset.

I really hate that React Native has these iOS only and Android only features. The team should implement the missing features to each platform to make the components behave identically.

Workaround for “blinking” - is use the overlay.

  constructor(props) {
    super(props);
    this.state = {
      overlayActive: true
    }
    this.scrollRef= React.createRef();
  }

I apply both of InteractionManager and setTimeout in componentDidMount. And then hide overlay

  componentDidMount() {
    const {offset} = this.props
    if (typeof offset === 'object') {
      InteractionManager.runAfterInteractions(() => {
        setTimeout(() => {
          this.scrollRef.current.scrollTo({...offset, animated: false})
          setTimeout(() => {
            this.setState({overlayActive: false})
          }, 1)
        }, 1)
      })
    }
  }
    return (
      <View>
        <ScrollView
          horizontal
          ref={this.scrollRef}
        >
          {renderArray}
        </ScrollView>

        { overlayActive &&
          <View style={[styles.overlay]} />
        }
      </View>
    )

Overlay stylesheet:

const styles = StyleSheet.create({
  overlay: {
    flex: 1,
    position: 'absolute',
    zIndex: 3,
    left: 0,
    top: 0,
    bottom: 0,
    right: 0,
    opacity: 1,
    backgroundColor: 'white',
  },
})

I have not read this issue but I have found that the initialScrollIndex of FlatList works well for some use cases. You need to implement getItemLayout however.

@mmazzarolo thanks, the “visibility” trick is interesting, will give it a try!

In my case I am trying to use FlatList to implement a swiper (image gallery). Here I know exactly the size of each element in the list (I am forced to set it for FlatList items anyway to be able to use paging). So I would expect for underlying android control to be able to render itself with initial offset (but I am no android expert).

@AlexSugak I think the issue is that is that the ScrollView (at least on Android) doesn’t know its size until it has been laid out. This means you’re forced to manually call scrollTo once the ScrollView onLayout has been invoked. @grabbou correct me if I’m wrong. I know, it’s not optimal, but I’m not sure how this issue could be really solved. Maybe by applying the scroll in the ScrollView onPreDraw?
In the meanwhile I’d suggest rendering the content of the ScrollView without making it visible (e.g.: by positioning it underneath your current screen) and show it to the user only once you have scrolled it to the desired position… this workaround has its drawbacks (e.g.: if the ScrollView content is huge it might take some time to be shown to the user) but at least the user won’t see the “flashing” ScrollView.

@shergin @ericvicenti @janicduplessis Any update on the future of contentOffset? The current workarounds with scrollTo in a setTimeout in componentDidMount or scrollTo in onLayout aren’t really cutting it.

(#15511)

New experimental listview components solve the problem.

Limiting the render window also reduces the amount of work that needs to be done by React and the native platform, e.g from view traversals. Even if you are rendering the last of a million elements, with these new lists there is no need to iterate through all those elements in order to render. You can even jump to the middle with scrollToIndex without excessive rendering.

https://facebook.github.io/react-native/blog/2017/03/13/better-list-views.html

https://www.facebook.com/groups/react.native.community/permalink/921378591331053/

render() {
  return (
    <ScrollView ref={(view) => this._scrollView = view}>
    </ScrollView>
  );
}

this._scrollView.getScrollResponder().scrollTo({x: 0, y: 0, animated: true})