Flipkart / recyclerlistview

High performance listview for React Native and web!
Apache License 2.0
5.22k stars 429 forks source link

What is a way to add header to Recyclerlistview ?? #233

Open Kimotn opened 6 years ago

Kimotn commented 6 years ago

Add header to Recyclerlistview same this : https://medium.com/appandflow/react-native-scrollview-animated-header-10a18cb9469e Thanks

naqvitalha commented 6 years ago

Pass your own ScrollView with the header added already. That would be an easy way. We don't support it by default because measuring header will cause an extra frame skip. Those who want it should do it themselves and then offset its height using distanceFromWindow

tafelito commented 6 years ago

@naqvitalha I'm using an externalScrollView to render a header like this

{renderHeader && renderHeader()}
<ScrollViewer
    ref="refS"
     {...this.props}
     onSizeChanged={() => {
         /* Do nothing */
     }}
/>

The problem here is that the header is always on top of the list while scrolling.

What did you mean when you said offset the height using distanceFromWindow?

dancherb commented 5 years ago

I added one by going into: node_modules\recyclerlistview\dist\reactnative\platform\reactnative\scrollcomponent\ScrollComponent.js

And adding the following line as line 53 (after "} }, ") this.props.renderHeader ? this.props.renderHeader() : null,

Maybe not good practise but has worked for me so far. This means the header 'scrolls away', instead of always being fixed at the top of the list.

evert-smit commented 5 years ago

@tafelito

What did you mean when you said offset the height using distanceFromWindow?

It's a prop of RecyclerListView that you can use, see here.

Note that your other issue ("The problem here is that the header is always on top of the list while scrolling.") is caused by rendering your header outside of the ScrollView - you want it to be IN the ScrollView.

Komeyl94 commented 5 years ago

I also had this issue and my solution was to add the header as the first item of the list and gave it its own ViewType, and it worked perfectly.

tafelito commented 5 years ago

@evert-smit distanceFromWindow won't work in this case, even if I add the header inside the ScrollView. The solution was to do something similar to what @Komeyl94 did. It's not nice, and it's a hack, and you have to know the height of the header beforehand

JhowEMK commented 5 years ago

I also had this issue and my solution was to add the header as the first item of the list and gave it its own ViewType, and it worked perfectly.

you can make example ?

phil-flyclops commented 5 years ago

I had to solve this problem as well so I thought it would be worth putting an annotated example of how you can do this in a way that is compatible will what RecyclerListView expects.

If you replace Header with whatever component you need you'll get a RecyclerListView with that header that exists within the confines of the list

import PropTypes from 'prop-types';
import React from 'react';
import { RecyclerListView } from 'recyclerlistview';
import { ScrollView, StyleSheet, View } from 'react-native';

// Create header component with gap
const style = StyleSheet.create({
  header: {
    marginTop: 16,
  },
});
const Header = () => <View style={style.header} />;

// Use forwardRef to wrap a ScrollView that has the Header before the rest of the
// Children are rendered.  forwardRef is there because RecyclerListView expects whatever you 
// pass it as the externalScrollView to have the same methods available as ScrollView
const ScrollViewWithHeader = React.forwardRef(({ children, ...props }, ref) => {
  return <ScrollView
    ref={ref}
    {...props} 
  >
    <Header />
    {children}
  </ScrollView>;
});

// Overriding PropType because it doesn't expect a forwardRef response even though that
// works without issue
RecyclerListView.propTypes.externalScrollView = PropTypes.object;

// Use the headered scroll view to underly the RecyclerList 
const RecyclerListViewWithHeader = (props) => {
  return <RecyclerListView
    externalScrollView={ScrollViewWithHeader}
    {...props} />;
};

export default RecyclerListViewWithHeader;
ilicnenad commented 5 years ago

I had to solve this problem as well so I thought it would be worth putting an annotated example of how you can do this in a way that is compatible will what RecyclerListView expects.

