How do you animate the height in react native when you don't know the size of the content?

JavascriptReact NativeAnimation

Javascript Problem Overview


In react-native, how do you animate the size of a View when you dont know the size of it's contents?

Let's say the View's content height can be anywhere from 0-400pts, and the content can grow and shrink dynamically, and you want to animate any changes to the height.

Basically, I'm looking to reproduce the behaviour of LayoutAnimation without using LayoutAnimation but using Animated.

I think what eludes me is I don't know how to animate towards a target height since I don't know the height of the content.

Javascript Solutions


Solution 1 - Javascript

I use LayoutAnimation for that, just before the state change that causes your component's height to change, add:

You can use different presets:

  • spring
  • easeInEaseOut
  • linear

You can read more about it here https://facebook.github.io/react-native/docs/layoutanimation.html

Solution 2 - Javascript

You are going to have to add some kind of size scaling, probably with percentages for best effect.

First thing first you would need to use Animated.View instead of View.

Next you would need to apply a transform to the style of the view lets say it looks like below. This is the part that updates and changes and creates the motion.

        <Animated.View style={[ styles.someStyleYouMade,
          {
            transform: [
              // scaleX, scaleY, scale, theres plenty more options you can find online for this.
              {  scaleX: ViewScaleValue } // this would be the result of the animation code below and is just a number.
            ]
          } 
        ]}
        >

This next part is basically an animated API example, you would write something like this (custom to how you want) and when this script is called it will animate in whatever way you specify.

  Animated.timing(                    // Animate over time
    this.state.ViewScale,             // The animated value to drive, this would be a new Animated.Value(0) object.
    {
      toValue: 100,                   // Animate the value
      duration: 5000,                 // Make it take a while
    }
  ).start();

Lastly you will probably want to apply an interpolation to the value to make it look as custom as possible.

(this will go into your render() function but before the return(). the ViewScaleValue will go into the Animated.View transform)

const ViewScaleValue = this.state.ViewScale.interpolate({
  inputRange: [0, 25, 50, 75, 100],
  outputRange: [0, .5, 0.75, 0.9, 1]
});

all of this code would make ViewScaleValue, a simple number, animate from 0-100, going fast and then slow (because of interpolation) and apply each iteration of the animation to the Animated.View transform.

Read the Animated API alongside this to get a good grasp on it.

Solution 3 - Javascript

Hi everyone, hope it's not too late...

for anyone who is dealing with React Native animation on view's height.
I know it is very annoying as:

✖️ React Native animation seems not supporting layout styles (e.g. width and height)
✖️ LayoutAnimation looks complicated to investigate
✖️ Wish to use an official way to animate instead of installing a third-party package
✖️ Sometimes content could be large to break your view styles

so here is my solution for you (class component way):

First, set the animated value in state:

state = { height: new Animated.Value(0) };

Next, set your animated view's max height with animation interpolation:

const maxHeight = this.state.height.interpolate({ 
  inputRange: [0, 1], 
  outputRange: [0, 2000]  // <-- any value larger than your content's height
};
return (<Animated.View style={[styles.box, { maxHeight: maxHeight }]} />); 
// any other fixed styles in styles.box

After that, set the animation inside the function you called,
or componentDidMount if you want it to show as soon as it rendered:

// or in any function that users interact
componentDidMount() {
  Animated.timing(this.state.formHeight, {
    toValue: 1,
    duration: 500,           // <-- animation duration
    easing: Easing.linear,   // <-- or any easing function
    useNativeDriver: false   // <-- need to set false to prevent yellow box warning
  }).start();
}

Be aware that don't set useNativeDriver to true as it is not supported on layout styles.


Sample

So below is a sample for you to interact with,
feel free to copy and paste to your React Native project to have a try:

import React, { PureComponent } from 'react';
import { Animated, Button, Easing, View, Text, StyleSheet } from 'react-native';

class AnimateBox extends PureComponent {
  state = { opacity: new Animated.Value(0), height: new Animated.Value(0) };

  showContent = () => {
    const { opacity, height } = this.state;

    Animated.timing(height, {
      toValue: 1,
      duration: 500,
      easing: Easing.linear,
      useNativeDriver: false  // <-- neccessary
    }).start(() => {
      Animated.timing(opacity, {
        toValue: 1,
        duration: 500,
        easing: Easing.linear,
        useNativeDriver: false  // <-- neccessary
      }).start();
    });
  };

  render() {
    const { opacity, height } = this.state;
    const maxHeight = height.interpolate({ 
      inputRange: [0, 1], 
      outputRange: [0, 1000]  // <-- value that larger than your content's height
    });

    return (
      <View style={styles.box}>
        <Animated.View style={{ opacity: opacity, maxHeight: maxHeight }}>
          <Text style={styles.content}>
            Lorem Ipsum is simply a dummy text of the printing and typesetting industry.
            Lorem Ipsum has been the industry's standard dummy text ever since the 1500s,
            when an unknown printer took a galley of type and scrambled it to make a type specimen book.
            It has survived not only five centuries, but also the leap into electronic typesetting,
            remaining essentially unchanged. It was popularised in the 1960s with the release of
            Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
          </Text>
        </Animated.View>
        <View style={styles.spacing}>
          <Button title="Show content" onPress={this.showContent} />
        </View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  box: {
    backgroundColor: '#fff',
    marginHorizontal: 15,
    paddingHorizontal: 15
  },
  spacing: {
    paddingVertical: 10
  },
  content: {
    fontSize: 16,
    lineHeight: 30,
    color: '#555'
  }
});


export default AnimateBox;

Happy Coding :)


More Explanation (Edit in 03 Feb 2021)

As mentioned in React Native documentation:
https://reactnative.dev/docs/animations#caveats > Not everything you can do with Animated is currently supported by the native driver. The main limitation is that you can only animate non-layout properties: things like transform and opacity will work, but Flexbox and position properties will not.

Disabling useNativeDriver can animate styles other than opacity and transform, but in fact increase the workload on JS thread as JS thread needs to calculate the UI on every frame.

And here in React Native 0.62's blog:
https://reactnative.dev/blog/2020/03/26/version-0.62#deprecations > Setting useNativeDriver is now required to support switching the default in the future.

You must need to set the useNativeDriver in animation options on React native v0.62 or above to prevent any warnings.

Solution 4 - Javascript

The method I've taken is to spend layout passes getting the height of the "truncated" component and the height of the "full size" component (you need a way for the truncated height to be deterministic, usually by knowing how to render a "row" of content). Essentially before you have those values, you render these as two separate views that are hidden:

hidden: {
  position: 'absolute',
  left: 0,
  top: 0,
  opacity: 0,
},

Use onLayout to capture their heights:

const onLayoutTruncated = ({nativeEvent}: LayoutChangeEvent) => {
  if (!doneProcessing) {
    truncatedHeight = nativeEvent.layout.height;
    checkIfDoneProcessingLayout();
  }
};

const onLayoutFull = ({nativeEvent}: LayoutChangeEvent) => {
  if (!doneProcessing) {
    fullHeight = nativeEvent.layout.height;
    checkIfDoneProcessingLayout();
  }
};

checkIfDoneProcessingLayout() will check if both truncatedHeight and fullHeight are set, and make a state change if they both are (doneProcessing = true).

From there you should unhide the truncated view and be able to animate between both height values using an Animated.Value and interpolation:

const expandingHeight = animatedValue.interpolate({
  inputRange: [0, 1],
  outputRange: [truncatedHeight, fullHeight],
});

Trigger the expanding/collapsing animation on click using Animated.timing

Animated.timing(animatedValue, {toValue: isExpanded ? 0 : 1, easing: SOME_EASING_FUNCTION, duration: SOME_DURATION}).start();

Solution 5 - Javascript

Bases on the solution of @Wing Choi I want to share my improved solution. This should work plug&play.

  • Instead of using a maxHeight (not so nice for animations) I used the onLayout-Event to always have the exact height. (Will be fired on all inner and outer Changes of the Layer)
  • Instead of just open the layer with a button, I added a property "open" (bool) to controle the Layer from a parent Component.
import React, { PureComponent } from 'react';
import { Animated, Easing, View } from 'react-native';
import PropTypes from "prop-types";

class AnimateBox extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      init: false,
      opacity: (this.props.open) ? 1 : 0,
      height: 1000
    };
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    if (this.state.init && prevState.open !== this.props.open) {
      this.showContent();
    }
  }

  adjustHeight(layout) {
    const height = Math.round(layout.height);

    this.setState({
      init: true,
      opacity: new Animated.Value((this.props.open) ? 1 : 0),
      height: new Animated.Value((this.props.open) ? height : 0),
      interpol: {
        inputRange: [0, 1],
        outputRange: [0, height]
      }
    });
  }

  showContent = () => {
    const { opacity, height } = this.state;

    Animated.timing(height, {
      toValue: (this.props.open) ? 1 : 0,
      duration: 400,
      easing: Easing.out(Easing.ease),
      useNativeDriver: false
    }).start(() => {
      Animated.timing(opacity, {
        toValue: (this.props.open) ? 1 : 0,
        duration: 300,
        easing: Easing.linear,
        useNativeDriver: false
      }).start();
    });
  };

  render() {
    const { opacity, height, interpol } = this.state;
    let animHeight = height;
    if (this.state.init) {
      animHeight = height.interpolate(interpol);
    }
    return (
      <Animated.View style={{ position: 'relative', opacity: opacity, height: animHeight, overflow: 'hidden' }}>
        <View style={{position: 'absolute', top: 0, left: 0, right: 0}}
              onLayout={(event) => { this.adjustHeight(event.nativeEvent.layout)}} >
          {this.props.children}
        </View>
      </Animated.View>);
  }
}

AnimateBox.propTypes = {
  open: PropTypes.bool
};

AnimateBox.defaultProps = {
  open: false
}

export default AnimateBox;

Solution 6 - Javascript

I use own component with @react-spring/native

something like this

import React from 'react';
import {View, ViewProps} from 'react-native';

interface GetDimensionsOfThisContainerProps extends ViewProps {
  children: React.ReactChild[];
  onDimensions: ({width, height}: {width: Number; height: Number}) => void;
}

export const GetDimensionsOfThisContainer: React.FC<
  GetDimensionsOfThisContainerProps
> = ({children, onDimensions, ...props}: GetDimensionsOfThisContainerProps) => {
  return (
    <View
      onLayout={event =>
        onDimensions({
          width: Math.round(event.nativeEvent.layout.width),
          height: Math.round(event.nativeEvent.layout.height),
        })
      }
      style={{position: 'absolute', width: '100%'}}
      {...props}>
      {children}
    </View>
  );
};

      <GetDimensionsOfThisContainer
        onDimensions={({height: _height}) => {
          if (dynamicHeight !== _height) setDynamicHeight(_height);
        }}>
       {children}
      </GetDimensionsOfThisContainer>

source: https://gist.github.com/sturmenta/af790331f6bd27322ecd73ee723c8c60

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionPoyanView Question on Stackoverflow
Solution 1 - JavascriptMahmoud FelfelView Answer on Stackoverflow
Solution 2 - JavascriptTierna D. Jack LawlessView Answer on Stackoverflow
Solution 3 - JavascriptWing ChoyView Answer on Stackoverflow
Solution 4 - JavascriptsbearbenView Answer on Stackoverflow
Solution 5 - JavascriptJoachim FeltkampView Answer on Stackoverflow
Solution 6 - JavascriptNicolas SturmView Answer on Stackoverflow