使用react-native-skia实现自绘地图实践与踩坑记录

587 阅读5分钟

react-native-skia版本: 1.9.1 官方文档 官方demo示例 版本更新日志

Tips: 1.9.1后启用了新的协调器,导致更新版本各种报错,所以没有升级到新版,先使用1.9.1版本

1、Text文字需要font文件?不想引入font文件怎么办

虽然官方文档给了Text使用默认字体的示例,但是实际验证并不生效

解决方案: 使用Paragraph相关api 可不指定字体 默认使用系统字体

2、想要增加字体描边效果,但没有相关api支持

解决方案: 使用Paragraph生成5遍相同文字,第一遍是黑色正文,后面是白色复制内容并分别偏移上下左右四个方向几像素模拟描边效果

3、图片与文字的定位问题

skia中元素定位默认都是以右下角为原点,需要自行偏移

解决方案: 建议给元素xy设置为0,外面包一层Group做transform偏移并且不设置origin 这样可以方便后续增加动画,简单实现可以直接包Group并设置origin, 如果直接设置xy和origin再加上transform偏移,容易造成双重偏移导致偏差

const carTransform = useDerivedValue(() => {
    return [
        { translateX: carAnimated.value.x },
        { translateY: carAnimated.value.y },
        { scale: carDynamicScale.value },
        { rotate: carAnimated.value.r },
        { translateX: -pivotOffsetX },
        { translateY: -pivotOffsetY }
    ];
});
return (
    <Group transform={carTransform}>
        <Image
            image={entityImage}
            x={0}
            y={0}
            width={modelWidth}
            height={modelHeight}
        />
    </Group>
)
 

4、旋转中心点问题

默认的旋转中心点是元素原点,如果设置origin改变旋转中心点遇到问题可以采用“先位移旋转再挪回定位点"方案,能达到同样效果

    // 图片定位点位置
    const pivotOffsetX = pivot?.x;
    const pivotOffsetY = pivot?.y;

    const carTransform = useDerivedValue(() => {
        return [
            { translateX: carAnimated.value.x },
            { translateY: carAnimated.value.y },
            { scale: carDynamicScale.value },
            { rotate: carAnimated.value.r },
            { translateX: -pivotOffsetX },
            { translateY: -pivotOffsetY }
        ];
    });

5、Paragraph多段不同字号颜色文字混排

解决方案: 目前Paragraph看起来只有链式增加文字,如果分开写的话可能会造成对不齐问题

const para = Skia.ParagraphBuilder.Make({
    maxLines: 1,
    textAlign: TextAlign.Center
})
    .pushStyle({
        fontStyle: { weight: deviceFontWeight },
        color: Skia.Color('#373C46'),
        fontSize: 11
    })
    .addText(showDistanceText ? '即将抵达 ' : '')
    .pushStyle({
        color: Skia.Color('#1D52FF'),
        fontStyle: { weight: deviceFontWeight },
        fontSize: 11
    })
    .addText(showTime ? String(time_left) : '')
    .pushStyle({
        color: Skia.Color('#5C5F66'),
        fontStyle: { weight: deviceFontWeight },
        fontSize: 11
    })
    .addText(showTime ? '分钟 ' : '')
    .pushStyle({
        fontStyle: { weight: deviceFontWeight },
        color: Skia.Color('#1D52FF'),
        fontSize: 11
    })
    .addText(transformDistance ? `${transformDistance}` : '')
    .pushStyle({
        color: Skia.Color('#5C5F66'),
        fontStyle: { weight: deviceFontWeight },
        fontSize: 11
    })
    .addText(remain_distance ? (distance >= 1000 ? '公里' : '米') : '')
    .build();
para.layout(500);

6、由四个点组成的不规则四边形path怎么添加圆角

解决方案: path内添加CornerPathEffect实现

<Path
    path={path}
    color={itemFillColor}
    style="fill"
    key={slotId}>
    <CornerPathEffect r={r} />
    <Paint
        color={itemStrokeColor}
        style="stroke"
        strokeJoin="round"
        strokeCap="round"
        strokeWidth={strokeWidth}>
        <CornerPathEffect r={r} />
    </Paint>
</Path>

7、元素点击事件实现

skia的所有元素都没有点击事件 需要自行实现

解决方案: 闭合的path可以使用path.contains(x,y)实现 非闭合的path及其他形状元素需要自行判断

注意:

1、如果元素偏移过,点击事件需要相应的偏移

2、触摸点需要通过屏幕坐标转换到canvas逻辑坐标并反转回自定义坐标系进行匹配

8、权重层级问题

不支持zIndex权重属性

解决方案: skia元素遵循后渲染的层级更高,或自行在数据中添加zIndex属性动态渲染实现

9、手势与state更新冲突引起卡顿

react的state更新会引起组件rerender,频率过高会影响手势操作和动画的更新

解决方案: 使用redux更新数据,需要注意redux数据并不会随着组件销毁而销毁,需要手动清除

10、canvasSize变化后图片位置异常

canvas宽高变化后path会重新绘制,但图片的定位并没有跟着更新位置,可能是skia的bug,

解决方案: 需要canvasSize变化后手动重新设置一遍图片的位置

11、线段缩减动画处理

使用Path组件自带的start&end属性,传递一个动画值

12、线段渐变

Path组件内使用LinearGradient

<Path
    path={path}
    color={config?.fillColor || '#377AFF'}
    start={progress}
    end={1}
    style="stroke"
    strokeJoin="round"
    strokeCap="round"
    strokeWidth={routeDynamicWidth}>
    <LinearGradient
        start={startPoint}
        end={endPoint}
        colors={['#3B76FF', 'rgba(21,92,255,0)']}
        positions={[0.55, 1]}
    />
</Path>

13、线段起始点绑定元素一起动画(场景:导航线起点绑定车辆)

使用useVectorInterpolation+useAnimatedReaction获取当前进度所在点位并绑定元素动画, 需要注意iniptRange和outputRange需要保持一致的距离比例,否则可能导致获取的点位和实际进度不一致

    const position = useVectorInterpolation(
        progress,
        inputRange.length <= 1 ? [0, 1] : inputRange,
        transformedCurPoints.length <= 1
            ? [
                  { x: 0, y: 0 },
                  { x: 0, y: 0 }
              ]
            : transformedCurPoints,
        {
            extrapolateRight: 'clamp'
        }
    );

    useAnimatedReaction(
        () => ({
            x: position.value.x,
            y: position.value.y
        }),
        (values) => {
            const { x, y } = values || {};
            runOnJS(setCarAnimated)({ x, y });
        }
    );

14、去除线段固定像素

使用虚线效果实现

{/* 去除导航线前20像素 */}
<DashPathEffect
    intervals={[pathLength, 0, pathLength, 20]}
    phase={-20}
/>

15、创建矩阵与矩阵的变换

创建:使用skia的Matrix方法,可以是动画值,方便后续制作动效

const defaultM = Skia.Matrix().translate(0, 0).scale(1, 1);
const matrix = useSharedValue(defaultM);

变换:利用useAnimatedReaction监听动画值的变化,再对应的变换矩阵

 useAnimatedReaction(
    () => ({
        tx: mapTranslateX.value,
        ty: mapTranslateY.value,
        s: mapScale.value,
        r: mapRotate.value
    }),
    (values) => {
            matrix.value = Skia.Matrix()
                .translate(values.tx, values.ty)
                .scale(values.s, values.s)
                .rotate(0);
    }
);

16、坐标矩阵转换与反转

使用skia的mapPoint3d和invert4 实现

const defaultM = Skia.Matrix();
const combinedMatrix = useRef<Matrix4>(defaultM as any);
// 坐标转换
const toMatrix = useCallback(
    (point: Point, invert = true) => {
        const { x = 0, y = 0 } = point || {};
        const [matrixX, matrixY] = mapPoint3d(combinedMatrix.current, [
            invert && drawInvertX ? -x : x,
            invert && drawInvertY ? -y : y,
            0
        ]);
        return { x: matrixX, y: matrixY };
    },
    [combinedMatrix.current]
);

// 坐标反转
const invert = useCallback(
    (point: Point) => {
        const { x = 0, y = 0 } = point || {};
        const [matrixX, matrixY] = mapPoint3d(
            invert4(combinedMatrix.current),
            [x, y, 0]
        );
        return {
            x: drawInvertX ? -matrixX : matrixX,
            y: drawInvertY ? -matrixY : matrixY
        };
    },
    [combinedMatrix.current]
);

17、canvas的手势缩放平移矩阵变换处理

canvas内增加Group组件,由Group接收matrix矩阵变换或transform变换,这样Group下面的所有子组件都会自动应用矩阵的变换

<Group
    matrix={matrix}
    // transform={mapTransform}
>
...
</Group>

18、元素视觉固定大小,不跟随缩放

方案一:需要固定大小的原生不给赋值缩放scale属性,但涉及到和缩放后的原生位置保持,不好实现较复杂

方案二:缩放后再单独处理元素的反缩放,视觉上保持大小不变

    useAnimatedReaction(
        () => ({
            s: mapScale.value
        }),
        (values) => {
            dynamicScale.value = Math.max(1 / values.s, 1) / 1;
        }
    );