Skip to content

Add dynamic fonts loading (and downloading) #2736

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions demo/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ module.exports = {
return require('./screens/componentScreens/WizardScreen').default;
},
// nativeComponentScreens
get DynamicFontsScreen() {
return require('./screens/nativeComponentScreens/DynamicFontsScreen').default;
},
get HighlightOverlayViewScreen() {
return require('./screens/nativeComponentScreens/HighlightOverlayViewScreen').default;
},
Expand Down
5 changes: 5 additions & 0 deletions demo/src/screens/MenuStructure.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ export const navigationData = {
tags: 'KeyboardAwareScrollView',
screen: 'unicorn.components.KeyboardAwareScrollViewScreen'
},
{
title: 'Dynamic Fonts',
tags: 'dynamic fonts load download',
screen: 'unicorn.nativeComponents.DynamicFontsScreen'
},
{
title: 'Highlight Overlay',
tags: 'native overlay',
Expand Down
120 changes: 120 additions & 0 deletions demo/src/screens/nativeComponentScreens/DynamicFontsScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import React, {Component, Fragment} from 'react';
import {ScrollView} from 'react-native';
import {View, Text, Button, DynamicFonts} from 'react-native-ui-lib';
import {renderMultipleSegmentOptions} from '../ExampleScreenPresenter';

enum FontLoadingEnum {
SINGLE_FONT = 'singleFont',
FONT_FAMILY = 'fontFamily'
}

type State = {
fontLoadingType: FontLoadingEnum;
loadedFonts: string[];
};

export default class DynamicFontsScreen extends Component<{}, State> {
private fontDownloader: InstanceType<typeof DynamicFonts> = new DynamicFonts({debug: true});

state = {
fontLoadingType: FontLoadingEnum.SINGLE_FONT,
loadedFonts: []
};

renderSingleFont = () => {
const {loadedFonts} = this.state;
return (
<Fragment>
<Text style={{fontSize: 24, lineHeight: 28, fontFamily: 'System'}}>{`
System:
ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz
`}</Text>
{loadedFonts.length > 0 && (
<Text marginT-20 style={{fontSize: 24, lineHeight: 28, fontFamily: loadedFonts[0]}}>{`
${loadedFonts}:
ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz
`}</Text>
)}
<Button
marginV-20
label="Load a single font"
onPress={async () => {
try {
const loadedFonts = await this.fontDownloader.getFont({
fontUri:
'https://wixmp-1d257fba8470f1b562a0f5f2.wixmp.com/TypographyKits/fonts/Vollkorn/Vollkorn-Regular.ttf',
fontName: 'Vollkorn-Regular',
fontExtension: 'ttf'
});
this.setState({loadedFonts: [loadedFonts]});
} catch (error) {
console.log('Error!', error);
}
}}
/>
</Fragment>
);
};

renderFontFamily = () => {
const {loadedFonts} = this.state;
return (
<Fragment>
<Text style={{fontSize: 16, lineHeight: 18, fontFamily: 'System'}}>{`
System:
ABCDEFGH abcdefgh
`}</Text>
<ScrollView>
{loadedFonts?.map(loadedFont => (
<Text key={loadedFont} style={{fontSize: 16, lineHeight: 18, fontFamily: loadedFont}}>{`
${loadedFont}:
ABCDEFGH abcdefgh
`}</Text>
))}
</ScrollView>

<Button
marginV-20
label="Load a complete font family"
onPress={async () => {
try {
const loadedFonts = await this.fontDownloader.getFontFamily('https://wixmp-1d257fba8470f1b562a0f5f2.wixmp.com/TypographyKits/fonts/Vollkorn/',
[
'Bold',
'BoldItalic',
'ExtraBold',
'ExtraBoldItalic',
'Italic',
'Medium',
'MediumItalic',
'Regular',
'SemiBold'
],
'ttf',
'Vollkorn-');
this.setState({loadedFonts});
} catch (error) {
console.log('Error!', error);
}
}}
/>
</Fragment>
);
};

render() {
const {fontLoadingType, loadedFonts} = this.state;
return (
<View bg-grey80 flex padding-20>
{renderMultipleSegmentOptions.call(this, 'Font loading:', 'fontLoadingType', [
{label: 'Single', value: FontLoadingEnum.SINGLE_FONT},
{label: 'Family', value: FontLoadingEnum.FONT_FAMILY}
])}
<View flex center key={`${loadedFonts}`}>
{fontLoadingType === FontLoadingEnum.SINGLE_FONT ? this.renderSingleFont() : this.renderFontFamily()}
{loadedFonts && <Text text80>Loaded fonts: {loadedFonts.map(loadedFont => `${loadedFont} `)}</Text>}
</View>
</View>
);
}
}
1 change: 1 addition & 0 deletions demo/src/screens/nativeComponentScreens/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export function registerScreens(registrar) {
registrar('unicorn.nativeComponents.DynamicFontsScreen', () => require('./DynamicFontsScreen').default);
registrar('unicorn.nativeComponents.HighlightOverlayViewScreen', () => require('./HighlightOverlayViewScreen').default);
registrar('unicorn.nativeComponents.SafeAreaSpacerViewScreen', () => require('./SafeAreaSpacerViewScreen').default);
registrar('unicorn.nativeComponents.KeyboardTrackingViewScreen', () => require('./KeyboardTrackingViewScreen').default);
Expand Down
11 changes: 11 additions & 0 deletions jestSetup/jest-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,17 @@ jest.mock('react-native', () => {
return reactNative;
});

jest.mock('react-native-fs',
() => {
return {
exists: jest.fn(() => true),
readFile: jest.fn(),
downloadFile: jest.fn(),
mkdir: jest.fn()
};
},
{virtual: true});

Animated.timing = (value, config) => ({
start: callback => {
value.setValue(config.toValue);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import android.app.Application;
import com.facebook.react.ReactPackage;

import com.wix.reactnativeuilib.dynamicfont.DynamicFontPackage;
import com.wix.reactnativeuilib.highlighterview.HighlighterViewPackage;
import com.wix.reactnativeuilib.keyboardinput.KeyboardInputPackage;
import com.wix.reactnativeuilib.textinput.TextInputDelKeyHandlerPackage;
Expand All @@ -20,6 +21,7 @@ public UiLibPackageList(Application application) {

public List<ReactPackage> getPackageList() {
return Arrays.asList(
new DynamicFontPackage(),
new HighlighterViewPackage(),
new TextInputDelKeyHandlerPackage(),
new KeyboardInputPackage(application)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package com.wix.reactnativeuilib.dynamicfont;

import android.app.Activity;
import android.graphics.Typeface;
import android.util.Base64;

import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.views.text.ReactFontManager;

import java.io.File;
import java.io.FileOutputStream;

public class DynamicFontModule extends ReactContextBaseJavaModule {
int tempNameCounter = 0;

public DynamicFontModule(ReactApplicationContext reactContext) {
super(reactContext);
}

@Override
public String getName() {
return "DynamicFont";
}

// TODO: Needs to be tested
// @ReactMethod
// public void loadFontFromFile(final ReadableMap options, final Callback callback) {
// Activity currentActivity = getCurrentActivity();
// if (currentActivity == null) {
// callback.invoke("Invalid activity");
// return;
// }

// String filePath = options.hasKey("filePath") ? options.getString("filePath") : null,
// name = (options.hasKey("name")) ? options.getString("name") : null;

// if (filePath == null || filePath.length() == 0) {
// callback.invoke("filePath property empty");
// return;
// }

// File f = new File(filePath);

// if (f.exists() && f.canRead()) {
// boolean wasLoaded = false;
// try {
// Typeface typeface = Typeface.createFromFile(f);
// //Cache the font for react
// TODO: probably needs to be Typeface.NORMAL and not typeface.getStyle()
// ReactFontManager.getInstance().setTypeface(name, typeface.getStyle(), typeface);
// wasLoaded = true;
// } catch (Throwable e) {
// callback.invoke(e.getMessage());
// } finally {
// if (wasLoaded) {
// callback.invoke(null, name);
// }
// }
// } else {
// callback.invoke("invalid file");
// }
// }

@ReactMethod
public void loadFont(final ReadableMap options, final Callback callback) throws Exception {
Activity currentActivity = getCurrentActivity();
if (currentActivity == null) {
callback.invoke("Invalid activity");
return;
}

String name = (options.hasKey("name")) ? options.getString("name") : null,
data = (options.hasKey("data")) ? options.getString("data") : null,
type = null;

if (name == null || name.length() == 0) {
callback.invoke("Name property empty");
return;
}

if (data == null || data.length() == 0) {
callback.invoke("Data property empty");
return;
}

if (("data:").equalsIgnoreCase(data.substring(0, 5))) {
Integer pos = data.indexOf(',');
if (pos > 0) {
String[] encodingParams = data.substring(5, pos).split(";");
String mimeType = encodingParams[0];

data = data.substring(pos + 1);

if (("application/x-font-ttf").equalsIgnoreCase(mimeType) ||
("application/x-font-truetype").equalsIgnoreCase(mimeType) ||
("font/ttf").equalsIgnoreCase(mimeType)) {
type = "ttf";
} else if (("application/x-font-opentype").equalsIgnoreCase(mimeType) ||
("font/opentype").equalsIgnoreCase(mimeType)) {
type = "otf";
}
}
}

if (options.hasKey("type"))
type = options.getString("type");

if (type == null)
type = "ttf";

try {
byte[] decodedBytes = Base64.decode(data, Base64.DEFAULT);
File cacheFile = new File(currentActivity.getCacheDir(), "tempFont" + (tempNameCounter++) + type);

FileOutputStream stream = new FileOutputStream(cacheFile);
try {
stream.write(decodedBytes);
} finally {
stream.close();
}

//Load the font from the temporary file we just created
Typeface typeface = Typeface.createFromFile(cacheFile);

//Cache the font for react
ReactFontManager.getInstance().setTypeface(name, Typeface.NORMAL, typeface);

cacheFile.delete();
} catch(Exception e) {
callback.invoke(e.getMessage());
} finally {
callback.invoke(null, name);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.wix.reactnativeuilib.dynamicfont;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

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

public class DynamicFontPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(new DynamicFontModule(reactContext));
}

// Deprecated from RN 0.47
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}

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