A step-by-step guide on creating an animated text preview expansion and collapse effect in React Native using Animated.View

Creating a special text component that starts as 2 lines and expands to full length when tapped, then collapses back on a second tap.

The return function code includes:

<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>

The state variables and toggleExpansion function are:

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);
  }
};

Expanding and collapsing works, but struggling with Animated.timing for smooth transitions. Attempted code snippet:

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

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

Encountering issues where text doesn't display correctly and animations are not smooth. Seeking advice on improving the animated expansion and collapse functionality.

Answer №1

To accommodate dynamic height components, I needed a solution to parse HTML-formatted text that may include extra lines and funky formatting. This approach expands the view by embedding HTML content. If you only need to adjust text layout, consider re-rendering the component by modifying the state of the text props. Additionally, feel free to customize or remove the gradient color to match your background.

The functionality of the component involves rendering the full-text view and determining its height using an "onLayout" listener. The initial view container has a fixed height, and if the rendered text surpasses this height, a "read more" button appears, allowing toggling between full and excerpted views.

For those interested in the spring animation technique utilized, check out this helpful resource: https://medium.com/kaliberinteractive/how-i-transitioned-from-ease-to-spring-animations-5a09eeca0325

Explore more on spring animations in React Native: 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 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(() => {
    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)', // Adjust gradient to suit 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', // Necessary for gradient
    height: 40,  
    width: '100%', 
    position:'absolute', 
    bottom: 20
  },
  readBtn: {
    flex: 1,
    color: 'blue',
    alignSelf: 'flex-end',
  },
});

export default MoreText;

Answer №2

Check out the solution to your issue below:

I've developed a View component that displays text which expands when clicked, and collapses when clicked again.

 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)' }
  });

Give it a try and see if it works for you!

Answer №3

this solution will resolve your issue

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,
  },
 
});

check out the demo on Expo!

Answer №4

Building upon Yoel's response, I have made some improvements. One way to achieve a desired effect is by animating a View, incorporating text elements, and implementing a callback function to handle the end of the animation, where you can limit the text to no more than 2 lines.

https://i.sstatic.net/srdaZ.gif

If you want to explore further, check out this Expo Snack with the corresponding code:

*There appears to be a delay when closing the collapse feature. If you have any insights on resolving this issue, please share your knowledge.

Answer №5

Hey there! I wanted to give a quick heads up regarding Lucas Kuhn's inquiry about the delay. It seems like the issue might be related to using maxHeight style instead of height in Animated.View. Have you tried switching it to height to see if that resolves the problem? Let me know if you need further clarification on this matter.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What is the best method for conducting comprehensive testing of all projects and libraries within NestJS (nx)?

Our NestJS project has been established with multiple libraries through Nx. We have successfully run tests on individual projects/libraries using the following command: npx nx test lib1 --coverage While this method works well, we are faced with numerous l ...

What prevents React-app (written in TypeScript) from being able to hot reload when making changes?

There are three specific instances that have occurred on Window 10 WSL Ubuntu: Recently, I initiated a new React application with the --template typescript command. Despite running yarn start, none of the changes made are triggering hot reloading. Upon ...

The Angular directive ng-if does not function properly when trying to evaluate if array[0] is equal to the string value 'Value'

In my code, I want to ensure that the icon is only visible if the value at array index 0 is equal to 'Value': HTML <ion-icon *ngIf="allFamily[0] === 'Value'" class="checkas" name="checkmark"></ion-icon> TS allFamily = [ ...

Utilizing the class decorator pattern in TypeScript with a recursive original class method: A guide

For clarity, I am utilizing the decorator pattern/approach from and not the experimental decorators feature in TypeScript. The scenario involves extending the next method on the main class Foo. Various features are implemented by different classes like B ...

What is the best way to place these two items into an array in React Native?

I have these objects from a UseState variable called: const [Data, setData]= useState(); Which i got by doing: setData([{id: dataG.id, title: dataG.status +" "+ dataG.Type, status: dataG.status, location: dataG.Location, type: dataG.Type, injure ...

How to send parameters with the fetch API

After completing a task that involved converting code from Angular HttpClient to using fetch API, I encountered an issue with passing parameters. Below is the original code snippet before my modifications: let activeUrl = new URL(this.serverAddress); ...

React-native-web cannot be installed when using React 18

https://i.sstatic.net/6EfWq.pngEncountered errors while running npm install react-native-web. Despite creating a bug report, the issue remained unresolved. npm install react-native-web npm ERR! code ERESOLVE npm ERR! ERESOLVE unable to resolve dependency t ...

Utilizing Typescript's type inference within a universal "promisify" function

Unique Context Not long ago, I found myself delving into the world of "promisification" while working on a third-party library. This library was packed with NodeJS async functions that followed the callback pattern. These functions had signatures similar ...

Customizing your Mui theme with Typescript may lead to unexpected errors

Within my MUI theme, I am aiming to customize the link element as shown below: components: { MuiLink: { defaultProps: { component: LinkComponent, }, }, } However, I encountered the following TypeScript error: Type error: Ty ...

Refining Interface Properties by Eliminating Null Options

I am facing an issue with an interface property that can be null. I need to ensure it's not null before passing the object into a typesafe object. While narrowing works when assigning the property to a variable, it seems to fail when trying to use th ...

What is the best way to guarantee that an object contains certain fields using Partial<>?

I am dealing with a function that accepts a parameter as shown below: const handleAccount = ( account: Partial<IAccountDocument>, ... ) => { ... } It is crucial that the interface for IAccountDocument cannot be modified to avoid requiring cer ...

Using Angular 4 to delete selected rows based on user input in typescript

I am facing a challenge with a table that contains rows and checkboxes. There is one main checkbox in the header along with multiple checkboxes for each row. I am now searching for a function that can delete rows from the table when a delete button is clic ...

How can I implement a countdown timer on a consistent view using React Native Expo?

As someone who is new to both react and react native, I am currently using the expo cli to create a react native app. My question is: how can I reset a countdown every time a user views a certain view? It seems that unmounting doesn't work as it never ...

Typescript Mongoose is unable to recognize an incorrect key

When working with typescript and mongoose, I encountered an issue where mongoose was unable to detect invalid keys and changed all typescript keys to partial. model.ts type Coin = { symbol: string; name: string; } interface IDocument extends Coin ...

Hierarchy-based state forwarding within React components

As I embark on the journey of learning Typescript+React in a professional environment, transitioning from working with technologies like CoffeeScript, Backbone, and Marionettejs, a question arises regarding the best approach to managing hierarchical views ...

The type Observable<any> cannot be assigned to Observable<any> type

I am currently working with angular 5 and ionic 3. I have defined an interface: export interface IAny { getDataSource: Observable<any>; } Components that implement this interface must have the following method: getDataSource () { return ...

Ensure that the query value remains constant in Express.js

Issue: The query value is changing unexpectedly. // url: "http://localhost:4000/sr?q=%C3%BCt%C3%BC" export const search = async (req: Request, res: Response) => { try { const query = String(req.query.q) console.log("query: &quo ...

IONIC is displaying an error message indicating that there is no provider available for MapsAPIL

I am trying to implement a search feature with a Google map in my Ionic/Angular project. However, I encountered the following error: Runtime Error No provider for MapsAPILoader! Stack Error: No provider for MapsAPILoader! at inje ...

Utilizing Ionic 2 with Typescript for executing forEach operations

I am in the process of migrating my AngularJS application to Angular 2. In my AngularJS controller, I had a JSON array that I was iterating through to display data in an accordion list. Now, I need to implement the same functionality in my Angular 2 compon ...

Conflicting TypeScript errors arise from a clash between React version 16.14 and @types/hoist-non-react-statics 3.3.1

Currently in the process of upgrading an older project to React 16.14, as we are not yet prepared for the potential breaking changes that would come with moving up to React 17 or 18. As part of this upgrade, I am also updating redux and react-redux to ver ...