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

Add support for sticky header and fix static header issues with numColumns #129

Merged
merged 19 commits into from Mar 2, 2022

Conversation

naqvitalha
Copy link
Collaborator

@naqvitalha naqvitalha commented Feb 25, 2022

Fixes header implementation and adds Sticky headers.

Changes

  1. Header now goes on top of autolayout and footer as well. New offsets added to to these are taken into account on the native side for blank and visible window detections.
  2. Sticky header implementation and fixes to correct them for use along with headers.
  3. Code refactor to make things more manageable
  4. New pure component wrapper that allows for preventing re-renders in an easier way
  5. Preventing some props to be passed to ScrollView which can impact behaviour of the list

Tophatting

I've tested the changes in Android/iOS. You can add header or pass sticky indices to existing samples.

resolves #95
resolves #44
resolves #96

@naqvitalha naqvitalha changed the title [Draft] Header upgrade Header upgrade Feb 28, 2022
type: "InvariantViolation",
},
stickyWhileHorizontalNotSupported: {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FlatList does the same, these features don't make much sense when in horizontal mode

[other: string]: unknown;
}

export class PureComponentWrapper extends React.PureComponent<PureComponentWrapperProps> {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any props can be passed to this wrapper and it rerenders if any prop changes. This view can also show or hide the view on request.

renderAheadOffset: renderAheadOffset,
windowSize: windowSize,
isHorizontal: horizontal
)
else { return }

let isNextCellVisible = isWithinBounds(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only to make blank events more accurate. Existing compute is not impacted

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add this to the tests, too?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that tests are not setup for iOS clearGaps method. It's not very straightforward, I'll track it in another issue. I've added the tests for Android. Good that you pointed this somehow I forgot.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracking here: #133

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, let's not increase the scope of this PR, thanks!

@@ -16,6 +16,7 @@ module.exports = {
"@shopify/jsx-no-hardcoded-content": "off",
"@shopify/jest/no-vague-titles": "off",
"@shopify/jsx-no-complex-expressions": "off",
"@shopify/react-prefer-private-members": "off",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can have certain public APIs that are not used internally. This rule didn't make sense for a library like this.

blankOffsetAtStart = lastMinBound - actualScrollOffset
blankOffsetAtEnd = actualScrollOffset + windowSize - renderOffset - lastMaxBound
fun computeBlankFromGivenOffset(actualScrollOffset: Int, distanceFromWindowStart: Int, distanceFromWindowEnd: Int): Int {
val actualScrollOffset = actualScrollOffset - offsetFromStart;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By subtracting minY of autolayout from scroll offset we can fix most of the compute

val startOffset = if (alShadow.horizontal) left else top
val endOffset = if (alShadow.horizontal) right else bottom

val distanceFromWindowStart = kotlin.math.max(startOffset - scrollOffset, 0)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Visible portions of header and footer will be removed from the blank area visible on the screen.

/// Checks for overlaps or gaps between adjacent items and then applies a correction.
/// Performance: RecyclerListView renders very small number of views and this is not going to trigger multiple layouts on the iOS side.
private func clearGaps(for cellContainers: [CellContainer]) {
var maxBound: CGFloat = 0
var minBound: CGFloat = CGFloat(Int.max)

var maxBoundNextCell: CGFloat = 0
let correctedScrollOffset = scrollOffset - (horizontal ? self.frame.minX : self.frame.minY)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accounting for offset from top in compute. I've tested the accuracy in iOS as well.

* You can also include any padding that might have been added to the start of the list. RecyclerFlatList needs to this to determine
* where the first item in the list starts from.
*/
estimatedHeaderSize?: number;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only required if someone wants to use initialScrollIndex and expect a lot of accuracy. It's rare but enabling that use case.

render() {
if (this.state.dataProvider.getSize() === 0) {
return this.props.ListEmptyComponent || null;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cleaned up

};
}
const {
drawDistance,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

preventing props being passed to scrollview unless we want some of them to

{children}
</AutoLayoutView>
<>
<PureComponentWrapper
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

header with no unnecessary renders using the new wrapper

>
{children}
</AutoLayoutView>
<PureComponentWrapper
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

footer with no unnecessary renders using the new wrapper

);
};

private itemContainer = (props, parentProps, children) => {
return (
<ItemContainer {...props} index={parentProps.index}>
<ItemContainer
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

styling fixes to support flex:1 in grid mode without breaking other layouts. Consistent with Flatlist now

public scrollToEnd(params?: { animated?: boolean | null | undefined }) {
this.rlvRef?.scrollToEnd(Boolean(params?.animated));
}

// eslint-disable-next-line @shopify/react-prefer-private-members
// TODO: Improve accuracy with headers
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do this later #131

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As of now scrollToIndex doesn't account for header. I'll improve this as a separate PR.

@naqvitalha naqvitalha changed the title Header upgrade Add support for sticky header and fix static header issues with numColumns Feb 28, 2022
ios/Sources/AutoLayoutView.swift Show resolved Hide resolved
ios/Sources/AutoLayoutView.swift Outdated Show resolved Hide resolved
ios/Sources/AutoLayoutView.swift Outdated Show resolved Hide resolved
renderAheadOffset: renderAheadOffset,
windowSize: windowSize,
isHorizontal: horizontal
)
else { return }

let isNextCellVisible = isWithinBounds(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add this to the tests, too?

src/RecyclerFlatList.tsx Outdated Show resolved Hide resolved
@@ -34,6 +39,13 @@ export interface RecyclerFlatListProps<T> extends FlatListProps<T> {
*/
estimatedListSize?: { height: number; width: number };

/**
* Provide estimated size of the header that is going to be rendered. This can help make initialScrollIndex prop more accurate.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, how exactly will the inaccuracy manifest?

Copy link
Collaborator Author

@naqvitalha naqvitalha Mar 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If will fall short by this exact value. Let's say you want to start at index 5 and each item is 100. So we want to start at 400px but if there's a header we need to start at header size + 400px.
This only impacts initial render. We can make it better however, right now I've just provided a way to handle it. In future we can try getting rid of this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree - could you create an issue for this if there already isn't one? I'm ok with the limitation for now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -117,6 +132,9 @@ class RecyclerFlatList<T> extends React.PureComponent<
this.listFixedDimensionSize = props.estimatedListSize.width;
}
}
this.distanceFromWindow = props.estimatedHeaderSize;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather keep the RecyclerFlatList named estimatedHeaderSize and do the translation to how recycler-list-view calls it as late as possible. To me, it's then more obvious why we rename it. But subjective for sure)

src/RecyclerFlatList.tsx Outdated Show resolved Hide resolved
src/WrapperComponent.tsx Outdated Show resolved Hide resolved
naqvitalha and others added 5 commits March 1, 2022 06:28
@naqvitalha
Copy link
Collaborator Author

@fortmarek I've addressed the comments. Regarding header size I've renamed it. I want to keep it as a workaround but I do want to explore alternative solutions. It's just not a big priority right now.

Copy link
Contributor

@fortmarek fortmarek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great, thanks Talha!

import GridLayoutProviderWithProps from "./GridLayoutProviderWithProps";
import CustomError from "./errors/CustomError";
import ExceptionList from "./errors/ExceptionList";

interface StickyProps extends StickyContainerProps {
children: any;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could this be JSX.Element?

@naqvitalha
Copy link
Collaborator Author

Thanks for the review @fortmarek. I've created another issue to track estimatedFirstItemOffset improvement. #134. Merging this.

@naqvitalha naqvitalha merged commit 3788556 into main Mar 2, 2022
@naqvitalha naqvitalha deleted the headerUpgrade branch March 2, 2022 15:11
@shopify-shipit shopify-shipit bot temporarily deployed to production March 2, 2022 17:53 Inactive
@shopify-shipit shopify-shipit bot temporarily deployed to production March 7, 2022 17:03 Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants