React-Native WebView动态加载字体

背景

使用react-native构建的iOS/Android双端APP,通过WebView加载本地页面,需要根据服务器提供的字体列表实现下载和动态加载。

本地字体检查

有些字体手机操作系统已经提供了,可以不需要下载和加载。

iOS

UIFont.familyNames提供了所有系统自带字体的familyNames,直接将结果返回给RN处理即可。

SHMFontsModul.h

//
//  SHMFontsModule.h
//  shimo
//
//  Created by Rex Rao on 2018/1/9.
//  Copyright © 2018年 shimo.im. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>

@interface SHMFontsModule : NSObject <RCTBridgeModule>

@end

SHMFontsModule.m

//
//  SHMFontsModule.m
//  shimo
//
//  Created by Rex Rao on 2018/1/9.
//  Copyright © 2018年 shimo.im. All rights reserved.
//

#import "SHMFontsModule.h"
#import <UIKit/UIKit.h>

@implementation SHMFontsModule

RCT_EXPORT_MODULE(SHMFonts);

RCT_REMAP_METHOD(fontFamilyNames,
                 resolver
                 : (RCTPromiseResolveBlock)resolve
                     rejecter
                 : (RCTPromiseRejectBlock)reject) {
    resolve(UIFont.familyNames);
}

@end

Android

安卓系统没有直接提供接口返回系统字体列表,经过调研和阅读源代码,发现有一个类中的私有静态变量存储了字体信息,反射即可得到。但因为Android版本原因,低版本系统代码不同无法通过此方法得到。继续对这个静态变量顺藤摸瓜,发现Android通过解析字体xml文件来设置此变量的值,根据系统不同,字体配置xml文件的位置和结构也有所不同。

  • Android 5.1及以下
    • 路径:/system/etc/system_fonts.xml
    • 结构样例请直接查看源文件
  • Android 5.1以上
    • 路径:/system/etc/fonts.xml
    • 结构样例请直接查看源文件

Android源码中有个FontListParser类用来解析此字体配置文件,我们可以参考此类完成自己的parser,分两种配置路径和结构获取系统的Font Families,然后传给RN处理。

FontListParser.java

package chuxin.shimo.shimowendang.fonts;

import android.util.Xml;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by sohobloo on 2018/1/17.
 */

/**
 * Parser for font config files.
 *
 */
public class FontListParser {

    public static List<String> parse(InputStream in) throws XmlPullParserException, IOException {
        List<String> familyNames = new ArrayList<String>();
        try {
            XmlPullParser parser = Xml.newPullParser();
            parser.setInput(in, null);
            parser.nextTag();
            parser.require(XmlPullParser.START_TAG, null, "familyset");
            while (parser.next() != XmlPullParser.END_TAG) {
                if (parser.getEventType() != XmlPullParser.START_TAG) {
                    continue;
                }
                String tag = parser.getName();
                switch (tag) {
                    case "family": {
                        String name = parser.getAttributeValue(null, "name");
                        if (name != null && !name.isEmpty()) {
                            familyNames.add(name);
                            skip(parser);
                        } else {
                            while (parser.next() != XmlPullParser.END_TAG) {
                                if (parser.getEventType() != XmlPullParser.START_TAG) {
                                    continue;
                                }
                                tag = parser.getName();
                                if (tag.equals("nameset")) {
                                    while (parser.next() != XmlPullParser.END_TAG) {
                                        if (parser.getEventType() != XmlPullParser.START_TAG) {
                                            continue;
                                        }
                                        tag = parser.getName();
                                        if (tag.equals("name")) {
                                            name = parser.nextText();
                                            if (name != null && !name.isEmpty()) {
                                                familyNames.add(name);
                                            }
                                        } else {
                                            skip(parser);
                                        }
                                    }
                                } else {
                                    skip(parser);
                                }
                            }
                        }
                        break;
                    }
                    case "alias": {
                        String name = parser.getAttributeValue(null, "name");
                        if (name != null && !name.isEmpty()) {
                            familyNames.add(name);
                        }
                        skip(parser);
                        break;
                    }
                    default:
                        skip(parser);
                        break;
                }
            }
        } finally {
            in.close();
        }

        return familyNames;
    }

    private static void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
        int depth = 1;
        while (depth > 0) {
            switch (parser.next()) {
                case XmlPullParser.START_TAG:
                    depth++;
                    break;
                case XmlPullParser.END_TAG:
                    depth--;
                    break;
                default:
                    break;
            }
        }
    }
}

FontsModule.java

package chuxin.shimo.shimowendang.fonts;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableArray;

import org.xmlpull.v1.XmlPullParserException;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.List;

/**
 * Created by sohobloo on 2018/1/9.
 */

public class FontsModule extends ReactContextBaseJavaModule {
    private static final String MODULE_NAME = "SHMFonts";

    private static final String SYSTEM_CONFIG_LOCATION = "/system/etc/";
    private static final String FONTS_CONFIG = "fonts.xml";
    private static final String SYSTEM_FONTS_CONFIG = "system_fonts.xml";

    FontsModule(ReactApplicationContext reactContext) {
        super(reactContext);
    }

    @Override
    public String getName() {
        return MODULE_NAME;
    }

    @ReactMethod
    public void fontFamilyNames(Promise promise) {
        WritableArray familyNames = null;
        File systemFontConfigLocation = new File(SYSTEM_CONFIG_LOCATION);
        File configFilename = new File(systemFontConfigLocation, FONTS_CONFIG);
        if (!configFilename.exists()) {
            configFilename = new File(systemFontConfigLocation, SYSTEM_FONTS_CONFIG);
        }
        if (configFilename.exists()) {
            try {
                FileInputStream fontsIn = new FileInputStream(configFilename);
                List<String> familyNameList = FontListParser.parse(fontsIn);
                familyNames = Arguments.fromList(familyNameList);
            } catch (XmlPullParserException | IOException e) {
                e.printStackTrace();
            }
        }

        promise.resolve(familyNames);

    }
}

FontsPackage.java

package chuxin.shimo.shimowendang.fonts;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Created by sohobloo on 2018/1/9.
 */

public class FontsPackage implements ReactPackage {
    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();
        modules.add(new FontsModule(reactContext));
        return modules;
    }

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }
}

RN

RN端通过对比项目需要加载的字体和调用原生iOS/Android模块获取到的系统字体列表交叉对比即可知道哪些字体系统以及存在了。对比时注意一下FamilyName的大小写/空格以及「-」连接符。

下载字体

下载不是本topic的主题,就不细讲了。下载到App目录中即可,下载前判断一下是否已经下载云云。
由于iOS的WKWebView没有读取Documents目录权限导致真机无法加载字体资源,根据调研需要拷贝字体文件到tmp/www/fonts目录中。参考

WebView动态加载字体

字体可以通过CSS的font-face来加载,这里就简单了,通过insertRule传入本地字体的familyName和path即可动态加载

function loadFontFace (name, path) {
  const sheet = document.styleSheets[0]
  sheet.insertRule(`@font-face {font-family: '${name}'; src:url('${path}');}`, sheet.cssRules.length || 0)
}

通过调用此js函数注入webview即可实现动态加载字体,无需刷新更无需重启APP。:-p

博客园的MarkDown没有预览功能吗?难道大神们写文章都这么牛X了,排版了然于心?

原文地址:https://www.cnblogs.com/sohobloo/p/8549346.html