'How to animate expanding / collapsing a text preview in react native with Animated.View

I'm creating a text component that I want to be 2 lines by default, and if the user taps on it, it will expand to the full length, and if the user taps on it again, it will collapse back to 2 lines.

So far I have something like this in my return function:

<TouchableWithoutFeedback
    onPress={() => {
      toggleExpansion();
    }}
>
  <Animated.View style={[{ height: animationHeight }]}>
    <Text
      style={styles.textStyle}
      onLayout={event => setHeight(event.nativeEvent.layout.height)}
      numberOfLines={numberOfLines}
    >
      {longText}
    </Text>
  </Animated.View>
</TouchableWithoutFeedback>

My state variables and toggleExpansion function look like this:

const [expanded, setExpanded] = useState(false);
const [height, setHeight] = useState(0);
const [numberOfLines, setNumberOfLines] = useState();

const toggleExpansion = () => {
  setExpanded(!expanded);
  if (expanded) {
    setNumberOfLines(undefined);
  } else {
    setNumberOfLines(2);
  }
};

So far this works to expand and collapse but I'm not sure how to set the Animated.timing function to animate it. I tried something like this:

const animationHeight = useRef(new Animated.Value(0)).current;

useEffect(() => {
  Animated.timing(animationHeight, {
    duration: 1000,
    toValue: height,
    easing: Easing.linear
  }).start();
}, [height]);

but it didn't quite work. It doesn't display the text at all, and when I try initializing the new Animated.Value to a bigger number than the 2 line height (like 50), the height always gets truncated to 16 no matter how many times I expand and collapse. What's the best way to animate expanding and collapsing the text?



Solution 1:[1]

I needed to solve this for a dynamic height component, the text can be parsed HTML so we account for funky formatting such as extra lines. This will expand the view with the embedded HTML. If you simply want to control text layout you can just re-render the component by changing the state of the text props. Remove or change the color of the gradient to match your background.

The component works rendering the full-text view and getting the height with the "onLayout" listener, the initial view container is set to a static height, if the full height of the rendered text view is larger than the initial height then the "read more" button is displayed and the full height value is set for the toggle.

Also, If anyone is curious about the spring animation used, here is a great resource: https://medium.com/kaliberinteractive/how-i-transitioned-from-ease-to-spring-animations-5a09eeca0325

https://reactnative.dev/docs/animated#spring

import React, { useEffect, useState, useRef } from 'react';
import { 
    Animated,
    StyleSheet,
    Text, 
    TouchableWithoutFeedback,
    View, 
} from 'react-native';
import LinearGradient from 'react-native-linear-gradient';

const MoreText = (props) => {
    // const [text, setText] = useState('');
    const startingHeight = 160;
    const [expander, setExpander] = useState(false);
    const [expanded, setExpanded] = useState(false);
    const [fullHeight, setFullHeight] = useState(startingHeight);
    const animatedHeight = useRef(new Animated.Value(startingHeight)).current;

useEffect(() => {
    // expanded?setText(props.text): setText(props.text.substring(0, 40));
    Animated.spring(animatedHeight, {
        friction: 100,
        toValue: expanded?fullHeight:startingHeight,
        useNativeDriver: false
    }).start();
}, [expanded]);

const onTextLayout = (e) => {
    let {x, y, width, height} = e.nativeEvent.layout;
    height = Math.floor(height) + 40;
    if(height > startingHeight ){
        setFullHeight(height);
        setExpander(true);
    }
};

  return (
    <View style={styles.container}>
        <Animated.View style={[styles.viewPort, { height: animatedHeight }]}>
            <View style={styles.textBox} onLayout={(e) => {onTextLayout(e)}}>
                <Text style={styles.text}>{props.text}</Text>
            </View>
        </Animated.View>

        {expander &&
        <React.Fragment>
            <LinearGradient
                colors={[
                    'rgba(22, 22, 22,0.0)', // Change this gradient to match BG  
                    'rgba(22, 22, 22,0.7)',               
                    'rgba(22, 22, 22,0.9)',      
                ]}
            style={styles.gradient}/>
            <TouchableWithoutFeedback onPress={() => {setExpanded(!expanded)}}>
                <Text style={styles.readBtn}>{expanded?'Read Less':'Read More'}</Text>
            </TouchableWithoutFeedback>
            </React.Fragment>
        }
    </View>
 
  );
}

