8

I am using Animation.view to change the height and the background of the header.

I set my height and the background settings like this:

const HeaderHeight = this.state.scrollY.interpolate({
      inputRange:[0, Header_Max_Height - Header_Min_Height],
      outputRange:[Header_Max_Height, Header_Min_Height],
      extrapolate:'clamp'
    })

const AnimateHeaderBackgroundColor = this.state.scrollY.interpolate({
        inputRange: [ 0, ( Header_Max_Height - Header_Min_Height )  ],
        outputRange: [ '#009688', '#00BCD4' ],
        extrapolate: 'clamp'
    })

This is my animated.view.

<Animated.View style={{width:'100%', height: HeaderHeight, backgroundColor:AnimateHeaderBackgroundColor}}></Animated.View>

Everything works well.

My question is there a way I could change the view like the height and the backgroundcolor?

For example, say I have two views:

//view1
<View style={{width:'100%',height:100, backgroundColor:'red'}}>
 <Text>View1</Text>
</View>

//view2
<View style={{width:'100%',height:100, backgroundColor:'blue'}}>
  <Text>View2</Text>
</View>

I want the view1 to show by default and show view2 as I scroll to the top of the screen. Placing the View in the outputRange would make this possible?

kirimi
  • 1,370
  • 4
  • 32
  • 57

3 Answers3

8

I guess there's no direct way in RN if you want to animated a change of view, however, in your case I can think of a little trick using the mix of opacity, position: absolute and interpolate(), here is a minimal example which you can directly copy and paste to test it:

import React, { Component } from 'react';
import { StyleSheet, Animated, View, ScrollView } from 'react-native';

class AnimationExample extends Component {
  constructor(props) {
    super(props)
    this.state = {
      showBlueView: false,
      animatedOpacityValue: new Animated.Value(0),
    }
  }

  handleScroll = (event) => {
    const { animatedOpacityValue, showBlueView } = this.state;
    const scrollPosition = event.nativeEvent.contentOffset.y;

    if (scrollPosition > 100 && !showBlueView) {
      Animated.timing(animatedOpacityValue, {
        toValue: 1,
      }).start(() => this.setState({ showBlueView: true }))
    }

    if (scrollPosition < 100 && showBlueView) {
      Animated.timing(animatedOpacityValue, {
        toValue: 0,
      }).start(() => this.setState({ showBlueView: false }))
    }
  }

  render() {
    const { animatedOpacityValue } = this.state;
    return (
      <ScrollView
        style={styles.scrollView}
        onScroll={this.handleScroll}
        scrollEventThrottle={16}
      >
        <View style={styles.green} />
        <View style={styles.animatedViewsPositioner}>
          <Animated.View
            style={{
              ...styles.red,
              opacity: animatedOpacityValue.interpolate({
                inputRange: [0, 1],
                outputRange: [1, 0],
              }),
            }}
          />
          <Animated.View
            style={{
              ...styles.blue,
              opacity: animatedOpacityValue.interpolate({
                inputRange: [0, 1],
                outputRange: [0, 1],
              }),
            }}
          />
        </View>
      </ScrollView>
    )
  }
}

const styles = StyleSheet.create({
  scrollView: {
    flex: 1,
  },
  green: {
    height: 600,
    width: '100%',
    backgroundColor: 'green',
  },
  red: {
    height: 300,
    width: '100%',
    backgroundColor: 'red',
  },
  blue: {
    position: 'absolute',
    height: 300,
    width: '100%',
    backgroundColor: 'blue',
  },
  animatedViewsPositioner: {
    position: 'relative',
  },
})

In the example above, I first access the scroll position by applying a handleScroll function to the scrollView. Make sure you have scrollEventThrottle set to 16 to ensure the function is triggered every second, but beware of possible performance issue caused by that (if you care, you might take a look at this for more info).

To achieve a view change triggered when user scroll to certain position (which is actually not, but it looks like that), I use a view to wrap both red and blue views, the red one is default with opacity: 1, while the blue one with default opacity: 0, sitting on top of the red one.

I hide the red view and show the blue one by animating their opacity using interpolate(). With the help of that, both opacity values are controlled by one animatedValue animatedOpacityValue put in the state. I added a state showBlueView to optimise the performance by avoid constantly setting states triggered by onScroll.


Here's an update to add touchableOpacities on both views, simply achieve by hiding the blue view when it's unused.

First, add a log function:

log = (stringToPrint) => () => {
  console.log(stringToPrint)
}

Next, change the scrollView like this by adding two touchableOpacity

<ScrollView
  style={styles.scrollView}
  onScroll={this.handleScroll}
  scrollEventThrottle={16}
