Taro系列中一直沒有跨端的繪圖工具,小程序端支持canvas但是不支持svg,RN端有 react-native-svg 支持svg,但是沒有很好原生的canvas插件,社區(qū)的canvas都是基于WebView實現(xiàn)的,或者skia,這個插件的書寫方式和canvas有較大的差異
所以開發(fā)了這個兼容小程序、H5和React Native 的Svg組件,來實現(xiàn)跨端繪圖
前言
在小程序上面,是不支持直接編寫svg代碼的,但是有間接的方式讓他支持,就是使用Image渲染svg,或者使用css的背景圖渲染一個svg,但是這樣寫有一定的局限性,例如要支持動畫、組件事件,將svg保存到本地等都無法實現(xiàn)
所以我選擇了使用Canvas來模擬開發(fā)svg的相關(guān)組件支持,在RN端則是使用現(xiàn)在已經(jīng)有的 react-native-svg 這個插件,因此我們只需要在小程序和H5端模擬 react-native-svg 組件的功能,組件屬性及其功能完全按照 react-native-svg 規(guī)范進(jìn)行開發(fā)
當(dāng)前組件屬性支持
Svg中有大量的組件,用來繪制各種圖形或者圖片
已經(jīng)支持的組件
- Svg
widthheightviewBoxpreserveAspectRatio - Rect
xywidthheight - Circle
cxcyr - Ellipse
cxcyrxry - Line
x1y1x2y2 - Image
xywidthheightpreserveAspectRatio - Text
xydxdyfontSizefontWeightfontFamilytextAnchorfontStyle - TSpan
xydxdyfontSizefontWeightfontFamilytextAnchorfontStyle - Polyline
points - Polygon
points - Path
d - Defs
- Use
hrefxy - G
- LinearGradient
x1y1x2y2 - RadialGradient
cxcyrxryfxfy - Stop
offsetstopColorstopOpacity - ClipPath
支持的公共屬性
- id
- style
- opacity
- fill
- fillOpacity
- stroke
- strokeWidth
- strokeOpacity
- strokeLinecap
- strokeLinejoin
- strokeDasharray
- strokeDashoffset
- clipPath
- origin
- originX
- originY
- translate
- translateX
- translateY
- rotation
- scale
- scaleX
- scaleY
- skew
- skewX
- skewY
- transform (如果要使用變換動畫,需要使用這個屬性實現(xiàn),其他變換屬性RN端不支持動畫)
動畫支持
因為選擇了 react-native-svg 作為開發(fā)參考,因此在動畫支持方面,也參照了RN的相關(guān)API Animated 和 Easing
下面是一個循環(huán)動畫的使用示例

import {
Svg, Rect,
Animated, Easing
} from '@/duxui/components/Svg'
import { useEffect, useRef } from 'react'
const AnimatedRect = Animated.createAnimatedComponent(Rect)
const Loop = () => {
const primary = duxappTheme.primaryColor
const secondary = duxappTheme.secondaryColor
const x = useRef(new Animated.Value(0)).current
const size = 50
useEffect(() => {
setTimeout(() => {
Animated.loop(
Animated.timing(x, {
toValue: 4,
duration: 3000,
easing: Easing.bounce,
useNativeDriver: false
})
).start()
}, 500)
}, [x])
const width = pxNum(702)
const height = pxNum(360)
return <GroupList.Item title='循環(huán)動畫' desc='利用 interpolate 將動畫值映射到任意范圍'>
<Svg width={width} height={height}>
<AnimatedRect
x={x.interpolate({
inputRange: [0, 1, 2, 3, 4],
outputRange: [0, width - size, width - size, 0, 0]
})}
y={x.interpolate({
inputRange: [0, 1, 2, 3, 4],
outputRange: [0, 0, height - size, height - size, 0]
})}
width={size}
height={size}
rx={10} ry={10} stroke={secondary} fill={primary}
/>
</Svg>
</GroupList.Item>
}
組件事件
為了實現(xiàn)組件事件,模擬 React Native 的 PanResponder API,在小程序和H5端實現(xiàn)了相關(guān)的功能
大多數(shù)組件都支持以下觸摸事件
- onPress
- onPressIn
- onPressOut
- onLongPress
下面兩個示例,一個展示點擊事件,一個事件結(jié)合動畫實現(xiàn)拖拽效果

