React Native ART 介绍与实践

- React FrontEnd RN D3
✨✨✨You can Edit this Article on Github site
✏️✏️✏️ This article .MD file was last updated at: Loading ...

React Native ART 由来

React-Art Reactjs 团队基于 Art(一个兼容各个浏览器 SVG 绘制的 API 封装)开发的模块,让 React 开发者能使用 jsx 语法绘制 svg

React Native 团队分别在 0.10.0 和 0.18.0 也添加了 iOS 和 Android 平台对 react-art 的支持,官网文档至今对其只字未提。本文旨在介绍安静躺在 react-native/Libraries/ 里的ART,并展示一些实践结果。

在 React Native 中 ART 是个非常重要的库,它让非常酷炫的绘图及动画变成了可能。但是可能是知道的人真的不多导致文档及少中文更少。很多都是把英文的参数列表翻译过来,也没有案例。

为什么用 ART

React Native 本身自带的 <Image> 有很多缺陷:

首先是不支持 SVG 格式的资源,目前的解决方案有通过 ReactNative-SVG 进行实现,但是这个库需要更改客户端 bundle 文件,带来一定的风险。

其次是 Image decoding can take more than a frame-worth of time,图片的解码由于不在主线程中进行,所以不能确保所有图片和内容在同一帧内出现,使用 <Image>标签的制作的组件里的图(比如 icon)可能是三三两两“闪现”出来的,让人怀疑是个webview,体验远不如原生,尤其是在开发环境下最为明显。

其次就是不能支持矢量图形,必须放置 @2x 或者 @3x 对应的图片。

ART 能干什么

俗话说,库如其名,背负着如此具有“艺术感”名字的 ART 生来就是为了绘制矢量图的,或者说是 画 UI 的,ART 可以解决上诉的所有缺陷。

在我看来,或者说我目前业务需求用到的功能:

  • ART 可以解决上诉 <Image> 的缺陷:解码和矢量图形
  • ART 可以实现 UI 上的一些渐变,比如渐变按钮,渐变背景或者底色。以及一些交互性较强的动画——画 UI
  • ART 另外一个场景就是简单数据可视化。

使用 ART

本文使用的 RN 版本为 0.50.1

本文一些英语词汇出于编写角度进行了简写,RN 是 React Native 的简写

Android 默认就包含 ART 库,IOS 需要单独添加依赖库。

  • ART 在 iOS 上使用需要事先导入 ART 的链接库,找到 node_modules/react-native/Libraries/ART/ART.xcdoeproj 拖入 Xcode 对应项目的 Libraries
  • 打开 General Settings添加 libART.aLinked Frameworks and Libraries 列表
  • cmd+b重新构建项目

基本 API

RN ART 文档 (非官方) 在 github 上有这样比较全一篇文档,可以选择直接看它了解使用。

ART 目前的 API 有:

  • Surface 标签对应 svg 中的SVG标签,所有 ART 的 jsx 内容需要被其包含
  • Group 标签对应 g 标签
  • Shape 标签对应 path 标签
  • Text 标签对应 text 标签
  • Transform 做图形变换的 API
  • Path 绘制路径 API
  • LinearGradient 创建线性渐变 API
  • RadialGradient 创建径向渐变 API

本文不介绍 SVG ,读者可以自行线下学习。

可以看到 ART 和 SVG 还是有不同的,有点像是阉割后的 SVG,当然已经有开发者做了实现,可以方便地使用 SVG 标签写 ReactNative-SVG ,这种方式的缺点上文已经说过。

在本文例子当中,我们使用原原本本的 ART 实践。

基础例子:

下面是一个绘制线段的 ART demo:

import React, { Component } from 'react'
import { View, Dimensions, ART } from 'react-native'

export default class Line extends Component {
  render() {
    return (
      <View>
        <ART.Surface width={Dimensions.get('window').width} height={100}>
          <ART.Shape
            d={new ART.Path().moveTo(50, 50).lineTo(100, 100)}
            stroke="#000000"
            strokeWidth={1}
          />
        </ART.Surface>
      </View>
    )
  }
}

