Spliid
Spliid

Reputation: 531

How do I make a list (FlatList) automatically scroll through the elements using Animated?

I have a horizontal FlatList, where each time it reaches the end, it automatically adds new elements to the list, so it kind of is an infinite list. I want the app to scroll through the list by itself automatically, while the user must still be able to scroll back and forth. This is what I have to far

export default class ImageCarousel extends Component {
  constructor(props) {
    super(props);

    this.scrollX = 0;
    this.offset = new Animated.Value(0);
    this.scrollTo = this.scrollTo.bind(this);
    this.handleScroll = this.handleScroll.bind(this);
    this.stopAnimation = this.stopAnimation.bind(this);
    // Listener to call the scrollToOffset function
    this.offset.addListener(this.scrollTo);
  }

  _scroller() {
    toValue = this.scrollX + 10; // Scroll 10 pixels in each loop
    this.animation = Animated.timing(
      this.offset,
      {
        toValue: toValue,
        duration: 1000, // A loop takes a second
        easing: Easing.linear,
      }
    );
    this.animation.start(() => this._scroller()); //Repeats itself when done
  }

  scrollTo(e) {
    this.carousel.scrollToOffset({offset: e.value});
  }

  handleScroll(event) {
    // Save the x (horizontal) value each time a scroll occurs
    this.scrollX = event.nativeEvent.contentOffset.x;
  }

  componentDidMount() {
    this._scroller();
  }
  render() {
    return (
      <View>
        <FlatList
          ref={el => this.carousel = el}
          data={someData}
          renderItem={renderFunction}
          horizontal={true}
          keyExtractor={someKeyFunction}
          onEndReached={loadMoreElementsFunction}
          onScroll={this.handleScroll}
        />
      </View>
    );
  }
}

It works in the sense that it is automatically scrolling through the list, the problem however, is I cannot manually scroll through the list, since the scroll position is constantly updated by the scrollTo listener. I have tried to add an onPress callback to disable the animation when the FlatList is pressed, I have however not been able to get it to work.

Upvotes: 5

Views: 35331

Answers (4)

Badal Saibo
Badal Saibo

Reputation: 3675

  1. Store currentIndex in a state
const [currentIndex, setCurrentIndex] = useState(0);
  1. Update this state using setInterval
  useEffect(() => {
    const intervalId = setInterval(() => {
      setCurrentIndex((prevIndex) => (prevIndex + 1) % DATA.length);
    }, 3000);

    return () => clearInterval(intervalId);
  }, []);
  1. Track this index and fire scrollToIndex method accordingly.
useEffect(() => {
    if (flatListRef.current) {
      flatListRef.current.scrollToIndex({
        index: currentIndex,
        animated: true,
      });
    }
  }, [currentIndex]);
  1. Extract into a hook for reusability
const useAutoScroll = ({ itemLength, flatListRef }) => {
  const [currentIndex, setCurrentIndex] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCurrentIndex((prevIndex) => (prevIndex + 1) % itemLength);
    }, 3000);

    return () => clearInterval(intervalId);
  }, [itemLength]);

  useEffect(() => {
    if (flatListRef.current) {
      flatListRef.current.scrollToIndex({
        index: currentIndex,
        animated: true,
      });
    }
  }, [currentIndex, flatListRef]);
};

Upvotes: 0

Robin de Rozario
Robin de Rozario

Reputation: 91

For those looking for a function-based component, this is my approach. The user can interact with the carousel and the automatic scroller will simply continue from the current slide.

The trick to achieving this is using an "onViewableItemsChanged" callback, where the "itemVisiblePercentThreshold" is >= 50%. This ensures the callback fires after the scroll to the new page is more than 50% complete (otherwise the automatic scroller triggers the callback to early and makes it scroll back).

import { useCallback, useEffect, useRef, useState } from "react";
import { Dimensions } from "react-native";
import { FlatList, Image, StyleSheet } from "react-native";

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

export const CarouselAutoScroll = ({ data, interval }) => {
    const imageRef = useRef();
    const [active, setActive] = useState(0);
    const indexRef = useRef(active);
    indexRef.current = active;

    useInterval(() => {
        if (active < Number(data?.length) - 1) {
            setActive(active + 1);
        } else {
            setActive(0);
        }
    }, interval);

    useEffect(() => {
        imageRef.current.scrollToIndex({ index: active, animated: true });
    }, [active]);

    const onViewableItemsChangedHandler = useCallback(
        ({ viewableItems, changed }) => {
            if (active != 0) {
                setActive(viewableItems[0].index);
            }
        },
        []
    );

    return (
        <FlatList
            showsHorizontalScrollIndicator={false}
            onViewableItemsChanged={onViewableItemsChangedHandler}
            viewabilityConfig={{
                itemVisiblePercentThreshold: 50,
            }}
            ref={imageRef}
            pagingEnabled
            data={data}
            horizontal
            renderItem={({ item, index }) => (
                <Image
                    key={index}
                    source={item.image}
                    resizeMode={"contain"}
                    style={{
                        flex: 1,
                        height: "100%",
                        width: width,
                    }}
                />
            )}
            style={{ ...StyleSheet.AbsoluteFill }}
        />
    );
};

const useInterval = (callback, delay) => {
    const savedCallback = useRef();

    useEffect(() => {
        savedCallback.current = callback;
    }, [callback]);

    useEffect(() => {
        const tick = () => {
            savedCallback.current();
        };
        if (delay !== null) {
            let id = setInterval(tick, delay);
            return () => clearInterval(id);
        }
    }, [delay]);
};

Upvotes: 0

kamalesh biswas
kamalesh biswas

Reputation: 1013

  1. This is my Data.

Blockquote

state = { 
link: [
  'https://image.shutterstock.com/image-vector/online-exam-computer-web-app-260nw-1105800884.jpg',
  'https://image.shutterstock.com/image-vector/online-exam-computer-web-app-260nw-1105800884.jpg',
  'https://image.shutterstock.com/image-vector/online-exam-computer-web-app-260nw-1105800884.jpg',
  'https://image.shutterstock.com/image-vector/online-exam-computer-web-app-260nw-1105800884.jpg',
],};
  1. Define FlatList Ref
flatList = createRef();
  1. FlatList component
   <FlatList
      style={{flex: 1}}
      data={this.state.link}
      keyExtractor={this._keyExtractor.bind(this)}
      renderItem={this._renderItem.bind(this)}
      horizontal={true}
      flatListRef={React.createRef()}
      ref={this.flatList}
    />
  1. Next slide
  _goToNextPage = () => {
     if (CurrentSlide >= this.state.link.length-1) CurrentSlide = 0;
     this.flatList.current.scrollToIndex({
     index: ++CurrentSlide,
     animated: true,
    });
  };
  1. Start and stop Interval
  _startAutoPlay = () => {
    this._timerId = setInterval(this._goToNextPage, IntervalTime);
  };



_stopAutoPlay = () => {
    if (this._timerId) {
      clearInterval(this._timerId);
      this._timerId = null;
    }
  };
  1. Associated function
componentDidMount() {
   this._stopAutoPlay();
   this._startAutoPlay();
}

componentWillUnmount() {
   this._stopAutoPlay();
}

_renderItem({item, index}) {
    return <Image source={{uri: item}} style={styles.sliderItems} />;
 }

_keyExtractor(item, index) {
    return index.toString();
 }

Full Code:

import React, {Component, createRef} from 'react';
import {
  Text,
  View,
  ScrollView,
  Image,
  StyleSheet,
  Dimensions,
  FlatList,
} from 'react-native';

let CurrentSlide = 0;
let IntervalTime = 4000;

export default class Slider extends Component {
  flatList = createRef();

  // TODO _goToNextPage()
  _goToNextPage = () => {
    if (CurrentSlide >= this.state.link.length-1) CurrentSlide = 0;

    this.flatList.current.scrollToIndex({
      index: ++CurrentSlide,
      animated: true,
    });
  };

  _startAutoPlay = () => {
    this._timerId = setInterval(this._goToNextPage, IntervalTime);
  };

  _stopAutoPlay = () => {
    if (this._timerId) {
      clearInterval(this._timerId);
      this._timerId = null;
    }
  };


  componentDidMount() {
    this._stopAutoPlay();
    this._startAutoPlay();
  }

  componentWillUnmount() {
    this._stopAutoPlay();
  }

  // TODO _renderItem()
  _renderItem({item, index}) {
    return <Image source={{uri: item}} style={styles.sliderItems} />;
  }

