跳到主要内容

Hooks

以下是在将 React Native Skia 与 Reanimated 一起使用时我们提供的动画 hooks。我们还提供用于在与 Reanimated 集成时创建纹理的 hooks。

usePathInterpolation

此 hook 基于进度值在不同的路径值之间进行插值,从而在提供的路径之间提供平滑的过渡。

路径需要是可插值的,这意味着它们必须包含相同数量和类型的命令。如果路径具有不同的命令或不同数量的命令,则插值可能不会按预期运行。请确保 outputRange 数组中的所有路径的结构相似,以便进行正确的插值。为了插值两个完全不同的路径,我们发现 flubber 库 与 Skia 配合使用效果良好(请参阅示例)。

tsx
import React, { useEffect } from 'react';
import { useSharedValue, withTiming } from 'react-native-reanimated';
import { Skia, usePathInterpolation, Canvas, Path } from '@shopify/react-native-skia';
 
const angryPath = Skia.Path.MakeFromSVGString("M 16 25 C 32 27 43 28 49 28 C 54 28 62 28 73 26 C 66 54 60 70 55 74 C 51 77 40 75 27 55 C 25 50 21 40 27 55 Z")!;
const normalPath = Skia.Path.MakeFromSVGString("M 21 31 C 31 32 39 32 43 33 C 67 35 80 36 81 38 C 84 42 74 57 66 60 C 62 61 46 59 32 50 C 24 44 20 37 21 31 Z")!;
const goodPath = Skia.Path.MakeFromSVGString("M 21 45 C 21 37 24 29 29 25 C 34 20 38 18 45 18 C 58 18 69 30 69 45 C 69 60 58 72 45 72 C 32 72 21 60 21 45 Z")!;
 
const Demo = () => {
const progress = useSharedValue(0);
useEffect(() => {
progress.value = withTiming(1, { duration: 1000 });
}, []);
 
const path = usePathInterpolation(progress, [0, 0.5, 1], [angryPath, normalPath, goodPath]);
return (
<Canvas style={{ flex: 1 }}>
<Path path={path} style="stroke" strokeWidth={5} strokeCap="round" strokeJoin="round" />
</Canvas>
);
};
tsx
import React, { useEffect } from 'react';
import { useSharedValue, withTiming } from 'react-native-reanimated';
import { Skia, usePathInterpolation, Canvas, Path } from '@shopify/react-native-skia';
 
const angryPath = Skia.Path.MakeFromSVGString("M 16 25 C 32 27 43 28 49 28 C 54 28 62 28 73 26 C 66 54 60 70 55 74 C 51 77 40 75 27 55 C 25 50 21 40 27 55 Z")!;
const normalPath = Skia.Path.MakeFromSVGString("M 21 31 C 31 32 39 32 43 33 C 67 35 80 36 81 38 C 84 42 74 57 66 60 C 62 61 46 59 32 50 C 24 44 20 37 21 31 Z")!;
const goodPath = Skia.Path.MakeFromSVGString("M 21 45 C 21 37 24 29 29 25 C 34 20 38 18 45 18 C 58 18 69 30 69 45 C 69 60 58 72 45 72 C 32 72 21 60 21 45 Z")!;
 
const Demo = () => {
const progress = useSharedValue(0);
useEffect(() => {
progress.value = withTiming(1, { duration: 1000 });
}, []);
 
const path = usePathInterpolation(progress, [0, 0.5, 1], [angryPath, normalPath, goodPath]);
return (
<Canvas style={{ flex: 1 }}>
<Path path={path} style="stroke" strokeWidth={5} strokeCap="round" strokeJoin="round" />
</Canvas>
);
};

usePathValue

此 hook 提供了一种简单的方式来为路径设置动画。在后台,它确保一切都尽可能高效地完成。有一个可选的第二个参数可用于设置默认路径值。

tsx
import {useSharedValue, withSpring} from "react-native-reanimated";
import {Gesture, GestureDetector} from "react-native-gesture-handler";
import {usePathValue, Canvas, Path, processTransform3d, Skia} from "@shopify/react-native-skia";
 
const rrct = Skia.Path.Make();
rrct.addRRect(Skia.RRectXY(Skia.XYWHRect(0, 0, 100, 100), 10, 10));
 
export const FrostedCard = () => {
const rotateY = useSharedValue(0);
 
const gesture = Gesture.Pan().onChange((event) => {
rotateY.value -= event.changeX / 300;
});
 
const clip = usePathValue((path) => {
"worklet";
path.transform(
processTransform3d([
{ translate: [50, 50] },
{ perspective: 300 },
{ rotateY: rotateY.value },
{ translate: [-50, -50] },
])
);
}, rrct);
return (
<GestureDetector gesture={gesture}>
<Canvas style={{ flex: 1 }}>
<Path path={clip} />
</Canvas>
</GestureDetector>
);
};
tsx
import {useSharedValue, withSpring} from "react-native-reanimated";
import {Gesture, GestureDetector} from "react-native-gesture-handler";
import {usePathValue, Canvas, Path, processTransform3d, Skia} from "@shopify/react-native-skia";
 
const rrct = Skia.Path.Make();
rrct.addRRect(Skia.RRectXY(Skia.XYWHRect(0, 0, 100, 100), 10, 10));
 
export const FrostedCard = () => {
const rotateY = useSharedValue(0);
 
const gesture = Gesture.Pan().onChange((event) => {
rotateY.value -= event.changeX / 300;
});
 
const clip = usePathValue((path) => {
"worklet";
path.transform(
processTransform3d([
{ translate: [50, 50] },
{ perspective: 300 },
{ rotateY: rotateY.value },
{ translate: [-50, -50] },
])
);
}, rrct);
return (
<GestureDetector gesture={gesture}>
<Canvas style={{ flex: 1 }}>
<Path path={clip} />
</Canvas>
</GestureDetector>
);
};

useClock

此 hook 返回一个数字,指示自 hook 激活以来经过的毫秒数。

tsx
import { Canvas, useClock, vec, Circle } from "@shopify/react-native-skia";
import { useDerivedValue } from "react-native-reanimated";
 
export default function App() {
const t = useClock();
 
const transform = useDerivedValue(() => {
const scale = (2 / (3 - Math.cos(2 * t.value))) * 200;
return [
{ translateX: scale * Math.cos(t.value) },
{ translateY: scale * (Math.sin(2 * t.value) / 2) },
];
});
 
return (
<Canvas style={{ flex: 1 }}>
<Circle c={vec(0, 0)} r={50} color="cyan" transform={transform} />
</Canvas>
);
}
tsx
import { Canvas, useClock, vec, Circle } from "@shopify/react-native-skia";
import { useDerivedValue } from "react-native-reanimated";
 
export default function App() {
const t = useClock();
 
const transform = useDerivedValue(() => {
const scale = (2 / (3 - Math.cos(2 * t.value))) * 200;
return [
{ translateX: scale * Math.cos(t.value) },
{ translateY: scale * (Math.sin(2 * t.value) / 2) },
];
});
 
return (
<Canvas style={{ flex: 1 }}>
<Circle c={vec(0, 0)} r={50} color="cyan" transform={transform} />
</Canvas>
);
}

画布大小

Canvas 元素具有一个 onSize 属性,该属性可以接收一个共享值,每当画布大小更改时,该共享值都会更新。

tsx
import {useSharedValue} from "react-native-reanimated";
import {Fill, Canvas} from "@shopify/react-native-skia";
 
const Demo = () => {
// size will be updated as the canvas size changes
const size = useSharedValue({ width: 0, height: 0 });
return (
<Canvas style={{ flex: 1 }} onSize={size}>
<Fill color="white" />
</Canvas>
);
};
tsx
import {useSharedValue} from "react-native-reanimated";
import {Fill, Canvas} from "@shopify/react-native-skia";
 
const Demo = () => {
// size will be updated as the canvas size changes
const size = useSharedValue({ width: 0, height: 0 });
return (
<Canvas style={{ flex: 1 }} onSize={size}>
<Fill color="white" />
</Canvas>
);
};

useRectBuffer

创建一个用于动画的矩形数组。可被任何使用矩形数组作为属性的组件使用,例如 Atlas API

tsx
import {useRectBuffer} from "@shopify/react-native-skia";
 
const width = 256;
const size = 10;
const rects = 100;
// Important to not forget the worklet directive
const rectBuffer = useRectBuffer(rects, (rect, i) => {
"worklet";
rect.setXYWH((i * size) % width, Math.floor(i / (width / size)) * size, size, size);
});
tsx
import {useRectBuffer} from "@shopify/react-native-skia";
 
const width = 256;
const size = 10;
const rects = 100;
// Important to not forget the worklet directive
const rectBuffer = useRectBuffer(rects, (rect, i) => {
"worklet";
rect.setXYWH((i * size) % width, Math.floor(i / (width / size)) * size, size, size);
});

useRSXformBuffer

创建一个用于动画的旋转缩放变换数组。可被任何使用旋转缩放变换数组作为属性的组件使用,例如 Atlas API

tsx
import {useRSXformBuffer} from "@shopify/react-native-skia";
import {useSharedValue} from "react-native-reanimated";
 
const xforms = 100;
const pos = useSharedValue({ x: 0, y: 0 });
// Important to not forget the worklet directive
const transforms = useRSXformBuffer(xforms, (val, i) => {
"worklet";
const r = Math.atan2(pos.value.y, pos.value.x);
val.set(Math.cos(r), Math.sin(r), 0, 0);
});
tsx
import {useRSXformBuffer} from "@shopify/react-native-skia";
import {useSharedValue} from "react-native-reanimated";
 
const xforms = 100;
const pos = useSharedValue({ x: 0, y: 0 });
// Important to not forget the worklet directive
const transforms = useRSXformBuffer(xforms, (val, i) => {
"worklet";
const r = Math.atan2(pos.value.y, pos.value.x);
val.set(Math.cos(r), Math.sin(r), 0, 0);
});