mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Support colors in AnimatedInterpolation on Android
Summary: Color support for AnimatedInterpolation was incomplete with native drivers, as only rgba type strings were supported. There was also an issue where color props instead a StyleAnimatedNode would never get applied. We were also potentially duplicating color parsing support, which is already centralized in `normalizeColor` / `processColor`. Changelog: [Android][Added] Enable AnimatedInterpolation to interpolate arbitrary color types. Reviewed By: mdvacca Differential Revision: D40571873 fbshipit-source-id: 41857ab0391279c5307bc31b855ea8fbcb4cccd8
This commit is contained in:
committed by
Facebook GitHub Bot
parent
b589123a3d
commit
e7dbfb2dbd
@@ -625,6 +625,7 @@ describe('Native Animated', () => {
|
||||
type: 'interpolation',
|
||||
inputRange: [10, 20],
|
||||
outputRange: [0, 1],
|
||||
outputType: null,
|
||||
extrapolateLeft: 'extend',
|
||||
extrapolateRight: 'extend',
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {PlatformConfig} from '../AnimatedPlatformConfig';
|
||||
import type AnimatedNode from './AnimatedNode';
|
||||
|
||||
import normalizeColor from '../../StyleSheet/normalizeColor';
|
||||
import processColor from '../../StyleSheet/processColor';
|
||||
import Easing from '../Easing';
|
||||
import NativeAnimatedHelper from '../NativeAnimatedHelper';
|
||||
import AnimatedWithChildren from './AnimatedWithChildren';
|
||||
@@ -377,19 +378,31 @@ export default class AnimatedInterpolation<
|
||||
super.__detach();
|
||||
}
|
||||
|
||||
__transformDataType(range: $ReadOnlyArray<OutputT>): Array<any> {
|
||||
return range.map(NativeAnimatedHelper.transformDataType);
|
||||
}
|
||||
|
||||
__getNativeConfig(): any {
|
||||
if (__DEV__) {
|
||||
NativeAnimatedHelper.validateInterpolation(this._config);
|
||||
}
|
||||
|
||||
// Only the `outputRange` can contain strings so we don't need to transform `inputRange` here
|
||||
let outputRange = this._config.outputRange;
|
||||
let outputType = null;
|
||||
if (typeof outputRange[0] === 'string') {
|
||||
// $FlowIgnoreMe[incompatible-cast]
|
||||
outputRange = ((outputRange: $ReadOnlyArray<string>).map(value => {
|
||||
const processedColor = processColor(value);
|
||||
if (typeof processedColor === 'number') {
|
||||
outputType = 'color';
|
||||
return processedColor;
|
||||
} else {
|
||||
return NativeAnimatedHelper.transformDataType(value);
|
||||
}
|
||||
}): any);
|
||||
}
|
||||
|
||||
return {
|
||||
inputRange: this._config.inputRange,
|
||||
// Only the `outputRange` can contain strings so we don't need to transform `inputRange` here
|
||||
outputRange: this.__transformDataType(this._config.outputRange),
|
||||
outputRange,
|
||||
outputType,
|
||||
extrapolateLeft:
|
||||
this._config.extrapolateLeft || this._config.extrapolate || 'extend',
|
||||
extrapolateRight:
|
||||
|
||||
+143
-88
@@ -8,11 +8,13 @@
|
||||
package com.facebook.react.animated;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.ReadableType;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -27,8 +29,10 @@ import java.util.regex.Pattern;
|
||||
public static final String EXTRAPOLATE_TYPE_CLAMP = "clamp";
|
||||
public static final String EXTRAPOLATE_TYPE_EXTEND = "extend";
|
||||
|
||||
private static final String fpRegex = "[+-]?(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?";
|
||||
private static final Pattern fpPattern = Pattern.compile(fpRegex);
|
||||
private static final Pattern sNumericPattern =
|
||||
Pattern.compile("[+-]?(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?");
|
||||
|
||||
private static final String COLOR_OUTPUT_TYPE = "color";
|
||||
|
||||
private static double[] fromDoubleArray(ReadableArray ary) {
|
||||
double[] res = new double[ary.size()];
|
||||
@@ -38,6 +42,44 @@ import java.util.regex.Pattern;
|
||||
return res;
|
||||
}
|
||||
|
||||
private static int[] fromIntArray(ReadableArray ary) {
|
||||
int[] res = new int[ary.size()];
|
||||
for (int i = 0; i < res.length; i++) {
|
||||
res[i] = ary.getInt(i);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
private static double[][] fromStringPattern(ReadableArray array) {
|
||||
int size = array.size();
|
||||
double[][] outputRange = new double[size][];
|
||||
|
||||
// Match the first pattern into a List, since we don't know its length yet
|
||||
Matcher m = sNumericPattern.matcher(array.getString(0));
|
||||
List<Double> firstOutputRange = new ArrayList<>();
|
||||
while (m.find()) {
|
||||
firstOutputRange.add(Double.parseDouble(m.group()));
|
||||
}
|
||||
double[] firstOutputRangeArr = new double[firstOutputRange.size()];
|
||||
for (int i = 0; i < firstOutputRange.size(); i++) {
|
||||
firstOutputRangeArr[i] = firstOutputRange.get(i).doubleValue();
|
||||
}
|
||||
outputRange[0] = firstOutputRangeArr;
|
||||
|
||||
for (int i = 1; i < size; i++) {
|
||||
double[] outputArr = new double[firstOutputRangeArr.length];
|
||||
|
||||
int j = 0;
|
||||
m = sNumericPattern.matcher(array.getString(i));
|
||||
while (m.find() && j < firstOutputRangeArr.length) {
|
||||
outputArr[j++] = Double.parseDouble(m.group());
|
||||
}
|
||||
outputRange[i] = outputArr;
|
||||
}
|
||||
|
||||
return outputRange;
|
||||
}
|
||||
|
||||
private static double interpolate(
|
||||
double value,
|
||||
double inputMin,
|
||||
@@ -110,6 +152,57 @@ import java.util.regex.Pattern;
|
||||
extrapolateRight);
|
||||
}
|
||||
|
||||
/*package*/ static int interpolateColor(double value, double[] inputRange, int[] outputRange) {
|
||||
int rangeIndex = findRangeIndex(value, inputRange);
|
||||
int outputMin = outputRange[rangeIndex];
|
||||
int outputMax = outputRange[rangeIndex + 1];
|
||||
if (outputMin == outputMax) {
|
||||
return outputMin;
|
||||
}
|
||||
|
||||
double inputMin = inputRange[rangeIndex];
|
||||
double inputMax = inputRange[rangeIndex + 1];
|
||||
if (inputMin == inputMax) {
|
||||
if (value <= inputMin) {
|
||||
return outputMin;
|
||||
}
|
||||
return outputMax;
|
||||
}
|
||||
|
||||
double ratio = (value - inputMin) / (inputMax - inputMin);
|
||||
return ColorUtils.blendARGB(outputMin, outputMax, (float) ratio);
|
||||
}
|
||||
|
||||
/*package*/ static String interpolateString(
|
||||
String pattern,
|
||||
double value,
|
||||
double[] inputRange,
|
||||
double[][] outputRange,
|
||||
String extrapolateLeft,
|
||||
String extrapolateRight) {
|
||||
int rangeIndex = findRangeIndex(value, inputRange);
|
||||
StringBuffer sb = new StringBuffer(pattern.length());
|
||||
|
||||
Matcher m = sNumericPattern.matcher(pattern);
|
||||
int i = 0;
|
||||
while (m.find() && i < outputRange[rangeIndex].length) {
|
||||
double val =
|
||||
interpolate(
|
||||
value,
|
||||
inputRange[rangeIndex],
|
||||
inputRange[rangeIndex + 1],
|
||||
outputRange[rangeIndex][i],
|
||||
outputRange[rangeIndex + 1][i],
|
||||
extrapolateLeft,
|
||||
extrapolateRight);
|
||||
int intVal = (int) val;
|
||||
m.appendReplacement(sb, intVal != val ? Double.toString(val) : Integer.toString(intVal));
|
||||
i++;
|
||||
}
|
||||
m.appendTail(sb);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static int findRangeIndex(double value, double[] ranges) {
|
||||
int index;
|
||||
for (index = 1; index < ranges.length - 1; index++) {
|
||||
@@ -120,70 +213,39 @@ import java.util.regex.Pattern;
|
||||
return index - 1;
|
||||
}
|
||||
|
||||
private enum OutputType {
|
||||
Number,
|
||||
Color,
|
||||
String,
|
||||
}
|
||||
|
||||
private final double mInputRange[];
|
||||
private final double mOutputRange[];
|
||||
private String mPattern;
|
||||
private double mOutputs[][];
|
||||
private final boolean mHasStringOutput;
|
||||
private final Matcher mSOutputMatcher;
|
||||
private final Object mOutputRange;
|
||||
private final OutputType mOutputType;
|
||||
private final @Nullable String mPattern;
|
||||
private final String mExtrapolateLeft;
|
||||
private final String mExtrapolateRight;
|
||||
private @Nullable ValueAnimatedNode mParent;
|
||||
private boolean mShouldRound;
|
||||
private int mNumVals;
|
||||
private Object mObjectValue;
|
||||
|
||||
public InterpolationAnimatedNode(ReadableMap config) {
|
||||
mInputRange = fromDoubleArray(config.getArray("inputRange"));
|
||||
ReadableArray output = config.getArray("outputRange");
|
||||
mHasStringOutput = output.getType(0) == ReadableType.String;
|
||||
if (mHasStringOutput) {
|
||||
/*
|
||||
* Supports string shapes by extracting numbers so new values can be computed,
|
||||
* and recombines those values into new strings of the same shape. Supports
|
||||
* things like:
|
||||
*
|
||||
* rgba(123, 42, 99, 0.36) // colors
|
||||
* -45deg // values with units
|
||||
*/
|
||||
int size = output.size();
|
||||
mOutputRange = new double[size];
|
||||
mPattern = output.getString(0);
|
||||
mShouldRound = mPattern.startsWith("rgb");
|
||||
mSOutputMatcher = fpPattern.matcher(mPattern);
|
||||
ArrayList<ArrayList<Double>> mOutputRanges = new ArrayList<>();
|
||||
for (int i = 0; i < size; i++) {
|
||||
String val = output.getString(i);
|
||||
Matcher m = fpPattern.matcher(val);
|
||||
ArrayList<Double> outputRange = new ArrayList<>();
|
||||
mOutputRanges.add(outputRange);
|
||||
while (m.find()) {
|
||||
Double parsed = Double.parseDouble(m.group());
|
||||
outputRange.add(parsed);
|
||||
}
|
||||
mOutputRange[i] = outputRange.get(0);
|
||||
}
|
||||
|
||||
// ['rgba(0, 100, 200, 0)', 'rgba(50, 150, 250, 0.5)']
|
||||
// ->
|
||||
// [
|
||||
// [0, 50],
|
||||
// [100, 150],
|
||||
// [200, 250],
|
||||
// [0, 0.5],
|
||||
// ]
|
||||
mNumVals = mOutputRanges.get(0).size();
|
||||
mOutputs = new double[mNumVals][];
|
||||
for (int j = 0; j < mNumVals; j++) {
|
||||
double[] arr = new double[size];
|
||||
mOutputs[j] = arr;
|
||||
for (int i = 0; i < size; i++) {
|
||||
arr[i] = mOutputRanges.get(i).get(j);
|
||||
}
|
||||
}
|
||||
if (COLOR_OUTPUT_TYPE.equals(config.getString("outputType"))) {
|
||||
mOutputType = OutputType.Color;
|
||||
mOutputRange = fromIntArray(output);
|
||||
mPattern = null;
|
||||
} else if (output.getType(0) == ReadableType.String) {
|
||||
mOutputType = OutputType.String;
|
||||
mOutputRange = fromStringPattern(output);
|
||||
mPattern = output.getString(0);
|
||||
} else {
|
||||
mOutputType = OutputType.Number;
|
||||
mOutputRange = fromDoubleArray(output);
|
||||
mSOutputMatcher = null;
|
||||
mPattern = null;
|
||||
}
|
||||
|
||||
mExtrapolateLeft = config.getString("extrapolateLeft");
|
||||
mExtrapolateRight = config.getString("extrapolateRight");
|
||||
}
|
||||
@@ -210,46 +272,39 @@ import java.util.regex.Pattern;
|
||||
@Override
|
||||
public void update() {
|
||||
if (mParent == null) {
|
||||
// The graph is in the middle of being created, just skip this
|
||||
// unattached node.
|
||||
// The graph is in the middle of being created, just skip this unattached node.
|
||||
return;
|
||||
}
|
||||
|
||||
double value = mParent.getValue();
|
||||
mValue = interpolate(value, mInputRange, mOutputRange, mExtrapolateLeft, mExtrapolateRight);
|
||||
if (mHasStringOutput) {
|
||||
// 'rgba(0, 100, 200, 0)'
|
||||
// ->
|
||||
// 'rgba(${interpolations[0](input)}, ${interpolations[1](input)}, ...'
|
||||
if (mNumVals > 1) {
|
||||
StringBuffer sb = new StringBuffer(mPattern.length());
|
||||
int i = 0;
|
||||
mSOutputMatcher.reset();
|
||||
while (mSOutputMatcher.find()) {
|
||||
double val =
|
||||
interpolate(value, mInputRange, mOutputs[i++], mExtrapolateLeft, mExtrapolateRight);
|
||||
if (mShouldRound) {
|
||||
// rgba requires that the r,g,b are integers.... so we want to round them, but we *dont*
|
||||
// want to
|
||||
// round the opacity (4th column).
|
||||
boolean isAlpha = i == 4;
|
||||
int rounded = (int) Math.round(isAlpha ? val * 1000 : val);
|
||||
String num =
|
||||
isAlpha ? Double.toString((double) rounded / 1000) : Integer.toString(rounded);
|
||||
mSOutputMatcher.appendReplacement(sb, num);
|
||||
} else {
|
||||
int intVal = (int) val;
|
||||
String num = intVal != val ? Double.toString(val) : Integer.toString(intVal);
|
||||
mSOutputMatcher.appendReplacement(sb, num);
|
||||
}
|
||||
}
|
||||
mSOutputMatcher.appendTail(sb);
|
||||
mAnimatedObject = sb.toString();
|
||||
} else {
|
||||
mAnimatedObject = mSOutputMatcher.replaceFirst(String.valueOf(mValue));
|
||||
}
|
||||
switch (mOutputType) {
|
||||
case Number:
|
||||
mValue =
|
||||
interpolate(
|
||||
value, mInputRange, (double[]) mOutputRange, mExtrapolateLeft, mExtrapolateRight);
|
||||
break;
|
||||
case Color:
|
||||
mObjectValue = Integer.valueOf(interpolateColor(value, mInputRange, (int[]) mOutputRange));
|
||||
break;
|
||||
case String:
|
||||
mObjectValue =
|
||||
interpolateString(
|
||||
mPattern,
|
||||
value,
|
||||
mInputRange,
|
||||
(double[][]) mOutputRange,
|
||||
mExtrapolateLeft,
|
||||
mExtrapolateRight);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getAnimatedObject() {
|
||||
return mObjectValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String prettyPrint() {
|
||||
return "InterpolationAnimatedNode[" + mTag + "] super: " + super.prettyPrint();
|
||||
}
|
||||
|
||||
@@ -102,7 +102,9 @@ import java.util.Map;
|
||||
((StyleAnimatedNode) node).collectViewUpdates(mPropMap);
|
||||
} else if (node instanceof ValueAnimatedNode) {
|
||||
Object animatedObject = ((ValueAnimatedNode) node).getAnimatedObject();
|
||||
if (animatedObject instanceof String) {
|
||||
if (animatedObject instanceof Integer) {
|
||||
mPropMap.putInt(entry.getKey(), (Integer) animatedObject);
|
||||
} else if (animatedObject instanceof String) {
|
||||
mPropMap.putString(entry.getKey(), (String) animatedObject);
|
||||
} else {
|
||||
mPropMap.putDouble(entry.getKey(), ((ValueAnimatedNode) node).getValue());
|
||||
|
||||
@@ -42,7 +42,14 @@ import java.util.Map;
|
||||
} else if (node instanceof TransformAnimatedNode) {
|
||||
((TransformAnimatedNode) node).collectViewUpdates(propsMap);
|
||||
} else if (node instanceof ValueAnimatedNode) {
|
||||
propsMap.putDouble(entry.getKey(), ((ValueAnimatedNode) node).getValue());
|
||||
Object animatedObject = ((ValueAnimatedNode) node).getAnimatedObject();
|
||||
if (animatedObject instanceof Integer) {
|
||||
propsMap.putInt(entry.getKey(), (Integer) animatedObject);
|
||||
} else if (animatedObject instanceof String) {
|
||||
propsMap.putString(entry.getKey(), (String) animatedObject);
|
||||
} else {
|
||||
propsMap.putDouble(entry.getKey(), ((ValueAnimatedNode) node).getValue());
|
||||
}
|
||||
} else if (node instanceof ColorAnimatedNode) {
|
||||
propsMap.putInt(entry.getKey(), ((ColorAnimatedNode) node).getColor());
|
||||
} else {
|
||||
|
||||
@@ -15,7 +15,6 @@ import com.facebook.react.bridge.ReadableMap;
|
||||
* library.
|
||||
*/
|
||||
/*package*/ class ValueAnimatedNode extends AnimatedNode {
|
||||
/*package*/ Object mAnimatedObject = null;
|
||||
/*package*/ double mValue = Double.NaN;
|
||||
/*package*/ double mOffset = 0;
|
||||
private @Nullable AnimatedNodeValueListener mValueListener;
|
||||
@@ -37,7 +36,7 @@ import com.facebook.react.bridge.ReadableMap;
|
||||
}
|
||||
|
||||
public Object getAnimatedObject() {
|
||||
return mAnimatedObject;
|
||||
return null;
|
||||
}
|
||||
|
||||
public void flattenOffset() {
|
||||
|
||||
+38
@@ -120,4 +120,42 @@ public class NativeAnimatedInterpolationTest {
|
||||
InterpolationAnimatedNode.EXTRAPOLATE_TYPE_IDENTITY))
|
||||
.isEqualTo(5);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInterpolateColor() {
|
||||
double[] input = new double[] {0, 1};
|
||||
int[] output = new int[] {0xFF000000, 0xFFFF0000};
|
||||
assertThat(InterpolationAnimatedNode.interpolateColor(0, input, output)).isEqualTo(0xFF000000);
|
||||
assertThat(InterpolationAnimatedNode.interpolateColor(0.5, input, output))
|
||||
.isEqualTo(0xFF7F0000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInterpolateString() {
|
||||
double[] input = new double[] {0, 1};
|
||||
double[][] output =
|
||||
new double[][] {
|
||||
new double[] {20, 20, 20, 80, 80, 80, 80, 20},
|
||||
new double[] {40, 40, 33, 60, 60, 60, 65, 40},
|
||||
};
|
||||
String pattern = "M20,20L20,80L80,80L80,20Z";
|
||||
assertThat(
|
||||
InterpolationAnimatedNode.interpolateString(
|
||||
pattern,
|
||||
0,
|
||||
input,
|
||||
output,
|
||||
InterpolationAnimatedNode.EXTRAPOLATE_TYPE_IDENTITY,
|
||||
InterpolationAnimatedNode.EXTRAPOLATE_TYPE_IDENTITY))
|
||||
.isEqualTo("M20,20L20,80L80,80L80,20Z");
|
||||
assertThat(
|
||||
InterpolationAnimatedNode.interpolateString(
|
||||
pattern,
|
||||
0.5,
|
||||
input,
|
||||
output,
|
||||
InterpolationAnimatedNode.EXTRAPOLATE_TYPE_IDENTITY,
|
||||
InterpolationAnimatedNode.EXTRAPOLATE_TYPE_IDENTITY))
|
||||
.isEqualTo("M30,30L26.5,70L70,70L72.5,30Z");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,25 @@ function AnimatedView({useNativeDriver}: {useNativeDriver: boolean}) {
|
||||
}),
|
||||
);
|
||||
|
||||
const animatedBaseValue = new Animated.Value(0);
|
||||
const interpolationAnimatedStyle = {
|
||||
backgroundColor: animatedBaseValue.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ['blue', 'red'],
|
||||
}),
|
||||
borderColor: animatedBaseValue.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ['orange', 'purple'],
|
||||
}),
|
||||
};
|
||||
animations.push(
|
||||
Animated.timing(animatedBaseValue, {
|
||||
toValue: 1,
|
||||
duration: 1000,
|
||||
useNativeDriver,
|
||||
}),
|
||||
);
|
||||
|
||||
const animatedFirstSpanTextStyle = {
|
||||
color: new Animated.Color('blue'),
|
||||
};
|
||||
@@ -81,7 +100,12 @@ function AnimatedView({useNativeDriver}: {useNativeDriver: boolean}) {
|
||||
}}>
|
||||
Press to animate
|
||||
</RNTesterButton>
|
||||
<Animated.View style={[styles.animatedView, animatedViewStyle]} />
|
||||
<View style={styles.boxes}>
|
||||
<Animated.View style={[styles.animatedView, animatedViewStyle]} />
|
||||
<Animated.View
|
||||
style={[styles.animatedView, interpolationAnimatedStyle]}
|
||||
/>
|
||||
</View>
|
||||
<Text style={styles.animatedText}>
|
||||
<Text>The </Text>
|
||||
<Animated.Text style={animatedFirstSpanTextStyle}>quick</Animated.Text>
|
||||
@@ -121,6 +145,7 @@ const styles = StyleSheet.create({
|
||||
height: 100,
|
||||
width: 100,
|
||||
borderWidth: 10,
|
||||
marginRight: 10,
|
||||
},
|
||||
animatedText: {
|
||||
fontSize: 20,
|
||||
@@ -130,6 +155,9 @@ const styles = StyleSheet.create({
|
||||
height: 100,
|
||||
width: 100,
|
||||
},
|
||||
boxes: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
});
|
||||
|
||||
export default ({
|
||||
|
||||
Reference in New Issue
Block a user