react-native之ART绘图方法详解
背景
在移动应用的开发过程中,绘制基本的二维图形或动画是必不可少的。然而,考虑到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' 添加;
2、选中项目根目录 ——> 点击'build phases‘ ——> 点击‘link binary with libraries' ——> 点击左下方‘+' ——> 选中‘libart.a'添加。
基础组件
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() : 封闭空间
代码示例
绘制直线
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像素空白
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做颜色填充.
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)的使用, 终点坐标距离起点坐标的相对距离。
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没看源码。
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', }, });
绘制扇形
在这里需要使用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> ) } }
综合示例
相关代码:
/** * 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, } });
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。