Пользователям Flutter не понаслышке знаком такой проект как Skia. Он является движком для рендеринга всего что мы видим на экране Flutter. С помощью него можно рисовать сложные элементы интерфейса и любые 2D сцены с поддержкой плавной анимации и различных эффектов. Так почему бы не взять это на вооружение, подумали ребята из Shopify и выпустили React Native Skia - библиотеку позволяющую использовать Skia в экосистеме React Native.
Для того чтобы посмотреть на что способна Skia предлагаю использовать example из репозитория библиотеки.
Графика
Начнем с базовой графики.
import React from 'react';
import { Group, Rect, RoundedRect, DiffRect, Canvas, rrect } from '@shopify/react-native-skia';
import { StatusBar, useWindowDimensions } from 'react-native';
const PADDING = 16;
export const Declarative = () => {
const { width } = useWindowDimensions();
const SIZE = width / 4;
const style = useMemo(() => ({ width, height: SIZE + 32 }), [SIZE, width]);
const outer = useMemo(
() => rrect(rect(2 * SIZE + 3 * 16, PADDING, SIZE, SIZE), 25, 25),
[SIZE]
);
const inner = useMemo(
() =>
rrect(
rect(2 * SIZE + 4 * PADDING, 2 * PADDING, SIZE - 32, SIZE - 32),
0,
0
),
[SIZE]
);
return (
<>
<Canvas style={style}>
<Group color="#61DAFB">
<Rect rect={{ x: PADDING, y: PADDING, width: 100, height: 100 }} />
<RoundedRect
x={SIZE + 2 * PADDING}
y={PADDING}
width={SIZE}
height={SIZE}
r={25}
/>
<DiffRect outer={outer} inner={inner} />
</Group>
</Canvas>
</>
);
};
Canvas - это корневой элемент для рисования с помощью Skia. К Canvas применяются стили как и к компоненту View с помощью свойства style. Помимо этого с помощью Canvas обрабатываются touch события передав функцию в свойство onTouch.
const MyComponent = () => {
const cx = useValue(100);
const cy = useValue(100);
const touchHandler = useTouchHandler({
onActive: ({ x, y }) => {
cx.current = x;
cy.current = y;
},
});
return (
<Canvas onTouch={touchHandler}>
<Circle cx={cx} cy={cy} r={10} color="red" />
</Canvas>
);
};
Компоненты React, RoundedRect, Circle, DiffRect, Line, Point и прочие фигуры используются для рисования фигур. Каждый компонент имеет свои специфичные свойства и свойства общие для любых компонентов которые мы рисуем с помощью Skia такие, как color, blendMode, style и т.д. Очень полезным может быть использование компонента Group который позволяет применять общие свойства всем дочерним компонентам.
export const PaintDemo = () => {
const r = 128;
return (
<Canvas style={{ flex: 1 }}>
<Circle cx={r} cy={r} r={r} color="#51AFED" />
<Group color="lightblue" style="stroke" strokeWidth={10}>
<Circle cx={r} cy={r} r={r / 2} />
<Circle cx={r} cy={r} r={r / 3} color="white" />
</Group>
</Canvas>
);
};
Продолжая тему рисования - ко всем фигурам мы можем применять различные маски, эффекты, фильтры и трансформации. Например BlurMask
const MaskFilterDemo = () => {
return (
<Canvas style={{ flex: 1}}>
<Circle c={vec(128)} r={128} color="lightblue">
<BlurMask blur={20} style="normal" />
</Circle>
</Canvas>
);
};
const SimpleTransform = () => {
return (
<Canvas style={{ flex: 1 }}>
<Fill color="#e8f4f8" />
<Group
color="lightblue"
origin={{ x: 128, y: 128 }}
transform={[{ skewX: Math.PI / 6 }]}
>
<RoundedRect x={64} y={64} width={128} height={128} r={10} />
</Group>
</Canvas>
);
};
Помимо рисования Skia поддерживает работы с изображениями и SVG. Изображения используются как отдельный компонент или могут быть вписаны в другие фигуры.
const Clip = () => {
const image = useImage(require("./assets/oslo.jpg"));
const star = Skia.Path.MakeFromSVGString(
"M 128 0 L 168 80 L 256 93 L 192 155 L 207 244 L 128 202 L 49 244 L 64 155 L 0 93 L 88 80 L 128 0 Z"
)!;
if (!image) {
return null;
}
return (
<Canvas style={{ flex: 1 }}>
<Group clip={star}>
<Image
image={image}
x={0}
y={0}
width={256}
height={256}
fit="cover"
/>
</Group>
</Canvas>
);
};
Отдельно стоит упомянуть поддержку шейдорв. В Skia реализован свой язык похожий на GLSL. Пример использования простейшего шейдера:
import {Skia, Canvas, Shader, Fill} from "@shopify/react-native-skia";
const source = Skia.RuntimeEffect.Make(`
vec4 main(vec2 pos) {
// normalized x,y values go from 0 to 1, the canvas is 256x256
vec2 normalized = pos/vec2(256);
return vec4(normalized.x, normalized.y, 0.5, 1);
}`)!;
const SimpleShader = () => {
return (
<Canvas style={{ width: 256, height: 256 }}>
<Fill>
<Shader source={source} />
</Fill>
</Canvas>
);
};
Анимация
Поддержка анимаций строиться на концепции Skia Values. С помощью Value мы храним состояние, которое может быть ассоциировано с объектом на Canvas и при изменении этого состояние объект будет перерисован. В качестве значения могут быть использованы строки, числа, объекты и массивы.
const MyComponent = () => {
const position = useValue(0);
const updateValue = useCallback(
() => (position.current = position.current + 10),
[position]
);
return (
<>
<Canvas style={{ flex: 1 }}>
<Rect x={position} y={100} width={10} height={10} color={"red"} />
</Canvas>
<Button title="Move it" onPress={updateValue} />
</>
);
};
С помощью хука useComputedValue можно рассчитать новое значения основываясь на другие values, а с помощью useValueEffect реагировать на изменения values. Для работы со сложными объектами или массивами нужно использовать функцию Selector которая принимаем на вход value и возвращает значение которое используется в конкретном свойстве объекта на Canvas.
const Heights = new Array(10).fill(0).map((_, i) => i * 0.1);
export const Demo = () => {
const loop = useLoop();
const heights = useComputedValue(
() => Heights.map((_, i) => loop.current * i * 10),
[loop]
);
return (
<Canvas style={{ flex: 1, marginTop: 50 }}>
{Heights.map((_, i) => (
<Rect
key={i}
x={i * 20}
y={0}
width={16}
height={Selector(heights, (v) => v[i])}
color="red"
/>
))}
</Canvas>
);
};
Для облегчения работы с values фреймворк поставляется с набором хуков таких как useTiming, useLoop, useSpring и функций interpolate, interpolatePaths, interpolateColors, runDecay для построение анимаций. Пример использования хуков
export const AnimationExample = () => {
const [toggled, setToggled] = useState(false);
const position = useSpring(toggled ? 100 : 0);
return (
<>
<Canvas style={{ flex: 1 }}>
<Rect x={position} y={100} width={10} height={10} color={"red"} />
</Canvas>
<Button title="Toggle" onPress={() => setToggled((p) => !p)} />
</>
);
};
Skia API
Кроме работы в декларативном стиле библиотека так же предлагает доступ к API Skia напрямую используя новые возможности React Native по синхронной коммуникации с нативным кодом (JSI). Это API практически на 100% совместимо с Flutter API. Пример использования.
import {Skia, SkiaView, useDrawCallback} from "@shopify/react-native-skia";
export const HelloWorld = () => {
const r = 128;
const onDraw = useDrawCallback((canvas) => {
const paint = Skia.Paint();
paint.setAntiAlias(true);
cyan.setColor(Skia.Color("cyan"));
canvas.drawCircle(r, r, r, paint);
});
return (
<SkiaView style={{ flex: 1 }} onDraw={onDraw} />
);
};
Выводы
Как видно из примеров в начале статьи фреймворк позволяет использовать много возможностей Skia для проектирования сложных интерфейсов. Поддержка JSI гарантирует минимальный оверхед при работе с нативным движком, это позволит добиться высокой производительности. Из плюсов отмечу поддержку от Shopify, проект скорее всего не будет заброшен и будет активно развиваться.
Из минусов неполная поддержка API Skia и статус Alpha версии библиотеки, API меняется от версии к версии, а частые релизы приносят баги и ломают обратную совместимость.
Подробнее о возможностях React Native Skia читайте в документации https://shopify.github.io/react-native-skia/ а примеры использования на канале одного из разработчиков William Candillon
Также подписывайтесь на мой Телеграм-канал React Native World — там я размещаю последние новости, обзоры и статьи из мира React Native.