React Native: Determine number of lines of Text component
Asked Answered
F

11

36

As the title says, I've been trying to find a way to determine the number of lines the text component AFTER it has been given text. Look at my example below.

<Text>
    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi semper ut ipsum in ultrices. Vivamus fringilla lacinia odio in accumsan. Proin sit amet pellentesque tortor. Nam mollis sit amet ligula id convallis. Etiam in semper diam. Cras id elit consectetur, interdum ante id, tincidunt nisi. Integer non elit placerat, dignissim nibh at, faucibus sem. Curabitur nec posuere turpis. Vivamus rhoncus nulla vitae mi imperdiet, elementum eleifend mi laoreet. Vestibulum molestie turpis non nibh elementum, sed ornare magna tristique. Aliquam erat volutpat. Phasellus volutpat mi vel tempor finibus.
</Text>

At runtime, how can I determine how many lines this Text component has rendered. This number will vary depending on device (eg. iPhone 5 will need to render more lines vs iPhone 6+ as it has a smaller screen size). I've checked the source code for the Text component but there doesn't seem to be anything I'm looking for.

I am using React Native 0.24.

Any ideas?

Cheers.

Fag answered 15/7, 2016 at 1:48 Comment(1)
Would like to know the answer to this as wellSucrose
C
56

Edit: THIS NO LONGER WORKS in newer versions of react-native, I'm aware of that.

It once worked but the nativeEvent no longer returns the number of lines. I'm leaving this here as someone might be on an older version of react-native.

--

I want to provide a modern solution. There is now a onTextLayout event that includes an array of lines which can be determined what number of lines are being rendered. There's other details in the lines array like actual height and width of every line which can be further used to determine if the text is being truncated.

const NUM_OF_LINES = 5;
const SOME_LONG_TEXT_BLOCK = 'Lorem ipsum ...';

function SomeComponent () { 
  const [ showMore, setShowMore ] = useState(false);
  const onTextLayout = useCallback(e => {
    setShowMore(e.nativeEvent.lines.length > NUM_OF_LINES);
  }, []);

  return (
    <Text numberOfLines={NUM_OF_LINES} onTextLayout={onTextLayout}>
      {SOME_LONG_TEXT_BLOCK}
    </Text>
  );
}
Cooke answered 30/10, 2019 at 19:7 Comment(12)
Thank you Amo, your solution is the most elegant. But in order to works both for Android and iOS, the condition must be: lines.length >= NUM_OF_LINESIrrepealable
Alternative solution (not tested yet): github.com/nlt2390/react-native-view-more-textIrrepealable
Alternative solution (not tested yet): github.com/expo/react-native-read-more-textIrrepealable
@Amo the answer works fine to show read more option. But in one case it's not working. Suppose my complete text fits in 5 lines, as there is '>=' condition. So it shows Read more button in this case still, which is incorrect & doesn't show. What can we do in this case?Piperidine
@PiyushNaredi just change it to > vs >= and it'll only show it when the lines are actually greater than what is being shown. I edited the answer to reflect this.Cooke
@Cooke I have tried it. ShowMore button don't shows if I set condition to '>'. It only works for '>=' case. Please recheck it once.Piperidine
@Cooke i think its a typescript right, can you give a solution for reactnative class componentSemasiology
@Cooke This looks quite elegant. However i was trying the above in an expo snack but it seems nothing happens on clicking the ... Any help?Remittee
This doesn't work. e.nativeEvent.lines.length always returns NUM_OF_LINES Lesbianism
e.nativeEvent.lines.length equals to numberOfLines prop on iOS. See https://mcmap.net/q/394517/-react-native-determine-when-text-is-cut-offTishtisha
This does indeed not work since e.nativeEvent.lines only returns the rendered lines, so truncating them and then requesting the amount of lines will always return the truncated amount (numberOfLines property). To fix this, you should remove your numberOfLines property, then save the total number of lines in a state using onTextLayout , and then truncate them based on the total number of lines. Looks like @Jimbo Magusib has this case covered in his solution.Heptad
@Heptad As you suggested, I removed numberOfLines property to achieve the desired behaviour here https://mcmap.net/q/393871/-react-native-determine-number-of-lines-of-text-componentMicamicaela
P
9

This one is working for both IOS AND Android

    const [ loadMore, setLoadMore ] = useState(false);
    const [ numOfLines, setNumOfLines ] = useState(0);

    const onTextLayout = useCallback(e => {
        if(numOfLines == 0)
            setNumOfLines(e.nativeEvent.lines.length);
    });

    const onLoadMoreToggle = () => {
        setLoadMore(!loadMore);
    }
    
    return (
        <View style={styles.container}>
            <Text 
                numberOfLines={numOfLines == 0 ? null : loadMore ? numOfLines : NUM_OF_LINES} 
                onTextLayout={onTextLayout} 
                style={styles.bodyText}
            >
                {props.children}
            </Text>
            {
                (numOfLines > NUM_OF_LINES) &&
                <View style={styles.linkContainer}>
                    <TouchableRipple onPress={onLoadMoreToggle}>
                        <Text style={styles.linkText}>{ loadMore? 'Load Less' :'Load More'}</Text>
                    </TouchableRipple>
                </View>
            }
        </View>
    )

const styles = StyleSheet.create({
    container: {
        display: 'flex',
        flexDirection:'column',
    },
    bodyText: {
        flex:1,
    },
    linkContainer: {
        flexDirection: 'row',
        justifyContent: 'flex-end'
    },  
    linkText: {
        color: '#2196f3'
    }
})
Pavkovic answered 26/5, 2021 at 11:37 Comment(1)
This line of code is super confusing to me 😕: numberOfLines={numOfLines == 0 ? null : loadMore ? numOfLines : NUM_OF_LINES}. What would be a better name for the numOfLines prop?Tishtisha
S
6

It looks like React Native 0.24 implements an onLayout function
http://facebook.github.io/react-native/docs/text.html#onlayout

onLayout function
Invoked on mount and layout changes with
{nativeEvent: {layout: {x, y, width, height}}}

So it looks like you could pass an onLayout callback function, get the height of the Text component and then do some calculations using the line height to get to the number of lines

Sunlight answered 11/10, 2016 at 20:26 Comment(0)
P
4

You can use this formula:

CPL = Width / (font-size / font-constant)

font-constant = a constant specified to each font. CPL = number of characters per line

Here's some fonts with their constant:

 - Serif Fonts:
 American Typewriter — 2.14
 Baskerville — 2.14
 Georgia — 1.91
 Times New Roman — 2.21

 - Sans-serif Fonts:
 Arial — 1.91
 Calibri — 2.1
 Helvetica Neue — 1.9
 Lucida Grande — 1.91
 Tahoma — 1.91
 Trebuchet MS — 2.11
 Verdana — 1.73

 - Monospace Font:
 Courier New — 1.64

for example:

function getNumberOfLines(text, fontSize, fontConstant, containerWidth){

    let cpl = Math.floor(containerWidth / (fontSize / fontConstant) );
    const words = text.split(' ');
    const elements = [];
    let line = '';

    while(words.length > 0){
        if(line.length + words[0].length + 1 <= cpl || line.length === 0 && words[0].length + 1 >= cpl){
            let word = words.splice(0,1);
            if(line.length === 0){
                line = word;
            }else {
                line = line + " " + word;
            }
            if(words.length === 0){
                elements.push(line);
            }
        }
        else {
            elements.push(line);
            line = "";
        }
    }
    return elements.length;
}
Pastrami answered 19/3, 2018 at 16:29 Comment(2)
How do you know the font constants?Anu
there are variety fonts, and case upper it will be different font constants (same font)Addie
U
3

The solution Garrett McCullough have provided seem working for me, and I just want to add some code example:

import React from 'react';
import { StyleSheet, Text, View, TouchableHighlight } from 'react-native';

const styles = StyleSheet.create({
    text: {
      fontSize: 24,
      lineHeight: 30,
    }
});

export default class App extends React.Component {

    onLayout = e => {
        const { height } = e.nativeEvent.layout;
        this.count = Math.floor(height / styles.text.lineHeight)
    }

    render() {
        return (
            <View style={styles.page}>
              <Text onLayout={this.onLayout} style={styles.text}>
                Random text. Random text. Random text. Random text. Random text. Random text. Random text.
                Random text. Random text. Random text. Random text. Random text. Random text. Random text.
                Random text. Random text. Random text. Random text. Random text. Random text. Random text.
                Random text. Random text. Random text. Random text. Random text. Random text.
              </Text>

              <TouchableHighlight onPress={() => alert(`text lines count is ${this.count}`)}>
                <Text style={{ fontSize: 50 }}>touch me!</Text>
              </TouchableHighlight>
            </View>
        );
    }
}

https://snack.expo.io/@devastr1/text-lines-count-example

Unbeatable answered 9/7, 2019 at 14:24 Comment(0)
U
0

I have tried above solutions. IMHO the code block below works well for lists. Because it does not flick or show long text on initial render

export const SomeComponent = ({text}) => {

  const NUM_LINES = 4; //Just define the minimum lines that you want to show, no matter what

  const [hasMore, setHasMore] = useState(false);
  const [showMore, setShowMore] = useState(false);
  const [numOfLines, setNumOfLines] = useState(NUM_LINES);
  const readMoreText = showMore ? 'Show Less' : 'Read More...';

  
  const onLoadMoreToggle = () => {
    setShowMore(!showMore);
  };

  const onTextLayout = e => {
    //we are checking if the original text lines are above our minimum number of lines
    setHasMore(e.nativeEvent.lines.length > NUM_LINES);
    //storing original number of lines
    setNumOfLines(e.nativeEvent.lines.length);
  };

  if (isBlank(post.text)) {
    return null;
  }

  return (
    <Pressable onPress={() => onLoadMoreToggle()}>
      <Text
        onTextLayout={onTextLayout}
        style={{opacity: 0, position: 'absolute', ...styles.text}}>
        {post.text}
      </Text>
      <Text
        style={styles.text}
        numberOfLines={
          !hasMore ? NUM_LINES : showMore ? numOfLines : NUM_LINES
        }>
        {text}
      </Text>
      {hasMore && (
        <View style={styles.readMoreContainer}>
          <Text style={styles.readMore}>{readMoreText}</Text>
        </View>
      )}
    </Pressable>
  );
};
Useless answered 16/11, 2021 at 15:2 Comment(0)
H
0

My solution looks like this. You need to take into account the fact that the onTextLayout() will trigger each time you adjust any property (like numberOfLines), so you only want to set the total number of lines once when you haven't truncated the text (= passing undefined to numberOfLines).

@Swizes Also has a good solution if you'd like to calculate the totalNumberOfLines without any flashing of the text. (I don't seem to have any flashing, but it's possible)

const TRUNCATED_NUMBER_OF_LINES = 3;

const ReadMoreCustomText: React.FC<Props> = () => {
    const [isExpanded, setIsExpanded] = useState<boolean>(false);
    const [totalNumberOfLines, setTotalNumberOfLines] = useState<number>(0);
    const canReadMore = totalNumberOfLines > TRUNCATED_NUMBER_OF_LINES;

    useEffect(() => {
        onReadMoreChange?.(isExpanded);
    }, [isExpanded]);

    const onPress = () => setIsExpanded(wasExpanded => !wasExpanded);
    const onTextLayout = ({ nativeEvent: { lines } }: NativeSyntheticEvent<TextLayoutEventData>) => {
        // Only set it once to the total number of lines
        if (totalNumberOfLines === 0) {
            setTotalNumberOfLines(lines.length);
        }
    };

    return (
        <View style={ApplicationStyles.container}>
            <CustomText
                {...customTextProps}
                numberOfLines={isExpanded || !totalNumberOfLines ? undefined : TRUNCATED_NUMBER_OF_LINES}
                onTextLayout={onTextLayout}
            />
            {canReadMore && (
                <CustomText
                        text={t(isExpanded ? 'readLess' : 'readMore')}
                />
            )}
        </View>
    );
};
Heptad answered 6/2, 2023 at 10:12 Comment(0)
G
0
<Text onTextLayout={(e) => console.log(e.nativeEvent.lines.length)}> ... </Text>

This will log the number of lines of your text

Goldwin answered 15/1 at 23:52 Comment(0)
P
0

You can use onTextLayout which triggers when the number of lines change,onTextLayout={(event) => setLines(event.nativeEvent.lines.length)}

Paleoasiatic answered 23/1 at 15:33 Comment(0)
M
0

Here's a working example on both iOS & android.

import React, { useState } from "react"
import { Text } from "react-native"

const NUMBER_OF_LINES = 3

const sampleDescription =
  "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."

const TestComponent = () => {
  const [truncatedText, setTruncatedText] = useState("")
  const [showDescription, setShowDescription] = useState(false)
  const [showMoreButton, setShowMoreButton] = useState(false)
  return (
    <Text
      style={{ opacity: showDescription ? 1 : 0 }}
      onTextLayout={(e) => {
        if (!truncatedText && e.nativeEvent.lines.length > NUMBER_OF_LINES) {
          let text = e.nativeEvent.lines
            .map((line, index) => (index < NUMBER_OF_LINES ? line.text : ""))
            .join("")
          setShowMoreButton(true)
          setTruncatedText(text.slice(0, -11))
        }
        setShowDescription(true)
      }}
    >
      {showMoreButton ? truncatedText : sampleDescription}
      {showMoreButton && (
        <Text onPress={() => console.log("Handle Show More Text")}>{` . . . More >`}</Text>
      )}
    </Text>
  )
}

export default TestComponent


Micamicaela answered 20/3 at 18:0 Comment(0)
I
-1

return(
<View style={styles.container}>
<Text numberOfLines={2} ellipsizeMode='tail' style={styles.paragraph}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
</Text>
</View>
)



`numberOfLines={2} ellipsizeMode='tail'`
Imitation answered 26/4, 2023 at 8:44 Comment(1)
The question is to determine number of lines of Text component, and not to truncate the text.Dillie

© 2022 - 2024 — McMap. All rights reserved.