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;
这个无缝轮播有问题,无论是手动滑动还是自动轮播最后一张到第一张的时候会出现卡顿往返晃一下的情况,解决一下
最新发布