React Native Flatlist元素onPress在列表呈现完成之前不会触发

ddrv8njm  于 2023-01-14  发布在  React
关注(0)|答案(1)|浏览(147)
    • bounty将在5天后过期**。此问题的答案可获得+50声望奖励。Jankapunkt正在寻找此问题的更详细答案:请解释为什么FlatList阻止点击Pressable Svg,直到所有屏幕外项目呈现,以及这是否与我的FlatList配置或Svg有关。

我有一个FlatList,它接收最多50个元素的(不可变的)数据,并且它使用react-native-svg在每个列表项Svg中呈现。
图形的一部分被用于选择元素的Pressable组件包裹。
现在的问题是,我不能选择任何元素,直到FlatList遍历了所有50个项目。
我不明白的是,屏幕外的项目甚至没有呈现,它只是容器。一旦它全部呈现,我可以点击元素,涟漪效应显示和事件被激发。
规格:

  • 博览会@46.0.0
  • 在0.69.6时React天然
  • 在18.0.0时React
  • 通过expo start --no-dev --minify运行android,然后在Expo Go中打开

复制:

import React, { useEffect, useState } from 'react'
import { FlatList } from 'react-native'
import { Foo } from '/path/to/Foo'
import { Bar } from '/path/to/Bar'

export const Overview = props => {
  const [data, setData] = useState(null)
  
  // 1. fetching data

  useEffect(() => {
    // load data from api
    const loaded = [{ id: 0, type: 'foo' }, { id: 1, type: 'bar' }] // make a list of ~50 here
    setData(loaded)
  }, [])

  if (!data?.length) {
    return null
  }

  // 2. render list item
  const onPressed = () => console.debug('pressed')

  const renderListItem = ({ index, item }) => {
    if (item.type === 'foo') {
      return (<Foo key={`foo-${index}`} onPressed={onPressed} />)
    } 

    if (item.type === 'bar') {
      return (<Foo key={`bar-${index}`} onPressed={onPressed} />)
    }
  
    return null
  }

  // at this point data exists but will not be changed anymore
  // so theoretically there should be no re-render
  return (
    <FlatList
       data={data}
       renderItem={renderListItem}
       inverted={true}
       decelerationRate="fast"
       disableIntervalMomentum={true}
       removeClippedSubviews={true}
       persistentScrollbar={true}
       keyExtractor={flatListKeyExtractor}
       initialNumToRender={10}
       maxToRenderPerBatch={10}
       updateCellsBatchingPeriod={100}
       getItemLayout={flatListGetItemLayout}
     />
    )
  }
}

// optimized functions
const flatListKeyExtractor = (item) => item.id
const flatListGetItemLayout = (data, index) => {
  const entry = data[index]
  const length = entry && ['foo', 'bar'].includes(entry.type)
    ? 110
    : 59
  return { length, offset: length * index, index }
}

svg组件,仅显示Foo,因为Bar在结构上相似,并且该问题影响以下两者:

import React from 'react'
import Svg, { G, Circle } from 'react-native-svg'

const radius = 25
const size = radius * 2

// this is a very simplified example, 
// rendering a pressable circle
const FooSvg = props => {
  return (
    <Pressable
      android_ripple={rippleConfig}
      pressRetentionOffset={0}
      hitSlop={0}
      onPress={props.onPress}
    >
      <Svg
        style={props.style}
        width={size}
        height={size}
        viewBox={`0 0 ${radius * 2} ${radius * 2}`}
      >
        <G>
          <Circle
            cx='50%'
            cy='50%'
            stroke='black'
            strokeWidth='2'
            r={radius}
            fill='red'
          />
        </G>
      </Svg>
    </Pressable>
  )
}

const rippleConfig = {
  radius: 50,
  borderless: true,
  color: '#00ff00'
}

// pure component
export const Foo = React.memo(FooSvg)

渲染性能本身是相当不错的,但我不明白,为什么我需要等待长达两秒钟,直到我可以按下圆圈,尽管他们已经渲染。
任何帮助都将不胜感激。

