React Native - Bounce effect in iOS messing with A

2019-08-15 04:03发布

问题:

EDIT: I hate googling for answers and finding some question that never got solved from 10 years ago so I am answering my own question for those that might want to know. In my case, I simply disabled the bounces prop for the scrollview. Since FlatList extends React's ScrollView, setting bounces to false in the animated FlatList component that I created stopped it from bouncing and solved my problem. Have a nice day.

hope you're having a great day. I am trying to animate my header dynamically but for some reason whenever I scroll beyond the beginning or the end of the scrollview, the bounce effect messes with the Animation.(as shown in the gif below)

GIF

Same GIF but higher resolution

As you can see, when I scroll to the top and enable the bounce animation, the header thinks that i am scrolling down as the bounce returns the first element in the list back to the top. How do I fix this? I saw on the web somewhere that adding an interpolator to the animated value would help, though I don't really understand. Below is my code. Thank You

const AnimatedFlatList = Animated.createAnimatedComponent(FlatList)

const tempArray = [
  ...(an array of my data)
]

export default class TempScreen extends React.Component {
  static navigationOptions = {
    header: null
  }

  constructor(props) {
    super(props)
    this.state = {
      animatedHeaderValue: new Animated.Value(0),
    }
  }

  render() {
    const animatedHeaderHeight = Animated.diffClamp(this.state.animatedHeaderValue, 0, 60)
      .interpolate({
        inputRange: [0, 70],
        outputRange: [70, 0],
      })
    return ( <
      View >
      <
      Animated.View style = {
        {
          backgroundColor: 'white',
          borderBottomColor: '#DEDEDE',
          borderBottomWidth: 1,
          padding: 15,
          width: Dimensions.get('window').width,
          height: animatedHeaderHeight,
        }
      } >
      <
      /Animated.View> <
      AnimatedFlatList scrollEventThrottle = {
        16
      }
      onScroll = {
        Animated.event(
          [{
            nativeEvent: {
              contentOffset: {
                y: this.state.animatedHeaderValue
              }
            }
          }]
        )
      }
      data = {
        tempArray
      }
      renderItem = {
        ({
          item
        }) =>
        <
        View style = {
          {
            flex: 1
          }
        } >
        <
        Text style = {
          {
            fontWeight: 'bold',
            fontSize: 30
          }
        } > {
          item.name
        } < /Text> <
        Text > {
          item.year
        } < /Text> <
        /View>
      }
      />

      <
      /View>

    )
  }
}

回答1:

If you want to solve the "bounce" problem only, the problem is that iOS gives to diffClamp negative scrollY values. You need to filter these and ensure scrollY remains >= 0 to avoid diffClamp being affected by overscroll.

const clampedScrollY = scrollY.interpolate({
  inputRange: [0, 1],
  outputRange: [0, 1],
  extrapolateLeft: 'clamp',
});

Another nice trick is to use a "cliff" technique, so that the header only disappear after a minimum scrollY position.

Here is code from my app:

const minScroll = 100;

const clampedScrollY = scrollY.interpolate({
  inputRange: [minScroll, minScroll + 1],
  outputRange: [0, 1],
  extrapolateLeft: 'clamp',
});

const minusScrollY = Animated.multiply(clampedScrollY, -1);

const translateY = Animated.diffClamp(
  minusScrollY,
  -AnimatedHeaderHeight,
  0,
);

const opacity = translateY.interpolate({
  inputRange: [-AnimatedHeaderHeight, 0],
  outputRange: [0.4, 1],
  extrapolate: 'clamp',
});

clampedScrollY will be:

  • 0 when scrollY=0
  • 0 when scrollY=50
  • 0 when scrollY=100
  • 30 when scrollY=130
  • 170 when scrollY=270

You get the idea. So diffClamp will only be > 0 if scrollY > 100, and increment 1 by 1 after that threshold.



回答2:

I had the same problem like two hour ago...

You can set Scrollview property bounces=false but if you want a RefreshControl for refreshing the ScrollView content (like in my case), the bounce property has to stay active.

I fixed this following this cool article: https://medium.com/appandflow/react-native-collapsible-navbar-e51a049b560a.

I'm not an expert of the Animated library, so I post my code:

constructor(props) {

    const scrollAnim = new Animated.Value(0);
    const offsetAnim = new Animated.Value(0);

    this.state = {
        scrollAnim,
        offsetAnim,
        AnimatedViewHeight: 1,
        clampedScroll: Animated.diffClamp(
            Animated.add(
                scrollAnim.interpolate({
                    inputRange: [0, 1],
                    outputRange: [0, 1],
                    extrapolateLeft: 'clamp',
                }),
                offsetAnim
            ),0, 1
        )
    }
}

render() {

    const minScroll = this.state.AnimatedViewHeight;

    const navbarTranslate = this.state.clampedScroll.interpolate({
        inputRange: [0, minScroll],
        outputRange: [0, -minScroll],
        extrapolate: 'clamp',
    });

    return (
        <View style={{
            flex: 1
        }}>

      <Animated.View
        onLayout={(event) => {
          var { height } = event.nativeEvent.layout;
          this.setState({
              AnimatedViewHeight: height,
              clampedScroll: Animated.diffClamp(
                    Animated.add(
                          this.state.scrollAnim.interpolate({
                                inputRange: [0, 1],
                                outputRange: [0, 1],
                                extrapolateLeft: 'clamp',
                          }),
                          this.state.offsetAnim
                    ), 0, height)
          })
        }}
        style={[{ transform: [{ translateY: navbarTranslate }] }]}>

       <View><Text>THIS IS YOUR HEADER</Text></View>

       </Animated.View>

       <AnimatedFlatList
           // iOS offset for RefreshControl
           contentInset={{
               top: this.state.AnimatedViewHeight,
           }}
           contentOffset={{
               y: -this.state.AnimatedViewHeight,
           }}
           scrollEventThrottle={1}
           onScroll={
               Animated.event(
                        [{ nativeEvent: { contentOffset: { y: this.state.scrollAnim } } }],
                        { useNativeDriver: true },
               )}
               data={this.state.data}
               keyExtractor={(item, idx) => idx}
               ListFooterComponent={this.renderFooter}
               renderItem={this.renderItem}
               onEndReached={this.handleLoadMore}
               refreshControl={
                    <RefreshControl
                        refreshing={this.state.refreshing}
                        onRefresh={this.onRefresh}
                        // Android offset for RefreshControl
                        progressViewOffset={this.state.AnimatedViewHeight}
                    />
                }
                onEndReachedThreshold={0.5} />
        </View>

    )
}

this.state.AnimatedViewHeight is the height of the header, retrieved by calling onLayout function. Inside of this function I also set a new clampedScroll because I have a new height (In my case, the header doesn't have a fixed size). After that, in render(), define a variable (navbarTranslate) for control the headerSize based on the scroll position of your Animated Scrollview.