React Native Skiaでふわふわ動くサークルを実装する。
こんにちは!ぞのりょーです。
今回、React Native Skiaを使ってふわふわと動くサークルを実装しました。Skiaを使ったアニメーションの実装をしたことがなかったのですが、使ってみると意外とスムーズに実装ができたので、実装の方法についてまとめてみます。
成果物
今回作成する最終的な成果物は以下のようなものになります。4つのサークルがふわふわとそれぞれ異なる動きで動いているようなUIを実装します。
開発環境
今回開発している環境として、ReactNative、Expo、Skia等のバージョンは以下のとおりです。
"react-native": "0.74.5",
"expo": "~51.0.28",
"react": "18.2.0",
"react-dom": "18.2.0",
"@shopify/react-native-skia": "1.2.3",
"react-native-reanimated": "~3.10.1",
プロジェクトの作成とSkiaのインストール
bunx create-expo-app@latest
cd project-name
bun add @shopify/react-native-skia
今回使用するSkiaのコンポーネント概要
今回はSkiaの<Canvas />
コンポーネントと<Circle />
コンポーネントを用いてサークルを描画します。以下でそれぞれに渡しているprops
について説明します。なお、今回使用していないprops
についての説明は行いません。気になる方は公式ドキュメントをご参照ください。
<Canvas />
- style: サークルを描画する領域のスタイリングを指定します。
width
とheight
、flex: 1
などでサークルを描画する範囲を指定することで画面上にサークルが描画されます。今回は、最終的に複数のサークルを画面全体に描画したいため、flex: 1
を指定し、画面全体を描画領域としています。また、描画範囲がわかりやすいようにbackground: blue
を指定しています。
<Circle />
- cx: サークルのX軸の位置を指定。
- cy: サークルのY軸の位置を指定。
- r : サークルの半径を指定。
- color: サークルのカラーを指定。
半径r
分、X軸とY軸を移動することでサークル全体が表示できるため、次項の初期実装ではcx
、cy
にそれぞれr
を指定しています(cx
、cy
にそれぞれ0
を指定すると1/4のサークルが描画されます)。
サークルの実装
今回は最終的に複数のサークルを実装したいので、components/FloatingCircle/index.tsx
とcomponents/FloatingCircle/FloatingCircleItem/index.tsx
を作成し、サークルの実装を行なっていきます。
import { Circle } from '@shopify/react-native-skia';
type Props = {
r: number;
color: string;
};
export const FloatingCircleItem: React.FC<Props> = ({ r, color }) => {
return <Circle cx={r} cy={r} r={r} color={color} />;
};
<FloatingCircleItem />
コンポーネントはSkiaの<Circle />
コンポーネントでサークルの実態を実装しています。
import { Canvas } from '@shopify/react-native-skia';
import { FloatingCircleItem } from './FloatingCircleItem';
type Props = {
items: React.ComponentProps<typeof FloatingCircleItem>[];
};
export const FloatingCircle: React.FC<Props> = ({ items }) => {
return (
<Canvas
style={{
flex: 1,
backgroundColor: 'blue',
}}
>
{items.map(({ r, color }) => (
<FloatingCircleItem key={color} r={r} color={color} />
))}
</Canvas>
);
};
<FloatingCircle />
コンポーネントはSkiaの<Canvas />
コンポーネントでラップした<FloatingCircleItem />
コンポーネントを配置し、受け取ったitemsをmapメソッドで展開しています。
import { FloatingCircle } from '@/components/FloatingCircle';
import { SafeAreaView, Text } from 'react-native';
const FLOATING_CIRCLE_ITEMS = [
{ r: 50, color: 'yellow' },
] as const satisfies React.ComponentProps<typeof FloatingCircle>['items'];
export default function Home() {
return (
<SafeAreaView style={{ flex: 1 }}>
<Text>Floating Circle App</Text>
<FloatingCircle items={FLOATING_CIRCLE_ITEMS} />
</SafeAreaView>
);
}
routingファイル側では<FloatingCircle />
コンポーネントに必要なitemsを渡す形とします。今回は最終的に4つのサークルを作りたいので配列としてFLOATING_CIRCLE_ITEMS
を作成していますが、まずはわかりやすいように1つのサークルから実装していきます。現在の実装で、アプリを起動すると<Canvas />
が青、<Circle />
が黄色で描画されているはずです。
サークルをふわふわさせる実装
上記で作成したサークルをふわふわと動かしてみます。ここからはロジックの実装になるため、カスタムフックとしてロジック部分を切り出します。components/FloatingCircle/FloatingCircleItem/useFloatingCircleItem.ts
を作成し、以下のコードを追加します。
import { useCallback, useEffect } from 'react';
import {
useSharedValue,
withRepeat,
withTiming,
useDerivedValue,
Easing,
} from 'react-native-reanimated';
type Props = {
x: number;
y: number;
animationSize: number;
duration: number;
};
export const useFloatingCircleItem = ({
x,
y,
animationSize,
duration,
}: Props) => {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const memoizedWithRepeat = useCallback(
() =>
withRepeat(
withTiming(animationSize, {
duration,
easing: Easing.inOut(Easing.ease),
}),
-1,
true
),
[animationSize, duration]
);
// X軸のアニメーション
translateX.value = memoizedWithRepeat();
// Y軸のアニメーション
translateY.value = memoizedWithRepeat();
const cx = useDerivedValue(() => translateX.value + x);
const cy = useDerivedValue(() => translateY.value + y);
return { cx, cy };
};
実装したコードの解説
components/FloatingCircle/FloatingCircleItem/useFloatingCircleItem.ts
で実装したアニメーションのコードをそれぞれ上から順番に見ていきます。
Props
type Props = {
x: number;
y: number;
animationSize: number;
duration: number;
};
複数のサークルを作成するにあたり、それぞれ個別の位置、アニメーションの大きさ、スピードを設定したいため上記のprops
を受け取るように実装しています。
useSharedValueでアニメーションの値を定義
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
useSharedValue()
はReact Native Reanimatedで用意されているAPIで、JavaScriptスレッドとUIスレッド間で値を共有してくれるものです。要はアニメーションするために必要な値を管理するstateのようなものです。X軸のアニメーションの値としてtranslateX
、Y軸のアニメーションの値としてtranslateY
をそれぞれuseSharedValue()
で定義し、初期値は0
としています。
アニメーションのロジックを実装
const memoizedWithRepeat = useCallback(
() =>
withRepeat(
withTiming(animationSize, {
duration,
easing: Easing.inOut(Easing.ease),
}),
-1,
true
),
[]
);
withTiming()
は前述のReact Native Reanimatedで用意されているアニメーションの仕方を指定できるAPIで、withRepeat()
はリピートの仕方を指定できるAPIです。useCallback()
を用いてメモ化したアニメーション関数を宣言しています。
withTiming()
- 第1引数: アニメーションの大きさ(type:
number
。本記事内のanimationSize
の部分。) - 第2引数: アニメーションの設定オブジェクト
Optional
- duration: アニメーションの速度(type:
number
) - easing: 動き方(type:
Easing
) - reduceMotion: デバイスの視覚効果を抑制するアクセシビリティ設定に対するアニメーションの応答方法(type:
ReduceMotion
)
- duration: アニメーションの速度(type:
withTiming()
は2つの引数を受け取ります。今回はeasing
にEasing.inOut(Easing.ease)
を指定することでゆっくりとふわふわした動きを指定しています。他にもどのような動き方を指定できるかは公式ドキュメントをご参照ください。
withRepeat()
- 第1引数: 繰り返したいアニメーションオブジェクト
- 第2引数: 繰り返し回数
Optional
(デフォルト値は2
。-1
を指定することでループさせることができる。) - 第3引数: リバース設定
Optional
(アニメーションを1回ごとに逆方向にも実行するか。デフォルト値はfalse
。) - 第4引数: コールバック関数
Optional
(アニメーション完了後に呼び出される関数を指定) - 第5引数: reduceMotion
Optional
(デバイスの視覚効果を抑制するアクセシビリティ設定に対するアニメーションの応答方法)
withRepeat()
は5つの引数を受け取ります。今回の場合、第1引数にアニメーションオブジェクトwithTimign()
、第2引数にループをさせるため-1
、第3引数に反対方向にもアニメーションを実行させるためtrue
を指定しており、第4引数、第5引数は指定していません。
アニメーションのロジックを計算した値を管理
// X軸のアニメーション
translateX.value = memoizedWithRepeat();
// Y軸のアニメーション
translateY.value = memoizedWithRepeat();
const cx = useDerivedValue(() => translateX.value + x);
const cy = useDerivedValue(() => translateY.value + y);
useSharedValue()
で定義したtranslateX
、translateY
の値にアニメーション関数の返り値を代入します。このままではアニメーションとしては利用できず、useDerivedValue()
というAPIを用いることで実際のアニメーションとして提供できる値として管理することができます。<Circle />
コンポーネントに渡す値としてcx
、cy
をそれぞれ定義しており、useSharedValue()
のコールバック関数内でアニメーションの範囲 + それぞれのサークルのポジション
として実装しています。
useDerivedValue()
useDerivedValue()
はコールバック関数の返り値として2つの引数を受け取ります。
- 第1引数: 更新関数。
useSharedValue()
などのstate
を返す関数を指定する。 - 第2引数: 依存関係
Optional
今回では、useSharedValue()
で管理しているtranslateX
およびtranslateY
の値を返す関数を指定しており、それぞれX軸、Y軸のポジション(サークルの半径r
分)を足しています。これによりuseSharedValue()
の値がアニメーション関数により計算された値に更新され、アニメーションとして利用することができるようになります。
UI側のコードを修正
カスタムフックに切り出したコードが作成できたので、UI側のそれぞれのファイルも修正していきます。
import { Circle } from '@shopify/react-native-skia';
type Props = {
+ x: number;
+ y: number;
r: number;
color: string;
+ animationSize: number;
+ duration: number;
};
export const FloatingCircleItem: React.FC<Props> = ({
+ x,
+ y,
r,
color,
+ animationSize,
+ duration,
}) => {
+ const { cx, cy } = useFloatingCircleItem({ x, y, animationSize, duration});
+ return <Circle cx={cx} cy={cy} r={r} color={color} />;
};
import { Canvas } from '@shopify/react-native-skia';
import { FloatingCircleItem } from './FloatingCircleItem';
type Props = {
items: React.ComponentProps<typeof FloatingCircleItem>[];
};
export const FloatingCircle: React.FC<Props> = ({ items }) => {
return (
<Canvas
style={{
flex: 1,
backgroundColor: 'blue',
}}
>
+ {items.map((item) => (
+ <FloatingCircleItem key={item.color} {...item} />
))}
</Canvas>
);
};
import { FloatingCircle } from '@/components/FloatingCircle';
import { SafeAreaView, Text } from 'react-native';
const FLOATING_CIRCLE_ITEMS = [
{
+ x: 50,
+ y: 50,
r: 50,
color: 'yellow',
+ animationSize: 20,
+ duration: 2000,
},
] as const satisfies React.ComponentProps<typeof FloatingCircle>['items'];
export default function Home() {
return (
<SafeAreaView style={{ flex: 1 }}>
<Text>Floating Circle App</Text>
<FloatingCircle items={FLOATING_CIRCLE_ITEMS} />
</SafeAreaView>
);
}
これらの実装で以下のようにふわふわと動くサークルの実装ができていると思います。
4つのサークルを実装する
さて、ここまで来ればあとはapp/index.tsx
で渡しているFLOATING_CIRCLE_ITEMS
にアイテムを追加してあげれば完成です。animationSize
に-
値を指定することで反対方向へのアニメーションから開始されるため、それぞれがバラバラの動きになりより不規則的な動きが実現できます。
import { FloatingCircle } from '@/components/FloatingCircle';
import { FloatingCircleItem } from '@/components/FloatingCircle/FloatingCircleItem';
import { SafeAreaView, Text } from 'react-native';
// 各サークルのポジション、大きさ、カラー、アニメーションの大きさ、スピードを指定
const FLOATING_CIRCLE_ITEMS = [
+ {
+ x: 100,
+ y: 100,
+ r: 70,
+ color: 'red',
+ animationSize: -25,
+ duration: 2000,
+ },
+ {
+ x: 700,
+ y: 300,
+ r: 50,
+ color: 'green',
+ animationSize: 35,
+ duration: 3000,
+ },
+ {
+ x: 350,
+ y: 250,
+ r: 40,
+ color: 'pink',
+ animationSize: -30,
+ duration: 2500,
+ },
+ {
+ x: 500,
+ y: 50,
+ r: 60,
+ color: 'yellow',
+ animationSize: 40,
+ duration: 3500,
+ },
] as const satisfies React.ComponentProps<typeof FloatingCircle>['items'];
export default function Home() {
return (
<SafeAreaView style={{ flex: 1 }}>
<Text>Floating Circle App</Text>
<FloatingCircle items={FLOATING_CIRCLE_ITEMS} />
</SafeAreaView>
);
}
完成系
これで4つのサークルをそれぞれバラバラの動き方でふわふわさせる実装が完了です!
最後に
今回、React Native Skiaを使ってふわふわ動くサークルを実装しました。本記事の内容をもとに様々な動き方をするサークルにアレンジができるかと思いますのでぜひ色々と試してみてください!本記事がReact Nativeアプリ開発者の皆様にとって参考になれば嬉しく思います。最後まで読んでいただきありがとうございます。
Discussion