Skip to content

Commit

Permalink
Honour sticky header height while scrolling in RLV (Flipkart#439)
Browse files Browse the repository at this point in the history
* Added height and width for StickyObject

* Refactoring onScroll logic in StickyObject

* Adding setter func for StickyHeader top offset

* Adding setter func for StickyHeader top offset

* Accomodate StickyHeader height for RecyclerListView scrollToIndex

* Remove for loop from StickyObject onScroll for perf

* Reverting changes made to RecyclerListView

* Reverting changes made to StickyContainer

* Adding container styles for StickyObject

* Adding overrideContainerRenderer for StickObjects logic in StickyContainer

* Reverting code in StickyContainer

* Reverting code in StickyContainer

* Removing trailing whitespace from StickyContainer and added comments

* Fixing logic to update smallestVisibleIndex

* Remove data field from OverrrideContainerRenderer

* Removed distanceFromWindow param; added correctedScrollOffset func param to StickyContainer

* Removed getCorrectedScrollOffset from StickyObject

* code refactor

* Fixing bugs related to scrollOffset

* Updated README.md

* Removing unused code

* Documentation and code cleaning

* Removing unused code

* Spell correction for RLV propTypes

* Updated propTypes for StickyContainer

* Added TODO comments

* Adding comments for clarification

* Rename stickyContainerRenderer to renderStickyContainer

* changed scrollOffsetCorrection to updateLogicalOffset

* Added images to README.md

* Update README.md

* Added getStartEndCorrection() to RecyclerListView

* Propogate start,end correction to StickyObject

* Added getWindowCorrection() param to StickyContainer

* Updated README.md

* Updated sticky/README.md

* Updated getWindowCorrection() docs

* Updated getWindowCorrection() docs

* Update README.md

* Removing null checks for windowCorrection

* Refactoring updateTrackingWindows code for ViewabilitityTracker

* Refactoring getWindowCorrection to applyWindowCorrection

* Changes in RecyclerListView to accomodate applyWindowCorrection

* Code changes for review comments

* Removing onLayout from StickyObject

* Changes to address review comments

* Refactoring ViewabilityTracker -> updateTrackingWindows

* Resolving review comments

* Refactoring container style for StickyObjects

* Passing WindowCorrection to StickyObjects

* Refactoring getWindowCorrection in StickyObjects

Co-authored-by: Talha Naqvi <naqvitalha@gmail.com>
  • Loading branch information
swapnil1104 and naqvitalha authored Feb 4, 2020
1 parent ee4d5f8 commit c80825f
Show file tree
Hide file tree
Showing 10 changed files with 199 additions and 87 deletions.
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ not be as fast.
| initialRenderIndex | No | number | Specify the initial item index you want rendering to start from. Preferred over initialOffset if both specified |
| scrollThrottle | No | number |iOS only; Scroll throttle duration |
| canChangeSize | No | boolean | Specify if size can change |
| distanceFromWindow | No | number | Web only; Specify how far away the first list item is from window top |
| distanceFromWindow | No | number | **(Depricated)** Use `applyWindowCorrection()` API with `windowShift`. **[Usage?](#applywindowcorrection-usage)** |
| applyWindowCorrection | No | (offset: number, windowCorrection: WindowCorrection) => void | (Enhancement/replacement to `distanceFromWindow` API) Allows updation of the visible windowBounds to based on correctional values passed. User can specify **windowShift**; in case entire RecyclerListWindow needs to shift down/up, **startCorrection**; in case when top window bound needs to be shifted for e.x. top window bound to be shifted down is a content overlapping the top edge of RecyclerListView, **endCorrection**: to alter bottom window bound for a similar use-case. **[Usage?](#applywindowcorrection-usage)** |
| useWindowScroll | No | boolean | Web only; Layout Elements in window instead of a scrollable div |
| disableRecycling | No | boolean | Turns off recycling |
| forceNonDeterministicRendering | No | boolean | Default is false; if enabled dimensions provided in layout provider will not be strictly enforced. Use this if item dimensions cannot be accurately determined |
Expand All @@ -110,6 +111,17 @@ not be as fast.
For full feature set have a look at prop definitions of [RecyclerListView](https://github.com/Flipkart/recyclerlistview/blob/21049cc89ad606ec9fe8ea045dc73732ff29eac9/src/core/RecyclerListView.tsx#L540-L634)
(bottom of the file). All `ScrollView` features like `RefreshControl` also work out of the box.

### applyWindowCorrection usage

`applyWindowCorrection` is used to alter the visible window bounds of the RecyclerListView dynamically. The windowCorrection of RecyclerListView along with the current scroll offset are exposed to the user. The `windowCorrection` object consists of 3 numeric values:
- `windowShift` - Direct replacement of `distanceFromWindow` parameter. Window shift is the offset value by which the RecyclerListView as a whole is displaced within the StickyContainer, use this param to specify how far away the first list item is from window top. This value corrects the scroll offsets for StickyObjects as well as RecyclerListView.
- `startCorrection` - startCorrection is used to specify the shift in the top visible window bound, with which user can receive the correct Sticky header instance even when an external factor like CoordinatorLayout toolbar.
- `endCorrection` - endCorrection is used to specify the shift in the bottom visible window bound, with which user can receive correct Sticky Footer instance when an external factor like bottom app bar is changing the visible view bound.

As seen in the example below

![Alt Text](/docs/images/getWindowCorrection_demo.gif)

## Typescript

Typescript works out of the box. The only execption is with the inherited Scrollview props. In order for Typescript to work with inherited Scrollview props, you must place said inherited Scrollview props within the scrollViewProps prop.
Expand Down
22 changes: 18 additions & 4 deletions docs/guides/sticky/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,21 @@ _setRef(recycler) {
* If using `overrideRowRenderer`, keep in mind that upon scrolling to the very top or bottom of the content, stickies will be hidden. eg. If the first item in the list is given as sticky, scrolling to the top will display the original view and not the overridden view.

### 2) Props
* `stickyHeaderIndices` - An array of indices whose corresponding items need to be stuck to the top of the RecyclerListView once the items scroll off the top. Every subsequent sticky index view will push the previous sticky view off the top to take its place. Needs to be sorted ascending.
* `stickyFooterIndices` - Works same as sticky headers, but for views to be stuck at the bottom of the recyclerView. Needs to be sorted ascending.
* `overrideRowRenderer` - Optional. Will be called instead of rowRenderer for all sticky items. Any changes to the item for when they are stuck can be done here. Refer to sample code for usage.
* `style` - Optional. Pass the same style that is applied to the RecyclerListView component here.
* `stickyHeaderIndices` - An array of indices whose corresponding items need to be stuck to the top of the RecyclerListView once the items scroll off the top. Every subsequent sticky index view will push the previous sticky view off the top to take its place. Needs to be sorted ascending.
* `stickyFooterIndices` - Works same as sticky headers, but for views to be stuck at the bottom of the recyclerView. Needs to be sorted ascending.
* `overrideRowRenderer` - Optional. Will be called instead of rowRenderer for all sticky items. Any changes to the item for when they are stuck can be done here. Refer to sample code for usage.
* `renderStickyContainer` - Optional. Pass a stylized container for StickyHeader and StickyFooter, providing user extensibility to customize the look and feel of these items.
* `applyWindowCorrection` - Optional. Enhancement/replacement of `distanceFromWindow` API. Used when window bound of visible view port needs to be altered. Should be used when visible window bound need to be updated for e.g. Other components overlaying on the RecyclerListView. **[Usage?](#applywindowcorrection-usage)**
* `style` - Optional. Pass the same style that is applied to the RecyclerListView component here.


### applyWindowCorrection usage

`applyWindowCorrection` is used to alter the visible window bounds of the RecyclerListView dynamically. The windowCorrection of RecyclerListView along with the current scroll offset are exposed to the user. The `windowCorrection` object consists of 3 numeric values:
- `windowShift` - Direct replacement of `distanceFromWindow` parameter. Window shift is the offset value by which the RecyclerListView as a whole is displaced within the StickyContainer, use this param to specify how far away the first list item is from window top. This value corrects the scroll offsets for StickyObjects as well as RecyclerListView.
- `startCorrection` - startCorrection is used to specify the shift in the top visible window bound, with which user can receive the correct Sticky header instance even when an external factor like CoordinatorLayout toolbar.
- `endCorrection` - endCorrection is used to specify the shift in the bottom visible window bound, with which user can receive correct Sticky Footer instance when an external factor like bottom app bar is changing the visible view bound.

As seen in the example below

![Alt Text](/docs/images/getWindowCorrection_demo.gif)
Binary file added docs/images/getWindowCorrection_demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 20 additions & 13 deletions src/core/RecyclerListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { Constants } from "./constants/Constants";
import { Messages } from "./constants/Messages";
import BaseScrollComponent from "./scrollcomponent/BaseScrollComponent";
import BaseScrollView, { ScrollEvent, ScrollViewDefaultProps } from "./scrollcomponent/BaseScrollView";
import { TOnItemStatusChanged } from "./ViewabilityTracker";
import { TOnItemStatusChanged, WindowCorrection } from "./ViewabilityTracker";
import VirtualRenderer, { RenderStack, RenderStackItem, RenderStackParams } from "./VirtualRenderer";
import ItemAnimator, { BaseItemAnimator } from "./ItemAnimator";
import { DebugHandlers } from "..";
Expand Down Expand Up @@ -95,7 +95,6 @@ export interface RecyclerListViewProps {
initialRenderIndex?: number;
scrollThrottle?: number;
canChangeSize?: boolean;
distanceFromWindow?: number;
useWindowScroll?: boolean;
disableRecycling?: boolean;
forceNonDeterministicRendering?: boolean;
Expand All @@ -108,6 +107,7 @@ export interface RecyclerListViewProps {
//For all props that need to be proxied to inner/external scrollview. Put them in an object and they'll be spread
//and passed down. For better typescript support.
scrollViewProps?: object;
applyWindowCorrection?: (offsetX: number, offsetY: number, windowCorrection: WindowCorrection) => void;
}

export interface RecyclerListViewState {
Expand All @@ -123,7 +123,6 @@ export default class RecyclerListView<P extends RecyclerListViewProps, S extends
initialRenderIndex: 0,
isHorizontal: false,
onEndReachedThreshold: 0,
distanceFromWindow: 0,
renderAheadOffset: IS_WEB ? 1000 : 250,
};

Expand All @@ -150,6 +149,7 @@ export default class RecyclerListView<P extends RecyclerListViewProps, S extends
private _initialOffset = 0;
private _cachedLayouts?: Layout[];
private _scrollComponent: BaseScrollComponent | null = null;
private _windowCorrection: WindowCorrection;

//If the native content container is used, then positions of the list items are changed on the native side. The animated library used
//by the default item animator also changes the same positions which could lead to inconsistency. Hence, the base item animator which
Expand All @@ -168,6 +168,10 @@ export default class RecyclerListView<P extends RecyclerListViewProps, S extends
internalSnapshot: {},
renderStack: {},
} as S;

this._windowCorrection = {
startCorrection: 0, endCorrection: 0, windowShift: 0,
};
}

public componentWillReceivePropsCompat(newProps: RecyclerListViewProps): void {
Expand Down Expand Up @@ -484,10 +488,14 @@ export default class RecyclerListView<P extends RecyclerListViewProps, S extends
this._pendingScrollToOffset = offset;
this.setState({});
} else {
this._virtualRenderer.startViewabilityTracker();
this._virtualRenderer.startViewabilityTracker(this._getWindowCorrection(offset.x, offset.y, this.props));
}
}

private _getWindowCorrection(offsetX: number, offsetY: number, props: RecyclerListViewProps): WindowCorrection {
return (props.applyWindowCorrection && props.applyWindowCorrection(offsetX, offsetY, this._windowCorrection)) || this._windowCorrection;
}

private _assertDependencyPresence(props: RecyclerListViewProps): void {
if (!props.dataProvider || !props.layoutProvider) {
throw new CustomError(RecyclerListViewExceptions.unresolvedDependenciesException);
Expand Down Expand Up @@ -583,8 +591,9 @@ export default class RecyclerListView<P extends RecyclerListViewProps, S extends
}

private _onScroll = (offsetX: number, offsetY: number, rawEvent: ScrollEvent): void => {
//Adjusting offsets using distanceFromWindow
this._virtualRenderer.updateOffset(offsetX, offsetY, -this.props.distanceFromWindow!, true);
// correction to be positive to shift offset upwards; negative to push offset downwards.
// extracting the correction value from logical offset and updating offset of virtual renderer.
this._virtualRenderer.updateOffset(offsetX, offsetY, true, this._getWindowCorrection(offsetX, offsetY, this.props));

if (this.props.onScroll) {
this.props.onScroll(rawEvent, offsetX, offsetY);
Expand Down Expand Up @@ -672,13 +681,6 @@ RecyclerListView.propTypes = {
//Specify if size can change, listview will automatically relayout items. For web, works only with useWindowScroll = true
canChangeSize: PropTypes.bool,

//Specify how far away the first list item is from start of the RecyclerListView. e.g, if you have content padding on top or left.
//This is an adjustment for optimization and to make sure onVisibileIndexesChanged callback is correct.
//Ideally try to avoid setting large padding values on RLV content. If you have to please correct offsets reported, handle
//them in a custom ScrollView and pass it as an externalScrollView. If you want this to be accounted in scrollToOffset please
//override the method and handle manually.
distanceFromWindow: PropTypes.number,

//Web only. Layout elements in window instead of a scrollable div.
useWindowScroll: PropTypes.bool,

Expand Down Expand Up @@ -721,4 +723,9 @@ RecyclerListView.propTypes = {
//For all props that need to be proxied to inner/external scrollview. Put them in an object and they'll be spread
//and passed down.
scrollViewProps: PropTypes.object,

// Used when the logical offsetY differs from actual offsetY of recyclerlistview, could be because some other component is overlaying the recyclerlistview.
// For e.x. toolbar within CoordinatorLayout are overlapping the recyclerlistview.
// This method exposes the windowCorrection object of RecyclerListView, user can modify the values in realtime.
applyWindowCorrection: PropTypes.func,
};
45 changes: 35 additions & 10 deletions src/core/StickyContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ import { BaseLayoutProvider, Dimension } from "./dependencies/LayoutProvider";
import { BaseDataProvider } from "./dependencies/DataProvider";
import { ReactElement } from "react";
import { ComponentCompat } from "../utils/ComponentCompat";
import { WindowCorrection } from "./ViewabilityTracker";

export interface StickyContainerProps {
children: RecyclerChild;
stickyHeaderIndices?: number[];
stickyFooterIndices?: number[];
overrideRowRenderer?: (type: string | number | undefined, data: any, index: number, extendedState?: object) => JSX.Element | JSX.Element[] | null;
applyWindowCorrection?: (offsetX: number, offsetY: number, winowCorrection: WindowCorrection) => void;
renderStickyContainer?: (stickyContent: JSX.Element, index: number, extendedState?: object) => JSX.Element | null;
style?: StyleProp<ViewStyle>;
}
export interface RecyclerChild extends React.ReactElement<RecyclerListViewProps> {
Expand All @@ -36,11 +39,12 @@ export default class StickyContainer<P extends StickyContainerProps> extends Com
private _layoutProvider: BaseLayoutProvider;
private _extendedState: object | undefined;
private _rowRenderer: ((type: string | number, data: any, index: number, extendedState?: object) => JSX.Element | JSX.Element[] | null);
private _distanceFromWindow: number;

private _stickyHeaderRef: StickyHeader<StickyObjectProps> | null = null;
private _stickyFooterRef: StickyFooter<StickyObjectProps> | null = null;
private _visibleIndicesAll: number[] = [];
private _windowCorrection: WindowCorrection = {
startCorrection: 0, endCorrection: 0, windowShift: 0,
};

constructor(props: P, context?: any) {
super(props, context);
Expand All @@ -50,7 +54,7 @@ export default class StickyContainer<P extends StickyContainerProps> extends Com
this._layoutProvider = childProps.layoutProvider;
this._extendedState = childProps.extendedState;
this._rowRenderer = childProps.rowRenderer;
this._distanceFromWindow = childProps.distanceFromWindow ? childProps.distanceFromWindow : 0;
this._getWindowCorrection(0, 0, props);
}

public componentWillReceivePropsCompat(newProps: P): void {
Expand All @@ -64,6 +68,7 @@ export default class StickyContainer<P extends StickyContainerProps> extends Com
ref: this._getRecyclerRef,
onVisibleIndicesChanged: this._onVisibleIndicesChanged,
onScroll: this._onScroll,
applyWindowCorrection: this._applyWindowCorrection,
});
return (
<View style={this.props.style ? this.props.style : { flex: 1 }}>
Expand All @@ -78,8 +83,9 @@ export default class StickyContainer<P extends StickyContainerProps> extends Com
getRLVRenderedSize={this._getRLVRenderedSize}
getContentDimension={this._getContentDimension}
getRowRenderer={this._getRowRenderer}
getDistanceFromWindow={this._getDistanceFromWindow}
overrideRowRenderer={this.props.overrideRowRenderer} />
overrideRowRenderer={this.props.overrideRowRenderer}
renderContainer={this.props.renderStickyContainer}
getWindowCorrection={this._getCurrentWindowCorrection} />
) : null}
{this.props.stickyFooterIndices ? (
<StickyFooter ref={(stickyFooterRef: any) => this._getStickyFooterRef(stickyFooterRef)}
Expand All @@ -91,8 +97,9 @@ export default class StickyContainer<P extends StickyContainerProps> extends Com
getRLVRenderedSize={this._getRLVRenderedSize}
getContentDimension={this._getContentDimension}
getRowRenderer={this._getRowRenderer}
getDistanceFromWindow={this._getDistanceFromWindow}
overrideRowRenderer={this.props.overrideRowRenderer} />
overrideRowRenderer={this.props.overrideRowRenderer}
renderContainer={this.props.renderStickyContainer}
getWindowCorrection={this._getCurrentWindowCorrection} />
) : null}
</View>
);
Expand All @@ -109,6 +116,10 @@ export default class StickyContainer<P extends StickyContainerProps> extends Com
}
}

private _getCurrentWindowCorrection = (): WindowCorrection => {
return this._windowCorrection;
}

private _getStickyHeaderRef = (stickyHeaderRef: any) => {
if (this._stickyHeaderRef !== stickyHeaderRef) {
this._stickyHeaderRef = stickyHeaderRef as (StickyHeader<StickyObjectProps> | null);
Expand Down Expand Up @@ -143,6 +154,7 @@ export default class StickyContainer<P extends StickyContainerProps> extends Com
}

private _onScroll = (rawEvent: ScrollEvent, offsetX: number, offsetY: number) => {
this._getWindowCorrection(offsetX, offsetY, this.props);
if (this._stickyHeaderRef) {
this._stickyHeaderRef.onScroll(offsetY);
}
Expand All @@ -154,6 +166,10 @@ export default class StickyContainer<P extends StickyContainerProps> extends Com
}
}

private _getWindowCorrection(offsetX: number, offsetY: number, props: StickyContainerProps): WindowCorrection {
return (props.applyWindowCorrection && props.applyWindowCorrection(offsetX, offsetY, this._windowCorrection)) || this._windowCorrection;
}

private _assertChildType = (): void => {
if (React.Children.count(this.props.children) !== 1 || !this._isChildRecyclerInstance()) {
throw new CustomError(RecyclerListViewExceptions.wrongStickyChildTypeException);
Expand Down Expand Up @@ -206,8 +222,10 @@ export default class StickyContainer<P extends StickyContainerProps> extends Com
return undefined;
}

private _getDistanceFromWindow = (): number => {
return this._distanceFromWindow;
private _applyWindowCorrection = (offsetX: number, offsetY: number, windowCorrection: WindowCorrection): void => {
if (this.props.applyWindowCorrection) {
this.props.applyWindowCorrection(offsetX, offsetY, windowCorrection);
}
}

private _initParams = (props: P) => {
Expand All @@ -216,7 +234,6 @@ export default class StickyContainer<P extends StickyContainerProps> extends Com
this._layoutProvider = childProps.layoutProvider;
this._extendedState = childProps.extendedState;
this._rowRenderer = childProps.rowRenderer;
this._distanceFromWindow = childProps.distanceFromWindow ? childProps.distanceFromWindow : 0;
}
}

Expand All @@ -239,4 +256,12 @@ StickyContainer.propTypes = {

// For all practical purposes, pass the style that is applied to the RecyclerListView component here.
style: PropTypes.object,

// For providing custom container to StickyHeader and StickyFooter allowing user extensibility to stylize these items accordingly.
renderStickyContainer: PropTypes.func,

// Used when the logical offsetY differs from actual offsetY of recyclerlistview, could be because some other component is overlaying the recyclerlistview.
// For e.x. toolbar within CoordinatorLayout are overlapping the recyclerlistview.
// This method exposes the windowCorrection object of RecyclerListView, user can modify the values in realtime.
applyWindowCorrection: PropTypes.func,
};
Loading

0 comments on commit c80825f

Please sign in to comment.