native decay animation
Summary: Add support for `useNativeDriver: true` to `Animated.decay`. Add example in Native Animated Example UIExplorer app. Reviewed By: ritzau Differential Revision: D3690127 fbshipit-source-id: eaa5e61293ed174191cec72255ea2677dbaa1757
This commit is contained in:
parent
37ab1c8844
commit
2a7f4be8f8
|
@ -41,12 +41,16 @@ class Tester extends React.Component {
|
||||||
current = 0;
|
current = 0;
|
||||||
|
|
||||||
onPress = () => {
|
onPress = () => {
|
||||||
|
const animConfig = (
|
||||||
|
this.current && this.props.reverseConfig ? this.props.reverseConfig : this.props.config
|
||||||
|
);
|
||||||
this.current = this.current ? 0 : 1;
|
this.current = this.current ? 0 : 1;
|
||||||
const config = {
|
const config = {
|
||||||
...this.props.config,
|
...animConfig,
|
||||||
toValue: this.current,
|
toValue: this.current,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// $FlowIssue #0000000
|
||||||
Animated[this.props.type](this.state.native, { ...config, useNativeDriver: true }).start();
|
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.js, { ...config, useNativeDriver: false }).start();
|
||||||
};
|
};
|
||||||
|
@ -344,6 +348,32 @@ exports.examples = [
|
||||||
</Tester>
|
</Tester>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
},{
|
||||||
|
title: 'translateX => Animated.decay',
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<Tester
|
||||||
|
type="decay"
|
||||||
|
config={{ velocity: 0.5 }}
|
||||||
|
reverseConfig={{ velocity: -0.5 }}
|
||||||
|
>
|
||||||
|
{anim => (
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.block,
|
||||||
|
{
|
||||||
|
transform: [
|
||||||
|
{
|
||||||
|
translateX: anim
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Tester>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Animated value listener',
|
title: 'Animated value listener',
|
||||||
|
|
|
@ -351,6 +351,7 @@ class DecayAnimation extends Animation {
|
||||||
_velocity: number;
|
_velocity: number;
|
||||||
_onUpdate: (value: number) => void;
|
_onUpdate: (value: number) => void;
|
||||||
_animationFrame: any;
|
_animationFrame: any;
|
||||||
|
_useNativeDriver: bool;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
config: DecayAnimationConfigSingle,
|
config: DecayAnimationConfigSingle,
|
||||||
|
@ -358,13 +359,24 @@ class DecayAnimation extends Animation {
|
||||||
super();
|
super();
|
||||||
this._deceleration = config.deceleration !== undefined ? config.deceleration : 0.998;
|
this._deceleration = config.deceleration !== undefined ? config.deceleration : 0.998;
|
||||||
this._velocity = config.velocity;
|
this._velocity = config.velocity;
|
||||||
|
this._useNativeDriver = config.useNativeDriver !== undefined ? config.useNativeDriver : false;
|
||||||
this.__isInteraction = config.isInteraction !== undefined ? config.isInteraction : true;
|
this.__isInteraction = config.isInteraction !== undefined ? config.isInteraction : true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
__getNativeAnimationConfig() {
|
||||||
|
return {
|
||||||
|
type: 'decay',
|
||||||
|
deceleration: this._deceleration,
|
||||||
|
velocity: this._velocity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
start(
|
start(
|
||||||
fromValue: number,
|
fromValue: number,
|
||||||
onUpdate: (value: number) => void,
|
onUpdate: (value: number) => void,
|
||||||
onEnd: ?EndCallback,
|
onEnd: ?EndCallback,
|
||||||
|
previousAnimation: ?Animation,
|
||||||
|
animatedValue: AnimatedValue,
|
||||||
): void {
|
): void {
|
||||||
this.__active = true;
|
this.__active = true;
|
||||||
this._lastValue = fromValue;
|
this._lastValue = fromValue;
|
||||||
|
@ -372,7 +384,11 @@ class DecayAnimation extends Animation {
|
||||||
this._onUpdate = onUpdate;
|
this._onUpdate = onUpdate;
|
||||||
this.__onEnd = onEnd;
|
this.__onEnd = onEnd;
|
||||||
this._startTime = Date.now();
|
this._startTime = Date.now();
|
||||||
this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this));
|
if (this._useNativeDriver) {
|
||||||
|
this.__startNativeAnimation(animatedValue);
|
||||||
|
} else {
|
||||||
|
this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpdate(): void {
|
onUpdate(): void {
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.facebook.react.animated;
|
||||||
|
|
||||||
|
import com.facebook.react.bridge.ReadableMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of {@link AnimationDriver} providing support for decay animations. The
|
||||||
|
* implementation is copied from the JS version in {@code AnimatedImplementation.js}.
|
||||||
|
*/
|
||||||
|
public class DecayAnimation extends AnimationDriver {
|
||||||
|
|
||||||
|
private final double mVelocity;
|
||||||
|
private final double mDeceleration;
|
||||||
|
|
||||||
|
private long mStartFrameTimeMillis = -1;
|
||||||
|
private double mFromValue;
|
||||||
|
private double mLastValue;
|
||||||
|
|
||||||
|
public DecayAnimation(ReadableMap config) {
|
||||||
|
mVelocity = config.getDouble("velocity");
|
||||||
|
mDeceleration = config.getDouble("deceleration");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void runAnimationStep(long frameTimeNanos) {
|
||||||
|
long frameTimeMillis = frameTimeNanos / 1000000;
|
||||||
|
if (mStartFrameTimeMillis == -1) {
|
||||||
|
// since this is the first animation step, consider the start to be on the previous frame
|
||||||
|
mStartFrameTimeMillis = frameTimeMillis - 16;
|
||||||
|
mFromValue = mAnimatedValue.mValue;
|
||||||
|
mLastValue = mAnimatedValue.mValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final double value = mFromValue +
|
||||||
|
(mVelocity / (1 - mDeceleration)) *
|
||||||
|
(1 - Math.exp(-(1 - mDeceleration) * (frameTimeMillis - mStartFrameTimeMillis)));
|
||||||
|
|
||||||
|
if (Math.abs(mLastValue - value) < 0.1) {
|
||||||
|
mHasFinished = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mLastValue = value;
|
||||||
|
mAnimatedValue.mValue = value;
|
||||||
|
}
|
||||||
|
}
|
|
@ -139,6 +139,8 @@ import javax.annotation.Nullable;
|
||||||
animation = new FrameBasedAnimationDriver(animationConfig);
|
animation = new FrameBasedAnimationDriver(animationConfig);
|
||||||
} else if ("spring".equals(type)) {
|
} else if ("spring".equals(type)) {
|
||||||
animation = new SpringAnimation(animationConfig);
|
animation = new SpringAnimation(animationConfig);
|
||||||
|
} else if ("decay".equals(type)) {
|
||||||
|
animation = new DecayAnimation(animationConfig);
|
||||||
} else {
|
} else {
|
||||||
throw new JSApplicationIllegalArgumentException("Unsupported animation type: " + type);
|
throw new JSApplicationIllegalArgumentException("Unsupported animation type: " + type);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@ import com.facebook.react.bridge.ReadableMap;
|
||||||
import com.facebook.react.uimanager.ReactStylesDiffMap;
|
import com.facebook.react.uimanager.ReactStylesDiffMap;
|
||||||
import com.facebook.react.uimanager.UIImplementation;
|
import com.facebook.react.uimanager.UIImplementation;
|
||||||
|
|
||||||
import org.junit.After;
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
@ -259,6 +258,63 @@ public class NativeAnimatedNodeTraversalTest {
|
||||||
verifyNoMoreInteractions(mUIImplementationMock);
|
verifyNoMoreInteractions(mUIImplementationMock);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDecayAnimation() {
|
||||||
|
createSimpleAnimatedViewWithOpacity(1000, 0d);
|
||||||
|
|
||||||
|
Callback animationCallback = mock(Callback.class);
|
||||||
|
mNativeAnimatedNodesManager.startAnimatingNode(
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
JavaOnlyMap.of(
|
||||||
|
"type",
|
||||||
|
"decay",
|
||||||
|
"velocity",
|
||||||
|
0.5d,
|
||||||
|
"deceleration",
|
||||||
|
0.998d),
|
||||||
|
animationCallback);
|
||||||
|
|
||||||
|
ArgumentCaptor<ReactStylesDiffMap> stylesCaptor =
|
||||||
|
ArgumentCaptor.forClass(ReactStylesDiffMap.class);
|
||||||
|
|
||||||
|
reset(mUIImplementationMock);
|
||||||
|
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
|
||||||
|
verify(mUIImplementationMock, atMost(1))
|
||||||
|
.synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture());
|
||||||
|
double previousValue = stylesCaptor.getValue().getDouble("opacity", Double.NaN);
|
||||||
|
double previousDiff = Double.POSITIVE_INFINITY;
|
||||||
|
/* run 3 secs of animation */
|
||||||
|
for (int i = 0; i < 3 * 60; i++) {
|
||||||
|
reset(mUIImplementationMock);
|
||||||
|
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
|
||||||
|
verify(mUIImplementationMock, atMost(1))
|
||||||
|
.synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture());
|
||||||
|
double currentValue = stylesCaptor.getValue().getDouble("opacity", Double.NaN);
|
||||||
|
double currentDiff = currentValue - previousValue;
|
||||||
|
// verify monotonicity
|
||||||
|
// greater *or equal* because the animation stops during these 3 seconds
|
||||||
|
assertThat(currentValue).as("on frame " + i).isGreaterThanOrEqualTo(previousValue);
|
||||||
|
// verify decay
|
||||||
|
if (i > 3) {
|
||||||
|
// i > 3 because that's how long it takes to settle previousDiff
|
||||||
|
if (i % 3 != 0) {
|
||||||
|
// i % 3 != 0 because every 3 frames we go a tiny
|
||||||
|
// bit faster, because frame length is 16.(6)ms
|
||||||
|
assertThat(currentDiff).as("on frame " + i).isLessThanOrEqualTo(previousDiff);
|
||||||
|
} else {
|
||||||
|
assertThat(currentDiff).as("on frame " + i).isGreaterThanOrEqualTo(previousDiff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previousValue = currentValue;
|
||||||
|
previousDiff = currentDiff;
|
||||||
|
}
|
||||||
|
// should be done in 3s
|
||||||
|
reset(mUIImplementationMock);
|
||||||
|
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
|
||||||
|
verifyNoMoreInteractions(mUIImplementationMock);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAnimationCallbackFinish() {
|
public void testAnimationCallbackFinish() {
|
||||||
createSimpleAnimatedViewWithOpacity(1000, 0d);
|
createSimpleAnimatedViewWithOpacity(1000, 0d);
|
||||||
|
|
Loading…
Reference in New Issue