Skip to content
Open
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 @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @fantom_flags useSharedAnimatedBackend:true updateRuntimeShadowNodeReferencesOnCommitThread:*
* @fantom_flags useSharedAnimatedBackend:true updateRuntimeShadowNodeReferencesOnCommitThread:* optimizedAnimatedPropUpdates:*
* @flow strict-local
* @format
*/
Expand All @@ -17,7 +17,7 @@ import * as Fantom from '@react-native/fantom';
import nullthrows from 'nullthrows';
import * as React from 'react';
import {Component, createRef, memo, useEffect, useMemo, useState} from 'react';
import {Animated, View, useAnimatedValue} from 'react-native';
import {Animated, Easing, View, useAnimatedValue} from 'react-native';

// marginLeft (and the other margin props) are only on the native animated
// allowlist when the shared backend is enabled. This verifies the prop is
Expand Down Expand Up @@ -77,6 +77,196 @@ test('animate marginLeft layout prop', () => {
);
});

test('non-layout props stay on the direct path while another view animates layout', () => {
const movingRef = createRef<HostInstance>();

let _translateX;
let _translateXAnimation;
let _siblingHeight;
let _siblingHeightAnimation;

function MyApp() {
const translateX = useAnimatedValue(0);
const siblingHeight = useAnimatedValue(10);
_translateX = translateX;
_siblingHeight = siblingHeight;
return (
<View collapsable={false}>
<Animated.View
ref={movingRef}
style={{width: 100, height: 100, transform: [{translateX}]}}
/>
<Animated.View style={{width: 100, height: siblingHeight}} />
</View>
);
}

const root = Fantom.createRoot();

Fantom.runTask(() => {
root.render(<MyApp />);
});

Fantom.runTask(() => {
_translateXAnimation = Animated.timing(_translateX, {
toValue: 100,
duration: 200,
useNativeDriver: true,
}).start();
_siblingHeightAnimation = Animated.timing(_siblingHeight, {
toValue: 110,
duration: 200,
useNativeDriver: true,
}).start();
});

Fantom.unstable_produceFramesForDuration(100);

// The sibling's height went through a commit; the transform did not.
expect(root.getRenderedOutput({props: ['height']}).toJSX()).toEqual(
<rn-view>
<rn-view key={0} height="100" />
<rn-view key={1} height="60" />
</rn-view>,
);
expect(
Fantom.unstable_getDirectManipulationProps(nullthrows(movingRef.current))
.transform,
).toEqual([{translateX: 50}]);

Fantom.runTask(() => {
_translateXAnimation?.stop();
_siblingHeightAnimation?.stop();
});
});

test('direct-path props survive a re-render while another view animates layout', () => {
let _opacity;
let _opacityAnimation;
let _siblingHeight;
let _siblingHeightAnimation;
let _setWidth;

function MyApp() {
const opacity = useAnimatedValue(0);
const siblingHeight = useAnimatedValue(10);
const [width, setWidth] = useState(100);
_opacity = opacity;
_siblingHeight = siblingHeight;
_setWidth = setWidth;
return (
<View collapsable={false}>
<Animated.View style={{width, height: 100, opacity}} />
<Animated.View style={{width: 100, height: siblingHeight}} />
</View>
);
}

const root = Fantom.createRoot();

Fantom.runTask(() => {
root.render(<MyApp />);
});

Fantom.runTask(() => {
_opacityAnimation = Animated.timing(_opacity, {
toValue: 0.5,
duration: 1000,
useNativeDriver: true,
}).start();
_siblingHeightAnimation = Animated.timing(_siblingHeight, {
toValue: 110,
duration: 1000,
useNativeDriver: true,
}).start();
});

Fantom.unstable_produceFramesForDuration(500);

Fantom.runTask(() => {
_setWidth(150);
});

expect(
root.getRenderedOutput({props: ['opacity', 'width', 'height']}).toJSX(),
).toEqual(
<rn-view>
<rn-view key={0} opacity="0.25" width="150" height="100" />
<rn-view key={1} width="100" height="60" />
</rn-view>,
);

Fantom.runTask(() => {
_opacityAnimation?.stop();
_siblingHeightAnimation?.stop();
});
});

