introduce experimental mechanism to subscribe to synchronous onScroll (#43593)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/43593

changelog: [internal]

Introduce experimental mechanism to subscribe to synchronous onScroll from outside of ScrollView.

Reviewed By: yungsters

Differential Revision: D55066385

fbshipit-source-id: b7cc173cb32e736f49496c1e0ba37de782ca02aa
This commit is contained in:
Samuel Susla
2024-03-25 06:32:29 -07:00
committed by Facebook GitHub Bot
parent 274faf4b39
commit 95a8c99f3f
8 changed files with 97 additions and 9 deletions
@@ -33,6 +33,7 @@ import StyleSheet from '../../StyleSheet/StyleSheet';
import Dimensions from '../../Utilities/Dimensions';
import dismissKeyboard from '../../Utilities/dismissKeyboard';
import Platform from '../../Utilities/Platform';
import EventEmitter from '../../vendor/emitter/EventEmitter';
import Keyboard from '../Keyboard/Keyboard';
import TextInputState from '../TextInput/TextInputState';
import View from '../View/View';
@@ -676,6 +677,9 @@ export type Props = $ReadOnly<{|
type State = {|
layoutHeight: ?number,
onScrollEmitter: ?EventEmitter<{
scroll: [{x: number, y: number}],
}>,
|};
const IS_ANIMATING_TOUCH_START_THRESHOLD_MS = 16;
@@ -761,6 +765,7 @@ class ScrollView extends React.Component<Props, State> {
state: State = {
layoutHeight: null,
onScrollEmitter: null,
};
componentDidMount() {
@@ -829,6 +834,8 @@ class ScrollView extends React.Component<Props, State> {
if (this._scrollAnimatedValueAttachment) {
this._scrollAnimatedValueAttachment.detach();
}
this.state.onScrollEmitter?.removeAllListeners();
}
/**
@@ -948,6 +955,40 @@ class ScrollView extends React.Component<Props, State> {
Commands.flashScrollIndicators(this._scrollView.nativeInstance);
};
_subscribeToOnScroll: (
callback: ({x: number, y: number}) => void,
) => EventSubscription = callback => {
// An undefined value means the listener has not been added, yet.
// A null value means the listener has been removed.
let subscription: ?EventSubscription;
this.setState(
({onScrollEmitter}) => ({
onScrollEmitter: onScrollEmitter ?? new EventEmitter(),
}),
() => {
// If `subscription` is null, that means it was removed before we got
// here so do nothing.
if (subscription !== null) {
subscription = nullthrows(this.state.onScrollEmitter).addListener(
'scroll',
callback,
);
}
},
);
return {
remove() {
// If `subscription` was created before this invocation, remove it.
subscription?.remove();
// Record this invocation by setting `subscription` to null, in case it
// ends up being created after this invocation.
subscription = null;
},
};
};
/**
* This method should be used as the callback to onFocus in a TextInputs'
* parent view. Note that any module using this mixin needs to return
@@ -1154,6 +1195,11 @@ class ScrollView extends React.Component<Props, State> {
_handleScroll = (e: ScrollEvent) => {
this._observedScrollSinceBecomingResponder = true;
this.props.onScroll && this.props.onScroll(e);
this.state.onScrollEmitter?.emit('scroll', {
x: e.nativeEvent.contentOffset.x,
y: e.nativeEvent.contentOffset.y,
});
};
_handleLayout = (e: LayoutEvent) => {
@@ -1202,6 +1248,8 @@ class ScrollView extends React.Component<Props, State> {
scrollToEnd: this.scrollToEnd,
flashScrollIndicators: this.flashScrollIndicators,
scrollResponderZoomTo: this.scrollResponderZoomTo,
// TODO: Replace unstable_subscribeToOnScroll once scrollView.addEventListener('scroll', (e: ScrollEvent) => {}, {passive: false});
unstable_subscribeToOnScroll: this._subscribeToOnScroll,
scrollResponderScrollNativeHandleToKeyboard:
this.scrollResponderScrollNativeHandleToKeyboard,
},
@@ -1778,6 +1826,7 @@ class ScrollView extends React.Component<Props, State> {
onScroll: this._handleScroll,
endDraggingSensitivityMultiplier:
experimental_endDraggingSensitivityMultiplier,
enableSyncOnScroll: this.state.onScrollEmitter ? true : undefined,
scrollEventThrottle: hasStickyHeaders
? 1
: this.props.scrollEventThrottle,
@@ -133,6 +133,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig =
contentInsetAdjustmentBehavior: true,
decelerationRate: true,
endDraggingSensitivityMultiplier: true,
enableSyncOnScroll: true, // iOS-Fabric only.
directionalLockEnabled: true,
disableIntervalMomentum: true,
indicatorStyle: true,
@@ -459,18 +459,33 @@ static void RCTSendScrollEventForNativeAnimations_DEPRECATED(UIScrollView *scrol
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (!_isUserTriggeredScrolling || CoreFeatures::enableGranularScrollViewStateUpdatesIOS) {
[self _updateStateWithContentOffset];
}
const auto &props = static_cast<const ScrollViewProps &>(*_props);
auto scrollMetrics = [self _scrollViewMetrics];
NSTimeInterval now = CACurrentMediaTime();
if ((_lastScrollEventDispatchTime == 0) || (now - _lastScrollEventDispatchTime > _scrollEventThrottle)) {
_lastScrollEventDispatchTime = now;
if (props.enableSyncOnScroll) {
if (_eventEmitter) {
static_cast<const ScrollViewEventEmitter &>(*_eventEmitter).onScroll([self _scrollViewMetrics]);
const auto &eventEmitter = static_cast<const ScrollViewEventEmitter &>(*_eventEmitter);
// TODO: temporary API to unblock testing of synchronous rendering.
eventEmitter.experimental_flushSync([&eventEmitter, &scrollMetrics, &self]() {
[self _updateStateWithContentOffset];
// TODO: temporary API to unblock testing of synchronous rendering.
eventEmitter.experimental_onDiscreteScroll(scrollMetrics);
});
}
} else {
if (!_isUserTriggeredScrolling || CoreFeatures::enableGranularScrollViewStateUpdatesIOS) {
[self _updateStateWithContentOffset];
}
RCTSendScrollEventForNativeAnimations_DEPRECATED(scrollView, self.tag);
NSTimeInterval now = CACurrentMediaTime();
if ((_lastScrollEventDispatchTime == 0) || (now - _lastScrollEventDispatchTime > _scrollEventThrottle)) {
_lastScrollEventDispatchTime = now;
if (_eventEmitter) {
static_cast<const ScrollViewEventEmitter &>(*_eventEmitter).onScroll(scrollMetrics);
}
RCTSendScrollEventForNativeAnimations_DEPRECATED(scrollView, self.tag);
}
}
[self _remountChildrenIfNeeded];
@@ -63,6 +63,16 @@ void ScrollViewEventEmitter::onScroll(const Metrics& scrollViewMetrics) const {
});
}
void ScrollViewEventEmitter::experimental_onDiscreteScroll(
const Metrics& scrollViewMetrics) const {
dispatchEvent(
"scroll",
[scrollViewMetrics](jsi::Runtime& runtime) {
return scrollViewMetricsPayload(runtime, scrollViewMetrics);
},
RawEvent::Category::Discrete);
}
void ScrollViewEventEmitter::onScrollToTop(
const Metrics& scrollViewMetrics) const {
dispatchUniqueEvent(
@@ -28,6 +28,7 @@ class ScrollViewEventEmitter : public ViewEventEmitter {
};
void onScroll(const Metrics& scrollViewMetrics) const;
void experimental_onDiscreteScroll(const Metrics& scrollViewMetrics) const;
void onScrollBeginDrag(const Metrics& scrollViewMetrics) const;
void onScrollEndDrag(const Metrics& scrollViewMetrics) const;
void onMomentumScrollBegin(const Metrics& scrollViewMetrics) const;
@@ -109,6 +109,15 @@ ScrollViewProps::ScrollViewProps(
"endDraggingSensitivityMultiplier",
sourceProps.endDraggingSensitivityMultiplier,
1)),
enableSyncOnScroll(
CoreFeatures::enablePropIteratorSetter
? sourceProps.enableSyncOnScroll
: convertRawProp(
context,
rawProps,
"enableSyncOnScroll",
sourceProps.enableSyncOnScroll,
false)),
directionalLockEnabled(
CoreFeatures::enablePropIteratorSetter
? sourceProps.directionalLockEnabled
@@ -368,6 +377,8 @@ void ScrollViewProps::setProp(
RAW_SET_PROP_SWITCH_CASE_BASIC(maximumZoomScale);
RAW_SET_PROP_SWITCH_CASE_BASIC(minimumZoomScale);
RAW_SET_PROP_SWITCH_CASE_BASIC(scrollEnabled);
RAW_SET_PROP_SWITCH_CASE_BASIC(enableSyncOnScroll);
RAW_SET_PROP_SWITCH_CASE_BASIC(endDraggingSensitivityMultiplier);
RAW_SET_PROP_SWITCH_CASE_BASIC(pagingEnabled);
RAW_SET_PROP_SWITCH_CASE_BASIC(pinchGestureEnabled);
RAW_SET_PROP_SWITCH_CASE_BASIC(scrollsToTop);
@@ -42,6 +42,7 @@ class ScrollViewProps final : public ViewProps {
bool automaticallyAdjustsScrollIndicatorInsets{true};
Float decelerationRate{0.998f};
Float endDraggingSensitivityMultiplier{1};
bool enableSyncOnScroll{false};
bool directionalLockEnabled{};
ScrollViewIndicatorStyle indicatorStyle{};
ScrollViewKeyboardDismissMode keyboardDismissMode{};
@@ -583,7 +583,7 @@ jsi::Value UIManagerBinding::get(
auto layoutMetrics = uiManager->getRelativeLayoutMetrics(
*shadowNodeFromValue(runtime, arguments[0]),
shadowNodeFromValue(runtime, arguments[1]).get(),
{/* .includeTransform = */ true});
{/* .includeTransform = */ false});
auto frame = layoutMetrics.frame;
auto result = jsi::Object(runtime);
result.setProperty(runtime, "left", frame.origin.x);