编辑

快速滚动列表时,我得到:

VirtualizedList: You have a large list that is slow to update - make sure your renderItem function renders components that follow React performance best practices like PureComponent, shouldComponentUpdate, etc. {"contentLength": 4740, "dt": 4156, "prevDt": 5142}

然而,组件已经被记忆化了(PureComponent),不是很复杂。一定还有另一个问题。

硬件

我用iPad进行了交叉测试,如果描述的问题没有,似乎只发生在Android上。

7dl7o3gd

7dl7o3gd1#

请忽略语法错误。
这是平面列表的问题。平面列表不适合在一个像联系人列表这样的地方呈现更大的列表。平面列表只适合像Facebook那样从教堂的API获取数据。从API获取10个元素,然后在下一个调用中再获取10个。
要呈现大量项目,如联系人列表(超过1000个)或类似的内容,请使用https://bolan9999.github.io/react-native-largelist/#/en/

import React, {useRef, useState} from 'react';
import {
  Image,
  StyleSheet,
  Text,
  TextInput,
  TouchableOpacity,
  View,
} from 'react-native';
import {LargeList} from 'react-native-largelist-v3';
import Modal from 'react-native-modal';
import {widthPercentageToDP as wp} from 'react-native-responsive-screen';
import FontAwesome from 'react-native-vector-icons/FontAwesome';
import fonts from '../constants/fonts';
import {moderateScale} from '../constants/scaling';
import colors from '../constants/theme';
import countries from '../Data/larger_countries.json';

const CountrySelectionModal = ({visible, setDefaultCountry, setVisible}) => {
  const pressable = useRef(true);
  const [country_data, setCountryData] = useState(countries);
  const [search_text, setSearchText] = useState('');

  const onScrollStart = () => {
    if (pressable.current) {
      pressable.current = false;
    }
  };

  const onScrollEnd = () => {
    if (!pressable.current) {
      setTimeout(() => {
        pressable.current = true;
      }, 100);
    }
  };

  const _renderHeader = () => {
    return (
      <View style={styles.headermainView}>
        <View style={styles.headerTextBg}>
          <Text style={styles.headerTitle}>Select your country</Text>
        </View>
        <View style={styles.headerInputBg}>
          <TouchableOpacity
            onPress={() => searchcountry(search_text)}
            style={styles.headericonBg}>
            <FontAwesome
              name="search"
              size={moderateScale(20)}
              color={colors.textColor}
            />
          </TouchableOpacity>
          <TextInput
            placeholder="Select country by name"
            value={search_text}
            placeholderTextColor={colors.textColor}
            style={styles.headerTextInput}
            onChangeText={text => searchcountry(text)}
          />
        </View>
      </View>
    );
  };

  const _renderEmpty = () => {
    return (
      <View
        style={{
          height: moderateScale(50),
          backgroundColor: colors.white,

          flex: 1,
          justifyContent: 'center',
        }}>
        <Text style={styles.notFoundText}>No Result Found</Text>
      </View>
    );
  };
  const _renderItem = ({section: section, row: row}) => {
    const country = country_data[section].items[row];
    return (
      <TouchableOpacity
        activeOpacity={0.95}
        onPress={() => {
          setDefaultCountry(country),
            setSearchText(''),
            setCountryData(countries),
            setVisible(false);
        }}
        style={styles.renderItemMainView}>
        <View style={styles.FlagNameView}>
          <Image
            source={{
              uri: `https://zoobiapps.com/country_flags/${country.code.toLowerCase()}.png`,
            }}
            style={styles.imgView}
          />
          <Text numberOfLines={1} ellipsizeMode="tail" style={styles.text}>
            {country.name}
          </Text>
        </View>
        <Text style={{...styles.text, marginRight: wp(5), textAlign: 'right'}}>
          (+{country.callingCode})
        </Text>
      </TouchableOpacity>
    );
  };

  const searchcountry = text => {
    setSearchText(text);
    const items = countries[0].items.filter(row => {
      const result = `${row.code}${row.name.toUpperCase()}`;
      const txt = text.toUpperCase();
      return result.indexOf(txt) > -1;
    });
    setCountryData([{header: 'countries', items: items}]);
  };

  return (
    <Modal
      style={styles.modalStyle}
      animationIn={'slideInUp'}
      animationOut={'slideOutDown'}
      animationInTiming={1000}
      backdropOpacity={0.3}
      animationOutTiming={700}
      hideModalContentWhileAnimating={true}
      backdropTransitionInTiming={500}
      backdropTransitionOutTiming={700}
      useNativeDriver={true}
      isVisible={visible}
      onBackdropPress={() => {
        setVisible(false);
      }}
      onBackButtonPress={() => {
        setVisible(false);
      }}>
      <LargeList
        showsHorizontalScrollIndicator={false}
        style={{flex: 1, padding: moderateScale(10)}}
        onMomentumScrollBegin={onScrollStart}
        onMomentumScrollEnd={onScrollEnd}
        contentStyle={{backgroundColor: '#fff'}}
        showsVerticalScrollIndicator={false}
        heightForIndexPath={() => moderateScale(49)}
        renderIndexPath={_renderItem}
        data={country_data}
        bounces={false}
        renderEmpty={_renderEmpty}
        renderHeader={_renderHeader}
        headerStickyEnabled={true}
        initialContentOffset={{x: 0, y: 600}}
      />
    </Modal>
  );
};
export default CountrySelectionModal;

