给RN应用增加绘图的翅膀 -- React Native SVG

4,556 阅读10分钟

一. 导言

React Native其实是一个比较残缺的跨平台框架. 它没有功能, 只有UI. 但凡是UI之外的功能性需求, 如请求地理位置, 在后台运行某任务, 权限申请, 都是得下沉到native端去做. 这就意味着相同逻辑的代码要写两遍.

那至少UI是RN的本职工作, 它应该做得好吧? 哎, 也不尽然. RN本身是用了yoga框架来绘制图形, 它能适用于基本的UI元素, 如TextView/UILabel, Button/UIButton, ... 但一到了要自己绘制时就马上不行了. 因为RN没有自己的绘制库. 它的View就是JSX, JSX全是由元素组成的. 要想像Android一样在onDraw()中自己调用 canvas来绘制就完全没招.

有人说, 其实facebook也不是没想到这一点. 比如community里就有这个库github.com/react-nativ… , 就是能让我们在RN上绘图的. 确实, 但网上关于这个库的资料不多, 而且从star与fork数上看, 大家对它多是不感冒的.

starfork
rn-webview3.2K1.6K
rn-svg4.9K666
rn-art19843

那在应用中要绘图是刚需吗?
: 要是你的应用只是简单的元素, 那没必要绘图. 但稍复杂些的应用, 你说只用系统元素, 这不太现实. 举个例子, 你说在Android和iOS上, 有多少个应用是一个自定义View都没有, 全是自己使用系统UI元素的?!

既然绘图算是刚需, 而RN又不能太好地支持. 那到了要绘图时, 我们怎么办?
: 这就是我们本篇文章的主角了, react-native-svg库(下面简称为rn-svg)

二. rn-svg简介

2.1 SVG

先说下什么是SVG吧. SVG就是一种矢量图的格式. 平常的图片格式, 如jpeg, png都是一个个像素堆叠起来的. 这样的图片在放大多倍时, 边缘就会有锯齿. 但矢量图不会, 你放大多少倍, 它看起来仍是很平滑. 来看个例子:

一个普通的SVG格式的文件可能长这样:

这个svg要是在Chrome等软件中打开, 长这样: ✔

从这个格式中可以看到svg就是描述了要怎么画自己, 比如画自己要一笔来完成✔的轮廓(第一个<path>没什么用), 先从哪里画到哪里, 再从哪里画到另一个点.

2.2 React-Native-SVG

rn-svg是facebok官方出的svg库. 其实facebook在上面花的心血可不少. 因为在不久前iOS还是不太支持SVG的, 要想使用SVG, 还得导入像SVGKit这样的三方库. 但Facebook楞是自己写代码, 去支持SVG的各个属性

下面图就是rn-svg的github中的代码结构

从这看得出来, Facebook是一一得去实现了element, shape, text这些元素, 并桥接到了RN上.

2.3 绘制圆

<Svg height="100" width="100">
  <Circle cx="50" cy="50" r="50" fill="pink" />
</Svg>

结果就是一个圆

drawing

2.4 绘制其它基本图形

SVG还能画矩形, 多边形, 线. 这些都蛮简单的, 就不一一浪费大家时间了. 详情可见这里

但值得一提的是, SVG还支持画Text与Image. 结合SVG中的一些高级特性, 如ClipPath, Mask, Pattern, 就让SVG里能做出灵活, 多变的文本与图片, 比如说: 做一个CircleIamgeView. 后面我们能看到更多的例子.

2.5 SVG中画RN元素

rn-svg绘图, 所有svg元素都在包在<Svg></Svg>之间, 而且包在其间的一般来说也只能是svg元素. 但人有三急, 说不定什么时候, 我们Svg里面也想包一个react native的元素, 比如说<View>, <FlatList>, ... 这时要怎么办呢?
: 答案就是使用<ForeignObject>. <ForeignObject>得在<Svg></Svg>之间, 但它的children可以是react native的元素. 比如说:

import {Image} from "react-native"
import Svg, {Circle, ForeignObject} from "react-native-svg"

<Svg>
	<Circle cx="50" cy="50" r="50" fill="pink" />
	<ForeignObject>
		<Image src={..} source={...}/>
	</ForeignObject>
</Svg>

三. SVG的应用

SVG能绘图, 但绘制的简单shape我上面没讲. 因为简单shape比较简单, 每个shape写一个三四行代码的实践就明白了. 我这里想有更大的篇幅来讲一下svg在应用开发中的作用.

3.1 画头像

Android中使用Xfermode, iOS有blend mode可以画出圆形头像, rn-svg也有类似的方法. 若有同学对Xfermode/BlendMode不太熟, 那简单来说就是: 两个图层叠在一起, 我们只显示某一部分 (可以是union, 可以是subtract, 可以intersect).

