Kotlinify ReactScrollViewManager

Summary:
## Changelog:
[Android] [Internal] - As in the title

Reviewed By: cortinico

Differential Revision: D72971263

fbshipit-source-id: 8bd0c4e29e48f7e3d47a97f1a4e988e7b0282646
This commit is contained in:
Ruslan Shestopalyuk
2025-04-16 04:57:43 -07:00
committed by Facebook GitHub Bot
parent e0a506fac3
commit 44c274ae6e
4 changed files with 421 additions and 440 deletions
@@ -6218,10 +6218,11 @@ public abstract interface class com/facebook/react/views/scroll/ReactScrollViewH
}
public class com/facebook/react/views/scroll/ReactScrollViewManager : com/facebook/react/uimanager/ViewGroupManager, com/facebook/react/views/scroll/ReactScrollViewCommandHelper$ScrollCommandHandler {
public static final field Companion Lcom/facebook/react/views/scroll/ReactScrollViewManager$Companion;
public static final field REACT_CLASS Ljava/lang/String;
public fun <init> ()V
public fun <init> (Lcom/facebook/react/views/scroll/FpsListener;)V
public static fun createExportedCustomDirectEventTypeConstants ()Ljava/util/Map;
public synthetic fun <init> (Lcom/facebook/react/views/scroll/FpsListener;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun createViewInstance (Lcom/facebook/react/uimanager/ThemedReactContext;)Landroid/view/View;
public fun createViewInstance (Lcom/facebook/react/uimanager/ThemedReactContext;)Lcom/facebook/react/views/scroll/ReactScrollView;
public fun flashScrollIndicators (Lcom/facebook/react/views/scroll/ReactScrollView;)V
@@ -6237,39 +6238,43 @@ public class com/facebook/react/views/scroll/ReactScrollViewManager : com/facebo
public synthetic fun scrollTo (Ljava/lang/Object;Lcom/facebook/react/views/scroll/ReactScrollViewCommandHelper$ScrollToCommandData;)V
public fun scrollToEnd (Lcom/facebook/react/views/scroll/ReactScrollView;Lcom/facebook/react/views/scroll/ReactScrollViewCommandHelper$ScrollToEndCommandData;)V
public synthetic fun scrollToEnd (Ljava/lang/Object;Lcom/facebook/react/views/scroll/ReactScrollViewCommandHelper$ScrollToEndCommandData;)V
public fun setBorderColor (Lcom/facebook/react/views/scroll/ReactScrollView;ILjava/lang/Integer;)V
public fun setBorderRadius (Lcom/facebook/react/views/scroll/ReactScrollView;IF)V
public fun setBorderStyle (Lcom/facebook/react/views/scroll/ReactScrollView;Ljava/lang/String;)V
public fun setBorderWidth (Lcom/facebook/react/views/scroll/ReactScrollView;IF)V
public fun setBottomFillColor (Lcom/facebook/react/views/scroll/ReactScrollView;I)V
public fun setContentOffset (Lcom/facebook/react/views/scroll/ReactScrollView;Lcom/facebook/react/bridge/ReadableMap;)V
public fun setDecelerationRate (Lcom/facebook/react/views/scroll/ReactScrollView;F)V
public fun setDisableIntervalMomentum (Lcom/facebook/react/views/scroll/ReactScrollView;Z)V
public fun setFadingEdgeLength (Lcom/facebook/react/views/scroll/ReactScrollView;I)V
public fun setHorizontal (Lcom/facebook/react/views/scroll/ReactScrollView;Z)V
public fun setIsInvertedVirtualizedList (Lcom/facebook/react/views/scroll/ReactScrollView;Z)V
public fun setMaintainVisibleContentPosition (Lcom/facebook/react/views/scroll/ReactScrollView;Lcom/facebook/react/bridge/ReadableMap;)V
public fun setNestedScrollEnabled (Lcom/facebook/react/views/scroll/ReactScrollView;Z)V
public final fun setBorderColor (Lcom/facebook/react/views/scroll/ReactScrollView;ILjava/lang/Integer;)V
public final fun setBorderRadius (Lcom/facebook/react/views/scroll/ReactScrollView;IF)V
public final fun setBorderStyle (Lcom/facebook/react/views/scroll/ReactScrollView;Ljava/lang/String;)V
public final fun setBorderWidth (Lcom/facebook/react/views/scroll/ReactScrollView;IF)V
public final fun setBottomFillColor (Lcom/facebook/react/views/scroll/ReactScrollView;I)V
public final fun setContentOffset (Lcom/facebook/react/views/scroll/ReactScrollView;Lcom/facebook/react/bridge/ReadableMap;)V
public final fun setDecelerationRate (Lcom/facebook/react/views/scroll/ReactScrollView;F)V
public final fun setDisableIntervalMomentum (Lcom/facebook/react/views/scroll/ReactScrollView;Z)V
public final fun setFadingEdgeLength (Lcom/facebook/react/views/scroll/ReactScrollView;I)V
public final fun setHorizontal (Lcom/facebook/react/views/scroll/ReactScrollView;Z)V
public final fun setIsInvertedVirtualizedList (Lcom/facebook/react/views/scroll/ReactScrollView;Z)V
public final fun setMaintainVisibleContentPosition (Lcom/facebook/react/views/scroll/ReactScrollView;Lcom/facebook/react/bridge/ReadableMap;)V
public final fun setNestedScrollEnabled (Lcom/facebook/react/views/scroll/ReactScrollView;Z)V
public fun setOverScrollMode (Lcom/facebook/react/views/scroll/ReactScrollView;Ljava/lang/String;)V
public fun setOverflow (Lcom/facebook/react/views/scroll/ReactScrollView;Ljava/lang/String;)V
public fun setPagingEnabled (Lcom/facebook/react/views/scroll/ReactScrollView;Z)V
public fun setPersistentScrollbar (Lcom/facebook/react/views/scroll/ReactScrollView;Z)V
public fun setPointerEvents (Lcom/facebook/react/views/scroll/ReactScrollView;Ljava/lang/String;)V
public fun setRemoveClippedSubviews (Lcom/facebook/react/views/scroll/ReactScrollView;Z)V
public fun setScrollEnabled (Lcom/facebook/react/views/scroll/ReactScrollView;Z)V
public fun setScrollEventThrottle (Lcom/facebook/react/views/scroll/ReactScrollView;I)V
public fun setScrollPerfTag (Lcom/facebook/react/views/scroll/ReactScrollView;Ljava/lang/String;)V
public fun setSendMomentumEvents (Lcom/facebook/react/views/scroll/ReactScrollView;Z)V
public fun setShowsVerticalScrollIndicator (Lcom/facebook/react/views/scroll/ReactScrollView;Z)V
public fun setSnapToAlignment (Lcom/facebook/react/views/scroll/ReactScrollView;Ljava/lang/String;)V
public fun setSnapToEnd (Lcom/facebook/react/views/scroll/ReactScrollView;Z)V
public fun setSnapToInterval (Lcom/facebook/react/views/scroll/ReactScrollView;F)V
public fun setSnapToOffsets (Lcom/facebook/react/views/scroll/ReactScrollView;Lcom/facebook/react/bridge/ReadableArray;)V
public fun setSnapToStart (Lcom/facebook/react/views/scroll/ReactScrollView;Z)V
public final fun setOverflow (Lcom/facebook/react/views/scroll/ReactScrollView;Ljava/lang/String;)V
public final fun setPagingEnabled (Lcom/facebook/react/views/scroll/ReactScrollView;Z)V
public final fun setPersistentScrollbar (Lcom/facebook/react/views/scroll/ReactScrollView;Z)V
public final fun setPointerEvents (Lcom/facebook/react/views/scroll/ReactScrollView;Ljava/lang/String;)V
public final fun setRemoveClippedSubviews (Lcom/facebook/react/views/scroll/ReactScrollView;Z)V
public final fun setScrollEnabled (Lcom/facebook/react/views/scroll/ReactScrollView;Z)V
public final fun setScrollEventThrottle (Lcom/facebook/react/views/scroll/ReactScrollView;I)V
public final fun setScrollPerfTag (Lcom/facebook/react/views/scroll/ReactScrollView;Ljava/lang/String;)V
public final fun setSendMomentumEvents (Lcom/facebook/react/views/scroll/ReactScrollView;Z)V
public final fun setShowsVerticalScrollIndicator (Lcom/facebook/react/views/scroll/ReactScrollView;Z)V
public final fun setSnapToAlignment (Lcom/facebook/react/views/scroll/ReactScrollView;Ljava/lang/String;)V
public final fun setSnapToEnd (Lcom/facebook/react/views/scroll/ReactScrollView;Z)V
public final fun setSnapToInterval (Lcom/facebook/react/views/scroll/ReactScrollView;F)V
public final fun setSnapToOffsets (Lcom/facebook/react/views/scroll/ReactScrollView;Lcom/facebook/react/bridge/ReadableArray;)V
public final fun setSnapToStart (Lcom/facebook/react/views/scroll/ReactScrollView;Z)V
public synthetic fun updateState (Landroid/view/View;Lcom/facebook/react/uimanager/ReactStylesDiffMap;Lcom/facebook/react/uimanager/StateWrapper;)Ljava/lang/Object;
public fun updateState (Lcom/facebook/react/views/scroll/ReactScrollView;Lcom/facebook/react/uimanager/ReactStylesDiffMap;Lcom/facebook/react/uimanager/StateWrapper;)Ljava/lang/Object;
}
public final class com/facebook/react/views/scroll/ReactScrollViewManager$Companion {
public final fun createExportedCustomDirectEventTypeConstants ()Ljava/util/Map;
}
public final class com/facebook/react/views/scroll/ScrollEvent : com/facebook/react/uimanager/events/Event {
public static final field Companion Lcom/facebook/react/views/scroll/ScrollEvent$Companion;
public fun canCoalesce ()Z
@@ -6996,4 +7001,3 @@ public class com/facebook/react/views/view/ReactViewManager : com/facebook/react
public final class com/facebook/react/views/view/ReactViewManager$Companion {
}
@@ -1111,7 +1111,7 @@ public class ReactScrollView extends ScrollView
}
}
public void setContentOffset(ReadableMap value) {
public void setContentOffset(@Nullable ReadableMap value) {
// When contentOffset={{x:0,y:0}} with lazy load items, setContentOffset
// is repeatedly called, causing an unexpected scroll to top behavior.
// Avoid updating contentOffset if the value has not changed.
@@ -1,409 +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.views.scroll;
import android.graphics.Color;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import com.facebook.infer.annotation.Nullsafe;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.RetryableMountingLayerException;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.BackgroundStyleApplicator;
import com.facebook.react.uimanager.LengthPercentage;
import com.facebook.react.uimanager.LengthPercentageType;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.PointerEvents;
import com.facebook.react.uimanager.ReactClippingViewGroupHelper;
import com.facebook.react.uimanager.ReactStylesDiffMap;
import com.facebook.react.uimanager.Spacing;
import com.facebook.react.uimanager.StateWrapper;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.ViewProps;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.annotations.ReactPropGroup;
import com.facebook.react.uimanager.style.BorderRadiusProp;
import com.facebook.react.uimanager.style.BorderStyle;
import com.facebook.react.uimanager.style.LogicalEdge;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* View manager for {@link ReactScrollView} components.
*
* <p>Note that {@link ReactScrollView} and {@link ReactHorizontalScrollView} are exposed to JS as a
* single ScrollView component, configured via the {@code horizontal} boolean property.
*/
@Nullsafe(Nullsafe.Mode.LOCAL)
@ReactModule(name = ReactScrollViewManager.REACT_CLASS)
public class ReactScrollViewManager extends ViewGroupManager<ReactScrollView>
implements ReactScrollViewCommandHelper.ScrollCommandHandler<ReactScrollView> {
public static final String REACT_CLASS = "RCTScrollView";
private static final int[] SPACING_TYPES = {
Spacing.ALL, Spacing.LEFT, Spacing.RIGHT, Spacing.TOP, Spacing.BOTTOM,
};
private @Nullable FpsListener mFpsListener = null;
public ReactScrollViewManager() {
this(null);
}
public ReactScrollViewManager(@Nullable FpsListener fpsListener) {
mFpsListener = fpsListener;
}
@Override
public String getName() {
return REACT_CLASS;
}
@Override
public ReactScrollView createViewInstance(ThemedReactContext context) {
return new ReactScrollView(context, mFpsListener);
}
@ReactProp(name = "scrollEnabled", defaultBoolean = true)
public void setScrollEnabled(ReactScrollView view, boolean value) {
view.setScrollEnabled(value);
// Set focusable to match whether scroll is enabled. This improves keyboarding
// experience by not making scrollview a tab stop when you cannot interact with it.
view.setFocusable(value);
}
@ReactProp(name = "showsVerticalScrollIndicator", defaultBoolean = true)
public void setShowsVerticalScrollIndicator(ReactScrollView view, boolean value) {
view.setVerticalScrollBarEnabled(value);
}
@ReactProp(name = "decelerationRate")
public void setDecelerationRate(ReactScrollView view, float decelerationRate) {
view.setDecelerationRate(decelerationRate);
}
@ReactProp(name = "disableIntervalMomentum")
public void setDisableIntervalMomentum(ReactScrollView view, boolean disableIntervalMomentum) {
view.setDisableIntervalMomentum(disableIntervalMomentum);
}
@ReactProp(name = "snapToInterval")
public void setSnapToInterval(ReactScrollView view, float snapToInterval) {
// snapToInterval needs to be exposed as a float because of the Javascript interface.
float density = PixelUtil.getDisplayMetricDensity();
view.setSnapInterval((int) (snapToInterval * density));
}
@ReactProp(name = "snapToOffsets")
public void setSnapToOffsets(ReactScrollView view, @Nullable ReadableArray snapToOffsets) {
if (snapToOffsets == null || snapToOffsets.size() == 0) {
view.setSnapOffsets(null);
return;
}
float density = PixelUtil.getDisplayMetricDensity();
List<Integer> offsets = new ArrayList<Integer>();
for (int i = 0; i < snapToOffsets.size(); i++) {
offsets.add((int) (snapToOffsets.getDouble(i) * density));
}
view.setSnapOffsets(offsets);
}
@ReactProp(name = "snapToAlignment")
public void setSnapToAlignment(ReactScrollView view, @Nullable String alignment) {
view.setSnapToAlignment(ReactScrollViewHelper.parseSnapToAlignment(alignment));
}
@ReactProp(name = "snapToStart")
public void setSnapToStart(ReactScrollView view, boolean snapToStart) {
view.setSnapToStart(snapToStart);
}
@ReactProp(name = "snapToEnd")
public void setSnapToEnd(ReactScrollView view, boolean snapToEnd) {
view.setSnapToEnd(snapToEnd);
}
@ReactProp(name = ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS)
public void setRemoveClippedSubviews(ReactScrollView view, boolean removeClippedSubviews) {
view.setRemoveClippedSubviews(removeClippedSubviews);
}
/**
* Computing momentum events is potentially expensive since we post a runnable on the UI thread to
* see when it is done. We only do that if {@param sendMomentumEvents} is set to true. This is
* handled automatically in js by checking if there is a listener on the momentum events.
*
* @param view
* @param sendMomentumEvents
*/
@ReactProp(name = "sendMomentumEvents")
public void setSendMomentumEvents(ReactScrollView view, boolean sendMomentumEvents) {
view.setSendMomentumEvents(sendMomentumEvents);
}
/**
* Tag used for logging scroll performance on this scroll view. Will force momentum events to be
* turned on (see setSendMomentumEvents).
*
* @param view
* @param scrollPerfTag
*/
@ReactProp(name = "scrollPerfTag")
public void setScrollPerfTag(ReactScrollView view, @Nullable String scrollPerfTag) {
view.setScrollPerfTag(scrollPerfTag);
}
@ReactProp(name = "pagingEnabled")
public void setPagingEnabled(ReactScrollView view, boolean pagingEnabled) {
view.setPagingEnabled(pagingEnabled);
}
/**
* When set, fills the rest of the scrollview with a color to avoid setting a background and
* creating unnecessary overdraw.
*
* @param view
* @param color
*/
@ReactProp(name = "endFillColor", defaultInt = Color.TRANSPARENT, customType = "Color")
public void setBottomFillColor(ReactScrollView view, int color) {
view.setEndFillColor(color);
}
/** Controls overScroll behaviour */
@ReactProp(name = "overScrollMode")
public void setOverScrollMode(ReactScrollView view, @Nullable String value) {
view.setOverScrollMode(ReactScrollViewHelper.parseOverScrollMode(value));
}
@ReactProp(name = "nestedScrollEnabled")
public void setNestedScrollEnabled(ReactScrollView view, boolean value) {
ViewCompat.setNestedScrollingEnabled(view, value);
}
@Override
public @Nullable Map<String, Integer> getCommandsMap() {
return ReactScrollViewCommandHelper.getCommandsMap();
}
@Override
public void receiveCommand(
ReactScrollView scrollView, int commandId, @Nullable ReadableArray args) {
ReactScrollViewCommandHelper.receiveCommand(this, scrollView, commandId, args);
}
@Override
public void receiveCommand(
ReactScrollView scrollView, String commandId, @Nullable ReadableArray args) {
ReactScrollViewCommandHelper.receiveCommand(this, scrollView, commandId, args);
}
@Override
public void flashScrollIndicators(ReactScrollView scrollView) {
scrollView.flashScrollIndicators();
}
@Override
public void scrollTo(
ReactScrollView scrollView, ReactScrollViewCommandHelper.ScrollToCommandData data) {
scrollView.abortAnimation();
if (data.mAnimated) {
scrollView.reactSmoothScrollTo(data.mDestX, data.mDestY);
} else {
scrollView.scrollTo(data.mDestX, data.mDestY);
}
}
@ReactPropGroup(
names = {
ViewProps.BORDER_RADIUS,
ViewProps.BORDER_TOP_LEFT_RADIUS,
ViewProps.BORDER_TOP_RIGHT_RADIUS,
ViewProps.BORDER_BOTTOM_RIGHT_RADIUS,
ViewProps.BORDER_BOTTOM_LEFT_RADIUS
},
defaultFloat = Float.NaN)
public void setBorderRadius(ReactScrollView view, int index, float borderRadius) {
@Nullable
LengthPercentage radius =
Float.isNaN(borderRadius)
? null
: new LengthPercentage(borderRadius, LengthPercentageType.POINT);
BackgroundStyleApplicator.setBorderRadius(view, BorderRadiusProp.values()[index], radius);
}
@ReactProp(name = "borderStyle")
public void setBorderStyle(ReactScrollView view, @Nullable String borderStyle) {
@Nullable
BorderStyle parsedBorderStyle =
borderStyle == null ? null : BorderStyle.fromString(borderStyle);
BackgroundStyleApplicator.setBorderStyle(view, parsedBorderStyle);
}
@ReactPropGroup(
names = {
ViewProps.BORDER_WIDTH,
ViewProps.BORDER_LEFT_WIDTH,
ViewProps.BORDER_RIGHT_WIDTH,
ViewProps.BORDER_TOP_WIDTH,
ViewProps.BORDER_BOTTOM_WIDTH,
},
defaultFloat = Float.NaN)
public void setBorderWidth(ReactScrollView view, int index, float width) {
BackgroundStyleApplicator.setBorderWidth(view, LogicalEdge.values()[index], width);
}
@ReactPropGroup(
names = {
"borderColor",
"borderLeftColor",
"borderRightColor",
"borderTopColor",
"borderBottomColor"
},
customType = "Color")
public void setBorderColor(ReactScrollView view, int index, @Nullable Integer color) {
BackgroundStyleApplicator.setBorderColor(view, LogicalEdge.ALL, color);
}
@ReactProp(name = "overflow")
public void setOverflow(ReactScrollView view, @Nullable String overflow) {
view.setOverflow(overflow);
}
@Override
public void scrollToEnd(
ReactScrollView scrollView, ReactScrollViewCommandHelper.ScrollToEndCommandData data) {
// ScrollView always has one child - the scrollable area. However, it's possible today that we
// execute this method as view command before the child view is mounted. Here we will retry the
// view commands as a workaround.
View child = scrollView.getChildAt(0);
if (child == null) {
throw new RetryableMountingLayerException("scrollToEnd called on ScrollView without child");
}
// ScrollView always has one child - the scrollable area
int bottom = child.getHeight() + scrollView.getPaddingBottom();
scrollView.abortAnimation();
if (data.mAnimated) {
scrollView.reactSmoothScrollTo(scrollView.getScrollX(), bottom);
} else {
scrollView.scrollTo(scrollView.getScrollX(), bottom);
}
}
@ReactProp(name = "persistentScrollbar")
public void setPersistentScrollbar(ReactScrollView view, boolean value) {
view.setScrollbarFadingEnabled(!value);
}
@ReactProp(name = "fadingEdgeLength")
public void setFadingEdgeLength(ReactScrollView view, int value) {
if (value > 0) {
view.setVerticalFadingEdgeEnabled(true);
view.setFadingEdgeLength(value);
} else {
view.setVerticalFadingEdgeEnabled(false);
view.setFadingEdgeLength(0);
}
}
@ReactProp(name = "contentOffset", customType = "Point")
public void setContentOffset(ReactScrollView view, ReadableMap value) {
view.setContentOffset(value);
}
@ReactProp(name = "maintainVisibleContentPosition")
public void setMaintainVisibleContentPosition(ReactScrollView view, ReadableMap value) {
if (value != null) {
view.setMaintainVisibleContentPosition(
MaintainVisibleScrollPositionHelper.Config.fromReadableMap(value));
} else {
view.setMaintainVisibleContentPosition(null);
}
}
@Override
public @Nullable Object updateState(
ReactScrollView view, ReactStylesDiffMap props, StateWrapper stateWrapper) {
view.setStateWrapper(stateWrapper);
return null;
}
@Override
public @Nullable Map<String, Object> getExportedCustomDirectEventTypeConstants() {
@Nullable
Map<String, Object> baseEventTypeConstants = super.getExportedCustomDirectEventTypeConstants();
Map<String, Object> eventTypeConstants =
baseEventTypeConstants == null ? new HashMap<String, Object>() : baseEventTypeConstants;
eventTypeConstants.putAll(createExportedCustomDirectEventTypeConstants());
return eventTypeConstants;
}
public static Map<String, Object> createExportedCustomDirectEventTypeConstants() {
return MapBuilder.<String, Object>builder()
.put(
ScrollEventType.getJSEventName(ScrollEventType.SCROLL),
MapBuilder.of("registrationName", "onScroll"))
.put(
ScrollEventType.getJSEventName(ScrollEventType.BEGIN_DRAG),
MapBuilder.of("registrationName", "onScrollBeginDrag"))
.put(
ScrollEventType.getJSEventName(ScrollEventType.END_DRAG),
MapBuilder.of("registrationName", "onScrollEndDrag"))
.put(
ScrollEventType.getJSEventName(ScrollEventType.MOMENTUM_BEGIN),
MapBuilder.of("registrationName", "onMomentumScrollBegin"))
.put(
ScrollEventType.getJSEventName(ScrollEventType.MOMENTUM_END),
MapBuilder.of("registrationName", "onMomentumScrollEnd"))
.build();
}
@ReactProp(name = ViewProps.POINTER_EVENTS)
public void setPointerEvents(ReactScrollView view, @Nullable String pointerEventsStr) {
view.setPointerEvents(PointerEvents.parsePointerEvents(pointerEventsStr));
}
@ReactProp(name = "scrollEventThrottle")
public void setScrollEventThrottle(ReactScrollView view, int scrollEventThrottle) {
view.setScrollEventThrottle(scrollEventThrottle);
}
@ReactProp(name = "horizontal")
public void setHorizontal(ReactScrollView view, boolean horizontal) {
// Do Nothing: Align with static ViewConfigs
}
@ReactProp(name = "isInvertedVirtualizedList")
public void setIsInvertedVirtualizedList(ReactScrollView view, boolean applyFix) {
// Usually when inverting the scroll view we are using scaleY: -1 on the list
// and on the parent container. HOWEVER, starting from android API 33 there is
// a bug that can cause an ANR due to that. Thus we are using different transform
// commands to circumvent the ANR. This however causes the vertical scrollbar to
// be on the wrong side. Thus we are moving it to the other side, when the list
// is inverted.
// See also:
// - https://github.com/facebook/react-native/issues/35350
// - https://issuetracker.google.com/issues/287304310
if (applyFix) {
view.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_LEFT);
} else {
view.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_DEFAULT);
}
}
}
@@ -0,0 +1,386 @@
/*
* 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.views.scroll
import android.graphics.Color
import android.view.View
import androidx.core.view.ViewCompat
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.RetryableMountingLayerException
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.BackgroundStyleApplicator.setBorderColor
import com.facebook.react.uimanager.BackgroundStyleApplicator.setBorderRadius
import com.facebook.react.uimanager.BackgroundStyleApplicator.setBorderStyle
import com.facebook.react.uimanager.BackgroundStyleApplicator.setBorderWidth
import com.facebook.react.uimanager.LengthPercentage
import com.facebook.react.uimanager.LengthPercentageType
import com.facebook.react.uimanager.PixelUtil.getDisplayMetricDensity
import com.facebook.react.uimanager.PointerEvents.Companion.parsePointerEvents
import com.facebook.react.uimanager.ReactClippingViewGroupHelper
import com.facebook.react.uimanager.ReactStylesDiffMap
import com.facebook.react.uimanager.StateWrapper
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewGroupManager
import com.facebook.react.uimanager.ViewProps
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.uimanager.annotations.ReactPropGroup
import com.facebook.react.uimanager.style.BorderRadiusProp
import com.facebook.react.uimanager.style.BorderStyle.Companion.fromString
import com.facebook.react.uimanager.style.LogicalEdge
import com.facebook.react.views.scroll.ReactScrollViewCommandHelper.Companion.receiveCommand
import com.facebook.react.views.scroll.ReactScrollViewCommandHelper.ScrollCommandHandler
import com.facebook.react.views.scroll.ReactScrollViewCommandHelper.ScrollToCommandData
import com.facebook.react.views.scroll.ReactScrollViewCommandHelper.ScrollToEndCommandData
import com.facebook.react.views.scroll.ReactScrollViewHelper.parseOverScrollMode
import com.facebook.react.views.scroll.ReactScrollViewHelper.parseSnapToAlignment
import com.facebook.react.views.scroll.ScrollEventType.Companion.getJSEventName
/**
* View manager for [ReactScrollView] components.
*
* Note that [ReactScrollView] and [ReactHorizontalScrollView] are exposed to JS as a single
* ScrollView component, configured via the `horizontal` boolean property.
*/
@ReactModule(name = ReactScrollViewManager.REACT_CLASS)
public open class ReactScrollViewManager
@JvmOverloads
constructor(private val fpsListener: FpsListener? = null) :
ViewGroupManager<ReactScrollView>(), ScrollCommandHandler<ReactScrollView> {
override fun getName(): String = REACT_CLASS
public override fun createViewInstance(context: ThemedReactContext): ReactScrollView =
ReactScrollView(context, fpsListener)
@ReactProp(name = "scrollEnabled", defaultBoolean = true)
public fun setScrollEnabled(view: ReactScrollView, value: Boolean) {
view.setScrollEnabled(value)
// Set focusable to match whether scroll is enabled. This improves keyboarding
// experience by not making scrollview a tab stop when you cannot interact with it.
view.isFocusable = value
}
@ReactProp(name = "showsVerticalScrollIndicator", defaultBoolean = true)
public fun setShowsVerticalScrollIndicator(view: ReactScrollView, value: Boolean) {
view.isVerticalScrollBarEnabled = value
}
@ReactProp(name = "decelerationRate")
public fun setDecelerationRate(view: ReactScrollView, decelerationRate: Float) {
view.setDecelerationRate(decelerationRate)
}
@ReactProp(name = "disableIntervalMomentum")
public fun setDisableIntervalMomentum(view: ReactScrollView, disableIntervalMomentum: Boolean) {
view.setDisableIntervalMomentum(disableIntervalMomentum)
}
@ReactProp(name = "snapToInterval")
public fun setSnapToInterval(view: ReactScrollView, snapToInterval: Float) {
// snapToInterval needs to be exposed as a float because of the Javascript interface.
val density = getDisplayMetricDensity()
view.setSnapInterval((snapToInterval * density).toInt())
}
@ReactProp(name = "snapToOffsets")
public fun setSnapToOffsets(view: ReactScrollView, snapToOffsets: ReadableArray?) {
if (snapToOffsets == null || snapToOffsets.size() == 0) {
view.setSnapOffsets(null)
return
}
val density = getDisplayMetricDensity()
val offsets: MutableList<Int> = ArrayList()
for (i in 0 until snapToOffsets.size()) {
offsets.add((snapToOffsets.getDouble(i) * density).toInt())
}
view.setSnapOffsets(offsets)
}
@ReactProp(name = "snapToAlignment")
public fun setSnapToAlignment(view: ReactScrollView, alignment: String?) {
view.setSnapToAlignment(parseSnapToAlignment(alignment))
}
@ReactProp(name = "snapToStart")
public fun setSnapToStart(view: ReactScrollView, snapToStart: Boolean) {
view.setSnapToStart(snapToStart)
}
@ReactProp(name = "snapToEnd")
public fun setSnapToEnd(view: ReactScrollView, snapToEnd: Boolean) {
view.setSnapToEnd(snapToEnd)
}
@ReactProp(name = ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS)
public fun setRemoveClippedSubviews(view: ReactScrollView, removeClippedSubviews: Boolean) {
view.removeClippedSubviews = removeClippedSubviews
}
/**
* Computing momentum events is potentially expensive since we post a runnable on the UI thread to
* see when it is done. We only do that if {@param sendMomentumEvents} is set to true. This is
* handled automatically in js by checking if there is a listener on the momentum events.
*
* @param view
* @param sendMomentumEvents
*/
@ReactProp(name = "sendMomentumEvents")
public fun setSendMomentumEvents(view: ReactScrollView, sendMomentumEvents: Boolean) {
view.setSendMomentumEvents(sendMomentumEvents)
}
/**
* Tag used for logging scroll performance on this scroll view. Will force momentum events to be
* turned on (see setSendMomentumEvents).
*
* @param view
* @param scrollPerfTag
*/
@ReactProp(name = "scrollPerfTag")
public fun setScrollPerfTag(view: ReactScrollView, scrollPerfTag: String?) {
view.setScrollPerfTag(scrollPerfTag)
}
@ReactProp(name = "pagingEnabled")
public fun setPagingEnabled(view: ReactScrollView, pagingEnabled: Boolean) {
view.setPagingEnabled(pagingEnabled)
}
/**
* When set, fills the rest of the scrollview with a color to avoid setting a background and
* creating unnecessary overdraw.
*
* @param view
* @param color
*/
@ReactProp(name = "endFillColor", defaultInt = Color.TRANSPARENT, customType = "Color")
public fun setBottomFillColor(view: ReactScrollView, color: Int) {
view.setEndFillColor(color)
}
/** Controls overScroll behaviour */
@ReactProp(name = "overScrollMode")
public open fun setOverScrollMode(view: ReactScrollView, value: String?) {
view.overScrollMode = parseOverScrollMode(value)
}
@ReactProp(name = "nestedScrollEnabled")
public fun setNestedScrollEnabled(view: ReactScrollView?, value: Boolean) {
if (view != null) {
ViewCompat.setNestedScrollingEnabled(view, value)
}
}
override fun getCommandsMap(): Map<String, Int>? = ReactScrollViewCommandHelper.getCommandsMap()
override fun receiveCommand(scrollView: ReactScrollView, commandId: Int, args: ReadableArray?) {
receiveCommand<ReactScrollView>(this, scrollView, commandId, args)
}
override fun receiveCommand(
scrollView: ReactScrollView,
commandId: String,
args: ReadableArray?
) {
receiveCommand<ReactScrollView>(this, scrollView, commandId, args)
}
override fun flashScrollIndicators(scrollView: ReactScrollView) {
scrollView.flashScrollIndicators()
}
override fun scrollTo(scrollView: ReactScrollView, data: ScrollToCommandData) {
scrollView.abortAnimation()
if (data.mAnimated) {
scrollView.reactSmoothScrollTo(data.mDestX, data.mDestY)
} else {
scrollView.scrollTo(data.mDestX, data.mDestY)
}
}
@ReactPropGroup(
names =
[
ViewProps.BORDER_RADIUS,
ViewProps.BORDER_TOP_LEFT_RADIUS,
ViewProps.BORDER_TOP_RIGHT_RADIUS,
ViewProps.BORDER_BOTTOM_RIGHT_RADIUS,
ViewProps.BORDER_BOTTOM_LEFT_RADIUS],
defaultFloat = Float.NaN)
public fun setBorderRadius(view: ReactScrollView?, index: Int, borderRadius: Float) {
if (view != null) {
val radius =
if (java.lang.Float.isNaN(borderRadius)) null
else LengthPercentage(borderRadius, LengthPercentageType.POINT)
setBorderRadius(view, BorderRadiusProp.entries[index], radius)
}
}
@ReactProp(name = "borderStyle")
public fun setBorderStyle(view: ReactScrollView?, borderStyle: String?) {
if (view != null) {
val parsedBorderStyle = if (borderStyle == null) null else fromString(borderStyle)
setBorderStyle(view, parsedBorderStyle)
}
}
@ReactPropGroup(
names =
[
ViewProps.BORDER_WIDTH,
ViewProps.BORDER_LEFT_WIDTH,
ViewProps.BORDER_RIGHT_WIDTH,
ViewProps.BORDER_TOP_WIDTH,
ViewProps.BORDER_BOTTOM_WIDTH],
defaultFloat = Float.NaN)
public fun setBorderWidth(view: ReactScrollView?, index: Int, width: Float) {
if (view != null) {
setBorderWidth(view, LogicalEdge.entries[index], width)
}
}
@ReactPropGroup(
names =
[
"borderColor",
"borderLeftColor",
"borderRightColor",
"borderTopColor",
"borderBottomColor"],
customType = "Color")
@Suppress("UNUSED_PARAMETER")
public fun setBorderColor(view: ReactScrollView?, index: Int, color: Int?) {
if (view != null) {
setBorderColor(view, LogicalEdge.ALL, color)
}
}
@ReactProp(name = "overflow")
public fun setOverflow(view: ReactScrollView, overflow: String?) {
view.setOverflow(overflow)
}
override fun scrollToEnd(scrollView: ReactScrollView, data: ScrollToEndCommandData) {
// ScrollView always has one child - the scrollable area. However, it's possible today that we
// execute this method as view command before the child view is mounted. Here we will retry the
// view commands as a workaround.
val child =
scrollView.getChildAt(0)
?: throw RetryableMountingLayerException(
"scrollToEnd called on ScrollView without child")
// ScrollView always has one child - the scrollable area
val bottom = child.height + scrollView.paddingBottom
scrollView.abortAnimation()
if (data.mAnimated) {
scrollView.reactSmoothScrollTo(scrollView.scrollX, bottom)
} else {
scrollView.scrollTo(scrollView.scrollX, bottom)
}
}
@ReactProp(name = "persistentScrollbar")
public fun setPersistentScrollbar(view: ReactScrollView, value: Boolean) {
view.isScrollbarFadingEnabled = !value
}
@ReactProp(name = "fadingEdgeLength")
public fun setFadingEdgeLength(view: ReactScrollView, value: Int) {
if (value > 0) {
view.isVerticalFadingEdgeEnabled = true
view.setFadingEdgeLength(value)
} else {
view.isVerticalFadingEdgeEnabled = false
view.setFadingEdgeLength(0)
}
}
@ReactProp(name = "contentOffset", customType = "Point")
public fun setContentOffset(view: ReactScrollView, value: ReadableMap?) {
view.setContentOffset(value)
}
@ReactProp(name = "maintainVisibleContentPosition")
public fun setMaintainVisibleContentPosition(view: ReactScrollView, value: ReadableMap?) {
if (value != null) {
view.setMaintainVisibleContentPosition(
MaintainVisibleScrollPositionHelper.Config.fromReadableMap(value))
} else {
view.setMaintainVisibleContentPosition(null)
}
}
override fun updateState(
view: ReactScrollView,
props: ReactStylesDiffMap,
stateWrapper: StateWrapper
): Any? {
view.setStateWrapper(stateWrapper)
return null
}
override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any>? {
val baseEventTypeConstants = super.getExportedCustomDirectEventTypeConstants()
val eventTypeConstants = baseEventTypeConstants ?: HashMap()
eventTypeConstants.putAll(createExportedCustomDirectEventTypeConstants())
return eventTypeConstants
}
@ReactProp(name = ViewProps.POINTER_EVENTS)
public fun setPointerEvents(view: ReactScrollView, pointerEventsStr: String?) {
view.pointerEvents = parsePointerEvents(pointerEventsStr)
}
@ReactProp(name = "scrollEventThrottle")
public fun setScrollEventThrottle(view: ReactScrollView, scrollEventThrottle: Int) {
view.scrollEventThrottle = scrollEventThrottle
}
@ReactProp(name = "horizontal")
@Suppress("UNUSED_PARAMETER")
public fun setHorizontal(view: ReactScrollView?, horizontal: Boolean) {
// Do Nothing: Align with static ViewConfigs
}
@ReactProp(name = "isInvertedVirtualizedList")
public fun setIsInvertedVirtualizedList(view: ReactScrollView, applyFix: Boolean) {
// Usually when inverting the scroll view we are using scaleY: -1 on the list
// and on the parent container. HOWEVER, starting from android API 33 there is
// a bug that can cause an ANR due to that. Thus we are using different transform
// commands to circumvent the ANR. This however causes the vertical scrollbar to
// be on the wrong side. Thus we are moving it to the other side, when the list
// is inverted.
// See also:
// - https://github.com/facebook/react-native/issues/35350
// - https://issuetracker.google.com/issues/287304310
if (applyFix) {
view.verticalScrollbarPosition = View.SCROLLBAR_POSITION_LEFT
} else {
view.verticalScrollbarPosition = View.SCROLLBAR_POSITION_DEFAULT
}
}
public companion object {
public const val REACT_CLASS: String = "RCTScrollView"
public fun createExportedCustomDirectEventTypeConstants(): Map<String, Any> =
mapOf(
getJSEventName(ScrollEventType.SCROLL) to mapOf("registrationName" to "onScroll"),
getJSEventName(ScrollEventType.BEGIN_DRAG) to
mapOf("registrationName" to "onScrollBeginDrag"),
getJSEventName(ScrollEventType.END_DRAG) to
mapOf("registrationName" to "onScrollEndDrag"),
getJSEventName(ScrollEventType.MOMENTUM_BEGIN) to
mapOf("registrationName" to "onMomentumScrollBegin"),
getJSEventName(ScrollEventType.MOMENTUM_END) to
mapOf("registrationName" to "onMomentumScrollEnd"))
}
}