react-native-text-input-with-layout
v1.0.0
Published
React Native TextInput with onTextLayout: the layout of every rendered line, like Text#onTextLayout reports it
Maintainers
Readme
react-native-text-input-with-layout
React Native's TextInput with onTextLayout: the lines it lays out, measured natively and reported the way
<Text onTextLayout> reports them. Draw behind, around or on top of the text as it's typed — a background per line for
story captions, highlights, line counts.
- It's React Native's own
TextInput, so every prop, event and ref method works as usual. - Measured from the platform's own text layout as it changes: TextKit 2 on iOS, the
EditText'sLayouton Android. No hidden copy of the text to keep in sync. - Each line also comes with
contentX/contentWidth: its text without the space the line was wrapped at. - iOS and Android, New Architecture, no other dependencies.
Requirements
- React Native with the New Architecture (a Fabric component; tested with React Native 0.86 and Expo SDK 57).
- iOS 15.1 or newer, Android 7.0 (API 24) or newer.
Installation
npx expo install react-native-text-input-with-layout
# or
npm install react-native-text-input-with-layoutThen rebuild the app (npx expo run:ios / run:android, or a new development build): the library has native code, so
it doesn't run in Expo Go. No config plugin is needed. In a React Native app without Expo, run pod install in ios/.
Usage
import { useState } from 'react';
import { View } from 'react-native';
import {
TextInputWithLayout,
type TextInputLayoutLine,
} from 'react-native-text-input-with-layout';
function Caption() {
const [text, setText] = useState('');
const [lines, setLines] = useState<TextInputLayoutLine[]>([]);
return (
<View>
{/* A background behind each line's text, under the input. */}
{lines.map((line, index) => (
<View
key={index}
style={{
position: 'absolute',
left: line.contentX - 6,
top: line.y,
width: line.contentWidth + 12,
height: line.height,
borderRadius: 6,
backgroundColor: '#e2486b',
}}
/>
))}
<TextInputWithLayout
multiline
value={text}
onChangeText={setText}
onTextLayout={(event) => setLines(event.nativeEvent.lines)}
style={{ padding: 16, fontSize: 24, color: '#fff' }}
/>
</View>
);
}Line frames are relative to the input's top-left corner, padding included, so a layer positioned over the input (as
above, or with StyleSheet.absoluteFill in the input's parent) takes them as they are.
API
<TextInputWithLayout>
Takes every TextInput prop, plus onTextLayout. Its ref is the TextInput's (focus(), blur(), clear(),
setSelection(), …).
The input is wrapped in a native view that measures it. Styles that place the input — margin*, position and the
insets, flex*, alignSelf, width/height and their min/max, aspectRatio, zIndex, transform — go to that
view; all the others (padding, border, background, font, …) stay on the input.
onTextLayout(event)
event.nativeEvent.lines holds one TextInputLayoutLine per rendered line, in order:
| Field | Description |
| -------------- | -------------------------------------------------------------------------------------------------------- |
| text | The line's characters, with the space or line break it ends with. |
| x, y | Top-left corner of the line, relative to the input's top-left corner. |
| width | Width of the line. On a wrapped line, this includes the space it was broken at, as in Text. |
| height | Height of the line. |
| contentX | Left edge of the line's text without its trailing whitespace. On a right-to-left line, it's moved right. |
| contentWidth | Width of the line's text without its trailing whitespace. Use these two to draw behind the text. |
| ascender | Distance from the top of the line to its baseline. |
| descender | Distance from the baseline to the bottom of the line. |
| capHeight | Height of a capital letter in the line's font. |
| xHeight | Height of a lowercase "x" in the line's font. |
All values are in points (dp). The fields are those of Text#onTextLayout, plus contentX and contentWidth.
It fires once the input is laid out, and again whenever its lines change: as text is typed or set from JS, and when a style or the width changes them. When the lines come out the same, it doesn't fire.
An empty input, and the line after a trailing line break, are reported as one empty line where the cursor goes.
Lines are in the coordinates of the text, not of the visible part of the input: when a multiline input with a fixed
height scrolls, subtract its scroll offset (from onScroll) to place them on screen. A single-line input reports one
line, which can be wider than the input when its text is.
<TextInputLayoutView>
The native view TextInputWithLayout is made of, for wrapping another TextInput-based component (say, one from a
gesture or keyboard library). It takes ViewProps, onTextLayout, and value, the input's controlled value, which iOS
needs to measure text set from JS. It measures the first TextInput inside it.
<TextInputLayoutView value={text} onTextLayout={onTextLayout}>
<SomeTextInput value={text} onChangeText={setText} />
</TextInputLayoutView>How it works
- iOS: the view finds the
UITextView(multiline) orUITextFieldof the input inside it, follows its text storage edits, and measures in its next layout pass. AUITextViewis read through TextKit 2'sNSTextLayoutManagerand is never switched over to TextKit 1 (on iOS 15, or if something else already switched it, TextKit 1 is read instead). AUITextFielddoesn't wrap, so its single line is measured from its text. - Android: right before each draw, the view checks a small key (the
Layout, its width, a hash of the text, the padding) and reads theEditText'sLayoutwhen it changed. React Native doesn't tellTextWatchers about text set from JS, so this catches every change; frames where nothing changed cost nothing.capHeightandxHeightare measured from "T" and "x", as React Native does forText.
The values come from each platform's own text layout, so they differ between iOS and Android as much as the text does.
On the web, the input renders as usual and onTextLayout doesn't fire.
Migrating from 0.1
0.2 replaces the input 0.1 drew itself, with Nitro, by React Native's own TextInput:
react-native-nitro-modulesisn't needed anymore.- Every
TextInputprop works. Events areTextInput's:onInputFocus/onInputBlurareonFocus/onBlur, andonSubmitEditing,onContentSizeChangeandonSelectionChangegetTextInput's events (the text isevent.nativeEvent.text). onTextLayoutgets a native event: the lines areevent.nativeEvent.lines(wereevent.lines).baselineis gone (it'sline.y + line.ascender);contentXandcontentWidthare new.- The
refis theTextInput's.measureLines()is gone: keep the lines from the lastonTextLayout. - Types:
TextLayoutisTextInputLayoutLine,TextLayoutEventisTextInputLayoutEvent, andTextInputWithLayoutInstanceisComponentRef<typeof TextInput>.TextInputWithLayoutViewis gone;TextInputLayoutViewis the wrapper described above.
Contributing
License
MIT
Made with create-react-native-library
