Skip to content

Commit 75c56cf

Browse files
jorge-cabfacebook-github-bot
authored andcommitted
Add fabric implementation to find next focusable view (#50196)
Summary: Currently when `removeClippedSubviews` is enabled on Android keyboard navigation breaks and we can never focus the elements that are clipped. iOS has a similar issue but not as drastic, it only happens when elements on the FlatList have a lot of margin between them. This algorithm aims to find the next focusable view and return it to native so that we can prevent the clipping of the view on the view clipping algorithm and hence fix keyboard navigation. For more information see D71324219 Fabric algorithm to find the next focusable view given: `parentTag`: Top most relevant parent of the focused view `focusedTag`: Tag of the currently focused view `direction`: Direction in which focus is moving Changelog: [Internal] Reviewed By: joevilches Differential Revision: D71558965
1 parent 9d5fe7a commit 75c56cf

File tree

5 files changed

+281
-0
lines changed

5 files changed

+281
-0
lines changed

Diff for: packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt

+2
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ internal class FabricUIManagerBinding : HybridClassBase() {
5555
isMountable: Boolean
5656
)
5757

58+
external fun findNextFocusableElement(parentTag: Int, focusedTag: Int, direction: Int): Int
59+
5860
external fun stopSurface(surfaceId: Int)
5961

6062
external fun stopSurfaceWithSurfaceHandler(surfaceHandler: SurfaceHandlerBinding)

Diff for: packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp

+52
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include "ComponentFactory.h"
1212
#include "EventBeatManager.h"
1313
#include "FabricMountingManager.h"
14+
#include "FocusOrderingHelper.h"
1415

1516
#include <cxxreact/TraceSection.h>
1617
#include <fbjni/fbjni.h>
@@ -195,6 +196,54 @@ void FabricUIManagerBinding::startSurface(
195196
}
196197
}
197198

199+
jint FabricUIManagerBinding::findNextFocusableElement(
200+
jint parentTag,
201+
jint focusedTag,
202+
jint direction) {
203+
ShadowNode::Shared nextNode;
204+
205+
std::optional<FocusDirection> focusDirection =
206+
FocusOrderingHelper::resolveFocusDirection(direction);
207+
208+
if (!focusDirection.has_value()) {
209+
return -1;
210+
}
211+
212+
std::shared_ptr<UIManager> uimanager = getScheduler()->getUIManager();
213+
214+
ShadowNode::Shared parentShadowNode =
215+
uimanager->findShadowNodeByTag_DEPRECATED(parentTag);
216+
ShadowNode::Shared focusedShadowNode =
217+
FocusOrderingHelper::findShadowNodeByTagRecursively(
218+
parentShadowNode, focusedTag);
219+
220+
LayoutMetrics childLayoutMetrics = uimanager->getRelativeLayoutMetrics(
221+
*focusedShadowNode, parentShadowNode.get(), {.includeTransform = true});
222+
223+
Rect sourceRect = childLayoutMetrics.frame;
224+
225+
/*
226+
* Traverse the tree recursively to find the next focusable element in the
227+
* given direction
228+
*/
229+
std::optional<Rect> nextRect = std::nullopt;
230+
FocusOrderingHelper::traverseAndUpdateNextFocusableElement(
231+
parentShadowNode,
232+
focusedShadowNode,
233+
parentShadowNode,
234+
focusDirection.value(),
235+
*uimanager,
236+
sourceRect,
237+
nextRect,
238+
nextNode);
239+
240+
if (nextNode == nullptr) {
241+
return -1;
242+
}
243+
244+
return nextNode->getTag();
245+
}
246+
198247
// Used by non-bridgeless+Fabric
199248
void FabricUIManagerBinding::startSurfaceWithConstraints(
200249
jint surfaceId,
@@ -667,6 +716,9 @@ void FabricUIManagerBinding::registerNatives() {
667716
makeNativeMethod(
668717
"stopSurfaceWithSurfaceHandler",
669718
FabricUIManagerBinding::stopSurfaceWithSurfaceHandler),
719+
makeNativeMethod(
720+
"findNextFocusableElement",
721+
FabricUIManagerBinding::findNextFocusableElement),
670722
});
671723
}
672724

Diff for: packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h

+3
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ class FabricUIManagerBinding : public jni::HybridClass<FabricUIManagerBinding>,
132132

133133
void reportMount(SurfaceId surfaceId);
134134

