React Native: ScrollView not scrolling

250 Views Asked by At

I want to create a scrollable selection of images using react native. After grabbing all the needed info regarding some images I'm processing, I stack the images next to each other horizontally under a ScrollView. The issue is that it's not scrolling. Any help would be very appreciated!

Notes: Styles.borderTop only rounds the top left and right borders radius.

    const ImageHeader = ({ place }) => {
    if (place.image_url) {
        const { Layout } = useTheme();

        const [height, setHeight] = useState(0);
        const [width, setWidth] = useState(0);
        const [counter, setCounter] = useState(0);
        const [oHeight, setOHeight] = useState(0);
        const [scWidth, setScWidth] = useState(0);
        const [images, setImages] = useState([]);

        useEffect(()=>{
            setScWidth(Dimensions.get('window').width - 2 * 8);
            image_pack = place.photos || [place.image_url];
            setImages(image_pack);
            image_pack.forEach((img)=>{
                Image.getSize(img, (iwidth, iheight) => {
                    setHeight(height + iheight);
                    setWidth(width + iwidth);
                    setCounter(counter + 1);
                });
            })
        },[])

        useEffect(()=> {
            setOHeight(scWidth * (height/counter) / (width/counter));
            console.log(`Height: ${height}, Width: ${width}, Coutner: ${counter}, OHeight: ${oHeight}, ScWidth: ${scWidth}`)
        }, [counter])

        return (
            <View style={[{
                height: oHeight || 0, 
                maxHeight: 450,   
                flex: 1
            }]}>
                <ScrollView horizontal showsHorizontalScrollIndicator={true} contentContainerStyle={{width}}>
                    {
                        images.map((uri, idx)=> (
                            <View key={idx} style={[styles.borderTop, {
                                width: scWidth,
                                height: '100%',
                                overflow: 'hidden'
                            }]}>
                                <Image resizeMode={'cover'} key={idx} style={[Layout.fill]} source={{uri}} />
                            </View>
                        ))
                    }
                </ScrollView>
            </View>
        )
    }
}
1

There are 1 best solutions below

0
Alen.Toma On

By adding contentContainerStyle width you are restricting the width of the whole list. You dont need that, try adding it to minWidth

And also add flexDirection: "row" to the contentContainerStyle so it can stack the items

By reference, I have created a component that will do the work for you, as you can take a look or simple copy and use.

as you can see below I am using recyclerlistview OR ScrollView.

Simple remove recyclerlistview if you dont want to use or simple install it

import {
  Dimensions,
  StyleSheet,
  View,
  Text,
  LayoutAnimation,
  TouchableHighlight,
  TouchableOpacity,
  ScrollView,
  NativeScrollEvent,
  NativeSyntheticEvent,
} from 'react-native';
import {
  RecyclerListView,
  LayoutProvider,
  DataProvider,
  BaseItemAnimator,
} from 'recyclerlistview/dist/reactnative';
import React, { useState, useEffect, useRef, useContext } from 'react';

