Kevin Old
Kevin Old

Reputation: 1029

"Sticky" center element of horizontal React Native ListView

My complete source code for this issue is posted as an Expo app: https://exp.host/@kevinoldlifeway/swipe-scrollview-of-webviews as well as on Github https://github.com/kevinold/swipe-scrollview-of-webviews

I am building a book view in React Native. Using a ScrollView, I would like to swipe left and right to navigate through the pages of a title that could have several hundred to several thousand.

Since that is the case, my goal is to only the minimal amount of data so that the user is able to swipe between pages, seeing the immediately previous and next pages.

I am loading a 3 element array like so:

[Previous, Current, Next]

That would be updated in the state by Redux (not used here to keep simple) and would re-render and refocus the list.

My goal is that my ScrollView is always "centered" on the "Current" page.

Page scrolls to the previous and next page are handled by a handleScroll method which loads the appropriate precomputed array so that the current page stays in focus, but the previous and next pages (offscreen) are updated appropriately.

    handleScroll (event) {
    //const x = event.nativeEvent.contentOffset.x;

    const { activeIndex, scrollTimes } = this.state;

    const windowWidth = Dimensions.get('window').width;
    const eventWidth = event.nativeEvent.contentSize.width;
    const offset = event.nativeEvent.contentOffset.x;
    console.log('event width: ', eventWidth);
    console.log('event offset: ', offset);

    console.log('scrollTimes: ', scrollTimes);
    //if (scrollTimes <= 1 ) return;

    if (windowWidth + offset >= eventWidth) {
        //ScrollEnd, do sth...
        console.log('scrollEnd right (nextPage)', offset);
        const nextIndex = activeIndex + 1;
        console.log('nextIndex: ', nextIndex);

  // Load next page
  this.loadMore()

    } else if (windowWidth - offset <= eventWidth) {
        //ScrollEnd, do sth...
        console.log('scrollEnd left (prevPage)', offset);

  // Load prev page
  this.loadPrev()
    }

    this.setState({ scrollTimes: scrollTimes + 1 });
}

I have tried to balance the "current" page using a combination of:

contentOffset={{ x: width, y: 0 }} on ScrollView

And

componentDidMount() {
    // Attempt to keep "center" element in array as focused "screen" in the horizontal list view
    this.scrollView.scrollTo({ x: width, y: 0, animated: false });
}

I've also tried to scrollTo in the callback after this.setState, but have not had any luck.

I'm wondering if this "centering" could be accomplished by using Animated.

Upvotes: 0

Views: 2242

Answers (1)

Spencer Carli
Spencer Carli

Reputation: 696

I gave this a shot but I'm not entirely sure I understood the problem, and I'm not sure how well this would hold up.

Basically I just simplified the handleScroll function significantly. First checking if we were on a scroll completion and if so determining if when we landed on that screen it was the "previous" screen or "next" - do nothing if it's already the middle screen.

I think in your code the issue was that it would fire and load data if it was the middle screen, not just the first or last. Therefore it would fire twice for each transition.

Here's the handleScroll that I think will work for you.

handleScroll (event) {
  const offset = event.nativeEvent.contentOffset.x;
  const mod = offset % width;

  if (mod === 0) { // only transition on scroll complete
    if (offset === width * 2) { // last screen
      console.log('load more')
      this.loadMore();
      this.scrollView.scrollTo({ x: width, y: 0, animated: false });
    } else if (offset !== width) { // first screen
      console.log('load prev')
      this.loadPrev();
      this.scrollView.scrollTo({ x: width, y: 0, animated: false });
    }
  }
}

And a Snack demoing it.

Upvotes: 1

Related Questions