mcpField
R
reactiive

reactiive

github.com/enzomanuelmanganoデザイン・UI1041 回インストール31 週間前に更新
リポジトリ

reactiive について

reactiiveは、React Native開発者向けのアニメーション支援スキルです。Reanimatedの共有値、ジェスチャー、スプリング、レイアウトアニメーションに加え、設計原則やよくある問題への対処法を扱います。

主な機能

  1. 01Reanimatedの共有値とアニメーションスタイル
  2. 02派生値によるアニメーション状態の整理
  3. 03共有値の変化に応じたコールバック
  4. 04ジェスチャーとスプリングアニメーションの設計
  5. 05レイアウトと共有要素のトランジション
  6. 06変換順序などの問題の解決

活用シーン

  1. 01タッチ操作へのフィードバック追加
  2. 02ドラッグやスワイプ操作の構築
  3. 03要素の表示・非表示やレイアウトのアニメーション
  4. 04画面間の共有要素トランジションの設計

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/enzomanuelmangano/skill --skill reactiive

インストール後、Claude Code で/reactiiveを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

design-engineering.md
easings.md
gestures.md
scroll-animations.md
sequences.md
skia-patterns.md
springs.md
text-animations.md
worklets.md

詳細

React Native アニメーションの作り方

最初の応答

このスキルが具体的な質問なしで初めて呼び出された場合は、次の内容だけを返してください。

自然な動きのアニメーションを作る準備ができています。

ユーザーから質問されるまでは、ほかの情報を提供しないでください。


基本のレシピ

すべてのアニメーションには、3つの要素が必要です。どれかが欠けると、動作しません。

// 1. A shared value that lives on the UI thread
const scale = useSharedValue(1);

// 2. An animated style that reads from it
const rStyle = useAnimatedStyle(() => ({
  transform: [{ scale: scale.get() }],
}));

// 3. An Animated.View that renders it
<Animated.View style={[styles.square, rStyle]} />

共有値にデータを保持します。アニメーションスタイルでそれをプロパティに変換します。Animated.View で描画します。

これさえ理解すれば、あとはその応用です。

注意: React Compiler との互換性のため、.value ではなく .get() と .set() を使用してください。


4つのフック

フック目的
useSharedValueUI スレッドで値を作成します
useAnimatedStyle共有値をスタイルに変換します
useDerivedValue既存の値から新しい値を計算します
useAnimatedReaction値が変化したときに副作用を実行します

useDerivedValue は特におすすめです。非常に重要なフックになります。


useDerivedValue

ほかの共有値に基づいて自動的に再計算される新しい共有値を定義します。

const isDragging = useSharedValue(false);

const rotate = useDerivedValue(() => {
  return withSpring(isDragging.get() ? '45deg' : '0deg');
});

const scale = useDerivedValue(() => {
  return withSpring(isDragging.get() ? 0.9 : 1);
});

ポイントは、戻り値をアニメーション関数でラップすることです。isDragging が変化すると、派生値が滑らかにアニメーションします。

派生値の連鎖
const color = useDerivedValue(() => {
  if (isDragging.get()) return '#0099ff';
  if (translateY.get() < 0) return 'black';
  if (translateY.get() > 0) return 'white';
  return '#0099ff';
});

const animatedColor = useDerivedValue(() => {
  return withTiming(color.get());
});

連鎖させましょう。色は状態から派生し、animatedColor でアニメーションを追加します。シンプルで宣言的です。


useAnimatedReaction

UI スレッド上で動作する useEffect のようなものです。共有値を監視し、変化したときにコールバックを実行します。

useAnimatedReaction(
  () => left.get(),
  (curr, prev) => {
    if (curr !== prev && curr !== 0) {
      cancelAnimation(scale);
      scale.set(0);
      scale.set(withSpring(1, { mass: 0.5 }));
    }
  },
);

