Skip to content

Commit d548122

Browse files
authored
[General] Fix layout jumps with native detector (#3930)
## Description Inside `GestureDetectorShadowNode`, we were checking whether a new layout is available before doing any custom logic: https://github.com/software-mansion/react-native-gesture-handler/blob/8e70b11a82fee38520761321cd67d341b2b4adf6/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerDetectorShadowNode.cpp#L41-L43 However, `this->yogaNode_.getHasNewLayout()` is always `false` at this point. `YogaLayoutableShadowNode::layout`, which goes over the child nodes and updates their metrics, resets the flag immediately: https://github.com/facebook/react-native/blob/490c5e8dcc6cdb19c334cc39e93a39a48ba71e96/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp#L701-L702 When visiting the detector node, the new layout information is available in the child node but only before calling `YogaLayoutableShadowNode::layout(layoutContext)`. If the child has no new layout, the one from the previous revision should be reused. To achieve that, I added a new optional value, keeping the layout metrics from the source shadow node used to create the current revision. ## Test plan Tested on this snipped which was able to consistently reproduce the issue: ```jsx import React from 'react'; import { View, StyleSheet, ScrollView, SectionList } from 'react-native'; import { GestureHandlerRootView, LegacySwitch, RectButton, Switch, } from 'react-native-gesture-handler'; const EXAMPLES = [ { sectionTitle: 'New api', data: Array.from({ length: 11 }).map((_, i) => ({ id: i })), }, ]; export default function MainScreen() { return ( <GestureHandlerRootView style={styles.container}> <SectionList renderScrollComponent={(props) => <ScrollView {...props} />} sections={EXAMPLES} ListHeaderComponent={() => ( <RectButton style={styles.autoOpenSetting}> {/* <LegacySwitch /> */} {/* <Switch /> */} </RectButton> )} renderItem={() => <></>} renderSectionHeader={() => <></>} ItemSeparatorComponent={() => <View style={styles.separator} />} /> </GestureHandlerRootView> ); } const styles = StyleSheet.create({ container: { flex: 1, paddingTop: 50, }, separator: { height: 2, }, autoOpenSetting: { margin: 16, height: 50, backgroundColor: 'red', }, }); ```
1 parent 5e1605f commit d548122

2 files changed

Lines changed: 24 additions & 3 deletions

File tree

packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerDetectorShadowNode.cpp

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,32 @@ void RNGestureHandlerDetectorShadowNode::initialize() {
3434
}
3535

3636
void RNGestureHandlerDetectorShadowNode::layout(LayoutContext layoutContext) {
37-
YogaLayoutableShadowNode::layout(layoutContext);
3837
// TODO: consider allowing more than one child and doing bounding box
3938
react_native_assert(getChildren().size() == 1);
4039

4140
auto child = std::static_pointer_cast<const YogaLayoutableShadowNode>(
4241
getChildren()[0]);
42+
auto childWithProtectedAccess =
43+
std::static_pointer_cast<const RNGestureHandlerDetectorShadowNode>(child);
44+
45+
auto shouldSkipCustomLayout =
46+
!childWithProtectedAccess->yogaNode_.getHasNewLayout();
47+
48+
// Do default layout after reading the new layout flag from the child.
49+
// Default layout will reset the flag on the child nodes.
50+
YogaLayoutableShadowNode::layout(layoutContext);
51+
52+
// The child node did not have its layout changed, we can reuse previous
53+
// values
54+
if (shouldSkipCustomLayout) {
55+
react_native_assert(previousLayoutMetrics_.has_value());
56+
setLayoutMetrics(previousLayoutMetrics_.value());
57+
return;
58+
}
4359

4460
child->ensureUnsealed();
4561
auto mutableChild = std::const_pointer_cast<YogaLayoutableShadowNode>(child);
4662

47-
// TODO: figure out the correct way to setup metrics between detector and
48-
// the child
4963
auto metrics = child->getLayoutMetrics();
5064
metrics.frame = child->getLayoutMetrics().frame;
5165
setLayoutMetrics(metrics);

packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerDetectorShadowNode.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,20 @@ class RNGestureHandlerDetectorShadowNode final
4545
const ShadowNode &sourceShadowNode,
4646
const ShadowNodeFragment &fragment)
4747
: ConcreteViewShadowNode(sourceShadowNode, fragment) {
48+
const auto &sourceDetectorNode =
49+
static_cast<const RNGestureHandlerDetectorShadowNode &>(
50+
sourceShadowNode);
51+
previousLayoutMetrics_ = sourceDetectorNode.getLayoutMetrics();
52+
4853
initialize();
4954
}
5055

5156
void layout(LayoutContext layoutContext) override;
5257

5358
private:
5459
void initialize();
60+
61+
std::optional<LayoutMetrics> previousLayoutMetrics_;
5562
};
5663

5764
} // namespace facebook::react

0 commit comments

Comments
 (0)