react native recyclerlistview indicator and performance

1.5k Views Asked by At

I use RecyclerlistView instead of FlatList. Its very nice but I got 2 Issues/Questions.

The first is:

How can I disable the indicator ?

My second Issue. I get fps laag on few Items if I scroll. Because I use in the recyclerlistview 2x 5 items to map and 1x FlatList to render 3-4 Images. How can I improve the performance ?

In the productData there is a list from 20 but I deleted because then the code is too long here

Code:

import React, { useState, useRef } from 'react';
import { StyleSheet, Text, View, TouchableOpacity, Animated, Dimensions, Image, FlatList, } from 'react-native';
import pure from 'recompose/pure';
import Modal from 'react-native-modal';
import DoubleClick from 'react-native-double-tap';
import Indicator from './Indicator';
import { AntDesign, Feather, Ionicons } from '@expo/vector-icons';
import { RecyclerListView, DataProvider, LayoutProvider } from 'recyclerlistview';

const width = Dimensions.get('screen').width;

const stars = [
  {
    "key":"1",
    "star": 1,
  },
  {
    "key":"2",
    "star": 2,
  },
  {
    "key":"3",
    "star": 3,
  },
  {
    "key":"4",
    "star": 4,
  },
  {
    "key":"5",
    "star": 5,
  },
];

const productData = [
  {
    type: 'NORMAL',
    item: {
      id: 1,
      name:"Calvin Klein Bag",
      price:"29.99€",
      rating:"5",
      product_image: [
        {
          key: "1",
          image:require('../../assets/images/products/bag1.png')
        },
        {
          key: "2",
          image: require('../../assets/images/products/bag2.png')
        }
      ],
      username:"Anna_maier05",
      user_profil_image:require('../../assets/images/products/12.jpg'),
      trousers_size: null,
      shoe_size: null,
      above_size: null,
      colors: ["black", "white", "#2b80ff", "red", "yellow", "pink", "gold"]
    }
  },
];

