React Native 动画的 interpolate 函数理解

1,057 阅读3分钟

React Native 动画的 interpolate 函数理解

下列是react-native 中,编写动画交互的时候时常用到的一个方法。约定好输入和输出的范围,并根据输入的值和指定的缓动函数(easing)的来映射出新的值。非常适合用在组合、联动效果中。

直接来看源代码(源代码经过处理,此处省略了,inputRangestring 的情况)

理解的内容直接写在代码注释中

import invariant from './invariant';

export enum ExtrapolateType {
  extend = 'extend',
  identity = 'identity',
  clamp = 'clamp',
}

export type InterpolationConfigType = {
  inputRange: Array<number>;
  outputRange: Array<number>;
  easing?: (input: number) => number;
  extrapolate?: ExtrapolateType;
  extrapolateLeft?: ExtrapolateType;
  extrapolateRight?: ExtrapolateType;
};

function findRange(input: number, inputRange: Array<number>) {
  let i;
  for (i = 1; i < inputRange.length - 1; ++i) {
    if (inputRange[i] >= input) {
      break;
    }
  }
  return i - 1;
}

const linear = (t: number) => t;

function checkValidInputRange(arr: Array<number>) {
  invariant(arr.length >= 2, 'inputRange must have at least 2 elements');
  for (let i = 1; i < arr.length; ++i) {
    invariant(
      arr[i] >= arr[i - 1],
      /* $FlowFixMe[incompatible-type] (>=0.13.0) - In the addition expression
       * below this comment, one or both of the operands may be something that
       * doesn't cleanly convert to a string, like undefined, null, and object,
       * etc. If you really mean this implicit string conversion, you can do
       * something like String(myThing) */
      'inputRange must be monotonically non-decreasing ' + arr
    );
  }
}

function checkInfiniteRange(name: string, arr: Array<number>) {
  invariant(arr.length >= 2, name + ' must have at least 2 elements');
  invariant(
    arr.length !== 2 || arr[0] !== -Infinity || arr[1] !== Infinity,
    /* $FlowFixMe[incompatible-type] (>=0.13.0) - In the addition expression
     * below this comment, one or both of the operands may be something that
     * doesn't cleanly convert to a string, like undefined, null, and object,
     * etc. If you really mean this implicit string conversion, you can do
     * something like String(myThing) */
    name + 'cannot be ]-infinity;+infinity[ ' + arr
  );
}
/**
 * Very handy helper to map input ranges to output ranges with an easing
 * function and custom behavior outside of the ranges.
 */
function createInterpolation(
  config: InterpolationConfigType
): (input: number) => number | string {
  /**
   * 获取inputRange 和outputRange,注意这里的两个参数的数组长度必须一致
   * 并且inputRange 的数组的每一项数值是递增的
   *
   */
  const outputRange: Array<number> = config.outputRange as any;
  checkInfiniteRange('outputRange', outputRange);

  const inputRange = config.inputRange;
  checkInfiniteRange('inputRange', inputRange);
  checkValidInputRange(inputRange);

  /**
   * easing
   * 缓动函数,默认是linear 函数。
   * React Native 内置了一些常规的缓动函数,这里不讨论
   *
   */
  const easing = config.easing || linear;

  /**
   * extrapolate
   * 约定当输入的值超出inputRange 的范围时的表现
   *
   */
  let extrapolateLeft: ExtrapolateType = ExtrapolateType.extend;
  if (config.extrapolateLeft !== undefined) {
    extrapolateLeft = config.extrapolateLeft;
  } else if (config.extrapolate !== undefined) {
    extrapolateLeft = config.extrapolate;
  }

  let extrapolateRight: ExtrapolateType = ExtrapolateType.extend;
  if (config.extrapolateRight !== undefined) {
    extrapolateRight = config.extrapolateRight;
  } else if (config.extrapolate !== undefined) {
    extrapolateRight = config.extrapolate;
  }

  /**
   *
   * return
   * 使用的时候先使用createInterpolation 约定好输入与输出的映射关系,
   * 方法会返回一个方法(input: number) => number
   *
   */
  return (input) => {

		/**
	   * 
		 * findRange
	   * 如果inputRange 和outputRange 的数组不止两项,需要通过
	   * 这个方法来找到当前输入值所处于的inputRange 和outputRange 
	   *
	   */
    const range = findRange(input, inputRange);
    return interpolate(
      input,
      inputRange[range],
      inputRange[range + 1],
      outputRange[range],
      outputRange[range + 1],
      easing,
      extrapolateLeft,
      extrapolateRight
    );
  };
}

function interpolate(
  input: number,
  inputMin: number,
  inputMax: number,
  outputMin: number,
  outputMax: number,
  easing: (input: number) => number,
  extrapolateLeft: ExtrapolateType,
  extrapolateRight: ExtrapolateType
) {
  /**
   * input 就是想要映射的值,可以是手势移动的量,可以是滑动的偏移量
   *
   */
  let result = input;

  // Extrapolate
  if (result < inputMin) {
    if (extrapolateLeft === 'identity') {
      return result;
    } else if (extrapolateLeft === 'clamp') {
      result = inputMin;
    } else if (extrapolateLeft === 'extend') {
      // noop
    }
  }

  if (result > inputMax) {
    if (extrapolateRight === 'identity') {
      return result;
    } else if (extrapolateRight === 'clamp') {
      result = inputMax;
    } else if (extrapolateRight === 'extend') {
      // noop
    }
  }

  /**
   *
   * 如果outputMin 和outputMax 相等,直接返回outputMin
   */
  if (outputMin === outputMax) {
    return outputMin;
  }

  if (inputMin === inputMax) {
    if (input <= inputMin) {
      return outputMin;
    }
    return outputMax;
  }

  // Input Range
  if (inputMin === -Infinity) {
    result = -result;
  } else if (inputMax === Infinity) {
    result = result - inputMin;
  } else {
    /**
     *
     * 获取当前输入的值在[inputMin, inputMax] 这个分段中的比值
     */
    result = (result - inputMin) / (inputMax - inputMin);
  }

  /**
   *
   * 这里默认是linear 即输入等于输出
   */
  // Easing
  result = easing(result);

  // Output Range
  if (outputMin === -Infinity) {
    result = -result;
  } else if (outputMax === Infinity) {
    result = result + outputMin;
  } else {
    /**
     *
     * 获取上一步根据输入得到的其在输出区间内的位置
     */
    result = result * (outputMax - outputMin) + outputMin;
  }

  return result;
}

export default createInterpolation;

总结

因为inputRangeoutputRange 的范围和区间值是固定的,通过findRange 方法找到当前输入值对应的区间之后,拿到输入值对应输入区间的比例。就可以获取输出值。

再配合easingextrapolate 就可以得到丰富的交互效果。