閱讀111 返回首頁    go 阿裏雲 go 技術社區[雲棲]


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,
    }
});


最後更新:2017-08-13 22:39:17

  上一篇:go  FFmpeg任意文件讀取漏洞分析
  下一篇:go  企業如何選擇合適的雲應用開發平台?