Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -912,3 +912,103 @@ test('animate width with memo and rerender (js sync test)', () => {
<rn-view width="200" />,
);
});

// A native scroll event pushes its values outside the frame loop. It must not
// step running time-based animations: frames step them with the frame time, so
// stepping them at the push's own time as well would move them whenever the
// two times differ.
test('native scroll event does not step a running timing animation', () => {
const scrollViewRef = createRef<HostInstance>();
const viewRef = createRef<HostInstance>();
const scrollFollowerRef = createRef<HostInstance>();
let _translateX;

component MyApp() {
const scrollY = useAnimatedValue(0);
const translateX = useAnimatedValue(0);
_translateX = translateX;
return (
<View style={{flex: 1}}>
<Animated.View
ref={viewRef}
style={{width: 10, height: 10, transform: [{translateX}]}}
/>
<Animated.View
ref={scrollFollowerRef}
style={{width: 10, height: 10, transform: [{translateY: scrollY}]}}
/>
<Animated.ScrollView
ref={scrollViewRef}
onScroll={Animated.event(
[{nativeEvent: {contentOffset: {y: scrollY}}}],
{useNativeDriver: true},
)}>
<View style={{height: 1000, width: 100}} />
</Animated.ScrollView>
</View>
);
}

// Stamp pushes far later than the frame clock, as on device, where a push
// between frames runs later than the next frame's vsync time.
const mockClock = Fantom.installHighResTimeStampMock();
mockClock.setTime(1e12);
let animation;
try {
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(<MyApp />);
});
const scrollViewElement = nullthrows(scrollViewRef.current);
const viewElement = nullthrows(viewRef.current);
const scrollFollowerElement = nullthrows(scrollFollowerRef.current);

const getTransformValue = (element: HostInstance, key: string): number => {
const transform =
Fantom.unstable_getDirectManipulationProps(element).transform;
const first = Array.isArray(transform) ? transform[0] : null;
if (
first == null ||
typeof first !== 'object' ||
typeof first[key] !== 'number'
) {
throw new Error(`Expected a numeric ${key} transform`);
}
return first[key];
};
const getTranslateX = (): number =>
getTransformValue(viewElement, 'translateX');

Fantom.runTask(() => {
animation = Animated.timing(nullthrows(_translateX), {
toValue: 100,
duration: 1000,
easing: Easing.linear,
useNativeDriver: true,
});
animation.start();
});
Fantom.unstable_produceFramesForDuration(500);

const beforeScroll = getTranslateX();
expect(beforeScroll).toBeGreaterThan(0);
expect(beforeScroll).toBeLessThan(100);

Fantom.scrollTo(scrollViewElement, {x: 0, y: 10});
// The scroll event reached the native graph...
expect(getTransformValue(scrollFollowerElement, 'translateY')).toBe(10);
// ...and left the running timing animation where the frames put it.
expect(getTranslateX()).toBe(beforeScroll);

Fantom.unstable_produceFramesForDuration(100);
expect(getTranslateX()).toBeGreaterThan(beforeScroll);
expect(getTranslateX()).toBeLessThan(100);
} finally {
Fantom.runTask(() => {
animation?.stop();
});
// Flush the work queued by stopping the animation.
Fantom.runWorkLoop();
mockClock.uninstall();
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -555,9 +555,14 @@ void NativeAnimatedNodesManager::handleAnimatedEvent(
// frames.
if (useSharedAnimatedBackend_) {
if (auto animationBackend = animationBackend_.lock()) {
// Pass stepAnimations false so this push only propagates event values.
// Frames step time-based drivers with the vsync time, which can be
// earlier than this push's time, so stepping them here would move them
// backward on the next frame.
animationBackend->pushAnimationMutations(
[this](AnimationTimestamp timestamp) -> AnimationMutations {
return pullAnimationMutations(timestamp);
return pullAnimationMutations(
timestamp, /* stepAnimations */ false);
});
}
} else {
Expand Down Expand Up @@ -1038,14 +1043,17 @@ void NativeAnimatedNodesManager::insertMutations(

AnimationMutations NativeAnimatedNodesManager::onAnimationFrameForBackend(
AnimatedPropsBuilder& propsBuilder,
AnimationTimestamp timestamp) {
AnimationTimestamp timestamp,
bool stepAnimations) {
AnimationMutations mutations{};
auto timestampMs = timestamp.count();
// Run all active animations
auto hasFinishedAnimations = false;
std::set<int> finishedAnimationValueNodes;
for (const auto& [_id, driver] : activeAnimations_) {
driver->runAnimationStep(timestampMs);
if (stepAnimations) {
driver->runAnimationStep(timestampMs);
}

if (driver->getIsComplete()) {
hasFinishedAnimations = true;
Expand Down Expand Up @@ -1083,7 +1091,8 @@ AnimationMutations NativeAnimatedNodesManager::onAnimationFrameForBackend(
}

AnimationMutations NativeAnimatedNodesManager::pullAnimationMutations(
AnimationTimestamp timestamp) {
AnimationTimestamp timestamp,
bool stepAnimations) {
if (!useSharedAnimatedBackend_) {
return {};
}
Expand Down Expand Up @@ -1113,7 +1122,8 @@ AnimationMutations NativeAnimatedNodesManager::pullAnimationMutations(
// Step through the animation loop
if (isAnimationUpdateNeeded()) {
AnimatedPropsBuilder propsBuilder;
mutations = onAnimationFrameForBackend(propsBuilder, timestamp);
mutations =
onAnimationFrameForBackend(propsBuilder, timestamp, stepAnimations);

if (mutations.batch.empty()) {
// The last animation tick didn't result in any changes to the UI.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,11 @@ class NativeAnimatedNodesManager : public std::enable_shared_from_this<NativeAni
AnimationMutations &mutations,
AnimatedPropsBuilder &propsBuilder,
bool hasLayoutUpdates = false);
AnimationMutations onAnimationFrameForBackend(AnimatedPropsBuilder &propsBuilder, AnimationTimestamp timestamp);
AnimationMutations pullAnimationMutations(AnimationTimestamp timestamp);
AnimationMutations onAnimationFrameForBackend(
AnimatedPropsBuilder &propsBuilder,
AnimationTimestamp timestamp,
bool stepAnimations = true);
AnimationMutations pullAnimationMutations(AnimationTimestamp timestamp, bool stepAnimations = true);

#pragma mark - Drivers

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,83 @@

#include "AnimationTestsBase.h"

#include <react/renderer/animated/drivers/AnimationDriverUtils.h>
#include <react/renderer/components/scrollview/ScrollEvent.h>
#include <react/renderer/core/ReactRootViewTagGenerator.h>

namespace facebook::react {

class EventAnimationDriverTests : public AnimationTestsBase {};
namespace {

// Runs event-driven updates at a timestamp the test sets, like a platform
// whose current time is later than the vsync time of the next frame.
class FakeAnimationBackend : public UIManagerAnimationBackend {
public:
AnimationTimestamp pushTimestamp{0};

void onAnimationFrame(AnimationTimestamp /*timestamp*/) override {}
CallbackId start(const Callback& /*callback*/) override {
return 0;
}
void stop(CallbackId /*callbackId*/) override {}
void clearRegistry(SurfaceId /*surfaceId*/) override {}
void clearRegistryOnSurfaceStop(SurfaceId /*surfaceId*/) override {}
void trigger() override {}
void pushAnimationMutations(const Callback& callback) override {
callback(pushTimestamp);
}
void registerJSInvoker(std::shared_ptr<CallInvoker> /*jsInvoker*/) override {}
};

} // namespace

class EventAnimationDriverTests : public AnimationTestsBase {
protected:
static constexpr double kStartTime = 1000;

std::shared_ptr<FakeAnimationBackend> backend_ =
std::make_shared<FakeAnimationBackend>();
Tag tag_{getNextRootViewTag()};
Tag viewTag_{++tag_};
Tag scrollValueTag_{++tag_};
Tag animatedValueTag_{++tag_};

// A shared-backend manager with a scroll event mapped to scrollValueTag_ and
// the given animation running on animatedValueTag_.
void startSharedBackendAnimation(const folly::dynamic& animationConfig) {
// Marks this thread as the render thread.
initNodesManager();
nodesManager_ = std::make_shared<NativeAnimatedNodesManager>(backend_);

const folly::dynamic valueNodeConfig =
folly::dynamic::object("type", "value")("value", 0)("offset", 0);
nodesManager_->createAnimatedNode(scrollValueTag_, valueNodeConfig);
nodesManager_->addAnimatedEventToView(
viewTag_,
"scroll",
folly::dynamic::object("animatedValueTag", scrollValueTag_)(
"nativeEventPath", folly::dynamic::array("contentOffset", "y")));
nodesManager_->createAnimatedNode(animatedValueTag_, valueNodeConfig);
nodesManager_->startAnimatingNode(
1, animatedValueTag_, animationConfig, std::nullopt);
}

void runBackendFrame(double timestamp) {
nodesManager_->pullAnimationMutations(AnimationTimestamp{timestamp});
}

void dispatchScrollAt(double timestamp, double offsetY) {
backend_->pushTimestamp = AnimationTimestamp{timestamp};
auto scrollEvent = std::make_shared<ScrollEvent>();
scrollEvent->contentOffset = {.x = 0, .y = static_cast<Float>(offsetY)};
(*nodesManager_->getEventEmitterListener())(
viewTag_, "scroll", *scrollEvent);
}

double animatedValue() {
return nodesManager_->getValue(animatedValueTag_).value();
}
};

TEST_F(EventAnimationDriverTests, subscribeToViewEvent) {
initNodesManager();
Expand Down Expand Up @@ -67,4 +138,41 @@ TEST_F(EventAnimationDriverTests, subscribeToViewEvent) {
EXPECT_EQ(nodesManager_->getValue(animatedValueTag2), 11);
}

TEST_F(
EventAnimationDriverTests,
sharedBackendEventDoesNotStepTimingAnimationAheadOfFrames) {
startSharedBackendAnimation(
folly::dynamic::object("type", "frames")(
"frames", folly::dynamic::array(0.0, 0.25, 0.5, 0.75, 1.0))(
"toValue", 100));

runBackendFrame(kStartTime);
runBackendFrame(kStartTime + SingleFrameIntervalMs);
const auto valueBeforeEvent = animatedValue();
EXPECT_GT(valueBeforeEvent, 0);

// The event is handled later than the next frame's vsync time.
dispatchScrollAt(kStartTime + SingleFrameIntervalMs * 3, 4);
EXPECT_EQ(nodesManager_->getValue(scrollValueTag_), 4);
EXPECT_EQ(animatedValue(), valueBeforeEvent);

runBackendFrame(kStartTime + SingleFrameIntervalMs * 2);
EXPECT_GT(animatedValue(), valueBeforeEvent);
}

TEST_F(EventAnimationDriverTests, sharedBackendEventDoesNotEndDecayAnimation) {
startSharedBackendAnimation(
folly::dynamic::object("type", "decay")("velocity", 1.0)(
"deceleration", 0.998)("iterations", 1));

runBackendFrame(kStartTime);
runBackendFrame(kStartTime + SingleFrameIntervalMs);
dispatchScrollAt(kStartTime + SingleFrameIntervalMs * 2, 4);
runBackendFrame(kStartTime + SingleFrameIntervalMs * 2);
const auto valueAfterEvent = animatedValue();

runBackendFrame(kStartTime + SingleFrameIntervalMs * 3);
EXPECT_GT(animatedValue(), valueAfterEvent);
}

} // namespace facebook::react
4 changes: 2 additions & 2 deletions scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -2839,8 +2839,8 @@ class facebook::react::NativeAnimatedNodesManager : public std::enable_shared_fr
public bool hasManagedProps() const noexcept;
public bool isOnRenderThread() const noexcept;
public bool useSharedAnimatedBackend() const noexcept;
public facebook::react::AnimationMutations onAnimationFrameForBackend(facebook::react::AnimatedPropsBuilder& propsBuilder, facebook::react::AnimationTimestamp timestamp);
public facebook::react::AnimationMutations pullAnimationMutations(facebook::react::AnimationTimestamp timestamp);
public facebook::react::AnimationMutations onAnimationFrameForBackend(facebook::react::AnimatedPropsBuilder& propsBuilder, facebook::react::AnimationTimestamp timestamp, bool stepAnimations = true);
public facebook::react::AnimationMutations pullAnimationMutations(facebook::react::AnimationTimestamp timestamp, bool stepAnimations = true);
public facebook::react::NativeAnimatedNodesManager& operator=(const facebook::react::NativeAnimatedNodesManager&) = delete;
public facebook::react::NativeAnimatedNodesManager& operator=(facebook::react::NativeAnimatedNodesManager&&) = delete;
public folly::dynamic getManagedProps(facebook::react::Tag tag) const noexcept;
Expand Down
4 changes: 2 additions & 2 deletions scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -5038,8 +5038,8 @@ class facebook::react::NativeAnimatedNodesManager : public std::enable_shared_fr
public bool hasManagedProps() const noexcept;
public bool isOnRenderThread() const noexcept;
public bool useSharedAnimatedBackend() const noexcept;
public facebook::react::AnimationMutations onAnimationFrameForBackend(facebook::react::AnimatedPropsBuilder& propsBuilder, facebook::react::AnimationTimestamp timestamp);
public facebook::react::AnimationMutations pullAnimationMutations(facebook::react::AnimationTimestamp timestamp);
public facebook::react::AnimationMutations onAnimationFrameForBackend(facebook::react::AnimatedPropsBuilder& propsBuilder, facebook::react::AnimationTimestamp timestamp, bool stepAnimations = true);
public facebook::react::AnimationMutations pullAnimationMutations(facebook::react::AnimationTimestamp timestamp, bool stepAnimations = true);
public facebook::react::NativeAnimatedNodesManager& operator=(const facebook::react::NativeAnimatedNodesManager&) = delete;
public facebook::react::NativeAnimatedNodesManager& operator=(facebook::react::NativeAnimatedNodesManager&&) = delete;
public folly::dynamic getManagedProps(facebook::react::Tag tag) const noexcept;
Expand Down
4 changes: 2 additions & 2 deletions scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -1703,8 +1703,8 @@ class facebook::react::NativeAnimatedNodesManager : public std::enable_shared_fr
public bool hasManagedProps() const noexcept;
public bool isOnRenderThread() const noexcept;
public bool useSharedAnimatedBackend() const noexcept;
public facebook::react::AnimationMutations onAnimationFrameForBackend(facebook::react::AnimatedPropsBuilder& propsBuilder, facebook::react::AnimationTimestamp timestamp);
public facebook::react::AnimationMutations pullAnimationMutations(facebook::react::AnimationTimestamp timestamp);
public facebook::react::AnimationMutations onAnimationFrameForBackend(facebook::react::AnimatedPropsBuilder& propsBuilder, facebook::react::AnimationTimestamp timestamp, bool stepAnimations = true);
public facebook::react::AnimationMutations pullAnimationMutations(facebook::react::AnimationTimestamp timestamp, bool stepAnimations = true);
public facebook::react::NativeAnimatedNodesManager& operator=(const facebook::react::NativeAnimatedNodesManager&) = delete;
public facebook::react::NativeAnimatedNodesManager& operator=(facebook::react::NativeAnimatedNodesManager&&) = delete;
public folly::dynamic getManagedProps(facebook::react::Tag tag) const noexcept;
Expand Down
Loading