Пользователям 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.