Surface 必须是 ART 内容的父层,并且其中不能包含非 ART 标签(否则直接闪退…),需要指定宽高,很多时候绘制无效或者缺角有可能是 Path 超过了 Shape 的绘制区域。

Group 可有可无,当绘制内容较多时可以用其统一管理,可以把它当做 View标签使用,可制定内容在画布绘制的起点。

Shape 是目前 ART 绘制的首要入口,d 属性对标 svg 的 path 标签上的 d 属性。

所有的 ART 标签都可以使用 transform 属性做变换。

Shape 当中的 d 类似于 svg 的 path,可以通过 ART.Path 生成,比如下面这些是绘制的一些简单图形:

  • 圆形
// 绘制圆形
function circlePathRender() {
  const path = Path()
    .moveTo(0, 50)
    .arc(0, radius * 2, radius)
    .arc(0, radius * -2, radius)
    .close() // 闭合路径

  // path 可以直接作为 props 传给 shape
  return <ART.Shape d={path} fill={'#2ba'} />
}
  • 绘制多边形:
// 绘制多边形
function polygonPathRender() {
  var path = Path()
    .moveTo(10, 10)
    .lineTo(20, 30)
    .lineTo(30, 40)
    .close() // 闭合路径

  return <ART.Shape d={path} fill={'#00a'} stroke="yellow" strokeWidth={4} />
}

Path 还有一些 API 来满足日常绘图要求:arcTo/curve/line/Text

  • 文字
// 绘制文字
function textRender() {
  return (
    <ART.Text
      font={`bold 13px "Helvetica Neue", "Helvetica", Arial`}
      fill="#749"
      x={0}
      y={0}
    >
      Lorem ipsum dolor sit amet
    </ART.Text>
  )
}
  • 渐变
// linearGradient 可赋值给Path或者Text标签的fill属性

function linearGradientRender() {
  const linearGradient = new ART.LinearGradient({
    "0": "#2ba",
    ".5": "#f90",
    "0.7": "#aa4422",
    "1": "rgba(255,255,255,0.5)"
  }, 0, 0, 100, 200);

  // 这里的 d props(Path) 简略
  return <ART.Shape
           d={...}
           fill={linearGradient}
          />
}

LinearGradient 构造函数第一个参数是设定渐变色的对象。

使用诸如0.3/.52/1这样的属性表示30%/52%/100%,值为颜色值,不符合要求的键值对会被忽略。 后面四个参数分别表示:起点 x,起点 y,终点 x,终点 y.

// radialGradient 可赋值给Path或者Text标签的fill属性

function radialGradientRender() {
  const radialGradient = new ART.RadialGradient({
    "0": "#2ba",
    ".5": "#f90",
    "0.7": "#aa4422",
    "1": "rgba(255,255,255,0.5)"
  }, 0, 0, 100, 200);

  // 这里的 d props(Path) 简略
  return <ART.Shape
           d={...}
           fill={radialGradient}
          />
}

RadialGradient构造函数第一个参数是和线性渐变相同的,后续六个分别表示:焦点 x,焦点 y,x 半轴长,y 半轴长,原点 x,原点 y。

ART 动画

大多数情况下,在 React Native 中创建动画是推荐使用 Animated API 的,其提供了三个主要的方法用于创建动画:

ART 简单数据可视化

本文采用 d3-shapes 生成 Path 来通过 ART.Shape 进行绘制可视化数据。

注意:这个 Path 和 SVG 的 Path 基本上一样,但是请注意 ART 只是 阉割版的 SVG,以下针对 D3 文档的描述和解释是个人对 D3 的理解

通过上面对 ART 提供的一些 API 简单介绍可以发现,我们可以指定一个特殊的 PathShape 组件(d props),可以实现一些曲线线段,比如下面代码:

