Back out "Fix keyboard navigation for FlatList with removeClippedSubviews enabled"

Summary:
Original commit changeset: 1df7f90066df

Original Phabricator Diff: D69618406

Original diff caused unintended scrolling on some interactions that trigger focus searching

Changelog: [Internal]

Reviewed By: Abbondanzo

Differential Revision: D71217683

fbshipit-source-id: faa977ba0fddbf776868d6204d283555e169ba96
This commit is contained in:
Jorge Cabiedes Acosta
2025-03-14 15:15:01 -07:00
committed by Facebook GitHub Bot
parent bcd6b5fbd9
commit fc7385c1ea
11 changed files with 0 additions and 487 deletions
@@ -2396,7 +2396,6 @@ 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;
@@ -2436,12 +2435,6 @@ 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;
@@ -6204,7 +6197,6 @@ 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;
@@ -6327,7 +6319,6 @@ 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;
@@ -6448,8 +6439,6 @@ 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
@@ -6459,7 +6448,6 @@ 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
@@ -29,7 +29,6 @@ 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;
@@ -261,51 +260,6 @@ 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)
@@ -55,12 +55,6 @@ 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)
@@ -1,10 +0,0 @@
/*
* 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)
@@ -11,7 +11,6 @@ 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;
@@ -32,7 +31,6 @@ 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;
@@ -774,17 +772,6 @@ 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) {
@@ -11,7 +11,6 @@ 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;
@@ -32,7 +31,6 @@ 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;
@@ -361,18 +359,6 @@ 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
@@ -11,19 +11,15 @@ 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
@@ -466,89 +462,6 @@ 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?,
@@ -12,7 +12,6 @@
#include "EventBeatManager.h"
#include "EventEmitterWrapper.h"
#include "FabricMountingManager.h"
#include "FocusOrderingHelper.h"
#include <cxxreact/TraceSection.h>
#include <fbjni/fbjni.h>
@@ -196,115 +195,6 @@ 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);
if (parentShadowNode == nullptr) {
return nullptr;
}
focusedShadowNode = FocusOrderingHelper::findShadowNodeByTagRecursively(
parentShadowNode, focusedTag);
if (focusedShadowNode == nullptr) {
return nullptr;
}
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,
@@ -766,9 +656,6 @@ void FabricUIManagerBinding::registerNatives() {
makeNativeMethod(
"stopSurfaceWithSurfaceHandler",
FabricUIManagerBinding::stopSurfaceWithSurfaceHandler),
makeNativeMethod(
"findNextFocusableElementMetrics",
FabricUIManagerBinding::findNextFocusableElementMetrics),
});
}
@@ -128,11 +128,6 @@ class FabricUIManagerBinding : public jni::HybridClass<FabricUIManagerBinding>,
void reportMount(SurfaceId surfaceId);
jfloatArray findNextFocusableElementMetrics(
jint parentTag,
jint focusedTag,
jint direction);
void uninstallFabricUIManager();
// Private member variables
@@ -1,139 +0,0 @@
/*
* 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
@@ -1,42 +0,0 @@
/*
* 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