SVGのアニメーション
react-native-svgの図形を、useAnimatedPropsやCSSアニメーション・トランジションで動かす方法を扱います。本書のデモには載せていないため、コード例と概念の説明が中心です。
Reanimatedはreact-native-svgのコンポーネントをアニメーションできます。円の半径やパスの形といった図形そのもの(cx・r・d・pointsなど)も、塗りや線といった見た目(fill・stroke・opacityなど)も対象です。
react-native-svgは本書のデモ環境には導入していないので、この章は動くデモではなくコード例と考え方の説明でお伝えします。実際に手を動かすときは、公式ドキュメントのデモとあわせて読むと分かりやすいはずです。
セットアップ
まずreact-native-svgをインストールします。SVGのコンポーネントはReanimatedに組み込まれていないので、アニメーションさせたいものはcreateAnimatedComponentでラップして対応させます。
import Animated from 'react-native-reanimated';
import { Circle } from 'react-native-svg';
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
このcreateAnimatedComponentは、第6部4章で扱った「Reanimated標準以外のコンポーネントをアニメーション対応にする」仕組みそのものです。
3つの動かし方
cx・r・d・fillといったSVGの属性は、React Nativeのstyleのキーではなくコンポーネントのpropsです。そのため、これらはstyleではなくpropsを通してアニメーションします。動かし方は3通りあります。
- インライン — 共有値をpropsへ直接渡す(
<AnimatedCircle r={r} />) useAnimatedProps— worklet内でpropsを計算し、その結果をanimatedPropsに渡す- CSS — SVGのpropsは素の値のまま置き、
animationNameやtransitionPropertyをanimatedPropsに加える
useAnimatedPropsは第2部2章で、CSSアニメーション・トランジションは第5部で扱った書き方が、そのままSVGにも通じます。
useAnimatedPropsで動かす
共有値をuseAnimatedProps経由で属性に流し込む例です。円の半径rを、1秒かけて往復させ続けます。
import { useEffect } from 'react';
import Animated, {
Easing,
useAnimatedProps,
useSharedValue,
withRepeat,
withTiming,
} from 'react-native-reanimated';
import { Circle, Svg } from 'react-native-svg';
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
function App() {
const r = useSharedValue(20);
useEffect(() => {
const easing = Easing.bezier(0.25, 0.1, 0.25, 1); // CSSの`ease`に相当
r.value = withRepeat(withTiming(50, { duration: 1000, easing }), -1, true);
}, []);
const animatedProps = useAnimatedProps(() => ({
r: r.value,
}));
return (
<Svg style={{ height: 100, width: '100%' }}>
<AnimatedCircle cx="50%" cy="50%" fill="#b58df1" animatedProps={animatedProps} />
</Svg>
);
}
共有値を起点に、worklet内で組み立てたpropsをanimatedPropsに渡す。第2部で身につけたコアAPIの型が、そのまま図形の属性に向いているだけです。
CSSアニメーションで動かす
同じ往復を、CSSのキーフレームアニメーションで書くこともできます。共有値もuseAnimatedPropsも使わず、animatedPropsにアニメーションの定義を並べるだけです。
<AnimatedCircle
cx="50%"
cy="50%"
r={20}
fill="#b58df1"
animatedProps={{
animationName: {
from: { r: 20 },
to: { r: 50 },
},
animationDuration: '1s',
animationIterationCount: 'infinite',
animationDirection: 'alternate',
animationTimingFunction: 'ease',
}}
/>
CSSトランジションで動かす
CSSトランジションは、対象の値がレンダーの間で変化したときに走ります。useStateやpropsから来る素の値が変われば、トランジションが動きます。
function App() {
const [grown, setGrown] = useState(false);
return (
<>
<Svg style={{ height: 100, width: '100%' }}>
<AnimatedCircle
cx="50%"
cy="50%"
fill="#b58df1"
r={grown ? 50 : 20}
animatedProps={{
transitionProperty: 'r',
transitionDuration: 300,
transitionTimingFunction: 'ease',
}}
/>
</Svg>
<Button title="Toggle" onPress={() => setGrown((value) => !value)} />
</>
);
}
最初のレンダーでは前の値がないのでアニメーションしません。以降、rが変わるたびに古い値から新しい値へトランジションします。
ここで一つ注意点があります。共有値だけは例外で、r.valueを変えても再レンダーが起きないため、トランジションは始まりません。共有値からpropを動かしたいときは、インラインでr={r}と渡すか、useAnimatedPropsを使ってください。
パスのモーフィング
Pathは、2つの形が同じコマンドの並びで書かれていれば、その間をなめらかに変形(モーフィング)できます。dは本物のCSSプロパティに対応するので、これはWeb上でも動きます。
import Animated from 'react-native-reanimated';
import { Path, Svg } from 'react-native-svg';
const AnimatedPath = Animated.createAnimatedComponent(Path);
const CIRCLE =
'M50 10 C72 10 90 28 90 50 C90 72 72 90 50 90 C28 90 10 72 10 50 C10 28 28 10 50 10 Z';
const STAR =
'M50 10 C50 22 78 50 90 50 C78 50 50 78 50 90 C50 78 22 50 10 50 C22 50 50 22 50 10 Z';
function App() {
return (
<Svg height={160} width="100%" viewBox="0 0 100 100">
<AnimatedPath
fill="#b58df1"
d={CIRCLE}
animatedProps={{
animationName: { from: { d: CIRCLE }, to: { d: STAR } },
animationDuration: '2s',
animationIterationCount: 'infinite',
animationDirection: 'alternate',
animationTimingFunction: 'ease-in-out',
}}
/>
</Svg>
);
}
iOSとAndroidでは、d(Path)とpoints(Polygon・Polyline)はどんな形の間でも自由にモーフィングします。WebではPathだけが対象で、しかもコマンド構造が一致するパス同士でないと、途中を補間せずにパッと切り替わります。
アニメーションできるコンポーネントとプロパティ
ここからは、CSSアニメーション・トランジションで何を、どのプラットフォームで動かせるかの一覧です。すべてのコンポーネントが共通の見た目プロパティをアニメーションでき、図形コンポーネントはそれに加えて自分の図形プロパティも動かせます。一覧にないプロパティはCSSではサポートされていないので、その場合はuseAnimatedPropsを使ってください。useAnimatedPropsは、コンポーネントが受け取るアニメーション可能な任意のpropを動かせます。
共通の見た目プロパティ
すべてのreact-native-svgコンポーネントが次のpropsを持つため、どのコンポーネント上でもアニメーションできます。
| プロパティ | Android | iOS | Web |
|---|---|---|---|
color |
✅ | ✅ | ✅ |
fill |
✅ | ✅ | ✅ |
fillOpacity |
✅ | ✅ | ✅ |
fillRule |
✅ | ✅ | ✅ |
stroke |
✅ | ✅ | ✅ |
strokeWidth |
✅ | ✅ | ✅ |
strokeOpacity |
✅ | ✅ | ✅ |
strokeDasharray |
✅ | ✅ | ✅ |
strokeDashoffset |
✅ | ✅ | ✅ |
strokeLinecap |
✅ | ✅ | ✅ |
strokeLinejoin |
✅ | ✅ | ✅ |
vectorEffect |
✅ | ✅ | ✅ |
opacity |
✅ | ✅ | ✅ |
pointerEvents |
✅ | ✅ | ✅ |
clipPath |
✅ | ✅ | ❌ |
clipRule |
✅ | ✅ | ❌ |
mask |
✅ | ✅ | ❌ |
filter |
✅ | ✅ | ❌ |
marker |
✅ | ✅ | ❌ |
コンポーネントごとの図形プロパティ
次のコンポーネントは、iOSとAndroidで自分の図形プロパティをアニメーションできます。Web列は、その図形がWebでも動くかどうかを表します。
| コンポーネント | 図形プロパティ | Web |
|---|---|---|
Circle |
cx, cy, r |
✅ |
Ellipse |
cx, cy, rx, ry |
✅ |
Rect |
x, y, width, height, rx, ry |
✅ |
Image |
x, y, width, height |
✅ |
Path |
d |
✅¹ |
Polygon, Polyline |
points |
❌ |
Line |
x1, y1, x2, y2 |
❌ |
Text |
x, y, dx, dy, rotate |
❌ |
Pattern |
x, y, width, height, patternUnits, patternContentUnits |
❌ |
LinearGradient |
x1, y1, x2, y2, gradient, gradientUnits |
❌ |
RadialGradient |
cx, cy, r, rx, ry, fx, fy, gradient, gradientUnits |
❌ |
¹ Webでは、Pathのdはコマンド構造が一致するパスの間だけモーフィングし、不一致のときはパッと切り替わります。iOSとAndroidは自由にモーフィングします。
残りのコンポーネント(G・Use・Symbol・Defs・ClipPath・Mask・Marker・TSpan・TextPath・ForeignObject)は、アニメーションできる図形プロパティを持たず、共通の見た目プロパティだけを動かせます。Webでは、このなかでGのみサポートされます。
Patternのx・yはiOS専用です(react-native-svgがアニメーション以前にAndroidで対応していません)。Textのx・y・dx・dy・rotateは、グリフごとの配列も受け取れます。
補足
モーフィングと単位の注意点
d(Path)とpoints(Polygon・Polyline)は、iOSとAndroidではどんな形の間でも自由にモーフィングします。WebではPathだけが対象で、Polygon・Polylineのpointsはまったくアニメーションしません。react-native-svgがこれらをネイティブの<polygon>・<polyline>として描画し、そのpointsはCSSプロパティではないためです。
図形プロパティ(cx・r・x・widthなど)は、素の数値かパーセント文字列('50%')を受け取り、1つのアニメーションのなかで両者を混ぜることもできます。ただしグラデーションとパターンの座標だけは例外で、パーセント文字列と0〜1の割合は互いに補間できないので、1つのアニメーションではどちらか一方に統一してください。
段階的に切り替わるプロパティ
一部のプロパティは、値を補間せずに離散的な値の間で切り替わります。strokeLinecap・strokeLinejoin・fillRule・vectorEffect・gradientUnits・patternUnitsが該当します。これはネイティブのSVGやCSSの挙動に合わせたものです。
グラデーション
react-native-svgはグラデーションの色の区切りを<Stop>子要素で定義しますが、これはアニメーションできません。そこでReanimatedはgradientpropを追加しています。これは{ offset, color, opacity }という区切りの配列で、子要素の代わりに使います。各区切りのoffset・color・opacityをアニメーションでき、区切りの数をfromとtoで変えることもできます。グラデーションはネイティブプラットフォームのみで動き(Webでは動きません)、RadialGradientのfx・fyはiOS専用です。
Web
Webでは、ReanimatedはSVGをCSS経由で動かすため、本物のCSSプロパティに対応する属性だけがアニメーションします。CSSに対応する属性がないもの(Polygon・Polylineのpoints、Lineの端点、Text・Pattern・グラデーションの座標、グラデーションの区切り)はWeb上ではCSSでアニメーションできません。これらにはuseAnimatedPropsを使ってください。