React Native字体问题解决方案指北

源码已上传 Github:  react-native-app-font

“怎么又是字体,老常的话题现在还拿出来说。关于字体适配的解决方式网上一搜几十篇!”。看到标题的烙铁心里一万个xxx疾驰飞腾。But! 我总是会给大家带点什么惊喜。关于 pxToDp、启动缩放 我们一点不说。本篇博客的主题很简单:如何控制App字体不随系统字体改变?

系统字体改变 一般有两种情况:

(1)调整系统字体缩放

(2)修改系统字体样式(方正体、彩云等等)

Text 组件字体缩放

手机设置中我们可以调整字体的大小来控制手机字体的显示。一般情况下很多App并没有考虑系统字体大小改变所带来的UI影响。所以,当用户调整系统字体大小后,App当前的UI布局因为尺寸未适配的原因导致布局错乱。如何快速解决这个问题呢?其实官方在字体缩放为我们提供了解决办法:allowFontScaling。只需要将Text组件的该属性设置为false。当修改系统字体大小时,App中的Text大小就不会随之改变。此时,我们可以在index入口文件,为Text组件添加全局属性配置,统一将Text组件allowFontScaling设置为false。

import { Text } from 'react-native';
const TextRender = Text.render;
Text.render = function (...args) {
    const originText = TextRender.apply(this, args);
    const { style } = originText.props;
    return React.cloneElement(originText, {
        allowFontScaling: false,
    });
};

TextInput 组件字体缩放

OK,我们兴奋的打开手机测试一番。突然发现在登录输入时,输入框中的字体仍然还是随着系统字体发生了改变。TextInput中我们可以通过style设置字体大小,那么是否也可以使用allowFontScaling控制呢?继续修改,为TextInput组件全局添加allowFontScaling为false的设置。运行发现 iOS设备一切正常,Android设备没有起到任何效果。所以,为TextInput设置allowFontScaling不能解决Android终端问题。如何解决Android端的问题呢?其实我们可以利用 PixelRatio 。

官方对 PixelRatio 的解释如下:

PixelRatio class gives access to the device pixel density.

翻译:

PixelRatio 类提供了访问设备的像素密度的方法。

在 PixelRatio 中提供了四种Api:

其中 getFontScale 可以获取当前字体大小的缩放比例。官方的解释如下:

Returns the scaling factor for font sizes. This is the ratio that is used to calculate the absolute font size, so any elements that heavily depend on that should use this to do calculations.

If a font scale is not set, this returns the device pixel ratio.

Currently this is only implemented on Android and reflects the user preference set in Settings > Display > Font size, on iOS it will always return the default pixel ratio. @platform android

返回字体大小缩放比例。这个比例可以用于计算绝对的字体大小,所以很多深度依赖字体大小的组件需要用此函数的结果进行计算。

如果没有设置字体缩放大小,它会直接返回设备的像素密度。

目前这个函数仅仅在 Android 设备上实现了,它会体现用户选项里的“设置 > 显示 > 字体大小”。在 iOS 设备上它会直接返回默认的像素密度。

可以看到,该Api目前只支持Android平台的使用。在iOS端会出现一些问题。 所以我们可以使用getFontScale来解决Android端TextInput字体缩放问题:

字体大小 = 当前字体大小值 / PixelRatio.getFontScale()

⚠️注意:此解决方法,Android平台请勿在TextInput中将allowTextScaling设置为false。

所以整体的解决方案如下:

封装一个工具function:

/**
 * TextInput 组件字体适配
 */
import { PixelRatio, Platform } from 'react-native';

export default function (fontSize) {
    return Platform.OS === 'android' ? fontSize / PixelRatio.getFontScale() : fontSize;
}

使用方式如下:

textInputFont: {
     fontSize: TextInputFontResponsive(18),
},

入口文件中配置TextInput全局属性allowTextScaling设置为false。

import { TextInput, Platform } from 'react-native';
const TextInputRender = TextInput.render;
TextInput.render = function (...args) {
    const originText = TextRender.apply(this, args);
    const { style } = originText.props;
    return React.cloneElement(originText, {
        allowFontScaling: Platform.OS === 'android', // 只在iOS平台设置为false
    });
};

字体样式

国内市场很多手机都可以通过root的形式来设置系统字体样式。有些手机厂商(小米、华为等等)还提供了不需要root就可以直接设置系统字体样式功能。同样当我们设置了手机系统的字体样式后,App中的字体也会改变。随之而来的可能是UI布局错乱,界面不能统一。

如下图所示:

解决这种问题,我们需要控制App中的字体样式不随系统样式改变即可。如何解决呢?设置自定义字体登场。

关于如何在React Native自定义字体我就不多赘述了。大家可以参考:Custom Font In React Native

我们仍然可以采用设置全局属性来设置自定义字体,改变系统字体样式将不会影响App中的字体样式。

const TextRender = Text.render;
Text.render = function (...args) {
    const originText = TextRender.apply(this, args);
    const { style } = originText.props;
    return React.cloneElement(originText, {
        allowFontScaling: false,
        style: [{
            ...PlatformStyle({ fontFamily: 'PingFangRegular' }),
        }, style],
    });
};



/**
 * 根据平台设置对应样式
 */
