- 本文為 Marno 翻譯,轉載必須保留出處!
- 公眾號【 aMarno 】,關注後回覆 RN 加入交流群
- React Native 優秀開源專案大全:www.marno.cn
一、前言
在移動應用中製作折線圖表是一件具有挑戰性的事。本文將會教你如何只用 Component 和 StyleSheet 在 React Native 中製作一個折線圖。
我們參考的是 《 Let’s drawing charts in React-Native without any library 》(需翻Q), 他介紹瞭如何在不引入三方庫的情況下,在 React Native 中繪製柱狀圖和條形圖。雖然在 react-native-chart這個庫中已經有折線圖了, 然而,今天我們要來定製我們自己的。
二、開始動手
首先,我們必須先繪製背景,為了顯示水平軸,第一步要先繪製一些數字和直線。程式碼如下:
import React from 'react';
import { View, StyleSheet, Text } from 'react-native';
export default function LevelSeparator({ label, height }) {
return (
<View style={[styles.container, { height }]}>
<Text style={styles.label}>
{label.toFixed(0)}
</Text>
<View style={styles.separatorRow}/>
</View>
);
}
LevelSeparator.propTypes = {
label: React.PropTypes.number.isRequired,
height: React.PropTypes.number.isRequired
};
export const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center'
},
label: {
textAlign: 'right',
width: 20
},
separatorRow: {
width: 250,
height: 1,
borderWidth: 0.5,
borderColor: 'rgba(0,0,0,0.3)',
marginHorizontal: 5
}
});複製程式碼
我們新增了一個 height 屬性,因為我們會在下一步用到它。
然後使用上面封裝好的直線元件,得到下圖 1。程式碼如下:
export default class lineChartExample extends Component {
render() {
return (
<View style={styles.container}>
<LevelSeparator height={30} label={10} />
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
height: 100
}
});複製程式碼
三、繪製背景
重複使用
import React from 'react';
import { View, StyleSheet } from 'react-native';
import LevelSeparator from './LevelSeparator';
export const range = (n) => {
return [...Array(n).keys()];
};
function createSeparator(totalCount, topValue, index, height) {
return (
<LevelSeparator
key={index}
label={topValue * (totalCount - index) / totalCount}
height={height / totalCount}
/>
);
}
function SeparatorsLayer({ topValue, separators, height, children, style }) {
return (
<View style={[styles.container, style]}>
{range(separators + 1).map((separatorNumber) => {
return createSeparator(separators, topValue, separatorNumber, height);
})}
{children}
</View>
);
}
SeparatorsLayer.propTypes = {
topValue: React.PropTypes.number.isRequired,
separators: React.PropTypes.number.isRequired,
height: React.PropTypes.number.isRequired
};
const styles = StyleSheet.create({
container: {
position: 'absolute'
}
});
export default SeparatorsLayer;複製程式碼
請注意下,這裡的接收到的 height 屬性,是如何傳遞給我們之前的那個
至於 label 值的計算,這裡給出一個計算公式 topValue * (totalCount - index) / totalCount
,需要注意的是 index 是從上到下排的序,下標從 0 開始。
使用一下上面程式碼中封裝好的元件。(這裡注意一下元件在傳遞的過程中名字發生了變化,如果沒有看懂,可以多看幾遍)
export default class lineChartExample extends Component {
render() {
return (
<View style={styles.container}>
<SeparatorsLayer topValue={10} separators={5} height={100} />
</View>
);
}
}複製程式碼
這裡設定: topValue 為 10 ,separators 為 5 ,計算得到的步距就是 10 / 5 = 2。最終呈現的結果如下圖:
四、新增資料
現在來到了比較棘手的部分,在剛剛繪製好的背景上,繪製折線圖所需的 點 和 折線。這裡我們將會用到 Point 和 代數運算。
import React from 'react';
export const Point = (x, y) => {
return { x, y };
};
export const dist = (pointA, pointB) => {
return Math.sqrt(
(pointA.x - pointB.x) * (pointA.x - pointB.x) +
(pointA.y - pointB.y) * (pointA.y - pointB.y)
);
};
export const diff = (pointA, pointB) => {
return Point(pointB.x - pointA.x, pointB.y - pointA.y);
};
export const add = (pointA, pointB) => {
return Point(pointA.x + pointB.x, pointA.y + pointB.y);
};
export const angle = (pointA, pointB) => {
const euclideanDistance = dist(pointA, pointB);
if (!euclideanDistance) {
return 0;
}
return Math.asin((pointB.y - pointA.y) / euclideanDistance);
};
export const pointPropTypes = {
x: React.PropTypes.number.isRequired,
y: React.PropTypes.number.isRequired
};複製程式碼
在渲染時對映我們的 point 列表,這將有助於防止出現渲染警告。
export const keyGen = (serializable, anotherSerializable) => {
return `${JSON.stringify(serializable)}-${JSON.stringify(anotherSerializable)}`;
};複製程式碼
接下來是有爭議的模組,我們將重新測量我們的 points:
import { Point } from './pointUtils';
export const startingPoint = Point(-20 , 8);
const endingPoint = Point(242, 100);
export function vectorTransform(point, maxValue, scaleCount) {
return Point(
point.x * (endingPoint.x / scaleCount) + endingPoint.x / scaleCount,
point.y * (endingPoint.y / maxValue)
);
}複製程式碼
startingPoint 和 endingPoint 的意義是什麼呢?
這些點分別代表的是我們所用到的 layer 內的 (0,0)和(MAX-X,MAX-Y)座標點。
scaleCount 只是為了幫助我們調整 X 軸的大小。
The scaleCount simply helps to resize the X-Axis (實現這一目的的另一種方法是處理 X 軸的最大值, 並且在座標之間進行類似的計算)。
五、折線圖成型
為了繪製 points ,我們需要:
export const createPoint = (coordinates, color, size = 8) => {
return {
backgroundColor: color,
left: coordinates.x - 3,
bottom: coordinates.y - 2,
position: 'absolute',
borderRadius: 50,
width: size,
height: size
};
};複製程式碼
我們通過 (-3,-2)定位我們的中心點座標,這些值取決於點的大小,更準確的說,是點的半徑。
export const createLine = (dist, angle, color, opacity, startingPoint) => {
return {
backgroundColor: color,
height: 4,
width: dist,
bottom: dist * Math.sin(angle) / 2 + startingPoint.y,
left: -dist * (1 - Math.cos(angle)) / 2 + startingPoint.x,
position: 'absolute',
opacity,
transform: [
{ rotate: `${(-1) * angle} rad` }
]
};
};複製程式碼
starting point 有助於在螢幕上移動我們的 line。這個初始點將很方便的連線它們之間的點:我們只需要簡單的將上一個點作為直線的起點即可。
為此,我們必須需要接收一個指定的距離和角度才能繪製折線。可能出現的一個問題是 Transform API 按照順時針旋轉,但是我們計算了 Z 軸正軸上的值,即逆時針方向的值。因此我們需要使用於此角度相反的值。
這裡遇到的另一個問題是,如果我們旋轉一個 View ,我們將需要確保旋轉中心是從當前 line 的起點開始的。這個 API 方法對 View 的旋轉是以該元件的中心點為軸心旋轉的,換句話說,我們需要將旋轉中心改為 line 的起點。你可以在這裡看到關於這部分的完整程式碼(公眾號使用者點選原文閱讀):gist.github.com/mvbattan/2c…
至此,我們已經完成了以下內容,如圖 3 。
mport SeparatorsLayer from './SeparatorsLayer';
import PointsPath from './PointsPath';
import { Point } from './pointUtils';
import { startingPoint, vectorTransform } from './Scaler';
const lightBlue = '#40C4FE';
const green = '#53E69D';
const lightBluePoints = [Point(0, 0), Point(1, 2), Point(2, 3), Point(3, 6), Point(5, 6)];
const greenPoints = [Point(0, 2), Point(3, 4), Point(4, 0), Point(5, 10)];
const MAX_VALUE = 10;
const Y_LEVELS = 5;
const X_LEVELS = 5;
export default class lineChartExample extends Component {
render() {
return (
<View style={styles.container}>
<SeparatorsLayer topValue={MAX_VALUE} separators={Y_LEVELS} height={100}>
<PointsPath
color={lightBlue}
pointList={lightBluePoints.map(
(point) => vectorTransform(point, MAX_VALUE, X_LEVELS)
)}
opacity={0.5}
startingPoint={startingPoint}
/>
<PointsPath
color={green}
pointList={greenPoints.map(
(point) => vectorTransform(point, MAX_VALUE, X_LEVELS)
)}
opacity={0.5}
startingPoint={startingPoint}
/>
</SeparatorsLayer>
</View>
);
}
}複製程式碼
六、迭代內容
回顧一下我們上文中提到的有爭議的模組,Scaler.js,一旦我們完成了這些 points 和 lines 的繪製,我們需要校準 startingPoint 和 endingPoint 。為此,我們準備了一個簡單的試錯過程(如果你發現了自動完成詞步驟的方法,請一定要告訴我!)。
七、幾乎完成
最終,我們很簡單的給 X 軸加上了座標,具體程式碼如下。(實現效果如圖 4)。原始碼地址在這裡:gist.github.com/mvbattan/e2…
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View
} from 'react-native';
import SeparatorsLayer from './SeparatorsLayer';
import PointsPath from './PointsPath';
import { Point } from './pointUtils';
import { startingPoint, vectorTransform } from './Scaler';
const lightBlue = '#40C4FE';
const green = '#53E69D';
const MAX_VALUE = 10;
const Y_LEVELS = 5;
const X_LEVELS = 5;
const lightBluePoints = [Point(0, 0), Point(1, 2), Point(2, 3), Point(3, 6), Point(5, 6)];
const greenPoints = [Point(0, 2), Point(3, 4), Point(4, 0), Point(5, 10)];
export default class lineChartExample extends Component {
render() {
return (
<View style={styles.container}>
<SeparatorsLayer topValue={MAX_VALUE} separators={Y_LEVELS} height={100}>
<PointsPath
color={lightBlue}
pointList={lightBluePoints.map(
(point) => vectorTransform(point, MAX_VALUE, X_LEVELS)
)}
opacity={0.5}
startingPoint={startingPoint}
/>
<PointsPath
color={green}
pointList={greenPoints.map(
(point) => vectorTransform(point, MAX_VALUE, X_LEVELS)
)}
opacity={0.5}
startingPoint={startingPoint}
/>
</SeparatorsLayer>
<View style={styles.horizontalScale}>
<Text>0</Text>
<Text>1</Text>
<Text>2</Text>
<Text>3</Text>
<Text>4</Text>
<Text>5</Text>
</View>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
height: 100
},
horizontalScale: {
flexDirection: 'row',
justifyContent: 'space-around',
marginTop: 150,
marginLeft: 20,
width: 290
}
});
AppRegistry.registerComponent('lineChartExample', () => lineChartExample);複製程式碼
八、結語
關於 React Native 自定義元件的好文章比較少,我覺得這就是一篇不錯的文章,看完以後覺得整體思路還是比較簡單的。非常適合初學者學習 React Native 自定義元件,當然結合文中的原始碼練習一下是比較好的。原始碼地址:gist.github.com/mvbattan
本文原作者說會在後續的文章中會介紹如對該折線圖新增動畫。如果文章更新了,我也會第一時間同步過來的。