React Native 仿抖音点赞特效

·  阅读 2885

前言

任何一款应用无疑都或多或少的使用到动画效果,它对于提升用户体验有着无比重要的作用。React Native同样提供了丰富的动画API供开发者调用,而对于此部分知识的掌握无疑是RN进阶的必经之路,本文通过案例带大家实践掌握Animated、ART等动画及绘图知识。

Animated ART 手势系统

实现的效果

动画基础

RN目前已更新至v0.56,其动画API也在不断的丰富,其中以Animated为主要核心,集中了动画创建、执行(组合)、运算(插值)、事件等功能。

创建动画值对象

this.anim1 = new Animated.Value(0) // 用于单个值
this.anim2 = new Animated.ValueXY({x: 0, y: 0}) // 用于矢量值
复制代码

执行与组合

  1. 所谓动画的执行,实质是改变动画对象的值,我们可以通过this.anim1.setValue(1)方法直接赋值,也可以利用Animated.timing Animated.spring Animated.decay等方法以动画的方式运行,以timing为例:
Animated.timing(
    this.anim1,  // 定义的动画值对象
    {
        toValue: 1, // 执行到的动画值
        duration: 300, // 持续时间
        easing: Easing.bounce, // Easing类提供了多种动画效果, 须注意属性和函数的区别
        isInteraction: true, // 是否在InteractionManager创建一个interaction handle,此动画可以加入同步队列,完成之后会执行runAfterInteractions函数
        useNativeDriver:true // 使用原生动画驱动,启动动画前将所有配置信息发送至原生端,之后利用原生代码在UI线程执行动画,无需两端沟通,脱离JS线程,更加流畅。
    }
).start() // 调用start方法开始执行

复制代码
  1. 同时也可以通过parallel(同时执行)、sequence(顺序执行)、loop(循环执行)、stagger和delay等方法组合动画,例如:
Animated.sequence([            // 首先执行decay动画,结束后同时执行spring和twirl动画
  Animated.decay(position, {   // 滑行一段距离后停止
    velocity: {x: gestureState.vx, y: gestureState.vy}, // 根据用户的手势设置速度
    deceleration: 0.997,
  }),
  Animated.parallel([          // 在decay之后并行执行:
    Animated.spring(position, {
      toValue: {x: 0, y: 0}    // 返回到起始点开始
    }),
    Animated.timing(twirl, {   // 同时开始旋转
      toValue: 360,
    }),
  ]),
]).start();                    // 执行这一整套动画序列
复制代码

运算与插值

  1. Animated提供了对动画值的加、减、乘、除、取模等运算。
Animated.add(a, b) // 加
Animated.subtract(a, b) // 减 v0.56新增
Animated.multiply(a, b) // 乘
Animated.divide(a, b) // 除
Animated.modulo(a, modulus) // 取模
Animated.diffClamp(a, min, max) // 返回一个介于min和max之间动画值
复制代码
  1. 插值也属于运算的一种方式,可以使用动画值对象的interpolate方法,进行输入与输出的映射. 例如:
this.anim1.interpolate({
  inputRange: [0, 1],
  outputRange: [0, 100], // 将动画0-1值映射为0-100进行输出
});
复制代码

组件

动画必须作用在特定组件之上,目前Animated封装了Image、ScrollView、Text、View组件并导出,可以直接通过Animated.XXX的形式使用。对于自定义组件,可以通过createAnimatedComponent方法创建,具体使用见案例部分。

事件

对于连续调用的事件,比如使用手势进行滚动、平移、缩放等操作,可以通过Animated.event进行结构化映射,从复杂的事件中提取值并映射到动画值对象,自动完成setValue方法的调用,例如:

onScroll={Animated.event(
   [{ nativeEvent: {
        contentOffset: {
          x: this.scrollAnimX // scrollAnimX为创建的动画对象, 映射到e.nativeEvent.contentOffset.x的值
        }
      }
    }]
 )}
复制代码

绘图基础

ART

ART(iOS端需预先引入)是RN提供的绘图API,主要涵盖了以下内容:

const {
  Surface,
  Shape,
  Group,
  Text,
  Path,
  ClippingRectangle,
  LinearGradient,
  RadialGradient,
  Pattern,
  Transform
} = React.ART
复制代码

同普通组件使用一致,通过设置属性的方式绘制图形,重点掌握Path类对路径的绘制,这里不再赘述,点击了解更多

