Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 829
Kevin Avatar asked Jan 01 '23 02:01

Kevin


1 Answers

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;
like image 150
Ed Barahona Avatar answered May 07 '23 20:05

Ed Barahona