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