diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt index 7b9f9ee4500..06ee1e4f503 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt @@ -55,6 +55,8 @@ internal class FabricUIManagerBinding : HybridClassBase() { isMountable: Boolean ) + external fun findNextFocusableElement(parentTag: Int, focusedTag: Int, direction: Int): Int + external fun stopSurface(surfaceId: Int) external fun stopSurfaceWithSurfaceHandler(surfaceHandler: SurfaceHandlerBinding) diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp index e3224ffac39..5473ceb47d5 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp @@ -11,6 +11,7 @@ #include "ComponentFactory.h" #include "EventBeatManager.h" #include "FabricMountingManager.h" +#include "FocusOrderingHelper.h" #include #include @@ -195,6 +196,54 @@ void FabricUIManagerBinding::startSurface( } } +jint FabricUIManagerBinding::findNextFocusableElement( + jint parentTag, + jint focusedTag, + jint direction) { + ShadowNode::Shared nextNode; + + std::optional focusDirection = + FocusOrderingHelper::resolveFocusDirection(direction); + + if (!focusDirection.has_value()) { + return -1; + } + + std::shared_ptr uimanager = getScheduler()->getUIManager(); + + ShadowNode::Shared parentShadowNode = + uimanager->findShadowNodeByTag_DEPRECATED(parentTag); + ShadowNode::Shared focusedShadowNode = + FocusOrderingHelper::findShadowNodeByTagRecursively( + parentShadowNode, focusedTag); + + LayoutMetrics childLayoutMetrics = uimanager->getRelativeLayoutMetrics( + *focusedShadowNode, parentShadowNode.get(), {.includeTransform = true}); + + Rect sourceRect = childLayoutMetrics.frame; + + /* + * Traverse the tree recursively to find the next focusable element in the + * given direction + */ + std::optional nextRect = std::nullopt; + FocusOrderingHelper::traverseAndUpdateNextFocusableElement( + parentShadowNode, + focusedShadowNode, + parentShadowNode, + focusDirection.value(), + *uimanager, + sourceRect, + nextRect, + nextNode); + + if (nextNode == nullptr) { + return -1; + } + + return nextNode->getTag(); +} + // Used by non-bridgeless+Fabric void FabricUIManagerBinding::startSurfaceWithConstraints( jint surfaceId, @@ -667,6 +716,9 @@ void FabricUIManagerBinding::registerNatives() { makeNativeMethod( "stopSurfaceWithSurfaceHandler", FabricUIManagerBinding::stopSurfaceWithSurfaceHandler), + makeNativeMethod( + "findNextFocusableElement", + FabricUIManagerBinding::findNextFocusableElement), }); } diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h index c3c404c9990..fd298701562 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h @@ -132,6 +132,9 @@ class FabricUIManagerBinding : public jni::HybridClass, void reportMount(SurfaceId surfaceId); + jint + findNextFocusableElement(jint parentTag, jint focusedTag, jint direction); + void uninstallFabricUIManager(); // Private member variables diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FocusOrderingHelper.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FocusOrderingHelper.cpp new file mode 100644 index 00000000000..e30b1e2c89a --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FocusOrderingHelper.cpp @@ -0,0 +1,186 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "FocusOrderingHelper.h" +#include +#include + +namespace facebook::react { + +int majorAxisDistanceRaw( + FocusDirection focusDirection, + Rect source, + Rect dest) { + switch (focusDirection) { + case FocusDirection::FocusLeft: + return static_cast( + source.origin.x - (dest.origin.x + dest.size.width)); + case FocusDirection::FocusRight: + return static_cast( + dest.origin.x - (source.origin.x + source.size.width)); + case FocusDirection::FocusUp: + return static_cast( + source.origin.y - (dest.origin.y + dest.size.height)); + case FocusDirection::FocusDown: + return static_cast( + dest.origin.y - (source.origin.y + source.size.height)); + } +} + +int majorAxisDistance(FocusDirection focusDirection, Rect source, Rect dest) { + return std::max(0, majorAxisDistanceRaw(focusDirection, source, dest)); +} + +int minorAxisDistance(FocusDirection direction, Rect source, Rect dest) { + switch (direction) { + case FocusDirection::FocusLeft: + case FocusDirection::FocusRight: + // the distance between the center verticals + return static_cast(abs((source.getMidY() - dest.getMidY()))); + case FocusDirection::FocusUp: + case FocusDirection::FocusDown: + // the distance between the center horizontals + return static_cast(abs((source.getMidX() - dest.getMidX()))); + } +} + +// 13 is a magic number that comes from Android's implementation. We opt to use +// this to get the same focus ordering as Android. See: +// https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/view/FocusFinder.java;l=547 +int getWeightedDistanceFor(int majorAxisDistance, int minorAxisDistance) { + return 13 * majorAxisDistance * majorAxisDistance + + minorAxisDistance * minorAxisDistance; +} + +// Make sure dest rect is actually on the direction of focus +bool isCandidate(Rect source, Rect dest, FocusDirection focusDirection) { + switch (focusDirection) { + case FocusDirection::FocusLeft: + return ((source.origin.x + source.size.width) > + (dest.origin.x + dest.size.width) || + source.origin.x >= (dest.origin.x + dest.size.width)) && + source.origin.x > dest.origin.x; + case FocusDirection::FocusRight: + return (source.origin.x < dest.origin.x || + (source.origin.x + source.size.width) <= dest.origin.x) && + (source.origin.x + source.size.width) < + (dest.origin.x + dest.size.width); + case FocusDirection::FocusUp: + return ((source.origin.y + source.size.height) > + (dest.origin.y + dest.size.height) || + source.origin.y >= (dest.origin.y + dest.size.height)) && + source.origin.y > dest.origin.y; + case FocusDirection::FocusDown: + return (source.origin.y < dest.origin.y || + (source.origin.y + source.size.height) <= dest.origin.y) && + ((source.origin.y + source.size.height) < + (dest.origin.y + dest.size.height)); + } +} + +bool isBetterCandidate( + FocusDirection focusDirection, + Rect source, + Rect current, + Rect candidate) { + if (!isCandidate(source, candidate, focusDirection)) { + return false; + } + + int candidateWeightedDistance = getWeightedDistanceFor( + majorAxisDistance(focusDirection, source, candidate), + minorAxisDistance(focusDirection, source, candidate)); + + int currentWeightedDistance = getWeightedDistanceFor( + majorAxisDistance(focusDirection, source, current), + minorAxisDistance(focusDirection, source, current)); + + return candidateWeightedDistance < currentWeightedDistance; +} + +void FocusOrderingHelper::traverseAndUpdateNextFocusableElement( + const ShadowNode::Shared& parentShadowNode, + const ShadowNode::Shared& focusedShadowNode, + const ShadowNode::Shared& currNode, + FocusDirection focusDirection, + const UIManager& uimanager, + Rect sourceRect, + std::optional& nextRect, + ShadowNode::Shared& nextNode) { + const auto* props = + dynamic_cast(currNode->getProps().get()); + + // We only care about focusable elements since only they can be both + // focused and present in the hierarchy + if (currNode->getTraits().check(ShadowNodeTraits::Trait::KeyboardFocusable) || + (props != nullptr && + (props->focusable || props->accessible || props->hasTVPreferredFocus))) { + LayoutMetrics nodeLayoutMetrics = uimanager.getRelativeLayoutMetrics( + *currNode, parentShadowNode.get(), {.includeTransform = true}); + + if (nextRect == std::nullopt && + isCandidate(sourceRect, nodeLayoutMetrics.frame, focusDirection)) { + nextNode = currNode; + nextRect = nodeLayoutMetrics.frame; + } else if ( + nextRect != std::nullopt && + isBetterCandidate( + focusDirection, + sourceRect, + nextRect.value(), + nodeLayoutMetrics.frame)) { + nextNode = currNode; + nextRect = nodeLayoutMetrics.frame; + } + } + + for (auto& child : currNode->getChildren()) { + if (child->getTraits().check(ShadowNodeTraits::Trait::RootNodeKind)) { + continue; + } + + traverseAndUpdateNextFocusableElement( + parentShadowNode, + focusedShadowNode, + child, + focusDirection, + uimanager, + sourceRect, + nextRect, + nextNode); + }; +}; + +ShadowNode::Shared FocusOrderingHelper::findShadowNodeByTagRecursively( + const ShadowNode::Shared& parentShadowNode, + Tag tag) { + if (parentShadowNode->getTag() == tag) { + return parentShadowNode; + } + + for (auto& shadowNode : parentShadowNode->getChildren()) { + if (auto result = findShadowNodeByTagRecursively(shadowNode, tag)) { + return result; + } + } + + return nullptr; +} + +std::optional FocusOrderingHelper::resolveFocusDirection( + int direction) { + switch (static_cast(direction)) { + case FocusDirection::FocusDown: + case FocusDirection::FocusUp: + case FocusDirection::FocusRight: + case FocusDirection::FocusLeft: + return static_cast(direction); + } + + return std::nullopt; +} +} // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FocusOrderingHelper.h b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FocusOrderingHelper.h new file mode 100644 index 00000000000..97eeba7d1ea --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FocusOrderingHelper.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include "FabricUIManagerBinding.h" + +namespace facebook::react { + +enum class FocusDirection { + FocusDown = 0, + FocusUp = 1, + FocusRight = 2, + FocusLeft = 3, +}; + +class FocusOrderingHelper { + public: + static void traverseAndUpdateNextFocusableElement( + const ShadowNode::Shared& parentShadowNode, + const ShadowNode::Shared& focusedShadowNode, + const ShadowNode::Shared& currNode, + FocusDirection focusDirection, + const UIManager& uimanager, + Rect sourceRect, + std::optional& nextRect, + ShadowNode::Shared& nextNode); + + static ShadowNode::Shared findShadowNodeByTagRecursively( + const ShadowNode::Shared& parentShadowNode, + Tag tag); + + static std::optional resolveFocusDirection(int direction); +}; +} // namespace facebook::react