mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-04-24 04:16:00 +08:00
Native Animated - Support Animated.loop on iOS
Summary: Follow up to #11973 to add support to Animated.loop with useNativeDriver on iOS. **Test plan** Test with new UIExplorer example Run unit tests Closes https://github.com/facebook/react-native/pull/13359 Differential Revision: D4960754 Pulled By: javache fbshipit-source-id: caa840281f1b060df7a2b1c50405fcae1e1b0de6
This commit is contained in:
committed by
Facebook Github Bot
parent
32d35c31f7
commit
11424a8bc6
@@ -183,6 +183,37 @@ static id RCTPropChecker(NSString *prop, NSNumber *value)
|
||||
[_uiManager verify];
|
||||
}
|
||||
|
||||
- (void)testFramesAnimationLoop
|
||||
{
|
||||
[self createSimpleAnimatedView:@1000 withOpacity:0];
|
||||
NSArray<NSNumber *> *frames = @[@0, @0.2, @0.4, @0.6, @0.8, @1];
|
||||
[_nodesManager startAnimatingNode:@1
|
||||
nodeTag:@1
|
||||
config:@{@"type": @"frames", @"frames": frames, @"toValue": @1, @"iterations": @5}
|
||||
endCallback:nil];
|
||||
|
||||
for (NSUInteger it = 0; it < 5; it++) {
|
||||
for (NSNumber *frame in frames) {
|
||||
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:@1000
|
||||
viewName:@"UIView"
|
||||
props:RCTPropChecker(@"opacity", frame)];
|
||||
[_nodesManager stepAnimations:_displayLink];
|
||||
[_uiManager verify];
|
||||
}
|
||||
}
|
||||
|
||||
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:@1000
|
||||
viewName:@"UIView"
|
||||
props:RCTPropChecker(@"opacity", @1)];
|
||||
|
||||
[_nodesManager stepAnimations:_displayLink];
|
||||
[_uiManager verify];
|
||||
|
||||
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
||||
[_nodesManager stepAnimations:_displayLink];
|
||||
[_uiManager verify];
|
||||
}
|
||||
|
||||
- (void)testNodeValueListenerIfNotListening
|
||||
{
|
||||
NSNumber *nodeId = @1;
|
||||
@@ -377,6 +408,62 @@ static id RCTPropChecker(NSString *prop, NSNumber *value)
|
||||
[_uiManager verify];
|
||||
}
|
||||
|
||||
- (void)testSpringAnimationLoop
|
||||
{
|
||||
[self createSimpleAnimatedView:@1000 withOpacity:0];
|
||||
[_nodesManager startAnimatingNode:@1
|
||||
nodeTag:@1
|
||||
config:@{@"type": @"spring",
|
||||
@"iterations": @5,
|
||||
@"friction": @7,
|
||||
@"tension": @40,
|
||||
@"initialVelocity": @0,
|
||||
@"toValue": @1,
|
||||
@"restSpeedThreshold": @0.001,
|
||||
@"restDisplacementThreshold": @0.001,
|
||||
@"overshootClamping": @NO}
|
||||
endCallback:nil];
|
||||
|
||||
BOOL didComeToRest = NO;
|
||||
CGFloat previousValue = 0;
|
||||
NSUInteger numberOfResets = 0;
|
||||
__block CGFloat currentValue;
|
||||
[[[_uiManager stub] andDo:^(NSInvocation *invocation) {
|
||||
__unsafe_unretained NSDictionary<NSString *, NSNumber *> *props;
|
||||
[invocation getArgument:&props atIndex:4];
|
||||
currentValue = props[@"opacity"].doubleValue;
|
||||
}] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
||||
|
||||
// Run for 3 seconds five times.
|
||||
for (NSUInteger i = 0; i < 3 * 60 * 5; i++) {
|
||||
[_nodesManager stepAnimations:_displayLink];
|
||||
|
||||
if (!didComeToRest) {
|
||||
// Verify that animation step is relatively small.
|
||||
XCTAssertLessThan(fabs(currentValue - previousValue), 0.1);
|
||||
}
|
||||
|
||||
// Test to see if it reset after coming to rest
|
||||
if (didComeToRest && currentValue == 0) {
|
||||
didComeToRest = NO;
|
||||
numberOfResets++;
|
||||
}
|
||||
|
||||
// Record that the animation did come to rest when it rests on toValue.
|
||||
didComeToRest = fabs(currentValue - 1) < 0.001 && fabs(currentValue - previousValue) < 0.001;
|
||||
|
||||
previousValue = currentValue;
|
||||
}
|
||||
|
||||
// Verify that value reset 4 times after finishing a full animation and is currently resting.
|
||||
XCTAssertEqual(numberOfResets, 4u);
|
||||
XCTAssertTrue(didComeToRest);
|
||||
|
||||
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
|
||||
[_nodesManager stepAnimations:_displayLink];
|
||||
[_uiManager verify];
|
||||
}
|
||||
|
||||
- (void)testAnimationCallbackFinish
|
||||
{
|
||||
[self createSimpleAnimatedView:@1000 withOpacity:0];
|
||||
|
||||
@@ -33,17 +33,23 @@ class Tester extends React.Component {
|
||||
current = 0;
|
||||
|
||||
onPress = () => {
|
||||
const animConfig = (
|
||||
this.current && this.props.reverseConfig ? this.props.reverseConfig : this.props.config
|
||||
);
|
||||
const animConfig = this.current && this.props.reverseConfig
|
||||
? this.props.reverseConfig
|
||||
: this.props.config;
|
||||
this.current = this.current ? 0 : 1;
|
||||
const config: Object = {
|
||||
...animConfig,
|
||||
toValue: this.current,
|
||||
};
|
||||
|
||||
Animated[this.props.type](this.state.native, { ...config, useNativeDriver: true }).start();
|
||||
Animated[this.props.type](this.state.js, { ...config, useNativeDriver: false }).start();
|
||||
Animated[this.props.type](this.state.native, {
|
||||
...config,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
Animated[this.props.type](this.state.js, {
|
||||
...config,
|
||||
useNativeDriver: false,
|
||||
}).start();
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -76,7 +82,7 @@ class ValueListenerExample extends React.Component {
|
||||
_current = 0;
|
||||
|
||||
componentDidMount() {
|
||||
this.state.anim.addListener((e) => this.setState({ progress: e.value }));
|
||||
this.state.anim.addListener(e => this.setState({progress: e.value}));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -90,7 +96,10 @@ class ValueListenerExample extends React.Component {
|
||||
toValue: this._current,
|
||||
};
|
||||
|
||||
Animated.timing(this.state.anim, { ...config, useNativeDriver: true }).start();
|
||||
Animated.timing(this.state.anim, {
|
||||
...config,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -103,7 +112,7 @@ class ValueListenerExample extends React.Component {
|
||||
styles.block,
|
||||
{
|
||||
opacity: this.state.anim,
|
||||
}
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
@@ -114,6 +123,40 @@ class ValueListenerExample extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
class LoopExample extends React.Component {
|
||||
state = {
|
||||
value: new Animated.Value(0),
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
Animated.loop(
|
||||
Animated.timing(this.state.value, {
|
||||
toValue: 1,
|
||||
duration: 5000,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
).start();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.row}>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.block,
|
||||
{
|
||||
opacity: this.state.value.interpolate({
|
||||
inputRange: [0, 0.5, 1],
|
||||
outputRange: [0, 1, 0],
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const RNTesterSettingSwitchRow = require('RNTesterSettingSwitchRow');
|
||||
class InternalSettings extends React.Component {
|
||||
_stallInterval: ?number;
|
||||
@@ -128,7 +171,8 @@ class InternalSettings extends React.Component {
|
||||
this._stallInterval = setInterval(() => {
|
||||
const start = Date.now();
|
||||
console.warn('burn CPU');
|
||||
while ((Date.now() - start) < 100) {}
|
||||
while (Date.now() - start < 100) {
|
||||
}
|
||||
}, 300);
|
||||
}}
|
||||
onDisable={() => {
|
||||
@@ -142,19 +186,23 @@ class InternalSettings extends React.Component {
|
||||
require('JSEventLoopWatchdog').install({thresholdMS: 25});
|
||||
this.setState({busyTime: '<none>'});
|
||||
require('JSEventLoopWatchdog').addHandler({
|
||||
onStall: ({busyTime}) => this.setState((state) => ({
|
||||
busyTime,
|
||||
filteredStall: (state.filteredStall || 0) * 0.97 + busyTime * 0.03,
|
||||
})),
|
||||
onStall: ({busyTime}) =>
|
||||
this.setState(state => ({
|
||||
busyTime,
|
||||
filteredStall: (state.filteredStall || 0) * 0.97 +
|
||||
busyTime * 0.03,
|
||||
})),
|
||||
});
|
||||
}}
|
||||
onDisable={() => {
|
||||
console.warn('Cannot disable yet....');
|
||||
}}
|
||||
/>
|
||||
{this.state && <Text>
|
||||
JS Stall filtered: {Math.round(this.state.filteredStall)}, last: {this.state.busyTime}
|
||||
</Text>}
|
||||
{this.state &&
|
||||
<Text>
|
||||
{`JS Stall filtered: ${Math.round(this.state.filteredStall)}, `}
|
||||
{`last: ${this.state.busyTime}`}
|
||||
</Text>}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -177,22 +225,23 @@ class EventExample extends React.Component {
|
||||
styles.block,
|
||||
{
|
||||
opacity,
|
||||
}
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Animated.ScrollView
|
||||
horizontal
|
||||
style={{ height: 100, marginTop: 16 }}
|
||||
style={{height: 100, marginTop: 16}}
|
||||
scrollEventThrottle={16}
|
||||
onScroll={
|
||||
Animated.event([{
|
||||
nativeEvent: { contentOffset: { x: this.state.scrollX } }
|
||||
}], {
|
||||
useNativeDriver: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
<View style={{ width: 600, backgroundColor: '#eee', justifyContent: 'center' }}>
|
||||
onScroll={Animated.event(
|
||||
[{nativeEvent: {contentOffset: {x: this.state.scrollX}}}],
|
||||
{useNativeDriver: true},
|
||||
)}>
|
||||
<View
|
||||
style={{
|
||||
width: 600,
|
||||
backgroundColor: '#eee',
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
<Text>Scroll me!</Text>
|
||||
</View>
|
||||
</Animated.ScrollView>
|
||||
@@ -218,52 +267,51 @@ exports.title = 'Native Animated Example';
|
||||
exports.description = 'Test out Native Animations';
|
||||
|
||||
exports.examples = [
|
||||
{
|
||||
{
|
||||
title: 'Multistage With Multiply and rotation',
|
||||
render: function() {
|
||||
return (
|
||||
<Tester
|
||||
type="timing"
|
||||
config={{ duration: 1000 }}>
|
||||
{anim => (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.block,
|
||||
{
|
||||
transform: [
|
||||
{
|
||||
translateX: anim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 200],
|
||||
})
|
||||
},
|
||||
{
|
||||
translateY: anim.interpolate({
|
||||
inputRange: [0, 0.5, 1],
|
||||
outputRange: [0, 50, 0],
|
||||
})
|
||||
},
|
||||
{
|
||||
rotate: anim.interpolate({
|
||||
inputRange: [0, 0.5, 1],
|
||||
outputRange: ['0deg', '90deg', '0deg'],
|
||||
})
|
||||
}
|
||||
],
|
||||
opacity: Animated.multiply(
|
||||
anim.interpolate({
|
||||
inputRange: [0,1],
|
||||
outputRange: [1,0]
|
||||
}), anim.interpolate({
|
||||
inputRange: [0,1],
|
||||
outputRange: [0.25,1]
|
||||
})
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Tester>
|
||||
<Tester type="timing" config={{duration: 1000}}>
|
||||
{anim => (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.block,
|
||||
{
|
||||
transform: [
|
||||
{
|
||||
translateX: anim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 200],
|
||||
}),
|
||||
},
|
||||
{
|
||||
translateY: anim.interpolate({
|
||||
inputRange: [0, 0.5, 1],
|
||||
outputRange: [0, 50, 0],
|
||||
}),
|
||||
},
|
||||
{
|
||||
rotate: anim.interpolate({
|
||||
inputRange: [0, 0.5, 1],
|
||||
outputRange: ['0deg', '90deg', '0deg'],
|
||||
}),
|
||||
},
|
||||
],
|
||||
opacity: Animated.multiply(
|
||||
anim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [1, 0],
|
||||
}),
|
||||
anim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0.25, 1],
|
||||
}),
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Tester>
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -271,42 +319,41 @@ exports.examples = [
|
||||
title: 'Multistage With Multiply',
|
||||
render: function() {
|
||||
return (
|
||||
<Tester
|
||||
type="timing"
|
||||
config={{ duration: 1000 }}>
|
||||
{anim => (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.block,
|
||||
{
|
||||
transform: [
|
||||
{
|
||||
translateX: anim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 200],
|
||||
})
|
||||
},
|
||||
{
|
||||
translateY: anim.interpolate({
|
||||
inputRange: [0, 0.5, 1],
|
||||
outputRange: [0, 50, 0],
|
||||
})
|
||||
}
|
||||
],
|
||||
opacity: Animated.multiply(
|
||||
anim.interpolate({
|
||||
inputRange: [0,1],
|
||||
outputRange: [1,0]
|
||||
}), anim.interpolate({
|
||||
inputRange: [0,1],
|
||||
outputRange: [0.25,1]
|
||||
})
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Tester>
|
||||
<Tester type="timing" config={{duration: 1000}}>
|
||||
{anim => (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.block,
|
||||
{
|
||||
transform: [
|
||||
{
|
||||
translateX: anim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 200],
|
||||
}),
|
||||
},
|
||||
{
|
||||
translateY: anim.interpolate({
|
||||
inputRange: [0, 0.5, 1],
|
||||
outputRange: [0, 50, 0],
|
||||
}),
|
||||
},
|
||||
],
|
||||
opacity: Animated.multiply(
|
||||
anim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [1, 0],
|
||||
}),
|
||||
anim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0.25, 1],
|
||||
}),
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Tester>
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -314,9 +361,7 @@ exports.examples = [
|
||||
title: 'Scale interpolation with clamping',
|
||||
render: function() {
|
||||
return (
|
||||
<Tester
|
||||
type="timing"
|
||||
config={{ duration: 1000 }}>
|
||||
<Tester type="timing" config={{duration: 1000}}>
|
||||
{anim => (
|
||||
<Animated.View
|
||||
style={[
|
||||
@@ -328,10 +373,10 @@ exports.examples = [
|
||||
inputRange: [0, 0.5],
|
||||
outputRange: [1, 1.4],
|
||||
extrapolateRight: 'clamp',
|
||||
})
|
||||
}
|
||||
}),
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
@@ -343,16 +388,14 @@ exports.examples = [
|
||||
title: 'Opacity with delay',
|
||||
render: function() {
|
||||
return (
|
||||
<Tester
|
||||
type="timing"
|
||||
config={{ duration: 1000, delay: 1000 }}>
|
||||
<Tester type="timing" config={{duration: 1000, delay: 1000}}>
|
||||
{anim => (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.block,
|
||||
{
|
||||
opacity: anim
|
||||
}
|
||||
opacity: anim,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
@@ -364,9 +407,7 @@ exports.examples = [
|
||||
title: 'Rotate interpolation',
|
||||
render: function() {
|
||||
return (
|
||||
<Tester
|
||||
type="timing"
|
||||
config={{ duration: 1000 }}>
|
||||
<Tester type="timing" config={{duration: 1000}}>
|
||||
{anim => (
|
||||
<Animated.View
|
||||
style={[
|
||||
@@ -377,10 +418,10 @@ exports.examples = [
|
||||
rotate: anim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ['0deg', '90deg'],
|
||||
})
|
||||
}
|
||||
}),
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
@@ -392,9 +433,7 @@ exports.examples = [
|
||||
title: 'translateX => Animated.spring',
|
||||
render: function() {
|
||||
return (
|
||||
<Tester
|
||||
type="spring"
|
||||
config={{ bounciness: 0 }}>
|
||||
<Tester type="spring" config={{bounciness: 0}}>
|
||||
{anim => (
|
||||
<Animated.View
|
||||
style={[
|
||||
@@ -405,24 +444,25 @@ exports.examples = [
|
||||
translateX: anim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 100],
|
||||
})
|
||||
}),
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Tester>
|
||||
);
|
||||
},
|
||||
},{
|
||||
},
|
||||
{
|
||||
title: 'translateX => Animated.decay',
|
||||
render: function() {
|
||||
return (
|
||||
<Tester
|
||||
type="decay"
|
||||
config={{ velocity: 0.5 }}
|
||||
reverseConfig={{ velocity: -0.5 }}>
|
||||
config={{velocity: 0.5}}
|
||||
reverseConfig={{velocity: -0.5}}>
|
||||
{anim => (
|
||||
<Animated.View
|
||||
style={[
|
||||
@@ -430,26 +470,23 @@ exports.examples = [
|
||||
{
|
||||
transform: [
|
||||
{
|
||||
translateX: anim
|
||||
translateX: anim,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Tester>
|
||||
);
|
||||
},
|
||||
},{
|
||||
},
|
||||
{
|
||||
title: 'Drive custom property',
|
||||
render: function() {
|
||||
return (
|
||||
<Tester
|
||||
type="timing"
|
||||
config={{ duration: 1000 }}>
|
||||
{anim => (
|
||||
<AnimatedSlider style={{}} value={anim} />
|
||||
)}
|
||||
<Tester type="timing" config={{duration: 1000}}>
|
||||
{anim => <AnimatedSlider style={{}} value={anim} />}
|
||||
</Tester>
|
||||
);
|
||||
},
|
||||
@@ -457,25 +494,25 @@ exports.examples = [
|
||||
{
|
||||
title: 'Animated value listener',
|
||||
render: function() {
|
||||
return (
|
||||
<ValueListenerExample />
|
||||
);
|
||||
return <ValueListenerExample />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Animated loop',
|
||||
render: function() {
|
||||
return <LoopExample />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Animated events',
|
||||
render: function() {
|
||||
return (
|
||||
<EventExample />
|
||||
);
|
||||
return <EventExample />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Internal Settings',
|
||||
render: function() {
|
||||
return (
|
||||
<InternalSettings />
|
||||
);
|
||||
return <InternalSettings />;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user