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

本文由 AI 协助更新

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

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

这里要注意:只限制字号通常不够。当 fontSizemaxFontSizeMultiplier 限住后,数字型 lineHeight 仍可能继续按系统字体缩放参与计算,最终出现「字没有继续变大,但行高和上下留白继续变大」的问题。因此还需要对 lineHeight 做反向换算,让最终渲染出来的行高回到接近设计值的范围。

思路

  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),保证小屏下也能大致按比例显示。
    • 行高适配(可选):对数字型 lineHeight 做一次反向换算(如 lineHeightToFit),避免字号已被限制后,行高仍按系统字体缩放被放得过高。
  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:

下面示例中的 path/to/node_modules/react-native 是占位写法,使用时请替换为项目中指向真实 react-native 包的相对路径或绝对路径。

// app/utils/react-native-proxy.js
const RN = require('path/to/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)时按比例缩小字号,避免小屏上文字过大溢出;大屏直接返回原字号。
  • lineHeightToFit(fontSize, lineHeight):当业务传入数字型 lineHeight 时,结合 PixelRatio.getFontScale()、最大字体缩放倍数和小屏字号适配结果,先计算目标行高,再除回系统字体缩放系数,避免最终渲染出来的行高过高。
// app/utils/shim/font-helpers.ts
import { Dimensions, PixelRatio } from 'react-native';
import type { 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;
}

export function lineHeightToFit(fontSize: number, lineHeight: unknown) {
  if (typeof lineHeight !== 'number') {
    return lineHeight;
  }

  const maxFontSizeMultiplier = maxFontSizeMultiplierByScreenWidth();
  const lineHeightMultiplier = Math.max(1, PixelRatio.getFontScale());
  const fontMultiplier = Math.max(
    1,
    Math.min(lineHeightMultiplier, maxFontSizeMultiplier),
  );
  const fittedFontSize = fontSizeToFit(fontSize);
  const fittedLineHeight = fontSizeToFit(lineHeight);
  const minScaledLineHeight = fittedFontSize * fontMultiplier * 1.2;
  const targetLineHeight = Math.max(fittedLineHeight, minScaledLineHeight);

  return Math.ceil(targetLineHeight / lineHeightMultiplier);
}

// 注入 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)可按设计稿和产品需求调整。

这里的 lineHeightToFit 只处理数字型 lineHeight,遇到非数字值会原样返回。PixelRatio.getFontScale() 反映当前系统字体缩放;由于 maxFontSizeMultiplier 只负责限制字号,行高需要按「实际允许放大的字号」算出目标值,再除回 RN 对 lineHeight 的系统缩放系数。这样最终渲染时,行高不会因为系统字体缩放过大而继续膨胀,同时也保留了 minScaledLineHeight 作为下限,避免极端情况下行高小于字号需要的空间。

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

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

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

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

const TextShim = (props: TextProps) => {
  const { style, ...rest } = props;
  const flatStyle = RN.StyleSheet.flatten(style) || {};
  const styleCopy = { ...flatStyle };
  const fontSize = (styleCopy.fontSize as number) || 14;
  styleCopy.fontFamily = fontFamilyByWeight(styleCopy.fontWeight);
  styleCopy.lineHeight = lineHeightToFit(fontSize, styleCopy.lineHeight);
  delete styleCopy.fontWeight;

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

export default TextShim;
  • maxFontSizeMultiplier:限制系统「大号字体」带来的放大倍数,应对无良测试放大字体的行为。
  • fontSizeToFit:小屏下按屏宽缩小字号,应对小屏或「放大屏幕」后逻辑宽度变小的场景。
  • lineHeightToFit:在保留设计稿行高比例的同时,抵消系统字体缩放对行高的额外放大,避免上下留白过大。

adjustsFontSizeToFit 不建议在 Shim 里统一设置。它的语义是「当文本放不下时自动缩小字号」,适合具体业务场景按需开启;若在全局 TextShim 中根据 numberOfLines 强制开启,会让所有单行文本都有可能被自动缩小,反而改变业务侧原本的排版预期。业务已显式传入的 adjustsFontSizeToFit 会通过 {...rest} 继续透传。

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

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

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

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

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

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

export default TextInputShim;

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

小结

手段作用
maxFontSizeMultiplier限制系统「大号字体」对 Text/TextInput 的放大倍数,应对无良测试放大字体
fontSizeToFit小屏下按屏宽缩小字号,应对小屏或放大屏幕导致的布局问题
lineHeightToFit按系统字体缩放和字号上限反向换算行高,避免字号被限制后行高仍被放得过高
font-helpers统一提供按屏宽的倍数与字号计算,供 TextShim / TextInputShim 复用

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

上次更新: