在Taro中開發(fā)一個跨端Svg組件,同時支持小程序、H5、React Native

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
    width height viewBox preserveAspectRatio
  • Rect
    x y width height
  • Circle
    cx cy r
  • Ellipse
    cx cy rx ry
  • Line
    x1 y1 x2 y2
  • Image
    x y width height preserveAspectRatio
  • Text
    x y dx dy fontSize fontWeight fontFamily textAnchor fontStyle
  • TSpan
    x y dx dy fontSize fontWeight fontFamily textAnchor fontStyle
  • Polyline
    points
  • Polygon
    points
  • Path
    d
  • Defs
  • Use
    href x y
  • G
  • LinearGradient
    x1 y1 x2 y2
  • RadialGradient
    cx cy rx ry fx fy
  • Stop
    offset stopColor stopOpacity
  • 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 AnimatedEasing

下面是一個循環(huán)動畫的使用示例

an.gif
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)拖拽效果

event.gif

事件示例

下面的代碼我們能看到點擊不同的組件將獲得對應(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容