🏇

React Native Skiaを使ってみよう!

2024/06/07に公開

こんにちは!テラーノベルでiOS/Android/Webとフロントエンド周りを担当している @kazutoyoです!

2024/05/30に行われました、「React Native Meetup #16 ft. カンム」でLTをさせていただきました!
https://react-native-meetup.connpass.com/event/317621/

今回はそちらの発表内容を元にした記事となります。

React Native Skiaについて

React Native Skiaは、Flutterなどでも使われるSkiaエンジンをReact Nativeで利用するためのライブラリです。

SkiaはCanvasライクなAPIを提供します。
それによって、React Native SkiaはネイティブのAPIでは表現しづらいリッチな要素のレンダリングに向いています。

たとえば、次のようなグラフの表示や描画パフォーマンスが必要になるようなUIを作るときに活用できます。
https://x.com/ekrembk/status/1782875120739971381

https://x.com/wcandillon/status/1759324282092499104

React Native Skiaを試してみよう!

円を描画する

まずは次のような画面に円を描画してみます。

Canvasの中に、Circleコンポーネントを配置し、Groupでブレンドモードを指定することで、色の乗算を行います。

import { Canvas, Circle, Group } from "@shopify/react-native-skia";
import { Dimensions } from "react-native";

const HelloWorld = () => {
    const width = Dimensions.get("window").width;
    const height = Dimensions.get("window").height;
    const r = width * 0.33;
    return (
      <Canvas style={{ width, height }}>
        <Group blendMode="multiply">
          <Circle cx={r} cy={r} r={r} color="cyan" />
          <Circle cx={width - r} cy={r} r={r} color="magenta" />
          <Circle cx={width / 2} cy={width - r} r={r} color="yellow" />
        </Group>
      </Canvas>
    );
}
export default HelloWorld;

簡単ですね!

アニメーションさせてみる

次は先程作った円をアニメーションさせてみましょう!
次のように円がグルグルと回りながら拡大、縮小します。

AnimatedCircleコンポーネントの作成

React Native Reanimatedを使い、円の位置と拡大/縮小アニメーションを行うコンポーネントを作成します。

type AnimatedCircleProps {
  color: string;
  size: number;
  centerX: number;
  centerY: number;
  initialAngle: number;
}

const AnimatedCircle = ({color, size, centerX, centerY, initialAngle}: AnimatedCircleProps) => {
  const distance = size * 0.25;
  const angle = useSharedValue(initialAngle);
  const r = useSharedValue(size * 0.3);
  const cx = useDerivedValue(
    () => centerX + distance * Math.cos(angle.value)
  );
  const cy = useDerivedValue(
    () => centerY + distance * Math.sin(angle.value)
  );

  useEffect(() => {
    angle.value = withRepeat(
      withTiming(angle.value + Math.PI * 6, { duration: 3000 }),
      -1
    );
  }, [angle]);

  useEffect(() => {
    r.value = withRepeat(withTiming(size * 1.15, { duration: 1500 }), -1, true);
  }, [r, size]);

  return <Circle cx={cx} cy={cy} r={r} color={color} />
}

AnimatedCircleコンポーネントをCanvasに配置

先程のCircleコンポーネントの変わりに、AnimatedCircleコンポーネントを配置します。

const AnimatedCircles = () => {
  const headerHeight = useHeaderHeight();
  const size = Dimensions.get("window").width * 0.33;
  const centerX = Dimensions.get("window").width / 2;
  const centerY = Dimensions.get("window").height / 2 - headerHeight;

  return (
    <Canvas style={{ flex: 1 }}>
      <Group blendMode="multiply">
        <AnimatedCircle centerX={centerX} centerY={centerY} size={size} initialAngle={0} color="cyan" />
        <AnimatedCircle centerX={centerX} centerY={centerY} size={size} initialAngle={(Math.PI * 2) / 3}  color="magenta" />
        <AnimatedCircle centerX={centerX} centerY={centerY} size={size} initialAngle={(Math.PI * 4) / 3}  color="yellow" />
      </Group>
    </Canvas>
  );
};

これによって次のようにアニメーションを描画することが出来ます!

テキストを描画する

Canvas上にテキストを描画し、ついでに塗りの色をアニメーションさせてみましょう!

テキストの描画 (1)

Textコンポーネントを使うことで単純なテキストを描画することが出来ます。
テキストの改行など、レイアウトが必要な場合はParagraph コンポーネントを使うこともできます。

import {Canvas, Text, useFont, matchFont} from "@shopify/react-native-skia";

