Flatlist alwaysBounceVertical

解决iOS FlatList下拉刷新难题
本文分享了一次解决iOS端FlatList组件在数据不满一屏时无法下拉刷新的问题经历,深入探讨了alwaysBounceVertical属性的作用及设置方法,为开发者提供了宝贵的实战经验。

接了别人写得代码,ios 端有个flatlist 当数据不满一屏时,不能下拉刷新,从排查布局,到请教别人,花费了好久时间。

终于有大神帮忙解决了问题,主要原因是加了flatlist 的这个属性

alwaysBounceVertical={false}

不知道,当时写代码的人的初衷是什么,但总之代码处处有惊喜!

官网给的解释:

alwaysBounceVertical

当此属性为 true 时,垂直方向即使内容比滚动视图本身还要小,也可以弹性地拉动一截。当horizontal={true}时默认值为 false,否则为 true。

类型必填平台
booliOS
import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react'; import { View, Image, FlatList, TouchableOpacity, StyleSheet, Dimensions, Platform, StyleProp, ViewStyle, ViewToken, NativeSyntheticEvent, NativeScrollEvent, GestureResponderEvent, } from 'react-native'; import { Uri } from 'base/navigator/Uri'; // 设计稿基础参数 const DESIGN_PIT_WIDTH = 320; const DESIGN_PIT_HEIGHT = 300; const OUTER_PADDING = 16; // 左右外边距,从30调整为16 const GAP = 16; // 坑之间间距,从30调整为16 const PIT_RADIUS = 18; // 单坑圆角 const AUTO_SCROLL_INTERVAL = 3000; const { width: screenWidth } = Dimensions.get('window'); // 动态计算单坑宽高(适配不同幕) const pitWidth = (screenWidth - OUTER_PADDING * 2 - GAP * 2) / 3; const pitHeight = pitWidth * DESIGN_PIT_HEIGHT / DESIGN_PIT_WIDTH; interface PitItem { image_url: string; jump_config?: { [key: string]: any; }; [key: string]: any; } interface HomeSmallPitBannerProps { data: PitItem[]; bgColor?: string; bgImage?: string; onPitPress?: (item: PitItem, index: number) => void; style?: StyleProp<ViewStyle>; scrollY?: number; // 新增:父组件传入的 scrollY } const HomeSmallPitBanner: React.FC<HomeSmallPitBannerProps> = (props) => { // 兼容新老数据结构 const rawData: any = props.data; let data: PitItem[] = []; let bgColor = props.bgColor; let bgImage = props.bgImage; if (Array.isArray(rawData)) { data = rawData; } else if (rawData && typeof rawData === 'object') { // 新mock结构:floor.data.data.jianyu_list 或 floor.data.jianyu_list data = rawData.data?.jianyu_list || rawData.jianyu_list || []; bgColor = rawData.data?.config?.bg_color || rawData.config?.bg_color || bgColor; bgImage = rawData.data?.config?.floorBgImg || rawData.config?.floorBgImg || bgImage; } const [currentIndex, setCurrentIndex] = useState(0); const [isVisible, setIsVisible] = useState(true); const [isManualScrolling, setIsManualScrolling] = useState(false); const [isTouching, setIsTouching] = useState(false); const flatListRef = useRef<FlatList<PitItem> | null>(null); const timerRef = useRef<NodeJS.Timeout | null>(null); const currentIndexRef = useRef(0); const isScrollingRef = useRef(false); const isJumpingRef = useRef(false); const bannerRef = useRef<View | null>(null); const originalDataLengthRef = useRef(data.length); const [bannerLayout, setBannerLayout] = useState({ pageY: 0, height: 0 }); // 触摸事件处理 - 通知父组件 const handleTouchStart = () => { setIsTouching(true); // 通知父组件轮播开始触摸 if (global.carouselTouchHandler) { global.carouselTouchHandler(true); } }; const handleTouchEnd = () => { setIsTouching(false); // 通知父组件轮播结束触摸 if (global.carouselTouchHandler) { global.carouselTouchHandler(false); } }; // 无缝滚动:采用复制首尾元素的方法,实现真正的无缝循环 const seamlessData = useMemo(() => { if (data.length === 0) return []; if (data.length <= 3) return data; // 更新原始数据长度引用 originalDataLengthRef.current = data.length; // 前面拼接最后3个数据(尾部元素复制到头部) const frontDuplicatedItems = data .slice(-3) .map((item, index) => ({ ...item, _duplicated: true, _originalIndex: data.length - 3 + index, _isFrontDuplicated: true, })); // 后面拼接前3个数据(首部元素复制到尾部) const backDuplicatedItems = data .slice(0, 3) .map((item, index) => ({ ...item, _duplicated: true, _originalIndex: index, _isBackDuplicated: true, })); return [...frontDuplicatedItems, ...data, ...backDuplicatedItems]; }, [data]); useEffect(() => { currentIndexRef.current = currentIndex; }, [currentIndex]); // 用 onLayout + scrollY 实检测可见性 useEffect(() => { if (typeof props.scrollY !== 'number') return; const windowHeight = Dimensions.get('window').height; const { pageY, height } = bannerLayout; // 只要有部分在幕内就算可见 const visible = pageY + height > props.scrollY && pageY < props.scrollY + windowHeight; setIsVisible(visible); }, [props.scrollY, bannerLayout]); // 监听可见性:不可见暂停轮播 const handleViewableItemsChanged = useCallback( ({ viewableItems }: { viewableItems: ViewToken[] }) => { const visible = viewableItems.some((item) => item.isViewable); setIsVisible(visible); }, [] ); // 边界跳转逻辑 - 采用复制首尾元素的方法 const handleEdgeJump = useCallback((index: number) => { if (seamlessData.length > 3) { if (index < 3) { // 滚动到头部复制品,无过渡跳回真实最后flatListRef.current?.scrollToOffset({ offset: (data.length + 3 - 1) * (pitWidth + GAP), animated: false, }); setCurrentIndex(data.length - 1); currentIndexRef.current = data.length - 1; return true; } else if (index >= data.length + 3) { // 滚动到尾部复制品,无过渡跳回真实第flatListRef.current?.scrollToOffset({ offset: 3 * (pitWidth + GAP), animated: false, }); setCurrentIndex(0); currentIndexRef.current = 0; return true; } } return false; }, [data.length, pitWidth, GAP, seamlessData.length]); // 自动轮播 - 优化版本,参考文章实现真正的无缝轮播 const startAutoScroll = useCallback(() => { if (timerRef.current) clearInterval(timerRef.current); if (seamlessData.length <= 3 || !isVisible || isTouching || isManualScrolling) return; timerRef.current = setInterval(() => { if (isScrollingRef.current || isJumpingRef.current || isTouching || isManualScrolling) return; let nextIndex = currentIndexRef.current + 1; if (nextIndex >= data.length) nextIndex = 0; // 直接滚动到下个位置,无缝轮播 const targetOffset = (nextIndex + 3) * (pitWidth + GAP); flatListRef.current?.scrollToOffset({ offset: targetOffset, animated: true, }); setCurrentIndex(nextIndex); currentIndexRef.current = nextIndex; }, AUTO_SCROLL_INTERVAL); }, [seamlessData.length, isVisible, isTouching, isManualScrolling, pitWidth, data.length, GAP]); // 监听可见性变化,自动暂停/恢复轮播 useEffect(() => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } if (isVisible) { startAutoScroll(); } // 不可见自动暂停,无需额外处理 return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } }; }, [isVisible, startAutoScroll]); // 滚动事件:更新当前索引 const handleScroll = useCallback( (event: NativeSyntheticEvent<NativeScrollEvent>) => { if (isJumpingRef.current) return; const offsetX = event.nativeEvent.contentOffset.x; const index = Math.round(offsetX / (pitWidth + GAP)); if (handleEdgeJump(index)) return; // 只处理有效的索引范围 if (index >= 0 && index < seamlessData.length) { if (index >= 3 && index < data.length + 3) { const actualIndex = (index - 3 + data.length) % data.length; if (actualIndex !== currentIndex) { setCurrentIndex(actualIndex); currentIndexRef.current = actualIndex; } } } }, [currentIndex, seamlessData.length, pitWidth, handleEdgeJump, data.length] ); // 手动拖动开始:暂停自动轮播 const handleScrollBegin = useCallback(() => { isScrollingRef.current = true; setIsManualScrolling(true); setIsTouching(true); // 通知父组件轮播开始触摸 if (global.carouselTouchHandler) { global.carouselTouchHandler(true); } // 立即清除定器 if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } }, []); // 手动拖动结束:恢复自动轮播 - 优化版本 const handleScrollEnd = useCallback((event) => { isScrollingRef.current = false; setIsManualScrolling(false); setIsTouching(false); if (global.carouselTouchHandler) { global.carouselTouchHandler(false); } const offsetX = event?.nativeEvent?.contentOffset?.x ?? 0; const index = Math.round(offsetX / (pitWidth + GAP)); handleEdgeJump(index); // 减少延迟间,提升响应性 setTimeout(() => { if (timerRef.current) clearInterval(timerRef.current); if (seamlessData.length <= 3 || !isVisible) return; timerRef.current = setInterval(() => { if (isScrollingRef.current || isJumpingRef.current || isTouching || isManualScrolling) return; let nextIndex = currentIndexRef.current + 1; if (nextIndex >= data.length) nextIndex = 0; const targetOffset = (nextIndex + 3) * (pitWidth + GAP); flatListRef.current?.scrollToOffset({ offset: targetOffset, animated: true, }); setCurrentIndex(nextIndex); currentIndexRef.current = nextIndex; }, AUTO_SCROLL_INTERVAL); }, 300); // 减少延迟间 }, [seamlessData.length, isVisible, pitWidth, handleEdgeJump, data.length]); // 动量滚动开始:暂停自动轮播 const handleMomentumScrollBegin = useCallback(() => { isScrollingRef.current = true; setIsManualScrolling(true); setIsTouching(true); // 通知父组件轮播开始触摸 if (global.carouselTouchHandler) { global.carouselTouchHandler(true); } if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } }, []); // 动量滚动结束:恢复自动轮播 - 优化版本 const handleMomentumScrollEnd = useCallback((event) => { isScrollingRef.current = false; setIsManualScrolling(false); setIsTouching(false); if (global.carouselTouchHandler) { global.carouselTouchHandler(false); } const offsetX = event?.nativeEvent?.contentOffset?.x ?? 0; const index = Math.round(offsetX / (pitWidth + GAP)); handleEdgeJump(index); // 减少延迟间,提升响应性 setTimeout(() => { if (timerRef.current) clearInterval(timerRef.current); if (seamlessData.length <= 3 || !isVisible) return; timerRef.current = setInterval(() => { if (isScrollingRef.current || isJumpingRef.current || isTouching || isManualScrolling) return; let nextIndex = currentIndexRef.current + 1; if (nextIndex >= data.length) nextIndex = 0; const targetOffset = (nextIndex + 3) * (pitWidth + GAP); flatListRef.current?.scrollToOffset({ offset: targetOffset, animated: true, }); setCurrentIndex(nextIndex); currentIndexRef.current = nextIndex; }, AUTO_SCROLL_INTERVAL); }, 200); // 进步减少延迟间 }, [seamlessData.length, isVisible, pitWidth, handleEdgeJump, data.length]); // 渲染单个坑位(只显示图片) const renderPit = useCallback( ({ item, index }: { item: PitItem & { _duplicated?: boolean; _originalIndex?: number; _isFrontDuplicated?: boolean; _isBackDuplicated?: boolean }; index: number }) => ( <TouchableOpacity style={[ styles.pitContainer, { width: pitWidth, height: pitHeight, marginRight: index !== seamlessData.length - 1 ? GAP : 0, // 硬件加速优化 - 使用兼容的transform transform: [{ translateX: 0 }], }, ]} activeOpacity={0.85} onPress={() => Uri.openUri(1, item.jump_config?.url)} > <Image source={{ uri: item.image_url }} style={[ styles.pitImage, // 硬件加速优化 - 使用兼容的transform { transform: [{ translateX: 0 }] } ]} resizeMode="contain" /> </TouchableOpacity> ), [pitWidth, pitHeight, seamlessData.length] ); // 唯 key 生成 const keyExtractor = useCallback( (item: PitItem & { _duplicated?: boolean; _originalIndex?: number; _isFrontDuplicated?: boolean; _isBackDuplicated?: boolean }, index: number) => { if (item._isFrontDuplicated) { return `front-${item._originalIndex}-${index}`; } if (item._isBackDuplicated) { return `back-${item._originalIndex}-${index}`; } return `original-${index}`; }, [] ); // 初始化滚动到正确位置(第4个位置,即原始数据的第1个) useEffect(() => { if (seamlessData.length > 3) { setTimeout(() => { flatListRef.current?.scrollToOffset({ offset: 3 * (pitWidth + GAP), animated: false, }); }, 100); } }, [seamlessData.length, pitWidth]); return ( <View ref={bannerRef} style={props.style} onLayout={e => { // 使用 onLayout 事件直接获取布局信息,避免 UIManager.measure 回调泄漏 const { y, height } = e.nativeEvent.layout; setBannerLayout({ pageY: y, height }); }} > {bgImage && ( <Image source={{ uri: bgImage }} style={styles.bgImage} resizeMode="cover" /> )} <FlatList ref={flatListRef} data={seamlessData} renderItem={renderPit} keyExtractor={keyExtractor} horizontal showsHorizontalScrollIndicator={false} snapToInterval={pitWidth + GAP} decelerationRate="fast" bounces={false} pagingEnabled={false} onScroll={handleScroll} onScrollBeginDrag={handleScrollBegin} onScrollEndDrag={handleScrollEnd} onMomentumScrollBegin={handleMomentumScrollBegin} onMomentumScrollEnd={handleMomentumScrollEnd} viewabilityConfig={{ viewAreaCoveragePercentThreshold: 50 }} onViewableItemsChanged={handleViewableItemsChanged} style={styles.flatList} contentContainerStyle={styles.flatListContent} removeClippedSubviews={false} // 添加触摸事件处理 onTouchStart={handleTouchStart} onTouchEnd={handleTouchEnd} // 简化手势处理,避免冲突 onStartShouldSetResponder={() => false} onMoveShouldSetResponder={() => false} // 确保轮播优先响应手势 - 参考文章优化 scrollEventThrottle={16} directionalLockEnabled={true} alwaysBounceHorizontal={false} alwaysBounceVertical={false} nestedScrollEnabled={true} // 新增:优化滚动体验 keyboardShouldPersistTaps="handled" getItemLayout={(data, index) => ({ length: pitWidth + GAP, offset: (pitWidth + GAP) * index, index, })} /> </View> ); }; const styles = StyleSheet.create({ container: { position: 'relative', overflow: 'hidden', paddingHorizontal: 0, }, bgImage: { position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', zIndex: 0, }, flatList: { zIndex: 1, }, flatListContent: { paddingVertical: 8, // 从12调整为8 paddingHorizontal: OUTER_PADDING, flexDirection: 'row', alignItems: 'center', }, pitContainer: { borderRadius: PIT_RADIUS, overflow: Platform.OS === 'android' ? 'hidden' : 'visible', backgroundColor: '#fff', justifyContent: 'center', alignItems: 'center', }, pitImage: { width: '100%', height: '100%', borderRadius: PIT_RADIUS, }, }); export default HomeSmallPitBanner; 这个无缝轮播有问题,无论是手动滑动还是自动轮播最后张到第张的候会出现卡顿往返晃下的情况,解决
最新发布
08-02
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值