import React, { Component } from 'react'
import { ART, View } from 'react-native'

const { Surface, Group, Shape } = ART

export default class ARTSimpleLine extends Component {
  render() {
    return (
      <View>
        <Surface width={500} height={500}>
          <Group>
            <Shape
              d="M0,100L60,160L120,60L180,140L240,100L300,120"
              stroke="#000"
              strokeWidth={1}
              // 设置 shape X 偏移值
              x={50}
            />
          </Group>
        </Surface>
      </View>
    )
  }
}

可以得到如下的一个简单图形:

ARTSimpleLine

实际上我们可以通过一些工具生成 Path 来做简单的数据可视化。

而 path data 由一系列的命令组成,比如上面代码当中的:

SVG 的 path 这里不详细介绍,可以参考 SVG path tutorial

M0,100L60,160L120,60L180,140L240,100L300,120

实际上,我们可以自己通过书写代码来创建这些 path 命令集,但是你会发现,写出这些代码虽然并不难,但是一定是很繁琐的,所以我们需要一个工具来自动生成这些命令集。

我们这里采用 D3.js path generator 的路径生成器,下面列出了一些常见的路径生成器:

这里推荐一个在线查看 D3 shape demo

我们暂时只简单的用到了 Lines 生成器。

首先生明一个 line generator

const lineGenerator = d3.line()

我们来定义一个坐标数组:

const points = [
  [0, 100],
  [60, 160],
  [120, 60],
  [180, 140],
  [240, 100],
  [300, 120],
]

接着我们传入 points 参数来调用 lineGenerator:

const pathData = lineGenerator(points)
// pathData is "M0,100L60,160L120,60L180,140L240,100L300,120"

lineGenerator 完成的工作就是创建了一个 M(move to)和 L(line to)命令的字符串,这样我们就得到了 Path Data。

这是我们想要的 Path Data 吗?

显然不是,我们需要 Path Data 尽量具有以下特征:

  • 线条扁平,曲线化
  • Data Driven

查阅文档,发现 line.curve 方法可以实现特征一,至于 Curve ,D3 当中有做了详细的解释:

While lines are defined as a sequence of two-dimensional [x, y] points, and areas are similarly defined by a topline and a baseline, there remains the task of transforming this discrete representation into a continuous shape: i.e., how to interpolate between the points. A variety of curves are provided for this purpose.

