英文:
React Native Flatlist element onPress not fired until List rendering is complete
问题
我有一个FlatList
,它接收最多50个元素的不可变数据,并在每个列表项中使用react-native-svg
渲染Svg。
图形的某些部分被包装在Pressable
组件中以选择元素。
现在的问题是,我无法选择任何元素,直到FlatList
遍历了所有50个项目。
我不明白的是,屏幕外的项目甚至没有被渲染,只有容器。一旦全部渲染完成,我就可以点击元素,涟漪效果会显示并触发事件。
规格:
- Expo @ 46.0.0
- React Native @ 0.69.6
- React @ 18.0.0
- 通过
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. 获取数据
useEffect(() => {
// 从API加载数据
const loaded = [{ id: 0, type: 'foo' }, { id: 1, type: 'bar' }] // 在这里制作一个约50个的列表
setData(loaded)
}, [])
if (!data?.length) {
return null
}
// 2. 渲染列表项
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
}
// 此时数据已存在,不会再更改
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}
/>
)
}
// 优化的函数
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
// 这是一个非常简化的示例,渲染一个可按压的圆圈
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'
}
// 纯组件
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上发生。
英文:
I have a FlatList
that receives (immutable) data of max. 50 elements and it renders in each list item Svg using react-native-svg
.
Parts of the graphics are wrapped with a Pressable
component for selecting the element.
Now the problem is, that I can't select any of the elements, until the FlatList
went through all 50 items.
What I don't get is, that the offscreen items aren't even rendered, it's just the containers. Once it's all rendered, I can click the elements, the ripple effect shows and the event is fired.
Specs:
- Expo @ 46.0.0
- React Native @ 0.69.6
- React @ 18.0.0
- Running with android via
expo start --no-dev --minify
then open in Expo Go
Reproduction:
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 component, only Foo
is shown, since Bar
is structurally similar and the issue affects both:
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)
The rendering performance itself is quite good, however I can't understand, why I need to wait up to two seconds, until I can press the circles, allthough they have already been rendered.
Any help is greatly appreciated.
Edit
When scrolling the list very fast, I get:
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}
However, the Components are already memoized (PureComponent) and not very complex. There must be another issue.
Hardware
I cross tested with an iPad and there is none if the issues described. It seems to only occur on Android.
答案1
得分: 1
以下是翻译好的部分:
这是FlatList的问题。FlatList不适合一次渲染较大的列表,比如联系人列表。FlatList只适用于像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}) => {
// ... 这里省略了一些代码
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({
// ... 这里省略了一些样式代码
});
英文:
Please ignore grammatical mistakes.
This is the issue with FlatList. Flat list is not good for rendering a larger list at one like contact list. Flatlist is only good for getting data from API in church's like Facebook do. get 10 element from API and. then in the next call get 10 more.
To render. a larger number of items like contact list (more than 1000) or something like this please use 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,
},
});
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论