mirror of
https://github.com/ikun0014/lx-music-mobile.git
synced 2025-07-05 04:28:55 +08:00
播放详情页歌词添加延迟滚动及着色动画
This commit is contained in:
parent
85a77adea1
commit
6b55871c1b
@ -22,6 +22,7 @@
|
|||||||
- 调整桌面歌词主题配色,增强歌词字体阴影(#276)
|
- 调整桌面歌词主题配色,增强歌词字体阴影(#276)
|
||||||
- 优化数据传输逻辑,列表同步指令使用队列机制,保证列表同步操作的顺序
|
- 优化数据传输逻辑,列表同步指令使用队列机制,保证列表同步操作的顺序
|
||||||
- 暂停播放时播放详情歌词页不要自动滚动歌词回播放位置
|
- 暂停播放时播放详情歌词页不要自动滚动歌词回播放位置
|
||||||
|
- 播放详情页歌词添加延迟滚动及着色动画
|
||||||
|
|
||||||
### 修复
|
### 修复
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -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) => {
|
||||||
|
@ -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) => {
|
||||||
|
31
src/utils/hooks/useAnimateColor.ts
Normal file
31
src/utils/hooks/useAnimateColor.ts
Normal 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
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user