欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

react-native之ART绘图方法详解

程序员文章站 2022-05-14 19:29:31
背景 在移动应用的开发过程中,绘制基本的二维图形或动画是必不可少的。然而,考虑到android和ios均有一套各自的api方案,因此采用一种更普遍接受的技术方案,更有利于...

背景

在移动应用的开发过程中,绘制基本的二维图形或动画是必不可少的。然而,考虑到android和ios均有一套各自的api方案,因此采用一种更普遍接受的技术方案,更有利于代码的双平台兼容。

art是一个旨在多浏览器兼容的node style commonjs模块。在它的基础上,facebook又开发了react-art ,封装art,使之可以被react.js所使用,即实现了前端的svg库。然而,考虑到react.js的jsx语法,已经支持将 等等svg标签直接插入到dom中(当然此时使用的就不是react-art库了)此外还有html canvas的存在,因此,在前端上,react-art并非不可替代。

然而,在移动端,考虑到跨平台的需求,加之web端的技术积累,react-art成为了现成的绘制图形的解决方案。react-native分别在0.10.0和0.18.0上添加了ios和android平台上对react-art的支持。

示例代码

react.js和react-native的区别,只在于下文所述的art获取上,然后该例子就可以同时应用在web端和移动端上了。react-art自带的官方例子:vector-widget

vector-widget额外实现了旋转,以及鼠标点击事件的旋转加速响应。web端可以看到点击加速,但是在移动端无效,原因是react native并未对group中onmousedown和onmouseup属性作处理。本文着重于静态svg的实现,暂时无视动画部分效果即可。

art

在react native中art是个非常重要的库,它让非常酷炫的绘图及动画变成了可能。需要注意的是,在react native引入art过程中,android默认就包含art库,ios需要单独添加依赖库。

ios添加依赖库

1、使用xcode中打开react-native中的ios项目,选中‘libraries'目录 ——> 右键选择‘add files to 项目名称' ——> ‘node_modules/react-native/libraries/art/art.xcodeproj' 添加;

react-native之ART绘图方法详解

2、选中项目根目录 ——> 点击'build phases‘ ——> 点击‘link binary with libraries' ——> 点击左下方‘+' ——> 选中‘libart.a'添加。

react-native之ART绘图方法详解

基础组件

art暴露的组件共有7个,本文介绍常用的四个组件:surface、group、shape、text。

  • surface - 一个矩形可渲染的区域,是其他元素的容器
  • group - 可容纳多个形状、文本和其他的分组
  • shape - 形状定义,可填充
  • text - 文本形状定义

属性

surface

  • width : 渲染区域的宽
  • height : 定义渲染区域的高

shape

  • d : 定义绘制路径
  • stroke : 描边颜色
  • strokewidth : 描边宽度
  • strokedash : 定义虚线
  • fill : 填充颜色

text

  • funt : 字体样式,定义字体、大小、是否加粗 如: bold 35px heiti sc

path

  • moveto(x,y) : 移动到坐标(x,y)
  • lineto(x,y) : 连线到(x,y)
  • arc() : 绘制弧线
  • close() : 封闭空间

代码示例

绘制直线

react-native之ART绘图方法详解

import react from 'react'
import {
  view,
  art
} from 'react-native'

export default class line extends react.component{

  render(){

    const path = art.path();
    path.moveto(1,1); //将起始点移动到(1,1) 默认(0,0)
    path.lineto(300,1); //连线到目标点(300,1)

    return(
      <view style={this.props.style}>
        <art.surface width={300} height={2}>
          <art.shape d={path} stroke="#000000" strokewidth={1} />
        </art.surface>
      </view>
    )
  }
}

绘制虚线

了解strokedash的参数,

[10,5] : 表示绘10像素实线在绘5像素空白,如此循环

[10,5,20,5] : 表示绘10像素实线在绘制5像素空白在绘20像素实线及5像素空白

react-native之ART绘图方法详解

import react from 'react'
import {
  view,
  art
} from 'react-native'

const {surface, shape, path} = art;

export default class dashline extends react.component{

  render(){

    const path = path()
      .moveto(1,1)
      .lineto(300,1);

    return(
      <view style={this.props.style}>
        <surface width={300} height={2}>
          <shape d={path} stroke="#000000" strokewidth={2} strokedash={[10,5]}/>
        </surface>
      </view>
    )
  }
}

绘制矩形

首先通过lineto绘制三条边,在使用close链接第四条边。fill做颜色填充.

react-native之ART绘图方法详解

import react from 'react'
import {
  view,
  art
} from 'react-native'

const {surface, shape, path} = art;

export default class rect extends react.component{

