diff --git a/publish/changeLog.md b/publish/changeLog.md index 6569a27..e47d606 100644 --- a/publish/changeLog.md +++ b/publish/changeLog.md @@ -12,6 +12,7 @@ - 新增列表设置-是否显示歌曲专辑名,默认关闭 - 新增列表设置-是否显示歌曲时长,默认开启 +- 新增是否允许通过歌词调整播放进度功能,默认关闭,可到播放详情页右上角设置开启 ### 优化 diff --git a/src/config/defaultSetting.ts b/src/config/defaultSetting.ts index d0fb754..c8b5bb0 100644 --- a/src/config/defaultSetting.ts +++ b/src/config/defaultSetting.ts @@ -31,6 +31,7 @@ const defaultSetting: LX.AppSetting = { 'playDetail.style.align': 'center', 'playDetail.vertical.style.lrcFontSize': 176, 'playDetail.horizontal.style.lrcFontSize': 180, + 'playDetail.isShowLyricProgressSetting': false, 'desktopLyric.enable': false, 'desktopLyric.isLock': false, diff --git a/src/lang/en_us.json b/src/lang/en_us.json index 4ff2288..b5dad42 100644 --- a/src/lang/en_us.json +++ b/src/lang/en_us.json @@ -115,6 +115,7 @@ "play_detail_setting_lrc_font_size": "Lyric font size", "play_detail_setting_playback_rate": "Playback rate", "play_detail_setting_playback_rate_reset": "reset", + "play_detail_setting_show_lyric_progress_setting": "Allows to adjust playback progress by lyrics", "play_detail_setting_title": "Player settings", "play_detail_setting_volume": "Volume", "play_detail_todo_tip": "What do you want? No, this function has not been implemented yet 😛, But you can try to locate the currently playing song by long pressing (only valid for playing songs in \"My List\")", diff --git a/src/lang/zh_cn.json b/src/lang/zh_cn.json index 66b971f..b018be6 100644 --- a/src/lang/zh_cn.json +++ b/src/lang/zh_cn.json @@ -115,6 +115,7 @@ "play_detail_setting_lrc_font_size": "歌词字体大小", "play_detail_setting_playback_rate": "播放速率", "play_detail_setting_playback_rate_reset": "重置", + "play_detail_setting_show_lyric_progress_setting": "允许通过歌词调整播放进度", "play_detail_setting_title": "播放器设置", "play_detail_setting_volume": "音量大小", "play_detail_todo_tip": "你想干嘛?不可以的,这个功能还没有实现哦😛,不过你可以试着长按来定位当前播放的歌曲(仅对播放“我的列表”里的歌曲有效哦)", diff --git a/src/screens/PlayDetail/Horizontal/Lyric.tsx b/src/screens/PlayDetail/Horizontal/Lyric.tsx index fe0cff9..94fc794 100644 --- a/src/screens/PlayDetail/Horizontal/Lyric.tsx +++ b/src/screens/PlayDetail/Horizontal/Lyric.tsx @@ -1,5 +1,5 @@ -import { memo, useMemo, useEffect, useRef } from 'react' -import { View, FlatList, type FlatListProps } from 'react-native' +import { memo, useMemo, useEffect, useRef, useCallback } from 'react' +import { View, FlatList, type FlatListProps, type NativeSyntheticEvent, type NativeScrollEvent, type LayoutChangeEvent } from 'react-native' // import { useLayout } from '@/utils/hooks' import { type Line, useLrcPlay, useLrcSet } from '@/plugins/lyric' import { createStyle } from '@/utils/tools' @@ -9,18 +9,21 @@ import { useSettingValue } from '@/store/setting/hook' import { AnimatedColorText } from '@/components/common/Text' import { setSpText } from '@/utils/pixelRatio' import playerState from '@/store/player/state' +import { scrollTo } from '@/utils/scroll' +import PlayLine, { type PlayLineType } from '../components/PlayLine' // import { screenkeepAwake } from '@/utils/nativeModules/utils' // import { log } from '@/utils/log' // import { toast } from '@/utils/tools' type FlatListType = FlatListProps - -const LrcLine = memo(({ line, lineNum, activeLine }: { +interface LineProps { line: Line lineNum: number activeLine: number -}) => { + onLayout: (lineNum: number, height: number, width: number) => void +} +const LrcLine = memo(({ line, lineNum, activeLine, onLayout }: LineProps) => { const theme = useTheme() const lrcFontSize = useSettingValue('playDetail.horizontal.style.lrcFontSize') const textAlign = useSettingValue('playDetail.style.align') @@ -38,10 +41,14 @@ const LrcLine = memo(({ line, lineNum, activeLine }: { ] }, [activeLine, lineNum, theme]) + const handleLayout = ({ nativeEvent }: LayoutChangeEvent) => { + onLayout(lineNum, nativeEvent.layout.height, nativeEvent.layout.width) + } + // textBreakStrategy="simple" 用于解决某些设备上字体被截断的问题 // https://stackoverflow.com/a/72822360 return ( - + { const lyricLines = useLrcSet() const { line } = useLrcPlay() const flatListRef = useRef(null) + const playLineRef = useRef(null) const isPauseScrollRef = useRef(true) const scrollTimoutRef = useRef(null) const delayScrollTimeout = useRef(null) - const lineRef = useRef(0) + const lineRef = useRef({ line: 0, prevLine: 0 }) const isFirstSetLrc = useRef(true) + const scrollInfoRef = useRef['nativeEvent'] | null>(null) + const listLayoutInfoRef = useRef<{ spaceHeight: number, lineHeights: number[] }>({ spaceHeight: 0, lineHeights: [] }) + const scrollCancelRef = useRef<(() => void) | null>(null) + const isShowLyricProgressSetting = useSettingValue('playDetail.isShowLyricProgressSetting') // useLock() // const [imgUrl, setImgUrl] = useState(null) // const theme = useGetter('common', 'theme') @@ -87,21 +99,45 @@ export default () => { // }, [playMusicInfo]) // const imgWidth = useMemo(() => layout.width * 0.75, [layout.width]) - const handleScrollToActive = (index = lineRef.current) => { + const handleScrollToActive = (index = lineRef.current.line) => { if (index < 0) return if (flatListRef.current) { - try { - flatListRef.current.scrollToIndex({ - index, - animated: true, - viewPosition: 0.4, - }) - } catch {} + // console.log('handleScrollToActive', index) + if (scrollCancelRef.current) { + scrollCancelRef.current() + scrollCancelRef.current = null + } + if (scrollInfoRef.current && lineRef.current.line - lineRef.current.prevLine == 1) { + let offset = listLayoutInfoRef.current.spaceHeight + for (let line = 0; line < index; line++) { + offset += listLayoutInfoRef.current.lineHeights[line] + } + try { + scrollCancelRef.current = scrollTo(flatListRef.current, scrollInfoRef.current, offset - scrollInfoRef.current.layoutMeasurement.height * 0.36, 300, () => { + scrollCancelRef.current = null + }) + } catch {} + } else { + try { + flatListRef.current.scrollToIndex({ + index, + animated: true, + viewPosition: 0.4, + }) + } catch {} + } } } + const handleScroll = ({ nativeEvent }: NativeSyntheticEvent) => { + scrollInfoRef.current = nativeEvent + if (isPauseScrollRef.current) { + playLineRef.current?.updateScrollInfo(nativeEvent) + } + } const handleScrollBeginDrag = () => { isPauseScrollRef.current = true + playLineRef.current?.setVisible(true) if (delayScrollTimeout.current) { clearTimeout(delayScrollTimeout.current) delayScrollTimeout.current = null @@ -116,6 +152,7 @@ export default () => { if (!isPauseScrollRef.current) return if (scrollTimoutRef.current) clearTimeout(scrollTimoutRef.current) scrollTimoutRef.current = setTimeout(() => { + playLineRef.current?.setVisible(false) scrollTimoutRef.current = null isPauseScrollRef.current = false if (!playerState.isPlay) return @@ -139,25 +176,43 @@ export default () => { useEffect(() => { // linesRef.current = lyricLines + listLayoutInfoRef.current.lineHeights = [] + lineRef.current.prevLine = 0 + lineRef.current.line = 0 if (!flatListRef.current) return flatListRef.current.scrollToOffset({ offset: 0, animated: false, }) - if (isFirstSetLrc.current) { - isFirstSetLrc.current = false - setTimeout(() => { - isPauseScrollRef.current = false - handleScrollToActive() - }, 100) - } else { - handleScrollToActive(0) - } + if (!lyricLines.length) return + playLineRef.current?.updateLyricLines(lyricLines) + requestAnimationFrame(() => { + if (isFirstSetLrc.current) { + isFirstSetLrc.current = false + setTimeout(() => { + isPauseScrollRef.current = false + handleScrollToActive() + }, 100) + } else { + if (delayScrollTimeout.current) clearTimeout(delayScrollTimeout.current) + delayScrollTimeout.current = setTimeout(() => { + handleScrollToActive(0) + }, 100) + } + }) }, [lyricLines]) useEffect(() => { - lineRef.current = line + if (line < 0) return + lineRef.current.prevLine = lineRef.current.line + lineRef.current.line = line if (!flatListRef.current || isPauseScrollRef.current) return + + if (line - lineRef.current.prevLine != 1) { + handleScrollToActive() + return + } + delayScrollTimeout.current = setTimeout(() => { delayScrollTimeout.current = null handleScrollToActive() @@ -165,39 +220,57 @@ export default () => { }, [line]) const handleScrollToIndexFailed: FlatListType['onScrollToIndexFailed'] = (info) => { - // console.log(info) void wait().then(() => { handleScrollToActive(info.index) }) } + const handleLineLayout = useCallback((lineNum, height) => { + listLayoutInfoRef.current.lineHeights[lineNum] = height + playLineRef.current?.updateLayoutInfo(listLayoutInfoRef.current) + }, []) + + const handleSpaceLayout = useCallback(({ nativeEvent }: LayoutChangeEvent) => { + listLayoutInfoRef.current.spaceHeight = nativeEvent.layout.height + playLineRef.current?.updateLayoutInfo(listLayoutInfoRef.current) + }, []) + + const handlePlayLine = useCallback((time: number) => { + playLineRef.current?.setVisible(false) + global.app_event.setProgress(time) + }, []) + const renderItem: FlatListType['renderItem'] = ({ item, index }) => { return ( - + ) } const getkey: FlatListType['keyExtractor'] = (item, index) => `${index}${item.text}` const spaceComponent = useMemo(() => ( - - ), []) + + ), [handleSpaceLayout]) return ( - + <> + + { isShowLyricProgressSetting ? : null } + ) } diff --git a/src/screens/PlayDetail/Vertical/Lyric.tsx b/src/screens/PlayDetail/Vertical/Lyric.tsx index 8a24ac4..01447f9 100644 --- a/src/screens/PlayDetail/Vertical/Lyric.tsx +++ b/src/screens/PlayDetail/Vertical/Lyric.tsx @@ -1,5 +1,5 @@ -import { memo, useMemo, useEffect, useRef } from 'react' -import { View, FlatList, type FlatListProps } from 'react-native' +import { memo, useMemo, useEffect, useRef, useCallback } from 'react' +import { View, FlatList, type FlatListProps, type LayoutChangeEvent, type NativeSyntheticEvent, type NativeScrollEvent } from 'react-native' // import { useLayout } from '@/utils/hooks' import { type Line, useLrcPlay, useLrcSet } from '@/plugins/lyric' import { createStyle } from '@/utils/tools' @@ -9,6 +9,8 @@ import { useSettingValue } from '@/store/setting/hook' import { AnimatedColorText } from '@/components/common/Text' import { setSpText } from '@/utils/pixelRatio' import playerState from '@/store/player/state' +import { scrollTo } from '@/utils/scroll' +import PlayLine, { type PlayLineType } from '../components/PlayLine' // import { screenkeepAwake } from '@/utils/nativeModules/utils' // import { log } from '@/utils/log' // import { toast } from '@/utils/tools' @@ -54,11 +56,13 @@ type FlatListType = FlatListProps // }, []) // } -const LrcLine = memo(({ line, lineNum, activeLine }: { +interface LineProps { line: Line lineNum: number activeLine: number -}) => { + onLayout: (lineNum: number, height: number, width: number) => void +} +const LrcLine = memo(({ line, lineNum, activeLine, onLayout }: LineProps) => { const theme = useTheme() const lrcFontSize = useSettingValue('playDetail.vertical.style.lrcFontSize') const textAlign = useSettingValue('playDetail.style.align') @@ -76,11 +80,15 @@ const LrcLine = memo(({ line, lineNum, activeLine }: { ] }, [activeLine, lineNum, theme]) + const handleLayout = ({ nativeEvent }: LayoutChangeEvent) => { + onLayout(lineNum, nativeEvent.layout.height, nativeEvent.layout.width) + } + // textBreakStrategy="simple" 用于解决某些设备上字体被截断的问题 // https://stackoverflow.com/a/72822360 return ( - + { const lyricLines = useLrcSet() const { line } = useLrcPlay() const flatListRef = useRef(null) + const playLineRef = useRef(null) const isPauseScrollRef = useRef(true) const scrollTimoutRef = useRef(null) const delayScrollTimeout = useRef(null) - const lineRef = useRef(0) + const lineRef = useRef({ line: 0, prevLine: 0 }) const isFirstSetLrc = useRef(true) + const scrollInfoRef = useRef['nativeEvent'] | null>(null) + const listLayoutInfoRef = useRef<{ spaceHeight: number, lineHeights: number[] }>({ spaceHeight: 0, lineHeights: [] }) + const scrollCancelRef = useRef<(() => void) | null>(null) + const isShowLyricProgressSetting = useSettingValue('playDetail.isShowLyricProgressSetting') // useLock() // const [imgUrl, setImgUrl] = useState(null) // const theme = useGetter('common', 'theme') @@ -126,21 +139,45 @@ export default () => { // }, [playMusicInfo]) // const imgWidth = useMemo(() => layout.width * 0.75, [layout.width]) - const handleScrollToActive = (index = lineRef.current) => { + const handleScrollToActive = (index = lineRef.current.line) => { if (index < 0) return if (flatListRef.current) { - try { - flatListRef.current.scrollToIndex({ - index, - animated: true, - viewPosition: 0.4, - }) - } catch {} + // console.log('handleScrollToActive', index) + if (scrollCancelRef.current) { + scrollCancelRef.current() + scrollCancelRef.current = null + } + if (scrollInfoRef.current && lineRef.current.line - lineRef.current.prevLine == 1) { + let offset = listLayoutInfoRef.current.spaceHeight + for (let line = 0; line < index; line++) { + offset += listLayoutInfoRef.current.lineHeights[line] + } + try { + scrollCancelRef.current = scrollTo(flatListRef.current, scrollInfoRef.current, offset - scrollInfoRef.current.layoutMeasurement.height * 0.36, 300, () => { + scrollCancelRef.current = null + }) + } catch {} + } else { + try { + flatListRef.current.scrollToIndex({ + index, + animated: true, + viewPosition: 0.4, + }) + } catch {} + } } } + const handleScroll = ({ nativeEvent }: NativeSyntheticEvent) => { + scrollInfoRef.current = nativeEvent + if (isPauseScrollRef.current) { + playLineRef.current?.updateScrollInfo(nativeEvent) + } + } const handleScrollBeginDrag = () => { isPauseScrollRef.current = true + playLineRef.current?.setVisible(true) if (delayScrollTimeout.current) { clearTimeout(delayScrollTimeout.current) delayScrollTimeout.current = null @@ -155,6 +192,7 @@ export default () => { if (!isPauseScrollRef.current) return if (scrollTimoutRef.current) clearTimeout(scrollTimoutRef.current) scrollTimoutRef.current = setTimeout(() => { + playLineRef.current?.setVisible(false) scrollTimoutRef.current = null isPauseScrollRef.current = false if (!playerState.isPlay) return @@ -178,25 +216,43 @@ export default () => { useEffect(() => { // linesRef.current = lyricLines + listLayoutInfoRef.current.lineHeights = [] + lineRef.current.prevLine = 0 + lineRef.current.line = 0 if (!flatListRef.current) return flatListRef.current.scrollToOffset({ offset: 0, animated: false, }) - if (isFirstSetLrc.current) { - isFirstSetLrc.current = false - setTimeout(() => { - isPauseScrollRef.current = false - handleScrollToActive() - }, 100) - } else { - handleScrollToActive(0) - } + if (!lyricLines.length) return + playLineRef.current?.updateLyricLines(lyricLines) + requestAnimationFrame(() => { + if (isFirstSetLrc.current) { + isFirstSetLrc.current = false + setTimeout(() => { + isPauseScrollRef.current = false + handleScrollToActive() + }, 100) + } else { + if (delayScrollTimeout.current) clearTimeout(delayScrollTimeout.current) + delayScrollTimeout.current = setTimeout(() => { + handleScrollToActive(0) + }, 100) + } + }) }, [lyricLines]) useEffect(() => { - lineRef.current = line + if (line < 0) return + lineRef.current.prevLine = lineRef.current.line + lineRef.current.line = line if (!flatListRef.current || isPauseScrollRef.current) return + + if (line - lineRef.current.prevLine != 1) { + handleScrollToActive() + return + } + delayScrollTimeout.current = setTimeout(() => { delayScrollTimeout.current = null handleScrollToActive() @@ -204,39 +260,57 @@ export default () => { }, [line]) const handleScrollToIndexFailed: FlatListType['onScrollToIndexFailed'] = (info) => { - // console.log(info) void wait().then(() => { handleScrollToActive(info.index) }) } + const handleLineLayout = useCallback((lineNum, height) => { + listLayoutInfoRef.current.lineHeights[lineNum] = height + playLineRef.current?.updateLayoutInfo(listLayoutInfoRef.current) + }, []) + + const handleSpaceLayout = useCallback(({ nativeEvent }: LayoutChangeEvent) => { + listLayoutInfoRef.current.spaceHeight = nativeEvent.layout.height + playLineRef.current?.updateLayoutInfo(listLayoutInfoRef.current) + }, []) + + const handlePlayLine = useCallback((time: number) => { + playLineRef.current?.setVisible(false) + global.app_event.setProgress(time) + }, []) + const renderItem: FlatListType['renderItem'] = ({ item, index }) => { return ( - + ) } const getkey: FlatListType['keyExtractor'] = (item, index) => `${index}${item.text}` const spaceComponent = useMemo(() => ( - - ), []) + + ), [handleSpaceLayout]) return ( - + <> + + { isShowLyricProgressSetting ? : null } + ) } @@ -251,8 +325,8 @@ const styles = createStyle({ paddingTop: '80%', }, line: { - marginTop: 10, - marginBottom: 10, + paddingTop: 10, + paddingBottom: 10, // opacity: 0, }, lineText: { diff --git a/src/screens/PlayDetail/components/PlayLine.tsx b/src/screens/PlayDetail/components/PlayLine.tsx new file mode 100644 index 0000000..1ba95b8 --- /dev/null +++ b/src/screens/PlayDetail/components/PlayLine.tsx @@ -0,0 +1,136 @@ +import { forwardRef, useImperativeHandle, useRef, useState } from 'react' +import { type NativeScrollEvent, type NativeSyntheticEvent, View, TouchableOpacity, Animated } from 'react-native' +import Text from '@/components/common/Text' +import { createStyle } from '@/utils/tools' +import { type Lines } from 'lrc-file-parser' +import { useTheme } from '@/store/theme/hook' +import { BorderWidths } from '@/theme' +import { formatPlayTime2 } from '@/utils' +import { Icon } from '@/components/common/Icon' + + +export interface PlayLineType { + updateScrollInfo: (scrollInfo: NativeSyntheticEvent['nativeEvent'] | null) => void + updateLayoutInfo: (listLayoutInfo: { spaceHeight: number, lineHeights: number[] }) => void + updateLyricLines: (lyricLines: Lines) => void + setVisible: (visible: boolean) => void +} + +export interface PlayLineProps { + onPlayLine: (time: number) => void +} + +const ANIMATION_DURATION = 300 + +export default forwardRef(({ onPlayLine }, ref) => { + const theme = useTheme() + const [scrollInfo, setScrollInfo] = useState['nativeEvent'] | null>(null) + const [listLayoutInfo, setListLayoutInfo] = useState<{ spaceHeight: number, lineHeights: number[] }>({ spaceHeight: 0, lineHeights: [] }) + const [lyricLines, setLyricLines] = useState([]) + const [visible, setVisible] = useState(false) + const opsAnim = useRef( + new Animated.Value(0), + ).current + + const setShow = (visible: boolean) => { + Animated.timing(opsAnim, { + toValue: visible ? 1 : 0, + duration: ANIMATION_DURATION, + useNativeDriver: true, + }).start(() => { + if (!visible) setVisible(false) + }) + } + + useImperativeHandle(ref, () => ({ + updateScrollInfo(scrollInfo) { + setScrollInfo(scrollInfo) + }, + updateLayoutInfo(listLayoutInfo) { + setListLayoutInfo(listLayoutInfo) + }, + updateLyricLines(lyricLines) { + setLyricLines(lyricLines) + }, + setVisible(visible) { + if (visible) { + setVisible(true) + } + requestAnimationFrame(() => { + setShow(visible) + }) + // setVisible() + }, + }), []) + + const handlePlayLine = () => { + onPlayLine(time / 1000) + } + + if (!scrollInfo || !visible) return null + const offset = scrollInfo.contentOffset.y + scrollInfo.layoutMeasurement.height * 0.4 + let lineOffset = listLayoutInfo.spaceHeight + let targetLineNum = -1 + for (let line = 0; line < listLayoutInfo.lineHeights.length; line++) { + lineOffset += listLayoutInfo.lineHeights[line] + if (lineOffset < offset) continue + targetLineNum = line + break + } + if (targetLineNum == -1) targetLineNum = listLayoutInfo.lineHeights.length - 1 + const time = lyricLines[targetLineNum]?.time ?? 0 + const timeLabel = formatPlayTime2(time / 1000) + return ( + + {timeLabel} + + + + + + + + ) +}) + +const styles = createStyle({ + playLine: { + position: 'absolute', + width: '100%', + top: '40%', + left: 0, + height: 2, + // paddingTop: 5, + // paddingBottom: 5, + // backgroundColor: 'rgba(0,0,0,0.1)', + }, + label: { + position: 'absolute', + right: 37, + bottom: 0, + flexDirection: 'row', + alignItems: 'center', + gap: 5, + }, + lineContent: { + // backgroundColor: 'rgba(0,0,0,0.1)', + position: 'absolute', + width: '100%', + height: 20, + top: -10, + flexDirection: 'row', + alignItems: 'center', + gap: 5, + }, + line: { + marginLeft: 15, + borderBottomWidth: BorderWidths.normal, + borderStyle: 'dashed', + flex: 1, + }, + button: { + flex: 0, + paddingLeft: 5, + paddingRight: 15, + }, +}) diff --git a/src/screens/PlayDetail/components/SettingPopup/index.tsx b/src/screens/PlayDetail/components/SettingPopup/index.tsx index db9684f..833ca59 100644 --- a/src/screens/PlayDetail/components/SettingPopup/index.tsx +++ b/src/screens/PlayDetail/components/SettingPopup/index.tsx @@ -3,6 +3,7 @@ import { ScrollView, View } from 'react-native' import Popup, { type PopupType, type PopupProps } from '@/components/common/Popup' import { useI18n } from '@/lang' +import SettingLyricProgress from './settings/SettingLyricProgress' import SettingVolume from './settings/SettingVolume' import SettingPlaybackRate from './settings/SettingPlaybackRate' import SettingLrcFontSize from './settings/SettingLrcFontSize' @@ -41,6 +42,7 @@ export default forwardRef(({ direction, ... true}> + diff --git a/src/screens/PlayDetail/components/SettingPopup/settings/SettingLyricProgress.tsx b/src/screens/PlayDetail/components/SettingPopup/settings/SettingLyricProgress.tsx new file mode 100644 index 0000000..68a6556 --- /dev/null +++ b/src/screens/PlayDetail/components/SettingPopup/settings/SettingLyricProgress.tsx @@ -0,0 +1,28 @@ +import { View } from 'react-native' +// import Text from '@/components/common/Text' +import { useSettingValue } from '@/store/setting/hook' +import { updateSetting } from '@/core/common' +import { useI18n } from '@/lang' +import CheckBox from '@/components/common/CheckBox' +import styles from './style' + + +export default () => { + const t = useI18n() + const isShowLyricProgressSetting = useSettingValue('playDetail.isShowLyricProgressSetting') + const setShowLyricProgressSetting = (showLyricProgressSetting: boolean) => { + updateSetting({ 'playDetail.isShowLyricProgressSetting': showLyricProgressSetting }) + } + + return ( + + + + + + + + + ) +} + diff --git a/src/types/app_setting.d.ts b/src/types/app_setting.d.ts index ebe8963..9b6788d 100644 --- a/src/types/app_setting.d.ts +++ b/src/types/app_setting.d.ts @@ -171,6 +171,11 @@ declare global { */ 'playDetail.horizontal.style.lrcFontSize': number + /** + * 播放详情页-是否允许通过歌词调整播放进度 + */ + 'playDetail.isShowLyricProgressSetting': boolean + /** * 是否启用桌面歌词 */ diff --git a/src/utils/scroll.ts b/src/utils/scroll.ts new file mode 100644 index 0000000..b578af0 --- /dev/null +++ b/src/utils/scroll.ts @@ -0,0 +1,93 @@ +import { type NativeScrollEvent, type FlatList, type NativeSyntheticEvent } from 'react-native' + +const easeInOutQuad = (t: number, b: number, c: number, d: number): number => { + t /= d / 2 + if (t < 1) return (c / 2) * t * t + b + t-- + return (-c / 2) * (t * (t - 2) - 1) + b +} + +type Noop = () => void +const noop: Noop = () => {} + +const handleScrollY = (element: FlatList, info: NativeSyntheticEvent['nativeEvent'], to: number, duration = 300, fn = noop): Noop => { + if (element == null) { + fn() + return noop + } + + const key = Math.random() + + + const start = info.contentOffset.y + let cancel = false + if (to > start) { + let maxScrollTop = info.contentSize.height - info.layoutMeasurement.height + if (to > maxScrollTop) to = maxScrollTop + } else if (to < start) { + if (to < 0) to = 0 + } else { + fn() + return noop + } + const change = to - start + const increment = 10 + if (!change) { + fn() + return noop + } + + let currentTime = 0 + let val + + const animateScroll = () => { + // @ts-expect-error + if (cancel || element.lx_scrollKey != key) { + fn() + return + } + currentTime += increment + val = Math.trunc(easeInOutQuad(currentTime, start, change, duration)) + element.scrollToOffset({ offset: val, animated: false }) + if (currentTime < duration) { + setTimeout(animateScroll, increment) + } else { + fn() + } + } + // @ts-expect-error + element.lx_scrollKey = key + requestAnimationFrame(() => { + animateScroll() + }) + + return () => { + cancel = true + } +} +/** + * 设置滚动条位置 + * @param element 要设置滚动的容器 dom + * @param to 滚动的目标位置 + * @param duration 滚动完成时间 ms + * @param fn 滚动完成后的回调 + * @param delay 延迟执行时间 + */ +export const scrollTo = (element: FlatList, info: NativeSyntheticEvent['nativeEvent'], to: number, duration = 300, fn = () => {}, delay = 0): () => void => { + let cancelFn: () => void + let timeout: NodeJS.Timeout | null + if (delay) { + let scrollCancelFn: Noop + cancelFn = () => { + timeout == null ? scrollCancelFn?.() : clearTimeout(timeout) + } + timeout = setTimeout(() => { + timeout = null + scrollCancelFn = handleScrollY(element, info, to, duration, fn) + }, delay) + } else { + cancelFn = handleScrollY(element, info, to, duration, fn) ?? noop + } + return cancelFn +} +