Skip to content
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

Improve Android fonts linking process #52

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
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
12 changes: 4 additions & 8 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,15 @@
"devDependencies": true,
"optionalDependencies": true
}
]
],
"no-bitwise": "off"
},
"overrides": [
{
"files": [
"*.test.js",
"*.spec.js",
"*.test.jsx",
"*.spec.jsx"
],
"files": ["*.test.js", "*.spec.js", "*.test.jsx", "*.spec.jsx"],
"env": {
"jest": true
}
}
]
}
}
38 changes: 36 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,43 @@ Instead this library writes `link-assets-manifest.json` to the root of `android`
## Parameters
* `-p, --path` - path to project, defaults to cwd.
* `-a, --assets` - assets paths, for example `react-native-asset -a ./src/font ./src/mp3`.
* `-ios-a, --ios-assets` - ios assets paths, will disable android linking
* `-ios-a, --ios-assets` - ios assets paths, will disable android linking.
* `-android-a, --android-assets` - android assets paths, will disable ios linking.
* `-n-u, --no-unlink` - Not to unlink assets which not longer exists, not recommanded.
* `-n-u, --no-unlink` - won't unlink assets which no longer exists, not recommended.

## Font assets linking and usage

### Android

Font assets are linked in Android by using [XML resources](https://developer.android.com/develop/ui/views/text-and-emoji/fonts-in-xml). For instance, if you add the **Lato** font to your project, it will generate a `lato.xml` file in `android/app/src/main/res/font/` folder with all the font variants that you added. It will also add a method call in `MainApplication.java` file in order to register the custom font during the app initialization. It will look something like this:

```java
public class MainApplication extends Application implements ReactApplication {

// other methods...

@Override
public void onCreate() {
super.onCreate();
ReactFontManager.getInstance().addCustomFont(this, "Lato", R.font.lato); // <- registers the custom font.
// ...
}
}
```

In this case, `Lato` is what you have to set in the `fontFamily` style of your `Text` component. To select the font variant e.g. weight and style, use `fontWeight` and `fontStyle` styles respectively.

```jsx
<Text style={{ fontFamily: 'Lato', fontWeight: '700', fontStyle: 'italic' }}>Lato Bold Italic</Text>
```

### iOS

Font assets are linked in iOS by editing `project.pbxproj` and `Info.plist` files. To use the font in your app, you can a combination of `fontFamily`, `fontWeight` and `fontStyle` styles in the same way you would use for Android. In case you didn't link your font assets in Android and you are not sure which value you have to set in `fontFamily` style, you can use `Font Book` app in your Mac to find out the correct value by looking the `Family Name` property.

## Migrating from 2.x

If you have already linked font assets in your Android project, when running this tool it will relink your fonts to use XML resources for them. **This migration will allow you to use your fonts in the code the same way you would use it for iOS**. Please update your code to use `fontFamily`, `fontWeight` and `fontStyle` styles correctly.

## Backward compatability
* to use react-native 0.59 and below, use version 1.1.4
227 changes: 227 additions & 0 deletions lib/android-font-assets-helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
const path = require('path');
const slugify = require('slugify');
const { sync: globSync } = require('glob');
const { XMLParser, XMLBuilder } = require('fast-xml-parser');
const log = require('npmlog');

const REACT_FONT_MANAGER_JAVA_IMPORT = 'com.facebook.react.views.text.ReactFontManager';

function toArrayBuffer(buffer) {
const arrayBuffer = new ArrayBuffer(buffer.length);
const view = new Uint8Array(arrayBuffer);

for (let i = 0; i < buffer.length; i += 1) {
view[i] = buffer[i];
}

return arrayBuffer;
}

function normalizeString(str) {
return slugify(str, { lower: true }).replaceAll('-', '_');
}

function getProjectFilePath(rootPath, name) {
const filePath = globSync(path.join(rootPath, `app/src/main/java/**/${name}.java`))[0];
return filePath;
}

function getFontFamily(fontFamily, preferredFontFamily) {
const availableFontFamily = preferredFontFamily || fontFamily;
return availableFontFamily.en || Object.values(availableFontFamily)[0];
}

/**
* Calculate a fallback weight to ensure it is multiple of 100 and between 100 and 900.
*
* Reference: https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#fallback_weights
*
* @param weight the font's weight.
* @returns a fallback weight multiple of 100, between 100 and 900, inclusive.
*/
// eslint-disable-next-line consistent-return
function getFontFallbackWeight(weight) {
if (weight <= 500) {
return Math.max(Math.floor(weight / 100) * 100, 100);
} else if (weight > 500) {
return Math.min(Math.ceil(weight / 100) * 100, 900);
}
}

function getFontResFolderPath(rootPath) {
return path.join(rootPath, 'app/src/main/res/font');
}

function getXMLFontId(fontFileName) {
return `@font/${path.basename(fontFileName, path.extname(fontFileName))}`;
}

function buildXMLFontObjectEntry(fontFile) {
return {
'@_app:fontStyle': fontFile.isItalic ? 'italic' : 'normal',
'@_app:fontWeight': fontFile.weight,
'@_app:font': getXMLFontId(fontFile.name),
};
}

function buildXMLFontObject(fontFiles) {
const fonts = [];
fontFiles.forEach((fontFile) => {
const xmlEntry = buildXMLFontObjectEntry(fontFile);

// We can't have style / weight duplicates.
const foundEntryIndex = fonts.findIndex(font =>
font['@_app:fontStyle'] === xmlEntry['@_app:fontStyle'] &&
font['@_app:fontWeight'] === xmlEntry['@_app:fontWeight']);

if (foundEntryIndex === -1) {
fonts.push(xmlEntry);
} else {
fonts[foundEntryIndex] = xmlEntry;
}
});

return {
'?xml': {
'@_version': '1.0',
'@_encoding': 'utf-8',
},
'font-family': {
'@_xmlns:app': 'http://schemas.android.com/apk/res-auto',
font: fonts,
},
};
}

function getAddCustomFontMethodCall(fontName, fontId) {
return `ReactFontManager.getInstance().addCustomFont(this, "${fontName}", R.font.${fontId});`;
}

function addImportToJavaFile(javaFileData, importToAdd) {
const importRegex = new RegExp(`import\\s+${importToAdd};`, 'gm');
const existingImport = importRegex.exec(javaFileData);

if (existingImport) {
return javaFileData;
}

const packageRegex = /package\s+[\w.]+;/;
const packageMatch = packageRegex.exec(javaFileData);

let insertPosition = 0;

if (packageMatch) {
insertPosition = packageMatch.index + packageMatch[0].length;
}

return `${javaFileData.slice(0, insertPosition)}\n\nimport ${importToAdd};${javaFileData.slice(insertPosition)}`;
}

function insertLineInJavaClassMethod(
javaFileData,
targetClass,
targetMethod,
codeToInsert,
lineToInsertAfter,
) {
const classRegex = new RegExp(`class\\s+${targetClass}(\\s+extends\\s+\\S+)?(\\s+implements\\s+\\S+)?\\s*\\{`, 'gm');
const classMatch = classRegex.exec(javaFileData);

if (!classMatch) {
log.error(null, `Class ${targetClass} not found.`);
return javaFileData;
}

const methodRegex = new RegExp(`(public|protected|private)\\s+(static\\s+)?\\S+\\s+${targetMethod}\\s*\\(`, 'gm');
let methodMatch = methodRegex.exec(javaFileData);

while (methodMatch) {
if (methodMatch.index > classMatch.index) {
break;
}
methodMatch = methodRegex.exec(javaFileData);
}

if (!methodMatch) {
log.error(null, `Method ${targetMethod} not found in class ${targetClass}.`);
return javaFileData;
}

const openingBraceIndex = javaFileData.indexOf('{', methodMatch.index);
let closingBraceIndex = -1;
let braceCount = 1;

for (let i = openingBraceIndex + 1; i < javaFileData.length; i += 1) {
if (javaFileData[i] === '{') {
braceCount += 1;
} else if (javaFileData[i] === '}') {
braceCount -= 1;
}

if (braceCount === 0) {
closingBraceIndex = i;
break;
}
}

if (closingBraceIndex === -1) {
log.error(null, `Could not find closing brace for method ${targetMethod} in class ${targetClass}.`);
return javaFileData;
}

const methodBody = javaFileData.slice(openingBraceIndex + 1, closingBraceIndex);

if (methodBody.includes(codeToInsert.trim())) {
return javaFileData;
}

let insertPosition = closingBraceIndex;

if (lineToInsertAfter) {
const lineIndex = methodBody.indexOf(lineToInsertAfter.trim());
if (lineIndex !== -1) {
insertPosition = openingBraceIndex + 1 + lineIndex + lineToInsertAfter.trim().length;
} else {
log.error(null, `Line "${lineToInsertAfter}" not found in method ${targetMethod} of class ${targetClass}.`);
return javaFileData;
}
}

return `${javaFileData.slice(0, insertPosition)}\n ${codeToInsert}${javaFileData.slice(insertPosition)}`;
}

function removeLineFromJavaFile(javaFileData, stringToRemove) {
const lines = javaFileData.split('\n');
const updatedLines = lines.filter(line => !line.includes(stringToRemove));
return updatedLines.join('\n');
}

const xmlParser = new XMLParser({
ignoreAttributes: false,
isArray: tagName => tagName === 'font',
});

const xmlBuilder = new XMLBuilder({
format: true,
ignoreAttributes: false,
suppressEmptyNode: true,
});

module.exports = {
REACT_FONT_MANAGER_JAVA_IMPORT,
toArrayBuffer,
normalizeString,
getProjectFilePath,
getFontFamily,
getFontFallbackWeight,
getFontResFolderPath,
getXMLFontId,
buildXMLFontObjectEntry,
buildXMLFontObject,
getAddCustomFontMethodCall,
addImportToJavaFile,
insertLineInJavaClassMethod,
removeLineFromJavaFile,
xmlParser,
xmlBuilder,
};
Loading