test('a view keeps writing its props directly after it starts animating layout', () => {
const viewRef = createRef<HostInstance>();

let _opacity;
let _opacityAnimation;
let _height;
let _heightAnimation;

function MyApp() {
const opacity = useAnimatedValue(0);
const height = useAnimatedValue(10);
_opacity = opacity;
_height = height;
return (
<Animated.View ref={viewRef} style={{width: 100, height, opacity}} />
);
}

const root = Fantom.createRoot();

Fantom.runTask(() => {
root.render(<MyApp />);
});

const viewElement = nullthrows(viewRef.current);

Fantom.runTask(() => {
_opacityAnimation = Animated.timing(_opacity, {
toValue: 0.5,
duration: 1000,
easing: Easing.linear,
useNativeDriver: true,
}).start();
});

Fantom.unstable_produceFramesForDuration(500);

expect(
Fantom.unstable_getDirectManipulationProps(viewElement).opacity,
).toBeCloseTo(0.25, 0.001);

Fantom.runTask(() => {
_heightAnimation = Animated.timing(_height, {
toValue: 110,
duration: 1000,
easing: Easing.linear,
useNativeDriver: true,
}).start();
});

Fantom.unstable_produceFramesForDuration(250);

expect(root.getRenderedOutput({props: ['height']}).toJSX()).toEqual(
<rn-view height="35" />,
);
expect(
Fantom.unstable_getDirectManipulationProps(viewElement).opacity,
).toBeCloseTo(0.375, 0.001);

Fantom.runTask(() => {
_opacityAnimation?.stop();
_heightAnimation?.stop();
});
});

