Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -844,19 +844,19 @@ - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
[self _handleFinishedScrolling:scrollView];
}

- (void)didMoveToWindow
- (void)willMoveToWindow:(UIWindow *)newWindow
{
[super didMoveToWindow];
[super willMoveToWindow:newWindow];

if (!self.window) {
if (!newWindow) {
// The view is being removed, ensure that the scroll end event is dispatched
[self _handleScrollEndIfNeeded];
}
}

- (void)_handleScrollEndIfNeeded
{
if (_scrollView.isDecelerating || !_scrollView.isTracking) {
if (_scrollView.isDecelerating) {
if (!_eventEmitter) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@

#import <React/RCTScrollViewComponentView.h>
#import <XCTest/XCTest.h>
#import <react/renderer/components/scrollview/ScrollViewEventEmitter.h>
#import <react/renderer/components/scrollview/ScrollViewProps.h>
#import <react/renderer/components/scrollview/ScrollViewShadowNode.h>

using facebook::react::EventDispatcher;
using facebook::react::Props;
using facebook::react::ScrollViewEventEmitter;
using facebook::react::ScrollViewProps;
using facebook::react::ScrollViewShadowNode;

Expand Down Expand Up @@ -61,6 +64,21 @@ - (void)testAutomaticallyAdjustKeyboardInsetsAcrossRecycling
XCTAssertEqual(view.scrollView.contentInset.bottom, 50);
}

- (void)testUnmountingIdleScrollViewDoesNotEndMomentum
{
UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
RCTScrollViewComponentView *view = [[RCTScrollViewComponentView alloc] initWithFrame:window.bounds];
[window addSubview:view];

auto eventEmitter = std::make_shared<ScrollViewEventEmitter>(nullptr, EventDispatcher::Weak{});
[view updateEventEmitter:eventEmitter];
[view setValue:@YES forKey:@"isUserTriggeredScrolling"];

[view removeFromSuperview];

XCTAssertTrue([[view valueForKey:@"isUserTriggeredScrolling"] boolValue]);
}

@end

#endif
56 changes: 56 additions & 0 deletions packages/rn-tester/.maestro/scrollview-momentum-events.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
appId: ${APP_ID} # iOS: com.meta.RNTester.localDevelopment | Android: com.facebook.react.uiapp
---
- launchApp
- stopApp
- openLink: rntester://example/ScrollViewExample/onMomentumScroll
- runFlow: ./helpers/confirm-open-link.yml
- extendedWaitUntil:
visible:
id: 'momentum-scroll-view'
timeout: 120000
- assertVisible:
id: 'momentum-scroll-begin-count'
text: 'onMomentumScrollBegin called 0 times'
- assertVisible:
id: 'momentum-scroll-end-count'
text: 'onMomentumScrollEnd called 0 times'
# Removing an idle ScrollView must not synthesize a momentum-end event.
- tapOn:
id: 'toggle-momentum-scroll-view'
- assertNotVisible:
id: 'momentum-scroll-view'
- waitForAnimationToEnd:
timeout: 1000
- assertVisible:
id: 'momentum-scroll-end-count'
text: 'onMomentumScrollEnd called 0 times'
- tapOn:
id: 'toggle-momentum-scroll-view'
- assertVisible:
id: 'momentum-scroll-view'
# One momentum scroll must produce exactly one begin and one end event.
- swipe:
from:
id: 'momentum-scroll-view'
point: 50%, 80%
direction: UP
duration: 300
- waitForAnimationToEnd:
timeout: 5000
- assertVisible:
id: 'momentum-scroll-begin-count'
text: 'onMomentumScrollBegin called 1 times'
- assertVisible:
id: 'momentum-scroll-end-count'
text: 'onMomentumScrollEnd called 1 times'
# Unmounting after momentum has ended must not increment the count again.
- tapOn:
id: 'toggle-momentum-scroll-view'
- waitForAnimationToEnd:
timeout: 1000
- assertVisible:
id: 'momentum-scroll-begin-count'
text: 'onMomentumScrollBegin called 1 times'
- assertVisible:
id: 'momentum-scroll-end-count'
text: 'onMomentumScrollEnd called 1 times'
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const Item = ({item, separators}: ListRenderItemInfo<string>) => {
};

type Props = Readonly<{
data?: ReadonlyArray<string>,
exampleProps: Partial<React.ElementConfig<typeof FlatList>>,
exampleTestID?: ?string,
onTest?: ?() => void,
Expand Down Expand Up @@ -95,7 +96,7 @@ const BaseFlatListExample: component(
ref={ref}
testID="flat_list"
// $FlowFixMe[incompatible-type]
data={DATA}
data={props.data ?? DATA}
keyExtractor={(item, index) => item + index}
style={styles.list}
// $FlowFixMe[incompatible-type]
Expand Down
126 changes: 111 additions & 15 deletions packages/rn-tester/js/examples/FlatList/FlatList-onEndReached.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,41 +12,137 @@

import type {RNTesterModuleExample} from '../../types/RNTesterTypes';

import BaseFlatListExample from './BaseFlatListExample';
import BaseFlatListExample, {ITEM_HEIGHT} from './BaseFlatListExample';
import * as React from 'react';
import {useRef, useState} from 'react';
import {Button, FlatList} from 'react-native';

const DATA = Array.from({length: 20}, (_, index) => `Item ${index}`);
const SCROLL_TO_ITEM = DATA[18];

type EventCounts = {
dragEvents: number,
onEndReached: number,
onMomentumScrollEnd: number,
onStartReached: number,
};

export component FlatList_onEndReached() {
const [output, setOutput] = useState('');
const exampleProps = {
onEndReached: (info: {distanceFromEnd: number, ...}) =>
setOutput('onEndReached'),
onEndReachedThreshold: 0,
const [output, setOutput] = useState('ready');
const listRef = useRef<?FlatList<string>>(null);
const actionRef = useRef<'end' | 'start'>('end');
const attemptsRef = useRef(0);
const eventCountsRef = useRef<EventCounts>({
dragEvents: 0,
onEndReached: 0,
onMomentumScrollEnd: 0,
onStartReached: 0,
});
const recordEventsRef = useRef(false);
const momentumEndedRef = useRef(false);

const report = () => {
const {dragEvents, onEndReached, onMomentumScrollEnd, onStartReached} =
eventCountsRef.current;
setOutput(
`${actionRef.current}: attempts=${attemptsRef.current}, ` +
`onEndReached=${onEndReached}, onStartReached=${onStartReached}, ` +
`onMomentumScrollEnd=${onMomentumScrollEnd}, dragEvents=${dragEvents}`,
);
};

const onEndReached = () => {
if (recordEventsRef.current) {
eventCountsRef.current.onEndReached++;
if (momentumEndedRef.current) {
report();
}
}
};

const onStartReached = () => {
if (recordEventsRef.current) {
eventCountsRef.current.onStartReached++;
if (momentumEndedRef.current) {
report();
}
}
};
const ref = useRef<any>(null);

const onTest = () => {
const scrollResponder = ref?.current?.getScrollResponder();
if (scrollResponder != null) {
scrollResponder.scrollToEnd();
const onMomentumScrollEnd = () => {
if (recordEventsRef.current) {
eventCountsRef.current.onMomentumScrollEnd++;
momentumEndedRef.current = true;
report();
}
};

const onDragEvent = () => {
if (recordEventsRef.current) {
eventCountsRef.current.dragEvents++;
}
};

const scrollToEnd = () => {
recordEventsRef.current = true;
momentumEndedRef.current = false;
actionRef.current = 'end';
attemptsRef.current++;
setOutput('running');
listRef.current?.scrollToItem({
animated: true,
item: SCROLL_TO_ITEM,
viewOffset: -ITEM_HEIGHT,
});
};

const scrollToStart = () => {
momentumEndedRef.current = false;
actionRef.current = 'start';
attemptsRef.current++;
setOutput('running');
listRef.current?.scrollToOffset({animated: true, offset: 0});
};

const exampleProps = {
initialNumToRender: 19,
onEndReached,
onEndReachedThreshold: 0.2,
onMomentumScrollEnd,
onScrollBeginDrag: onDragEvent,
onScrollEndDrag: onDragEvent,
onStartReached,
onStartReachedThreshold: 0.1,
windowSize: 2,
};

return (
<BaseFlatListExample
ref={ref}
ref={listRef}
data={DATA}
exampleProps={exampleProps}
testOutput={output}
onTest={onTest}
/>
onTest={scrollToEnd}
testLabel="Scroll to item">
<Button
testID="scroll_to_start"
onPress={scrollToStart}
title="Scroll to start"
/>
<Button
testID="scroll_to_end"
onPress={scrollToEnd}
title="Scroll to item"
/>
</BaseFlatListExample>
);
}

export default {
title: 'onEndReached',
name: 'onEndReached',
description:
'Scroll to end of list or tap Test button to see `onEndReached` triggered.',
'Programmatic scrolling calls edge callbacks once and does not emit drag callbacks.',
render: function () {
return <FlatList_onEndReached />;
},
Expand Down
41 changes: 31 additions & 10 deletions packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -382,9 +382,10 @@ const examples: Array<RNTesterModuleExample> = [
},
},
{
name: 'onMomentumScroll',
title: '<ScrollView> OnMomentumScroll\n',
description:
'An alert will be called when the momentum scroll starts or ends.',
'Counts momentum scroll events and supports unmounting the ScrollView.',
render(): React.Node {
return <OnMomentumScroll />;
},
Expand Down Expand Up @@ -920,17 +921,37 @@ const OnScrollOptions = () => {
};

const OnMomentumScroll = () => {
const [scroll, setScroll] = useState('none');
const [scrollViewMounted, setScrollViewMounted] = useState(true);
const [momentumScrollBeginCount, setMomentumScrollBeginCount] = useState(0);
const [momentumScrollEndCount, setMomentumScrollEndCount] = useState(0);

return (
<View>
<RNTesterText>Scroll State: {scroll}</RNTesterText>
<ScrollView
style={[styles.scrollView, {height: 200}]}
onMomentumScrollBegin={() => setScroll('onMomentumScrollBegin')}
onMomentumScrollEnd={() => setScroll('onMomentumScrollEnd')}
nestedScrollEnabled>
{ITEMS.map(createItemRow)}
</ScrollView>
<RNTesterText testID="momentum-scroll-begin-count">
onMomentumScrollBegin called {momentumScrollBeginCount} times
</RNTesterText>
<RNTesterText testID="momentum-scroll-end-count">
onMomentumScrollEnd called {momentumScrollEndCount} times
</RNTesterText>
<Button
label={scrollViewMounted ? 'Unmount ScrollView' : 'Mount ScrollView'}
onPress={() => setScrollViewMounted(mounted => !mounted)}
testID="toggle-momentum-scroll-view"
/>
{scrollViewMounted ? (
<ScrollView
style={styles.scrollView}
onMomentumScrollBegin={() =>
setMomentumScrollBeginCount(count => count + 1)
}
onMomentumScrollEnd={() =>
setMomentumScrollEndCount(count => count + 1)
}
testID="momentum-scroll-view"
nestedScrollEnabled>
{ITEMS.map(createItemRow)}
</ScrollView>
) : null}
</View>
);
};
Expand Down
Loading
Loading