一些插件

  1. d3-shape 更加方便的绘制路径
  2. d3-scale 实现抽象的数据映射
  3. react-native-svg 对于熟悉Svg开发的同学推荐使用

案例1

接下来开始我们的编码工作,首先实现抖音的双击点赞特效

原理解析

利用手势系统监听双击事件,拿到当前触摸点的坐标值并创建心形组件,组件内部执行放大并降低透明度的动画,动画完毕后移除组件。

实现代码

  1. 单独创建一个心形组件,其内部完成动画效果

const ROTATE_ANGLE = ['-35deg','-25deg','0deg','25deg','35deg']

class AnimHeart extends Component{
  constructor(props){
    super(props)

    // 创建一个动画值对象,并使用插值运算实现透明度和缩放的效果
    this.anim = new Animated.Value(0)

    // 设置随机旋转角度
    this.rotateValue = ROTATE_ANGLE[Math.floor(Math.random()*4)]

  }
  render(){
    const {x, y} = this.props
    
    return <Animated.Image
        style={{
            position:'absolute',
            left: x,
            top: y,
            opacity: this.anim.interpolate({
              inputRange:[0, 1, 2],
              outputRange:[1, 1, 0] // 根据动画值0-1-2的变化,调整透明度
            }),
            transform: [{
              scale: this.anim.interpolate({
                inputRange: [0, 1, 2],
                outputRange: [1, 0.8, 2] // 根据动画值0-1-2的变化,调整缩放比例
              })
            },{
              rotate: this.rotateValue
            }]
        }}
        source={require('./cc-heart.png')} 
    />
  }
  
  componentDidMount(){
    // 使用顺序执行动画函数
    Animated.sequence([
      
        // 使用弹簧动画函数
        Animated.spring(
            this.anim,
            {
                toValue: 1,
                useNativeDriver: true, // 使用原生驱动
                bounciness: 5 // 设置弹簧比例
            }
        ),

        // 使用定时动画函数
        Animated.timing(
            this.anim,
            {
                toValue: 2,
                useNativeDriver: true
            }
        )
    ]).start(()=>{
        // 动画完成后回调
        this.props.onEnd && this.props.onEnd()
    })
  }

  componentWillUnmount(){
    //console.warn('unmount')
  }

  // 禁止该组件重新渲染,提升性能
  shouldComponentUpdate(){
    return false
  }
}

/*
注意: 
动画序列中,如果第一个动画中的useNativeDriver设置为true,
此时动画便交于原生端进行执行,不可再切换为JS驱动,后续动画的useNativeDriver也必须设置为true
*/
复制代码
  1. 创建手势,检测双击事件,根据坐标点渲染AnimHeart
class App extends Component {
  constructor(props){
    super(props)
    
    this.state = {
        heartList: []
    }
    this.tapStartTime = null
    this._panResponder = PanResponder.create({
        onStartShouldSetPanResponder: (evt, gestureState) => true,
        onPanResponderGrant: this._onPanResponderGrant
    })
  }
  
  _onPanResponderGrant = (ev)=>{
    if(!this.isDoubleTap()) return
    const { pageX, pageY } = ev.nativeEvent
    
    // 设置位置数据,渲染AnimHeart组件
    this.setState(({heartList})=>{
        heartList.push({
            x: pageX - 60,
            y: pageY - 60,
            key: shortid.generate() // 使用shortid生成唯一的key值
        })
        return {
            heartList
        }
    })
  }
  
  // 检测是否为双击
  isDoubleTap(){
    const curTime = +new Date()
    if(!this.tapStartTime || curTime - this.tapStartTime > 300) {
        this.tapStartTime = curTime
        return false
    }
    this.tapStartTime = null
    return true
  }
  
  render() {
    return (
      <View 
        {...this._panResponder.panHandlers}
        style={styles.container}>
        {
          this.state.heartList.map(({x, y, key}, index)=>{
            return <AnimHeart 
              onEnd={()=>{
                // 动画完成后销毁组件
                this.setState(({heartList})=>{
                    heartList.splice(index,1)
                    return {
                        heartList
                    }
                })
              }} 
              key={key} // 不要使用index作为key值
              x={x} 
              y={y} 
            />
          })
        }
      </View>
    );
  }
}
复制代码
  1. 最终效果如下

案例2