React Native Skia:将矩形填充波浪动画改为自定义角度扇形填充
问题描述
我希望将当前的矩形填充波浪动画修改为可自定义角度(例如40度)的扇形填充效果,示例如下:
当前动画效果:
我尝试使用<Group clip={...}>实现,但出现了整个扇形抖动的问题,未能成功解决。当前代码如下:
import React from "react"; import { Alert, Dimensions, SafeAreaView, View, StyleSheet, Text as RNText, TouchableOpacity, } from "react-native"; import { Skia, Canvas, Path, vec, useComputedValue, useClockValue, useValue, useTouchHandler, LinearGradient, Text, useFont, rrect,rect, } from "@shopify/react-native-skia"; import { line, curveBasis } from "d3"; const dimens = Dimensions.get("screen"); const width = 150; const frequency = 2; const initialAmplitude = 5; const verticalShiftConst = 100; const height = 600; const horizontalShift = (dimens.width - width) / 2; const indicatorArray = Array.from({ length: 11 }, (_, i) => i); export const WaveMeter = () => { const verticalShift = useValue(verticalShiftConst); const amplitude = useValue(initialAmplitude); const clock = useClockValue(); const touchHandler = useTouchHandler({ onActive: ({ y }) => { if (y > verticalShiftConst) { verticalShift.current = Math.min(height, y); amplitude.current = Math.max( 0, (height - verticalShift.current) * 0.025 ); } }, }); const createWavePath = (phase = 20) => { let points = Array.from({ length: width + horizontalShift }, (_, index) => { const angle = ((index - horizontalShift) / width) * (Math.PI * frequency) + phase; return [ index, amplitude.current * Math.sin(angle) + verticalShift.current, ]; }); const shiftedPoints = points.slice(horizontalShift, 300); const lineGenerator = line().curve(curveBasis); const waveLine = lineGenerator(shiftedPoints); const bottomLine = `L${ width + horizontalShift },${height} L${horizontalShift},${height}`; const extendedWavePath = `${waveLine} ${bottomLine} Z`; return extendedWavePath; }; const animatedPath = useComputedValue(() => { const speedFactor = 0.5; const current = (clock.current / 225) % 225; const start = Skia.Path.MakeFromSVGString(createWavePath(current)); const end = Skia.Path.MakeFromSVGString(createWavePath(Math.PI * current * speedFactor)); return start.interpolate(end, 0.5); }, [clock, verticalShift]); const trianglePath = useComputedValue(() => { return [ vec(horizontalShift * 2.6, verticalShift.current - 20), vec(horizontalShift * 2.6, verticalShift.current + 20), vec(horizontalShift * 2.3, verticalShift.current), ]; }, [verticalShift]); const font = useFont( require('../../../../shared/assets/fonts/Hammersmith_One/HammersmithOne-Regular.ttf')) const gradientStart = useComputedValue(() => { return vec(0, verticalShift.current); }, [verticalShift]); const gradientEnd = useComputedValue(() => { return vec(0, verticalShift.current + 150); }, [verticalShift]); const getLabelYValueOffset = (position) => { return verticalShiftConst + 50 * position; }; const getYLabelValue = (position) => { return `${(100 - position * 10)/10}`; }; const alertValue = () => { const adjustedShift = (verticalShiftConst - verticalShift.current) / (height - verticalShiftConst) + 1; Alert.alert("VALUE", `Your value is: ${Math.round(adjustedShift * 10)}`); }; const size = 600 const r = size / 2; const padding = size / 30; const innerCircleSize = size - padding * 2; const roundedRectangle = rrect( rect(padding, padding, innerCircleSize, innerCircleSize), r/2, r, ); return ( <SafeAreaView style={styles.container}> <Canvas style={styles.canvas} onTouch={touchHandler}> <Path path={animatedPath} style="fill"> <LinearGradient start={gradientStart} end={gradientEnd} colors={['cyan', 'blue']} /> </Path> </Canvas> <TouchableOpacity style={styles.buttonContainer} onPress={alertValue}> <RNText style={styles.buttonText}>GET VALUE</RNText> </TouchableOpacity> </SafeAreaView> ); }; const styles = StyleSheet.create({ container: { flex: 1, }, canvas: { flex: 1, }, buttonContainer: { height: 60, borderRadius: 8, backgroundColor: "#FF5349", marginHorizontal: 50, marginBottom: 20, justifyContent: "center", alignItems: "center", }, buttonText: { color: "white", fontSize: 20, fontWeight: "bold", }, });
解决方案
之前用Group clip出现抖动,是因为裁剪路径和波浪路径的坐标系统不匹配,加上动画插值时的精度误差导致的。下面是修复后的完整代码,支持自定义扇形角度:
import React from "react"; import { Alert, Dimensions, SafeAreaView, View, StyleSheet, Text as RNText, TouchableOpacity, } from "react-native"; import { Skia, Canvas, Path, vec, useComputedValue, useClockValue, useValue, useTouchHandler, LinearGradient, Text, useFont, Group } from "@shopify/react-native-skia"; import { line, curveBasis } from "d3"; const dimens = Dimensions.get("screen"); const frequency = 3; const initialAmplitude = 8; // 自定义扇形角度(单位:度) const fanAngle = 40; // 转换为弧度 const fanAngleRad = (fanAngle * Math.PI) / 180; // 扇形中心和半径 const center = vec(dimens.width / 2, dimens.height / 2); const radius = 250; // 控制填充高度的变量 const fillRatio = useValue(0.2); export const WaveMeter = () => { const amplitude = useValue(initialAmplitude); const clock = useClockValue(); const touchHandler = useTouchHandler({ onActive: ({ y }) => { // 根据触摸位置计算填充比例(从顶部到底部对应0到1) const ratio = Math.max(0, Math.min(1, (dimens.height - y) / dimens.height)); fillRatio.current = ratio; amplitude.current = Math.max(2, ratio * 15); }, }); // 创建扇形裁剪路径(固定不变,避免抖动) const fanClipPath = useComputedValue(() => { const path = Skia.Path.Make(); path.moveTo(center.x, center.y); // 从中心画到扇形起始点(右侧) path.lineTo(center.x + radius, center.y); // 画扇形弧 path.arcTo( center.x + radius, center.y + radius, center.x - radius, center.y + radius, radius, false, true ); // 闭合路径 path.close(); return path; }, []); const createWavePath = (phase) => { // 生成扇形弧上的波浪点 const pointCount = 100; const points = Array.from({ length: pointCount }, (_, i) => { // 计算当前点在扇形上的角度(从0到fanAngleRad) const angle = (i / (pointCount - 1)) * fanAngleRad; // 计算基础半径(根据填充比例) const baseRadius = radius * fillRatio.current; // 计算波浪偏移 const waveOffset = amplitude.current * Math.sin((angle / fanAngleRad) * Math.PI * frequency + phase); // 最终半径 const finalRadius = baseRadius + waveOffset; // 转换为笛卡尔坐标 const x = center.x + finalRadius * Math.cos(angle); const y = center.y + finalRadius * Math.sin(angle); return [x, y]; }); // 生成波浪路径并闭合到扇形中心 const lineGenerator = line().curve(curveBasis); const waveLine = lineGenerator(points); const closePath = `L${center.x},${center.y} Z`; return `${waveLine} ${closePath}`; }; const animatedPath = useComputedValue(() => { const speedFactor = 0.8; const currentPhase = (clock.current / 300) % (Math.PI * 2); const startPath = Skia.Path.MakeFromSVGString(createWavePath(currentPhase)); const endPath = Skia.Path.MakeFromSVGString(createWavePath(currentPhase + Math.PI * speedFactor)); return startPath.interpolate(endPath, 0.5); }, [clock, fillRatio, amplitude]); const gradientStart = useComputedValue(() => { return vec(center.x, center.y); }, []); const gradientEnd = useComputedValue(() => { return vec(center.x + radius, center.y + radius); }, []); const alertValue = () => { Alert.alert("VALUE", `填充比例: ${Math.round(fillRatio.current * 100)}%`); }; const font = useFont(require('../../../../shared/assets/fonts/Hammersmith_One/HammersmithOne-Regular.ttf')); return ( <SafeAreaView style={styles.container}> <Canvas style={styles.canvas} onTouch={touchHandler}> {/* 扇形裁剪容器,裁剪路径固定不变 */} <Group clip={fanClipPath}> {/* 填充波浪 */} <Path path={animatedPath} style="fill"> <LinearGradient start={gradientStart} end={gradientEnd} colors={['cyan', 'blue']} /> </Path> </Group> {/* 绘制扇形边框 */} <Path path={fanClipPath} style="stroke" strokeWidth={2} color="#fff" /> </Canvas> <TouchableOpacity style={styles.buttonContainer} onPress={alertValue}> <RNText style={styles.buttonText}>GET VALUE</RNText> </TouchableOpacity> </SafeAreaView> ); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#000", }, canvas: { flex: 1, }, buttonContainer: { height: 60, borderRadius: 8, backgroundColor: "#FF5349", marginHorizontal: 50, marginBottom: 20, justifyContent: "center", alignItems: "center", }, buttonText: { color: "white", fontSize: 20, fontWeight: "bold", }, });
关键修改说明
- 自定义扇形配置:新增
fanAngle变量直接设置扇形角度,自动转换为弧度用于计算,同时固定扇形的中心和半径 - 稳定裁剪路径:用Skia原生Path API创建固定的扇形裁剪路径,只在初始化时生成一次,避免动画中路径变化导致抖动
- 极坐标波浪生成:把原来的水平坐标转换为扇形上的角度,计算每个点的极坐标位置,让波浪完美贴合扇形弧度
- 触摸交互适配:调整触摸逻辑,根据触摸位置计算填充比例,控制波浪的基础高度
- 消除抖动:裁剪路径固定,只让波浪路径做动画插值,彻底解决之前的抖动问题
内容的提问来源于stack exchange,提问作者user19020361
相关产品推荐
相关产品推荐

