Skip to content

Commit d026c0f

Browse files
Bruno Ramosmeta-codesync[bot]
authored andcommitted
Refresh padding and text after a pixel density change (#59049)
Summary: Pull Request resolved: #59049 When an Android app handles density changes itself (`density` in `android:configChanges`), its surface is laid out again at the new density, but some mounted views keep sizes from the old one: - Padding and overflow insets are sent to views in pixels, but only when their dp values change, which a density change usually doesn't do. A `TextInput` keeps its old pixel padding, so its text and placeholder can be clipped. - Paragraph state only updates when the text, its attributes or its measured size change, so `Text` keeps the layout it built at the old density and draws at the old size. With the new `fixTextLayoutOnPixelDensityChange` feature flag on, padding and overflow insets are re-sent when the pixel density changes (zero insets are skipped: they are zero pixels at any density), and paragraph state is rebuilt. Both text paths are covered: with `enablePreparedTextLayout`, the paragraph prepares a new layout at the new density (the prepared text cache is keyed by density) and its state now updates when only the density changed; without it, the state records the density too. The flag is off by default. **How to use:** return `true` from your app's feature flag overrides, before React Native starts: ``` override fun fixTextLayoutOnPixelDensityChange(): Boolean = true ``` Changelog: [Internal] Reviewed By: javache Differential Revision: D124076552 fbshipit-source-id: f103c71785298a9d06888548c38049dc8847a62a
1 parent 9a20e06 commit d026c0f

30 files changed

Lines changed: 452 additions & 59 deletions

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<d824f6746b2cc7d4560164c04d123988>>
7+
* @generated SignedSource<<eb0bafceba60cb439a4be3d68b890725>>
88
*/
99

1010
/**
@@ -390,6 +390,12 @@ public object ReactNativeFeatureFlags {
390390
@JvmStatic
391391
public fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean = accessor.fixMappingOfEventPrioritiesBetweenFabricAndReact()
392392

393+
/**
394+
* Re-measures text and re-sends padding and overflow insets to mounted views when the pixel density changes, so text and padded views do not keep sizes from the old density.
395+
*/
396+
@JvmStatic
397+
public fun fixTextLayoutOnPixelDensityChange(): Boolean = accessor.fixTextLayoutOnPixelDensityChange()
398+
393399
/**
394400
* Fix flex basis computation to not apply FitContent constraint in the main axis for non-measure container nodes, preventing unnecessary re-measurement in scroll containers.
395401
*/

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<ea771505a9f0b070dd8b2c4b50411c6e>>
7+
* @generated SignedSource<<0107593110db613063b6f4ceb50d8e58>>
88
*/
99

1010
/**
@@ -80,6 +80,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
8080
private var enableVirtualViewContainerStateExperimentalCache: Boolean? = null
8181
private var fixBorderlessRippleAndroidCache: Boolean? = null
8282
private var fixMappingOfEventPrioritiesBetweenFabricAndReactCache: Boolean? = null
83+
private var fixTextLayoutOnPixelDensityChangeCache: Boolean? = null
8384
private var fixYogaFlexBasisFitContentInMainAxisCache: Boolean? = null
8485
private var fuseboxAssertSingleHostStateCache: Boolean? = null
8586
private var fuseboxEnabledReleaseCache: Boolean? = null
@@ -653,6 +654,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
653654
return cached
654655
}
655656

657+
override fun fixTextLayoutOnPixelDensityChange(): Boolean {
658+
var cached = fixTextLayoutOnPixelDensityChangeCache
659+
if (cached == null) {
660+
cached = ReactNativeFeatureFlagsCxxInterop.fixTextLayoutOnPixelDensityChange()
661+
fixTextLayoutOnPixelDensityChangeCache = cached
662+
}
663+
return cached
664+
}
665+
656666
override fun fixYogaFlexBasisFitContentInMainAxis(): Boolean {
657667
var cached = fixYogaFlexBasisFitContentInMainAxisCache
658668
if (cached == null) {

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<51b7baf7b3c2758ec619c77cb4fa637d>>
7+
* @generated SignedSource<<fa0374808e3cbe7122ca993780c0b627>>
88
*/
99

1010
/**
@@ -148,6 +148,8 @@ public object ReactNativeFeatureFlagsCxxInterop {
148148

149149
@DoNotStrip @JvmStatic public external fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean
150150

151+
@DoNotStrip @JvmStatic public external fun fixTextLayoutOnPixelDensityChange(): Boolean
152+
151153
@DoNotStrip @JvmStatic public external fun fixYogaFlexBasisFitContentInMainAxis(): Boolean
152154

153155
@DoNotStrip @JvmStatic public external fun fuseboxAssertSingleHostState(): Boolean

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<e8540d339cda9300ee0eac6e2ce316d2>>
7+
* @generated SignedSource<<768077a22c110aa98ab02bc96eec695b>>
88
*/
99

1010
/**
@@ -143,6 +143,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
143143

144144
override fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean = false
145145

146+
override fun fixTextLayoutOnPixelDensityChange(): Boolean = false
147+
146148
override fun fixYogaFlexBasisFitContentInMainAxis(): Boolean = false
147149

148150
override fun fuseboxAssertSingleHostState(): Boolean = true

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<3e94c46a8a6fda2a4c0378e326291d73>>
7+
* @generated SignedSource<<86ebc7307e7ebe2023e909a868ed3117>>
88
*/
99

1010
/**
@@ -84,6 +84,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
8484
private var enableVirtualViewContainerStateExperimentalCache: Boolean? = null
8585
private var fixBorderlessRippleAndroidCache: Boolean? = null
8686
private var fixMappingOfEventPrioritiesBetweenFabricAndReactCache: Boolean? = null
87+
private var fixTextLayoutOnPixelDensityChangeCache: Boolean? = null
8788
private var fixYogaFlexBasisFitContentInMainAxisCache: Boolean? = null
8889
private var fuseboxAssertSingleHostStateCache: Boolean? = null
8990
private var fuseboxEnabledReleaseCache: Boolean? = null
@@ -717,6 +718,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
717718
return cached
718719
}
719720

721+
override fun fixTextLayoutOnPixelDensityChange(): Boolean {
722+
var cached = fixTextLayoutOnPixelDensityChangeCache
723+
if (cached == null) {
724+
cached = currentProvider.fixTextLayoutOnPixelDensityChange()
725+
accessedFeatureFlags.add("fixTextLayoutOnPixelDensityChange")
726+
fixTextLayoutOnPixelDensityChangeCache = cached
727+
}
728+
return cached
729+
}
730+
720731
override fun fixYogaFlexBasisFitContentInMainAxis(): Boolean {
721732
var cached = fixYogaFlexBasisFitContentInMainAxisCache
722733
if (cached == null) {

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<0c7d62679814835a3ef86a63174c7f7f>>
7+
* @generated SignedSource<<c83fba10dc6f7d596e3bcd9725c64e6e>>
88
*/
99

1010
/**
@@ -143,6 +143,8 @@ public interface ReactNativeFeatureFlagsProvider {
143143

144144
@DoNotStrip public fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean
145145

146+
@DoNotStrip public fun fixTextLayoutOnPixelDensityChange(): Boolean
147+
146148
@DoNotStrip public fun fixYogaFlexBasisFitContentInMainAxis(): Boolean
147149

148150
@DoNotStrip public fun fuseboxAssertSingleHostState(): Boolean

‎packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,8 @@ void FabricMountingManager::executeMount(
599599

600600
bool maintainMutationOrder =
601601
ReactNativeFeatureFlags::disableMountItemReorderingAndroid();
602+
bool updateViewsOnDensityChange =
603+
ReactNativeFeatureFlags::fixTextLayoutOnPixelDensityChange();
602604

603605
auto revisionNumber = telemetry.getRevisionNumber();
604606

@@ -688,12 +690,19 @@ void FabricMountingManager::executeMount(
688690
CppMountItem::UpdateStateMountItem(newChildShadowView));
689691
}
690692

693+
bool pointScaleChanged = updateViewsOnDensityChange &&
694+
oldChildShadowView.layoutMetrics.pointScaleFactor !=
695+
newChildShadowView.layoutMetrics.pointScaleFactor;
696+
691697
// Padding: padding mountItems must be executed before layout props
692698
// are updated in the view. This is necessary to ensure that events
693699
// (resulting from layout changes) are dispatched with the correct
694700
// padding information.
695701
if (oldChildShadowView.layoutMetrics.contentInsets !=
696-
newChildShadowView.layoutMetrics.contentInsets) {
702+
newChildShadowView.layoutMetrics.contentInsets ||
703+
(pointScaleChanged &&
704+
newChildShadowView.layoutMetrics.contentInsets !=
705+
EdgeInsets::ZERO)) {
697706
(maintainMutationOrder ? cppCommonMountItems
698707
: cppUpdatePaddingMountItems)
699708
.push_back(
@@ -713,8 +722,11 @@ void FabricMountingManager::executeMount(
713722
// children of the current view. The layout of current view may not
714723
// change, and we separate this part from layout mount items to not
715724
// pack too much data there.
716-
if ((oldChildShadowView.layoutMetrics.overflowInset !=
717-
newChildShadowView.layoutMetrics.overflowInset)) {
725+
if (oldChildShadowView.layoutMetrics.overflowInset !=
726+
newChildShadowView.layoutMetrics.overflowInset ||
727+
(pointScaleChanged &&
728+
newChildShadowView.layoutMetrics.overflowInset !=
729+
EdgeInsets::ZERO)) {
718730
(maintainMutationOrder ? cppCommonMountItems
719731
: cppUpdateOverflowInsetMountItems)
720732
.push_back(

‎packages/react-native/ReactAndroid/src/main/jni/react/fabric/test_helper/FabricMountingManagerTestHelper.cpp‎

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,60 @@
77

88
#include "FabricMountingManagerTestHelper.h"
99

10+
#include <react/featureflags/ReactNativeFeatureFlags.h>
11+
#include <react/featureflags/ReactNativeFeatureFlagsDefaults.h>
1012
#include <react/renderer/animationbackend/AnimatedProps.h>
1113
#include <react/renderer/components/view/ViewComponentDescriptor.h>
1214
#include <react/renderer/components/view/ViewProps.h>
1315
#include <react/renderer/core/ComponentDescriptor.h>
16+
#include <react/renderer/core/PropsParserContext.h>
17+
#include <react/renderer/core/RawProps.h>
1418
#include <react/renderer/mounting/ShadowView.h>
1519

1620
namespace facebook::react {
1721

22+
static LayoutConstraints makeLayoutConstraints(
23+
jfloat widthDp,
24+
jfloat heightDp) {
25+
Size size{.width = widthDp, .height = heightDp};
26+
return LayoutConstraints{
27+
.minimumSize = size,
28+
.maximumSize = size,
29+
.layoutDirection = LayoutDirection::LeftToRight};
30+
}
31+
1832
FabricMountingManagerTestHelper::FabricMountingManagerTestHelper(
1933
jni::alias_ref<JFabricUIManager::javaobject> jFabricUIManager)
2034
: javaUIManager_(jni::make_global(jFabricUIManager)) {
2135
mountingManager_ = std::make_shared<FabricMountingManager>(javaUIManager_);
2236

37+
contextContainer_ = std::make_shared<ContextContainer>();
2338
auto params = ComponentDescriptorParameters{
2439
.eventDispatcher = EventDispatcher::Shared{},
25-
.contextContainer = std::make_shared<ContextContainer>(),
40+
.contextContainer = contextContainer_,
2641
.flavor = nullptr};
2742
viewComponentDescriptor_ = std::make_unique<ViewComponentDescriptor>(params);
43+
44+
class TestShadowTreeDelegate : public ShadowTreeDelegate {
45+
public:
46+
RootShadowNode::Unshared shadowTreeWillCommit(
47+
[[maybe_unused]] const ShadowTree& shadowTree,
48+
[[maybe_unused]] const RootShadowNode::Shared& oldRootShadowNode,
49+
const RootShadowNode::Unshared& newRootShadowNode,
50+
[[maybe_unused]] const ShadowTreeCommitOptions& commitOptions)
51+
const override {
52+
return newRootShadowNode;
53+
}
54+
55+
void shadowTreeDidFinishTransaction(
56+
[[maybe_unused]] std::shared_ptr<const MountingCoordinator>
57+
mountingCoordinator,
58+
[[maybe_unused]] bool mountSynchronously) const override {}
59+
60+
void shadowTreeDidFinishReactCommit(
61+
[[maybe_unused]] const ShadowTree& shadowTree) const override {}
62+
};
63+
shadowTreeDelegate_ = std::make_unique<TestShadowTreeDelegate>();
2864
}
2965

3066
void FabricMountingManagerTestHelper::initHybrid(
@@ -81,6 +117,112 @@ void FabricMountingManagerTestHelper::synchronouslyUpdateAnimatedProps(
81117
mountingManager_->synchronouslyUpdateAnimatedProps(updates);
82118
}
83119

120+
void FabricMountingManagerTestHelper::startSurfaceWithConstraints(
121+
jint surfaceId,
122+
jfloat widthDp,
123+
jfloat heightDp,
124+
jfloat pointScaleFactor) {
125+
shadowTree_ = std::make_unique<ShadowTree>(
126+
surfaceId,
127+
makeLayoutConstraints(widthDp, heightDp),
128+
LayoutContext{.pointScaleFactor = pointScaleFactor},
129+
*shadowTreeDelegate_,
130+
*contextContainer_);
131+
mountingManager_->onSurfaceStart(surfaceId);
132+
}
133+
134+
void FabricMountingManagerTestHelper::mountViewWithPadding(
135+
jint surfaceId,
136+
jint tag,
137+
jfloat padding) {
138+
using ShadowNodeList = std::vector<std::shared_ptr<const ShadowNode>>;
139+
PropsParserContext parserContext{surfaceId, *contextContainer_};
140+
auto createView = [&](Tag viewTag,
141+
folly::dynamic style,
142+
ShadowNodeList children) {
143+
auto family = viewComponentDescriptor_->createFamily(
144+
{.tag = viewTag, .surfaceId = surfaceId, .instanceHandle = nullptr});
145+
auto props = viewComponentDescriptor_->cloneProps(
146+
parserContext, nullptr, RawProps(std::move(style)));
147+
return viewComponentDescriptor_->createShadowNode(
148+
{.props = props,
149+
.children =
150+
std::make_shared<const ShadowNodeList>(std::move(children)),
151+
.state = viewComponentDescriptor_->createInitialState(props, family)},
152+
family);
153+
};
154+
155+
auto overflowingChild = createView(
156+
tag + 1,
157+
folly::dynamic::object("collapsable", false)("position", "absolute")(
158+
"left", -padding)("top", -padding)("width", 100 + 2 * padding)(
159+
"height", 100 + 2 * padding),
160+
{});
161+
auto paddedView = createView(
162+
tag,
163+
folly::dynamic::object("collapsable", false)("width", 100)("height", 100)(
164+
"padding", padding),
165+
{overflowingChild});
166+
167+
shadowTree_->commit(
168+
[&](const RootShadowNode& oldRootShadowNode) {
169+
return std::static_pointer_cast<RootShadowNode>(
170+
oldRootShadowNode.ShadowNode::clone(
171+
{.children = std::make_shared<const ShadowNodeList>(
172+
ShadowNodeList{paddedView})}));
173+
},
174+
{});
175+
mountPendingTransaction();
176+
}
177+
178+
void FabricMountingManagerTestHelper::updateSurfaceConstraints(
179+
jint surfaceId,
180+
jfloat widthDp,
181+
jfloat heightDp,
182+
jfloat pointScaleFactor) {
183+
PropsParserContext parserContext{surfaceId, *contextContainer_};
184+
shadowTree_->commit(
185+
[&](const RootShadowNode& oldRootShadowNode) {
186+
return oldRootShadowNode.clone(
187+
parserContext,
188+
makeLayoutConstraints(widthDp, heightDp),
189+
LayoutContext{.pointScaleFactor = pointScaleFactor});
190+
},
191+
{});
192+
mountPendingTransaction();
193+
}
194+
195+
void FabricMountingManagerTestHelper::overrideFeatureFlags(
196+
jboolean fixTextLayoutOnPixelDensityChange) {
197+
class TestFeatureFlags : public ReactNativeFeatureFlagsDefaults {
198+
public:
199+
explicit TestFeatureFlags(bool fixTextLayoutOnPixelDensityChange)
200+
: fixTextLayoutOnPixelDensityChange_(
201+
fixTextLayoutOnPixelDensityChange) {}
202+
203+
bool fixTextLayoutOnPixelDensityChange() override {
204+
return fixTextLayoutOnPixelDensityChange_;
205+
}
206+
207+
private:
208+
bool fixTextLayoutOnPixelDensityChange_;
209+
};
210+
ReactNativeFeatureFlags::dangerouslyForceOverride(
211+
std::make_unique<TestFeatureFlags>(
212+
fixTextLayoutOnPixelDensityChange != 0));
213+
}
214+
215+
void FabricMountingManagerTestHelper::resetFeatureFlags() {
216+
ReactNativeFeatureFlags::dangerouslyReset();
217+
}
218+
219+
void FabricMountingManagerTestHelper::mountPendingTransaction() {
220+
auto transaction = shadowTree_->getMountingCoordinator()->pullTransaction();
221+
if (transaction.has_value()) {
222+
mountingManager_->executeMount(*transaction);
223+
}
224+
}
225+
84226
void FabricMountingManagerTestHelper::registerNatives() {
85227
registerHybrid({
86228
makeNativeMethod(
@@ -99,6 +241,21 @@ void FabricMountingManagerTestHelper::registerNatives() {
99241
makeNativeMethod(
100242
"synchronouslyUpdateAnimatedProps",
101243
FabricMountingManagerTestHelper::synchronouslyUpdateAnimatedProps),
244+
makeNativeMethod(
245+
"startSurfaceWithConstraints",
246+
FabricMountingManagerTestHelper::startSurfaceWithConstraints),
247+
makeNativeMethod(
248+
"mountViewWithPadding",
249+
FabricMountingManagerTestHelper::mountViewWithPadding),
250+
makeNativeMethod(
251+
"updateSurfaceConstraints",
252+
FabricMountingManagerTestHelper::updateSurfaceConstraints),
253+
makeNativeMethod(
254+
"overrideFeatureFlags",
255+
FabricMountingManagerTestHelper::overrideFeatureFlags),
256+
makeNativeMethod(
257+
"resetFeatureFlags",
258+
FabricMountingManagerTestHelper::resetFeatureFlags),
102259
});
103260
}
104261

0 commit comments

Comments
 (0)