rn-svg中类似的做法, 是利用了ClipPath或Mask组件. 我们来看下ClipPath组件如何做到画一个圆形头像的.

先假设我们有这样一个图片

drawing

我们可以用它来制作一个圆形头像. 在开始动工之前, 我们先要介绍一个极重要的元素, 叫ClipPath

3.1.1 ClipPath

每个SVG的shape都可以应用ClipPath.

比如说我现在有一个圆:

<Svg>  
   <Circle cx="110" cy="110" r="100" fill="#9c6" />  
</Svg>
drawing

下面我小小修改一下, 我们新加一个ClipPath, 它里面画的是一个矩形, 并且id叫"id".

<Circle clipPath="url(#id)"/>应用了这个clipPath, 那其实最终就会显示Rect与Circle重合的部分.

<Svg>

 <Defs>  
   <ClipPath id="clip-1">  
     <Rect x="10" y="10" width="200" height="100" />  
   </ClipPath>  
 </Defs>

 <Circle cx="110" cy="110" r="100" fill="#9c6" clip-path="url(#clip-1)" />

</Svg>

这里明显 clippath的rect更小, 所以圆与矩形重合后, 只有clipPath定义过的区域才会显示. 圆的其它部分都会消失:

drawing

注意几点要素:

1). ClipPath要定义在<Defs>

2). Defs中定义的东西是不会画出来的. 除非有人用<Use>, 或fill 或 clippath等属性来引用Defs中的元素, 它们才会显示出来

