如何无侵入增强 TextInput,解决首字高度变动问题
在 React Native 中,当 TextInput 同时设置了 lineHeight 和 placeholder 时,部分机型上会出现:用户输入第一个字符的瞬间,输入框高度发生跳动。原因是系统在「仅显示 placeholder」与「显示实际文字」时对占位的计算不一致,导致布局重算后高度变化。
若希望业务侧继续写 import { TextInput } from 'react-native' 且不改现有用法,可采用 无侵入增强 React Native 组件方案 中的 Babel 别名 + react-native-proxy + TextInputShim:在 Shim 内用外层 View 固定布局 + 绝对定位的 placeholder Text,让 TextInput 本身不再接收 placeholder,从而避免「从 placeholder 切换到首字」时的高度变动。
思路
- 用 Babel 的
module-resolver把react-native指到项目内的react-native-proxy.js(若已为其他 Shim 配过则复用)。 - proxy 对
TextInput返回 TextInputShim,其余组件透传。 - 在 TextInputShim 内:
- 用一层 View 作为容器,接收业务传入的
style(含lineHeight、height、minHeight等),保证高度由容器统一决定。 - placeholder 不再传给原生
TextInput,而是用单独的 Text 组件、绝对定位叠在输入区域,仅当value为空时显示;样式用placeholderTextColor和可选的placeholderStyle。 - 原生 TextInput 不传
placeholder,style设为padding: 0, margin: 0并继承字号、颜色等,这样其高度不会因「有无 placeholder」或「首字」而变。
- 用一层 View 作为容器,接收业务传入的
- 若项目已有 TextInputShim(如 字体/屏幕缩放),可在其内合并本逻辑,不必新建 Shim。
1. 安装依赖
本方案依赖 Babel 的 module-resolver 插件做别名解析,需先安装(若已为其他 Shim 配过可跳过):
yarn add -D babel-plugin-module-resolver
2. Babel 配置
在 babel.config.js 的 module-resolver 里为 react-native 配置别名,指向项目中的 proxy 文件(路径按项目结构调整;若已为其他 Shim 配过可跳过):
// babel.config.js
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: [
[
'module-resolver',
{
root: ['./'],
extensions: ['.ts', '.tsx', '.ios.js', '.android.js', '.js', '.json'],
alias: {
'^react-native$': './app/utils/react-native-proxy.js',
// ... 其他 alias
},
},
],
],
};
3. react-native-proxy 中为 TextInput 返回 Shim
在 proxy 的 get(target, prop) 中为 TextInput 返回 TextInputShim(若已有 TextInput 相关分支,复用即可):
// app/utils/react-native-proxy.js
if (prop === 'TextInput') {
if (!module.exports.__TextInputShim) {
module.exports.__TextInputShim = require('./shim/TextInputShim').default;
}
return module.exports.__TextInputShim;
}
4. TextInputShim 实现
核心:容器 View 承载 style(含 lineHeight/height)、绝对定位的 Text 显示 placeholder、TextInput 不传 placeholder 且 padding/margin 为 0。
// app/utils/shim/TextInputShim.tsx
import React from 'react';
import type { TextInputProps, TextStyle, StyleProp } from 'react-native';
const RN = require('../../node_modules/react-native');
const TextInputShim = (props: TextInputProps & { placeholderStyle?: StyleProp<TextStyle> }) => {
const {
placeholder,
placeholderTextColor,
placeholderStyle,
style,
value,
...rest
} = props;
const flatStyle = RN.StyleSheet.flatten(style) || {};
const containerStyle = { justifyContent: 'center' as const, ...flatStyle };
const inputStyle = {
padding: 0,
margin: 0,
color: flatStyle.color,
fontSize: flatStyle.fontSize,
};
return (
<RN.View style={containerStyle}>
<RN.Text
style={[
{ position: 'absolute', color: placeholderTextColor ?? '#999' },
flatStyle,
placeholderStyle,
]}
pointerEvents="none">
{value != null && value !== '' ? '' : placeholder ?? ''}
</RN.Text>
<RN.TextInput
{...rest}
value={value}
style={inputStyle}
textAlignVertical="center"
/>
</RN.View>
);
};
export default TextInputShim;
说明:
- containerStyle:继承业务传入的
style(含lineHeight、height、minHeight等),并加justifyContent: 'center',使输入内容与 placeholder 垂直居中,高度由容器决定,不会随首字输入而变。 - placeholder:从 props 中解出,不传给
RN.TextInput;用绝对定位的RN.Text在无value时显示,避免原生 placeholder 参与高度计算。 - placeholderStyle:可选,便于单独设置 placeholder 的字体、行高。若需 TypeScript 感知,可在
react-native.d.ts中扩展TextInputProps。 - TextInput:
placeholder=""、padding: 0, margin: 0,仅保留颜色、字号等必要样式,避免额外占位导致高度跳动。
若项目已在 TextInputShim 中做了 字体缩放 或 吞字 的 style 处理,可在上述基础上继续对 flatStyle 做 fontFamilyByWeight、fontSizeToFit、maxFontSizeMultiplier 等处理后再赋给容器和 input。
小结
| 做法 | 作用 |
|---|---|
| 容器 View + 业务 style | 用 lineHeight/height/minHeight 固定输入区域高度,不随内容切换变化 |
| placeholder 用绝对定位 Text | 不把 placeholder 交给原生 TextInput,避免「placeholder → 首字」时系统重算高度 |
| TextInput padding/margin 0、placeholder="" | 输入框不额外占位,高度完全由容器和 lineHeight 决定 |
业务侧继续 import { TextInput } from 'react-native',无需改写法,即可无侵入地解决「lineHeight + placeholder 导致输入首字时高度变动」的问题。