Пользователям Flutter не понаслышке знаком такой проект как Skia. Он является движком для рендеринга всего что мы видим на экране Flutter. С помощью него можно рисовать сложные элементы интерфейса и любые 2D сцены с поддержкой плавной анимации и различных эффектов. Так почему бы не взять это на вооружение, подумали ребята из Shopify и выпустили React Native Skia - библиотеку позволяющую использовать Skia в экосистеме React Native.

Для того чтобы посмотреть на что способна Skia предлагаю использовать example из репозитория библиотеки.

Примеры интерфейсов созданных с помощью React Native Skia
Примеры интерфейсов созданных с помощью React Native Skia

Графика

Начнем с базовой графики.

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

Комментарии (7)


  1. ARyabchikov
    31.08.2022 13:24

    Наличие собственно 2D движка для рендеринга было одним из ключевых плюсов Flutter по сравнению с RN. Интересно, нужно смотреть во что разовьется эта история


    1. kirill3333 Автор
      31.08.2022 13:54

      еще забавно что есть совместимость с API и можно брать реализации на Dart для Flutter и легко адаптировать под себя


  1. nikita_dol
    31.08.2022 21:41

    Иронично, что Skia основа Flutter, но в либе для RN можно использовать её полный потенциал, а во Flutter нет

    Например, рисование отдельных глифов не реализовано во Flutter и поэтому все используют ParagraphBuilder который оптимизирован (но это не точно) для большого текста, но ужасно дорог для маленького текста, которого большенство????‍♂️


    1. avdosev
      02.09.2022 13:49

      Можно вот так сделать, но будет только один отдельный символ рисоваться


      Icon(
        IconData('Л'.codeUnits[0])
      )


      1. nikita_dol
        02.09.2022 13:56

        1. Icon - это RichText со всеми вытекающими (видно тут)

        2. Категорически не советую делать так str.codeUnits[0], так как codeUnits это не тоже самое, что рисуемые символы (не просто так существуют такие пакеты)


        1. avdosev
          02.09.2022 14:08

          1. удивлен что для такого простого на первый взгляд виджета используется RichText
          2. да, код выше скорее как базовый пример задумывался, но пункт 1 слегка подпортил задумку


  1. avdosev
    02.09.2022 13:45

    Насколько увеличивается размер инсталятора и самого приложения при подключении Skia к существующему React Native приложению?