  // TODO _keyExtractor()
  _keyExtractor(item, index) {
    // console.log(item);
    return index.toString();
  }
  state = {
    link: [
      'https://image.shutterstock.com/image-vector/online-exam-computer-web-app-260nw-1105800884.jpg',
      'https://image.shutterstock.com/image-vector/online-exam-computer-web-app-260nw-1105800884.jpg',
    //   'https://picsum.photos/200/300',
      'https://image.shutterstock.com/image-vector/online-exam-computer-web-app-260nw-1105800884.jpg',
      'https://image.shutterstock.com/image-vector/online-exam-computer-web-app-260nw-1105800884.jpg',
    ],
  };

  render() {
    return (
      <View style={{marginTop: 10, marginBottom: 10}}>
        <FlatList
          style={{
            flex: 1,
            // TODO Remove extera global padding
            // marginLeft: -size.padding,
            // marginRight: -size.padding,
          }}
          data={this.state.link}
          keyExtractor={this._keyExtractor.bind(this)}
          renderItem={this._renderItem.bind(this)}
          horizontal={true}
          flatListRef={React.createRef()}
          ref={this.flatList}
        />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  sliderItems: {
    marginLeft: 5,
    marginRight: 5,
    height: 200,
    width: Dimensions.get('window').width,
  },
});

Upvotes: 9

cendekiapp
cendekiapp

Reputation: 39

Just in case you're still not found the answer yet, this is my approach to create autoscroll carousel using FlatList

import React, { Component } from 'react'
import {
  StyleSheet,
  View,
  FlatList,
  ScrollView,
  Dimensions,
  Image
} from 'react-native'

const { width } = Dimensions.get('window');
const height = width * 0.2844;

export default class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      search: '',
      sliderIndex: 0,
      maxSlider: 2,
      banners: [
        {_id: 1, imageUrl: 'https://www.do-cart.com/img/slider/1.jpg'},
        {_id: 2, imageUrl: 'https://www.do-cart.com/img/slider/2.jpg'},
        {_id: 3, imageUrl: 'https://www.do-cart.com/img/slider/3.jpg'},
      ],
    }
 }

 setRef = (c) => {
   this.listRef = c;
 }

 scrollToIndex = (index, animated) => {
   this.listRef && this.listRef.scrollToIndex({ index, animated })
 }

 componentWillMount() {
   setInterval(function() {
     const { sliderIndex, maxSlider } = this.state
     let nextIndex = 0

     if (sliderIndex < maxSlider) {
       nextIndex = sliderIndex + 1
     }

     this.scrollToIndex(nextIndex, true)
     this.setState({sliderIndex: nextIndex})
   }.bind(this), 3000)
 }

 render() {
   return (
     <View style={styles.container}>
       <View style={{height: 80, backgroundColor: '#123866', width:'100%'}}></View>

    <ScrollView style={styles.scrollContainer} showsVerticalScrollIndicator={false}>
      <FlatList
        ref={this.setRef}
        data={this.state.banners}
        horizontal
        showsHorizontalScrollIndicator={false}
        pagingEnabled
        keyExtractor={item => item._id}
        renderItem={({item, i}) => (
          <View key={i} style={{ height, width}}>
            <Image style={{ height, width }} source={{ uri: item.imageUrl }} />
          </View>
        )}
        onMomentumScrollEnd={(event) => {
          let sliderIndex = event.nativeEvent.contentOffset.x ? event.nativeEvent.contentOffset.x/width : 0
          this.setState({sliderIndex})
        }}
      />
      <View style={styles.sliderContainer}>
        {
          this.state.banners.map(function(item, index) {
            return (
              <View key={index} style={styles.sliderBtnContainer}>
                <View style={styles.sliderBtn}>
                  {
                    this.state.sliderIndex == index ? <View style={styles.sliderBtnSelected}/> : null
                  }
                </View>
              </View>
            )
          }.bind(this))
        }
      </View>
    </ScrollView>
  </View>
);
   }
 }

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  scrollContainer: {
    flex: 1
  },
  sliderContainer: {
    flexDirection: 'row',
    position: 'absolute',
    top: 80,
    alignSelf: 'center'
  },
  sliderBtn: {
    height: 13,
    width: 13,
    borderRadius: 12,
    borderWidth: 1,
    borderColor: 'white',
    alignItems: 'center',
    justifyContent: 'center',
    marginRight: 10
  },
  sliderBtnSelected: {
    height: 12,
    width: 12,
    borderRadius: 6,
    backgroundColor: 'white',
  },
  sliderBtnContainer: {
    flexDirection: 'row', marginBottom: 24
  },
});

https://snack.expo.io/rJ9DOn0Ef

Upvotes: 3

Related Questions