  render(){

    const path = new path()
      .moveto(1,1)
      .lineto(1,99)
      .lineto(99,99)
      .lineto(99,1)
      .close();

    return(
      <view style={this.props.style}>
        <surface width={100} height={100}>
          <shape d={path} stroke="#000000" fill="#892265" strokewidth={1} />
        </surface>
      </view>
    )
  }
}

绘圆

了解arc(x,y,radius)的使用, 终点坐标距离起点坐标的相对距离。

react-native之ART绘图方法详解

import react from 'react'
import {
  view,
  art
} from 'react-native'

const {surface, shape, path} = art;

export default class circle extends react.component{

  render(){

    const path = new path()
      .moveto(50,1)
      .arc(0,99,25)
      .arc(0,-99,25)
      .close();


    return(
      <view style={this.props.style}>
        <surface width={100} height={100}>
          <shape d={path} stroke="#000000" strokewidth={1}/>
        </surface>
      </view>
    )
  }
}

绘制文字

了解funt属性的使用,规则是“粗细 字号 字体”

注意: 字体应该是支持path属性的,应该是实现bug并没有不生效。 android通过修改源码是可以解决的,ios没看源码。

react-native之ART绘图方法详解

import react, {component} from 'react';
import {
  appregistry,
  stylesheet,
  art,
  view
} from 'react-native';

const {surface, text, path} = art;

export default class arttextview extends component {

  render() {

    return (
      <view style={styles.container}>
        <surface width={100} height={100}>
          <text strokewidth={1} stroke="#000" font="bold 35px heiti sc" path={new path().moveto(40,40).lineto(99,10)} >react</text>
        </surface>

      </view>

    );
  }
}

const styles = stylesheet.create({
  container: {
    flex: 1,
    justifycontent: 'center',
    alignitems: 'center',
    backgroundcolor: '#f5fcff',
  },

});

绘制扇形

react-native之ART绘图方法详解

在这里需要使用arc做路径绘制。

wedge.js

import react, { component, proptypes } from 'react';
import { art } from 'react-native';
const { shape, path } = art;

/**
 * wedge is a react component for drawing circles, wedges and arcs. like other
 * reactart components, it must be used in a <surface>.
 */
export default class wedge extends component<void, any, any> {

  static proptypes = {
    outerradius: proptypes.number.isrequired,
    startangle: proptypes.number.isrequired,
    endangle: proptypes.number.isrequired,
    originx: proptypes.number.isrequired,
    originy: proptypes.number.isrequired,
    innerradius: proptypes.number,
  };


  constructor(props : any) {
    super(props);
    (this:any).circleradians = math.pi * 2;
    (this:any).radiansperdegree = math.pi / 180;
    (this:any)._degreestoradians = this._degreestoradians.bind(this);
  }

  /**
   * _degreestoradians(degrees)
   *
   * helper function to convert degrees to radians
   *
   * @param {number} degrees
   * @return {number}
   */
  _degreestoradians(degrees : number) : number {
    if (degrees !== 0 && degrees % 360 === 0) { // 360, 720, etc.
      return (this:any).circleradians;
    }
    return degrees * (this:any).radiansperdegree % (this:any).circleradians;
  }

  /**
   * _createcirclepath(or, ir)
   *
   * creates the reactart path for a complete circle.
   *
   * @param {number} or the outer radius of the circle
   * @param {number} ir the inner radius, greater than zero for a ring
   * @return {object}
   */
  _createcirclepath(or : number, ir : number) : path {
    const path = new path();

    path.move(0, or)
      .arc(or * 2, 0, or)
      .arc(-or * 2, 0, or);

    if (ir) {
      path.move(or - ir, 0)
        .counterarc(ir * 2, 0, ir)
        .counterarc(-ir * 2, 0, ir);
    }

    path.close();

    return path;
  }

