react-native-animatable 解析 (未完)

Github Source Code

最近開始學習 JavaScript, 趁著 trace 別人寫的 code 來認識基本語法; 本篇就從為了要實做一些動畫效果, 一邊觀察 react-native-animatable 這個模組來一邊學習動畫的操作。

Animation Reference @browniefed

關於動畫功能介紹與舉例皆滿詳盡的一本 GitBook。

ReactJS Component 生命週期

在介紹模組內容之前, 要先補齊一些對元件的基礎觀念。( Reference IReference II )

3 Stages

  • Mounting:元件正準備要被寫入 DOM
  • Updating:元件偵測到狀態的改變準備重新渲染。
  • Unmounting:元件正要被從 DOM 中移除。

    分別會用 Will、Did 來修飾被觸發的 Callback Function Name, 來代表在這些 Stage 發生前或後的關係。

Mounting Stage

  • getInitialState() / constructor():當物件被調用時此方法會在寫入 DOM 之前被觸發, 通常用來管理狀態的元件可以用這個方法初始化一些資料, constructor 的 naming 比較符合物件導向習慣的用語, 但是在 ES6 才支援。
  • componentWillMount:當元件內部的結構處理完畢 準備寫入 DOM 之前觸發
  • componentDidMount(DOMElement rootNode):當元件 被寫入 DOM 之後觸發

    當初始化需要操作 DOM 元素就可以用這個方法。

Updating Stage

  • componentWillReceiveProps(nextProps):已掛載的元件收到新的 props 時被觸發。

    在這個方法裡你通常會去比較 this.props 和 nextProps 然後再用 this.setState 去改變狀態。

  • shouldComponentUpdate(nextProps, nextState):這個函式需要回傳一個布林值, 當元件判斷是否需要更新 DOM 時會被觸發。

    你可以在這個方法裡面去比較 this.props、this.state、nextProps、nextState 來決定是否需要更新, 回傳 false 則會跳過此次觸發不更新, 如果你什麼都不回傳預設會當做 false。

  • componentWillUpdate:例如在上面 shouldComponentUpdate 你回傳了 true, 元件確定要更新了, 在準備更新前這個方法會被觸發。

  • componentDidupdate(prevProps, prevState, rootNode):更新後觸發。

Unmounting Stage

  • componentWillUnmount():當元件準備要被移除或破壞時觸發。

補充

  • 掛載後才能使用的方法
    • getDOMNode():使用此方法會傳回一個 DOM 元素物件, 透過這個方法你可以取得一個參考物件直接操作 DOM 節點。
    • forceUpdate():任何已掛載的元件, 當你知道元件內部有些狀態已經改變但他不是透過 this.setState() 去修改值的時候可以呼叫這個方法強迫更新。
  • componentDidMount() 和 componentDidUpdate() 的 rootNode 參數只是提供你一個比較方便的方式存取 DOM , 實際使用和 this.getDOMNode() 是一樣的。

react-native-animatable 的模組結構很單純, Source Code 的部分只有一個 index.js 檔案, 後面將對 Source Code 的部分拆分成幾個 Section (片段) 來介紹。

Code Section 1

// Transform an object to an array the way react native wants it for transform styles
// { a: x, b: y } => [{ a: x }, { b: y }]
function createKeyedArray(obj) {  
  return Object.keys(obj).map(key => {
    let keyed = {};
    keyed[key] = obj[key]; // Copy value
    return keyed;
  });
}

Goal: { a: x, b: y } => [{ a: x }, { b: y }]

語法複習

  • Object.keys(obj) 從 object 中取出所有的鍵值, 物件的屬性構成由 (key, value)
  • Array.prototype.map(Callback) 會將當前陣列的每個元素執行 Callback 回傳的結果, 集合成另一個陣列回傳
  • => fat arrow function (箭頭函式)
    • (param1, param2, …, paramN) => { statements }
    • (param1, param2, …, paramN) => expression // 等於 : => { return expression; }
    • singleParam => { statements } // 只有一個參數時,
    • () => { statements } //若無參數,就一定要加括號:

Code Section 2

// Helper function to calculate transform values, args:
// direction: in|out
// originOrDestination: up|down|left|right
// verticalValue: amplitude for up/down animations
// horizontalValue: amplitude for left/right animations
function getAnimationValueForDirection(direction, originOrDestination, verticalValue, horizontalValue) {  
  const isVertical = originOrDestination === 'up' || originOrDestination === 'down';
  const modifier = (isVertical && direction === 'out' ? -1 : 1) * (originOrDestination === 'down' || originOrDestination === 'left' ? -1 : 1);
  return modifier * (isVertical ? verticalValue : horizontalValue);
}

Code Section 3

const TRANSFORM_STYLE_PROPERTIES = [  
  'rotate',
  'rotateX',
  'rotateY',
  'rotateZ',
  'scale',
  'scaleX',
  'scaleY',
  'translateX',
  'translateY',
  'skewX',
  'skewY',
];

// Transforms { translateX: 1 } to { transform: [{ translateX: 1 }]}
function wrapStyleTransforms(style) {  
  let wrapped = {};
  Object.keys(style).forEach(key => {
    if (TRANSFORM_STYLE_PROPERTIES.indexOf(key) !== -1) {
      if (!wrapped.transform) {
        wrapped.transform = []; // Create new array.
      }
      wrapped.transform.push({
        [key]: style[key],
      });
    } else {
      wrapped[key] = style[key];
    }
  });
  return wrapped;
}

Goal: Transforms { translateX: 1, undefined: value } to { transform: [{ translateX: 1 }], undefined: value}

語法複習

  • forEach 如同 map 會反覆迭代陣列中的元素, 差別是不會回傳值
// Returns a flattened version of style with only `keys` values.
function getStyleValues(keys, style) {  
  if (!StyleSheet.flatten) {
    throw new Error('StyleSheet.flatten not available, upgrade React Native or polyfill with StyleSheet.flatten = require(\'flattenStyle\');');
  }
  let values = {};
  let flatStyle = Object.assign({}, StyleSheet.flatten(style));
  if (flatStyle.transform) {
    flatStyle.transform.forEach(transform => {
      const key = Object.keys(transform)[0];
      flatStyle[key] = transform[key];
    });
    delete flatStyle.transform;
  }

  (typeof keys === 'string' ? [keys] : keys).forEach(key => {
    values[key] = (key in flatStyle ? flatStyle[key] : getDefaultStyleValue(key));
  });
  return values;
}
  • StyleSheet.flatten Flattens an array of style objects, 將多個 style object 合併成單一有效的 style object (重複的元素會剔除掉無效的) Reference
  • Object.assign(target, ...sources) 回傳從 source 端複製後的 target object

    Object.assign 對於 object 中如果存在其他指向 object 的屬性, 會僅複製到 reference, 造成兩物件的物件屬性如果變化會產生連動關係, 可透過lodash 的 clonedeep 來解決。Reference IReference II

  • Property initializers ES6+ 的寫法之一是透過 static propType、static defaultProps 為 property 建立檢查的機制與預設值

  • ... 擴展運算符 (Spread Operator)

var str = 'hello';  
[...str] //  ['h','e','l','l','o']