Search code examples
react-nativereact-native-scrollview

How can I smoothen Animated marginTop transitions in a ScrollView?


For my project I'm looking to implement a ScrollView that shows items as if you're browsing through a box of business cards. I based my current implementation on Albert Brand's article on Animated ScrollViews (after trying many other approaches).

However, the whole thing becomes very jittery now. Can anyone advise me how to smoothen the current implementation? Alternatively, does anyone have a tip how to get this behaviour in a different manner? Any solution should work in both iOS and Android.

This gif shows my current implementation including the jittering problem. It also should give a clear picture of what behaviour I'm after: screencapture of the ScrollView in iOS simulator

This is my current implementation:

import React from 'react'
import {
  Animated,
  Dimensions,
  ScrollView,
  StyleSheet,
  Text,
  View,
  StatusBar,
} from 'react-native'

const SCREEN_HEIGHT = Dimensions.get('window').height

const yOffset = new Animated.Value(0)

const onScroll = Animated.event(
  [{ nativeEvent: { contentOffset: { y: yOffset } } }],
)

function CardView(props: { children?: ReactElement<*> }) {
  return (
    <Animated.ScrollView
      scrollEventThrottle={16}
      onScroll={onScroll}
      pagingEnabled
    >
      {props.children}
    </Animated.ScrollView>
  )
}

function Page(props: { children?: ReactElement<*>, index: 1 }) {
  return (
    <Animated.View style={[style.scrollPage]}>
      {props.children}
    </Animated.View>
  )
}

function Card(props: { text: string, index: number }) {
  return (
    <Animated.View style={[style.card, marginTransform(props.index)]}>
      <Text>
        {props.text}
      </Text>
    </Animated.View>
  )
}

function marginTransform(index: number) {
  return {
    marginTop: yOffset.interpolate({
      inputRange: [
        (index - 1) * SCREEN_HEIGHT,
        index * SCREEN_HEIGHT,
        (index + 1) * SCREEN_HEIGHT,
        (index + 2) * SCREEN_HEIGHT,
        (index + 3) * SCREEN_HEIGHT,
      ],
      outputRange: [
        -40,
        80,
        SCREEN_HEIGHT + 40,
        2 * SCREEN_HEIGHT + 20,
        3 * SCREEN_HEIGHT,
      ],
      extrapolate: 'clamp',
    }),
  }
}

export default function App() {
  return (
    <CardView>
      {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => {
        return(
          <Page key={i}>
            <Card text={`Card ${i}`} index={i}>
            </Card>
          </Page>
        )
      })}
    </CardView>
  )
}

const style = StyleSheet.create({
  scrollPage: {
    height: SCREEN_HEIGHT,
    backgroundColor: 'transparent',
  },
  card: {
    height: SCREEN_HEIGHT,
    alignItems: 'center',
    borderRadius: 4,
    borderWidth: 1,
    backgroundColor: '#F5FCFF',
  }
})

Solution

  • The short answer I found: ScrollViews will never animate smoothly if you mess with it's height while scrolling. On a sidenote I did find that performance improved drastically when I set scrollEventThrottle={1}

    The solution I ended up implementing was making my own component; rendering a ScrollView on top of a list of elements. I connected the Animated height values of those elements to the scroll y offset of the ScrollView.

    If anyone ever encounters the same usecase implementation issue, feel free to use my work: https://github.com/isilher/react-native-card-scroll