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

Horizontal flatlist? #5

Open
sfalihi opened this issue Mar 12, 2021 · 17 comments
Open

Horizontal flatlist? #5

sfalihi opened this issue Mar 12, 2021 · 17 comments
Labels
enhancement New feature or request

Comments

@sfalihi
Copy link

sfalihi commented Mar 12, 2021

it throw error with horizontal=true
does it support horizontal flatlist?

@vishalnarkhede
Copy link
Collaborator

vishalnarkhede commented Mar 12, 2021

@sfalihi No!! Not at the moment

@vishalnarkhede vishalnarkhede added the enhancement New feature or request label Mar 17, 2021
@vishalnarkhede
Copy link
Collaborator

Going to keep this open to see if there is interest from more users for this!!

@brobin002
Copy link

brobin002 commented Apr 27, 2021

Yep I am very interrested in this feature...

@day-me-an
Copy link

Also interested.

@Findiglay
Copy link

I'm also interested in this feature for a horizontal time-based calendar-like component that allows users to scroll back and forth infinately.

@the-kenneth
Copy link

Definitely interested as well

@vishalnarkhede vishalnarkhede pinned this issue May 17, 2021
@bonnmh
Copy link

bonnmh commented May 25, 2021

Has this issue been resolved? :(

@raajnadar-cb
Copy link

Yes I need this feature ✌🏽

@hampani
Copy link

hampani commented Sep 3, 2021

Interested!

@faizansahib
Copy link

just write horizontal in flatlist it will work

@jacobmolby
Copy link

just write horizontal in flatlist it will work

No it will not.
onEndReached and onStartReached are not firing then.

@DJWassink
Copy link

Also interested 👍

@AndrzejRokosz
Copy link

yes it would be nice to have it 👍

@Nautman
Copy link

Nautman commented Jun 22, 2022

I'm also interested in this feature for a horizontal time-based calendar-like component that allows users to scroll back and forth infinately.
@Findiglay

I am also facing the same problem. Did you by any chance find out how to have a bidirectional scroll in a calendar-like component?

@pdpino
Copy link

pdpino commented Jun 22, 2022

@Nautman @Findiglay or anyone interested in a calendar-like component with horizontal bidirectional infinite scroll*,

you can check this library https://github.com/hoangnm/react-native-week-view for a week view (disclaimer: I'm a maintainer), plus we are currently working on a month view: hoangnm/react-native-week-view#95 (comment) 😃

(*) for now is implemented with a FlatList plus some workarounds

@hadnet
Copy link

hadnet commented Apr 22, 2023

@vishalnarkhede please, man, add the horizontal feature!

@krmao
Copy link

krmao commented Oct 16, 2023

here is it !

/* eslint-disable @typescript-eslint/no-unused-vars */
// noinspection JSUnusedLocalSymbols

/**
 * https://github.com/GetStream/react-native-bidirectional-infinite-scroll
 */
// noinspection JSUnusedGlobalSymbols

import React, { MutableRefObject, useRef, useState } from 'react';
import {
  ActivityIndicator,
  FlatList as FlatListType,
  FlatListProps,
  ScrollViewProps,
  StyleSheet,
  View,
} from 'react-native';
import { FlatList } from '@stream-io/flat-list-mvcp';

const styles = StyleSheet.create({
  indicatorContainer: {
    paddingVertical: 5,
    width: '100%',
  },
});

export type Props<T> = Omit<FlatListProps<T>, 'maintainVisibleContentPosition'> & {
  ref?: ((instance: FlatListType<T> | null) => void) | MutableRefObject<FlatListType<T> | null> | null;
  /**
   * Called once when the scroll position gets close to end of list. This must return a promise.
   * You can `onEndReachedThreshold` as distance from end of list, when this function should be called.
   */
  onEndReached: () => Promise<void>;
  /**
   * Called once when the scroll position gets close to begining of list. This must return a promise.
   * You can `onStartReachedThreshold` as distance from beginning of list, when this function should be called.
   */
  onStartReached: () => Promise<void>;
  /** Color for inline loading indicator */
  activityIndicatorColor?: string;
  /**
   * Enable autoScrollToTop.
   * In chat type applications, you want to auto scroll to bottom, when new message comes it.
   */
  enableAutoscrollToTop?: boolean;
  /**
   * If `enableAutoscrollToTop` is true, the scroll threshold below which auto scrolling should occur.
   */
  autoscrollToTopThreshold?: number;
  /** Scroll distance from beginning of list, when onStartReached should be called. */
  onStartReachedThreshold?: number;
  /**
   * Scroll distance from end of list, when onStartReached should be called.
   * Please note that this is different from onEndReachedThreshold of FlatList from react-native.
   */
  onEndReachedThreshold?: number;
  /** If true, inline loading indicators will be shown. Default - true */
  showDefaultLoadingIndicators?: boolean;
  /** Custom UI component for header inline loading indicator */
  HeaderLoadingIndicator?: React.ComponentType;
  /** Custom UI component for footer inline loading indicator */
  FooterLoadingIndicator?: React.ComponentType;
  /** Custom UI component for header indicator of FlatList. Only used when `showDefaultLoadingIndicators` is false */
  ListHeaderComponent?: React.ComponentType;
  /** Custom UI component for footer indicator of FlatList. Only used when `showDefaultLoadingIndicators` is false */
  ListFooterComponent?: React.ComponentType;
};
/**
 * Note:
 * - `onEndReached` and `onStartReached` must return a promise.
 * - `onEndReached` and `onStartReached` only get called once, per content length.
 * - maintainVisibleContentPosition is fixed, and can't be modified through props.
 * - doesn't accept `ListFooterComponent` via prop, since it is occupied by `FooterLoadingIndicator`.
 *    Set `showDefaultLoadingIndicators` to use `ListFooterComponent`.
 * - doesn't accept `ListHeaderComponent` via prop, since it is occupied by `HeaderLoadingIndicator`
 *    Set `showDefaultLoadingIndicators` to use `ListHeaderComponent`.
 */
export const BidirectionalFlatList = React.forwardRef(
  <T extends any>(
    props: Props<T>,
    ref: ((instance: FlatListType<T> | null) => void) | MutableRefObject<FlatListType<T> | null> | null,
  ) => {
    const {
      activityIndicatorColor = 'black',
      autoscrollToTopThreshold = 100,
      data,
      enableAutoscrollToTop,
      FooterLoadingIndicator,
      HeaderLoadingIndicator,
      ListHeaderComponent,
      ListFooterComponent,
      onEndReached = () => Promise.resolve(),
      onEndReachedThreshold = 10,
      onScroll,
      onStartReached = () => Promise.resolve(),
      onStartReachedThreshold = 10,
      showDefaultLoadingIndicators = true,
      horizontal = false,
    } = props;
    const [onStartReachedInProgress, setOnStartReachedInProgress] = useState(false);
    const [onEndReachedInProgress, setOnEndReachedInProgress] = useState(false);

    const onStartReachedTracker = useRef<Record<number, boolean>>({});
    const onEndReachedTracker = useRef<Record<number, boolean>>({});

    const onStartReachedInPromise = useRef<Promise<void> | null>(null);
    const onEndReachedInPromise = useRef<Promise<void> | null>(null);

    const maybeCallOnStartReached = () => {
      // If onStartReached has already been called for given data length, then ignore.
      if (data?.length && onStartReachedTracker.current[data.length]) {
        return;
      }

      if (data?.length) {
        onStartReachedTracker.current[data.length] = true;
      }

      setOnStartReachedInProgress(true);
      const p = () => {
        return new Promise<void>(resolve => {
          onStartReachedInPromise.current = null;
          setOnStartReachedInProgress(false);
          resolve();
        });
      };

      if (onEndReachedInPromise.current) {
        onEndReachedInPromise.current.finally(() => {
          onStartReachedInPromise.current = onStartReached().then(p);
        });
      } else {
        onStartReachedInPromise.current = onStartReached().then(p);
      }
    };

    const maybeCallOnEndReached = () => {
      // If onEndReached has already been called for given data length, then ignore.
      // console.log(
      //   '-- maybeCallOnEndReached',
      //   data?.length,
      //   data?.length ? onEndReachedTracker.current[data.length] : false,
      // );

      if (data?.length && onEndReachedTracker.current[data.length]) {
        return;
      }

      if (data?.length) {
        onEndReachedTracker.current[data.length] = true;
      }

      setOnEndReachedInProgress(true);
      const p = () => {
        return new Promise<void>(resolve => {
          onStartReachedInPromise.current = null;
          setOnEndReachedInProgress(false);
          resolve();
        });
      };

      if (onStartReachedInPromise.current) {
        onStartReachedInPromise.current.finally(() => {
          onEndReachedInPromise.current = onEndReached().then(p);
        });
      } else {
        onEndReachedInPromise.current = onEndReached().then(p);
      }
    };

    const handleScroll: ScrollViewProps['onScroll'] = event => {
      // Call the parent onScroll handler, if provided.
      onScroll?.(event);

      const offset = event.nativeEvent?.contentOffset?.x;
      const visibleLength = horizontal
        ? event.nativeEvent.layoutMeasurement.width
        : event.nativeEvent.layoutMeasurement.height;
      const contentLength = horizontal ? event.nativeEvent.contentSize.width : event.nativeEvent.contentSize.height;
      // Check if scroll has reached either start of end of list.
      const isScrollAtStart = offset < onStartReachedThreshold;
      const isScrollAtEnd = contentLength - visibleLength - offset < onEndReachedThreshold;
      // console.log('-- isScrollAtStart=', isScrollAtStart, 'isScrollAtEnd=', isScrollAtEnd, 'horizontal=', horizontal);

      if (isScrollAtStart) {
        maybeCallOnStartReached();
      }

      if (isScrollAtEnd) {
        maybeCallOnEndReached();
      }
    };

    const renderHeaderLoadingIndicator = () => {
      if (!showDefaultLoadingIndicators) {
        if (ListHeaderComponent) {
          return <ListHeaderComponent />;
        } else {
          return null;
        }
      }

      if (!onStartReachedInProgress) {
        return null;
      }

      if (HeaderLoadingIndicator) {
        return <HeaderLoadingIndicator />;
      }

      return (
        <View style={styles.indicatorContainer}>
          <ActivityIndicator size={'small'} color={activityIndicatorColor} />
        </View>
      );
    };

    const renderFooterLoadingIndicator = () => {
      if (!showDefaultLoadingIndicators) {
        if (ListFooterComponent) {
          return <ListFooterComponent />;
        } else {
          return null;
        }
      }

      if (!onEndReachedInProgress) {
        return null;
      }

      if (FooterLoadingIndicator) {
        return <FooterLoadingIndicator />;
      }

      return (
        <View style={styles.indicatorContainer}>
          <ActivityIndicator size={'small'} color={activityIndicatorColor} />
        </View>
      );
    };

    return (
      <>
        <FlatList<T>
          {...props}
          ref={ref}
          progressViewOffset={50}
          ListHeaderComponent={renderHeaderLoadingIndicator}
          ListFooterComponent={renderFooterLoadingIndicator}
          onEndReached={null}
          onScroll={handleScroll}
          /*
            // v0.73.0-rc.2
            // https://github.com/facebook/react-native/commit/1a1a79871b2d040764537433b431bc3b416904e3
            maintainVisibleContentPosition={{
            autoscrollToTopThreshold: enableAutoscrollToTop ? autoscrollToTopThreshold : undefined,
            minIndexForVisible: 1,
          }}*/
        />
      </>
    );
  },
) as unknown as BidirectionalFlatListType;

type BidirectionalFlatListType = <T extends any>(props: Props<T>) => React.ReactElement;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests