Skip to content

Instantly share code, notes, and snippets.

@jevakallio
Created December 9, 2016 23:54
Show Gist options
  • Save jevakallio/e95b8ee3c649eb64b2dc768be9375e11 to your computer and use it in GitHub Desktop.
Save jevakallio/e95b8ee3c649eb64b2dc768be9375e11 to your computer and use it in GitHub Desktop.
React Native: Synchronized ScrollViews
import Exponent from 'exponent';
import React from 'react';
import { range } from 'lodash';
import {
StyleSheet,
Dimensions,
ScrollView,
Animated,
Text,
View,
} from 'react-native';
// each page is full-screen wide and 1000px tall
const w = Dimensions.get('window').width;
const h = 1000;
// render arbitrary row, not important
const Row = ({ title, index, height, color }) => (
<View
key={`${title}-${index}`}
style={{
height,
backgroundColor: index % 2 === 0 ? 'white' : color,
flex: 1,
alignItems: 'center',
justifyContent: 'center'
}}
>
<Text>{title} #{index}</Text>
</View>
);
class Page extends React.Component {
componentDidMount() {
// when the scroll position of the currently visible
// view changes, update this scrollview position
this.listener = this.props.position.addListener((position) => {
if (!this.props.active) {
this._scrollInstance.scrollTo({
x: 0,
y: position.value,
animated: false
});
}
});
}
componentWillUnmount() {
this.props.position.removeListener(this.listener);
}
render() {
const {name, rowCount, onScroll, active, color} = this.props;
return (
<ScrollView
key={name}
ref={(instance) => this._scrollInstance = instance}
style={{flex: 1, width: w}}
scrollEventThrottle={100}
onScroll={active ? onScroll : null}
>
{range(rowCount).map((index) => (
<Row
key={`row-${index}`}
color={color}
title={name}
index={index}
height={h / rowCount}
/>
))}
</ScrollView>
);
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {page: 0};
// create an animated value and update it with the y-scroll
// position on the active scrollview when it scrolls.
// we will then pass the position to all the scroll views,
// who will synchronize their scroll states to the current position
// on each change
this.pageScrollPosition = new Animated.Value(0);
this.pageScrollListener = Animated.event(
[{nativeEvent: {contentOffset: {y: this.pageScrollPosition }}}]
);
}
render() {
return (
<ScrollView
style={{flex: 1}}
horizontal={true}
pagingEnabled={true}
scrollEventThrottle={200}
onScroll={(e) => {
this.setState({
page: Math.floor(e.nativeEvent.contentOffset.x / w)
});
}}
>
<Page
name='One'
rowCount={25}
color='rgba(255, 0, 0, 0.2)'
onScroll={this.pageScrollListener}
position={this.pageScrollPosition}
active={this.state.page === 0}
/>
<Page
name='Two'
rowCount={25}
color='rgba(0, 255, 0, 0.2)'
onScroll={this.pageScrollListener}
position={this.pageScrollPosition}
active={this.state.page === 1}
/>
<Page
name='Three'
rowCount={25}
color='rgba(0, 0, 255, 0.2)'
onScroll={this.pageScrollListener}
position={this.pageScrollPosition}
active={this.state.page === 2}
/>
</ScrollView>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1
},
});
Exponent.registerRootComponent(App);
@itsrifat
Copy link

itsrifat commented May 11, 2017

Similar effect can also be achieved with a horizontal ScrollView nested inside a vertical ScrollView without having to track and sync offset of adjacent pages. Something like:

import Exponent from 'exponent';
import React from 'react';
import { range } from 'lodash';

import { Dimensions, ScrollView, Text, View } from 'react-native';

// each page is full-screen wide and 1000px tall
const w = Dimensions.get('window').width;
const h = 1000;

// render arbitrary row, not important
const Row = ({ title, index, height, color }) => (
  <View
    key={`${title}-${index}`}
    style={{
      height,
      backgroundColor: index % 2 === 0 ? 'white' : color,
      flex: 1,
      alignItems: 'center',
      justifyContent: 'center',
    }}
  >
    <Text>{title} #{index}</Text>
  </View>
);

// Page renders a View instead of a ScrollView
const Page = ({ name, rowCount, color }) => (
  <View key={name} style={{ flex: 1, width: w }}>
    {range(rowCount).map(index => (
      <Row key={`row-${index}`} color={color} title={name} index={index} height={h / rowCount} />
    ))}
  </View>
);

export class App extends React.Component {
  render() {
    return (
      <ScrollView>
        <ScrollView
          ref={(view) => {
            this.view = view;
          }}
          horizontal
          pagingEnabled
          scrollEventThrottle={16}
        >
          <Page name="One" rowCount={25} color="rgba(255, 0, 0, 0.2)" />
          <Page name="Two" rowCount={25} color="rgba(0, 255, 0, 0.2)" />
          <Page name="Three" rowCount={25} color="rgba(0, 0, 255, 0.2)" />
        </ScrollView>
      </ScrollView>
    );
  }
}

Exponent.registerRootComponent(App);

@joshjhargreaves
Copy link

@itsrifat, good spot! Thanks.

@km-86
Copy link

km-86 commented Jun 9, 2020

it doesn't work in a multi scrollView sync structure .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment