import Svg, { Circle, Ellipse, G, Text, TSpan, TextPath, Path, Polygon, Polyline, Line, Rect, Use, Image, Symbol, Defs, LinearGradient, RadialGradient, Stop, ClipPath, Pattern, Mask, } from 'react-native-svg'; import { useState, useImperativeHandle, forwardRef, memo, useMemo, useRef, useCallback, } from 'react'; import type { ForwardedRef } from 'react'; import { Platform, View, Image as RNImage } from 'react-native'; import { setPlatformAPI, // DEFAULT_FONT_FAMILY as zrenderFontFamily, } from 'zrender/lib/core/platform.js'; import { measureText } from '../utils/platform'; // import { DEFAULT_FONT_FAMILY } from './utils/font'; import { GestureHandler } from '../components/GestureHandler'; import { dispatchEventsToZRender } from '../components/events'; import type { ChartElement, DispatchEvents, SVGChartProps, SVGVNode, } from '../types'; export { SVGRenderer } from './SVGRenderer'; export * from '../types'; setPlatformAPI({ measureText }); const tagMap = { svg: Svg, circle: Circle, ellipse: Ellipse, g: G, text: Text, tspan: TSpan, textPath: TextPath, path: Path, polygon: Polygon, polyline: Polyline, line: Line, rect: Rect, use: Use, image: Image, symbol: Symbol, defs: Defs, linearGradient: LinearGradient, radialGradient: RadialGradient, stop: Stop, clipPath: ClipPath, pattern: Pattern, mask: Mask, }; const imageSizeMap: any = {}; interface ImageSize { width: number; height: number; } // 使用 async/await 获取图片尺寸 const getImageSize = async (uri: string): Promise => { return new Promise((resolve, reject) => { RNImage.getSize( uri, (width, height) => resolve({ width, height }), (error) => reject(error) ); }); }; async function imageInit(url: string) { const { width, height } = await getImageSize(url); imageSizeMap[url] = { width, height, }; } function toCamelCase(str: string) { var reg = /-(\w)/g; return str.replace(reg, function (_: any, $1: string) { return $1.toUpperCase(); }); } interface SVGVEleProps { node: SVGVNode; touchStart?: any; touchMove?: any; touchEnd?: any; } const fontStyleReg = /([\w-]+):([\w-]+)/; function SvgEle(props: SVGVEleProps) { const { node } = props; if (!node) return null; const { tag, text, children } = node; // const Tag = tagMap[tag as keyof typeof tagMap]; // @ts-ignore const Tag = tagMap[tag]; if (!Tag) return null; const attrs: any = Object.entries(node.attrs).reduce( (carry, [key, value]) => { carry[toCamelCase(key)] = value; return carry; }, {} as Record ); if (tag === 'text') { if (attrs.style) { // TODO: 全局替换字体做法比较暴力,或者实用定义字体,可能某些场景字体设置失效,需要修复 // attrs.style = attrs.style.replace(new RegExp(zrenderFontFamily, 'g'), DEFAULT_FONT_FAMILY); const matches = attrs.style.split(';'); matches .filter((match: string) => fontStyleReg.test(match)) .forEach((match: string) => { const parts = match.split(':'); const key = parts[0]?.trim(); let value = parts[1]?.trim(); if (key) { // echart里默认字体sans-serif,ios无法识别 if ( Platform.OS === 'ios' && key === 'font-family' && value === 'sans-serif' ) { value = 'Helvetica Neue'; } attrs[toCamelCase(key)] = value; } }); } if (!attrs.alignmentBaseline && attrs.dominantBaseline) { attrs.alignmentBaseline = 'middle'; } // fix: https://github.com/react-native-svg/react-native-svg/issues/1862 if (attrs.paintOrder === 'stroke') { attrs.strokeWidth = 0; } // fixed svg fillOpacity bug in some render processes if (attrs.fillOpacity === undefined) { attrs.fillOpacity = 1; } return {text}; } // fix: https://github.com/react-native-svg/react-native-svg/issues/983 if (attrs.clipPath && !attrs.clipRule && Platform.OS === 'android') { attrs.clipRule = 'nonzero'; } if (tag === 'path') { // 全部数据为空,iOS渲染有问题,无效的path过滤掉 if (!attrs.d) return null; return ; } if (tag === 'linearGradient' || tag === 'radialGradient') { // note: 强制刷新渐变 // https://github.com/software-mansion/react-native-svg/issues/1762 return ( {children?.map((child) => SvgEle({ node: child, }) )} ); } if (tag === 'image' && !attrs.width) { if (imageSizeMap[attrs.href]) { const { width, height } = imageSizeMap[attrs.href]; attrs.width = (attrs.height / height) * width; if (attrs.height === 0) { attrs.opacity = 0; } } else { imageInit(attrs.href); } } return ( {children?.map((child) => ( ))} ); } function SvgRoot(props: SVGVEleProps) { const { node } = props; const { attrs, children } = node; const { width, height, viewBox } = attrs; return ( {children?.map((child) => ( ))} ); } function SvgComponent( props: SVGChartProps, ref: ForwardedRef<(ChartElement & any) | null> ) { const { node, style, handleGesture = true, ...gestureProps } = props; const [svgNode, setSvgNode] = useState(node); const width = useMemo( () => Number((svgNode?.attrs?.width || style?.width) ?? 0), [svgNode?.attrs?.width, style?.width] ); const height = useMemo( () => Number((svgNode?.attrs?.height || style?.height) ?? 0), [svgNode?.attrs?.height, style?.height] ); const zrenderId = useRef(undefined); const dispatchEvents = useCallback( (types, nativeEvent, eventArgs) => { if (zrenderId.current === undefined) return; dispatchEventsToZRender(zrenderId.current, types, nativeEvent, eventArgs); }, [] ); const containerStyle = useMemo( () => ({ ...style, width, height, }), [height, style, width] ); useImperativeHandle( ref, () => ({ elm: { setAttribute: (_name: string, _value: any) => {}, setAttributeNS: (_name: string, _value: any) => {}, removeAttribute: (_name: string) => {}, patch: (_oldVnode: SVGVNode, vnode: SVGVNode) => { setSvgNode((prevNode) => (prevNode === vnode ? prevNode : vnode)); }, setZrenderId: (id: number) => { zrenderId.current = id; }, }, dispatchEvents, getChartSize: () => { return { width, height, }; }, }), [dispatchEvents, width, height] ); return svgNode ? ( {handleGesture ? ( ) : null} ) : null; } const SvgChart = memo(forwardRef(SvgComponent)); SvgChart.displayName = 'SvgChart'; export default SvgChart;