export type RenderItem = (item: any, index: any) => JSX.Element;
export type ItemPress = (item: any, index: number) => void;
const HorizontalItemList = ({
  items,
  renderItem,
  onItemPress,
  style,
  onEndReached,
  itemWidth,
  itemHeight,
  scrollType,
  snapToInterval,
  decelerationRate,
  snapToAlignment,
  selectedItem,
  scrollEventThrottle
}: {
  items: any[];
  renderItem: RenderItem;
  onItemPress: ItemPress;
  selectedItem?: any;
  style?: { top?: any; left?: any };
  onEndReached?: () => void;
  itemWidth?: number,
  itemHeight?: number,
  scrollType?: "RecyclerListView" | "ScrollView",
  snapToInterval?: number,
  decelerationRate?: "fast" | "normal" | number,
  snapToAlignment?: "start" | "center" | "end",
  scrollEventThrottle?: number
}) => {
  const [dataSource, setDataSource] = useState(items);
  const [initialOffset, setInitialOffset] = useState(0);
  const [posX, setPosX] = useState(0);
  const [positions] = useState([] as { item: any, width: number, height: number }[])
  const [refs] = useState(useRef<View[]>([]))
  const timeout = useRef(undefined as any)
  const [dataProvider, setDataProvider] = useState(
    new DataProvider((r1, r2) => {
      return r1 !== r2;
    }).cloneWithRows(items),
  );
  const scrollprops = { scrollEventThrottle: scrollEventThrottle, showsHorizontalScrollIndicator: false, pagingEnabled: true, horizontal: true } as any;
  if (snapToInterval)
    scrollprops.snapToInterval = snapToInterval;

  if (decelerationRate)
    scrollprops.decelerationRate = decelerationRate;

  if (snapToAlignment)
    scrollprops.snapToAlignment = snapToAlignment;
  // when items update create new provider

  const getItem = (item: any, index: any) => {
    return (
      <View style={styles.container} key={index} onLayout={() => {
        if (positions.length != dataSource.length && refs.current[index])
          refs.current[index]?.measure((ox, oy, width, height, px, py) => {
            positions.push({ item: items[index], height: height, width: width })
            if (selectedItem && posX <= 0 && index == items.length - 1) {
              validatePos();
            }
          });
      }} ref={(c => {
        if (c)
          refs.current[index] = c;
      })}>
        <TouchableOpacity onPress={() => onItemPress(item, index)}>
          <View>{renderItem(item, index)}</View>
        </TouchableOpacity>
      </View>
    );
  };

  const validatePos = () => {
    if (positions.length === items.length && selectedItem) {
      var p = positions.filter((x, i) => i < positions.findIndex(f => f.item == selectedItem)).reduce((c, v) => { return c + v.width }, 0);
      setPosX(p);
    }
  }

  useEffect(()=> {
    return ()=> clearTimeout(timeout.current);
  }, [])

  useEffect(() => {
    validatePos();
  }, [selectedItem])

  useEffect(() => {
    setDataProvider(new DataProvider((r1, r2) => {
      return r1 !== r2;
    }).cloneWithRows(items));
    setDataSource(items);
  }, [items])

  const isCloseToBottom = ({ layoutMeasurement, contentOffset, contentSize }: any) => {
    const paddingLeft = 20;
    return layoutMeasurement.width + contentOffset.x >= contentSize.width - paddingLeft;
  };

  const onscroll = (event: any) => {
    clearTimeout(timeout.current);
    timeout.current = setTimeout(() => {

      if (isCloseToBottom(event)) {
        console.log("onEndReached")
        if (onEndReached)
          onEndReached();
      }
    }, 500);

  }

  //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) => {

      dim.width = (itemWidth ?? 100) + 5;
      dim.height = itemHeight ?? 90;
    },
  );
  if (scrollType === "RecyclerListView") {
    if (items.length <= 0)
      return null;
    return (
      <View style={[styles.listContainer, style, { flex: 1, minHeight: itemHeight ?? 100, minWidth: itemWidth ?? 100 }]}>
        <RecyclerListView

          isHorizontal={true}
          initialOffset={initialOffset}
          rowRenderer={(type, item, index) => getItem(item, index)}
          dataProvider={dataProvider}
          useWindowScroll={true}
          canChangeSize={true}
          layoutProvider={layoutProvider}
          onEndReached={onEndReached}
          renderFooter={() => {
            return <View style={{ height: itemHeight ?? 100, paddingLeft: itemWidth ?? 100 }}></View>
          }}
        />
      </View>
    )
  }
  if (!scrollType || scrollType === "ScrollView")
    return (
      <>
        <View style={[styles.listContainer, style]}>
          <ScrollView
            onScroll={({ nativeEvent }) => onscroll(nativeEvent)}
            horizontal={true}
            contentOffset={{ y: 0, x: posX }}
            showsHorizontalScrollIndicator={false}
            scrollEventThrottle={16}>
            <View style={[{ flexDirection: "row" }, itemHeight ? { height: itemHeight } : undefined]}>
              {
                dataSource.map((x: any, i: number) => {
                  return getItem(x, i)
                }
                )
              }
            </View>
          </ScrollView>
        </View>
      </>
    );

  return null;
};

export default HorizontalItemList;

const styles = StyleSheet.create({
  container: {
    marginTop: 5,
    flex: 1
  },

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