如何无侵入式地应对系统放大字体与屏幕缩放

测试或用户在系统里开启「大号字体」「显示大小」或「屏幕缩放」后,React Native 的 TextTextInput 会跟随系统缩放,导致布局错位、文字溢出、甚至整屏错乱。

若希望在不改业务代码的前提下,限制这种「无良测试 / 极端用户设置」带来的放大效果,可采用 无侵入增强 React Native 组件方案 中的 Babel 别名 + react-native-proxy + Shim 方案:对 TextTextInput 返回 TextShim / TextInputShim,在 Shim 里统一设置 maxFontSizeMultiplier(并可按屏宽做 fontSize 适配),从而把系统字体缩放和极端屏宽控制在可接受范围内。

思路

  1. 用 Babel 的 module-resolverreact-native 指到项目内的 react-native-proxy.js(若已为其他 Shim 配过则复用)。
  2. proxy 对 TextTextInput 返回 TextShimTextInputShim,其余组件透传。
  3. 在 Shim 内对真实 RN 的 Text / TextInput 做两件事:
    • 限制系统缩放:设置 maxFontSizeMultiplier(例如按屏宽返回 1、1.1、1.2),系统「大号字体」再大,也不会超过该倍数,避免布局被撑爆。
    • 小屏适配(可选):对小屏设备按屏宽对 fontSize 做一次缩放(如 fontSizeToFit),保证小屏下也能大致按比例显示。
  4. 若项目已按 无侵入增强 React Native 组件方案 配置了 TextShim 并做过 fontFamily 注入,可在同一批 Shim 里同时做「吞字修复」和「缩放上限」,逻辑放在公共的 font-helpers 里复用。

这样业务侧继续 import { Text, TextInput } from 'react-native',无需改写法,即可无侵入地应对放大字体、放大屏幕的测试行为。

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 中为 Text / TextInput 返回 Shim

在 proxy 里为 TextTextInput 返回 Shim:

// app/utils/react-native-proxy.js
const RN = require('../../node_modules/react-native');

module.exports = new Proxy(RN, {
  get(target, prop) {
    if (prop === 'Text') {
      if (!module.exports.__TextShim) {
        module.exports.__TextShim = require('./shim/TextShim').default;
      }
      return module.exports.__TextShim;
    }
    if (prop === 'TextInput') {
      if (!module.exports.__TextInputShim) {
        module.exports.__TextInputShim = require('./shim/TextInputShim').default;
      }
      return module.exports.__TextInputShim;
    }
    // View、Image、Pressable 等若已有 Shim 可在此一并代理
    return target[prop];
  },
});

4. 公共逻辑:font-helpers

把「按屏宽限制最大缩放倍数」和「按屏宽适配字号」抽成公共方法,供 TextShim / TextInputShim 共用。若在 fontFamilyByWeight 中使用自定义字体(如下示例中的 LexendDeca 系列),需先按 如何在 React Native 中使用自定义字体 完成字体文件的添加与配置。

  • maxFontSizeMultiplierByScreenWidth():根据当前窗口短边宽度返回 maxFontSizeMultiplier(如 1、1.1、1.2)。系统字体缩放时,RN 会将设计稿字号乘以一个系数,但不会超过该值,从而避免无良测试把字体放到巨大。
  • fontSizeToFit(fontSize):小屏(例如宽度 < 360)时按比例缩小字号,避免小屏上文字过大溢出;大屏直接返回原字号。
// app/utils/shim/font-helpers.ts
import { Dimensions, TextStyle } from 'react-native';

export function maxFontSizeMultiplierByScreenWidth(): number {
  const window = Dimensions.get('window');
  const width = Math.min(window.width, window.height);
  if (width >= 400) return 1.2;
  if (width >= 375) return 1.1;
  return 1;
}

export function fontSizeToFit(fontSize: number): number {
  const window = Dimensions.get('window');
  const width = Math.min(window.width, window.height);
  if (width >= 360) return fontSize;
  const fontScale = width / 360;
  return fontSize * fontScale;
}