135+
jint
136+
findNextFocusableElement(jint parentTag, jint focusedTag, jint direction);
137+
135138
void uninstallFabricUIManager();
136139

137140
// Private member variables
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include "FocusOrderingHelper.h"
9+
#include <android/log.h>
10+
#include <react/renderer/uimanager/UIManager.h>
11+
12+
namespace facebook::react {
13+
14+
int majorAxisDistanceRaw(
15+
FocusDirection focusDirection,
16+
Rect source,
17+
Rect dest) {
18+
switch (focusDirection) {
19+
case FocusDirection::FocusLeft:
20+
return static_cast<int>(
21+
source.origin.x - (dest.origin.x + dest.size.width));
22+
case FocusDirection::FocusRight:
23+
return static_cast<int>(
24+
dest.origin.x - (source.origin.x + source.size.width));
25+
case FocusDirection::FocusUp:
26+
return static_cast<int>(
27+
source.origin.y - (dest.origin.y + dest.size.height));
28+
case FocusDirection::FocusDown:
29+
return static_cast<int>(
30+
dest.origin.y - (source.origin.y + source.size.height));
31+
}
32+
}
33+
34+
int majorAxisDistance(FocusDirection focusDirection, Rect source, Rect dest) {
35+
return std::max(0, majorAxisDistanceRaw(focusDirection, source, dest));
36+
}
37+
38+
int minorAxisDistance(FocusDirection direction, Rect source, Rect dest) {
39+
switch (direction) {
40+
case FocusDirection::FocusLeft:
41+
case FocusDirection::FocusRight:
42+
// the distance between the center verticals
43+
return static_cast<int>(abs((source.getMidY() - dest.getMidY())));
44+
case FocusDirection::FocusUp:
45+
case FocusDirection::FocusDown:
46+
// the distance between the center horizontals
47+
return static_cast<int>(abs((source.getMidX() - dest.getMidX())));
48+
}
49+
}
50+
51+
// 13 is a magic number that comes from Android's implementation. We opt to use
52+
// this to get the same focus ordering as Android. See:
53+
// https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/view/FocusFinder.java;l=547
54+
int getWeightedDistanceFor(int majorAxisDistance, int minorAxisDistance) {
55+
return 13 * majorAxisDistance * majorAxisDistance +
56+
minorAxisDistance * minorAxisDistance;
57+
}
58+
59+
// Make sure dest rect is actually on the direction of focus
60+
bool isCandidate(Rect source, Rect dest, FocusDirection focusDirection) {
61+
switch (focusDirection) {
62+
case FocusDirection::FocusLeft:
63+
return ((source.origin.x + source.size.width) >
64+
(dest.origin.x + dest.size.width) ||
65+
source.origin.x >= (dest.origin.x + dest.size.width)) &&
66+
source.origin.x > dest.origin.x;
67+
case FocusDirection::FocusRight:
68+
return (source.origin.x < dest.origin.x ||
69+
(source.origin.x + source.size.width) <= dest.origin.x) &&
70+
(source.origin.x + source.size.width) <
71+
(dest.origin.x + dest.size.width);
72+
case FocusDirection::FocusUp:
73+
return ((source.origin.y + source.size.height) >
74+
(dest.origin.y + dest.size.height) ||
75+
source.origin.y >= (dest.origin.y + dest.size.height)) &&
76+
source.origin.y > dest.origin.y;
77+
case FocusDirection::FocusDown:
78+
return (source.origin.y < dest.origin.y ||
79+
(source.origin.y + source.size.height) <= dest.origin.y) &&
80+
((source.origin.y + source.size.height) <
81+
(dest.origin.y + dest.size.height));
82+
}
83+
}
84+
85+
bool isBetterCandidate(
86+
FocusDirection focusDirection,
87+
Rect source,
88+
Rect current,
89+
Rect candidate) {
90+
if (!isCandidate(source, candidate, focusDirection)) {
91+
return false;
92+
}
93+
94+
int candidateWeightedDistance = getWeightedDistanceFor(
95+
majorAxisDistance(focusDirection, source, candidate),
96+
minorAxisDistance(focusDirection, source, candidate));
97+
98+
int currentWeightedDistance = getWeightedDistanceFor(
99+
majorAxisDistance(focusDirection, source, current),
100+
minorAxisDistance(focusDirection, source, current));
101+
102+
return candidateWeightedDistance < currentWeightedDistance;
103+
}
104+
105+
void FocusOrderingHelper::traverseAndUpdateNextFocusableElement(
106+
const ShadowNode::Shared& parentShadowNode,
107+
const ShadowNode::Shared& focusedShadowNode,
108+
const ShadowNode::Shared& currNode,
109+
FocusDirection focusDirection,
110+
const UIManager& uimanager,
111+
Rect sourceRect,
112+
std::optional<Rect>& nextRect,
113+
ShadowNode::Shared& nextNode) {
114+
const auto* props =
115+
dynamic_cast<const ViewProps*>(currNode->getProps().get());
116+
117+
// We only care about focusable elements since only they can be both
118+
// focused and present in the hierarchy
119+
if (currNode->getTraits().check(ShadowNodeTraits::Trait::KeyboardFocusable) ||
120+
(props != nullptr &&
121+
(props->focusable || props->accessible || props->hasTVPreferredFocus))) {
122+
LayoutMetrics nodeLayoutMetrics = uimanager.getRelativeLayoutMetrics(
123+
*currNode, parentShadowNode.get(), {.includeTransform = true});
124+
125+
if (nextRect == std::nullopt &&
126+
isCandidate(sourceRect, nodeLayoutMetrics.frame, focusDirection)) {
127+
nextNode = currNode;
128+
nextRect = nodeLayoutMetrics.frame;
129+
} else if (
130+
nextRect != std::nullopt &&
131+
isBetterCandidate(
132+
focusDirection,
133+
sourceRect,
134+
nextRect.value(),
135+
nodeLayoutMetrics.frame)) {
136+
nextNode = currNode;
137+
nextRect = nodeLayoutMetrics.frame;
138+
}
139+
}
140+
141+
for (auto& child : currNode->getChildren()) {
142+
if (child->getTraits().check(ShadowNodeTraits::Trait::RootNodeKind)) {
143+
continue;
144+
}
145+
146+
traverseAndUpdateNextFocusableElement(
147+
parentShadowNode,
148+
focusedShadowNode,
149+
child,
150+
focusDirection,
151+
uimanager,
152+
sourceRect,
153+
nextRect,
154+
nextNode);
155+
};
156+
};
157+
158+
ShadowNode::Shared FocusOrderingHelper::findShadowNodeByTagRecursively(
159+
const ShadowNode::Shared& parentShadowNode,
160+
Tag tag) {
161+
if (parentShadowNode->getTag() == tag) {
162+
return parentShadowNode;
163+
}
164+
165+
for (auto& shadowNode : parentShadowNode->getChildren()) {
166+
if (auto result = findShadowNodeByTagRecursively(shadowNode, tag)) {
167+
return result;
168+
}
169+
}
170+
171+
return nullptr;
172+
}
173+
174+
std::optional<FocusDirection> FocusOrderingHelper::resolveFocusDirection(
175+
int direction) {
176+
switch (static_cast<FocusDirection>(direction)) {
177+
case FocusDirection::FocusDown:
178+
case FocusDirection::FocusUp:
179+
case FocusDirection::FocusRight:
180+
case FocusDirection::FocusLeft:
181+
return static_cast<FocusDirection>(direction);
182+
}
183+
184+
return std::nullopt;
185+
}
186+
} // namespace facebook::react
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include <react/renderer/uimanager/UIManager.h>
9+
#include "FabricUIManagerBinding.h"
10+
11+
namespace facebook::react {
12+
13+
enum class FocusDirection {
14+
FocusDown = 0,
15+
FocusUp = 1,
16+
FocusRight = 2,
17+
FocusLeft = 3,
18+
};
19+
20+
class FocusOrderingHelper {
21+
public:
22+
static void traverseAndUpdateNextFocusableElement(
23+
const ShadowNode::Shared& parentShadowNode,
24+
const ShadowNode::Shared& focusedShadowNode,
25+
const ShadowNode::Shared& currNode,
26+
FocusDirection focusDirection,
27+
const UIManager& uimanager,
28+
Rect sourceRect,
29+
std::optional<Rect>& nextRect,
30+
ShadowNode::Shared& nextNode);
31+
32+
static ShadowNode::Shared findShadowNodeByTagRecursively(
33+
const ShadowNode::Shared& parentShadowNode,
34+
Tag tag);
35+
36+
static std::optional<FocusDirection> resolveFocusDirection(int direction);
37+
};
38+
} // namespace facebook::react

0 commit comments

Comments
 (0)