Skip to content

Commit

Permalink
Support colors in AnimatedInterpolation on Android
Browse files Browse the repository at this point in the history
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
  • Loading branch information
javache authored and facebook-github-bot committed Nov 30, 2022
1 parent b589123 commit e7dbfb2
Show file tree
Hide file tree
Showing 8 changed files with 242 additions and 99 deletions.
1 change: 1 addition & 0 deletions Libraries/Animated/__tests__/AnimatedNative-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,7 @@ describe('Native Animated', () => {
type: 'interpolation',
inputRange: [10, 20],
outputRange: [0, 1],
outputType: null,
extrapolateLeft: 'extend',
extrapolateRight: 'extend',
},
Expand Down
25 changes: 19 additions & 6 deletions Libraries/Animated/nodes/AnimatedInterpolation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -27,8 +29,10 @@
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()];
Expand All @@ -38,6 +42,44 @@ private static double[] fromDoubleArray(ReadableArray ary) {
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,
Expand Down Expand Up @@ -110,6 +152,57 @@ private static double interpolate(
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++) {
Expand All @@ -120,70 +213,39 @@ private static int findRangeIndex(double value, double[] ranges) {
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");
}
Expand All @@ -210,46 +272,39 @@ public void onDetachedFromNode(AnimatedNode parent) {
@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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ public final void updateView() {
((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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,14 @@ public void collectViewUpdates(JavaOnlyMap propsMap) {
} 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
* library.
*/
/*package*/ class ValueAnimatedNode extends AnimatedNode {
/*package*/ Object mAnimatedObject = null;
/*package*/ double mValue = Double.NaN;
/*package*/ double mOffset = 0;
private @Nullable AnimatedNodeValueListener mValueListener;
Expand All @@ -37,7 +36,7 @@ public double getValue() {
}

public Object getAnimatedObject() {
return mAnimatedObject;
return null;
}

public void flattenOffset() {
Expand Down
Loading

0 comments on commit e7dbfb2

Please sign in to comment.