Skip to content

Commit 8f27fa6

Browse files
dylanjeffersclaude
andauthored
feat(mobile): now playing indicator with animated eq bars (#14369)
## Summary - Adds a now-playing indicator to track rows in playlist / track list views on mobile - Active track row now uses a subtle purple background tint (`rgba(130,86,220,0.07)`) and a purple title color (`#8256DC`) - While the track is playing, the artwork shows an animated equalizer overlay (4 bars, `#CC5DE8`, height ~4→18px, staggered durations / delays) using React Native `Animated` with `useNativeDriver: false` - New reusable `AnimatedEqBars` component lives next to `TrackArtwork` in `packages/mobile/src/components/track-list/` The paused-but-active state (active row, not currently playing) still shows the existing play-icon overlay — only the playing state swaps in the equalizer animation. ## Files changed - `packages/mobile/src/components/track-list/AnimatedEqBars.tsx` (new) - `packages/mobile/src/components/track-list/TrackArtwork.tsx` - `packages/mobile/src/components/track-list/TrackListItem.tsx` ## Test plan - [ ] Open a playlist on iOS, tap a track to play it - [ ] That row gets a purple tint and the title turns purple - [ ] Bars animate over the artwork (4 bars, anchored bottom-center, staggered) - [ ] Pause playback - [ ] Row stays purple-tinted; artwork shows the play icon (no animation) - [ ] Skip to another track in the same list - [ ] Indicator follows to the new row; old row reverts to default styling - [ ] Same checks on Android - [ ] Reorderable playlist (drag mode): tinted row still renders correctly - [ ] Locked / unlisted / deleted tracks remain unaffected when not active Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f3d55fa commit 8f27fa6

3 files changed

Lines changed: 111 additions & 12 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { useEffect, useRef } from 'react'
2+
3+
import { Animated, Easing, StyleSheet, View } from 'react-native'
4+
5+
const BAR_COUNT = 4
6+
const BAR_MIN_HEIGHT = 4
7+
const BAR_MAX_HEIGHT = 18
8+
const BAR_COLOR = '#CC5DE8'
9+
10+
const BAR_DURATIONS_MS = [520, 410, 640, 470]
11+
const BAR_DELAYS_MS = [0, 180, 90, 260]
12+
13+
type AnimatedEqBarsProps = {
14+
isPlaying: boolean
15+
}
16+
17+
export const AnimatedEqBars = ({ isPlaying }: AnimatedEqBarsProps) => {
18+
const heights = useRef(
19+
Array.from({ length: BAR_COUNT }, () => new Animated.Value(BAR_MIN_HEIGHT))
20+
).current
21+
22+
useEffect(() => {
23+
if (!isPlaying) {
24+
heights.forEach((h) => h.stopAnimation())
25+
return
26+
}
27+
28+
const loops = heights.map((value, i) =>
29+
Animated.loop(
30+
Animated.sequence([
31+
Animated.delay(BAR_DELAYS_MS[i]),
32+
Animated.timing(value, {
33+
toValue: BAR_MAX_HEIGHT,
34+
duration: BAR_DURATIONS_MS[i],
35+
easing: Easing.inOut(Easing.quad),
36+
useNativeDriver: false
37+
}),
38+
Animated.timing(value, {
39+
toValue: BAR_MIN_HEIGHT,
40+
duration: BAR_DURATIONS_MS[i],
41+
easing: Easing.inOut(Easing.quad),
42+
useNativeDriver: false
43+
})
44+
])
45+
)
46+
)
47+
48+
loops.forEach((loop) => loop.start())
49+
50+
return () => {
51+
loops.forEach((loop) => loop.stop())
52+
}
53+
}, [isPlaying, heights])
54+
55+
return (
56+
<View style={styles.container} pointerEvents='none'>
57+
{heights.map((height, i) => (
58+
<Animated.View key={i} style={[styles.bar, { height }]} />
59+
))}
60+
</View>
61+
)
62+
}
63+
64+
const styles = StyleSheet.create({
65+
container: {
66+
flexDirection: 'row',
67+
alignItems: 'flex-end',
68+
justifyContent: 'center',
69+
gap: 3,
70+
height: BAR_MAX_HEIGHT,
71+
paddingBottom: 2
72+
},
73+
bar: {
74+
width: 4,
75+
borderRadius: 2,
76+
backgroundColor: BAR_COLOR
77+
}
78+
})

packages/mobile/src/components/track-list/TrackArtwork.tsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,14 @@ import type { Track } from '@audius/common/models'
22
import { SquareSizes } from '@audius/common/models'
33
import { View } from 'react-native'
44

5-
import {
6-
IconVisibilityHidden,
7-
IconPause,
8-
IconPlay
9-
} from '@audius/harmony-native'
5+
import { IconVisibilityHidden, IconPlay } from '@audius/harmony-native'
106
import { makeStyles } from 'app/styles'
117
import { useThemeColors } from 'app/utils/theme'
128

139
import { TrackImage } from '../image/TrackImage'
1410

11+
import { AnimatedEqBars } from './AnimatedEqBars'
12+
1513
type TrackArtworkProps = {
1614
track: Track
1715
isActive?: boolean
@@ -33,6 +31,14 @@ const useStyles = makeStyles(({ spacing }) => ({
3331
alignItems: 'center',
3432
borderRadius: 4,
3533
backgroundColor: 'rgba(0,0,0,0.4)'
34+
},
35+
nowPlayingOverlay: {
36+
height: '100%',
37+
width: '100%',
38+
justifyContent: 'flex-end',
39+
alignItems: 'center',
40+
borderRadius: 4,
41+
backgroundColor: 'rgba(0,0,0,0.45)'
3642
}
3743
}))
3844

@@ -41,8 +47,6 @@ export const TrackArtwork = (props: TrackArtworkProps) => {
4147
const styles = useStyles()
4248
const { staticWhite } = useThemeColors()
4349

44-
const ActiveIcon = isPlaying ? IconPause : IconPlay
45-
4650
return (
4751
<TrackImage
4852
trackId={track.track_id}
@@ -54,9 +58,13 @@ export const TrackArtwork = (props: TrackArtworkProps) => {
5458
<IconVisibilityHidden fill={staticWhite} />
5559
</View>
5660
) : null}
57-
{isActive ? (
61+
{isActive && isPlaying ? (
62+
<View style={styles.nowPlayingOverlay}>
63+
<AnimatedEqBars isPlaying={isPlaying} />
64+
</View>
65+
) : isActive ? (
5866
<View style={styles.artworkIcon}>
59-
<ActiveIcon color='white' style={{ opacity: 0.8 }} />
67+
<IconPlay color='white' style={{ opacity: 0.8 }} />
6068
</View>
6169
) : null}
6270
</TrackImage>

packages/mobile/src/components/track-list/TrackListItem.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ const useStyles = makeStyles(({ palette, spacing, typography }) => ({
6666
backgroundColor: palette.white
6767
},
6868
trackContainerActive: {
69-
backgroundColor: palette.neutralLight9
69+
backgroundColor: 'rgba(130,86,220,0.07)'
7070
},
7171
trackContainerDisabled: {
7272
backgroundColor: palette.neutralLight9
@@ -102,6 +102,9 @@ const useStyles = makeStyles(({ palette, spacing, typography }) => ({
102102
paddingTop: 2,
103103
color: palette.neutral
104104
},
105+
trackTitleTextActive: {
106+
color: '#8256DC'
107+
},
105108
downloadIndicator: {
106109
marginLeft: spacing(1)
107110
},
@@ -405,11 +408,21 @@ const TrackListItemComponent = (props: TrackListItemComponentProps) => {
405408
<View style={styles.trackTitle}>
406409
<Text
407410
numberOfLines={1}
408-
style={[styles.trackTitleText, { maxWidth: titleMaxWidth }]}
411+
style={[
412+
styles.trackTitleText,
413+
isActive && styles.trackTitleTextActive,
414+
{ maxWidth: titleMaxWidth }
415+
]}
409416
>
410417
{title}
411418
</Text>
412-
<Text numberOfLines={1} style={[styles.trackTitleText]}>
419+
<Text
420+
numberOfLines={1}
421+
style={[
422+
styles.trackTitleText,
423+
isActive && styles.trackTitleTextActive
424+
]}
425+
>
413426
{messages.deleted}
414427
</Text>
415428
</View>

0 commit comments

Comments
 (0)