If you replace Header with whatever component you need you'll get a RecyclerListView with that header that exists within the confines of the list

import PropTypes from 'prop-types';
import React from 'react';
import { RecyclerListView } from 'recyclerlistview';
import { ScrollView, StyleSheet, View } from 'react-native';

// Create header component with gap
const style = StyleSheet.create({
  header: {
    marginTop: 16,
  },
});
const Header = () => <View style={style.header} />;

// Use forwardRef to wrap a ScrollView that has the Header before the rest of the
// Children are rendered.  forwardRef is there because RecyclerListView expects whatever you 
// pass it as the externalScrollView to have the same methods available as ScrollView
const ScrollViewWithHeader = React.forwardRef(({ children, ...props }, ref) => {
  return <ScrollView
    ref={ref}
    {...props} 
  >
    <Header />
    {children}
  </ScrollView>;
});

// Overriding PropType because it doesn't expect a forwardRef response even though that
// works without issue
RecyclerListView.propTypes.externalScrollView = PropTypes.object;

// Use the headered scroll view to underly the RecyclerList 
const RecyclerListViewWithHeader = (props) => {
  return <RecyclerListView
    externalScrollView={ScrollViewWithHeader}
    {...props} />;
};

export default RecyclerListViewWithHeader;

I tried you implementation and it works ok, but I noticed one problem, by scrolling down rows rendering ok, and by scrolling up row is rendered when I reach half of the row, so I can see white row until I reach half of it. Then it is populated. When I remove Header it works normally. Do you have solution for this? This is how Recycle view looks like :

<RecyclerListView style={{paddingTop: 8, paddingBottom: 60}} contentContainerStyle={{ alignSelf: 'center', }} externalScrollView={this.externalScrollView} forceNonDeterministicRendering={true} canChangeSize={true} dataProvider={this.state.dataProvider} rowRenderer={this.rowRenderer} layoutProvider={this.layoutProvider} refreshing={true} onRefresh={this.onRefresh} keyExtractor={item => item.id.toString()} onScroll={this._scrolled.bind(this)} extendedState={this.state} onVisibleIndexesChanged={this.onViewableItemsChanged} onEndReached={this.onEndReached} onEndReachedThreshold={0.9} viewabilityConfig={this.viewabilityConfig} snapToAlignment={"center"} />

sbearben commented 5 years ago

@naqvitalha @evert-smit distanceFromWindow would only be a requirement for web, right? Or would this be necessary in a react-native project?

I've implemented a header using the external scrollview approach, however I get some weird scrolling behaviour/jumping in the list when we've scrolled near the end of it

abduraufsherkulov commented 4 years ago

The issue is still present, haven't found a decent example how to implement a header. Passing it as a first item doesn't help, because if I have an action in header which triggers new data load, header also gets rerendered which is annoying. By the way, passing my own header also rerenders the header.

chanmathewrbc commented 4 years ago

The issue is still present, haven't found a decent example how to implement a header. Passing it as a first item doesn't help, because if I have an action in header which triggers new data load, header also gets rerendered which is annoying. By the way, passing my own header also rerenders the header.

I actually have the opposite issue, because the header doesn't consistently re-render, sometimes it doesn't re-render when I need it to when the state has updated. So sometimes when I click a dropdown menu I've implemented in the header, the dropdown menu doesn't re-render to reflect the selected option, even if the list rows do get updated...

dariuscosden commented 3 years ago

@abduraufsherkulov @chanmathewrbc I had the same problems with re-rendering the header both when I didn't want and also not re-rendering when I want.

What I did to fix my issues:

1) Use the custom ScrollView listed above, but wrap it in useMemo. Give it a dependency array if you need to control when it should re-render or not. Note: This is only for the outer ScrollView component, not for the header

2) If you do the above with an empty dependency array, your header won't re-render at all. Your first instinct might be to add your header component to the dependency array of the above useMemo. If you do that, your header will re-render, but so will your entire scroll view and you will always be scrolled instantly to the top.

3) Fortunately, you can pass props with scrollViewProps. These props bypass the useMemo and will always be updated, regardless of the scroll view.

So for example

const ScrollViewWithHeader = React.useMemo(() => React.forwardRef(({ children, ...props }, ref) => {
  return <ScrollView
    ref={ref}
    {...props} 
  >
    {props.headerComponent}
    {children}
  </ScrollView>;
}), []); // Pass dependencies for ScrollView here

const RecyclerListViewWithHeader = (props) => {
  return <RecyclerListView
    externalScrollView={ScrollViewWithHeader}
    scrollViewProps={
        { headerComponent: Header }
    }
    {...props} />;
};

Now, your ScrollView is completely controllable and will re-render only when you want to. At the same time, your headerComponent can be controlled separately. If you want to control when it re-renders, you can wrap it in its own useMemo.

This fixed my scenario where I needed to have a custom header component that only re-rendered certain times, while leaving the rest of my list intact. I gave my ScrollView an empty dependency array and just re-rendered my headerComponent.

Hope this helps!

ehsan6sha commented 3 years ago

{...props} />;

Thank you for sharing. Can you please also share how your layoutProvider looks and how you accounted for header height? Does this support multiple headers in between data?

dariuscosden commented 3 years ago

@ehsan6sha Header height should be automatically considered since it is rendered on top of the list-view. If you only have a header and then your list items, regardless of the header height, it should work.

As for multiple headers in between data, it probably will need more customisation. I haven't had this use case so I am not sure.

My layout provider is pretty basic

    const [layoutProvider] = useState(
      new LayoutProvider(
        () => {
          return 0;
        },
        (type, dim) => {
          dim.width = Dimensions.get("screen").width;
          dim.height = estimatedLayoutHeight; // Comes from parent or defaults to 80
        }
      )
    );
dzpt commented 3 years ago

@dariuscosden the error scrolling up and down with half white cell item is persist. @ilicnenad have you found the solution yet?

troberts-28 commented 3 weeks ago

Very late to the party, I've written a generic RecyclerListViewWithHeader component that builds on @dariuscosden's work and also addresses the whitespace issue raised by @dzpt and @ilicnenad by applying a window correction to the RLV.

The only limitation is that the header has to have a fixed height. It has to be pre-determined (rather than measured on rendering the header) because updating the RLV windowCorrectionConfig once it's rendered has no effect.

It could be worth adding something about this to the docs, given it's a very common use-case, and the only info on it is all buried in this issue.

import React, { forwardRef } from "react";

import { ScrollView } from "react-native";
import { RecyclerListView } from "recyclerlistview";

import type {
    RecyclerListViewWithHeaderProps,
    ScrollViewWithHeaderProps,
} from "~/components/general/RecyclerListViewWithHeader/types";

// this component makes it possible to render a scrollable header above an RLV
// https://github.com/Flipkart/recyclerlistview/issues/233
// note that the header height needs to be fixed so that RLV window can be shifted correctly

const ScrollViewWithHeader = forwardRef<ScrollView, ScrollViewWithHeaderProps>(
    (props, ref) => {
        const { children, renderHeader } = props;

        return (
            <ScrollView ref={ref} {...props}>
                {renderHeader}
                {children}
            </ScrollView>
        );
    }
);

const RecyclerListViewWithHeader = (props: RecyclerListViewWithHeaderProps) => {
    const { headerHeight, renderHeader } = props;

    return (
        <RecyclerListView
            // @ts-expect-error RLV typing of externalScrollView is overly fussy
            externalScrollView={ScrollViewWithHeader}
            scrollViewProps={{ renderHeader }}
            // need to adjust the window for the header otherwise we'll see blank space
            windowCorrectionConfig={{
                value: {
                    windowShift: -headerHeight,
                    startCorrection: 0,
                    endCorrection: 0,
                },
            }}
            {...props}
        />
    );
};

export default React.memo(RecyclerListViewWithHeader);