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?
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;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With