xref: /MusicFree/src/components/musicBar/musicInfo.tsx (revision ed067386d74ad02ad6f817b5bcbae7b589b9e81f)
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