Creating Custom Fonts with React-Native-Skia
Introduction
When working with custom designs in React Native, creating custom fonts with specific styles can be challenging. In this article, we will explore how to implement a custom font using @shopify/react-native-skia
library.
Problem
The design requirements for our custom font are as follows:
Let’s talk a bit — what we need to implement:
- The font should have a 1px stroke.
- The font should have a bottom shadow with a width of 4px and a Y translation of 4px.
React-Native-Skia
Skia is a cross-platform 2D graphics library that provides a set of drawing primitives that run on iOS, Android, macOS, Windows, Linux, and the browser. React-native-skia brings this library to react native.
Installation:
- Install package:
yarn add @shopify-react-native-skia
2. iOS pod install:
npx pod-install ios
If you have any issues with package installation please check Official Docs
Implementation
We will divide the font rendering into three main components:
- Outer Stroke
- Inner Font
- Outer Shadow
Preparing Skia for the Base Font
We’ll use the Anek-Latin font as our base font. (here is guide how to install custom fonts)
First, define the base font style and create the Skia font object:
import { Skia } from "@shopify/react-native-skia";
export const fontStyle = {
textStyle: {
color: Skia.Color('white'),
fontFamilies: ["Anek-Bold"],
fontSize: 36,
lineHeight: 24,
letterSpacing: -0.8,
},
paragraphStyle: {
foregroundPrimaryStyle: 1,
foregroundSecondaryStyle: 0,
foregroundPrimaryColor: Skia.Color('black'),
foregroundSecondaryColor: Skia.Color('white'),
foregroundStrokeWidth: 4,
},
shadow: {
dx: 0,
dy: 4,
},
};
Note: very important to use
Skia.Color
for color definition.
Create font manager using “useFont()”
export const CustomFont = () => {
const customFontMgr = useFonts({
["Anek-Bold"]: [require("../../assets/fonts/AnekLatin-Bold.ttf")],
});
...
}
Let’s Rock! Outer stroke.
Create paint object and paragraph primitive using Skia.Paint()
and Skia.ParagraphBuilder.Make()
...
const outerStroke = useMemo(() => {
if (!customFontMgr) {
return null;
}
const foregroundPaint = Skia.Paint();
foregroundPaint.setStyle(fontStyle.paragraphStyle.foregroundPrimaryStyle);
foregroundPaint.setColor(fontStyle.paragraphStyle.foregroundPrimaryColor);
foregroundPaint.setStrokeWidth(
fontStyle.paragraphStyle.foregroundStrokeWidth,
);
return Skia.ParagraphBuilder.Make({
textAlign: TextAlign.Center
}, customFontMgr)
.pushStyle({ ...fontStyle.textStyle, ...style }, foregroundPaint)
.addText(text)
.pop()
.build();
}, [customFontMgr, text]);
...
Let’s speak a bit what we see here:
Skia.Paint()
— createsPaint
primitive.- Now we can assign any properties for foregroundPaint
Skia.ParagraphBuilderMake()
createsParagraph
primitive.
Let’s render something on the screen:
....
return (
<Canvas style={{ width, height }}>
<Paragraph paragraph={outerStroke} x={0} y={0} width={width} />
</Canvas>
);
Inner Font
Now, let’s implement the body of the font:
...
const innerFont = useMemo(() => {
if (!customFontMgr) {
return null;
}
const innerFontStyle = {
...fontStyle.textStyle,
...style,
color: Skia.Color(color ?? "white"),
};
return Skia.ParagraphBuilder.Make(
{ textAlign: TextAlign.Center },
customFontMgr,
)
.pushStyle(innerFontStyle)
.addText(text)
.pop()
.build();
}, [customFontMgr, text]);
...
return (
<Canvas style={{ width, height }}>
<Paragraph paragraph={primaryParagraph} x={0} y={0} width={width} />
<Paragraph paragraph={secondaryParagraph} x={0} y={0} width={width} />
</Canvas>
);
Outer Shadow
Finally, let’s implement the outer shadow using the Group
, Paint
, and Shadow
components from @shopify/react-native-skia
:
import { Group, Paint, Shadow } from '@shopify/react-native-skia';
...
// Component
return (
<Canvas
style={{
width: width,
height: height,
}}
>
<Group
layer={
<Paint>
<Shadow
blur={0}
dx={fontStyle.shadow.dx}
dy={fontStyle.shadow.dy}
color={colors.black}
/>
</Paint>
}
>
<Paragraph paragraph={outerStroke} x={0} y={0} width={width} />
<Paragraph paragraph={innerFont} x={0} y={0} width={width} />
</Group>
</Canvas>
);
<h3>WHAT’S IN THE BOOOOOOOOX ?!!!</h3>
Dynamic Canvas Size
Maybe you saw that <Canvas />
component contains { width: width, height: height}
parameters.
We want our canvas to adjust its size dynamically based on the content. React-Native-Skia provides the measureText
helper:
const font = useFont(require('../assets/fonts/AnekLatin-Bold.ttf'), textStyle.fontSize);
const { width, height } = font?.measureText(text);
Let’s create some hook:
// useFontMeasurements.ts
import { DataSourceParam, useFont } from "@shopify/react-native-skia";
import { useEffect, useState } from "react";
type Props = {
text: string;
size: number;
fontSource: DataSourceParam;
};
export const useFontMeasurements = ({ text, size, fontSource }: Props) => {
const [textParams, setTextParams] = useState<{
height: number;
width: number;
}>({ height: 0, width: 0 });
const font = useFont(fontSource, size);
useEffect(() => {
if (!fontSource || !text || !size || !font) return;
const { width, height, y } = font.measureText(text);
setTextParams({ width, height: height - y / 2 });
}, [font, size, fontSource, text]);
return {
height: textParams?.height,
width: textParams?.width,
};
};
Now we can use it in our <CustomFont />
component:
...
const { height, width } = useFontMeasurements({
text: text,
fontSource: require("../../assets/fonts/AnekLatin-Bold.ttf"),
size: style?.fontSize ?? fontStyle.textStyle.fontSize,
});
...
You can find source code here:
https://github.com/gitstashapply/medium-custom-fonts
Please let me know if you found this article useful and if you’re interested in more articles like this.
Cheers!