Is it possible to know how many lines of text are rendered by React Native when using numberOfLines prop? - react-native

The React Native numberOfLines prop is very useful but I want to programmatically adjust the height of my row between two numbers based on how many lines of text are actually rendered.
For example, I have a Text component of this form <Text numberOfLines={2} ellipsizeMode={'tail'}>{item.text}</Text>
If the text is longer than two lines, it defaults to two lines as desired. But when it is less than two lines, it just shows a single line, again as desired. I just want to know when the content is a single lien versus two lines. Is there any way of finding this out?

possible answer here
herehttps://stackoverflow.com/a/58632169/11816387
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>
);
}

In React Native, Text component has a props called onLayout
http://facebook.github.io/react-native/docs/text.html#onlayout
with {nativeEvent: {layout: {x, y, width, height}}}
So first, have a state
state = {
numOfLines: 0
}
Then in your Text component
<Text
numberOfLines={this.state.numOfLines}
onLayout={(e) => {
this.setState({ numOfLines: e.nativeEvent.layout.height > YOUR_FONT_SIZE ? 2 : 1 })
}
>
{item.text}
</Text>
I am not totally sure with this solution because I just think it from my mind straight away. But, my logic is if your text height is more than your text fontSize it means that it is more than one line?
Let me know if it is work or not

If you don't mind using an npm package react-native-text-size will solve your problem.
From their docs:
const size = await rnTextSize.measure({
text, // text to measure, can include symbols
width, // max-width of the "virtual" container
...fontSpecs, // RN font specification
})
"size" is not only the size, it has more info, one of which is lineCount, which is what you need.
Personally, I needed to send allowFontScaling: false in the fontSpecs, because we handle that internally.

Related

React Native flatlist inverted

I have a flatlist that shows a messages chat. I want to start at the bottom of the flatlist (showing the most recent messages). I added 'inverted={true}' and 'data={data.reverse()}' as the props.
It is sort of working, but the data alternates being reversed and not reversed every time i leave and enter the chat again.
Anyone know the fix?
Attached code
.reverse() mutates the original list.
If you do want your list reversed, do it like this
const App = ({ data }) => {
const reversed = [...data].reverse(); // Make a shallow copy, then reverse the copy
return <Flatlist data={reversed} inverted />;
};
However, you will probably notice that you don't want your list reversed, because inverted is already reversed.
const App = ({ data }) => {
return <Flatlist data={data} inverted />;
};

How to virtualize a FlatList inside FlatList (RN Web)?

How do I convince a vertical React Native FlatList to virtualize correctly inside another vertical (non-virtualizing) FlatList, in React Native Web?
So far, it seems that by default, scrolling to a certain point or responsive resize re-renderings tend to cause the virtualization to go haywire. This Snack demonstrates the problem. Be sure you're on the "Web" tab as the device builds seem to work correctly. Here's a repro through codesandbox too.
Update: Per request, here's the code inline as well. This is a full program that can paste into, say, a new expo init project (or similar) to see the strange behavior and experiment with it.
import React, { useCallback } from 'react';
import { FlatList, Text, useWindowDimensions, View } from 'react-native';
// Make 200 rows for the big list (which will draw green and red with some info).
const bigListData = Array(200).fill(0).map((element, index) => index);
function onViewableChange({ viewableItems }) {
if (viewableItems.length < 2) {
console.log(`VIEWABLE CHANGE! Only ${viewableItems.length} visible...`);
} else {
console.log(`VIEWABLE CHANGE! ${viewableItems[0].index} to ${viewableItems[viewableItems.length - 1].index}`);
}
}
function BigList() {
const { height, width } = useWindowDimensions();
const betweenRows = 10;
const itemHeight = height / 8;
const totalRowHeight = itemHeight + betweenRows;
const renderer = useCallback(({ item }) => {
const key = `i_${item}`;
return <View key={key} style={{
backgroundColor: item % 2 ? "red" : "green",
height: itemHeight,
width: '90%',
marginLeft: '5%',
marginBottom: betweenRows }}>
<Text>{key}, rh: {totalRowHeight}, offset: {totalRowHeight * item}, i {item}</Text>
</View>;
}, [itemHeight, totalRowHeight]);
const getItemLayout = useCallback((__data, index) => ({
index,
length: itemHeight,
offset: index * totalRowHeight
}), [itemHeight, totalRowHeight]);
return <FlatList
data={bigListData}
getItemLayout={getItemLayout}
key={'flatList'}
numColumns={1}
onViewableItemsChanged={onViewableChange}
renderItem={renderer}
/>;
}
function NoNestedFlatLists() {
const windowHeight = useWindowDimensions().height;
return <View style={{ height: windowHeight, width: '80%' }}><BigList /></View>;
}
function renderComponent({ item }) {
if (item.type === "widget") {
// Using height 600 here, but assume we cannot easily predict this height (due to text wrappings).
return <View key={item.type} style={{ backgroundColor: 'blue', height: 600, width: '100%', marginBottom: 15 }} />
}
return <BigList key={item.type} />;
}
function NestedFlatLists() {
const windowHeight = useWindowDimensions().height;
const components = [{ type: "widget" }, { type: "bigList" }];
return <FlatList
data={components}
key={'dynamicAppFlatList'}
numColumns={1}
renderItem={renderComponent}
style={{ height: windowHeight, width: '80%' }}
/>;
}
export default function App() {
const windowHeight = useWindowDimensions().height;
// Rendering just the following has no virtualization issues.
// The viewable change events make sense, no items suddenly disappear, no complete app meltdown...
//return <NoNestedFlatLists />;
// However:
// Any useful dynamic "rows of components" architecture melts down when virtualization comes into play.
// This sample represents such an app whose feeds have asked the app to render a "widget" followed by a
// "bigList" who could well have a few hundred items itself and thus really needs virtualization to work
// well on low-end devices. This demo leans on console logs. In snack.expo.dev, at time of writing, these
// feel hidden: Click the footer bar, either on the checkmark or an empty space, and then the "Logs" tab.
// Once you scroll down about half way in the "App", even slowly, you'll get logs like the following:
// Chrome: VIEWABLE CHANGE! 83 to 90
// Chrome: VIEWABLE CHANGE! 85 to 92
// Chrome: VIEWABLE CHANGE! Only 0 visible...
// Chrome: VIEWABLE CHANGE! 176 to 183
// Chrome: VIEWABLE CHANGE! 177 to 184
// At which time, all the UI disappears. What it thinks is viewable is quite wrong. Try to scroll around,
// but none of the mid rows are drawing. There is no easy way to repair app behavior from this state. The
// only rows which still draw correctly during the problem are the top and bottom non-virtualizing rows.
//
// As an alternate repro, you can scroll to near the middle and then resize the bottom of the window, and
// similar virtualization problems can occur. (In our real app, we can be scrolled almost anywhere out of
// the non-virtualizing rows, and make a 1px window resize to break the app. We have a more complex app
// structure, but I'm hoping a fix for this snack will still be applicable to our own symptoms...)
return <NestedFlatLists />;
}
Hopefully I am missing something trivial, as it seems clear React Native is attempting to handle nested FlatLists of the same orientation, and for the most part does great. Until you happen to have enough data items to bring virtualization into play, and even then, only fails for Web. (We've tried upgrading React Native to all the way to 0.67.2 and React Native Web to 0.17.5 - the latest releases - with no luck, and none of the Expo dropdown versions yield correct behavior in the linked Snack either.) What can I change in either sample to have correct virtualization in the nested FlatList?
Short answer is: You can't convince FlatList to virtualize this way correctly. At least currently (0.17), it's broken.
Although I was able to get some FlatList virtualization improvements into React Native Web's 0.18 preview, ultimately the measurement problems are deeper than I could afford to spend more weeks to fully fix. (If someone wants to try picking up from there - I recommend to focus on reconciling RNW's ScrollView versus RN's ScrollView and then digging into the ScrollView's measurements going absolutely haywire in the repro scenario, if replicating RN's evolution of ScrollView to RNW isn't enough.)
It ended up being much faster though to build our own virtualizing list component from scratch. Ours is specialized to our needs ATM so probably won't become open source, but who knows. But if you need to go this route... think about throttling reactions to scroll events and such to ".measure" the container view ref periodically and decide which things you need to render versus just rendering reserved empty space for... etc. There are other approaches but that seems to work.

How do I create a proper text editor in React Native?

I'm creating a note taking app on React Native, and at the moment the text editor is an enhanced TextInput with some extra functionalities like copying, pasting, inserting dates, etc.
The problem is that this is very limited as I can't add line numbers, nor change styles, coloring text, etc. Performance is also a concern for big documents.
I'm experimenting with splitting the text into lines and create one text input per line, but some problems appear: I can't select text across lines, I have to handle individual keystrokes to catch line breaks, the cursor won't move between text inputs, etc.
Another problem is that I can catch soft keyboard events, but no physical keyboard events, as per the onKeyPress documentation.
I wonder whether there is a good solution for this as it seems right now that using TextInputs won't allow me to do what I need.
A good answer would be either a good library, or directions on how to do this by hand, directly using components and catching keyboard events (assuming that this is even possible).
For clarification, I don't want a rich text editor library, I want the tools to build it. I also don't want to use a webview.
This is is the best thing I have found so far. It consists on adding Text components as children to the TextInput. It dates from 2015 and does not fully solve the problem though, but it's a start.
<TextInput
ref={this.textInputRef}
style={styles.input}
multiline={true}
scrollEnabled={true}
onChangeText={this.onChangeText}
>{
lines.map((line, index) => {
return <Text>{line + '\n'}</Text>;
})
}</TextInput>
It also confirms that this is not a trivial thing to do in React Native.
Commit in the React Native GitHub repository: Added rich text input support
According to this, images can be added too (but I haven't tested it).
I will edit if I find something else.
I will update later (I have to go to work), so I will post what I have and leave comments on what I was thinking
import React, {
useState, useEffect, useRef,
} from 'react';
import {
View, StyleSheet, TextInput, Text, useWindowDimensions,
KeyboardAvoidingView, ScrollView
} from 'react-native';
const TextEditor = ({lineHeight=20}) => {
const { height, width } = useWindowDimensions()
// determine max number of TextInputs you can make
const maxLines = Math.floor(height/lineHeight);
const [ textLines, setTextLines ] = useState(Array(maxLines).fill(''));
return (
<KeyboardAvoidingView
style={styles.container}
behavior={"height"}
>
<ScrollView style={{height:height,width:'100%'}}>
{/*Make TextInputs to fill whole screen*/}
<View style={{justifyContent:'flex-end'}}>
{textLines.map((text,i)=>{
let style = [styles.textInput,{height:lineHeight}]
// if first line give it extra space to look like notebook paper
if(i ==0)
style.push({height:lineHeight*3,paddingTop:lineHeight*2})
return (
<>
<TextInput
style={style}
onChangeText={newVal=>{
textLines[i] = newVal
setTextLines(textLines)
}}
key={"textinput-"+i}
/>
</>
)
})}
</View>
</ScrollView>
</KeyboardAvoidingView>
)
}
const styles = StyleSheet.create({
container:{
flex:1,
// paddingTop:40
},
textInput:{
padding:0,
margin:0,
borderBottomColor:'lightblue',
borderBottomWidth:1,
width:'100%',
}
})
export default TextEditor
Here's what I'm thinking:
wrap this in a ScrollView and use onEndReached prop to render an additional TextInput if the last TextInput is in focused and has reached maxed character limit
store all textinput values in an array

How do I get the vertical offset of a React Native TextInput in the onFocus event?

I want to get the vertical offset of a React Native TextInput in the onFocus event for that element. I have the following code in a TextInputCustom component I created:
let textInput;
<TextInput
onFocus={(evt) => {
textInput.measure((x, y, width, height, pageX, pageY) => {
console.log('textInput.measure', x, y, width, height, pageX, pageY);
});
}}
ref={(ref) => textInput = ref}
/>
This "works", in that I can see a value for pageY, but the problem is that when I add multiple TextInputCustom components to a screen, it seems to report out the same pageY value for all of them, as if the ref is shared by all of them or something like that. (Honestly, I'm not sure what's going on, but I'm sure that they're all reporting out the same pageY value, even though they're all clearly at differing vertical offsets.)
I was thinking about changing textInput.measure to evt.target.measure, but evt.target seems to be an element ID and not an actual element. I read some stuff on SO about using ReactNativeComponentTree to get the actual element from the element ID, but I can't seem to properly require/import it.
At this point I'm stuck, and looking for any advice on how to properly get the vertical offset of a TextInput element when it's focused on, even when there are multiple TextInput elements on a given screen. Thank you.
Edit: Max, as per your comments, I modified my functional component as follows:
const TextInputCustom = (props) => {
const textInputElem = useRef(null);
<TextInput
onFocus={(evt) => {
textInputElem.current.measure((x, y, width, height, pageX, pageY) => {
console.log('textInput.measure', x, y, width, height, pageX, pageY);
});
}}
ref={textInputElem}
/>
};
I've noticed that the onFocus event fires for the first TextInputCustom component on the screen, but none of the others. Any thoughts? Thank you.

Is there a way to animate the increased size of a View when new children are added?

I’m currently using LayoutAnimation to animate a view when children are added. However, since LayoutAnimation causes everything to be animated, globally, and I can’t easily use built-in Animated library to fit my use-case, I’m wondering if react-native-reanimated is able to help.
Here's a snack of my current solution:
https://snack.expo.io/#insats/height-adapation
This is what the result of that looks like:
Is there a way to achieve the same thing without using LayoutAnimation? I've looked through all exampled in react-native-reanimated, and I've read through the docs but I'm still not sure if this is possible to do or how I should get started. I've thought about using Animated to move the item-wrapper out of the viewable area and "scroll" it upwards (using transform translateY) when items are added, but that would require fixed height, which I don't have.
I have 2 approaches that I can suggest out of my mind:
You can configure your LayoutAnimation only when your desired state changed. If you use hooks it would be too easy:
const [state,setState] = useState([]);
useEffect(()=>{
/*rest code*/
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
},[state])
Or if you use class component you can catch your desired state change in componentDidUpdate:
componentDidUpdate(prevProps,prevState){
if(prevState.items!==state.items){
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
}
}
You can use onLayout function of view:
addItem = () => {
this.setState({
items: [...this.state.items, {title:'An item',isNew:true}]
})
};
renderItems = () => {
return this.state.items.map((item, index) => {
let opacity = new Animated.Value(0);
return (
<Animated.View onLayout={({nativeEvent})=>{
if(this.state.item.isNew){
// here you got the height from nativeEvent.layout.height
// Then you have to store the height animate height and opacity to its precise value
// PS I used opacity:0 to calculate the height
}
}} key={index} style={[styles.item,{opacity}>
<Text>{item.title}</Text>
</View>
)
});
};
When it comes to react-native-reanimated I regard it as more faster version of react-native's Animated library. So either way you will have to calculate the height!