>
  <View style={styles.green} />
  <View style={styles.animatedViewsPositioner}>
    <Animated.View
      style={{
        ...styles.red,
        opacity: animatedOpacityValue.interpolate({
          inputRange: [0, 1],
          outputRange: [1, 0],
        }),
      }}
    >
      <TouchableOpacity
        style={{ backgroundColor: 'black', width: 80, height: 30 }}
        onPress={this.log('click on red')}
      />
    </Animated.View>
    {showBlueView && (
      <Animated.View
        style={{
          ...styles.blue,
          opacity: animatedOpacityValue.interpolate({
            inputRange: [0, 1],
            outputRange: [0, 1],
          }),
        }}
      >
        <TouchableOpacity
          style={{ backgroundColor: 'black', width: 80, height: 30 }}
          onPress={this.log('click on blue')}
        />
      </Animated.View>
    )}
  </View>
</ScrollView>

Note that I added showBlueView && to hide the blue view when its opacity is 0, so that it will not block any click event applied to the red view (even though the blue view is hidden, it is actually on top of the red view with opacity: 0).

Andus
  • 1,713
  • 13
  • 30
  • thanks very much for the answer. I am trying it out. I placed `TouchableOpacity` inside the `Animated.View` but won't work. Do you know how to handle `TouchableOpacity` inside `Animated.View`? – kirimi Jan 09 '20 at 07:15
  • What do you mean won't work? what are you trying to achieve? – Andus Jan 09 '20 at 07:17
  • sorry if I wasn't clear. I like your idea and it works well. But the reason I am trying to swipe `views ` is to place `buttons` and `touchableOpacity` inside the `animated.view`. But `onPress` will not work if I place a `button` or `touchableOpacity` inside your code `Animated.View` – kirimi Jan 09 '20 at 07:22
  • This is because you are actually having the blue view covering you red view, are you putting a `touchableOpacity` on the red view? – Andus Jan 09 '20 at 07:36
  • Actually I was planning to have `touchableOpacity` in both views. Is it possible to have `touchableOpacity` in both view? – kirimi Jan 09 '20 at 07:39
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/205643/discussion-between-andus-and-kirimi). – Andus Jan 09 '20 at 07:52
3

@Andus 's ans with Animated.event

The idea is to get the latest scrollY then wrap it to view's opacity. The example input range of blue target is 0-50 and got opacity 1 to 0. That means it would fade out when scrolling down the first 50 px.

The red one is the reverse one with input range 0-200 and out to opacity 0 to 1.(fade in)

import React, { Component } from 'react';
import { StyleSheet, Animated, View, ScrollView, SafeAreaView } from 'react-native';

export default class AnimationExample extends Component {
  constructor(props) {
    super(props)
    this.state = {
      scrollY: new Animated.Value(0)
    }
  }

  render() {
  const {scrollY} = this.state;
    return (
      <SafeAreaView style={{flex: 1}}>
      <ScrollView
        style={styles.scrollView}
        onScroll={Animated.event(
          [{nativeEvent: {contentOffset: {y: this.state.scrollY}}}]
        )}
        scrollEventThrottle={16}
      >
        <View style={styles.animatedViewsPositioner}>
          <Animated.View
            style={[styles.box, styles.target, {
             opacity: scrollY.interpolate({
                inputRange: [0, 50],
                outputRange: [1, 0],
              }),
            }]}
          />
          <Animated.View
           style={[styles.box, styles.origin, {
             opacity: scrollY.interpolate({
                inputRange: [0, 200],
                outputRange: [0, 1],
              }),
            }]}
          />
        </View>

      </ScrollView>
      </SafeAreaView>
    )
  }
}

const styles = StyleSheet.create({
  scrollView: {
    flex: 1,
  },
  box: {
    height: 1000,
    width: '100%',
    position: 'absolute'
  },
  origin: {
    backgroundColor: 'red',
    zIndex: 1
  },
  target: {
    backgroundColor: 'blue',
    zIndex: 2
  },
  animatedViewsPositioner: {
    position: 'relative',
    backgroundColor: 'pink',
    height: 10000
  },
})
Horst
  • 1,733
  • 15
  • 20
-1

If you are using ScrollView in displaying the View, I believe you can use the onScroll callback to get the position of your screen inside the ScrollView and change the height and color dynamically when your user scroll to the top.

<ScrollView onScroll={this.handleScroll} />

And getting the position,

handleScroll: function(event: Object) {
  console.log(event.nativeEvent.contentOffset.y);
},

Reference: Get current scroll position of ScrollView in React Native

brendan
  • 3,062
  • 3
  • 12
  • 16
  • Thanks for replying. I know I could dynamically change the color or the height. But this is not what I am trying to achieve. – kirimi Jan 08 '20 at 23:42