const ProductCard = () => {
  const scrollX = useRef(new Animated.Value(0)).current;

  const [testModal, setTestModal] = useState(false);

  const [modal, setModal] = useState(false);
  const [modalID, setModalID] = useState(null);

  /* RecyclerList options */
  const [dataList, setDataList] = useState(new DataProvider((r1, r2) => r1 !== r2).cloneWithRows(productData));

  const [dataLayoutProvider, setDataLayoutProvider] = useState(
    new LayoutProvider((i) => {
      return dataList.getDataForIndex(i).type;
    }, (type, dim) => {
      switch(type) {
        case 'NORMAL':
          dim.width = width,
          dim.height = 650;
          break;
        default:
          dim.width = 0;
          dim.height = 0;
          break;
      }
    })
  )

  const rowRenderer = (type, data) => {
    const { name, price, product_image, username, user_profil_image, colors } = data.item;
    return (
      <View style={styles.container}>
        <View style={styles.header}>
          <TouchableOpacity style={styles.headerLeft}>
            <Image resizeMode="contain" style={styles.profilImage} source={user_profil_image} />
            <View>
              <Text style={styles.username}>{username}</Text>
              <Text style={styles.usernameSubtitle}>Instagram</Text>
            </View>
          </TouchableOpacity>
          <View style={styles.headerRight}>
            <TouchableOpacity style={{marginRight: 10}}>
              <Ionicons name="heart-circle-outline" size={32} color="#444" />
            </TouchableOpacity>
            <TouchableOpacity onPress={() => setTestModal(true)}>
              <Ionicons name="ios-ellipsis-vertical" size={24} color="#444" />
            </TouchableOpacity>
          </View>
        </View>
        <View>
        { /* Product Image */ }
          <Animated.FlatList 
            data={product_image}
            horizontal
            pagingEnabled
            showsHorizontalScrollIndicator={false}
            scrollEventThrottle={20}
            onScroll={Animated.event(
              [{ nativeEvent: {contentOffset: { x: scrollX}}}],
              {
                useNativeDriver: false
              }
            )}
            keyExtractor={item => item.key}
            renderItem={({ item }) => {
              return (
                <DoubleClick
                singleTap={() => {
                  console.log("single tap");
                }}
                doubleTap={() => {
                  console.log("double tap");
                }}
                delay={200}
                >
                <Image source={item.image} resizeMode="contain" style={{height: 200, width: width * 0.9,}} />
              </DoubleClick>
              )
            }}
          />
          <Indicator scrollX={scrollX} data={product_image} backgroundColor="#444" absolute={false} />
          </View>
        <View style={styles.productDetailContainer}>
          <View style={styles.productTitleContainer}>
            <Text style={styles.productName}>{name}</Text>
            <View style={styles.ratingContainer}>
            {
              stars.map((_, i) => {
                return (
                  <TouchableOpacity key={`stars-${i}`}>
                    <AntDesign name="staro" size={20} color="#C71FF7" />
                  </TouchableOpacity>
                )
              })
            }
            </View>
          </View>
          <View style={{marginVertical: 30}}>
          </View>
          <Text style={{fontFamily: 'montserrat-light', color: '#555', marginBottom: 6, fontSize: 13}}>In Farben erhältlich:</Text>
          <View style={styles.sizeAndColorContainer}>
            <View style={styles.colorContainer}>
              {
                colors.length > 0 ? 
                colors.map((hex, i) => {
                  return (
                    <TouchableOpacity key={`color-${i}`} style={[styles.colors, { backgroundColor: hex }]}>
                      <Text></Text>
                    </TouchableOpacity>
                  )
                })
                : null
              }
            </View>
            <TouchableOpacity style={styles.sizeButton}>
              <Text style={styles.sizeButtonText}>Größe auswählen</Text>
            </TouchableOpacity>
          </View>
          <View style={styles.buttonContainer}>
            <View style={styles.bottomLeftButtons}>
              <TouchableOpacity>
                <Feather name="info" size={24} color="#333" />
              </TouchableOpacity>
              <TouchableOpacity style={styles.heartLeftMargin}>
                <AntDesign name="hearto" size={24} color="#333" />
              </TouchableOpacity>
            </View>
            <TouchableOpacity style={styles.buttonColored}>
              <Text style={styles.buttonColoredText}>In den Warenkorb</Text>
            </TouchableOpacity>
          </View>
        </View>
      </View>
    )
  };


  return (
    <View style={{flex: 1, marginLeft: width * 0.05, marginRight: width * 0.05}}>
      <RecyclerListView
        style={{flex: 1,  paddingTop: 20}}
        rowRenderer={rowRenderer}
        dataProvider={dataList}
        layoutProvider={dataLayoutProvider}
      />
    </View>
  )
};
1

There are 1 best solutions below

3
On

RecyclerListView Is a little complicated, You have to set the height of the item so that it won't overlap. I really can't see why the list lags from your code without testing it out and debugging the code.

But I will do you one better, I have built and tested a controller around RecyclerListView and it's fast and work without any problems.

To see the controller on action, see here.

Here I will share it with you:

Controller

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

import {
  Dimensions,
  StyleSheet,
  View,
  Text,
  LayoutAnimation,
  TouchableHighlight,
  TouchableOpacity,
  ViewStyle,
  StyleProp,
  RefreshControl
} from 'react-native';
import React, { useState, useEffect, useRef } from 'react';

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

