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
CodePudding user response:
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>
);
}
CodePudding user response:
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>