xref: /MusicFree/src/pages/musicDetail/components/content/lyric/index.tsx (revision 095287552b9baf2f2ceeb9397c563c292a4f7934)
1import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
2import {LayoutRectangle, StyleSheet, Text, View} from 'react-native';
3import rpx from '@/utils/rpx';
4import useDelayFalsy from '@/hooks/useDelayFalsy';
5import {FlatList, TapGestureHandler} from 'react-native-gesture-handler';
6import {fontSizeConst} from '@/constants/uiConst';
7import {IconButtonWithGesture} from '@/components/base/iconButton';
8import Loading from '@/components/base/loading';
9import globalStyle from '@/constants/globalStyle';
10import {showPanel} from '@/components/panels/usePanel';
11import LyricManager from '@/core/lyricManager';
12import TrackPlayer from '@/core/trackPlayer';
13import {musicIsPaused} from '@/utils/trackUtils';
14import delay from '@/utils/delay';
15import DraggingTime from './draggingTime';
16import LyricItemComponent from './lyricItem';
17
18const ITEM_HEIGHT = rpx(92);
19
20interface IItemHeights {
21    blankHeight?: number;
22    [k: number]: number;
23}
24
25export default function Lyric() {
26    const {loading, meta, lyrics: lyric} = LyricManager.useLyricState();
27    const currentLrcItem = LyricManager.useCurrentLyric();
28
29    // 是否展示拖拽
30    const dragShownRef = useRef(false);
31    const [draggingIndex, setDraggingIndex, setDraggingIndexImmi] =
32        useDelayFalsy<number | undefined>(undefined, 2000);
33    const listRef = useRef<FlatList<ILyric.IParsedLrcItem> | null>();
34    const musicState = TrackPlayer.useMusicState();
35
36    const [layout, setLayout] = useState<LayoutRectangle>();
37
38    // 用来缓存高度
39    const itemHeightsRef = useRef<IItemHeights>({});
40
41    // 设置空白组件,获取组件高度
42    const blankComponent = useMemo(() => {
43        return (
44            <View
45                style={styles.empty}
46                onLayout={evt => {
47                    itemHeightsRef.current.blankHeight =
48                        evt.nativeEvent.layout.height;
49                }}
50            />
51        );
52    }, []);
53
54    const handleLyricItemLayout = useCallback(
55        (index: number, height: number) => {
56            itemHeightsRef.current[index] = height;
57        },
58        [],
59    );
60
61    useEffect(() => {
62        // 暂停且拖拽才返回
63        if (
64            lyric.length === 0 ||
65            draggingIndex !== undefined ||
66            (draggingIndex === undefined && musicIsPaused(musicState)) ||
67            lyric[lyric.length - 1].time < 1
68        ) {
69            return;
70        }
71        if (currentLrcItem?.index === -1 || !currentLrcItem) {
72            listRef.current?.scrollToIndex({
73                index: 0,
74                viewPosition: 0.5,
75            });
76        } else {
77            listRef.current?.scrollToIndex({
78                index: Math.min(currentLrcItem.index ?? 0, lyric.length - 1),
79                viewPosition: 0.5,
80            });
81        }
82        // 音乐暂停状态不应该影响到滑动,所以不放在依赖里,但是这样写不好。。
83    }, [currentLrcItem, lyric, draggingIndex]);
84
85    useEffect(() => {
86        if (currentLrcItem?.index !== undefined && currentLrcItem.index > -1) {
87            listRef.current?.scrollToIndex({
88                index: currentLrcItem.index,
89                viewPosition: 0.5,
90                animated: false,
91            });
92        }
93    }, []);
94
95    // 开始滚动时拖拽生效
96    const onScrollBeginDrag = () => {
97        dragShownRef.current = true;
98    };
99
100    const onScrollEndDrag = async () => {
101        if (draggingIndex !== undefined) {
102            setDraggingIndex(undefined);
103        }
104        dragShownRef.current = false;
105    };
106
107    const onScroll = (e: any) => {
108        if (dragShownRef.current) {
109            const offset =
110                e.nativeEvent.contentOffset.y +
111                e.nativeEvent.layoutMeasurement.height / 2;
112
113            const itemHeights = itemHeightsRef.current;
114            let height = itemHeights.blankHeight!;
115            if (offset <= height) {
116                setDraggingIndex(0);
117                return;
118            }
119            for (let i = 0; i < lyric.length; ++i) {
120                height += itemHeights[i] ?? 0;
121                if (height > offset) {
122                    setDraggingIndex(i);
123                    return;
124                }
125            }
126        }
127    };
128
129    const onLyricSeekPress = async () => {
130        if (draggingIndex !== undefined) {
131            const time = lyric[draggingIndex].time + +(meta?.offset ?? 0);
132            if (time !== undefined && !isNaN(time)) {
133                await TrackPlayer.seekTo(time);
134                await TrackPlayer.play();
135                setDraggingIndexImmi(undefined);
136            }
137        }
138    };
139
140    return (
141        <View style={globalStyle.fwflex1}>
142            {loading ? (
143                <Loading color="white" />
144            ) : lyric?.length ? (
145                <FlatList
146                    ref={_ => {
147                        listRef.current = _;
148                    }}
149                    onLayout={e => {
150                        setLayout(e.nativeEvent.layout);
151                    }}
152                    viewabilityConfig={{
153                        itemVisiblePercentThreshold: 100,
154                    }}
155                    onScrollToIndexFailed={({index}) => {
156                        delay(120).then(() => {
157                            listRef.current?.scrollToIndex({
158                                index: Math.min(index ?? 0, lyric.length - 1),
159                                viewPosition: 0.5,
160                            });
161                        });
162                    }}
163                    fadingEdgeLength={120}
164                    ListHeaderComponent={blankComponent}
165                    ListFooterComponent={blankComponent}
166                    onScrollBeginDrag={onScrollBeginDrag}
167                    onMomentumScrollEnd={onScrollEndDrag}
168                    onScroll={onScroll}
169                    scrollEventThrottle={32}
170                    style={styles.wrapper}
171                    data={lyric}
172                    initialNumToRender={30}
173                    overScrollMode="never"
174                    extraData={currentLrcItem}
175                    renderItem={({item, index}) => (
176                        <LyricItemComponent
177                            index={index}
178                            text={item.lrc}
179                            onLayout={handleLyricItemLayout}
180                            light={draggingIndex === index}
181                            highlight={currentLrcItem?.index === index}
182                        />
183                    )}
184                />
185            ) : (
186                <View style={globalStyle.fullCenter}>
187                    <Text style={styles.white}>暂无歌词</Text>
188                    <TapGestureHandler
189                        onActivated={() => {
190                            showPanel('SearchLrc', {
191                                musicItem: TrackPlayer.getCurrentMusic(),
192                            });
193                        }}>
194                        <Text style={styles.searchLyric}>搜索歌词</Text>
195                    </TapGestureHandler>
196                </View>
197            )}
198            {draggingIndex !== undefined && (
199                <View
200                    style={[
201                        styles.draggingTime,
202                        layout?.height
203                            ? {
204                                  top: (layout.height - ITEM_HEIGHT) / 2,
205                              }
206                            : null,
207                    ]}>
208                    <DraggingTime
209                        time={
210                            (lyric[draggingIndex]?.time ?? 0) +
211                            +(meta?.offset ?? 0)
212                        }
213                    />
214                    <View style={styles.singleLine} />
215
216                    <IconButtonWithGesture
217                        style={styles.playIcon}
218                        sizeType="small"
219                        name="play"
220                        onPress={onLyricSeekPress}
221                    />
222                </View>
223            )}
224        </View>
225    );
226}
227
228const styles = StyleSheet.create({
229    wrapper: {
230        width: '100%',
231        marginVertical: rpx(48),
232        flex: 1,
233    },
234    empty: {
235        paddingTop: '70%',
236    },
237    white: {
238        color: 'white',
239    },
240    draggingTime: {
241        position: 'absolute',
242        width: '100%',
243        height: ITEM_HEIGHT,
244        top: '40%',
245        marginTop: rpx(48),
246        paddingHorizontal: rpx(18),
247        right: 0,
248        flexDirection: 'row',
249        alignItems: 'center',
250        justifyContent: 'space-between',
251    },
252    draggingTimeText: {
253        color: '#dddddd',
254        fontSize: fontSizeConst.description,
255        width: rpx(90),
256    },
257    singleLine: {
258        width: '67%',
259        height: 1,
260        backgroundColor: '#cccccc',
261        opacity: 0.4,
262    },
263    playIcon: {
264        width: rpx(90),
265        textAlign: 'right',
266        color: 'white',
267    },
268    searchLyric: {
269        width: rpx(180),
270        marginTop: rpx(14),
271        paddingVertical: rpx(10),
272        textAlign: 'center',
273        alignSelf: 'center',
274        color: '#66eeff',
275        textDecorationLine: 'underline',
276    },
277});
278