mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Fix keyboard navigation for FlatList with removeClippedSubviews enabled (#49543)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49543 When using `ReactScrollView` or `ReactHorizontalScrollView` Views with `removeClippedSubviews` keyboard navigation didn't work. This is because keyboard navigation relies on Android's View hierarchy to find the next focusable element. With `removeClippedSubviews` the next View might've been removed from the hierarchy. With this change we delegate the job of figuring out the next focusable element to the Shadow Tree, which will always contain layout information of the next element of the ScrollView. Changelog: [Android][Fixed] - Fix keyboard navigation on lists with `removeClippedSubviews` enabled Reviewed By: joevilches Differential Revision: D69618406 fbshipit-source-id: 1df7f90066dfc685e74a89b29222937777714b87
This commit is contained in:
committed by
Facebook GitHub Bot
parent
81405b450c
commit
c068c599c6
@@ -2396,6 +2396,7 @@ public class com/facebook/react/fabric/FabricUIManager : com/facebook/react/brid
|
||||
public fun dispatchCommand (IILcom/facebook/react/bridge/ReadableArray;)V
|
||||
public fun dispatchCommand (IILjava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V
|
||||
public fun dispatchCommand (ILjava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V
|
||||
public fun findNextFocusableElementMetrics (III)Lcom/facebook/react/fabric/NextFocusableNode;
|
||||
public fun getColor (I[Ljava/lang/String;)I
|
||||
public fun getEventDispatcher ()Lcom/facebook/react/uimanager/events/EventDispatcher;
|
||||
public fun getPerformanceCounters ()Ljava/util/Map;
|
||||
@@ -2435,6 +2436,12 @@ public final class com/facebook/react/fabric/FabricUIManagerProviderImpl : com/f
|
||||
public fun createUIManager (Lcom/facebook/react/bridge/ReactApplicationContext;)Lcom/facebook/react/bridge/UIManager;
|
||||
}
|
||||
|
||||
public final class com/facebook/react/fabric/NextFocusableNode {
|
||||
public fun <init> (IF)V
|
||||
public final fun getDeltaScroll ()F
|
||||
public final fun getId ()I
|
||||
}
|
||||
|
||||
public class com/facebook/react/fabric/StateWrapperImpl : com/facebook/jni/HybridClassBase, com/facebook/react/uimanager/StateWrapper {
|
||||
public fun destroyState ()V
|
||||
public fun getStateData ()Lcom/facebook/react/bridge/ReadableNativeMap;
|
||||
@@ -6198,6 +6205,7 @@ public class com/facebook/react/views/scroll/ReactHorizontalScrollView : android
|
||||
public fun executeKeyEvent (Landroid/view/KeyEvent;)Z
|
||||
public fun flashScrollIndicators ()V
|
||||
public fun fling (I)V
|
||||
public fun focusSearch (Landroid/view/View;I)Landroid/view/View;
|
||||
public fun getChildVisibleRect (Landroid/view/View;Landroid/graphics/Rect;Landroid/graphics/Point;)Z
|
||||
public fun getClippingRect (Landroid/graphics/Rect;)V
|
||||
public fun getFlingAnimator ()Landroid/animation/ValueAnimator;
|
||||
@@ -6320,6 +6328,7 @@ public class com/facebook/react/views/scroll/ReactScrollView : android/widget/Sc
|
||||
public fun executeKeyEvent (Landroid/view/KeyEvent;)Z
|
||||
public fun flashScrollIndicators ()V
|
||||
public fun fling (I)V
|
||||
public fun focusSearch (Landroid/view/View;I)Landroid/view/View;
|
||||
public fun getChildVisibleRect (Landroid/view/View;Landroid/graphics/Rect;Landroid/graphics/Point;)Z
|
||||
public fun getClippingRect (Landroid/graphics/Rect;)V
|
||||
public fun getFlingAnimator ()Landroid/animation/ValueAnimator;
|
||||
@@ -6440,6 +6449,8 @@ public final class com/facebook/react/views/scroll/ReactScrollViewHelper {
|
||||
public static final fun emitScrollEvent (Landroid/view/ViewGroup;FF)V
|
||||
public static final fun emitScrollMomentumBeginEvent (Landroid/view/ViewGroup;II)V
|
||||
public static final fun emitScrollMomentumEndEvent (Landroid/view/ViewGroup;)V
|
||||
public static final fun findNextClippedElement (Landroid/view/ViewGroup;Landroid/view/View;ILcom/facebook/react/bridge/ReactContext;)Landroid/view/View;
|
||||
public static final fun findNextFocusableView (Landroid/view/ViewGroup;Landroid/view/View;IZ)Landroid/view/View;
|
||||
public static final fun forceUpdateState (Landroid/view/ViewGroup;)V
|
||||
public static final fun getDefaultScrollAnimationDuration (Landroid/content/Context;)I
|
||||
public static final fun getNextFlingStartValue (Landroid/view/ViewGroup;III)I
|
||||
@@ -6449,6 +6460,7 @@ public final class com/facebook/react/views/scroll/ReactScrollViewHelper {
|
||||
public final fun registerFlingAnimator (Landroid/view/ViewGroup;)V
|
||||
public static final fun removeLayoutChangeListener (Lcom/facebook/react/views/scroll/ReactScrollViewHelper$LayoutChangeListener;)V
|
||||
public static final fun removeScrollListener (Lcom/facebook/react/views/scroll/ReactScrollViewHelper$ScrollListener;)V
|
||||
public static final fun resolveAbsoluteDirection (IZI)I
|
||||
public static final fun smoothScrollTo (Landroid/view/ViewGroup;II)V
|
||||
public static final fun updateFabricScrollState (Landroid/view/ViewGroup;)V
|
||||
public final fun updateFabricScrollState (Landroid/view/ViewGroup;II)V
|
||||
|
||||
+46
@@ -29,6 +29,7 @@ import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.core.view.ViewCompat.FocusRealDirection;
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.infer.annotation.ThreadConfined;
|
||||
import com.facebook.proguard.annotations.DoNotStripAny;
|
||||
@@ -260,6 +261,51 @@ public class FabricUIManager
|
||||
return rootTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the next focusable element's id and position relative to the parent from the shadow tree
|
||||
* based on the current focusable element and the direction.
|
||||
*
|
||||
* @return A NextFocusableNode object where the 'id' is the reactId/Tag of the next focusable
|
||||
* view, and 'deltaScroll' is the scroll delta needed to make the view visible on the screen.
|
||||
* Returns null if no valid node is found.
|
||||
*/
|
||||
public @Nullable NextFocusableNode findNextFocusableElementMetrics(
|
||||
int parentTag, int focusedTag, @FocusRealDirection int direction) {
|
||||
if (mBinding == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int generalizedDirection;
|
||||
|
||||
switch (direction) {
|
||||
case View.FOCUS_DOWN:
|
||||
generalizedDirection = 0;
|
||||
break;
|
||||
case View.FOCUS_UP:
|
||||
generalizedDirection = 1;
|
||||
break;
|
||||
case View.FOCUS_RIGHT:
|
||||
generalizedDirection = 2;
|
||||
break;
|
||||
case View.FOCUS_LEFT:
|
||||
generalizedDirection = 3;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
float[] serializedNextFocusableNodeMetrics =
|
||||
mBinding.findNextFocusableElementMetrics(parentTag, focusedTag, generalizedDirection);
|
||||
|
||||
if (serializedNextFocusableNodeMetrics == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new NextFocusableNode(
|
||||
(int) serializedNextFocusableNodeMetrics[0], serializedNextFocusableNodeMetrics[1]);
|
||||
}
|
||||
|
||||
@Override
|
||||
@AnyThread
|
||||
@ThreadConfined(ANY)
|
||||
|
||||
+6
@@ -55,6 +55,12 @@ internal class FabricUIManagerBinding : HybridClassBase() {
|
||||
isMountable: Boolean
|
||||
)
|
||||
|
||||
external fun findNextFocusableElementMetrics(
|
||||
parentTag: Int,
|
||||
focusedTag: Int,
|
||||
direction: Int
|
||||
): FloatArray
|
||||
|
||||
external fun stopSurface(surfaceId: Int)
|
||||
|
||||
external fun stopSurfaceWithSurfaceHandler(surfaceHandler: SurfaceHandlerBinding)
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package com.facebook.react.fabric
|
||||
|
||||
public class NextFocusableNode(public val id: Int, public val deltaScroll: Float)
|
||||
+13
@@ -11,6 +11,7 @@ import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNME
|
||||
import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_DISABLED;
|
||||
import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_END;
|
||||
import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_START;
|
||||
import static com.facebook.react.views.scroll.ReactScrollViewHelper.findNextFocusableView;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.ValueAnimator;
|
||||
@@ -31,6 +32,7 @@ import android.widget.HorizontalScrollView;
|
||||
import android.widget.OverScroller;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.ViewCompat.FocusRealDirection;
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.infer.annotation.Nullsafe;
|
||||
@@ -772,6 +774,17 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View focusSearch(View focused, @FocusRealDirection int direction) {
|
||||
@Nullable View nextfocusableView = findNextFocusableView(this, focused, direction, true);
|
||||
|
||||
if (nextfocusableView != null) {
|
||||
return nextfocusableView;
|
||||
}
|
||||
|
||||
return super.focusSearch(focused, direction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateClippingRect() {
|
||||
if (!mRemoveClippedSubviews) {
|
||||
|
||||
+14
@@ -11,6 +11,7 @@ import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNME
|
||||
import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_DISABLED;
|
||||
import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_END;
|
||||
import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_START;
|
||||
import static com.facebook.react.views.scroll.ReactScrollViewHelper.findNextFocusableView;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.ValueAnimator;
|
||||
@@ -31,6 +32,7 @@ import android.widget.ScrollView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.ViewCompat.FocusRealDirection;
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.infer.annotation.Nullsafe;
|
||||
@@ -359,6 +361,18 @@ public class ReactScrollView extends ScrollView
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View focusSearch(View focused, @FocusRealDirection int direction) {
|
||||
|
||||
@Nullable View nextfocusableView = findNextFocusableView(this, focused, direction, false);
|
||||
|
||||
if (nextfocusableView != null) {
|
||||
return nextfocusableView;
|
||||
}
|
||||
|
||||
return super.focusSearch(focused, direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Since ReactScrollView handles layout changes on JS side, it does not call super.onlayout due to
|
||||
* which mIsLayoutDirty flag in ScrollView remains true and prevents scrolling to child when
|
||||
|
||||
+87
@@ -11,15 +11,19 @@ import android.animation.Animator
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.Point
|
||||
import android.view.FocusFinder
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.OverScroller
|
||||
import androidx.core.view.ViewCompat.FocusRealDirection
|
||||
import com.facebook.common.logging.FLog
|
||||
import com.facebook.react.bridge.ReactContext
|
||||
import com.facebook.react.bridge.WritableMap
|
||||
import com.facebook.react.bridge.WritableNativeMap
|
||||
import com.facebook.react.common.ReactConstants
|
||||
import com.facebook.react.fabric.FabricUIManager
|
||||
import com.facebook.react.uimanager.PixelUtil.toDIPFromPixel
|
||||
import com.facebook.react.uimanager.PixelUtil.toPixelFromDIP
|
||||
import com.facebook.react.uimanager.StateWrapper
|
||||
import com.facebook.react.uimanager.UIManagerHelper
|
||||
import com.facebook.react.uimanager.common.UIManagerType
|
||||
@@ -462,6 +466,89 @@ public object ReactScrollViewHelper {
|
||||
return Point(scroller.finalX, scroller.finalY)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun findNextFocusableView(
|
||||
host: ViewGroup,
|
||||
focused: View,
|
||||
@FocusRealDirection direction: Int,
|
||||
horizontal: Boolean
|
||||
): View? {
|
||||
val absDir = resolveAbsoluteDirection(direction, horizontal, host.getLayoutDirection())
|
||||
|
||||
/*
|
||||
* Check if we can focus the next element in the absolute direction within the ScrollView,
|
||||
* if we can't, look into the shadow tree to find the next focusable element
|
||||
*/
|
||||
val ff = FocusFinder.getInstance()
|
||||
val result = ff.findNextFocus(host, focused, absDir)
|
||||
|
||||
if (result != null) {
|
||||
return result
|
||||
}
|
||||
|
||||
/*
|
||||
* Attempt to focus the next focusable but clipped element on the list if there is one, since
|
||||
* the view is clipped it is not currently in the hierarchy so we scroll it into view and then
|
||||
* focus it.
|
||||
*/
|
||||
return findNextClippedElement(host, focused, absDir, host.context as ReactContext)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to focus the next element in the specified direction within the scrollView.
|
||||
*
|
||||
* @return true if a new element was successfully focused, otherwise false.
|
||||
*/
|
||||
@JvmStatic
|
||||
public fun findNextClippedElement(
|
||||
scrollView: ViewGroup,
|
||||
focused: View,
|
||||
@FocusRealDirection direction: Int,
|
||||
context: ReactContext,
|
||||
): View? {
|
||||
val uimanager = UIManagerHelper.getUIManager(context, UIManagerType.FABRIC) ?: return null
|
||||
|
||||
val nextFocusableViewMetrics =
|
||||
(uimanager as FabricUIManager).findNextFocusableElementMetrics(
|
||||
scrollView.id, focused.id, direction)
|
||||
|
||||
if (nextFocusableViewMetrics != null) {
|
||||
|
||||
when (direction) {
|
||||
View.FOCUS_UP,
|
||||
View.FOCUS_DOWN -> {
|
||||
scrollView.scrollBy(0, toPixelFromDIP(nextFocusableViewMetrics.deltaScroll).toInt())
|
||||
}
|
||||
View.FOCUS_RIGHT,
|
||||
View.FOCUS_LEFT -> {
|
||||
scrollView.scrollBy(toPixelFromDIP(nextFocusableViewMetrics.deltaScroll).toInt(), 0)
|
||||
}
|
||||
else -> return null
|
||||
}
|
||||
return scrollView.findViewById(nextFocusableViewMetrics.id)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun resolveAbsoluteDirection(
|
||||
@FocusRealDirection direction: Int,
|
||||
horizontal: Boolean,
|
||||
layoutDirection: Int
|
||||
): Int {
|
||||
val rtl: Boolean = layoutDirection == View.LAYOUT_DIRECTION_RTL
|
||||
|
||||
return if (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD) {
|
||||
if (horizontal) {
|
||||
if ((direction == View.FOCUS_FORWARD) != rtl) View.FOCUS_RIGHT else View.FOCUS_LEFT
|
||||
} else {
|
||||
if (direction == View.FOCUS_FORWARD) View.FOCUS_DOWN else View.FOCUS_UP
|
||||
}
|
||||
} else {
|
||||
direction
|
||||
}
|
||||
}
|
||||
|
||||
public interface ScrollListener {
|
||||
public fun onScroll(
|
||||
scrollView: ViewGroup?,
|
||||
|
||||
+104
@@ -12,6 +12,7 @@
|
||||
#include "EventBeatManager.h"
|
||||
#include "EventEmitterWrapper.h"
|
||||
#include "FabricMountingManager.h"
|
||||
#include "FocusOrderingHelper.h"
|
||||
|
||||
#include <cxxreact/TraceSection.h>
|
||||
#include <fbjni/fbjni.h>
|
||||
@@ -195,6 +196,106 @@ void FabricUIManagerBinding::startSurface(
|
||||
}
|
||||
}
|
||||
|
||||
jfloatArray FabricUIManagerBinding::findNextFocusableElementMetrics(
|
||||
jint parentTag,
|
||||
jint focusedTag,
|
||||
jint direction) {
|
||||
/*
|
||||
* Holds the values of the next focusable element
|
||||
* [0] - tag
|
||||
* [1] - delta
|
||||
*/
|
||||
std::array<float, 2> nodeValues{-1, -1};
|
||||
|
||||
JNIEnv* env = jni::Environment::current();
|
||||
|
||||
ShadowNode::Shared nextNode;
|
||||
|
||||
auto nodeValuesSize = static_cast<jsize>(nodeValues.size());
|
||||
jfloatArray result = env->NewFloatArray(nodeValuesSize);
|
||||
|
||||
std::optional<FocusDirection> focusDirection =
|
||||
FocusOrderingHelper::resolveFocusDirection(direction);
|
||||
|
||||
if (!focusDirection.has_value()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ShadowNode::Shared parentShadowNode;
|
||||
ShadowNode::Shared focusedShadowNode;
|
||||
std::shared_ptr<UIManager> uimanager = getScheduler()->getUIManager();
|
||||
|
||||
parentShadowNode = uimanager->findShadowNodeByTag_DEPRECATED(parentTag);
|
||||
focusedShadowNode = FocusOrderingHelper::findShadowNodeByTagRecursively(
|
||||
parentShadowNode, focusedTag);
|
||||
|
||||
LayoutMetrics childLayoutMetrics = uimanager->getRelativeLayoutMetrics(
|
||||
*focusedShadowNode, parentShadowNode.get(), {.includeTransform = true});
|
||||
|
||||
/*
|
||||
* Set currentDelta to the origin of the focused node
|
||||
* Set nextDelta to the max int value in the direction we want to go (down =
|
||||
* INT_MIN, up = INT_MAX)
|
||||
*/
|
||||
auto [currentDelta, nextDelta] = FocusOrderingHelper::initScrollDeltas(
|
||||
focusDirection.value(), childLayoutMetrics.frame.origin);
|
||||
|
||||
/*
|
||||
* Traverse the tree recursively to find the next focusable element in the
|
||||
* given direction
|
||||
*/
|
||||
FocusOrderingHelper::traverseAndUpdateNextFocusableElementMetrics(
|
||||
parentShadowNode,
|
||||
focusedShadowNode,
|
||||
parentShadowNode,
|
||||
focusDirection.value(),
|
||||
*uimanager,
|
||||
currentDelta,
|
||||
nextDelta,
|
||||
nextNode);
|
||||
|
||||
if (nextNode == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
LayoutMetrics nextNodeLayoutMetrics = uimanager->getRelativeLayoutMetrics(
|
||||
*nextNode, parentShadowNode.get(), {.includeTransform = true});
|
||||
|
||||
LayoutMetrics parentNodeLayoutMetrics = uimanager->getRelativeLayoutMetrics(
|
||||
*parentShadowNode, nullptr, {.includeTransform = true});
|
||||
|
||||
nodeValues[0] = static_cast<float>(nextNode->getTag());
|
||||
|
||||
/*
|
||||
* In the end we want to scroll enough to make the next node visible
|
||||
* When going up or right we want delta to be the origin of the next node
|
||||
* When going down or left we want delta to be the end of the next node
|
||||
*/
|
||||
switch (focusDirection.value()) {
|
||||
case FocusDirection::FocusDown:
|
||||
nextDelta = (nextNodeLayoutMetrics.frame.origin.y -
|
||||
parentNodeLayoutMetrics.frame.size.height) +
|
||||
nextNodeLayoutMetrics.frame.size.height;
|
||||
nodeValues[1] = nextDelta;
|
||||
break;
|
||||
case FocusDirection::FocusRight:
|
||||
nextDelta = (nextNodeLayoutMetrics.frame.origin.x -
|
||||
parentNodeLayoutMetrics.frame.size.width) +
|
||||
nextNodeLayoutMetrics.frame.size.width;
|
||||
nodeValues[1] = nextDelta;
|
||||
break;
|
||||
case FocusDirection::FocusLeft:
|
||||
case FocusDirection::FocusUp:
|
||||
nodeValues[1] = nextDelta;
|
||||
break;
|
||||
}
|
||||
|
||||
// Build the jfloatArray with the updates nodeValues
|
||||
env->SetFloatArrayRegion(result, 0, nodeValuesSize, nodeValues.data());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Used by non-bridgeless+Fabric
|
||||
void FabricUIManagerBinding::startSurfaceWithConstraints(
|
||||
jint surfaceId,
|
||||
@@ -656,6 +757,9 @@ void FabricUIManagerBinding::registerNatives() {
|
||||
makeNativeMethod(
|
||||
"stopSurfaceWithSurfaceHandler",
|
||||
FabricUIManagerBinding::stopSurfaceWithSurfaceHandler),
|
||||
makeNativeMethod(
|
||||
"findNextFocusableElementMetrics",
|
||||
FabricUIManagerBinding::findNextFocusableElementMetrics),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -128,6 +128,11 @@ class FabricUIManagerBinding : public jni::HybridClass<FabricUIManagerBinding>,
|
||||
|
||||
void reportMount(SurfaceId surfaceId);
|
||||
|
||||
jfloatArray findNextFocusableElementMetrics(
|
||||
jint parentTag,
|
||||
jint focusedTag,
|
||||
jint direction);
|
||||
|
||||
void uninstallFabricUIManager();
|
||||
|
||||
// Private member variables
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* 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 <react/renderer/uimanager/UIManager.h>
|
||||
|
||||
namespace facebook::react {
|
||||
|
||||
void FocusOrderingHelper::traverseAndUpdateNextFocusableElementMetrics(
|
||||
const ShadowNode::Shared& parentShadowNode,
|
||||
const ShadowNode::Shared& focusedShadowNode,
|
||||
const ShadowNode::Shared& currNode,
|
||||
FocusDirection focusDirection,
|
||||
const UIManager& uimanager,
|
||||
float& currentDelta,
|
||||
float& nextDelta,
|
||||
ShadowNode::Shared& nextNode) {
|
||||
const auto* props =
|
||||
dynamic_cast<const ViewProps*>(currNode->getProps().get());
|
||||
|
||||
// We only care about focusable elements since only they can be both
|
||||
// focused and present in the hierarchy
|
||||
if (props != nullptr && props->focusable) {
|
||||
LayoutMetrics nodeLayoutMetrics = uimanager.getRelativeLayoutMetrics(
|
||||
*currNode, parentShadowNode.get(), {.includeTransform = true});
|
||||
|
||||
switch (focusDirection) {
|
||||
case FocusDirection::FocusDown:
|
||||
if (nodeLayoutMetrics.frame.origin.y > currentDelta &&
|
||||
nodeLayoutMetrics.frame.origin.y < nextDelta &&
|
||||
currNode != focusedShadowNode) {
|
||||
nextNode = currNode;
|
||||
nextDelta = nodeLayoutMetrics.frame.origin.y;
|
||||
}
|
||||
break;
|
||||
case FocusDirection::FocusUp:
|
||||
if (nodeLayoutMetrics.frame.origin.y < currentDelta &&
|
||||
nodeLayoutMetrics.frame.origin.y > nextDelta &&
|
||||
currNode != focusedShadowNode) {
|
||||
nextNode = currNode;
|
||||
nextDelta = nodeLayoutMetrics.frame.origin.y;
|
||||
}
|
||||
break;
|
||||
case FocusDirection::FocusRight:
|
||||
if (nodeLayoutMetrics.frame.origin.x > currentDelta &&
|
||||
nodeLayoutMetrics.frame.origin.x < nextDelta &&
|
||||
currNode != focusedShadowNode) {
|
||||
nextNode = currNode;
|
||||
nextDelta = nodeLayoutMetrics.frame.origin.x;
|
||||
}
|
||||
break;
|
||||
case FocusDirection::FocusLeft:
|
||||
if (nodeLayoutMetrics.frame.origin.x < currentDelta &&
|
||||
nodeLayoutMetrics.frame.origin.x > nextDelta &&
|
||||
currNode != focusedShadowNode) {
|
||||
nextNode = currNode;
|
||||
nextDelta = nodeLayoutMetrics.frame.origin.x;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& child : currNode->getChildren()) {
|
||||
if (child->getTraits().check(ShadowNodeTraits::Trait::RootNodeKind)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
traverseAndUpdateNextFocusableElementMetrics(
|
||||
parentShadowNode,
|
||||
focusedShadowNode,
|
||||
child,
|
||||
focusDirection,
|
||||
uimanager,
|
||||
currentDelta,
|
||||
nextDelta,
|
||||
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<FocusDirection> FocusOrderingHelper::resolveFocusDirection(
|
||||
int direction) {
|
||||
switch (static_cast<FocusDirection>(direction)) {
|
||||
case FocusDirection::FocusDown:
|
||||
case FocusDirection::FocusUp:
|
||||
case FocusDirection::FocusRight:
|
||||
case FocusDirection::FocusLeft:
|
||||
return static_cast<FocusDirection>(direction);
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::tuple<float, float> FocusOrderingHelper::initScrollDeltas(
|
||||
FocusDirection focusDirection,
|
||||
Point refPoint) {
|
||||
float currentDelta = 0;
|
||||
float nextDelta = 0;
|
||||
|
||||
switch (focusDirection) {
|
||||
case FocusDirection::FocusDown:
|
||||
currentDelta = refPoint.y;
|
||||
nextDelta = std::numeric_limits<float>::max();
|
||||
break;
|
||||
case FocusDirection::FocusUp:
|
||||
currentDelta = refPoint.y;
|
||||
nextDelta = -std::numeric_limits<float>::max();
|
||||
break;
|
||||
case FocusDirection::FocusRight:
|
||||
currentDelta = refPoint.x;
|
||||
nextDelta = std::numeric_limits<float>::max();
|
||||
break;
|
||||
case FocusDirection::FocusLeft:
|
||||
currentDelta = refPoint.x;
|
||||
nextDelta = -std::numeric_limits<float>::max();
|
||||
break;
|
||||
}
|
||||
return std::make_tuple(currentDelta, nextDelta);
|
||||
}
|
||||
} // namespace facebook::react
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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 <react/renderer/uimanager/UIManager.h>
|
||||
#include "FabricUIManagerBinding.h"
|
||||
|
||||
namespace facebook::react {
|
||||
|
||||
enum class FocusDirection {
|
||||
FocusDown = 0,
|
||||
FocusUp = 1,
|
||||
FocusRight = 2,
|
||||
FocusLeft = 3,
|
||||
};
|
||||
|
||||
class FocusOrderingHelper {
|
||||
public:
|
||||
static void traverseAndUpdateNextFocusableElementMetrics(
|
||||
const ShadowNode::Shared& parentShadowNode,
|
||||
const ShadowNode::Shared& focusedShadowNode,
|
||||
const ShadowNode::Shared& currNode,
|
||||
FocusDirection focusDirection,
|
||||
const UIManager& uimanager,
|
||||
float& currentDelta,
|
||||
float& nextDelta,
|
||||
ShadowNode::Shared& nextNode);
|
||||
|
||||
static ShadowNode::Shared findShadowNodeByTagRecursively(
|
||||
const ShadowNode::Shared& parentShadowNode,
|
||||
Tag tag);
|
||||
|
||||
static std::optional<FocusDirection> resolveFocusDirection(int direction);
|
||||
|
||||
static std::tuple<float, float> initScrollDeltas(
|
||||
FocusDirection focusDirection,
|
||||
Point refPoint);
|
||||
};
|
||||
} // namespace facebook::react
|
||||
Reference in New Issue
Block a user