mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Fix connection of animated nodes and scroll offset with useNativeDriver. (#24177)
Summary: Add example showing regression before this fix is applied. https://github.com/facebook/react-native/pull/18187 Was found to introduce a regression in some internal facebook code-base end to end test which couldn't be shared. I was able to create a reproducible demo of a regression I found, and made a fix for it. Hopefully this will fix the internal test, such that the pr can stay merged. ## Changelog [GENERAL] [Fixed] - Fix connection of animated nodes and scroll offset with useNativeDriver. Pull Request resolved: https://github.com/facebook/react-native/pull/24177 Reviewed By: rickhanlonii Differential Revision: D14845617 Pulled By: cpojer fbshipit-source-id: 1f121dbe773b0cde2adf1ee5a8c3c0266034e50d
This commit is contained in:
committed by
Facebook Github Bot
parent
417e191a1c
commit
bdc530b9bb
@@ -27,11 +27,25 @@ let __nativeAnimationIdCount = 1; /* used for started animations */
|
||||
|
||||
let nativeEventEmitter;
|
||||
|
||||
let queueConnections = false;
|
||||
let queue = [];
|
||||
|
||||
/**
|
||||
* Simple wrappers around NativeAnimatedModule to provide flow and autocmplete support for
|
||||
* the native module methods
|
||||
*/
|
||||
const API = {
|
||||
enableQueue: function(): void {
|
||||
queueConnections = true;
|
||||
},
|
||||
disableQueue: function(): void {
|
||||
invariant(NativeAnimatedModule, 'Native animated module is not available');
|
||||
queueConnections = false;
|
||||
while (queue.length) {
|
||||
const args = queue.shift();
|
||||
NativeAnimatedModule.connectAnimatedNodes(args[0], args[1]);
|
||||
}
|
||||
},
|
||||
createAnimatedNode: function(tag: ?number, config: AnimatedNodeConfig): void {
|
||||
invariant(NativeAnimatedModule, 'Native animated module is not available');
|
||||
NativeAnimatedModule.createAnimatedNode(tag, config);
|
||||
@@ -46,6 +60,10 @@ const API = {
|
||||
},
|
||||
connectAnimatedNodes: function(parentTag: ?number, childTag: ?number): void {
|
||||
invariant(NativeAnimatedModule, 'Native animated module is not available');
|
||||
if (queueConnections) {
|
||||
queue.push([parentTag, childTag]);
|
||||
return;
|
||||
}
|
||||
NativeAnimatedModule.connectAnimatedNodes(parentTag, childTag);
|
||||
},
|
||||
disconnectAnimatedNodes: function(
|
||||
@@ -197,7 +215,7 @@ function addWhitelistedInterpolationParam(param: string): void {
|
||||
function validateTransform(
|
||||
configs: Array<
|
||||
| {type: 'animated', property: string, nodeTag: ?number}
|
||||
| {type: 'static', property: string, value: number},
|
||||
| {type: 'static', property: string, value: number | string},
|
||||
>,
|
||||
): void {
|
||||
configs.forEach(config => {
|
||||
@@ -263,7 +281,7 @@ function shouldUseNativeDriver(config: AnimationConfig | EventConfig): boolean {
|
||||
return config.useNativeDriver || false;
|
||||
}
|
||||
|
||||
function transformDataType(value: number | string): number {
|
||||
function transformDataType(value: number | string): number | string {
|
||||
// Change the string type to number type so we can reuse the same logic in
|
||||
// iOS and Android platform
|
||||
if (typeof value !== 'string') {
|
||||
@@ -274,8 +292,7 @@ function transformDataType(value: number | string): number {
|
||||
const radians = (degrees * Math.PI) / 180.0;
|
||||
return radians;
|
||||
} else {
|
||||
// Assume radians
|
||||
return parseFloat(value) || 0;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,9 @@ class Animation {
|
||||
onEnd && onEnd(result);
|
||||
}
|
||||
__startNativeAnimation(animatedValue: AnimatedValue): void {
|
||||
NativeAnimatedHelper.API.enableQueue();
|
||||
animatedValue.__makeNative();
|
||||
NativeAnimatedHelper.API.disableQueue();
|
||||
this.__nativeId = NativeAnimatedHelper.generateNewAnimationId();
|
||||
NativeAnimatedHelper.API.startAnimatingNode(
|
||||
this.__nativeId,
|
||||
|
||||
@@ -181,7 +181,7 @@ function colorToRgba(input: string): string {
|
||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||
}
|
||||
|
||||
const stringShapeRegex = /[0-9\.-]+/g;
|
||||
const stringShapeRegex = /[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g;
|
||||
|
||||
/**
|
||||
* Supports string shapes by extracting numbers so new values can be computed,
|
||||
@@ -242,10 +242,11 @@ function createInterpolationFromStringOutputRange(
|
||||
// ->
|
||||
// 'rgba(${interpolations[0](input)}, ${interpolations[1](input)}, ...'
|
||||
return outputRange[0].replace(stringShapeRegex, () => {
|
||||
const val = +interpolations[i++](input);
|
||||
const rounded =
|
||||
shouldRound && i < 4 ? Math.round(val) : Math.round(val * 1000) / 1000;
|
||||
return String(rounded);
|
||||
let val = +interpolations[i++](input);
|
||||
if (shouldRound) {
|
||||
val = i < 4 ? Math.round(val) : Math.round(val * 1000) / 1000;
|
||||
}
|
||||
return String(val);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -157,11 +157,11 @@ class AnimatedNode {
|
||||
);
|
||||
if (this.__nativeTag == null) {
|
||||
const nativeTag: ?number = NativeAnimatedHelper.generateNewNodeTag();
|
||||
this.__nativeTag = nativeTag;
|
||||
NativeAnimatedHelper.API.createAnimatedNode(
|
||||
nativeTag,
|
||||
this.__getNativeConfig(),
|
||||
);
|
||||
this.__nativeTag = nativeTag;
|
||||
this.__shouldUpdateListenersForNewNativeTag = true;
|
||||
}
|
||||
return this.__nativeTag;
|
||||
|
||||
@@ -151,6 +151,7 @@ class AnimatedProps extends AnimatedNode {
|
||||
for (const propKey in this._props) {
|
||||
const value = this._props[propKey];
|
||||
if (value instanceof AnimatedNode) {
|
||||
value.__makeNative();
|
||||
propsConfig[propKey] = value.__getNativeTag();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,9 @@ class AnimatedStyle extends AnimatedWithChildren {
|
||||
const styleConfig = {};
|
||||
for (const styleKey in this._style) {
|
||||
if (this._style[styleKey] instanceof AnimatedNode) {
|
||||
styleConfig[styleKey] = this._style[styleKey].__getNativeTag();
|
||||
const style = this._style[styleKey];
|
||||
style.__makeNative();
|
||||
styleConfig[styleKey] = style.__getNativeTag();
|
||||
}
|
||||
// Non-animated styles are set using `setNativeProps`, no need
|
||||
// to pass those as a part of the node config
|
||||
|
||||
@@ -9,27 +9,90 @@
|
||||
|
||||
#import "RCTAnimationUtils.h"
|
||||
|
||||
static NSRegularExpression *regex;
|
||||
|
||||
@implementation RCTInterpolationAnimatedNode
|
||||
{
|
||||
__weak RCTValueAnimatedNode *_parentNode;
|
||||
NSArray<NSNumber *> *_inputRange;
|
||||
NSArray<NSNumber *> *_outputRange;
|
||||
NSArray<NSArray<NSNumber *> *> *_outputs;
|
||||
NSArray<NSString *> *_soutputRange;
|
||||
NSString *_extrapolateLeft;
|
||||
NSString *_extrapolateRight;
|
||||
NSUInteger _numVals;
|
||||
bool _hasStringOutput;
|
||||
bool _shouldRound;
|
||||
NSArray<NSTextCheckingResult*> *_matches;
|
||||
}
|
||||
|
||||
- (instancetype)initWithTag:(NSNumber *)tag
|
||||
config:(NSDictionary<NSString *, id> *)config
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
NSString *fpRegex = @"[+-]?(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?";
|
||||
regex = [NSRegularExpression regularExpressionWithPattern:fpRegex options:NSRegularExpressionCaseInsensitive error:nil];
|
||||
});
|
||||
if ((self = [super initWithTag:tag config:config])) {
|
||||
_inputRange = [config[@"inputRange"] copy];
|
||||
NSMutableArray *outputRange = [NSMutableArray array];
|
||||
NSMutableArray *soutputRange = [NSMutableArray array];
|
||||
NSMutableArray<NSMutableArray<NSNumber *> *> *_outputRanges = [NSMutableArray array];
|
||||
|
||||
_hasStringOutput = NO;
|
||||
for (id value in config[@"outputRange"]) {
|
||||
if ([value isKindOfClass:[NSNumber class]]) {
|
||||
[outputRange addObject:value];
|
||||
} else if ([value isKindOfClass:[NSString class]]) {
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
NSMutableArray *output = [NSMutableArray array];
|
||||
[_outputRanges addObject:output];
|
||||
[soutputRange addObject:value];
|
||||
|
||||
_matches = [regex matchesInString:value options:0 range:NSMakeRange(0, [value length])];
|
||||
for (NSTextCheckingResult *match in _matches) {
|
||||
NSString* strNumber = [value substringWithRange:match.range];
|
||||
[output addObject:[NSNumber numberWithDouble:strNumber.doubleValue]];
|
||||
}
|
||||
|
||||
_hasStringOutput = YES;
|
||||
[outputRange addObject:[output objectAtIndex:0]];
|
||||
}
|
||||
}
|
||||
if (_hasStringOutput) {
|
||||
// ['rgba(0, 100, 200, 0)', 'rgba(50, 150, 250, 0.5)']
|
||||
// ->
|
||||
// [
|
||||
// [0, 50],
|
||||
// [100, 150],
|
||||
// [200, 250],
|
||||
// [0, 0.5],
|
||||
// ]
|
||||
_numVals = [_matches count];
|
||||
NSString *value = [soutputRange objectAtIndex:0];
|
||||
_shouldRound = [value containsString:@"rgb"];
|
||||
_matches = [regex matchesInString:value options:0 range:NSMakeRange(0, [value length])];
|
||||
NSMutableArray<NSMutableArray<NSNumber *> *> *outputs = [NSMutableArray arrayWithCapacity:_numVals];
|
||||
NSUInteger size = [soutputRange count];
|
||||
for (NSUInteger j = 0; j < _numVals; j++) {
|
||||
NSMutableArray *output = [NSMutableArray arrayWithCapacity:size];
|
||||
[outputs addObject:output];
|
||||
for (int i = 0; i < size; i++) {
|
||||
[output addObject:[[_outputRanges objectAtIndex:i] objectAtIndex:j]];
|
||||
}
|
||||
}
|
||||
_outputs = [outputs copy];
|
||||
}
|
||||
_outputRange = [outputRange copy];
|
||||
_soutputRange = [soutputRange copy];
|
||||
_extrapolateLeft = config[@"extrapolateLeft"];
|
||||
_extrapolateRight = config[@"extrapolateRight"];
|
||||
}
|
||||
@@ -61,11 +124,48 @@
|
||||
|
||||
CGFloat inputValue = _parentNode.value;
|
||||
|
||||
self.value = RCTInterpolateValueInRange(inputValue,
|
||||
_inputRange,
|
||||
_outputRange,
|
||||
_extrapolateLeft,
|
||||
_extrapolateRight);
|
||||
CGFloat interpolated = RCTInterpolateValueInRange(inputValue,
|
||||
_inputRange,
|
||||
_outputRange,
|
||||
_extrapolateLeft,
|
||||
_extrapolateRight);
|
||||
self.value = interpolated;
|
||||
if (_hasStringOutput) {
|
||||
// 'rgba(0, 100, 200, 0)'
|
||||
// ->
|
||||
// 'rgba(${interpolations[0](input)}, ${interpolations[1](input)}, ...'
|
||||
if (_numVals > 1) {
|
||||
NSString *text = _soutputRange[0];
|
||||
NSMutableString *formattedText = [NSMutableString stringWithString:text];
|
||||
NSUInteger i = _numVals;
|
||||
for (NSTextCheckingResult *match in [_matches reverseObjectEnumerator]) {
|
||||
CGFloat val = RCTInterpolateValueInRange(inputValue,
|
||||
_inputRange,
|
||||
_outputs[--i],
|
||||
_extrapolateLeft,
|
||||
_extrapolateRight);
|
||||
NSString *str;
|
||||
if (_shouldRound) {
|
||||
// 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).
|
||||
bool isAlpha = i == 3;
|
||||
CGFloat rounded = isAlpha ? round(val * 1000) / 1000 : round(val);
|
||||
str = isAlpha ? [NSString stringWithFormat:@"%1.3f", rounded] : [NSString stringWithFormat:@"%1.0f", rounded];
|
||||
} else {
|
||||
NSNumber *numberValue = [NSNumber numberWithDouble:val];
|
||||
str = [numberValue stringValue];
|
||||
}
|
||||
|
||||
[formattedText replaceCharactersInRange:[match range] withString:str];
|
||||
}
|
||||
self.animatedObject = formattedText;
|
||||
} else {
|
||||
self.animatedObject = [regex stringByReplacingMatchesInString:_soutputRange[0]
|
||||
options:0
|
||||
range:NSMakeRange(0, _soutputRange[0].length)
|
||||
withTemplate:[NSString stringWithFormat:@"%1f", interpolated]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -115,8 +115,13 @@
|
||||
|
||||
} else if ([parentNode isKindOfClass:[RCTValueAnimatedNode class]]) {
|
||||
NSString *property = [self propertyNameForParentTag:parentTag];
|
||||
CGFloat value = [(RCTValueAnimatedNode *)parentNode value];
|
||||
self->_propsDictionary[property] = @(value);
|
||||
id animatedObject = [(RCTValueAnimatedNode *)parentNode animatedObject];
|
||||
if (animatedObject) {
|
||||
self->_propsDictionary[property] = animatedObject;
|
||||
} else {
|
||||
CGFloat value = [(RCTValueAnimatedNode *)parentNode value];
|
||||
self->_propsDictionary[property] = @(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
- (void)extractOffset;
|
||||
|
||||
@property (nonatomic, assign) CGFloat value;
|
||||
@property (nonatomic, strong) id animatedObject;
|
||||
@property (nonatomic, weak) id<RCTValueAnimatedNodeObserver> valueObserver;
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @format
|
||||
* @flow
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
const ReactNative = require('react-native');
|
||||
const {Component} = React;
|
||||
const {
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
Animated,
|
||||
Easing,
|
||||
TouchableOpacity,
|
||||
Dimensions,
|
||||
} = ReactNative;
|
||||
|
||||
class ScrollViewAnimatedExample extends Component<{}> {
|
||||
_scrollViewPos = new Animated.Value(0);
|
||||
|
||||
startAnimation = () => {
|
||||
this._scrollViewPos.setValue(0);
|
||||
Animated.timing(this._scrollViewPos, {
|
||||
toValue: 100,
|
||||
duration: 10000,
|
||||
easing: Easing.linear,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
};
|
||||
|
||||
render() {
|
||||
const interpolated = this._scrollViewPos.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 0.1],
|
||||
});
|
||||
const interpolated2 = this._scrollViewPos.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ['0deg', '1deg'],
|
||||
});
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Animated.View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 100,
|
||||
backgroundColor: 'black',
|
||||
transform: [{translateX: interpolated}, {rotate: interpolated2}],
|
||||
}}
|
||||
/>
|
||||
<Animated.ScrollView
|
||||
horizontal
|
||||
scrollEventThrottle={16}
|
||||
onScroll={Animated.event(
|
||||
[{nativeEvent: {contentOffset: {x: this._scrollViewPos}}}],
|
||||
{useNativeDriver: true},
|
||||
)}>
|
||||
<TouchableOpacity onPress={this.startAnimation}>
|
||||
<View style={styles.button}>
|
||||
<Text>Scroll me horizontally</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Animated.ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const {width, height} = Dimensions.get('window');
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#F5FCFF',
|
||||
},
|
||||
button: {
|
||||
margin: 50,
|
||||
width: width,
|
||||
marginRight: width,
|
||||
height: height / 2,
|
||||
},
|
||||
});
|
||||
|
||||
exports.title = '<ScrollViewAnimated>';
|
||||
exports.description = 'Component that is animated when ScrollView is offset.';
|
||||
|
||||
exports.examples = [
|
||||
{
|
||||
title: 'Animated by scroll view',
|
||||
render: function(): React.Element<typeof ScrollViewAnimatedExample> {
|
||||
return <ScrollViewAnimatedExample />;
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -65,6 +65,10 @@ const ComponentExamples: Array<RNTesterExample> = [
|
||||
key: 'ScrollViewSimpleExample',
|
||||
module: require('../examples/ScrollView/ScrollViewSimpleExample'),
|
||||
},
|
||||
{
|
||||
key: 'ScrollViewAnimatedExample',
|
||||
module: require('../examples/ScrollView/ScrollViewAnimatedExample'),
|
||||
},
|
||||
{
|
||||
key: 'SectionListExample',
|
||||
module: require('../examples/SectionList/SectionListExample'),
|
||||
|
||||
@@ -113,6 +113,11 @@ const ComponentExamples: Array<RNTesterExample> = [
|
||||
module: require('../examples/ScrollView/ScrollViewExample'),
|
||||
supportsTVOS: true,
|
||||
},
|
||||
{
|
||||
key: 'ScrollViewAnimatedExample',
|
||||
module: require('../examples/ScrollView/ScrollViewAnimatedExample'),
|
||||
supportsTVOS: true,
|
||||
},
|
||||
{
|
||||
key: 'SectionListExample',
|
||||
module: require('../examples/SectionList/SectionListExample'),
|
||||
|
||||
+96
-2
@@ -9,6 +9,12 @@ package com.facebook.react.animated;
|
||||
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;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
@@ -22,6 +28,9 @@ import javax.annotation.Nullable;
|
||||
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++) {
|
||||
@@ -116,13 +125,68 @@ import javax.annotation.Nullable;
|
||||
|
||||
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"));
|
||||
mOutputRange = fromDoubleArray(config.getArray("outputRange"));
|
||||
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);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mOutputRange = fromDoubleArray(output);
|
||||
mSOutputMatcher = null;
|
||||
}
|
||||
mExtrapolateLeft = config.getString("extrapolateLeft");
|
||||
mExtrapolateRight = config.getString("extrapolateRight");
|
||||
}
|
||||
@@ -153,6 +217,36 @@ import javax.annotation.Nullable;
|
||||
// unattached node.
|
||||
return;
|
||||
}
|
||||
mValue = interpolate(mParent.getValue(), mInputRange, mOutputRange, mExtrapolateLeft, mExtrapolateRight);
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,12 @@ import javax.annotation.Nullable;
|
||||
} else if (node instanceof StyleAnimatedNode) {
|
||||
((StyleAnimatedNode) node).collectViewUpdates(mPropMap);
|
||||
} else if (node instanceof ValueAnimatedNode) {
|
||||
mPropMap.putDouble(entry.getKey(), ((ValueAnimatedNode) node).getValue());
|
||||
Object animatedObject = ((ValueAnimatedNode) node).getAnimatedObject();
|
||||
if (animatedObject instanceof String) {
|
||||
mPropMap.putString(entry.getKey(), (String)animatedObject);
|
||||
} else {
|
||||
mPropMap.putDouble(entry.getKey(), ((ValueAnimatedNode) node).getValue());
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported type of node used in property node " +
|
||||
node.getClass());
|
||||
|
||||
@@ -16,6 +16,7 @@ import javax.annotation.Nullable;
|
||||
* library.
|
||||
*/
|
||||
/*package*/ class ValueAnimatedNode extends AnimatedNode {
|
||||
/*package*/ Object mAnimatedObject = null;
|
||||
/*package*/ double mValue = Double.NaN;
|
||||
/*package*/ double mOffset = 0;
|
||||
private @Nullable AnimatedNodeValueListener mValueListener;
|
||||
@@ -33,6 +34,10 @@ import javax.annotation.Nullable;
|
||||
return mOffset + mValue;
|
||||
}
|
||||
|
||||
public Object getAnimatedObject() {
|
||||
return mAnimatedObject;
|
||||
}
|
||||
|
||||
public void flattenOffset() {
|
||||
mValue += mOffset;
|
||||
mOffset = 0;
|
||||
|
||||
Reference in New Issue
Block a user