From fb819798b1d869b384e32d8b0473d0cc43eeecd4 Mon Sep 17 00:00:00 2001 From: Developer Date: Sat, 14 Mar 2026 12:27:03 +0800 Subject: [PATCH] 1 --- app/index.tsx | 285 ++++++++++++++++++++---------------- components/BigVideoCard.tsx | 38 ++--- components/LiveCard.tsx | 19 ++- hooks/useVideoList.ts | 2 +- utils/videoRows.ts | 24 ++- 5 files changed, 207 insertions(+), 161 deletions(-) diff --git a/app/index.tsx b/app/index.tsx index ae3ac6f..6115efd 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -18,6 +18,7 @@ import { FlatList, ScrollView, Linking, + useWindowDimensions, } from "react-native"; import { SafeAreaView, @@ -31,7 +32,12 @@ import { LoginModal } from "../components/LoginModal"; import { useVideoList } from "../hooks/useVideoList"; import { useLiveList } from "../hooks/useLiveList"; import { useAuthStore } from "../store/authStore"; -import { toListRows, type ListRow, type BigRow, type LiveRow } from "../utils/videoRows"; +import { + toListRows, + type ListRow, + type BigRow, + type LiveRow, +} from "../utils/videoRows"; import { BigVideoCard } from "../components/BigVideoCard"; import type { LiveRoom } from "../services/types"; @@ -62,7 +68,8 @@ const LIVE_AREAS = [ export default function HomeScreen() { const router = useRouter(); - const { pages, liveRooms, loading, refreshing, load, refresh } = useVideoList(); + const { pages, liveRooms, loading, refreshing, load, refresh } = + useVideoList(); const { rooms, loading: liveLoading, @@ -76,8 +83,10 @@ export default function HomeScreen() { const [activeTab, setActiveTab] = useState("hot"); const [liveAreaId, setLiveAreaId] = useState(0); + const { width: SCREEN_W } = useWindowDimensions(); const [visibleBigKey, setVisibleBigKey] = useState(null); const rows = useMemo(() => toListRows(pages, liveRooms), [pages, liveRooms]); + const tabAnim = useRef(new Animated.Value(0)).current; const hotListRef = useRef(null); const liveListRef = useRef(null); @@ -134,10 +143,9 @@ export default function HomeScreen() { const onLiveScroll = useMemo( () => - Animated.event( - [{ nativeEvent: { contentOffset: { y: liveScrollY } } }], - { useNativeDriver: true }, - ), + Animated.event([{ nativeEvent: { contentOffset: { y: liveScrollY } } }], { + useNativeDriver: true, + }), [], ); @@ -156,6 +164,11 @@ export default function HomeScreen() { } // 切换 tab setActiveTab(key); + Animated.timing(tabAnim, { + toValue: key === "hot" ? 0 : 1, + duration: 250, + useNativeDriver: true, + }).start(); if (key === "live" && rooms.length === 0) { liveLoad(true, liveAreaId); } @@ -192,21 +205,15 @@ export default function HomeScreen() { } if (row.type === "live") { return ( - - - Linking.openURL(`https://live.bilibili.com/${row.left.roomid}`)} - /> - - {row.right && ( - - Linking.openURL(`https://live.bilibili.com/${row.right!.roomid}`)} - /> - - )} + + + Linking.openURL(`https://live.bilibili.com/${row.left.roomid}`) + } + /> ); } @@ -237,11 +244,11 @@ export default function HomeScreen() { ({ item }: { item: { left: LiveRoom; right?: LiveRoom } }) => ( - + {item.right && ( - + )} @@ -263,112 +270,127 @@ export default function HomeScreen() { const currentHeaderOpacity = activeTab === "hot" ? headerOpacity : liveHeaderOpacity; + const tabSlideX = tabAnim.interpolate({ + inputRange: [0, 1], + outputRange: [0, -SCREEN_W], + }); + return ( - {/* 热门列表 */} - {activeTab === "hot" && ( - - row.type === "big" - ? `big-${row.item.bvid}` - : row.type === "live" - ? `live-${row.left.roomid}-${row.right?.roomid ?? "empty"}` - : `pair-${row.left.bvid}-${row.right?.bvid ?? "empty"}` - } - contentContainerStyle={{ - paddingTop: insets.top + NAV_H + 6, - paddingBottom: insets.bottom + 16, - }} - renderItem={renderItem} - refreshControl={ - + + {/* 热门列表 */} + + + row.type === "big" + ? `big-${row.item.bvid}` + : row.type === "live" + ? `live-${row.left.roomid}-${row.right?.roomid ?? "empty"}` + : `pair-${row.left.bvid}-${row.right?.bvid ?? "empty"}` + } + contentContainerStyle={{ + paddingTop: insets.top + NAV_H + 6, + paddingBottom: insets.bottom + 16, + }} + renderItem={renderItem} + refreshControl={ + + } + onEndReached={() => load()} + onEndReachedThreshold={0.5} + extraData={visibleBigKey} + viewabilityConfig={VIEWABILITY_CONFIG} + onViewableItemsChanged={onViewableItemsChangedRef} + ListFooterComponent={ + + {loading && } + + } + onScroll={onScroll} + scrollEventThrottle={16} /> - } - onEndReached={() => load()} - onEndReachedThreshold={0.5} - extraData={visibleBigKey} - viewabilityConfig={VIEWABILITY_CONFIG} - onViewableItemsChanged={onViewableItemsChangedRef} - ListFooterComponent={ - - {loading && } - - } - onScroll={onScroll} - scrollEventThrottle={16} - /> - )} + - {/* 直播列表 */} - {activeTab === "live" && ( - - `live-${index}-${item.left.roomid}-${item.right?.roomid ?? "empty"}` - } - contentContainerStyle={{ - paddingTop: insets.top + NAV_H + 6, - paddingBottom: insets.bottom + 16, - }} - renderItem={renderLiveItem} - ListHeaderComponent={ - - {LIVE_AREAS.map((area) => ( - handleLiveAreaPress(area.id)} - activeOpacity={0.7} + {/* 直播列表 */} + + + `live-${index}-${item.left.roomid}-${item.right?.roomid ?? "empty"}` + } + contentContainerStyle={{ + paddingTop: insets.top + NAV_H + 6, + paddingBottom: insets.bottom + 16, + }} + renderItem={renderLiveItem} + ListHeaderComponent={ + - - {area.name} - - - ))} - - } - refreshControl={ - liveRefresh(liveAreaId)} - progressViewOffset={insets.top + NAV_H} + {LIVE_AREAS.map((area) => ( + handleLiveAreaPress(area.id)} + activeOpacity={0.7} + > + + {area.name} + + + ))} + + } + refreshControl={ + liveRefresh(liveAreaId)} + progressViewOffset={insets.top + NAV_H} + /> + } + onEndReached={() => liveLoad()} + onEndReachedThreshold={1.5} + ListFooterComponent={ + liveLoading ? ( + + + 加载中... + + ) : null + } + onScroll={onLiveScroll} + scrollEventThrottle={16} /> - } - onEndReached={() => liveLoad()} - onEndReachedThreshold={1.5} - ListFooterComponent={ - liveLoading ? ( - - - 加载中... - - ) : null - } - onScroll={onLiveScroll} - scrollEventThrottle={16} - /> - )} + + + {/* 绝对定位导航栏 */} Math.abs(gs.dx) > SWIPE_THRESHOLD && Math.abs(gs.dx) > Math.abs(gs.dy), + onMoveShouldSetPanResponderCapture: (_, gs) => + Math.abs(gs.dx) > SWIPE_THRESHOLD && Math.abs(gs.dx) > Math.abs(gs.dy), onPanResponderGrant: () => { seekingRef.current = true; swipeStartTime.current = currentTimeRef.current; @@ -255,23 +257,25 @@ export function BigVideoCard({ item, isVisible, onPress }: Props) { )} - {/* Progress bar between video and info */} - {videoUrl && duration > 0 && ( - - - - - )} + {/* Progress bar between video and info — always rendered to avoid height jump */} + + {videoUrl && duration > 0 && ( + <> + + + + )} + {/* Info */} diff --git a/components/LiveCard.tsx b/components/LiveCard.tsx index 24eb95d..08878f9 100644 --- a/components/LiveCard.tsx +++ b/components/LiveCard.tsx @@ -18,24 +18,35 @@ const CARD_WIDTH = (width - 14) / 2; interface Props { item: LiveRoom; + isLivePulse?: Boolean; onPress?: () => void; + fullWidth?: boolean; } -export function LiveCard({ item, onPress }: Props) { +export function LiveCard({ + item, + onPress, + fullWidth, + isLivePulse = false, +}: Props) { + const cardWidth = fullWidth ? width - 8 : CARD_WIDTH; return ( - + {isLivePulse && } 直播中 diff --git a/hooks/useVideoList.ts b/hooks/useVideoList.ts index cc7628a..ebb022f 100644 --- a/hooks/useVideoList.ts +++ b/hooks/useVideoList.ts @@ -28,7 +28,7 @@ export function useVideoList() { setPages(prev => reset ? [data] : [...prev, data]); if (reset || idx === 0) { // Take top 2 by online count - const sorted = [...live].sort((a, b) => b.online - a.online).slice(0, 2); + const sorted = [...live].sort((a, b) => b.online - a.online).slice(0, 10); setLiveRooms(sorted); } freshIdxRef.current = idx + 1; diff --git a/utils/videoRows.ts b/utils/videoRows.ts index 962b4c3..2d00450 100644 --- a/utils/videoRows.ts +++ b/utils/videoRows.ts @@ -21,7 +21,7 @@ export type ListRow = NormalRow | BigRow | LiveRow; export function toListRows(pages: VideoItem[][], liveRooms?: LiveRoom[]): ListRow[] { const rows: ListRow[] = []; - let liveInjected = false; + let roomIdx = 0; for (const chunk of pages) { if (chunk.length === 0) continue; @@ -37,26 +37,24 @@ export function toListRows(pages: VideoItem[][], liveRooms?: LiveRoom[]): ListRo const bigItem = chunk[bigIdx]; const rest = chunk.filter((_, i) => i !== bigIdx); - rows.push({ type: 'big', item: bigItem }); - - const pairs: NormalRow[] = []; + const pairs: (NormalRow | LiveRow)[] = []; for (let i = 0; i < rest.length; i += 2) { pairs.push({ type: 'pair', left: rest[i], right: rest[i + 1] ?? null }); } - // Inject LiveRow once, after ~3 pairs in the first chunk - if (!liveInjected && liveRooms && liveRooms.length > 0) { - const insertAt = Math.min(3, pairs.length); - const liveRow: LiveRow = { + // Inject 1 LiveRow per chunk at a deterministic position (seed = first video aid) + if (liveRooms && roomIdx < liveRooms.length && pairs.length > 0) { + const seed = chunk[0]?.aid ?? 0; + const insertAt = seed % (pairs.length + 1); + pairs.splice(insertAt, 0, { type: 'live', - left: liveRooms[0], - right: liveRooms[1], - }; - pairs.splice(insertAt, 0, liveRow as any); - liveInjected = true; + left: liveRooms[roomIdx], + }); + roomIdx++; } rows.push(...pairs); + rows.push({ type: 'big', item: bigItem }); } return rows; }