curr !== 0 のチェックにより、マウント時の実行を防ぎます。新しいアニメーションを開始する前に、実行中のアニメーションをキャンセルしてください。


よくある間違い

変換の順序

変換の順序は重要です。移動の前に拡大縮小すると、移動量に倍率がかかります。

// ✗ Wrong: translateX of 50 becomes 50 * 1.5 = 75
transform: [
  { scale: 1.5 },
  { translateX: 50 },
]

// ✓ Correct: translate first, then scale
transform: [
  { translateX: translateX.get() },
  { translateY: translateY.get() },
  { scale: scale.get() },
  { rotate: rotate.get() },
]
配列インデックスのキー

配列インデックスをキーに使うと、レイアウトアニメーションが正しく動作しなくなります。

// ✗ Wrong: indices shift when items change
{items.map((item, index) => (
  <Animated.View key={index} layout={LinearTransition} />
))}

// ✓ Correct: stable unique IDs
{items.map((item) => (
  <Animated.View key={item.id} layout={LinearTransition} />
))}
iOS のバウンス

iOS のスクロールビューは境界を越えてバウンスします。値をクランプしないと、補間が壊れます。

import { clamp } from 'react-native-reanimated';

// Raw progress can go negative or exceed 1
const rawProgress = offset / maxOffset;

// Clamped progress stays valid
progress.set(clamp(rawProgress, 0, 1));
変換に対するタイミングアニメーション

変換にタイミングアニメーションを使うと、動きが機械的に感じられます。スプリングを使いましょう。

// ✗ Mechanical
scale.set(withTiming(1.2, { duration: 300 }));

// ✓ Natural
scale.set(withSpring(1.2));
速度の指定漏れ

ジェスチャーの終了時には、速度を渡してください。渡さないと、アニメーションに一体感がなくなります。

.onFinalize((event) => {
  translateX.set(withSpring(0, {
    velocity: event.velocityX,
  }));
});

スプリング

スプリングは物理法則をシミュレートするため、自然な動きになります。実際の物体は、決まった時間で動くわけではありません。

dampingRatio

おすすめの範囲:0.95〜1。 プロフェッショナルな UI にはこの範囲を使いましょう。

dampingRatio動作
0.95 - 1臨界減衰 — オーバーシュートなし
< 0.95減衰不足 — 意図的に弾む動き
> 1過減衰 — 動きが鈍くなるため避けてください
// Default: clean, professional
scale.set(withSpring(1, { dampingRatio: 1 }));

// Slightly softer feel
translateY.set(withSpring(0, { dampingRatio: 0.95 }));
使い分け
アニメーションの種類使用するもの
変換(scale、translate、rotate)withSpring
静的プロパティ(opacity、色)withTiming + easing
// ✓ Transforms: springs
scale.set(withSpring(1.1));

// ✓ Opacity: timing
opacity.set(withTiming(1, { duration: 200 }));

コンテキストパターン

新たにタッチすると、パンジェスチャーは開始位置に戻ります。解決策は、ジェスチャー開始時に位置を保存することです。

const translateX = useSharedValue(0);
const context = useSharedValue({ x: 0 });

const panGesture = Gesture.Pan()
  .onBegin(() => {
    context.set({ x: translateX.get() });
  })
  .onUpdate((event) => {
    translateX.set(event.translationX + context.get().x);
  });

pressto によるタッチフィードバック

タップには毎回フィードバックが必要です。pressto は自然な動きのアニメーション付きタッチ可能コンポーネントを提供します。

import { PressableScale } from 'pressto';

<PressableScale onPress={handlePress}>
  <View style={styles.button}>
    <Text>Tap me</Text>
  </View>
</PressableScale>
グローバルな触覚フィードバック
import { PressablesConfig } from 'pressto';
import * as Haptics from 'expo-haptics';

<PressablesConfig
  globalHandlers={{
    onPress: () => Haptics.selectionAsync(),
  }}
>
  {children}
</PressablesConfig>

レイアウトアニメーション

マウント時とアンマウント時のアニメーションを、最も簡単に実装できます。

import Animated, { FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated';

{isVisible && (
  <Animated.View
    entering={FadeIn.duration(300)}
    exiting={FadeOut.duration(200)}
    layout={LinearTransition.springify()}
  />
)}

退出アニメーションを手動で実装するのは大変です。Reanimated は遅延、アニメーション、アンマウントを自動的に処理します。

LinearTransition.springify()
// Snappy
LinearTransition.springify().mass(0.3).damping(20).stiffness(250)

// Smooth
LinearTransition.springify().mass(1).damping(20).stiffness(120)

共有トランジション

要素が変形しながら画面間を移動するアニメーションです。

import { Image } from 'expo-image';
import Animated from 'react-native-reanimated';

const AnimatedImage = Animated.createAnimatedComponent(Image);

// Home screen
<AnimatedImage
  source={{ uri: item.url }}
  sharedTransitionTag={`image-${item.id}`}
  style={{ width: 100, height: 100 }}
/>

// Detail screen - same tag
<AnimatedImage
  source={{ uri: item.url }}
  sharedTransitionTag={`image-${item.id}`}
  style={{ width: '100%', aspectRatio: 1 }}
/>

詳細画面には containedTransparentModal のプレゼンテーションを使用してください。


魔法のボタン

記憶に残るアニメーションとは、このようなものです。タップすると、拡大縮小、グラデーションの回転、ぼかしが起こります。

const isTouched = useSharedValue(false);

const scale = useDerivedValue(() => {
  return withSpring(isTouched.get() ? 1.2 : 1);
});

const rotate = useDerivedValue(() => {
  return withTiming(isTouched.get() ? Math.PI * 2 : 0, { duration: 1000 });
});

<Canvas style={{ width: realWidth, height: realHeight }}>
  <Group origin={vec(centerX, centerY)} transform={[{ rotate: rotate.get() }]}>
    <RoundedRect x={realX} y={realY} width={width} height={height} r={width / 2}>
      <SweepGradient c={center} colors={['cyan', 'magenta', 'yellow', 'cyan']} />
      <BlurMask blur={20} style="solid" />
    </RoundedRect>
  </Group>
</Canvas>

機能的なアニメーションはタッチを確認します。記憶に残るアニメーションは驚きを与えます。魔法のボタンはその両方を実現します。


機能的なアニメーションと記憶に残るアニメーション

機能的なアニメーションは、ユーザーが期待するものです。ボタンをタップすると、フィードバックが返ってきます。これは欠かせません。なければ、アプリが壊れているように感じられます。しかし、ユーザーの記憶にアプリを残すことはできません。

記憶に残るアニメーションは、ユーザーが予想していないものです。光りながら回転するグラデーション。3D フリップ。こうしたアニメーションはユーザーを驚かせます。

まず機能的なアニメーションを作りましょう。その後、記憶に残るアニメーションを加えます。


哲学

すべてのアニメーションには役割が必要
役割アニメーション
タッチを確認する押したときに縮小する
接続を示すカードを詳細表示にモーフィングする
注意を誘導するトリガーの方向からフェードインする
位置関係を保つ画面間で共有要素を使う
進行状況を示すパルスまたはシマー

役割を説明できないなら、そのアニメーションは削除してください。

使用頻度に合わせて強さを調整する

何度も起こるものほど、控えめにする必要があります。

// Happens constantly: clean, no bounce
const buttonScale = withSpring(pressed ? 0.97 : 1, {
  dampingRatio: 1,
});
制約は早めに決める
  • アプリ全体で使うスプリング設定は 3 種類
  • タッチフィードバックにはスケールと不透明度のみを使用
  • すべてのタイミングアニメーションに同じイージングカーブを使用

一度決めたことを、どこでも適用します。そうすることで、アプリに一貫性が生まれます。


リファレンスファイル


リソース

コメント