1import React, {memo, useLayoutEffect, useMemo} from 'react'; 2import {StyleSheet, Text, View} from 'react-native'; 3import rpx from '@/utils/rpx'; 4import FastImage from '../base/fastImage'; 5import {ImgAsset} from '@/constants/assetsConst'; 6import Color from 'color'; 7import ThemeText from '../base/themeText'; 8import useColors from '@/hooks/useColors'; 9import {ROUTE_PATH, useNavigate} from '@/core/router'; 10import {Gesture, GestureDetector} from 'react-native-gesture-handler'; 11import TrackPlayer from '@/core/trackPlayer'; 12import Animated, { 13 runOnJS, 14 SharedValue, 15 useAnimatedStyle, 16 useSharedValue, 17 withTiming, 18} from 'react-native-reanimated'; 19import {useSafeAreaInsets} from 'react-native-safe-area-context'; 20import {timingConfig} from '@/constants/commonConst'; 21 22interface IBarMusicItemProps { 23 musicItem: IMusic.IMusicItem | null; 24 activeIndex: number; // 当前展示的是0/1/2 25 transformSharedValue: SharedValue<number>; 26} 27function _BarMusicItem(props: IBarMusicItemProps) { 28 const {musicItem, activeIndex, transformSharedValue} = props; 29 const colors = useColors(); 30 const safeAreaInsets = useSafeAreaInsets(); 31 32 const animatedStyles = useAnimatedStyle(() => { 33 return { 34 left: `${(transformSharedValue.value + activeIndex) * 100}%`, 35 }; 36 }, [activeIndex]); 37 38 if (!musicItem) { 39 return null; 40 } 41 42 return ( 43 <Animated.View 44 style={[ 45 styles.container, 46 { 47 paddingLeft: rpx(24) + safeAreaInsets.left, 48 }, 49 animatedStyles, 50 ]}> 51 <FastImage 52 style={styles.artworkImg} 53 uri={musicItem.artwork} 54 emptySrc={ImgAsset.albumDefault} 55 /> 56 <Text 57 ellipsizeMode="tail" 58 accessible={false} 59 style={styles.textWrapper} 60 numberOfLines={1}> 61 <ThemeText fontSize="content" fontColor="musicBarText"> 62 {musicItem?.title} 63 </ThemeText> 64 {musicItem?.artist && ( 65 <ThemeText 66 fontSize="description" 67 color={Color(colors.musicBarText) 68 .alpha(0.6) 69 .toString()}> 70 {' '} 71 -{musicItem.artist} 72 </ThemeText> 73 )} 74 </Text> 75 </Animated.View> 76 ); 77} 78 79const BarMusicItem = memo( 80 _BarMusicItem, 81 (prev, curr) => 82 prev.musicItem === curr.musicItem && 83 prev.activeIndex === curr.activeIndex, 84); 85 86const styles = StyleSheet.create({ 87 container: { 88 flexDirection: 'row', 89 width: '100%', 90 alignItems: 'center', 91 position: 'absolute', 92 }, 93 textWrapper: { 94 flexGrow: 1, 95 flexShrink: 1, 96 }, 97 artworkImg: { 98 width: rpx(96), 99 height: rpx(96), 100 borderRadius: rpx(48), 101 marginRight: rpx(24), 102 }, 103}); 104 105interface IMusicInfoProps { 106 musicItem: IMusic.IMusicItem | null; 107 paddingLeft?: number; 108} 109 110function skipMusicItem(direction: number) { 111 if (direction === -1) { 112 TrackPlayer.skipToNext(); 113 } else if (direction === 1) { 114 TrackPlayer.skipToPrevious(); 115 } 116} 117 118export default function MusicInfo(props: IMusicInfoProps) { 119 const {musicItem} = props; 120 const navigate = useNavigate(); 121 const playLists = TrackPlayer.usePlayList(); 122 const siblingMusicItems = useMemo(() => { 123 if (!musicItem) { 124 return { 125 prev: null, 126 next: null, 127 }; 128 } 129 return { 130 prev: TrackPlayer.getPreviousMusic(), 131 next: TrackPlayer.getNextMusic(), 132 }; 133 }, [musicItem, playLists]); 134 135 // +- 1 136 const transformSharedValue = useSharedValue(0); 137 138 const musicItemWidthValue = useSharedValue(0); 139 140 const tapGesture = Gesture.Tap() 141 .onStart(() => { 142 navigate(ROUTE_PATH.MUSIC_DETAIL); 143 }) 144 .runOnJS(true); 145 146 useLayoutEffect(() => { 147 transformSharedValue.value = 0; 148 }, [musicItem]); 149 150 const panGesture = Gesture.Pan() 151 .minPointers(1) 152 .maxPointers(1) 153 .onUpdate(e => { 154 if (musicItemWidthValue.value) { 155 transformSharedValue.value = 156 e.translationX / musicItemWidthValue.value; 157 } 158 }) 159 .onEnd((e, success) => { 160 if (!success) { 161 // 还原到原始位置 162 transformSharedValue.value = withTiming( 163 0, 164 timingConfig.animationFast, 165 ); 166 } else { 167 // fling 168 const deltaX = e.translationX; 169 const vX = e.velocityX; 170 171 let skip = 0; 172 if (musicItemWidthValue.value) { 173 const rate = deltaX / musicItemWidthValue.value; 174 175 if (Math.abs(rate) > 0.3) { 176 // 先判断距离 177 skip = vX > 0 ? 1 : -1; 178 transformSharedValue.value = withTiming( 179 skip, 180 timingConfig.animationFast, 181 () => { 182 runOnJS(skipMusicItem)(skip); 183 }, 184 ); 185 } else if (Math.abs(vX) > 1500) { 186 // 再判断速度 187 skip = vX > 0 ? 1 : -1; 188 transformSharedValue.value = skip; 189 runOnJS(skipMusicItem)(skip); 190 } else { 191 transformSharedValue.value = withTiming( 192 0, 193 timingConfig.animationFast, 194 ); 195 } 196 } else { 197 transformSharedValue.value = 0; 198 } 199 } 200 }); 201 202 const gesture = Gesture.Race(panGesture, tapGesture); 203 204 return ( 205 <GestureDetector gesture={gesture}> 206 <View 207 style={musicInfoStyles.infoContainer} 208 onLayout={e => { 209 musicItemWidthValue.value = e.nativeEvent.layout.width; 210 }}> 211 <BarMusicItem 212 transformSharedValue={transformSharedValue} 213 musicItem={siblingMusicItems.prev} 214 activeIndex={-1} 215 /> 216 <BarMusicItem 217 transformSharedValue={transformSharedValue} 218 musicItem={musicItem} 219 activeIndex={0} 220 /> 221 <BarMusicItem 222 transformSharedValue={transformSharedValue} 223 musicItem={siblingMusicItems.next} 224 activeIndex={1} 225 /> 226 </View> 227 </GestureDetector> 228 ); 229} 230 231const musicInfoStyles = StyleSheet.create({ 232 infoContainer: { 233 flex: 1, 234 height: '100%', 235 alignItems: 'center', 236 flexDirection: 'row', 237 overflow: 'hidden', 238 }, 239}); 240