const styles = StyleSheet.create({
  modalStyle: {
    margin: moderateScale(15),
    borderRadius: moderateScale(10),
    overflow: 'hidden',
    backgroundColor: '#fff',
    marginVertical: moderateScale(60),
    justifyContent: 'center',
  },
  headermainView: {
    height: moderateScale(105),
    backgroundColor: '#fff',
  },
  headerTextBg: {
    height: moderateScale(50),
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#fff',
  },
  headerTitle: {
    textAlign: 'center',
    fontFamily: fonts.Bold,
    fontSize: moderateScale(16),
    color: colors.textColor,
    textAlignVertical: 'center',
  },
  headerInputBg: {
    height: moderateScale(40),
    borderRadius: moderateScale(30),
    overflow: 'hidden',
    justifyContent: 'center',
    alignItems: 'center',
    paddingHorizontal: moderateScale(10),
    backgroundColor: colors.inputbgColor,
    flexDirection: 'row',
  },
  headericonBg: {
    backgroundColor: colors.inputbgColor,
    alignItems: 'center',
    justifyContent: 'center',
    width: moderateScale(40),
    height: moderateScale(40),
  },
  headerTextInput: {
    backgroundColor: colors.inputbgColor,
    height: moderateScale(30),
    flex: 1,
    paddingTop: 0,
    includeFontPadding: false,
    fontFamily: fonts.Medium,
    color: colors.textColor,
    paddingBottom: 0,
    paddingHorizontal: 0,
  },
  notFoundText: {
    fontFamily: fonts.Medium,
    textAlign: 'center',
    fontSize: moderateScale(14),
    textAlignVertical: 'center',
    color: colors.textColor,
  },
  renderItemMainView: {
    backgroundColor: colors.white,
    flexDirection: 'row',
    alignSelf: 'center',
    height: moderateScale(43),
    alignItems: 'center',
    justifyContent: 'space-between',
    width: wp(100) - moderateScale(30),
  },
  FlagNameView: {
    flexDirection: 'row',
    justifyContent: 'center',
    paddingLeft: moderateScale(12),
    alignItems: 'center',
  },
  imgView: {
    height: moderateScale(30),
    width: moderateScale(30),
    marginRight: moderateScale(10),
    borderRadius: moderateScale(30),
  },
  text: {
    fontSize: moderateScale(13),
    color: colors.textColor,
    marginLeft: 1,
    fontFamily: fonts.Medium,
  },
});

相关问题