xref: /MusicFree/src/pages/musicDetail/components/content/lyric/index.tsx (revision 13cebe639f366cc764bcbd619c248f8037fbbb8f)
18d82ecd9S猫头猫import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
28d82ecd9S猫头猫import {LayoutRectangle, StyleSheet, Text, View} from 'react-native';
38d82ecd9S猫头猫import rpx from '@/utils/rpx';
48d82ecd9S猫头猫import useDelayFalsy from '@/hooks/useDelayFalsy';
5*13cebe63S猫头猫import {
6*13cebe63S猫头猫    FlatList,
7*13cebe63S猫头猫    Gesture,
8*13cebe63S猫头猫    GestureDetector,
9*13cebe63S猫头猫    TapGestureHandler,
10*13cebe63S猫头猫} from 'react-native-gesture-handler';
118d82ecd9S猫头猫import {fontSizeConst} from '@/constants/uiConst';
128d82ecd9S猫头猫import {IconButtonWithGesture} from '@/components/base/iconButton';
138d82ecd9S猫头猫import Loading from '@/components/base/loading';
148d82ecd9S猫头猫import globalStyle from '@/constants/globalStyle';
158d82ecd9S猫头猫import {showPanel} from '@/components/panels/usePanel';
168d82ecd9S猫头猫import LyricManager from '@/core/lyricManager';
178d82ecd9S猫头猫import TrackPlayer from '@/core/trackPlayer';
188d82ecd9S猫头猫import {musicIsPaused} from '@/utils/trackUtils';
198d82ecd9S猫头猫import delay from '@/utils/delay';
208d82ecd9S猫头猫import DraggingTime from './draggingTime';
218d82ecd9S猫头猫import LyricItemComponent from './lyricItem';
22*13cebe63S猫头猫import PersistStatus from '@/core/persistStatus';
23*13cebe63S猫头猫import LyricOperations from './lyricOperations';
248d82ecd9S猫头猫
258d82ecd9S猫头猫const ITEM_HEIGHT = rpx(92);
268d82ecd9S猫头猫
278d82ecd9S猫头猫interface IItemHeights {
288d82ecd9S猫头猫    blankHeight?: number;
298d82ecd9S猫头猫    [k: number]: number;
308d82ecd9S猫头猫}
318d82ecd9S猫头猫
32*13cebe63S猫头猫interface IProps {
33*13cebe63S猫头猫    onTurnPageClick?: () => void;
34*13cebe63S猫头猫}
35*13cebe63S猫头猫
36*13cebe63S猫头猫export default function Lyric(props: IProps) {
37*13cebe63S猫头猫    const {onTurnPageClick} = props;
38*13cebe63S猫头猫
397e883dbbS猫头猫    const {loading, meta, lyrics, translationLyrics, hasTranslation} =
407e883dbbS猫头猫        LyricManager.useLyricState();
418d82ecd9S猫头猫    const currentLrcItem = LyricManager.useCurrentLyric();
42*13cebe63S猫头猫    const showTranslation = PersistStatus.useValue(
43*13cebe63S猫头猫        'lyric.showTranslation',
44*13cebe63S猫头猫        false,
45*13cebe63S猫头猫    );
468d82ecd9S猫头猫
478d82ecd9S猫头猫    // 是否展示拖拽
488d82ecd9S猫头猫    const dragShownRef = useRef(false);
498d82ecd9S猫头猫    const [draggingIndex, setDraggingIndex, setDraggingIndexImmi] =
508d82ecd9S猫头猫        useDelayFalsy<number | undefined>(undefined, 2000);
518d82ecd9S猫头猫    const listRef = useRef<FlatList<ILyric.IParsedLrcItem> | null>();
528d82ecd9S猫头猫    const musicState = TrackPlayer.useMusicState();
538d82ecd9S猫头猫
548d82ecd9S猫头猫    const [layout, setLayout] = useState<LayoutRectangle>();
55*13cebe63S猫头猫    const isMountedRef = useRef(true);
568d82ecd9S猫头猫
578d82ecd9S猫头猫    // 用来缓存高度
588d82ecd9S猫头猫    const itemHeightsRef = useRef<IItemHeights>({});
598d82ecd9S猫头猫
608d82ecd9S猫头猫    // 设置空白组件,获取组件高度
618d82ecd9S猫头猫    const blankComponent = useMemo(() => {
628d82ecd9S猫头猫        return (
638d82ecd9S猫头猫            <View
647aed04d4S猫头猫                style={styles.empty}
658d82ecd9S猫头猫                onLayout={evt => {
668d82ecd9S猫头猫                    itemHeightsRef.current.blankHeight =
678d82ecd9S猫头猫                        evt.nativeEvent.layout.height;
688d82ecd9S猫头猫                }}
698d82ecd9S猫头猫            />
708d82ecd9S猫头猫        );
718d82ecd9S猫头猫    }, []);
728d82ecd9S猫头猫
738d82ecd9S猫头猫    const handleLyricItemLayout = useCallback(
748d82ecd9S猫头猫        (index: number, height: number) => {
758d82ecd9S猫头猫            itemHeightsRef.current[index] = height;
768d82ecd9S猫头猫        },
778d82ecd9S猫头猫        [],
788d82ecd9S猫头猫    );
798d82ecd9S猫头猫
80*13cebe63S猫头猫    // 滚到当前item
81*13cebe63S猫头猫    const scrollToCurrentLrcItem = useCallback(() => {
82*13cebe63S猫头猫        if (!listRef.current) {
83*13cebe63S猫头猫            return;
84*13cebe63S猫头猫        }
85*13cebe63S猫头猫        const currentLrcItem = LyricManager.getCurrentLyric();
86*13cebe63S猫头猫        const lyrics = LyricManager.getLyricState().lyrics;
87*13cebe63S猫头猫        if (currentLrcItem?.index === -1 || !currentLrcItem) {
88*13cebe63S猫头猫            listRef.current?.scrollToIndex({
89*13cebe63S猫头猫                index: 0,
90*13cebe63S猫头猫                viewPosition: 0.5,
91*13cebe63S猫头猫            });
92*13cebe63S猫头猫        } else {
93*13cebe63S猫头猫            listRef.current?.scrollToIndex({
94*13cebe63S猫头猫                index: Math.min(currentLrcItem.index ?? 0, lyrics.length - 1),
95*13cebe63S猫头猫                viewPosition: 0.5,
96*13cebe63S猫头猫            });
97*13cebe63S猫头猫        }
98*13cebe63S猫头猫    }, []);
99*13cebe63S猫头猫
100*13cebe63S猫头猫    const delayedScrollToCurrentLrcItem = useMemo(() => {
101*13cebe63S猫头猫        let sto: number;
102*13cebe63S猫头猫
103*13cebe63S猫头猫        return () => {
104*13cebe63S猫头猫            if (sto) {
105*13cebe63S猫头猫                clearTimeout(sto);
106*13cebe63S猫头猫            }
107*13cebe63S猫头猫            sto = setTimeout(() => {
108*13cebe63S猫头猫                if (isMountedRef.current) {
109*13cebe63S猫头猫                    scrollToCurrentLrcItem();
110*13cebe63S猫头猫                }
111*13cebe63S猫头猫            }, 200) as any;
112*13cebe63S猫头猫        };
113*13cebe63S猫头猫    }, []);
114*13cebe63S猫头猫
1158d82ecd9S猫头猫    useEffect(() => {
1168d82ecd9S猫头猫        // 暂停且拖拽才返回
1178d82ecd9S猫头猫        if (
1187e883dbbS猫头猫            lyrics.length === 0 ||
1198d82ecd9S猫头猫            draggingIndex !== undefined ||
1208d82ecd9S猫头猫            (draggingIndex === undefined && musicIsPaused(musicState)) ||
1217e883dbbS猫头猫            lyrics[lyrics.length - 1].time < 1
1228d82ecd9S猫头猫        ) {
1238d82ecd9S猫头猫            return;
1248d82ecd9S猫头猫        }
1258d82ecd9S猫头猫        if (currentLrcItem?.index === -1 || !currentLrcItem) {
1268d82ecd9S猫头猫            listRef.current?.scrollToIndex({
1278d82ecd9S猫头猫                index: 0,
1288d82ecd9S猫头猫                viewPosition: 0.5,
1298d82ecd9S猫头猫            });
1308d82ecd9S猫头猫        } else {
1318d82ecd9S猫头猫            listRef.current?.scrollToIndex({
1327e883dbbS猫头猫                index: Math.min(currentLrcItem.index ?? 0, lyrics.length - 1),
1338d82ecd9S猫头猫                viewPosition: 0.5,
1348d82ecd9S猫头猫            });
1358d82ecd9S猫头猫        }
1368d82ecd9S猫头猫        // 音乐暂停状态不应该影响到滑动,所以不放在依赖里,但是这样写不好。。
1377e883dbbS猫头猫    }, [currentLrcItem, lyrics, draggingIndex]);
1388d82ecd9S猫头猫
139a48a4bb0S猫头猫    useEffect(() => {
140*13cebe63S猫头猫        scrollToCurrentLrcItem();
141*13cebe63S猫头猫        return () => {
142*13cebe63S猫头猫            isMountedRef.current = false;
143*13cebe63S猫头猫        };
144a48a4bb0S猫头猫    }, []);
145a48a4bb0S猫头猫
1468d82ecd9S猫头猫    // 开始滚动时拖拽生效
1478d82ecd9S猫头猫    const onScrollBeginDrag = () => {
1488d82ecd9S猫头猫        dragShownRef.current = true;
1498d82ecd9S猫头猫    };
1508d82ecd9S猫头猫
1518d82ecd9S猫头猫    const onScrollEndDrag = async () => {
1528d82ecd9S猫头猫        if (draggingIndex !== undefined) {
1538d82ecd9S猫头猫            setDraggingIndex(undefined);
1548d82ecd9S猫头猫        }
1558d82ecd9S猫头猫        dragShownRef.current = false;
1568d82ecd9S猫头猫    };
1578d82ecd9S猫头猫
1588d82ecd9S猫头猫    const onScroll = (e: any) => {
1598d82ecd9S猫头猫        if (dragShownRef.current) {
1608d82ecd9S猫头猫            const offset =
1618d82ecd9S猫头猫                e.nativeEvent.contentOffset.y +
1628d82ecd9S猫头猫                e.nativeEvent.layoutMeasurement.height / 2;
1638d82ecd9S猫头猫
1648d82ecd9S猫头猫            const itemHeights = itemHeightsRef.current;
1658d82ecd9S猫头猫            let height = itemHeights.blankHeight!;
1668d82ecd9S猫头猫            if (offset <= height) {
1678d82ecd9S猫头猫                setDraggingIndex(0);
1688d82ecd9S猫头猫                return;
1698d82ecd9S猫头猫            }
1707e883dbbS猫头猫            for (let i = 0; i < lyrics.length; ++i) {
1718d82ecd9S猫头猫                height += itemHeights[i] ?? 0;
1728d82ecd9S猫头猫                if (height > offset) {
1738d82ecd9S猫头猫                    setDraggingIndex(i);
1748d82ecd9S猫头猫                    return;
1758d82ecd9S猫头猫                }
1768d82ecd9S猫头猫            }
1778d82ecd9S猫头猫        }
1788d82ecd9S猫头猫    };
1798d82ecd9S猫头猫
1808d82ecd9S猫头猫    const onLyricSeekPress = async () => {
1818d82ecd9S猫头猫        if (draggingIndex !== undefined) {
1827e883dbbS猫头猫            const time = lyrics[draggingIndex].time + +(meta?.offset ?? 0);
1838d82ecd9S猫头猫            if (time !== undefined && !isNaN(time)) {
1848d82ecd9S猫头猫                await TrackPlayer.seekTo(time);
1858d82ecd9S猫头猫                await TrackPlayer.play();
1868d82ecd9S猫头猫                setDraggingIndexImmi(undefined);
1878d82ecd9S猫头猫            }
1888d82ecd9S猫头猫        }
1898d82ecd9S猫头猫    };
1908d82ecd9S猫头猫
191*13cebe63S猫头猫    const tap = Gesture.Tap()
192*13cebe63S猫头猫        .onStart(() => {
193*13cebe63S猫头猫            onTurnPageClick?.();
194*13cebe63S猫头猫        })
195*13cebe63S猫头猫        .runOnJS(true);
196*13cebe63S猫头猫
1978d82ecd9S猫头猫    return (
198*13cebe63S猫头猫        <>
199*13cebe63S猫头猫            <GestureDetector gesture={tap}>
2008d82ecd9S猫头猫                <View style={globalStyle.fwflex1}>
2018d82ecd9S猫头猫                    {loading ? (
2028d82ecd9S猫头猫                        <Loading color="white" />
2037e883dbbS猫头猫                    ) : lyrics?.length ? (
2048d82ecd9S猫头猫                        <FlatList
2058d82ecd9S猫头猫                            ref={_ => {
2068d82ecd9S猫头猫                                listRef.current = _;
2078d82ecd9S猫头猫                            }}
2088d82ecd9S猫头猫                            onLayout={e => {
2098d82ecd9S猫头猫                                setLayout(e.nativeEvent.layout);
2108d82ecd9S猫头猫                            }}
2118d82ecd9S猫头猫                            viewabilityConfig={{
2128d82ecd9S猫头猫                                itemVisiblePercentThreshold: 100,
2138d82ecd9S猫头猫                            }}
2148d82ecd9S猫头猫                            onScrollToIndexFailed={({index}) => {
2158d82ecd9S猫头猫                                delay(120).then(() => {
2168d82ecd9S猫头猫                                    listRef.current?.scrollToIndex({
217*13cebe63S猫头猫                                        index: Math.min(
218*13cebe63S猫头猫                                            index ?? 0,
219*13cebe63S猫头猫                                            lyrics.length - 1,
220*13cebe63S猫头猫                                        ),
2218d82ecd9S猫头猫                                        viewPosition: 0.5,
2228d82ecd9S猫头猫                                    });
2238d82ecd9S猫头猫                                });
2248d82ecd9S猫头猫                            }}
2258d82ecd9S猫头猫                            fadingEdgeLength={120}
2268d82ecd9S猫头猫                            ListHeaderComponent={blankComponent}
2278d82ecd9S猫头猫                            ListFooterComponent={blankComponent}
2288d82ecd9S猫头猫                            onScrollBeginDrag={onScrollBeginDrag}
2298d82ecd9S猫头猫                            onMomentumScrollEnd={onScrollEndDrag}
2308d82ecd9S猫头猫                            onScroll={onScroll}
2318d82ecd9S猫头猫                            scrollEventThrottle={32}
2327aed04d4S猫头猫                            style={styles.wrapper}
2337e883dbbS猫头猫                            data={lyrics}
2348d82ecd9S猫头猫                            initialNumToRender={30}
2358d82ecd9S猫头猫                            overScrollMode="never"
2368d82ecd9S猫头猫                            extraData={currentLrcItem}
2377e883dbbS猫头猫                            renderItem={({item, index}) => {
2387e883dbbS猫头猫                                let text = item.lrc;
2397e883dbbS猫头猫
2407e883dbbS猫头猫                                if (showTranslation && hasTranslation) {
241*13cebe63S猫头猫                                    const transLrc =
242*13cebe63S猫头猫                                        translationLyrics?.[index]?.lrc;
2437e883dbbS猫头猫                                    if (transLrc) {
2447e883dbbS猫头猫                                        text += `\n${transLrc}`;
2457e883dbbS猫头猫                                    }
2467e883dbbS猫头猫                                }
2477e883dbbS猫头猫
2487e883dbbS猫头猫                                return (
2498d82ecd9S猫头猫                                    <LyricItemComponent
2508d82ecd9S猫头猫                                        index={index}
2517e883dbbS猫头猫                                        text={text}
2528d82ecd9S猫头猫                                        onLayout={handleLyricItemLayout}
2538d82ecd9S猫头猫                                        light={draggingIndex === index}
254*13cebe63S猫头猫                                        highlight={
255*13cebe63S猫头猫                                            currentLrcItem?.index === index
256*13cebe63S猫头猫                                        }
2578d82ecd9S猫头猫                                    />
2587e883dbbS猫头猫                                );
2597e883dbbS猫头猫                            }}
2608d82ecd9S猫头猫                        />
2618d82ecd9S猫头猫                    ) : (
2628d82ecd9S猫头猫                        <View style={globalStyle.fullCenter}>
2637aed04d4S猫头猫                            <Text style={styles.white}>暂无歌词</Text>
2648d82ecd9S猫头猫                            <TapGestureHandler
2658d82ecd9S猫头猫                                onActivated={() => {
2668d82ecd9S猫头猫                                    showPanel('SearchLrc', {
267*13cebe63S猫头猫                                        musicItem:
268*13cebe63S猫头猫                                            TrackPlayer.getCurrentMusic(),
2698d82ecd9S猫头猫                                    });
2708d82ecd9S猫头猫                                }}>
2717aed04d4S猫头猫                                <Text style={styles.searchLyric}>搜索歌词</Text>
2728d82ecd9S猫头猫                            </TapGestureHandler>
2738d82ecd9S猫头猫                        </View>
2748d82ecd9S猫头猫                    )}
2758d82ecd9S猫头猫                    {draggingIndex !== undefined && (
2768d82ecd9S猫头猫                        <View
2778d82ecd9S猫头猫                            style={[
2787aed04d4S猫头猫                                styles.draggingTime,
2798d82ecd9S猫头猫                                layout?.height
2808d82ecd9S猫头猫                                    ? {
281*13cebe63S猫头猫                                          top:
282*13cebe63S猫头猫                                              (layout.height - ITEM_HEIGHT) / 2,
2838d82ecd9S猫头猫                                      }
2848d82ecd9S猫头猫                                    : null,
2858d82ecd9S猫头猫                            ]}>
2868d82ecd9S猫头猫                            <DraggingTime
2878d82ecd9S猫头猫                                time={
2887e883dbbS猫头猫                                    (lyrics[draggingIndex]?.time ?? 0) +
2898d82ecd9S猫头猫                                    +(meta?.offset ?? 0)
2908d82ecd9S猫头猫                                }
2918d82ecd9S猫头猫                            />
2927aed04d4S猫头猫                            <View style={styles.singleLine} />
2938d82ecd9S猫头猫
2948d82ecd9S猫头猫                            <IconButtonWithGesture
2957aed04d4S猫头猫                                style={styles.playIcon}
2968d82ecd9S猫头猫                                sizeType="small"
2978d82ecd9S猫头猫                                name="play"
2988d82ecd9S猫头猫                                onPress={onLyricSeekPress}
2998d82ecd9S猫头猫                            />
3008d82ecd9S猫头猫                        </View>
3018d82ecd9S猫头猫                    )}
3028d82ecd9S猫头猫                </View>
303*13cebe63S猫头猫            </GestureDetector>
304*13cebe63S猫头猫            <LyricOperations
305*13cebe63S猫头猫                scrollToCurrentLrcItem={delayedScrollToCurrentLrcItem}
306*13cebe63S猫头猫            />
307*13cebe63S猫头猫        </>
3088d82ecd9S猫头猫    );
3098d82ecd9S猫头猫}
3108d82ecd9S猫头猫
3117aed04d4S猫头猫const styles = StyleSheet.create({
3128d82ecd9S猫头猫    wrapper: {
3138d82ecd9S猫头猫        width: '100%',
3148d82ecd9S猫头猫        marginVertical: rpx(48),
3158d82ecd9S猫头猫        flex: 1,
3168d82ecd9S猫头猫    },
3178d82ecd9S猫头猫    empty: {
3188d82ecd9S猫头猫        paddingTop: '70%',
3198d82ecd9S猫头猫    },
3208d82ecd9S猫头猫    white: {
3218d82ecd9S猫头猫        color: 'white',
3228d82ecd9S猫头猫    },
3238d82ecd9S猫头猫    draggingTime: {
3248d82ecd9S猫头猫        position: 'absolute',
3258d82ecd9S猫头猫        width: '100%',
3268d82ecd9S猫头猫        height: ITEM_HEIGHT,
3278d82ecd9S猫头猫        top: '40%',
3288d82ecd9S猫头猫        marginTop: rpx(48),
3298d82ecd9S猫头猫        paddingHorizontal: rpx(18),
3308d82ecd9S猫头猫        right: 0,
3318d82ecd9S猫头猫        flexDirection: 'row',
3328d82ecd9S猫头猫        alignItems: 'center',
3338d82ecd9S猫头猫        justifyContent: 'space-between',
3348d82ecd9S猫头猫    },
3358d82ecd9S猫头猫    draggingTimeText: {
3368d82ecd9S猫头猫        color: '#dddddd',
3378d82ecd9S猫头猫        fontSize: fontSizeConst.description,
3388d82ecd9S猫头猫        width: rpx(90),
3398d82ecd9S猫头猫    },
3408d82ecd9S猫头猫    singleLine: {
3418d82ecd9S猫头猫        width: '67%',
3428d82ecd9S猫头猫        height: 1,
3438d82ecd9S猫头猫        backgroundColor: '#cccccc',
3448d82ecd9S猫头猫        opacity: 0.4,
3458d82ecd9S猫头猫    },
3468d82ecd9S猫头猫    playIcon: {
3478d82ecd9S猫头猫        width: rpx(90),
3488d82ecd9S猫头猫        textAlign: 'right',
3498d82ecd9S猫头猫        color: 'white',
3508d82ecd9S猫头猫    },
3518d82ecd9S猫头猫    searchLyric: {
3528d82ecd9S猫头猫        width: rpx(180),
3538d82ecd9S猫头猫        marginTop: rpx(14),
3548d82ecd9S猫头猫        paddingVertical: rpx(10),
3558d82ecd9S猫头猫        textAlign: 'center',
3568d82ecd9S猫头猫        alignSelf: 'center',
3578d82ecd9S猫头猫        color: '#66eeff',
3588d82ecd9S猫头猫        textDecorationLine: 'underline',
3598d82ecd9S猫头猫    },
3608d82ecd9S猫头猫});
361