如何无侵入式地应对系统放大字体与屏幕缩放
测试或用户在系统里开启「大号字体」「显示大小」或「屏幕缩放」后,React Native 的 Text、TextInput 会跟随系统缩放,导致布局错位、文字溢出、甚至整屏错乱。
若希望在不改业务代码的前提下,限制这种「无良测试 / 极端用户设置」带来的放大效果,可采用 无侵入增强 React Native 组件方案 中的 Babel 别名 + react-native-proxy + Shim 方案:对 Text、TextInput 返回 TextShim / TextInputShim,在 Shim 里统一设置 maxFontSizeMultiplier(并可按屏宽做 fontSize 适配),从而把系统字体缩放和极端屏宽控制在可接受范围内。
思路
- 用 Babel 的
module-resolver把react-native指到项目内的react-native-proxy.js(若已为其他 Shim 配过则复用)。 - proxy 对
Text、TextInput返回 TextShim、TextInputShim,其余组件透传。 - 在 Shim 内对真实 RN 的
Text/TextInput做两件事:- 限制系统缩放:设置
maxFontSizeMultiplier(例如按屏宽返回 1、1.1、1.2),系统「大号字体」再大,也不会超过该倍数,避免布局被撑爆。 - 小屏适配(可选):对小屏设备按屏宽对
fontSize做一次缩放(如fontSizeToFit),保证小屏下也能大致按比例显示。
- 限制系统缩放:设置
- 若项目已按 无侵入增强 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.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 中为 Text / TextInput 返回 Shim
在 proxy 里为 Text、TextInput 返回 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 只需统一加上 maxFontSizeMultiplier 和 fontSizeToFit(以及若做吞字则同样用 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 后就不会触发吞字),无需去掉该逻辑;只做缩放限制时也可只保留 maxFontSizeMultiplier 和 fontSizeToFit。
小结
| 手段 | 作用 |
|---|---|
| maxFontSizeMultiplier | 限制系统「大号字体」对 Text/TextInput 的放大倍数,应对无良测试放大字体 |
| fontSizeToFit | 小屏下按屏宽缩小字号,应对小屏或放大屏幕导致的布局问题 |
| font-helpers | 统一提供按屏宽的倍数与字号计算,供 TextShim / TextInputShim 复用 |
业务侧无需改任何 import 或组件用法,由 proxy 统一替换为 TextShim / TextInputShim,即可无侵入式地应对系统放大字体、放大屏幕的测试行为。