Flipkart / recyclerlistview

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

Unable to change width when the screen oriantation change #754

Open AlenToma opened 1 year ago

AlenToma commented 1 year ago

Hi I am having trouble to change item width when the screen change.

I can see that width change but somehow it is not applied to the item or maybe the item gets cached somehow.

This is the custom component I created

import {
  RecyclerListView,
  LayoutProvider,
  DataProvider,
} from 'recyclerlistview/dist/reactnative';

import {
  Dimensions,
  StyleSheet,
  ViewStyle,
  StyleProp,
  RefreshControl
} from 'react-native';
import React, { useState, useEffect, useRef, useContext } from 'react';
import { Text, View, TouchableOpacity } from '.'
import { ThemeContext } from './theme/ThemeProvider';
import AppContext from '../context/AppContext';

export type RenderItem = (type: any, item: any, index: any, productWidth: number) => JSX.Element;
export type ItemPress = (item: any, index: number) => void;

export interface IttemListProps {
  scrollToOffset: (x: number, y: number, animate?: boolean) => void;
  forceRerender(): void;
  scrollToItem(data: any, animate?: boolean): void;
  scrollToIndex: (index: number, animate?: boolean) => void;
  scrollToEnd: (animate?: boolean) => void;
  forceRerender: () => void;
}

export type Seperator = { x_name: "Seperator", content: string }

const ItemList = ({
  items,
  renderItem,
  onItemPress,
  onItemLongPress,
  onRefresh,
  seperator,
  onEndReached,
  itemHeight,
  columnPerRaw,
  onIni,
  selectedItem,
  onScroll,
  style,
  isHorizontal,
  contentContainerStyle,
  activeOpacity,
  initialoffsetY,
  nestedScrollEnabled,
  ignoreMargin,
  updators
}: {
  items: any[];
  renderItem: RenderItem;
  onItemPress?: ItemPress;
  onItemLongPress?: ItemPress;
  onRefresh?: () => Promise<void>;
  seperator?: Boolean;
  onEndReached?: Function;
  itemHeight?: number;
  columnPerRaw?: number;
  onIni?: (componenet: IttemListProps) => void;
  selectedItem?: any;
  onScroll?: (nativeEvent: any, offsetX: number, offsetY: number) => any;
  style?: StyleProp<ViewStyle>,
  contentContainerStyle?: StyleProp<ViewStyle>,
  isHorizontal?: boolean,
  activeOpacity?: number;
  initialoffsetY?: number;
  nestedScrollEnabled?: boolean;
  ignoreMargin?: boolean,
  updators?: any[]
}) => {
  const columnPer = () => {
    return (columnPerRaw ?? 1);
  }

  const globalContext = useContext(AppContext);
  const recyclerListViewRef = useRef<IttemListProps>();
  const [loading, setLoading] = useState(false)
  const [containerWidth, setContainerWidth] = useState(0)
  const [loadingSize, setloadingSize] = useState(0)
  const themeContext = useContext(ThemeContext)

  const [dataSource, setDataSource] = useState(
    new DataProvider((r1, r2) => {
      return r1 !== r2;
    }).cloneWithRows(items),
  );
  const ItemWidth = () => {
    return containerWidth || globalContext.windowDimension.width;
  }

  const calProductWidth = () => {
    return (ItemWidth() / columnPer());
  }

  const [initialOffset, setInitialOffset] = useState(initialoffsetY ?? 0);
  const [productWidth, setProductWidth] = useState(calProductWidth())
  const init = useRef(false);
  const userDrag = useRef(false);
  const endReashedTimer = useRef(undefined as any)
  const updateTimer = useRef(undefined as any)
  const layoutProvider = useRef(new LayoutProvider(
    (index) => {
      return 0;
    },
    (type, dim) => {
      dim.width = (ItemWidth() / columnPer());
      dim.height = itemHeight ?? 30;
    },
  ));

  // layoutProvider.current.shouldRefreshWithAnchoring = false;

  // when items update create new provider
  useEffect(() => {
    clearTimeout(endReashedTimer.current)
    if (items && items.length > 0 && init.current) {
      setDataSource(
        new DataProvider((r1, r2) => {
          return r1 !== r2;
        }).cloneWithRows(items),
      );
    }
  }, [items, updators]);

  useEffect(() => {
    init.current = true;
    return () => {
      clearTimeout(endReashedTimer.current)
      clearTimeout(updateTimer.current)
      init.current = false;
    }
  }, [])

  useEffect(() => {
    if (itemHeight && selectedItem) {
      setInitialOffset(items.findIndex((x) => x == selectedItem) * itemHeight);
      recyclerListViewRef.current?.forceRerender();
    }
  }, [selectedItem,]);

  const update =()=> {
    clearTimeout( updateTimer.current)
    updateTimer.current = setTimeout(() => {
      if (recyclerListViewRef.current) {
        recyclerListViewRef.current.forceRerender()
      }
    }, 10);

  }

  if (updators)
    useEffect(() => {
      update();
    }, updators)

  useEffect(() => {
    update();
  }, [productWidth])

  useEffect(() => {
    setProductWidth(calProductWidth());
  }, [columnPerRaw, containerWidth])

  const handleListEnd = () => {
    if (onEndReached) onEndReached();
  };
  const itemGetter = (type: any, item: any, index: any, extendedState: any) => {
    const width = columnPerRaw && columnPerRaw > 1 ? productWidth - 5 : containerWidth;
    return (
      <View key={index} style={[styles.container, { width: width, borderBottomColor: "#242529", borderBottomWidth: seperator === true ? 1 : 0 }]}>
        <TouchableOpacity
          incBackGround={false}  activeOpacity={activeOpacity ?? 0.5} onLongPress={() => onItemLongPress ? onItemLongPress(item, index) : null} onPress={onItemPress ? () => onItemPress(item, index) : undefined}>
          {renderItem(type, item, index, width)}
        </TouchableOpacity>
      </View>
    );
  };

  const scrollProps = {
    nestedScrollEnabled: nestedScrollEnabled,
    onScrollBeginDrag: () => {
      userDrag.current = true
    }
  } as any;

  if (onRefresh)
    scrollProps.refreshControl = (
      <RefreshControl
        refreshing={loading}
        onRefresh={async () => {
          setLoading(true);
          await onRefresh();
          setLoading(false);
        }}
      />
    )

  const getViewWidth = () => {
    if (columnPer() <= 1)
      return ItemWidth()
    return ((productWidth) * Math.floor(columnPer()));
  }
  return (
    <View style={[styles.listContainer, style, { alignItems: "center", width: "100%" }]} onLayout={(e) => {
      setContainerWidth(e.nativeEvent.layout.width);
    }}>
      {
        containerWidth > 0 && productWidth > 0 ? (
          <RecyclerListView
            suppressBoundedSizeException={true}
            disableRecycling={false}
            contentContainerStyle={[contentContainerStyle, styles.containers, { width: getViewWidth() }]}
            ref={(c: any) => {
              recyclerListViewRef.current = c;
              if (onIni) onIni(c);
            }}
            scrollViewProps={scrollProps}
            isHorizontal={isHorizontal}
            onScroll={(event: any, x: any, y: any) => {
              if (!initialOffset)
                setInitialOffset(y + (itemHeight ?? 100));
              if (onScroll)
                return onScroll(event, x, y);
            }}
            initialOffset={initialOffset}
            rowRenderer={itemGetter}
            dataProvider={dataSource}
            layoutProvider={layoutProvider.current}
            useWindowScroll={true}
            onEndReached={() => {
              clearTimeout(endReashedTimer.current)
              endReashedTimer.current = setTimeout(async () => {
                clearTimeout(endReashedTimer.current)
                if (userDrag.current)
                  await handleListEnd();
                clearTimeout(endReashedTimer.current)
              }, 500);

            }}
            onEndReachedThreshold={0.2}
            canChangeSize={true}
            renderFooter={() => {
              return <View style={{ height: itemHeight || 100 }}></View>
            }}
          />
        ) : null
      }
    </View>
  );
};

