FlatList scrollToIndex 超出范围

FlatList scrollToIndex out of range

我有一个 FlatList,我试图每隔 X 秒滚动浏览我的数据数组的每个索引。现在我的数组中只有两个项目,但可能会有更多。当前代码适用于前两次迭代,但随后似乎无法正确重置,我得到了 scrollToIndex out of range error: index is 2 but maximum is 1。我会认为当 currentIndex>= data.length 我的 if 语句会 setCurrentIndex 回到 0 但它似乎不起作用。基本上我想做的是自动循环 Flatlist 中的项目,但每个项目暂停几秒钟。

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 *
 * @format
 * @flow strict-local
 */
import 'react-native-gesture-handler';
import React,  {useState, useEffect, useRef} from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator, HeaderBackButton } from '@react-navigation/stack';
import {
  SafeAreaView,
  StyleSheet,
  ScrollView,
  View,
  Text,
  StatusBar,
  ImageBackground,
  Image,
  TextInput,
  Button,
  TouchableNativeFeedback,
  TouchableWithoutFeedback,
  TouchableOpacity,
  Modal,
  Pressable,
  PanResponder,
  FlatList,
  Dimensions
} from 'react-native';

import { Immersive } from 'react-native-immersive';

import {
  Header,
  LearnMoreLinks,
  Colors,
  DebugInstructions,
  ReloadInstructions,
} from 'react-native/Libraries/NewAppScreen';

import WineList from './screens/WineList';
import Home from './screens/Home';
import Rate from './screens/Rate';
import Thankyou from './screens/Thankyou';

const Stack = createStackNavigator();

const { width: windowWidth, height: windowHeight } = Dimensions.get("window");

const wineclub = require('./images/wineclub.png');
const gaspers = require('./images/gaspers.png');
const qrcode = require('./images/wineclubQR.png');

let ads = [
  {
    adImg: wineclub,
    adTitle: 'Space will be limited so join online today!',
    adInfo: ' Upon joining, both clubs will be billed our Trio Pre-Opening Promotion',
    qrCodeImg: qrcode
  },
  {
    adImg: gaspers,
    adTitle: 'Coming Soon!',
    adInfo: 'Gourmet chef designed menu. Stunning views. Modern romantic decor',
    qrCodeImg: qrcode
  }
]


function AdSlider({data}){
    
  return(
   
             <View style={{alignContent:'center', alignItems:'center', backgroundColor:'#4B4239', height:1400}}>

               <Image source={data.adImg} style={{width:640,height:500}} ></Image>

               <Text style={{color:'white', fontFamily:'LaoMN', fontSize:30, marginTop:20}}>{data.adTitle}</Text>

               <Text style={{color:'white', fontFamily:'LaoMN', fontSize:20, marginTop:20, textAlign:'center'}} > {data.adInfo} </Text>

               

               <View style={{flexDirection:'row', justifyContent:'flex-start', alignContent:'center', alignItems:'center', marginTop:20}}>
                 <Text style={{fontSize:40, color:'white', padding:20}}>Scan Here </Text>

                 <Image source={data.qrCodeImg}></Image>
               </View>

             </View>
            
  )
}

const App: () => React$Node = () => {
  Immersive.on()
  Immersive.setImmersive(true)

  const navigationRef = useRef(null);

    
  const myRef = useRef(null);   

  const currentIndex = useRef(0);

  const [modalVisible, setModalVisible] = useState(false);

  const timerId = useRef(false);

  const [timeForInactivityInSecond, setTimeForInactivityInSecond] = useState(
    5
  )

 

  useEffect(() => {
    resetInactivityTimeout()
  },[])

  const panResponder = React.useRef(
    PanResponder.create({
      onStartShouldSetPanResponderCapture: () => {
        // console.log('user starts touch');
        
        setModalVisible(false)
        resetInactivityTimeout()
      },
    })
  ).current

  const resetInactivityTimeout = () => {
    clearTimeout(timerId.current)
    
    timerId.current = setTimeout(() => {
      // action after user has been detected idle
      
      setModalVisible(true)
      navigationRef.current?.navigate('Home');
    }, timeForInactivityInSecond * 1000)
  }

 
// for the slider
  useEffect(() => {
    const timer = setInterval(() => {
      currentIndex.current = currentIndex.current === ads.length - 1
        ? 0
        : currentIndex.current + 1;
        myRef.current.scrollToIndex({
          animated: true,
          index: currentIndex.current ,
        });
    }, 5000);
    return () => clearInterval(timer);
  }, []);
  

  

  return (
    
    <NavigationContainer ref={navigationRef} >
       <View {...panResponder.panHandlers}  style={{ flex:1}}>

         <TouchableWithoutFeedback >
       <Modal
             
            animationType="slide"
            transparent={false}
            hardwareAccelerated={false}
            visible={modalVisible}
      
            >
              <FlatList
              ref={myRef}
              data={ads}
              renderItem={({ item, index }) => {
              return <AdSlider key={index} data={item} dataLength={ads.length} />;
              }}
              pagingEnabled
              horizontal
              showsHorizontalScrollIndicator={false}

              />
             
              
            </Modal>
              </TouchableWithoutFeedback>
        <Stack.Navigator navigationOptions={{headerTintColor: '#ffffff',}} screenOptions={{
           headerTintColor: '#ffffff',
          cardStyle: { backgroundColor: '#4B4239' },
          }} >
          <Stack.Screen name="Home"
          component={Home}  options={{
            headerShown: false,
          }} />  

          <Stack.Screen name="WineList" component={WineList} options={{
          title: 'Exit',
          headerStyle: {
            backgroundColor: '#4B4239',
          },
          headerTintColor: '#fff',
          headerTitleStyle: {
            fontWeight: 'bold',
          },
        }}/>

          <Stack.Screen name="Rate" component={Rate} options={{
          title: 'Back to Selections',
          headerStyle: {
            backgroundColor: '#4B4239',
          },
          headerTintColor: '#fff',
          headerTitleStyle: {
            fontWeight: 'bold',
          },
        }}/>

          <Stack.Screen name="Thankyou" component={Thankyou} 
          options={
          {  
          headerShown: false,    
          title: 'Home',  
          headerStyle: {
            backgroundColor: '#4B4239',
          },
          headerTintColor: '#fff',
          headerTitleStyle: {
            fontWeight: 'bold',
          },
        }}/>
        </Stack.Navigator>    
    </View>
      </NavigationContainer>

  );
};



export default App;

看来你的if说法不正确,最大索引应该是totalLength - 1。 比如我们有一个数组,有3个元素:[{id: 1, index: 0}, {id: 2, index: 1}, {id: 3, index: 2}],那么数组的长度是3,但是最大索引是2,所以当当前索引是">= 2 (totalLength - 1)”,您应该将其重置为 0。对于其他条件,将下一个索引设置为 'currentIdx + 1'

      if(activeIdx === ITEMS.length - 1){
        setActiveIdx(0)
      } else {
        setActiveIdx(idx => idx + 1);
      }

更详细的代码可能如下所示:

function Slider(props) {
  const ref = React.useRef(null);
  const [activeIdx, setActiveIdx] = React.useState(0)
  
  React.useEffect(() => {
    ref.current.scroll({left: ITEM_WIDTH * activeIdx, behavior: "smooth"}) // please use .scrollToIndex here
  }, [activeIdx]);

  React.useEffect(() => {
    let timer = setTimeout(() => {
      let nextIdx = activeIdx === ITEMS.length - 1 ? 0 : activeIdx + 1;
      setActiveIdx(nextIdx)
    }, 3000);
    return () => clearTimeout(timer)
  }, [activeIdx]);

  return (...)
}

您收到此错误是因为您将 item 作为 data 传递给 AdSlider 组件并且它没有任何 length 属性 当然因此它 returns undefined 对于 data.length 并且不计算表达式 currentIndex === data.length - 1 它变成 currentIndex === undefined - 1 因此 currentIndex 会增加通过 1 而不停止,它将达到超出范围的 2 的值。

您的代码有几个问题。

  1. 你不应该在另一个组件中有一个组件,尤其是在使用来自父组件的效果和状态时。删除 App 组件外的 AdSlider

  2. 您正在将项目作为 data 传递给 AdSlider,并且您正试图将其作为 data.length 获取,这显然不会工作,因为 dataitem,它是一个对象而不是数组。

  3. 您不需要使用 AdSlider 内的效果,只在 App 内设置一个效果并将 currentIndex 改为 ref一个状态变量,因为你不需要它改变状态来重新渲染,因为你正在调用 scrollToIndex 强制列表更新和重新渲染。

使用状态和 setTimeout

使其工作

如果你想让代码 wotk currentIndex 处于状态(你不需要),你可以在 App 组件并将 data.length 更改为 ads.length,它将起作用。

const App: () => React$Node = () => {
  Immersive.on()
  Immersive.setImmersive(true)

  const navigationRef = useRef(null);
  const myRef = useRef(null);   
  const [currentIndex, setCurrentIndex] = useState(0);

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

  useEffect(()=> {
    const timer = setTimeout(()=> {
      // Change data.length to ads.length here
      const nextIndex = currentIndex === ads.length - 1
        ? 0
        : currentIndex + 1;
      setCurrentIndex(nextIndex);
    }, 5000);
    return () => clearTimeout(timer);
  }, [currentIndex]);

  ...
}

使用 ref 和 setInterval

使其工作

不过,最好的办法是将 currentIndex 转换为 ref 并使用 setInterval 而不是 setTimeout 每 5 秒调用一次循环计时器:

const App: () => React$Node = () => {
  Immersive.on()
  Immersive.setImmersive(true)

  const navigationRef = useRef(null);
  const myRef = useRef(null);   
  // Make currentIndex a ref instead of a state variable,
  // because we don't need the re-renders
  // nor to trigger any effects depending on it
  const currentIndex = useRef(0);

  useEffect(() => {
    // Have a timer call the function every 5 seconds using setInterval
    const timer = setInterval(() => {
      // Change data.length to ads.length here
      currentIndex.current = currentIndex.current === ads.length - 1
        ? 0
        : currentIndex.current + 1;
      myRef.current.scrollToIndex({
        animated: true,
        index: currentIndex.current,
      });
    }, 5000);
    return () => clearInterval(timer);
  }, []);

  ...
}

您可以查看有效的 Expo Snack here