const GradientText = () => {
  const fontSize = 32;
  const fontFamily = Platform.select({ ios: "Helvetica", default: "serif" });
  const font = matchFont({
    fontFamily: fontFamily,
    fontSize: fontSize,
    fontWeight: "bold",
  });
  return (
    <Canvas style={{ flex: 1 }}>
      <Text x={20} y={100} font={font} text="Hello React Native Skia!" />
    </Canvas>
  );
};

テキストの塗りのアニメーション

次のようにReact Natie ReanimatedのinterpolateColors関数などを使い、グラデーションの色を変化させます。

const startColors = ["rgba(34, 193, 195, 0.4)", "rgba(34,193,195,0.4)", "rgba(63,94,251,1)", "rgba(253,29,29,0.4)"];
const endColors = ["rgba(0,212,255,0.4)", "rgba(253,187,45,0.4)", "rgba(252,70,107,1)", "rgba(252,176,69,0.4)"];

const colorsIndex = useSharedValue(0);
// colorsIndexを3秒毎に変える
useEffect(() => {
  colorsIndex.value = withRepeat(
    withTiming(startColors.length - 1, {
      duration: 3000,
    }),
    -1,
    true
  );
}, [colorsIndex]);

// colorIndexの値から色を変化させるアニメーション
const gradientColors = useDerivedValue(() => {
  return [
    interpolateColors(colorsIndex.value, [0, 1, 2, 3], startColors),
    interpolateColors(colorsIndex.value, [0, 1, 2, 3], endColors),
  ];
});

テキストにLinearGradientを指定

TextとLinearGradientを指定して、Canvasに描画します。

return (
  <Canvas style={{ flex: 1 }}>
    <Fill color="#fff" />
    <Text x={20} y={100} font={font} text="Hello React Native Skia!">
      <LinearGradient
          start={vec(0, 0)}
          end={vec(width, fontSize)}
          colors={gradientColors}
      />
    </Text>
  </Canvas>
);

これでテキストの表示と塗りのアニメーションが完成です!🥳

画像を描画する

次のように、画像の描画 + React Native Gesture Handler + Blur + Maskといったものをつくってみましょう!

React Native Gesture Handlerの設定

React Native Gesture Handler(RNGH)を使って、パンジェスチャーを設定します。

次のように Gesture.Pan()onChange でoffsetX, offsetYを変更し、指を離したタイミングで0地点に戻します。

また、その移動した距離を使ってブラーの強さを0〜10の間で調整します。

const offsetX = useSharedValue<number>(0);
const offsetY = useSharedValue<number>(0);

const pan = Gesture.Pan()
  .onChange((event) => {
    offsetX.value = event.translationX;
    offsetY.value = event.translationY;
  })
  .onFinalize(() => {
    offsetX.value = withSpring(0);
    offsetY.value = withSpring(0);
  });

const imageX = useDerivedValue(() => {
  return (width - size.width) / 2 + offsetX.value;
});

const imageY = useDerivedValue(() => {
  return (height - size.height) / 2 + offsetY.value;
});

const blur = useDerivedValue(() => {
  const offset = Math.max(Math.abs(offsetX.value), Math.abs(offsetY.value));
  return Math.min((offset / 100), 1.0) * 10;
});

画像の読み込み

useImage hookを使って画像を読み込みます。
useImage hookは require でローカルの画像を読み込むか、 https://~ で指定してリモートの画像を読み込むことができます。

useImage hookで読み込まれたリソースをImageコンポーネントに渡し表示できます。

また、MaskBlurなどのコンポーネントも配置します。

const image = useImage(require("./assets/images/image.png"));

~~~

  <GestureDetector gesture={pan}>
    <Canvas style={{ flex: 1 }}>
      <Fill color="#333" />
      <Mask
        mode='luminance'
        mask={
          <Circle cx={width / 2} cy={height / 2} r={width/2} color="white" />
        }
      >
        <Image image={image}  x={imageX} y={imageY} width={size.width} height={size.height}>
          <Blur blur={blur} />
        </Image>
      </Mask>
    </Canvas>
  </GestureDetector>

以上で次のようなUIが実装できました!

まとめ

今回は簡単な表現のみでしたが、冒頭の例でも紹介ように、React Native Skiaを使うことでリッチでハイパフォーマンスなUIを作成することが出来ます。

React Native Skiaでどんな表現が出来るかは、CandillonさんのXYouTubeチャンネルをチェックすることで、より詳しく知ることが出来るかと思います。
https://www.youtube.com/wcandillon

もちろん通常のReact NativeでUIを作るより、レイアウトや実装が大変な面もありますが、表現の幅は無限大になると思います。

画面の一部だけReact Native Skiaを使いたい!ということもできるので、必要なときにチャレンジしてみてはいかがでしょうか?

それでは良いReact Nativeライフを!

テラーノベル テックブログ

Discussion