const ItemList = ({
  items,
  renderItem,
  onItemPress,
  onItemLongPress,
  onRefresh,
  seperator,
  onEndReached,
  itemHeight,
  columnPerRaw,
  onIni,
  selectedItem,
  onScroll,
  style,
  isHorizontal,
  contentContainerStyle,
  activeOpacity,
  initialoffsetY
}: {
  items: any[];
  renderItem: RenderItem;
  onItemPress: ItemPress;
  onItemLongPress?: ItemPress;
  onRefresh?: () => Promise<void>;
  seperator?: Boolean;
  onEndReached?: Function;
  itemHeight?: number;
  columnPerRaw?: number;
  onIni?: Function;
  selectedItem?: any;
  onScroll?: (nativeEvent: any, offsetX: number, offsetY: number) => any;
  style?: StyleProp<ViewStyle>,
  contentContainerStyle?: StyleProp<ViewStyle>,
  isHorizontal?: boolean,
  activeOpacity?: number;
  initialoffsetY?: number;
}) => {
  const [recyclerListViewRef] = useState(useRef());
  const [loading, setLoading] = useState(false)
  const [dataSource, setDataSource] = useState(
    new DataProvider((r1, r2) => {
      return r1 !== r2;
    }).cloneWithRows(items),
  );
  const [initialOffset, setInitialOffset] = useState(initialoffsetY ?? 0);
  const [productWidth, setProductWidth] = useState(Dimensions.get('window').width / Math.floor(columnPerRaw ?? 1))
  // when items update create new provider
  useEffect(() => {
    if (items && items.length > 0)
      setDataSource(
        new DataProvider((r1, r2) => {
          return r1 !== r2;
        }).cloneWithRows(items),
      );
  }, [items]);

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

  //The layout provider must be provided with two methods. The first method is the get layout based on index which determines the layout type
  //based on the index. In the second method, the layout's estimated size i.e its height and width is specified on basis of the layout type.
  const layoutProvider = new LayoutProvider(
    (index) => {
      return 0;
    },
    (type, dim) => {
      const { width, height } = Dimensions.get('window');

      dim.width = width / (columnPerRaw ?? 1);
      dim.height = itemHeight ?? 30;
    },
  );

  useEffect(() => {
    var w = Dimensions.get('window').width / ((columnPerRaw ?? 1));
    setProductWidth(w);
  }, [columnPerRaw])

  const getMargin = () => {
    if (!columnPerRaw || columnPerRaw <= 1)
      return 0;
    return Math.abs(Dimensions.get('window').width - (productWidth * Math.floor(columnPerRaw ?? 1))) / 2;
  }

  const handleListEnd = () => {
    if (onEndReached) onEndReached();
  };
  const itemGetter = (type: any, item: any, index: any, extendedState: any) => {
    return (
      <View style={[styles.container, { width: columnPerRaw && columnPerRaw > 1 ? productWidth : undefined, borderBottomColor: "#dddddd", borderBottomWidth: seperator === true ? 1 : 0 }]} key={index}>
        <TouchableOpacity activeOpacity={activeOpacity ?? 0.5} onLongPress={() => onItemLongPress ? onItemLongPress(item, index) : null} onPress={() => onItemPress(item, index)}>
          <>
            {renderItem(type, item, index)}
          </>
        </TouchableOpacity>
      </View>
    );
  };
  return (
    <>
      <View style={[styles.listContainer, style]}>
        <RecyclerListView
          contentContainerStyle={[contentContainerStyle, { marginLeft: getMargin() }]}
          ref={(c) => {
            recyclerListViewRef.current = c;
            if (onIni) onIni(c);
          }}
          scrollViewProps={onRefresh ? {
            refreshControl: (
              <RefreshControl
                refreshing={loading}
                onRefresh={async () => {
                  setLoading(true);
                  await onRefresh();
                  setLoading(false);
                }}
              />
            )
          } : undefined}
          isHorizontal={isHorizontal}
          onScroll={onScroll}
          initialOffset={initialOffset}
          rowRenderer={itemGetter}
          dataProvider={dataSource}
          layoutProvider={layoutProvider}
          useWindowScroll={true}
          onEndReached={handleListEnd}
          canChangeSize={true}
          renderFooter={() => {
            return <View style={{ height: itemHeight ?? 100 }}></View>
          }}
        />
      </View>
    </>
  );
};

export default ItemList;

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

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

How to use it

  <ItemList
            contentContainerStyle={{ zIndex: 100, paddingTop: contentPaddingTop }}
            onScroll={Animated.event(
              [
                {
                  nativeEvent: {
                    contentOffset: {
                      y: fadeAnim,
                    },
                  },
                },
              ],
              { useNativeDriver: false },
            )}
            onRefresh={async () => {
              await clear();
            }}
            columnPerRaw={columnPerRar}
            itemHeight={150}
            onIni={(s: any) => {
              setScroll(s);
            }}
            onItemPress={(item, index) => itemClick(item as item)}
            items={data ?? []}
            renderItem={renderItem}
            onEndReached={async () => {
              if (globalContext.value.panination && !isLoading)
                setEffectTrigger(Math.random());
            }}
          />