如何无侵入增强 TextInput,解决首字高度变动问题

在 React Native 中,当 TextInput 同时设置了 lineHeightplaceholder 时,部分机型上会出现:用户输入第一个字符的瞬间,输入框高度发生跳动。原因是系统在「仅显示 placeholder」与「显示实际文字」时对占位的计算不一致,导致布局重算后高度变化。

若希望业务侧继续写 import { TextInput } from 'react-native' 且不改现有用法,可采用 无侵入增强 React Native 组件方案 中的 Babel 别名 + react-native-proxy + TextInputShim:在 Shim 内用外层 View 固定布局 + 绝对定位的 placeholder Text,让 TextInput 本身不再接收 placeholder,从而避免「从 placeholder 切换到首字」时的高度变动。

思路

  1. 用 Babel 的 module-resolverreact-native 指到项目内的 react-native-proxy.js(若已为其他 Shim 配过则复用)。
  2. proxy 对 TextInput 返回 TextInputShim,其余组件透传。
  3. 在 TextInputShim 内:
    • 用一层 View 作为容器,接收业务传入的 style(含 lineHeightheightminHeight 等),保证高度由容器统一决定。
    • placeholder 不再传给原生 TextInput,而是用单独的 Text 组件、绝对定位叠在输入区域,仅当 value 为空时显示;样式用 placeholderTextColor 和可选的 placeholderStyle
    • 原生 TextInput 不传 placeholderstyle 设为 padding: 0, margin: 0 并继承字号、颜色等,这样其高度不会因「有无 placeholder」或「首字」而变。
  4. 若项目已有 TextInputShim(如 字体/屏幕缩放),可在其内合并本逻辑,不必新建 Shim。

1. 安装依赖

本方案依赖 Babel 的 module-resolver 插件做别名解析,需先安装(若已为其他 Shim 配过可跳过):

yarn add -D babel-plugin-module-resolver

2. Babel 配置

babel.config.jsmodule-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(含 lineHeightheightminHeight 等),并加 justifyContent: 'center',使输入内容与 placeholder 垂直居中,高度由容器决定,不会随首字输入而变。
  • placeholder:从 props 中解出,不传给 RN.TextInput;用绝对定位的 RN.Text 在无 value 时显示,避免原生 placeholder 参与高度计算。
  • placeholderStyle:可选,便于单独设置 placeholder 的字体、行高。若需 TypeScript 感知,可在 react-native.d.ts 中扩展 TextInputProps
  • TextInputplaceholder=""padding: 0, margin: 0,仅保留颜色、字号等必要样式,避免额外占位导致高度跳动。

若项目已在 TextInputShim 中做了 字体缩放吞字 的 style 处理,可在上述基础上继续对 flatStylefontFamilyByWeightfontSizeToFitmaxFontSizeMultiplier 等处理后再赋给容器和 input。

小结

做法作用
容器 View + 业务 style用 lineHeight/height/minHeight 固定输入区域高度,不随内容切换变化
placeholder 用绝对定位 Text不把 placeholder 交给原生 TextInput,避免「placeholder → 首字」时系统重算高度
TextInput padding/margin 0、placeholder=""输入框不额外占位,高度完全由容器和 lineHeight 决定

业务侧继续 import { TextInput } from 'react-native',无需改写法,即可无侵入地解决「lineHeight + placeholder 导致输入首字时高度变动」的问题。

上次更新: