Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix iOS Paper Scroll Event RTL check #40751

Closed
wants to merge 3 commits into from
Closed
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 @@ -27,6 +27,6 @@
*/
@property (nonatomic, assign) YGDirection baseDirection;

- (void)layoutWithAffectedShadowViews:(NSHashTable<RCTShadowView *> *)affectedShadowViews;
- (void)layoutWithAffectedShadowViews:(NSPointerArray *)affectedShadowViews;

@end
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ - (void)insertReactSubview:(RCTShadowView *)subview atIndex:(NSInteger)atIndex
}
}

- (void)layoutWithAffectedShadowViews:(NSHashTable<RCTShadowView *> *)affectedShadowViews
- (void)layoutWithAffectedShadowViews:(NSPointerArray *)affectedShadowViews
{
NSHashTable<NSString *> *other = [NSHashTable new];

Expand Down
2 changes: 1 addition & 1 deletion packages/react-native/React/Modules/RCTUIManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView *
{
RCTAssertUIManagerQueue();

NSHashTable<RCTShadowView *> *affectedShadowViews = [NSHashTable weakObjectsHashTable];
NSPointerArray *affectedShadowViews = [NSPointerArray weakObjectsPointerArray];
[rootShadowView layoutWithAffectedShadowViews:affectedShadowViews];

if (!affectedShadowViews.count) {
Expand Down
2 changes: 1 addition & 1 deletion packages/react-native/React/Views/RCTLayout.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ typedef struct CG_BOXABLE RCTLayoutMetrics RCTLayoutMetrics;

struct RCTLayoutContext {
CGPoint absolutePosition;
__unsafe_unretained NSHashTable<RCTShadowView *> *_Nonnull affectedShadowViews;
__unsafe_unretained NSPointerArray *_Nonnull affectedShadowViews;
__unsafe_unretained NSHashTable<NSString *> *_Nonnull other;
};
typedef struct CG_BOXABLE RCTLayoutContext RCTLayoutContext;
Expand Down
2 changes: 1 addition & 1 deletion packages/react-native/React/Views/RCTRootShadowView.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@
*/
@property (nonatomic, assign) YGDirection baseDirection;

- (void)layoutWithAffectedShadowViews:(NSHashTable<RCTShadowView *> *)affectedShadowViews;
- (void)layoutWithAffectedShadowViews:(NSPointerArray *)affectedShadowViews;

@end
2 changes: 1 addition & 1 deletion packages/react-native/React/Views/RCTRootShadowView.m
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ - (instancetype)init
return self;
}

- (void)layoutWithAffectedShadowViews:(NSHashTable<RCTShadowView *> *)affectedShadowViews
- (void)layoutWithAffectedShadowViews:(NSPointerArray *)affectedShadowViews
{
NSHashTable<NSString *> *other = [NSHashTable new];

Expand Down
2 changes: 1 addition & 1 deletion packages/react-native/React/Views/RCTShadowView.m
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ - (void)layoutWithMetrics:(RCTLayoutMetrics)layoutMetrics layoutContext:(RCTLayo
{
if (!RCTLayoutMetricsEqualToLayoutMetrics(self.layoutMetrics, layoutMetrics)) {
self.layoutMetrics = layoutMetrics;
[layoutContext.affectedShadowViews addObject:self];
[layoutContext.affectedShadowViews addPointer:((__bridge void *)self)];
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1058,7 +1058,7 @@ - (void)sendScrollEventWithName:(NSString *)eventName
}

CGPoint offset = scrollView.contentOffset;
if ([UIApplication sharedApplication].userInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft) {
if ([self reactLayoutDirection] == UIUserInterfaceLayoutDirectionRightToLeft) {
offset.x = scrollView.contentSize.width - scrollView.frame.size.width - offset.x;
}

Expand Down
4 changes: 2 additions & 2 deletions packages/rn-tester/RNTesterUnitTests/RCTShadowViewTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ - (void)testApplyingLayoutRecursivelyToShadowView
[self.parentView insertReactSubview:mainView atIndex:1];
[self.parentView insertReactSubview:footerView atIndex:2];

[self.parentView layoutWithAffectedShadowViews:[NSHashTable weakObjectsHashTable]];
[self.parentView layoutWithAffectedShadowViews:[NSPointerArray weakObjectsPointerArray]];

XCTAssertTrue(
CGRectEqualToRect([self.parentView measureLayoutRelativeToAncestor:self.parentView], CGRectMake(0, 0, 440, 440)));
Expand Down Expand Up @@ -187,7 +187,7 @@ - (void)_withShadowViewWithStyle:(void (^)(YGNodeRef node))configBlock
RCTShadowView *view = [self _shadowViewWithConfig:configBlock];
[self.parentView insertReactSubview:view atIndex:0];
view.intrinsicContentSize = contentSize;
[self.parentView layoutWithAffectedShadowViews:[NSHashTable weakObjectsHashTable]];
[self.parentView layoutWithAffectedShadowViews:[NSPointerArray weakObjectsPointerArray]];
CGRect actualRect = [view measureLayoutRelativeToAncestor:self.parentView];
XCTAssertTrue(
CGRectEqualToRect(expectedRect, actualRect),
Expand Down
100 changes: 9 additions & 91 deletions packages/virtualized-lists/Lists/ListMetricsAggregator.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ import {keyExtractor as defaultKeyExtractor} from './VirtualizeUtils';

import invariant from 'invariant';

type LayoutEventDirection = 'top-down' | 'bottom-up';

export type CellMetrics = {
/**
* Index of the item in the list
Expand Down Expand Up @@ -55,25 +53,12 @@ export type CellMetricProps = {
...
};

type UnresolvedCellMetrics = {
index: number,
layout: Layout,
isMounted: boolean,

// The length of list content at the time of layout is needed to correctly
// resolve flow relative offset in RTL. We are lazily notified of this after
// the layout of the cell, unless the cell relayout does not cause a length
// change. To keep stability, we use content length at time of query, or
// unmount if never queried.
listContentLength?: ?number,
};

/**
* Provides an interface to query information about the metrics of a list and its cells.
*/
export default class ListMetricsAggregator {
_averageCellLength = 0;
_cellMetrics: Map<string, UnresolvedCellMetrics> = new Map();
_cellMetrics: Map<string, CellMetrics> = new Map();
_contentLength: ?number;
_highestMeasuredCellIndex = 0;
_measuredCellsLength = 0;
Expand All @@ -83,10 +68,6 @@ export default class ListMetricsAggregator {
rtl: false,
};

// Fabric and Paper may call onLayout in different orders. We can tell which
// direction layout events happen on the first layout.
_onLayoutDirection: LayoutEventDirection = 'top-down';

/**
* Notify the ListMetricsAggregator that a cell has been laid out.
*
Expand All @@ -103,39 +84,22 @@ export default class ListMetricsAggregator {
orientation: ListOrientation,
layout: Layout,
}): boolean {
if (this._contentLength == null) {
this._onLayoutDirection = 'bottom-up';
}

this._invalidateIfOrientationChanged(orientation);

// If layout is top-down, our most recently cached content length
// corresponds to this cell. Otherwise, we need to resolve when events fire
// up the tree to the new length.
const listContentLength =
this._onLayoutDirection === 'top-down' ? this._contentLength : null;

const next: UnresolvedCellMetrics = {
const next: CellMetrics = {
index: cellIndex,
layout: layout,
length: this._selectLength(layout),
isMounted: true,
listContentLength,
offset: this.flowRelativeOffset(layout),
};
const curr = this._cellMetrics.get(cellKey);

if (
!curr ||
this._selectOffset(next.layout) !== this._selectOffset(curr.layout) ||
this._selectLength(next.layout) !== this._selectLength(curr.layout) ||
(curr.listContentLength != null &&
curr.listContentLength !== this._contentLength)
) {
if (!curr || next.offset !== curr.offset || next.length !== curr.length) {
if (curr) {
const dLength =
this._selectLength(next.layout) - this._selectLength(curr.layout);
const dLength = next.length - curr.length;
this._measuredCellsLength += dLength;
} else {
this._measuredCellsLength += this._selectLength(next.layout);
this._measuredCellsLength += next.length;
this._measuredCellsCount += 1;
}

Expand Down Expand Up @@ -174,21 +138,7 @@ export default class ListMetricsAggregator {
layout: $ReadOnly<{width: number, height: number}>,
}): void {
this._invalidateIfOrientationChanged(orientation);
const newLength = this._selectLength(layout);

// Fill in any just-measured cells which did not have this length available.
// This logic assumes that cell relayout will always change list content
// size, which isn't strictly correct, but issues should be rare and only
// on Paper.
if (this._onLayoutDirection === 'bottom-up') {
for (const cellMetric of this._cellMetrics.values()) {
if (cellMetric.listContentLength == null) {
cellMetric.listContentLength = newLength;
}
}
}

this._contentLength = newLength;
this._contentLength = this._selectLength(layout);
}

/**
Expand Down Expand Up @@ -245,7 +195,7 @@ export default class ListMetricsAggregator {
keyExtractor(getItem(data, index), index),
);
if (frame && frame.index === index) {
return this._resolveCellMetrics(frame);
return frame;
}

if (getItemLayout) {
Expand Down Expand Up @@ -286,26 +236,6 @@ export default class ListMetricsAggregator {
return this._contentLength != null;
}

/**
* Whether the ListMetricsAggregator is notified of cell metrics before
* ScrollView metrics (bottom-up) or ScrollView metrics before cell metrics
* (top-down).
*
* Must be queried after cell layout
*/
getLayoutEventDirection(): LayoutEventDirection {
return this._onLayoutDirection;
}

/**
* Whether the ListMetricsAggregator must be aware of the current length of
* ScrollView content to be able to correctly resolve the (flow-relative)
* metrics of a cell.
*/
needsContentLengthForCellMetrics(): boolean {
return this._orientation.horizontal && this._orientation.rtl;
}

/**
* Finds the flow-relative offset (e.g. starting from the left in LTR, but
* right in RTL) from a layout box.
Expand Down Expand Up @@ -352,7 +282,6 @@ export default class ListMetricsAggregator {

if (orientation.horizontal !== this._orientation.horizontal) {
this._averageCellLength = 0;
this._contentLength = null;
this._highestMeasuredCellIndex = 0;
this._measuredCellsLength = 0;
this._measuredCellsCount = 0;
Expand All @@ -371,15 +300,4 @@ export default class ListMetricsAggregator {
_selectOffset({x, y}: $ReadOnly<{x: number, y: number, ...}>): number {
return this._orientation.horizontal ? x : y;
}

_resolveCellMetrics(metrics: UnresolvedCellMetrics): CellMetrics {
const {index, layout, isMounted, listContentLength} = metrics;

return {
index,
length: this._selectLength(layout),
isMounted,
offset: this.flowRelativeOffset(layout, listContentLength),
};
}
}
35 changes: 3 additions & 32 deletions packages/virtualized-lists/Lists/VirtualizedList.js
Original file line number Diff line number Diff line change
Expand Up @@ -1302,39 +1302,13 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
orientation: this._orientation(),
});

// In RTL layout we need parent content length to calculate the offset of a
// cell from the start of the list. In Paper, layout events are bottom up,
// so we do not know this yet, and must defer calculation until after
// `onContentSizeChange` is called.
const deferCellMetricCalculation =
this._listMetrics.getLayoutEventDirection() === 'bottom-up' &&
this._listMetrics.needsContentLengthForCellMetrics();

// Note: In Paper RTL logical position may have changed when
// `layoutHasChanged` is false if a cell maintains same X/Y coordinates,
// but contentLength shifts. This will be corrected by
// `onContentSizeChange` triggering a cell update.
if (layoutHasChanged) {
this._scheduleCellsToRenderUpdate({
allowImmediateExecution: !deferCellMetricCalculation,
});
this._scheduleCellsToRenderUpdate();
}

this._triggerRemeasureForChildListsInCell(cellKey);

this._computeBlankness();

if (deferCellMetricCalculation) {
if (!this._pendingViewabilityUpdate) {
this._pendingViewabilityUpdate = true;
setTimeout(() => {
this._updateViewableItems(this.props, this.state.cellsAroundViewport);
this._pendingViewabilityUpdate = false;
}, 0);
}
} else {
this._updateViewableItems(this.props, this.state.cellsAroundViewport);
}
this._updateViewableItems(this.props, this.state.cellsAroundViewport);
};

_onCellFocusCapture(cellKey: string) {
Expand Down Expand Up @@ -1765,9 +1739,7 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
}
}

_scheduleCellsToRenderUpdate(opts?: {allowImmediateExecution?: boolean}) {
const allowImmediateExecution = opts?.allowImmediateExecution ?? true;

_scheduleCellsToRenderUpdate() {
// Only trigger high-priority updates if we've actually rendered cells,
// and with that size estimate, accurately compute how many cells we should render.
// Otherwise, it would just render as many cells as it can (of zero dimension),
Expand All @@ -1776,7 +1748,6 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
// If this is triggered in an `componentDidUpdate` followed by a hiPri cellToRenderUpdate
// We shouldn't do another hipri cellToRenderUpdate
if (
allowImmediateExecution &&
this._shouldRenderWithPriority() &&
(this._listMetrics.getAverageCellLength() || this.props.getItemLayout) &&
!this._hiPriInProgress
Expand Down
Loading