Skip to content

[2021-06-28]: H5移动端字体大小缩放的React相关 #20

@Lemonreds

Description

@Lemonreds

截屏2021-06-28下午7.25.55.png

需求

在一个h5的移动端项目中,需要支持设置多种字体大小,实现方案是通过webview来实现的,见文章:移动端字体放大问题的研究。简单来说,iOS中,通过设置body标签style的-webkit-text-size-adjust 值来实现,例如设置为137.5%,则在safari内核中的浏览器展示页面,所有DOM的font-size都会被放大37.5%;而在android中,则是通过android-webview的setTextZoom这个api来实现的。

问题

前端如何获取到当前页面设置的字体大小类型?

方式1: 通过原生app

最简单的方式,就是由app的同学告诉我们,当前字体的缩放类型,例如,可以放置在每一个页面的url中,添加一个查询参数,该参数的值是约定的字体类型值,例如:

/home?fs=[sm,nm,md,lg]

其中sm表示小号,nm表示正常,md表示略大,lg表示超大。前端同学就可以通过解析url,来获取当前的字体类型。

又或者可以通过下发事件的方式,当页面被初始化的时候,触发事件,将具体的字体大小类型传给我们,如在前端页面中,我们可以这样监听app下发的事件

document.addEventListener('fontsizeInit',handle)

其中fontsizeInit与app同学约定好的事件名称,handle函数则是具体的取参逻辑等。

方式2: 前端自行判断

在一些第三方的webview中,无法要求他们在告知前端页面的当前字体大小类型,就可以通过前端自行来判断,本质的原理是根据android与iOS实现不同,具体来获取,也是较灵活的一种方式。

思路是,可以提取这部分逻辑成一个公用hook,然后在全局文件中(例如baseLayout.js全局layout中)使用,获取到当前的字体类型,然后注入给需要的组件。

如baseLayout.js:

import React, { useLayoutEffect, useMemo } from 'react';
import useFontSize from './useFontSize';

import styles from './BaseLayout.less';

const GlobalContext = React.createContext({});

const BaseLayout = (props) => {
  const { dispatch, children } = props;
  const [scale, sizeType] = useFontSize();

  const value = useMemo(
    () => ({
      // 字体放大
      sizeType, // 字体类型
      scale, // 放大比例
    }),
    [scale, sizeType]
  );

  return (
  
      <GlobalContext.Provider
        // 全局配置context
        value={value}
      >
        <div className={styles.root}>{children}</div>
      </GlobalContext.Provider>

  );
};

export default BaseLayout;

而具体的useFontSize的代码判断逻辑:

import { useState, useEffect } from 'react';
import { isAndroid, isIOS } from '@/utils';


const Sized = { // 字体类型枚举
  i: 'i', // 初始状态
  sm: 'sm', // 小号
  nm: 'nm',// 默认
  md: 'md',// 大号
  lg: 'lg',// 超大号
};

const SizedIOS = { // ios的webkitTextSizeAdjust值
  87.5: Sized.sm, // 87.5%
  100: Sized.nm, // 100%
  118.75: Sized.md, // 118.75%
  137.5: Sized.lg, // 137.5%
};

const SizedAndroid = { // android的字体缩放比例
  0.875: Sized.sm, // 0.875
  1: Sized.nm, // 1
  1.1875: Sized.md, // 1.1875
  1.375: Sized.lg, // 1.375
};

const useFontSize = () => {
  const [sizeType, setSizeType] = useState(Sized.i); // 字体类型
  const [scale, setScale] = useState(1); // 放大的比例

  useEffect(() => {
    if (isIOS) {
      const callback = () => {
        let webkitTextSizeAdjust = Number(
          document.body.getAttribute('style')?.match(/\d+\.+\d+/gi)?.[0]
        );
        if (
          Number.isNaN(webkitTextSizeAdjust) ||
          webkitTextSizeAdjust / 100 === scale
        ) {
          // no changed
          return;
        }

        webkitTextSizeAdjust = webkitTextSizeAdjust || 100; // set default Value
        window.console.log('webkitTextSizeAdjust', webkitTextSizeAdjust);
        setSizeType(SizedIOS[webkitTextSizeAdjust] || Sized.nm);
        setScale(webkitTextSizeAdjust / 100, 1); // ios 需要除百分比
      };
      const observer = new window.MutationObserver((mutations) => {
        mutations.forEach(() => {
          callback();
        });
      });
      observer.observe(document.body, {
        attributes: true,
        attributeFilter: ['style'],
      });
      callback();
    }

    if (isAndroid) {
      const div = document.createElement('div');
      div.style = 'font-size:16px;';
      document.body.appendChild(div);
      const scaledFontSize = Math.round(
        window
          .getComputedStyle(div, null)
          .getPropertyValue('font-size')
          .replace('px', '')
      );

      document.body.removeChild(div);
      const scaleRate = scaledFontSize / 16;
      const _sizeType = SizedAndroid[scaleRate];
      window.console.log('scaleRate:', scaleRate, _sizeType);
      setSizeType(_sizeType);
      setScale(scaleRate);
    }
  }, []);

  return [scale, sizeType];
};

