skip to Main Content

On TikTok and Instagram, they can generate automatically text font size like this image, where alternating lines have different font sizes automatically. I’m trying to figure out how to code that in React Native for mobile IOS and Android: [[enter image description here](https://i.stack.imgur.com/vkhIo.jpg)](https://i.stack.imgur.com/XcjLq.jpg)

I couldn’t figure it out. I made something that I’m not crazy about, which is just having a larger font on the first three lines and then a smaller font. See image: But I don’t like it. enter image description here

2

Answers


  1. I totally misunderstood what PixelRatio.getFontScale did. I thought it would provide the average width a single character takes up on screen. If you can find a way to get a rough estimate of the width of a single character, then this method will work link:

    import { useEffect, useState } from 'react';
    import { View, StyleSheet, Text, PixelRatio } from 'react-native';
    import rnTextSize from 'react-native-text-size';
    import reduce from 'awaity/reduce';
    import useViewLayout from './useViewLayout';
    const fontScale = PixelRatio.getFontScale();
    
    const showLayoutValues = true
    
    export default function MultiLineText({
      width,
      containerStyle,
      textStyle1 = { fontSize: 16 },
      textStyle2 = { fontSize: 22 },
      str,
      ...textProps
    }) {
      // containerLayout will provide the max width each line can have
      const [containerLayout, onContainerLayout] = useViewLayout();
    
      // lines was created in a useMemo hook but I wasnt sure if
      // useMemo could handle async
      const [lines, setLines] = useState([]);
      useEffect(() => {
        const calcLines = async () => {
          let words = str.split(' ').filter((s) => s.trim().length);
          let newLines = await words.reduce(
            async (prevPromise, curr, index) => {
              const prev = await prevPromise;
              let lineIndex = prev.length - 1;
              let style = index % 2 == 0 ? textStyle1 : textStyle2;
              const fontSize = style.fontSize;
              // I wanted to use this https://github.com/aMarCruz/react-native-text-size/
              // to measure text width but expo doesnt support it
              const config = {
              // if you exported from expo and link rnTextSize set this to true
                useMeasureModule:false,
                fontProps:style
              }
              const useMeasureModule = false;
              let lineWidth = await getTextWidth(
                prev[lineIndex],
                fontSize,
                config
              );
              let wordWidth = await getTextWidth(curr, fontSize, config);
              // if currentLine can fit the next word add it
              if (lineWidth + wordWidth < (width || containerLayout.width))
                prev[lineIndex] += curr + ' ';
              // or put it on the next line
              else {
                prev[lineIndex + 1] = curr + ' ';
              }
              return prev;
            },
            ['']
          );
          setLines(newLines);
        };
        calcLines();
      }, [str, containerLayout, width, textStyle1, textStyle2]);
      return (
        <>
          {showLayoutValues && <Text>Container Layout: {JSON.stringify(containerLayout,null,4)}</Text>}
          <View
            style={[styles.container, containerStyle]}
            onLayout={onContainerLayout}>
            {lines.map((line, i) => (
              <Text
                {...textProps}
                // to ensure that lines dont wrap
                numberOfLines={1}
                style={[textProps.style, i % 2 == 0 ? textStyle1 : textStyle2]}>
                {line}
              </Text>
            ))}
          </View>
        </>
      );
    }
    
    const getTextWidth = async (str, fontSize, config={}) => {
      const {fontProps, useMeasureModule} = config;
      if (!useMeasureModule) {
        // random mathing
        return str.length * fontScale * fontSize ** 0.8;
      }
      let measure = await rnTextSize.measure({
        ...fontProps,
        text: str,
      });
      return measure.width;
    };
    
    const styles = StyleSheet.create({
      container: {
        width: '100%',
      },
    });
    

    And then in use:

    
    export default function App() {
      return (
        <View style={styles.container}>
          <MultiLineText
            containerStyle={{
              width: '100%',
              backgroundColor: '#eef',
              alignSelf: 'center',
              alignItems: 'center',
            }}
            textStyle1={styles.text}
            textStyle2={styles.text2}
            str="I am really not sure as of how long this text needs to be to exceed at least 3 lines. I could copy and paste some stuff here but I think if I just type and type it would be quicker than googling, copying, and pasting"
          />
        </View>
      );
    }
    
    Login or Signup to reply.
  2. I just found out that onTextLayout is a thing. It gives about each line in the Text component, including info about the characters present on each line. This could be used to figure out where to break lines of text (no planned web support):

    After tying the str prop to a text input it became very clear that it is ideal to prevent this component from re-rendering as much as possible so I made additional changes (demo)

    import { useState, useCallback, useEffect, memo } from 'react';
    import { View, StyleSheet, Text, ScrollView } from 'react-native';
    
    // text lines will alternate between these styles
    const defaultLineStyles = [
      { color: 'red', fontSize: 16 },
      { color: 'blue', fontSize: 22 },
      { color: 'green', fontSize: 28 },
    ];
    
    function MultiLineText({
      containerStyle,
      lineStyles = defaultLineStyles,
      str,
      ...textProps
    }) {
      const [lines, setLines] = useState([]);
      // each time a substring is added to line,
      // remove the substring from remainingStr
      const [remainingStr, setRemainingStr] = useState('');
      const onTextLayout = useCallback((e) => {
        // the first line of text will have the proper styling
        let newLine = e.nativeEvent.lines[0].text;
        setLines((prev) => {
          return [...prev, newLine];
        });
        // remove newLine from remainingStr
        setRemainingStr((prev) => prev.replace(newLine, ''));
      }, []);
      // when str changes reset lines, and set remainingStr to str
      useEffect(() => {
        setLines([]);
        setRemainingStr(str);
        
      }, [str]);
      return (
        <>
        <View style={[styles.container, containerStyle]}>
          <ScrollView style={{ flex: 1 }}>
            {lines.map((line, i) => (
              <Text
                {...textProps}
                style={[textProps.style, lineStyles[i % lineStyles.length]]}>
                {line}
              </Text>
            ))}
          </ScrollView>
          {/* this view will be invisible*/}
          {remainingStr.length > 0 && (
            <View style={{ opacity: 0 }}>
              <Text
                {...textProps}
                onTextLayout={onTextLayout}
                style={[
                  textProps.style,
                  // use lines.length to get proper style
                  lineStyles[lines.length % lineStyles.length],
                ]}>
                {remainingStr}
              </Text>
            </View>
          )}
        </View>
        </>
      );
    }
    
    const styles = StyleSheet.create({
      container: {
        width: '100%',
        height:'50%'
      },
    });
    // be careful when passing non memoized array/objects
    export default memo(MultiLineText)
    

    Its important to note that objects/arrays that arent memoized/state/refs will cause the memoized component to re-render, even if the values are static e.g

    <MultiLineText
       containerStyle={{
         width: '100%',
         height: 200,
         backgroundColor: '#eef',
         alignSelf: 'center',
         alignItems: 'center',
      }}
      style={styles.defaultTextStyle}
      str={text}
      lineStyles={[styles.text,styles.text2]}
    />
    

    containerStyle and lineStyles are getting new objects and arrays every time its parent component re-render, which will make MultiLineText re-render (even though its memoized). After moving the containerStyle to the stylesheet and memoizing lineStyles re-rendering becomes better:

    const lineStyles = React.useMemo(()=>{
        return [styles.text,styles.text2]
      },[])
      return (
        <View style={styles.container}>
          <TextInput onChangeText={setText} label="Enter some text" value={text} />
          <MultiLineText
            containerStyle={styles.textContainer}
            style={styles.defaultTextStyle}
            str={text}
            lineStyles={lineStyles}
          />
        </View>
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search