React Native 动画的 interpolate 函数理解
下列是react-native 中,编写动画交互的时候时常用到的一个方法。约定好输入和输出的范围,并根据输入的值和指定的缓动函数(easing)的来映射出新的值。非常适合用在组合、联动效果中。
直接来看源代码(源代码经过处理,此处省略了,inputRange 为string 的情况)
理解的内容直接写在代码注释中
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;
总结
因为inputRange 和outputRange 的范围和区间值是固定的,通过findRange 方法找到当前输入值对应的区间之后,拿到输入值对应输入区间的比例。就可以获取输出值。
再配合easing 和extrapolate 就可以得到丰富的交互效果。