事件示例
下面的代碼我們能看到點擊不同的組件將獲得對應(yīng)組件的事件,這三個形狀是有覆蓋關(guān)系的
const Event = () => {
return <GroupList.Item title='組件事件' desc='點擊對應(yīng)的圖形會有對應(yīng)的點擊事件'>
<Svg width={100} height={100}>
<Circle
cx='50%'
cy='50%'
r='38%'
fill='red'
onPress={() => toast('點擊圓形')}
/>
<Rect
x='20%'
y='20%'
width='60%'
height='60%'
fill='blue'
onPress={() => toast('點擊正方形')}
/>
<Path
d='M50,5L20,99L95,39L5,39L80,99z'
fill='pink'
onPress={() => toast('點擊五角星')}
/>
</Svg>
</GroupList.Item>
}
結(jié)合動畫
const TouchEvent = () => {
const primary = duxappTheme.primaryColor
const secondary = duxappTheme.secondaryColor
const movePan = useRef(new Animated.ValueXY({ x: 10, y: 10 }, { useNativeDriver: false })).current
const moveEvent = useRef(PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
movePan.setOffset({
x: movePan.x._value,
y: movePan.y._value
})
},
onPanResponderMove: (e, gestureState) => {
movePan.setValue({ x: gestureState.dx, y: gestureState.dy })
},
onPanResponderRelease: () => {
movePan.flattenOffset()
}
})).current
const moveOriginPan = useRef(new Animated.ValueXY({ x: 150, y: 50 }, { useNativeDriver: true })).current
const moveOriginEvent = useRef(PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onPanResponderMove: (e, gestureState) => {
moveOriginPan.setValue({ x: gestureState.dx + 150, y: gestureState.dy + 50 })
},
onPanResponderRelease: () => {
Animated.timing(moveOriginPan, {
toValue: { x: 150, y: 50 },
duration: 600,
easing: Easing.bounce,
useNativeDriver: true
}).start()
}
})).current
return <GroupList.Item title='結(jié)合動畫' desc='藍(lán)色拖拽,紅色拖拽后回彈'>
<Svg width={pxNum(702)} height={pxNum(300)}>
<Rect width={pxNum(702)} height={pxNum(300)} fill='#fff' />
<RectAnimated
width={50}
height={50}
fill={secondary}
{...moveEvent.panHandlers}
x={movePan.x}
y={movePan.y}
/>
<RectAnimated
width={50}
height={50}
fill={primary}
{...moveOriginEvent.panHandlers}
x={moveOriginPan.x}
y={moveOriginPan.y}
// translateX={moveOriginPan.x}
// translateY={moveOriginPan.y}
/>
</Svg>
</GroupList.Item>
}
轉(zhuǎn)換為圖片
為了方便導(dǎo)入為圖片,還封裝了一個 SvgToImage 組件,專門將Svg導(dǎo)出為圖片方便下一步的處理,請前往文檔查看具體的使用方法
繼續(xù)
Svg相關(guān)功能是duxui庫中提供的一個組件,詳情請查看duxui文檔
當(dāng)前UI庫中的二維碼(QRCode)、簽名(Sign) 組件都是使用這個Svg組件來實現(xiàn)的
如果你對這個項目有興趣,可以查看文檔,繼續(xù)了解詳情
Svg文檔:http://duxapp.com/docs/duxui/svg
框架文檔:http://duxapp.com
GitHub:https://github.com/duxapp