【问题标题】:FlatList scrollToIndex out of rangeFlatList scrollToIndex 超出范围
【发布时间】:2021-04-29 21:42:09
【问题描述】:

我有一个 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;

【问题讨论】:

  • 前 2 行似乎不属于某个组件。 useState 和 useRef 必须在组件的开头使用。
  • 我更新了帖子以反映完整的代码

标签: javascript reactjs react-native react-hooks use-state


【解决方案1】:

您收到此错误是因为您将item 作为data 传递给AdSlider 组件,它当然没有任何length 属性,因此它为data.length 返回undefined 和不计算表达式currentIndex === data.length - 1,它变成currentIndex === undefined - 1,因此currentIndex 将在不停止的情况下增加1,它将达到超出范围的2 的值。

您的代码有几个问题。

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

  2. 您将作为data 的项目传递给AdSlider,并且您正试图以data.length 的形式获取它,这显然是行不通的,因为dataitem一个对象而不是一个数组。

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

使用 state 和 setTimeout 使其工作

如果你想让currentIndex 处于状态的代码wotk(你不需要),你可以在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

【讨论】:

  • 这似乎可行,但应用程序崩溃了一会儿,然后它给出了这个错误:null is not an object(evaluating 'myRef.current.scrollToIndex')。我更新了我的帖子以反映新的变化。
  • 我会将您的答案标记为正确。我认为我引入的模态panResponder 代码现在导致了另一个问题,即我现在遇到的错误。谢谢!
  • 感谢您的赏金。错误null is not an object(evaluating 'myRef.current.scrollToIndex') 很可能是由于快速刷新(或热重载)而发生的,并且效果在myRef 获取实际列表引用之前触发,因此它在第一次调用之后为空重新渲染。你要么检查myRef.current,如果它不是假的,或者更好地使用可选链来调用scrollToIndex,就像这样myRef?.current?.scrollToIndex(...)
【解决方案2】:

您的if 语句似乎不正确,最大索引应为totalLength - 1。 比如我们有一个3个元素的数组:[{id: 1, index: 0}, {id: 2, index: 1}, {id: 3, index: 2}],那么数组的长度是3,但是最大索引是2,所以当当前索引是">= 2 (totalLength - 1) ",您应该将其重置为0。对于 else 条件,将下一个索引设置为 '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 (...)
}

【讨论】:

  • 我实施了您的建议,但我仍然收到scrollToIndex out of range: requested index 2 but maximum is 1。我用新代码更新了帖子
猜你喜欢
  • 2021-07-04
  • 2021-12-01
  • 2021-06-16
  • 1970-01-01
  • 2020-04-06
  • 1970-01-01
  • 2023-03-27
  • 1970-01-01
  • 2019-07-01
相关资源
最近更新 更多