const styles = StyleSheet.create({
  absolute: {
    position: "absolute",
    height: 60,
    left: 0,
    bottom: 20,
    right: 0
  },
  container: {
    flex: 1,
  },
  viewPort: {
    flex: 1,
    overflow: 'hidden',
    top: 12,
    marginBottom: 20,
  },
  textBox: {
    flex: 1,
    position: 'absolute',
  },
  text: {
    color: '#fff',
    alignSelf: 'flex-start',
    textAlign: 'justify',
    fontSize: 14,
    fontFamily: 'Avenir',
  },
  gradient:{
    backgroundColor:'transparent', // required for gradient
    height: 40,  
    width: '100%', 
    position:'absolute', 
    bottom: 20
  },
  readBtn: {
    flex: 1,
    color: 'blue',
    alignSelf: 'flex-end',
  },
});

export default MoreText;

Solution 2:[2]

Here is the solution of your problem.

I'm creating a View component that I have text when you click on it, it will expand and if you again click on it, it will collapse.

 import React, { Component } from 'react';
 import { Text, View, StyleSheet, LayoutAnimation, Platform, UIManager,TouchableOpacity } from 'react-native';
 export default class App extends Component {
   constructor(){
     super();
      this.state = { expanded: false }
      if (Platform.OS === 'android') {
      UIManager.setLayoutAnimationEnabledExperimental(true);
      }
   }
   changeLayout = () => {
     LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
     this.setState({ expanded: !this.state.expanded });
   }
    render() {
      return ( 
       <View style={styles.container}> 
          <View style={styles.btnTextHolder}> 
             <TouchableOpacity activeOpacity={0.8} 
                    onPress={this.changeLayout} style={styles.Btn}> 
                <Text style={styles.btnText}>Expand / Collapse</Text>
             </TouchableOpacity>
             <View style={{ height: this.state.expanded ? null : 0,
                    overflow: 'hidden' }}>
             <Text style={styles.text}>
                Lorem Ipsum is simply 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>
           </View>
         </View> 
       </View> 
    );
   }
 }
  const styles = StyleSheet.create({
    container: { 
       flex: 1,
       paddingHorizontal: 10,
       justifyContent: 'center',
       paddingTop: (Platform.OS === 'ios') ? 20 : 0 },
    text: { 
       fontSize: 17,
       color: 'black',
       padding: 10 },
    btnText: {
       textAlign: 'center',
       color: 'white',
       fontSize: 20 },
    btnTextHolder: {
       borderWidth: 1,
       borderColor: 'rgba(0,0,0,0.5)' },
    Btn: { 
       padding: 10,
       backgroundColor: 'rgba(0,0,0,0.5)' }
  });

Hope it will works for you.

Solution 3:[3]

this solves your problem

import * as React from 'react';
import { Text, View, StyleSheet, Image,Animated,TouchableWithoutFeedback ,Easing} from 'react-native';

export default function AssetExample() {

const [expanded, setExpanded] = React.useState(true);
const animationHeight = React.useRef(new Animated.Value(2)).current;

const toggleExpansion = () => {
  setExpanded(!expanded);
};

React.useEffect(() => {
   if (expanded) {
  Animated.timing(animationHeight, {
    duration: 1000,
    toValue: 60,
    easing: Easing.linear
  }).start();
   }
   else{
     Animated.timing(animationHeight, {
    duration: 1000,
    toValue: 5,
    easing: Easing.linear
  }).start();
  }
}, [expanded]);

  return (
    <View style={styles.container}>
     <TouchableWithoutFeedback
    onPress={() => {
      toggleExpansion();
    }}
>
  <Animated.View style={[{ height: animationHeight }]}>
      <Text numberOfLines={expanded ? 30 : 2} ellipsizeMode="tail">
      {' line 1'}
    {'\n'}
      {'line 2'}
    {'\n'}
      {'line 3'}
    </Text>
  </Animated.View>
</TouchableWithoutFeedback>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    alignItems: 'center',
    justifyContent: 'center',
    padding: 24,
  },
 
});

expo

Solution 4:[4]

Improved a bit upon Yoel answer. You can animate a View, add a Text inside, and add a callback to when the animation ends, you clamp the text to a max number of 2 lines.

enter image description here

Expo snack with code: https://snack.expo.io/@lucaskuhn/collapsible-card-

*There seems to be a delay when closing the collapse, if anyone knows how to fix this issue please let me know.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Talha Akbar
Solution 3 Pavel Chuchuva
Solution 4 Lucas Kuhn