import { Platform } from 'react-native';
export default function platformStyle(androidStyle = {}, iosStyle = {}) {
    if (Platform.OS === 'android') {
        return androidStyle;
    }
    return iosStyle;
}

因为在iOS平台不会有字体样式的问题,所以我们采用PlatformStyle区分设置。继续运行App,卧槽!!怎么有些字体又使用了系统字体。

如下图:

其实在 React Native 中当我们为Text组件设置了 fontStyle 或者 fontWeight 属性后,就会出现该问题。卧槽,这就尴尬了,难道让我违背设计稿,粗体或者斜体字都不设置了?这肯定不行,所以,我们只能通过设置相应字体实现。为了统一,为们自定义Text组件:

import React, { Component } from 'react';
import { Text, StyleSheet, Platform } from 'react-native';
export default class TextWidget extends Component {

    renderAndroidText() {

        let { style, children } = this.props;
        let fontStyle = null;
        if (style) {
            if (style instanceof Array) {
                style = StyleSheet.flatten(style);
            }
            fontStyle = style.fontWeight ? {
                fontWeight: 'normal',
                fontFamily: 'PingFangBold',
            } : { fontFamily: 'PingFangRegular' };
        }

        return (
            <Text
                {...this.props}
                style={[ style, fontStyle ]}
            >
                {children}
            </Text>
        );
    }

    render() {
        return Platform.OS === 'ios' ? <Text {...this.props} /> : this.renderAndroidText();
    }
}

上面组件中,我们通过判断是否又设置fontWeight属性,来选择对应的自定义字体。其实上面这样做还是不够完善,如果我们确实想用某种fontWeight 或者 fontStyle,该如何做呢?

可以根据如下规则进行设置:

fontWeight

300: "Light",      400: "Regular",       700: "Bold",      900: "Black",      normal: "Regular",

fontStyle: 

bold: "Bold"      italic: "Italic"

实现大致分为如下:

(1)通过检测Text设置的style属性中的fontWeight 和 fontStyle 得到对应的字体名称

(2)对 fontWeight 和 fontStyle属性进行过滤,确保不含有两个属性的设置,否则将无效

Text 属性检测

// getFontFamily.js

// 包含所有字体
const fonts = {
  SonglcyFont: {
    fontWeights: {
      300: "Light",
      400: "Regular",
      700: "Bold",
      900: "Black",
      normal: "Regular",
      bold: "Bold"
    },
    fontStyles: {
      normal: "",
      italic: "Italic"
    }
  },
};

const getFontFamily = (baseFontFamily, styles = {}) => {
  const { fontWeight, fontStyle } = styles;
  const font = fonts[baseFontFamily];
  const weight = fontWeight
    ? font.fontWeights[fontWeight]
    : font.fontWeights.normal;
  const style = fontStyle
    ? font.fontStyles[fontStyle]
    : font.fontStyles.normal;

  if (style === font.fontStyles.italic && weight === font.fontWeights.normal) {
    return `${baseFontFamily}-${style}`;
  }

  return `${baseFontFamily}-${weight}${style}`;
};

export default getFontFamily;

上面我们定义了一个 getFontFamily 文件,在文件中,首先我们 fonts 常量,在 fonts 中 声明项目中用到的所有字体配置。每个字体下又包含 fontWeight 所对应的字体,例如,300 即对应了SonglcyFont字体下的Light形式的字体。以此类推。在 getFontFamily 方法中,baseFontFamily 为字体名,styles即Text组件的style。接着在该方法中,通过字体名拿到fonts对应的字体属性,然后判断Text组件的style中是否包含fontWeight,如果包含则取出对应的字体名,不包含则采用Regular字体样式。fontStyle与此相同。最后返回对应字体名称。例如:SonglcyFont-Light

最终自定义Text组件如下:

import React from "react";
import { Text, StyleSheet } from "react-native";
import getFontFamily from "./getFontFamily";

// 过滤 fontWeight fontStyle 属性, 生成新的 style 对象
const omit = (obj, keys) => {
  return Object.keys(obj)
    .reduce((result, key) => {
      if (!keys.includes(key)) {
        result[key] = obj[key];
      }

      return result;
    }, {});
};

const AppText = ({style, ...props}) => {
  // Text style
  const resolvedStyle = StyleSheet.flatten(style);
  // 通过对 Text style 的检测,拿到对应自定义字体
  const fontFamily = getFontFamily(resolvedStyle.fontFamily, resolvedStyle);
  // 过滤掉 Text style 中的 fontWeight fontStyle 得到新的 style 对象
  const newStyle = omit({...resolvedStyle, fontFamily},["fontStyle", "fontWeight"]);

  return (
    <Text
      {...props}
      style={newStyle}
    />
  );
};

export default AppText;

上面自定义Text组件中,我们做了两件事:

(1)通过对 Text style 的检测,拿到对应自定义字体
(2)过滤掉 Text style 中的 fontWeight fontStyle 得到新的 style 对象

终于可以愉快的玩耍啦~ 

总结

本篇对 React Native 中的字体问题做了总结性的方案概述。从最初的简单问题抛出,一步步实现最终的解决方案。希望对你有所帮助。

发布了214 篇原创文章 · 获赞 371 · 访问量 92万+

猜你喜欢

转载自blog.csdn.net/u013718120/article/details/90112623