export default ItemList;

const styles = StyleSheet.create({
  container: {
    justifyContent: 'space-around',
    flex: 1,
    margin: 2,
    overflow: "hidden",
  },

  containers: {
    justifyContent: "space-between",
    flexGrow: 1,
    padding: 0,
    margin: 0
  },

  listContainer: {
    flex: 1,
    minHeight: 10,
    minWidth: 10,
  },
});

and here is how I call it

      {state.data && state.data.length > 0 ? (
        <ItemList
          isHorizontal={false}
          itemHeight={75}
          seperator={true}
          onItemPress={(item, index) => itemClick(item)}
          items={state.data ?? []}
          renderItem={(type: any, item: any, index: any, width: any) => renderItem(type, item, index, width)}
          onEndReached={() => {
            console.log("enReached")
            if (state.currentParser.searchPagination && !state.endResult)
              state.effectTrigger++;
          }}
        />
      ) : null}
AlenToma commented 1 year ago

Ok was able to fix this by creating new LayoutProvider each time the size change.

as calProductWidth somehow return the old value instead of the updated one.

this is a bug in my opinion as each time i create new LayoutProvider the scroll gets misset upp a little which I could live with

  useEffect(() => {
    layoutProvider.current = new LayoutProvider(
      (index) => {
        return 0;
      },
      (type, dim, index) => {
        dim.width = calProductWidth()
        dim.height = itemHeight ?? 30;
      },
    );
  }, [productWidth, containerWidth])
OmarJay1 commented 1 year ago

I had a similar problem and I fixed it by adding a variable height style to the RowRenderer. I update the height with a global variable, which isn't very nice, but I have only 1 copy of the list in my app.

      Dimensions.addEventListener('change', () => {

        this.setState({dataProvider: this.state.dataProvider.cloneWithRows(this.props.data)});        

       g_width = Dimensions.get('screen').width;
}