// 注入 fontFamily 可避免 Android 吞字。若无自定义字体,直接返回空字符串 '' 即可
// 若有自定义字体则按 fontWeight 映射到对应字重。
export function fontFamilyByWeight(weight: TextStyle['fontWeight']): string {
  // 无自定义字体时:return '';
  switch (weight) {
    case '100': return 'LexendDeca-Thin';
    case '200': return 'LexendDeca-ExtraLight';
    case '300': return 'LexendDeca-Light';
    case '400':
    case 'normal': return 'LexendDeca-Regular';
    case '500': return 'LexendDeca-Medium';
    case '600': return 'LexendDeca-SemiBold';
    case '700':
    case 'bold': return 'LexendDeca-Bold';
    case '800': return 'LexendDeca-ExtraBold';
    case '900': return 'LexendDeca-Black';
    default: return 'LexendDeca-Regular';
  }
}

阈值(400、375、360)可按设计稿和产品需求调整。

5. TextShim:限制缩放与小屏字号

在 TextShim 中:对 style 做 flatten,用 fontFamilyByWeight 注入/替换 fontFamily(解决吞字时可保留),用 fontSizeToFit 得到适配后的字号;传给 RN.Text 时设置 maxFontSizeMultiplier,从而限制系统放大。

// app/utils/shim/TextShim.tsx
import React from 'react';
import type { TextProps } from 'react-native';
import {
  fontFamilyByWeight,
  fontSizeToFit,
  maxFontSizeMultiplierByScreenWidth,
} from './font-helpers';

const RN = require('../../node_modules/react-native');

const TextShim = (props: TextProps) => {
  const { style, numberOfLines, ...rest } = props;
  const flatStyle = RN.StyleSheet.flatten(style) || {};
  const styleCopy = { ...flatStyle };
  styleCopy.fontFamily = fontFamilyByWeight(styleCopy.fontWeight);
  delete styleCopy.fontWeight;

  return (
    <RN.Text
      {...rest}
      numberOfLines={numberOfLines}
      maxFontSizeMultiplier={maxFontSizeMultiplierByScreenWidth()}
      style={{
        ...styleCopy,
        fontSize: fontSizeToFit((styleCopy.fontSize as number) || 14),
      }}
    />
  );
};

export default TextShim;
  • maxFontSizeMultiplier:限制系统「大号字体」带来的放大倍数,应对无良测试放大字体的行为。
  • fontSizeToFit:小屏下按屏宽缩小字号,应对小屏或「放大屏幕」后逻辑宽度变小的场景。

6. TextInputShim:同样限制缩放与小屏字号

TextInput 只需统一加上 maxFontSizeMultiplierfontSizeToFit(以及若做吞字则同样用 fontFamilyByWeight):

// app/utils/shim/TextInputShim.tsx
import React from 'react';
import type { TextInputProps } from 'react-native';
import {
  fontFamilyByWeight,
  fontSizeToFit,
  maxFontSizeMultiplierByScreenWidth,
} from './font-helpers';

const RN = require('../../node_modules/react-native');

const TextInputShim = (props: TextInputProps) => {
  const { style, ...rest } = props;
  const flatStyle = RN.StyleSheet.flatten(style) || {};
  const styleCopy = { ...flatStyle };
  styleCopy.fontFamily = fontFamilyByWeight(styleCopy.fontWeight);
  delete styleCopy.fontWeight;

  return (
    <RN.TextInput
      {...rest}
      maxFontSizeMultiplier={maxFontSizeMultiplierByScreenWidth()}
      style={{
        ...styleCopy,
        fontSize: fontSizeToFit((styleCopy.fontSize as number) || 14),
      }}
    />
  );
};

export default TextInputShim;

若没有自定义字体,fontFamilyByWeight 可直接返回空字符串 '',即可避免 Android 吞字(因 style 带上 fontFamily 后就不会触发吞字),无需去掉该逻辑;只做缩放限制时也可只保留 maxFontSizeMultiplierfontSizeToFit

小结

手段作用
maxFontSizeMultiplier限制系统「大号字体」对 Text/TextInput 的放大倍数,应对无良测试放大字体
fontSizeToFit小屏下按屏宽缩小字号,应对小屏或放大屏幕导致的布局问题
font-helpers统一提供按屏宽的倍数与字号计算,供 TextShim / TextInputShim 复用

业务侧无需改任何 import 或组件用法,由 proxy 统一替换为 TextShim / TextInputShim,即可无侵入式地应对系统放大字体、放大屏幕的测试行为。

上次更新: