直接看代码吧
import React, { Component } from 'react'
import { View, Animated, Easing } from 'react-native'
import { PropTypes } from 'prop-types'
const extend = function (out) {
out = out || {}
for (var i = 1; i < arguments.length; i++) {
var obj = arguments[i]
if (!obj) {
continue
}
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === 'object') {
out[key] = extend(out[key], obj[key])
} else {
out[key] = obj[key]
}
}
}
}
return out
}
export default class Parabolic extends Component {
static propTypes = {
renderChildren: PropTypes.func,
animateEnd: PropTypes.func,
duration: PropTypes.number,
style: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.object),
PropTypes.object
])
}
static defaultProps = {
duration: 350,
animateEnd: function () { },
renderChildren: function () { }
}
constructor(props) {
super(props)
this.state = {
animateBtnX: 0,
animateBtnY: 0,
runAnim: new Animated.Value(0),
}
}
/**
* 设置组件参数
*/
setOptions (options) {
// 默认值
let defaults = {
version: '1.0.0',
autoPlay: true,
vertexRtop: 20, // 默认顶点高度top值
speed: 1.2,
start: {}, // top, left, width, height
end: {},
onEnd () { },
// 抛点自定义结束事件
customOnEnd () { }
}
this.settings = extend({}, defaults, options)
let settings = this.settings
let { start, end } = settings
// 运动轨迹最高点top值
let vertexTop = Math.min(start.top, end.top) - Math.abs(start.left - end.left) / 3
if (vertexTop < settings.vertexRtop) {
// 可能出现起点或者终点就是运动曲线顶点的情况
vertexTop = Math.min(settings.vertexRtop, Math.min(start.top, end.top))
}
/**
* ======================================================
* 运动轨迹在页面中的top值可以抽象成函数 y = a * x*x + b;
* a = curvature
* b = vertexTop
* ======================================================
*/
let distance = Math.sqrt(Math.pow(start.top - end.top, 2) + Math.pow(start.left - end.left, 2))
// 元素移动次数
let steps = Math.ceil(Math.min(Math.max(Math.log(distance) / 0.05 - 75, 30), 100) / settings.speed)
let ratio = start.top === vertexTop ? 0 : -Math.sqrt((end.top - vertexTop) / (start.top - vertexTop))
let vertexLeft = (ratio * start.left - end.left) / (ratio - 1)
// 特殊情况,出现顶点left==终点left,将曲率设置为0,做直线运动。
let curvature = end.left === vertexLeft ? 0 : (end.top - vertexTop) / Math.pow(end.left - vertexLeft, 2)
extend(settings, {
count: -1, // 每次重置为-1
steps: steps,
vertex_left: vertexLeft,
vertex_top: vertexTop,
curvature: curvature
})
}
move () {
let [inputRange = [], outputX = [], outputY = []] = []
inputRange.push(outputX.length)
outputX.push(this.settings.start.left)
outputY.push(this.settings.start.top)
let step = () => {
let settings = this.settings
let { start, count, steps, end } = settings
// 计算left top值
let left = start.left + (end.left - start.left) * count / steps
let top = settings.curvature === 0 ? start.top + (end.top - start.top) * count / steps : settings.curvature * Math.pow(left - settings.vertex_left, 2) + settings.vertex_top
settings.count++
if (count !== steps) {
if (count > 0) {
inputRange.push(outputX.length)
outputX.push(left)
outputY.push(top)
}
step()
}
}
step()
return { inputRange, outputX, outputY }
}
run (options = {}, data = {}) {
this.state.runAnim.setValue(0)
this.setOptions(options)
let { inputRange, outputX, outputY } = this.move()
if (inputRange.length === 1) {
inputRange.push(0)
}
if (outputX.length === 1) {
outputX.push(0)
}
if (outputY.length === 1) {
outputY.push(0)
}
this.setState({
animateBtnX: this.state.runAnim.interpolate({
inputRange, outputRange: outputX
}),
animateBtnY: this.state.runAnim.interpolate({
inputRange, outputRange: outputY
})
})
Animated.timing(this.state.runAnim, {
toValue: inputRange.length,
duration: this.props.duration,
useNativeDriver: true,
easing: Easing.linear
}).start(() => { this.props.animateEnd(data) })
}
render () {
return (
<Animated.View style={[this.props.style, {
transform: [
{ translateX: this.state.animateBtnX },
{ translateY: this.state.animateBtnY }
]
}]}>
{this.props.renderChildren()}
</Animated.View>
)
}
}
定义封装
import React, { Component } from 'react'
import { StyleSheet, View, Text, DeviceEventEmitter } from 'react-native'
import Parabolic from './Parabolic'
const eventconfig = { PARABOLA: 'PARABOLA' }
// 定义最多5个小球
const max = 5
/**
* 抛物线
*/
export default class ParabolicBase extends Component {
constructor(props) {
super(props)
this.state = {
balls: []
}
}
componentDidMount () {
// 新增数据支持最多5个小球
let balls = [...this.state.balls]
data = {}
for (let i = 0; i < max; i++) {
balls.push({ id: i, key: `parabolic${i}` })
let key = `${i}_hide`
data[key] = new Animated.Value(0)
}
data.balls = balls
this.setState({
...data
})
// 赋值正在运行的id
this.runIds = []
this.listener = DeviceEventEmitter.addListener(eventconfig.PARABOLA, (params) => this._handle(params))
}
// 处理动画
_handle (params) {
let balls = this.state.balls
let runIds = this.runIds
// 查询空闲的小球
let findIndex = balls.findIndex(v => runIds.indexOf(v.id) === -1)
if (findIndex === -1) {
// 都不在空闲状态
return
}
this.runIds.push(findIndex)
this.state[`${balls[findIndex].id}_hide`].setValue(1)
let key = balls[findIndex].key
let { startX, startY, endX, endY } = params
let options = {
start: {
left: startX,
top: startY
},
end: {
left: endX,
top: endY
},
speed: 3.1
}
this.refs[key].run(options, { id: balls[findIndex].id })
}
// 结束回调
animateEnd (data) {
let runIds = this.runIds || []
if (runIds.length > 0) {
let findIndex = runIds.findIndex(v => v === data.id)
if (findIndex !== -1) {
this.state[`${data.id}_hide`].setValue(0)
// 移除正在运行的
this.runIds.splice(findIndex, 1)
}
}
}
render () {
let { balls = [] } = this.state
return (
<View style={[styles.main]}>
{
balls.map((v, i) => {
return <Parabolic
key={i}
ref={v.key}
style={{ position: 'absolute' }}
renderChildren={() => {
return <Animated.View style={{ backgroundColor: "#f00", width: 28, height: 28, borderRadius: 28 / 2 ,opacity: this.state[`${v.id}_hide`]}}></Animated.View>
}}
animateEnd={(data) => this.animateEnd(data)}
duration={6000}
/>
})
}
</View>
)
}
componentWillUnmount () {
// Warning Can't perform a react state update on an unmounted component
// https://blog.csdn.net/twodogya/article/details/86620289
this.setState = (state, callback) => {
return
}
this.listener && this.listener.remove()
}
}
const styles = StyleSheet.create({
main: {
width: 1,
height: 1,
position: 'absolute',
zIndex: 9999,
backgroundColor: '#00f',
top: 0,
left: 0
}
})
调用
import React, { Component } from 'react'
import { StyleSheet, View, Text, DeviceEventEmitter } from 'react-native'
import ParabolicBase from './ParabolicBase'
export default class Main extends Component {
render () {
return (
<View style={{ flex: 1, backgroundColor: 'white' }}>
<ParabolicBase />
<Text
onPress={(event) => {
let { pageX, pageY, locationX, locationY } = event.nativeEvent
let startX = pageX - locationX
let startY = pageY - locationY
console.log(event.nativeEvent)
// startX = 300
// startY = 200
DeviceEventEmitter.emit('PARABOLA', { startX, startY, endX: 65, endY: 736 })
}}
>调用了</Text>
</View>
)
}
}