From 44c274ae6e06f9760dedfd10dc4e66b8655b208a Mon Sep 17 00:00:00 2001 From: Ruslan Shestopalyuk Date: Wed, 16 Apr 2025 04:57:43 -0700 Subject: [PATCH] Kotlinify ReactScrollViewManager Summary: ## Changelog: [Android] [Internal] - As in the title Reviewed By: cortinico Differential Revision: D72971263 fbshipit-source-id: 8bd0c4e29e48f7e3d47a97f1a4e988e7b0282646 --- .../ReactAndroid/api/ReactAndroid.api | 64 +-- .../react/views/scroll/ReactScrollView.java | 2 +- .../views/scroll/ReactScrollViewManager.java | 409 ------------------ .../views/scroll/ReactScrollViewManager.kt | 386 +++++++++++++++++ 4 files changed, 421 insertions(+), 440 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 02e8b9a82fe..ff493913ad3 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -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 ()V public fun (Lcom/facebook/react/views/scroll/FpsListener;)V - public static fun createExportedCustomDirectEventTypeConstants ()Ljava/util/Map; + public synthetic fun (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 { } - diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java index 82f968b36bd..8cdbcae3d8a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java @@ -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. diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java deleted file mode 100644 index b9260c5ca5a..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java +++ /dev/null @@ -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. - * - *

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 - implements ReactScrollViewCommandHelper.ScrollCommandHandler { - - 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 offsets = new ArrayList(); - 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 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 getExportedCustomDirectEventTypeConstants() { - @Nullable - Map baseEventTypeConstants = super.getExportedCustomDirectEventTypeConstants(); - Map eventTypeConstants = - baseEventTypeConstants == null ? new HashMap() : baseEventTypeConstants; - eventTypeConstants.putAll(createExportedCustomDirectEventTypeConstants()); - return eventTypeConstants; - } - - public static Map createExportedCustomDirectEventTypeConstants() { - return MapBuilder.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); - } - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.kt new file mode 100644 index 00000000000..fa56fec6719 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.kt @@ -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(), ScrollCommandHandler { + + 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 = 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? = ReactScrollViewCommandHelper.getCommandsMap() + + override fun receiveCommand(scrollView: ReactScrollView, commandId: Int, args: ReadableArray?) { + receiveCommand(this, scrollView, commandId, args) + } + + override fun receiveCommand( + scrollView: ReactScrollView, + commandId: String, + args: ReadableArray? + ) { + receiveCommand(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? { + 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 = + 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")) + } +}