/* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.react.animated; import androidx.annotation.Nullable; 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.regex.Matcher; import java.util.regex.Pattern; /** * Animated node that corresponds to {@code AnimatedInterpolation} from AnimatedImplementation.js. * *

Currently only a linear interpolation is supported on an input range of an arbitrary size. */ /*package*/ class InterpolationAnimatedNode extends ValueAnimatedNode { public static final String EXTRAPOLATE_TYPE_IDENTITY = "identity"; 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 double[] fromDoubleArray(ReadableArray ary) { double[] res = new double[ary.size()]; for (int i = 0; i < res.length; i++) { res[i] = ary.getDouble(i); } return res; } private static double interpolate( double value, double inputMin, double inputMax, double outputMin, double outputMax, String extrapolateLeft, String extrapolateRight) { double result = value; // Extrapolate if (result < inputMin) { switch (extrapolateLeft) { case EXTRAPOLATE_TYPE_IDENTITY: return result; case EXTRAPOLATE_TYPE_CLAMP: result = inputMin; break; case EXTRAPOLATE_TYPE_EXTEND: break; default: throw new JSApplicationIllegalArgumentException( "Invalid extrapolation type " + extrapolateLeft + "for left extrapolation"); } } if (result > inputMax) { switch (extrapolateRight) { case EXTRAPOLATE_TYPE_IDENTITY: return result; case EXTRAPOLATE_TYPE_CLAMP: result = inputMax; break; case EXTRAPOLATE_TYPE_EXTEND: break; default: throw new JSApplicationIllegalArgumentException( "Invalid extrapolation type " + extrapolateRight + "for right extrapolation"); } } if (outputMin == outputMax) { return outputMin; } if (inputMin == inputMax) { if (value <= inputMin) { return outputMin; } return outputMax; } return outputMin + (outputMax - outputMin) * (result - inputMin) / (inputMax - inputMin); } /*package*/ static double interpolate( double value, double[] inputRange, double[] outputRange, String extrapolateLeft, String extrapolateRight) { int rangeIndex = findRangeIndex(value, inputRange); return interpolate( value, inputRange[rangeIndex], inputRange[rangeIndex + 1], outputRange[rangeIndex], outputRange[rangeIndex + 1], extrapolateLeft, extrapolateRight); } private static int findRangeIndex(double value, double[] ranges) { int index; for (index = 1; index < ranges.length - 1; index++) { if (ranges[index] >= value) { break; } } return index - 1; } private final double mInputRange[]; private final double mOutputRange[]; private String mPattern; private double mOutputs[][]; private final boolean mHasStringOutput; private final Matcher mSOutputMatcher; private final String mExtrapolateLeft; private final String mExtrapolateRight; private @Nullable ValueAnimatedNode mParent; private boolean mShouldRound; private int mNumVals; 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> mOutputRanges = new ArrayList<>(); for (int i = 0; i < size; i++) { String val = output.getString(i); Matcher m = fpPattern.matcher(val); ArrayList 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); } } } else { mOutputRange = fromDoubleArray(output); mSOutputMatcher = null; } mExtrapolateLeft = config.getString("extrapolateLeft"); mExtrapolateRight = config.getString("extrapolateRight"); } @Override public void onAttachedToNode(AnimatedNode parent) { if (mParent != null) { throw new IllegalStateException("Parent already attached"); } if (!(parent instanceof ValueAnimatedNode)) { throw new IllegalArgumentException("Parent is of an invalid type"); } mParent = (ValueAnimatedNode) parent; } @Override public void onDetachedFromNode(AnimatedNode parent) { if (parent != mParent) { throw new IllegalArgumentException("Invalid parent node provided"); } mParent = null; } @Override public void update() { if (mParent == null) { // 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)); } } } }