3). clipPath属性的值是一个string, 但格式得是url(#ID). 这个ID就是上面<Defs> <ClipPath id="ID">中的ID.

3.1.2 画圆头像

那明白了ClipPath之后, 画圆形头像就容易了. 我们其实也是两层嘛

• 第一层就是batman的图, 形状为矩形

• 第二层(clipPath)是一个圆形.

这样二者一重合, 圆比batman图要小, 这样就只会有在圆内的batman图才会显示出来.

思路如上, 代码见这:

import Svg, { Circle, ClipPath, Defs, Image } from "react-native-svg";

  const { size } = props;
  const radius = size / 2;

    <Svg width={size} height={size} style={props.style}>
      <Defs>
        <ClipPath id="clip">
          <Circle cx={radius} cy={radius} r={radius}/>
        </ClipPath>
      </Defs>
      <Image href={props.href} preserveAspectRatio={scale}
             width={size} height={size}
             clipPath="url(#clip)"
      />
    </Svg>

这样的结果就是:

drawing

3.1.3 思考

那现在我们要是从UI那拿到这样一个设计

drawing

是不是我们也会做了?

: 答案, 就是一个clippath, 开头是上面的箭头形. 然后图片应用这个clipPath, 就得到了上面的结果了.

3.2 画渐变色

网上有一个库就是专门做这个事的, 叫react-native-linear-gradient. Star数也有3.7K, 算是不少关注的库了.

但其实我们完全没有必要引入这个库, 一个rn-svg就能实现它了.

3.2.1 渐变色的按钮

先来看成品:

drawing

再来看代码:

这个代码明显, 也就是一个Rect, 填充色为渐变. 然后中间一个Text, 写"Login"字样. 不是很难理解. 但我们要着重讲一下LinearGradient

备注: 上面代码中的<G>是group的意思, 相当于iOS中的embeded in a View, 或是Android中的外面再套一层 FrameLayout一样.

3.2.2 LinearGradient

首先, 像ClipPath一样, LinearGradient也得定义在<Defs>之中.

其次, LinearGradient的x1, x2, y1, y2属性就是从(x1,y1)到(x2, y2)开始渐变. 这4个属性的值全是[0,1]之间.

• 要是x1=0 y1=0 x2=1 y2=1, 那就是从左上角到右下角做渐变

• 要是x2=0 y1=0.5 x2=1 y2=0.5, 那就是从中间水平地做渐变了

第三就是LinearGraident中有<Stop>做children, 分别定义了颜色怎么变化

为了方便理解, 我们来看一下一个Sketch中画渐变是如何画的:

我们给一个文字的填充色设为渐变, 共有三个结点 (对应了svg文件中的三个<Stop>). 颜色分别是: 蓝色, 紫色, 红色.

最后成像效果是:

这个图中三个点是可以拖拽的, 以调整从哪里渐变到哪里. 比如说我们拖一下这些结点, 变成从上往下渐变, 结果就成了:

这里讲完, 那再回头看上面Login按钮中的渐变色, 是不是就了解了.

3.2.3 圆角矩形

上面代码有一个隐藏知识点, 就是如何画圆角矩形. Android中画圆角矩形得单独搞一个shape的xml; iOS中则要用layer.cornerRadius, 都不是很爽利; 但在rn-svg中就方便得多了, 只要给Rect添加rx, ry属性, 就有了圆角矩形了

<Rect ... rx={20} ry={20} />

rn-svg对svg的支付是不全面, 但基本够用的.

3.3 图标变色

以前我做过支付SDK. SDK嘛, 自然是被多个厂商所用. 但每个厂商的风格不一样, 有的是红色系, 有的是蓝色系. 这样我们SDK中的图标要是总只有一个颜色系显得有点突兀. 所以我还专门做过研究来根据接入方的theme来变化我们图标的颜色. 示意图如下:

现在我们以一个"翻译.svg"为例. 它本来长这样:

drawing

现在我们加以修改, 把这个svg的内容给copy到RN代码中, 小小修改(如小写path,svg改为Path, Svg), 就可以做到变色. 代码如下:

import Svg, { Symbol, Defs, Path, ClipPath, Use } from "react-native-svg";
export const Svg101_Icon_ChangeColor = () => {
  return (
    <Svg>
    
      <Symbol id="lang" viewBox="0 0 24 24">
        <Path
          d="M12.87 15.07l-2.54-2… Z”/>
      </Symbol>
      
      <Use href="#lang" x={40} y={40} width={180} height={180} fill="black"/>
      <Use href="#lang" x={220} y={40} width={180} height={180} fill="red"/>
      <Use href="#lang" x={440} y={40} width={180} height={180} fill="blue"/>
      
    </Svg>
  );
};

效果就是:

上面代码的主要关键点是<Symbol><Use>. 下面我们来一一讲解

3.3.1 Symbol 与 Use

Symbol有点类似Defs, 是用来定义一些东西的. 它在SVG中的作用主要就是避免重复. 你可以把Symbol想像成一个Utils function, 然后每个Use都是要调用这个utils function的类.

为了避免重复, 我们肯定是有一个utils function最好. 这样万一以后要修改, 就只要修改一处就好了. 完美地做到了"Don't Repeat Yourself".

SVG世界中的DRY原则就体现在Defs与Symbol上. Symbol中定义的开头, Use都可以拿来用.

同时Use还可以有不同的属性, 如x, y, fill等. 这样就造成类似的形状, 细微的差别(如大小不一样, 填充色fill不一样, ...).

3.3.2 Path

上面的"文A"图, 其实是用Path画的. 你可以把Path理解为你用钢笔画的画. 一般画画都是直线, 曲线之类的, 这样的直线/曲线都是有坐标信息的. 这个信息就存在了Path的d属性了. d是data的意思.

备注: 其实SVG中, 最强大, 最多变的就是这个Path. 根据d的不同,你可以展示很多不同的东西, 也可以做出多变灵活的动画. 这一块我打算放到下一篇文章中讲.

3.4 Pattern

Pattern其实就是 形状中要填充某元素. 这个元素要是很小, 那就平铺填满的意思. 是的, Pattern你就可以理解为平铺.

比如我们有这样一个图片:

drawing

我们仍是在<Defs>中定义一个<Pattern id="brick">, 这个<Pattern>里再定义一个Image, 来放这个brick.jpg.

然后在一个矩形中应用它: <Rect x={50} y={560} width={300} height={150} fill="url(#brick)"/>

结果就成了:

drawing

代码则是:

<Svg>
  <Defs>
    <Pattern id="brick">
      <Image href={requires("brick.jpg")}/>
    </Pattern>
  </Defs>

  <Rect x={0} y={0} width={100} height={80} fill="url(#brick)"/>
</Svg>  

注意, Pattern也是定义在Defs里的. 因为brick.jpg很小, 但要画的墙很大, 所以我们需要不断地重复brick.jpg, 这时就是Pattern的作用了. 你可以理解 Pattern 就是"平铺"效果.

四. 总结

RN-SVG主要是还是想补强RN的绘图功能, 加上SVG又比较强大的功能, 所以RN-SVG其实在日常开发中还是很有用武之地的.

当然, 这里要多讲一句, 不是所有svg的元素, 所有的svg属性都被RN-SVG所支持. 举个例子, rn-svg到现在都还不支持<Filter>元素, 不过在TODO的backlog中了. 饶是如此, 现有的rn-svg已经比较强大了, 大可放心地使用.

最后, 要是大家对SVG感兴趣, 那下一篇文章就会介绍利用SVG来做动画. 这块就更强大, 也更有意思, 但也更难些. 好了, 到这本篇就完了, 多谢你阅读.