export default useFontSize;

export { Sized };

代码里面,ios是通过body标签的-webkit-text-size-adjust属性来获取当前缩放比例的,由于app在切换字体的时候,不会重新渲染页面,也就不会再执行这段获取逻辑,所以需要通过 MutationObserver 来监听body标签的style属性变化,确保同步。
android则是新建了一个test dom,设置基准字体为16px,插入到页面后,通过getComputedStyle获取实际展示的字体小大,然后相除,获取所放的比例。

至此,字体大小类型、缩放比例,都存储到globalContext中,以便其他组件进行使用。

应用1:DOM的排版样式适配

设置字体大小后,页面的排版难免会发生错乱,理想的情况下,是可以一套样式,适配多种字体,降低维护成本。但是有些情况,仍然需要写多套样式来做排版样式适配。

所以,我们可以编写一个这样的组件,可以根据当前的字体类型,自动匹配styles中的classname,在不同字体大小classname下写样式适配代码,如:

import styles from './test.less';

<FontSizeAdapter styles={styles}>
     <div classsName={styles.label}>xxxx</div>
</FontSizeAdapter>

test.less内容如下:

.sm,
.nm {  // sm 小号字体,nm 正常字体
  .label {
      .line(2); // 最多展示2行
  }
}
.md,
.lg { // md 大号字体, lg 超大号字体
  .label {
      .ellipsis(); // 单行省略
  }
}

FontSizeAdapter的实现方式就是消费globalcontext中字体类型,然后给容器添加一个样式名。

import React, { useContext } from 'react';
import classnames from 'classnames';
import GlobalContext from '@/layouts/GlobalContext';
import { Sized } from '@/layouts/useFontSize';

const defaultStyles = {
  [Sized.i]: 'fs-init',
  [Sized.sm]: 'fs-small',
  [Sized.nm]: 'fs-normal',
  [Sized.md]: 'fs-medium',
  [Sized.lg]: 'fs-large',
};

const FontSizeAdapter = (props) => {
  const { styles = defaultStyles, children } = props;
  const globalContext = useContext(GlobalContext);

  return (
    <div className={classnames('fs-adapter', styles[globalContext.sizeType])}>
      {children}
    </div>
  );
};

export default FontSizeAdapter;

应用2:文案保留原有的大小

部分场景下,有些文案不需要随字体大小类型变化,保留原有的font-size,如前面的文章说的,我们可以缩放回去,来实现,就像该组件。

用法:

<DontScaleFontSize>
	我是不需要放大的文案
</DontScaleFontSize>

DontScaleFontSize实现如下,也是一个globalContext的字体缩放比例,以及字体大虾类型的一个消费者:

import React, { useContext, useRef, useState, useEffect } from 'react';
import GlobalContext from '@/layouts/GlobalContext';


const DontScaleFontSize = (props) => {
  const { children } = props;
  const ref = useRef();
  const { scale } = useContext(GlobalContext); 
  const [fs, setFs] = useState(undefined); // font-size
  const [lh, setLH] = useState(undefined); // line-height

  useEffect(() => {
    if (typeof scale === 'undefined') {
      throw new Error('scale not injected in');
    }

    if (scale === 1) {
      return;
    }

    const scaledFontSize = Math.round(
      window
        .getComputedStyle(ref.current, null)
        .getPropertyValue('font-size')
        .replace('px', '')
    );
    const scaledLineHeight = Math.round(
      window
        .getComputedStyle(ref.current, null)
        .getPropertyValue('line-height')
        .replace('px', '')
    );

    /**
     * scaledFontSize / scale,是原字体的大小,放入到页面中,
     * 依然会放大,所以需要再除去放大比例
     */
    const originFS = Math.round(scaledFontSize / scale / scale);
    const originLH = Math.round(scaledLineHeight / scale / scale);
    
    setFs(originFS);
    setLH(originLH);
  }, [scale]);

  const wrapStyle = fs ? { fontSize: `${fs}px`, lineHeight: `${lh}px` } : null;

  return (
    <span ref={ref} style={wrapStyle}>
      {children}
    </span>
  );
};

export default DontScaleFontSize;

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions