RN 加入购物车抛物线动画

2 篇文章 0 订阅
2 篇文章 0 订阅

直接看代码吧

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>
    )
  }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值