  /**
   * _createarcpath(sa, ea, ca, or, ir)
   *
   * creates the reactart path for an arc or wedge.
   *
   * @param {number} startangle the starting degrees relative to 12 o'clock
   * @param {number} endangle the ending degrees relative to 12 o'clock
   * @param {number} or the outer radius in pixels
   * @param {number} ir the inner radius in pixels, greater than zero for an arc
   * @return {object}
   */
  _createarcpath(originx : number, originy : number, startangle : number, endangle : number, or : number, ir : number) : path {
    const path = new path();

    // angles in radians
    const sa = this._degreestoradians(startangle);
    const ea = this._degreestoradians(endangle);

    // central arc angle in radians
    const ca = sa > ea ? (this:any).circleradians - sa + ea : ea - sa;

    // cached sine and cosine values
    const ss = math.sin(sa);
    const es = math.sin(ea);
    const sc = math.cos(sa);
    const ec = math.cos(ea);

    // cached differences
    const ds = es - ss;
    const dc = ec - sc;
    const dr = ir - or;

    // if the angle is over pi radians (180 degrees)
    // we will need to let the drawing method know.
    const large = ca > math.pi;

    // todo (sema) please improve theses comments to make the math
    // more understandable.
    //
    // formula for a point on a circle at a specific angle with a center
    // at (0, 0):
    // x = radius * math.sin(radians)
    // y = radius * math.cos(radians)
    //
    // for our starting point, we offset the formula using the outer
    // radius because our origin is at (top, left).
    // in typical web layout fashion, we are drawing in quadrant iv
    // (a.k.a. southeast) where x is positive and y is negative.
    //
    // the arguments for path.arc and path.counterarc used below are:
    // (endx, endy, radiusx, radiusy, largeangle)

    path.move(or + or * ss, or - or * sc) // move to starting point
      .arc(or * ds, or * -dc, or, or, large) // outer arc
      .line(dr * es, dr * -ec);  // width of arc or wedge

    if (ir) {
      path.counterarc(ir * -ds, ir * dc, ir, ir, large); // inner arc
    }

    return path;
  }

  render() : any {
    // angles are provided in degrees
    const startangle = this.props.startangle;
    const endangle = this.props.endangle;
    // if (startangle - endangle === 0) {
    // return null;
    // }

    // radii are provided in pixels
    const innerradius = this.props.innerradius || 0;
    const outerradius = this.props.outerradius;

    const { originx, originy } = this.props;

    // sorted radii
    const ir = math.min(innerradius, outerradius);
    const or = math.max(innerradius, outerradius);

    let path;
    if (endangle >= startangle + 360) {
      path = this._createcirclepath(or, ir);
    } else {
      path = this._createarcpath(originx, originy, startangle, endangle, or, ir);
    }

    return <shape {...this.props} d={path} />;
  }
}

示例代码:

import react from 'react'
import {
  view,
  art
} from 'react-native'

const {surface} = art;
import wedge from './wedge'

export default class fan extends react.component{

  render(){

    return(
      <view style={this.props.style}>
        <surface width={100} height={100}>
          <wedge
           outerradius={50}
           startangle={0}
           endangle={60}
           originx={50}
           originy={50}
           fill="blue"/>

        </surface>
      </view>
    )
  }
}

综合示例

react-native之ART绘图方法详解

相关代码:

/**
 * sample react native app
 * https://github.com/facebook/react-native
 * @flow
 */

import react, {
  component
}from 'react';
import {
  art as art,
  stylesheet,
  view,
  dimensions,
  touchablewithoutfeedback,
  animated
} from 'react-native';

var heart_svg = "m130.4-0.8c25.4 0 46 20.6 46 46.1 0 13.1-5.5 24.9-14.2 33.3l88 153.6 12.5 77.3c-7.9-8.3-12.8-19.6-12.8-31.9 0-25.5 20.6-46.1 46-46.2 19.1 0 35.5 11.7 42.4 28.4c94.9 11 111.3-0.8 130.4-0.8"
var heart_color = 'rgb(226,38,77,1)';
var gray_heart_color = "rgb(204,204,204,1)";

var fill_colors = [
  'rgba(221,70,136,1)',
  'rgba(212,106,191,1)',
  'rgba(204,142,245,1)',
  'rgba(204,142,245,1)',
  'rgba(204,142,245,1)',
  'rgba(0,0,0,0)'
];

var particle_colors = [
  'rgb(158, 202, 250)',
  'rgb(161, 235, 206)',
  'rgb(208, 148, 246)',
  'rgb(244, 141, 166)',
  'rgb(234, 171, 104)',
  'rgb(170, 163, 186)'
]

getxyparticle = (total, i, radius) => {
  var angle = ( (2 * math.pi) / total ) * i;

  var x = math.round((radius * 2) * math.cos(angle - (math.pi / 2)));
  var y = math.round((radius * 2) * math.sin(angle - (math.pi / 2)));
  return {
    x: x,
    y: y,
  }
}

getrandomint = (min, max) => {
  return math.floor(math.random() * (max - min)) + min;
}

shufflearray = (array) => {
  for (var i = array.length - 1; i > 0; i--) {
    var j = math.floor(math.random() * (i + 1));
    var temp = array[i];
    array[i] = array[j];
    array[j] = temp;
  }
  return array;
}


var {
  surface,
  group,
  shape,
  path
} = art;

//使用animated.createanimatedcomponent对其他组件创建对话
//创建一个灰色的新型图片
var animatedshape = animated.createanimatedcomponent(shape);

var {
  width: devicewidth,
  height: deviceheight
} = dimensions.get('window');

export default class artanimview extends component {
  constructor(props) {
    super(props);

    this.state = {
      animation: new animated.value(0)
    };
  }

  explode = () => {
    animated.timing(this.state.animation, {
      duration: 1500,
      tovalue: 28
    }).start(() => {
      this.state.animation.setvalue(0);
      this.forceupdate();
    });
  }

  getsmallexplosions = (radius, offset) => {
    return [0, 1, 2, 3, 4, 5, 6].map((v, i, t) => {

      var scaleout = this.state.animation.interpolate({
        inputrange: [0, 5.99, 6, 13.99, 14, 21],
        outputrange: [0, 0, 1, 1, 1, 0],
        extrapolate: 'clamp'
      });

      var moveup = this.state.animation.interpolate({
        inputrange: [0, 5.99, 14],
        outputrange: [0, 0, -15],
        extrapolate: 'clamp'
      });

      var movedown = this.state.animation.interpolate({
        inputrange: [0, 5.99, 14],
        outputrange: [0, 0, 15],
        extrapolate: 'clamp'
      });

      var color_top_particle = this.state.animation.interpolate({
        inputrange: [6, 8, 10, 12, 17, 21],
        outputrange: shufflearray(particle_colors)
      })

      var color_bottom_particle = this.state.animation.interpolate({
        inputrange: [6, 8, 10, 12, 17, 21],
        outputrange: shufflearray(particle_colors)
      })

      var position = getxyparticle(7, i, radius)

      return (
        <group
          x={position.x + offset.x }
          y={position.y + offset.y}
          rotation={getrandomint(0, 40) * i}
        >
          <animatedcircle
            x={moveup}
            y={moveup}
            radius={15}
            scale={scaleout}
            fill={color_top_particle}
          />
          <animatedcircle
            x={movedown}
            y={movedown}
            radius={8}
            scale={scaleout}
            fill={color_bottom_particle}
          />
        </group>
      )
    }, this)
  }

  render() {
    var heart_scale = this.state.animation.interpolate({
      inputrange: [0, .01, 6, 10, 12, 18, 28],
      outputrange: [1, 0, .1, 1, 1.2, 1, 1],
      extrapolate: 'clamp'
    });

    var heart_fill = this.state.animation.interpolate({
      inputrange: [0, 2],
      outputrange: [gray_heart_color, heart_color],
      extrapolate: 'clamp'
    })

    var heart_x = heart_scale.interpolate({
      inputrange: [0, 1],
      outputrange: [90, 0],
    })

    var heart_y = heart_scale.interpolate({
      inputrange: [0, 1],
      outputrange: [75, 0],
    })

    var circle_scale = this.state.animation.interpolate({
      inputrange: [0, 1, 4],
      outputrange: [0, .3, 1],
      extrapolate: 'clamp'
    });

    var circle_stroke_width = this.state.animation.interpolate({
      inputrange: [0, 5.99, 6, 7, 10],
      outputrange: [0, 0, 15, 8, 0],
      extrapolate: 'clamp'
    });

    var circle_fill_colors = this.state.animation.interpolate({
      inputrange: [1, 2, 3, 4, 4.99, 5],
      outputrange: fill_colors,
      extrapolate: 'clamp'
    })

    var circle_opacity = this.state.animation.interpolate({
      inputrange: [1, 9.99, 10],
      outputrange: [1, 1, 0],
      extrapolate: 'clamp'
    })


    return (
      <view style={styles.container}>
        <touchablewithoutfeedback onpress={this.explode} style={styles.container}>
          <view style={{transform: [{scale: .8}]}}>
            <surface width={devicewidth} height={deviceheight}>
              <group x={75} y={200}>
                <animatedshape
                  d={heart_svg}
                  x={heart_x}
                  y={heart_y}
                  scale={heart_scale}
                  fill={heart_fill}
                />
                <animatedcircle
                  x={89}
                  y={75}
                  radius={150}
                  scale={circle_scale}
                  strokewidth={circle_stroke_width}
                  stroke={fill_colors[2]}
                  fill={circle_fill_colors}
                  opacity={circle_opacity}
                />

                {this.getsmallexplosions(75, {x: 89, y: 75})}
              </group>
            </surface>
          </view>
        </touchablewithoutfeedback>
      </view>
    );
  }
};

class animatedcircle extends component {
  render() {
    var radius = this.props.radius;
    var path = path().moveto(0, -radius)
      .arc(0, radius * 2, radius)
      .arc(0, radius * -2, radius)
      .close();
    return react.createelement(animatedshape);
  }
}

var styles = stylesheet.create({
  container: {
    flex: 1,
  }
});

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。