播放详情页歌词添加延迟滚动及着色动画

This commit is contained in:
lyswhut 2023-09-01 13:58:41 +08:00
parent 85a77adea1
commit 6b55871c1b
5 changed files with 127 additions and 16 deletions

View File

@ -22,6 +22,7 @@
- 调整桌面歌词主题配色,增强歌词字体阴影(#276 - 调整桌面歌词主题配色,增强歌词字体阴影(#276
- 优化数据传输逻辑,列表同步指令使用队列机制,保证列表同步操作的顺序 - 优化数据传输逻辑,列表同步指令使用队列机制,保证列表同步操作的顺序
- 暂停播放时播放详情歌词页不要自动滚动歌词回播放位置 - 暂停播放时播放详情歌词页不要自动滚动歌词回播放位置
- 播放详情页歌词添加延迟滚动及着色动画
### 修复 ### 修复

View File

@ -1,6 +1,8 @@
import { Text, type TextProps as _TextProps, StyleSheet } from 'react-native' import { type ComponentProps } from 'react'
import { Text, type TextProps as _TextProps, StyleSheet, Animated, type ColorValue, type TextStyle } from 'react-native'
import { useTheme } from '@/store/theme/hook' import { useTheme } from '@/store/theme/hook'
import { setSpText } from '@/utils/pixelRatio' import { setSpText } from '@/utils/pixelRatio'
import { useAnimateColor } from '@/utils/hooks/useAnimateColor'
// import { AppColors } from '@/theme' // import { AppColors } from '@/theme'
export interface TextProps extends _TextProps { export interface TextProps extends _TextProps {
@ -11,7 +13,7 @@ export interface TextProps extends _TextProps {
/** /**
* *
*/ */
color?: string color?: ColorValue
} }
export default ({ style, size = 15, color, children, ...props }: TextProps) => { export default ({ style, size = 15, color, children, ...props }: TextProps) => {
@ -25,3 +27,27 @@ export default ({ style, size = 15, color, children, ...props }: TextProps) => {
) )
} }
type _AnimatedTextProps = ComponentProps<(typeof Animated)['Text']>
export interface AnimatedColorTextProps extends _AnimatedTextProps {
/**
*
*/
size?: number
/**
*
*/
color?: string
}
export const AnimatedColorText = ({ style, size = 15, color: _color, children, ...props }: AnimatedColorTextProps) => {
const theme = useTheme()
const [color] = useAnimateColor(_color ?? theme['c-font'])
return (
<Animated.Text
style={StyleSheet.compose({ fontSize: setSpText(size), color: color as unknown as ColorValue }, style as TextStyle)}
{...props}
>{children}</Animated.Text>
)
}

View File

@ -6,7 +6,7 @@ import { createStyle } from '@/utils/tools'
// import { useComponentIds } from '@/store/common/hook' // import { useComponentIds } from '@/store/common/hook'
import { useTheme } from '@/store/theme/hook' import { useTheme } from '@/store/theme/hook'
import { useSettingValue } from '@/store/setting/hook' import { useSettingValue } from '@/store/setting/hook'
import Text from '@/components/common/Text' import { AnimatedColorText } from '@/components/common/Text'
import { setSpText } from '@/utils/pixelRatio' import { setSpText } from '@/utils/pixelRatio'
import playerState from '@/store/player/state' import playerState from '@/store/player/state'
// import { screenkeepAwake } from '@/utils/nativeModules/utils' // import { screenkeepAwake } from '@/utils/nativeModules/utils'
@ -27,22 +27,33 @@ const LrcLine = memo(({ line, lineNum, activeLine }: {
const size = lrcFontSize / 10 const size = lrcFontSize / 10
const lineHeight = setSpText(size) * 1.25 const lineHeight = setSpText(size) * 1.25
const colors = useMemo(() => {
const active = activeLine == lineNum
return active ? [
theme['c-primary'],
theme['c-primary-alpha-200'],
] : [
theme['c-350'],
theme['c-300'],
]
}, [activeLine, lineNum, theme])
// textBreakStrategy="simple" 用于解决某些设备上字体被截断的问题 // textBreakStrategy="simple" 用于解决某些设备上字体被截断的问题
// https://stackoverflow.com/a/72822360 // https://stackoverflow.com/a/72822360
return ( return (
<View style={styles.line}> <View style={styles.line}>
<Text style={{ <AnimatedColorText style={{
...styles.lineText, ...styles.lineText,
textAlign, textAlign,
lineHeight, lineHeight,
}} textBreakStrategy="simple" color={activeLine == lineNum ? theme['c-primary'] : theme['c-350']} size={size}>{line.text}</Text> }} textBreakStrategy="simple" color={colors[0]} size={size}>{line.text}</AnimatedColorText>
{ {
line.extendedLyrics.map((lrc, index) => { line.extendedLyrics.map((lrc, index) => {
return (<Text style={{ return (<AnimatedColorText style={{
...styles.lineTranslationText, ...styles.lineTranslationText,
textAlign, textAlign,
lineHeight: lineHeight * 0.8, lineHeight: lineHeight * 0.8,
}} textBreakStrategy="simple" key={index} color={activeLine == lineNum ? theme['c-primary-alpha-200'] : theme['c-350']} size={size * 0.8}>{lrc}</Text>) }} textBreakStrategy="simple" key={index} color={colors[1]} size={size * 0.8}>{lrc}</AnimatedColorText>)
}) })
} }
</View> </View>
@ -60,6 +71,7 @@ export default () => {
const flatListRef = useRef<FlatList>(null) const flatListRef = useRef<FlatList>(null)
const isPauseScrollRef = useRef(true) const isPauseScrollRef = useRef(true)
const scrollTimoutRef = useRef<NodeJS.Timeout | null>(null) const scrollTimoutRef = useRef<NodeJS.Timeout | null>(null)
const delayScrollTimeout = useRef<NodeJS.Timeout | null>(null)
const lineRef = useRef(0) const lineRef = useRef(0)
const isFirstSetLrc = useRef(true) const isFirstSetLrc = useRef(true)
// useLock() // useLock()
@ -90,7 +102,14 @@ export default () => {
const handleScrollBeginDrag = () => { const handleScrollBeginDrag = () => {
isPauseScrollRef.current = true isPauseScrollRef.current = true
if (scrollTimoutRef.current) clearTimeout(scrollTimoutRef.current) if (delayScrollTimeout.current) {
clearTimeout(delayScrollTimeout.current)
delayScrollTimeout.current = null
}
if (scrollTimoutRef.current) {
clearTimeout(scrollTimoutRef.current)
scrollTimoutRef.current = null
}
} }
const onScrollEndDrag = () => { const onScrollEndDrag = () => {
@ -107,6 +126,10 @@ export default () => {
useEffect(() => { useEffect(() => {
return () => { return () => {
if (delayScrollTimeout.current) {
clearTimeout(delayScrollTimeout.current)
delayScrollTimeout.current = null
}
if (scrollTimoutRef.current) { if (scrollTimoutRef.current) {
clearTimeout(scrollTimoutRef.current) clearTimeout(scrollTimoutRef.current)
scrollTimoutRef.current = null scrollTimoutRef.current = null
@ -135,7 +158,10 @@ export default () => {
useEffect(() => { useEffect(() => {
lineRef.current = line lineRef.current = line
if (!flatListRef.current || isPauseScrollRef.current) return if (!flatListRef.current || isPauseScrollRef.current) return
handleScrollToActive() delayScrollTimeout.current = setTimeout(() => {
delayScrollTimeout.current = null
handleScrollToActive()
}, 300)
}, [line]) }, [line])
const handleScrollToIndexFailed: FlatListType['onScrollToIndexFailed'] = (info) => { const handleScrollToIndexFailed: FlatListType['onScrollToIndexFailed'] = (info) => {

View File

@ -6,7 +6,7 @@ import { createStyle } from '@/utils/tools'
// import { useComponentIds } from '@/store/common/hook' // import { useComponentIds } from '@/store/common/hook'
import { useTheme } from '@/store/theme/hook' import { useTheme } from '@/store/theme/hook'
import { useSettingValue } from '@/store/setting/hook' import { useSettingValue } from '@/store/setting/hook'
import Text from '@/components/common/Text' import { AnimatedColorText } from '@/components/common/Text'
import { setSpText } from '@/utils/pixelRatio' import { setSpText } from '@/utils/pixelRatio'
import playerState from '@/store/player/state' import playerState from '@/store/player/state'
// import { screenkeepAwake } from '@/utils/nativeModules/utils' // import { screenkeepAwake } from '@/utils/nativeModules/utils'
@ -65,22 +65,34 @@ const LrcLine = memo(({ line, lineNum, activeLine }: {
const size = lrcFontSize / 10 const size = lrcFontSize / 10
const lineHeight = setSpText(size) * 1.25 const lineHeight = setSpText(size) * 1.25
const colors = useMemo(() => {
const active = activeLine == lineNum
return active ? [
theme['c-primary'],
theme['c-primary-alpha-200'],
] : [
theme['c-350'],
theme['c-300'],
]
}, [activeLine, lineNum, theme])
// textBreakStrategy="simple" 用于解决某些设备上字体被截断的问题 // textBreakStrategy="simple" 用于解决某些设备上字体被截断的问题
// https://stackoverflow.com/a/72822360 // https://stackoverflow.com/a/72822360
return ( return (
<View style={styles.line}> <View style={styles.line}>
<Text style={{ <AnimatedColorText style={{
...styles.lineText, ...styles.lineText,
textAlign, textAlign,
lineHeight, lineHeight,
}} textBreakStrategy="simple" color={activeLine == lineNum ? theme['c-primary'] : theme['c-350']} size={size}>{line.text}</Text> }} textBreakStrategy="simple" color={colors[0]} size={size}>{line.text}</AnimatedColorText>
{ {
line.extendedLyrics.map((lrc, index) => { line.extendedLyrics.map((lrc, index) => {
return (<Text style={{ return (<AnimatedColorText style={{
...styles.lineTranslationText, ...styles.lineTranslationText,
textAlign, textAlign,
lineHeight: lineHeight * 0.8, lineHeight: lineHeight * 0.8,
}} textBreakStrategy="simple" key={index} color={activeLine == lineNum ? theme['c-primary-alpha-200'] : theme['c-350']} size={size * 0.8}>{lrc}</Text>) }} textBreakStrategy="simple" key={index} color={colors[1]} size={size * 0.8}>{lrc}</AnimatedColorText>)
}) })
} }
</View> </View>
@ -98,6 +110,7 @@ export default () => {
const flatListRef = useRef<FlatList>(null) const flatListRef = useRef<FlatList>(null)
const isPauseScrollRef = useRef(true) const isPauseScrollRef = useRef(true)
const scrollTimoutRef = useRef<NodeJS.Timeout | null>(null) const scrollTimoutRef = useRef<NodeJS.Timeout | null>(null)
const delayScrollTimeout = useRef<NodeJS.Timeout | null>(null)
const lineRef = useRef(0) const lineRef = useRef(0)
const isFirstSetLrc = useRef(true) const isFirstSetLrc = useRef(true)
// useLock() // useLock()
@ -128,7 +141,14 @@ export default () => {
const handleScrollBeginDrag = () => { const handleScrollBeginDrag = () => {
isPauseScrollRef.current = true isPauseScrollRef.current = true
if (scrollTimoutRef.current) clearTimeout(scrollTimoutRef.current) if (delayScrollTimeout.current) {
clearTimeout(delayScrollTimeout.current)
delayScrollTimeout.current = null
}
if (scrollTimoutRef.current) {
clearTimeout(scrollTimoutRef.current)
scrollTimoutRef.current = null
}
} }
const onScrollEndDrag = () => { const onScrollEndDrag = () => {
@ -145,6 +165,10 @@ export default () => {
useEffect(() => { useEffect(() => {
return () => { return () => {
if (delayScrollTimeout.current) {
clearTimeout(delayScrollTimeout.current)
delayScrollTimeout.current = null
}
if (scrollTimoutRef.current) { if (scrollTimoutRef.current) {
clearTimeout(scrollTimoutRef.current) clearTimeout(scrollTimoutRef.current)
scrollTimoutRef.current = null scrollTimoutRef.current = null
@ -173,7 +197,10 @@ export default () => {
useEffect(() => { useEffect(() => {
lineRef.current = line lineRef.current = line
if (!flatListRef.current || isPauseScrollRef.current) return if (!flatListRef.current || isPauseScrollRef.current) return
handleScrollToActive() delayScrollTimeout.current = setTimeout(() => {
delayScrollTimeout.current = null
handleScrollToActive()
}, 300)
}, [line]) }, [line])
const handleScrollToIndexFailed: FlatListType['onScrollToIndexFailed'] = (info) => { const handleScrollToIndexFailed: FlatListType['onScrollToIndexFailed'] = (info) => {

View File

@ -0,0 +1,31 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { Animated } from 'react-native'
const ANIMATION_DURATION = 200
export const useAnimateColor = (color: string) => {
const anim = useMemo(() => new Animated.Value(0), [color])
const [finished, setFinished] = useState(true)
const currentColor = useRef(color)
const nextColor = useMemo(() => color, [color])
const animColor = anim.interpolate({
inputRange: [0, 1],
outputRange: [currentColor.current, nextColor],
})
useEffect(() => {
setFinished(false)
Animated.timing(anim, {
toValue: 1,
duration: ANIMATION_DURATION,
useNativeDriver: false,
}).start(() => {
currentColor.current = nextColor
setFinished(true)
})
}, [nextColor])
return [animColor, finished] as const
}