React Native 仿抖音點贊特效

魚湯俠發表於2018-07-19

前言

任何一款應用無疑都或多或少的使用到動畫效果,它對於提升使用者體驗有著無比重要的作用。React Native同樣提供了豐富的動畫API供開發者呼叫,而對於此部分知識的掌握無疑是RN進階的必經之路,本文通過案例帶大家實踐掌握Animated、ART等動畫及繪圖知識。

Animated ART 手勢系統

實現的效果

React Native 仿抖音點贊特效
React Native 仿抖音點贊特效

動畫基礎

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. 最終效果如下

React Native 仿抖音點贊特效

案例2

相關文章