简单翻译一下:线被定义为二维[x,y]点序列,为了将离散转换为连续形状的任务,D3 提供了各种曲线(ps:曲线实现是一个黑盒,使用者无需关心如何实现,可以查看 splines

D3 提供了 line.curve 方法。

curve 通常情况下不会直接构建或者生成,只会在调用 line.curve 方法的时候生成,很多方法来辅助生成 Curve:

  • curveBasis
  • curveCardinal

  • curveBasisClosed

这里只简单列出一些方法,而且我们这里只用到一种方法:curveCardinal

按照 D3 给出的例子,代码是下面这样的:

const line = d3.line().curve(d3.curveCardinal)

curveCardinal 的作用官方是这样介绍的:

Produces a cubic cardinal spline using the specified control points, with one-sided differences used for the first and last piece. The default tension is 0.

官方介绍太学术化,简单理解为:使用用户传入的控制点坐标来生成曲线。

简单和其他的方法比较一下,没有产生一个闭环,而且使用了第一个和最后一个控制点坐标,而且张力默认为 0。

然后我们就可以生成这样一个 Path Data

M0,100C0,100,40,166.66666666666666,60,160C80,153.33333333333334,100,63.333333333333336,120,60C140,56.666666666666664,160,133.33333333333334,180,140C200,146.66666666666666,220,103.33333333333333,240,100C260,96.66666666666667,300,120,300,120

实际得到的图形如下:

Art Curve

当然 D3.curve 传入不同的辅助方法会生成不同的 Path Data 以及曲线,读者可以自行线下尝试

这样看下来似乎已经很完美了,但是注意,这个 Path Data 是根据我们自定义的数据(预先设定完毕的二维坐标数组)生成的,实际业务当中,数据只会是数据,比如:

// 一周内的降雨流量数据(伪造)
const data = [820, 932, 631, 934, 890, 1330, 1320]

具体图表上的二维坐标我们是无法确定或者无法及时变动的,所以,似乎我们需要一个方法来自定义二维坐标的生成?

当然可以,上诉代码可以修改为:

const line = d3
  .line()
  .x(function(d) {
    return x(d.date)
  })
  .y(function(d) {
    return y(d.value)
  })
  .curve(d3.curveCardinal)

你可能会说这个 line.xline.y 是个什么鬼?

关于 line.x 官方是这样解释:

If x is specified, sets the x accessor to the specified function or number and returns this line generator. If x is not specified, returns the current x accessor, which defaults to:

> function x(d) {
>   return d[0]
> }
> ```

简单来说就是:如果指定 `x` ,则将 x accessor 设置为指定的函数或编号并返回此行 generator,什么意思呢?

这个可以理解为一个辅助函数,参照上面例子我们自定义的一个坐标数组,默认情况下每一个数组元素都代表了一个二维的数组,比如 `[0,100]` ,然而我们也可以告诉 `line generator` 来如何自定义解读传入的数据,而这就要使用对应的 `accessor functions` 了。

至于 `line.y` 当然基本也是一样的概念,但是默认的 Y accessor 为:

```javascript
function y(d) {
  return d[1]
}

这也是为什么之前我们构造的坐标为什么是一个二维数组。

按照上面伪造的一周内降雨流量数据:(这里复制一次,便于阅读

// 一周内的降雨流量数据(伪造)
const data = [820, 932, 631, 934, 890, 1330, 1320]

我们来一步步解析一下 line 图表的生成方法:

  • Y Axis 上需要设置最小值和最大值(最大值可能会动态变化),根据数据实际情况计算出来 Y 轴坐标
  • x Axis 上需要设置一周内的日期或者周一至周日,这 7 天的 x 轴坐标为了美观必须是等分的,所以计算出来对应 X 轴坐标

通过简单的上诉两个步骤,我们就能得到数据实际对应的二维坐标,这样才能生成 Path Data,绘制 line chart。

import React, { Component } from 'react'
import * as d3 from 'd3-shape'
import { ART, View } from 'react-native'

const { Surface, Group, Shape } = ART

const data = [
  { data: 'Mon', value: 820 },
  { data: 'Tue', value: 932 },
  { data: 'Wed', value: 631 },
  { data: 'Thu', value: 934 },
  { data: 'Fri', value: 890 },
  { data: 'Sat', value: 1330 },
  { data: 'Sun', value: 1320 },
]

const CHART_WIDTH = 375
const CHART_HEIGHT = 300

const X_AXIS_OFFSET = 50
const MAX_Y_AXIS = 1600
const MIN_Y_AXIS = 0

const lineGenerator = d3
  .line()
  .x((d, i) => i * X_AXIS_OFFSET)
  .y(d => CHART_HEIGHT - CHART_HEIGHT * Math.min(1, d.value / MAX_Y_AXIS))
  .curve(d3.curveCardinal)

const path = lineGenerator(data)

export default class ReactNativeART extends Component {
  render() {
    return (
      <View>
        <Surface width={CHART_WIDTH} height={CHART_HEIGHT}>
          <Group>
            <Shape d={path} stroke="#000" strokeWidth={1} />
          </Group>
        </Surface>
      </View>
    )
  }
}

上述代码当中,我们定义了一些常用的常量:

  • 图表的高度:CHART_HEIGHT
  • Y 轴最大值:MAX_Y_AXIS
  • X 轴每一项的等分距离:X_AXIS_OFFSET

实际生产的二维坐标为:

;[
  [0, 146.25],
  [50, 125.25],
  [100, 131.0625],
  [150, 124.875],
  [200, 58.125],
  [250, 50.625],
  [300, 52.5],
]

根据坐标生成 Path Data 绘制出来的图表为:

Art Line Chart One

一个基本的 line 图表就这样生成了,接下来我们做一些美化操作:

  • 添加 Point
  • 添加 X 和 Y 轴的坐标和 label

我们可以通过 ART.Path 来绘制圆形来模拟 Point:

  renderPoints(data) {

    const pointArc = 3
    return data.map((d, i) => {

      if (i === 0) return null

      return (
        <Shape
          key={i}
          d={new Path()
            .moveTo(X_AXIS_OFFSET * i, CHART_HEIGHT - pointArc - CHART_HEIGHT * Math.min(1, (d.value / MAX_Y_AXIS)))
            .arc(0, 2 * pointArc, pointArc)
            .arc(0, -2 * pointArc, pointArc)
            .close()
          }
          strokeWidth={0}
          stroke={'#9573D4'}
          fill={'#f00'}
        />
      )
    })
  }

绘制结果如下:

ART curve with points

接下来实现 X 轴和 Y 轴。

注意这个 Y 轴需要固定在左侧,所以 Y 轴 需要使用 RN 的 Text 绝对定位在 ScrollView 的左侧

 renderYAxis(data) {
    const split = 6
    return new Array(split).fill(null).map((d, i) => {
      if (i === 0) return null
      return <Text
        key={i}
        style={{
          position: 'absolute',
          fontSize: 12,
          color: '#000',
          left: '3%',
          backgroundColor: 'transparent',
          top: CHART_HEIGHT - (i / split) * CHART_HEIGHT
        }}
        alignment={'center'}
      >
        {String((i / split) * MAX_Y_AXIS)}
      </Text>
    })
  }

  renderXAxis(data) {

    return data.map((d, i) => {
      return <ART.Text
        key={i}
        fill={'#000'}
        font={`normal 12px Heiti SC`}
        x={i * X_AXIS_OFFSET + 12}
        y={CHART_HEIGHT - 12}
        alignment={'center'}
      >
        {d.data}
      </ART.Text>
    })
  }
  render() {

    return (
      <View
        style={{
          position: 'relative',
          width: Dimensions.get('window').width,
          height: CHART_HEIGHT
        }}
      >
        <ScrollView
          bounces={false}
          showsHorizontalScrollIndicator={false}
          horizontal
        >
          <Surface
            width={CHART_REAL_WIDTH}
            height={CHART_HEIGHT}
          >
            <Group>
              {this.renderLine(data)}
              {this.renderPoints(data)}
              {this.renderXAxis(data)}
            </Group>
          </Surface>
        </ScrollView>
        {this.renderYAxis(data)}
      </View>
    )
  }

Art curve with axis

这个图表看起来似乎很单调,我们添加一些 area fill 效果。

这里采用 D3 area 生成 area fill 图形,并且以 ART.LinearGradient 来 fill 渐变色。

renderPath(data) {
    const areaGenerator = d3.area()
      .x((d, i) => i * X_AXIS_OFFSET)
      .y0(CHART_HEIGHT)
      .y1(d => CHART_HEIGHT - CHART_HEIGHT * Math.min(1, (d.value / MAX_Y_AXIS)))
      .curve(d3.curveMonotoneX);

    return <Shape
      d={areaGenerator(data)}
      fill={new LinearGradient({
          '0': '#875CD5',
          '.88': '#A084D4'
        }, 0, 0, 0, CHART_HEIGHT
      )}
      strokeWidth={1}
    />
  }

得到效果如下:

Art curve chart with gradient

同样的道理,我们可以添加 背景色渐变,更改 X、Y 轴的字体颜色来进一步美化图表,最终效果如下图:

Art curve chart final

完整代码可以再 GitHub gist 上找到

似乎这个 chart 有点单调,生硬,我们需要添加一些动画、过渡效果

TODO

Reference