test('animated opacity', () => {
let _opacity;
let _opacityAnimation;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<a39494512e761e266940e5d1853f38fc>>
* @generated SignedSource<<5a3796afdf02ba39aa578d83dc3f8332>>
*/

/**
Expand Down Expand Up @@ -409,7 +409,7 @@ public object ReactNativeFeatureFlags {
public fun fuseboxWebSocketEventsEnabled(): Boolean = accessor.fuseboxWebSocketEventsEnabled()

/**
* When enabled, uses optimized platform-specific paths to apply animated props synchronously. On Android, this uses a batched int/double buffer protocol with a single JNI call. On iOS, this passes AnimatedProps directly through the delegate chain and applies them via cloneProps, avoiding the folly::dynamic round-trip.
* When enabled, uses optimized platform-specific paths to apply animated props synchronously. On Android, this uses a batched int/double buffer protocol with a single JNI call. Other platforms apply the props of each view through the existing synchronous update.
*/
@JvmStatic
public fun optimizedAnimatedPropUpdates(): Boolean = accessor.optimizedAnimatedPropUpdates()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<4c93a39dd2a8ee5ba24f3f8bc99ac1ae>>
* @generated SignedSource<<467f462832f58b37ed97105595fbc843>>
*/

/**
Expand Down Expand Up @@ -357,7 +357,7 @@ class ReactNativeFeatureFlags {
RN_EXPORT static bool fuseboxWebSocketEventsEnabled();

/**
* When enabled, uses optimized platform-specific paths to apply animated props synchronously. On Android, this uses a batched int/double buffer protocol with a single JNI call. On iOS, this passes AnimatedProps directly through the delegate chain and applies them via cloneProps, avoiding the folly::dynamic round-trip.
* When enabled, uses optimized platform-specific paths to apply animated props synchronously. On Android, this uses a batched int/double buffer protocol with a single JNI call. Other platforms apply the props of each view through the existing synchronous update.
*/
RN_EXPORT static bool optimizedAnimatedPropUpdates();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,40 @@
*/

#include "AnimatedPropsRegistry.h"
#include <react/debug/react_native_assert.h>
#include <react/renderer/core/PropsParserContext.h>
#include "AnimatedProps.h"

namespace facebook::react {

void mergeAnimatedRawProps(
folly::dynamic& target,
const folly::dynamic& source) {
if (!target.isObject() || !source.isObject()) {
target = source;
return;
}
for (const auto& [key, value] : source.items()) {
target[key] = value;
}
}

void AnimatedPropsRegistry::update(
const std::unordered_map<SurfaceId, SurfaceUpdates>& surfaceUpdates) {
const std::vector<AnimationMutations>& batches) {
auto lock = std::lock_guard(mutex_);
for (const auto& [surfaceId, updates] : surfaceUpdates) {
auto contextIt = surfaceContexts_.find(surfaceId);
if (contextIt == surfaceContexts_.end()) {
continue;
}
auto& surfaceContext = contextIt->second;
auto& pendingMap = surfaceContext.pendingMap;
auto& pendingFamilies = surfaceContext.pendingFamilies;

auto& updatesMap = updates.propsMap;
auto& updatesFamilies = updates.families;

for (auto& family : updatesFamilies) {
pendingFamilies.insert(family);
}

for (auto& [tag, animatedProps] : updatesMap) {
for (const auto& mutations : batches) {
for (const auto& mutation : mutations.batch) {
const auto& family = mutation.family;
react_native_assert(family != nullptr);
auto contextIt = surfaceContexts_.find(family->getSurfaceId());
if (contextIt == surfaceContexts_.end()) {
continue;
}
auto& surfaceContext = contextIt->second;
auto& pendingMap = surfaceContext.pendingMap;
surfaceContext.pendingFamilies.insert(family);
const auto tag = mutation.tag;
const auto& animatedProps = mutation.props;
auto it = pendingMap.find(tag);
if (it == pendingMap.end()) {
it = pendingMap.insert_or_assign(tag, std::make_unique<PropsSnapshot>())
Expand All @@ -44,8 +53,7 @@ void AnimatedPropsRegistry::update(
auto& currentRawProps = snapshot->rawProps;

if (currentRawProps) {
auto newRawPropsDynamic = newRawProps.toDynamic();
currentRawProps->merge_patch(newRawPropsDynamic);
mergeAnimatedRawProps(*currentRawProps, newRawProps.toDynamic());
} else {
currentRawProps =
std::make_unique<folly::dynamic>(newRawProps.toDynamic());
Expand Down Expand Up @@ -83,7 +91,8 @@ AnimatedPropsRegistry::getMap(SurfaceId surfaceId) {
auto& currentSnapshot = currentIt->second;
if (propsSnapshot->rawProps) {
if (currentSnapshot->rawProps) {
currentSnapshot->rawProps->merge_patch(*propsSnapshot->rawProps);
mergeAnimatedRawProps(
*currentSnapshot->rawProps, *propsSnapshot->rawProps);
} else {
currentSnapshot->rawProps = std::move(propsSnapshot->rawProps);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <react/renderer/uimanager/UIManager.h>
#include <react/renderer/uimanager/UIManagerCommitHook.h>
#include "AnimatedProps.h"
#include "AnimationMutation.h"

namespace facebook::react {

Expand All @@ -29,17 +30,17 @@ struct SurfaceContext {
std::unordered_set<std::shared_ptr<const ShadowNodeFamily>> pendingFamilies, families;
};

struct SurfaceUpdates {
std::unordered_set<std::shared_ptr<const ShadowNodeFamily>> families;
std::unordered_map<Tag, AnimatedProps> propsMap;
bool hasLayoutUpdates{false};
};

using SnapshotMap = std::unordered_map<Tag, std::unique_ptr<PropsSnapshot>>;

/*
* Merges raw animated props per key, like the props merged by the animation
* drivers. A null value is kept, so it still resets the prop on commit.
*/
void mergeAnimatedRawProps(folly::dynamic &target, const folly::dynamic &source);

class AnimatedPropsRegistry {
public:
void update(const std::unordered_map<SurfaceId, SurfaceUpdates> &surfaceUpdates);
void update(const std::vector<AnimationMutations> &batches);
void initializeSurface(SurfaceId surfaceId);
void clear(SurfaceId surfaceId);
void clearOnSurfaceStop(SurfaceId surfaceId);
Expand Down
Loading
Loading