improved large album/playlist performance

switched to flatlist for all of those
This commit is contained in:
austinried 2021-08-25 16:13:35 +09:00
parent 81b5bfb56b
commit fac2b62ec2
13 changed files with 210 additions and 140 deletions

View File

@ -0,0 +1,54 @@
import React from 'react'
import { FlatList, FlatListProps, useWindowDimensions, View, StyleSheet } from 'react-native'
import colors from '@app/styles/colors'
import GradientBackground, { GradientBackgroundProps } from '@app/components/GradientBackground'
import { useLayout } from '@react-native-community/hooks'
import NothingHere from './NothingHere'
import ImageGradientBackground, { ImageGradientBackgroundProps } from './ImageGradientBackground'
export type BackgroundHeaderFlatListPropsBase<ItemT> = FlatListProps<ItemT> & {
contentMarginTop?: number
}
export type BackgroundHeaderFlatListProp<ItemT> = BackgroundHeaderFlatListPropsBase<ItemT> & {
BackgroundComponent: typeof ImageGradientBackground | typeof GradientBackground
backgroundProps?: ImageGradientBackgroundProps | GradientBackgroundProps
}
function BackgroundHeaderFlatList<ItemT>(props: BackgroundHeaderFlatListProp<ItemT>) {
const window = useWindowDimensions()
const headerLayout = useLayout()
let marginBottom = -window.height + (props.contentMarginTop || 0)
if (props.ListHeaderComponent) {
marginBottom += headerLayout.height || window.height
}
const headerStyle = { marginBottom }
return (
<FlatList
{...props}
contentContainerStyle={[props.contentContainerStyle, { minHeight: window.height }]}
style={[props.style, styles.list]}
ListHeaderComponent={
<props.BackgroundComponent position="relative" {...props.backgroundProps}>
<View onLayout={headerLayout.onLayout}>{props.ListHeaderComponent}</View>
</props.BackgroundComponent>
}
ListHeaderComponentStyle={[headerStyle]}
ListEmptyComponent={<NothingHere style={styles.nothing} />}
/>
)
}
const styles = StyleSheet.create({
list: {
backgroundColor: colors.gradient.low,
},
nothing: {
width: '100%',
},
})
export default BackgroundHeaderFlatList

View File

@ -3,14 +3,27 @@ import { useWindowDimensions, ViewStyle } from 'react-native'
import LinearGradient from 'react-native-linear-gradient' import LinearGradient from 'react-native-linear-gradient'
import colorStyles from '@app/styles/colors' import colorStyles from '@app/styles/colors'
const GradientBackground: React.FC<{ export type GradientBackgroundPropsBase = {
height?: number | string height?: number | string
width?: number | string width?: number | string
position?: 'relative' | 'absolute' position?: 'relative' | 'absolute'
style?: ViewStyle style?: ViewStyle
}
export type GradientBackgroundProps = GradientBackgroundPropsBase & {
colors?: string[] colors?: string[]
locations?: number[] locations?: number[]
}> = ({ height, width, position, style, colors, locations, children }) => { }
const GradientBackground: React.FC<GradientBackgroundProps> = ({
height,
width,
position,
style,
colors,
locations,
children,
}) => {
const layout = useWindowDimensions() const layout = useWindowDimensions()
return ( return (

View File

@ -1,28 +1,13 @@
import GradientBackground, { GradientBackgroundProps } from '@app/components/GradientBackground'
import React from 'react' import React from 'react'
import { FlatList, FlatListProps, StyleSheet, useWindowDimensions } from 'react-native' import BackgroundHeaderFlatList, { BackgroundHeaderFlatListPropsBase } from './BackgroundHeaderFlatList'
import colors from '@app/styles/colors'
import GradientBackground from '@app/components/GradientBackground'
function GradientFlatList<ItemT>(props: FlatListProps<ItemT>) { export type GradientFlatListProps<ItemT> = BackgroundHeaderFlatListPropsBase<ItemT> & {
const layout = useWindowDimensions() backgroundProps?: GradientBackgroundProps
}
const contentContainerStyle = StyleSheet.flatten(props.contentContainerStyle) function GradientFlatList<ItemT>(props: GradientFlatListProps<ItemT>) {
return <BackgroundHeaderFlatList BackgroundComponent={GradientBackground} {...props} />
return (
<FlatList
{...props}
style={{
...(props.style as any),
backgroundColor: colors.gradient.low,
}}
ListHeaderComponent={() => <GradientBackground position="relative" />}
ListHeaderComponentStyle={{
marginBottom: -layout.height,
marginHorizontal: -(contentContainerStyle.paddingHorizontal || 0),
top: -(contentContainerStyle.paddingTop || 0),
}}
/>
)
} }
export default GradientFlatList export default GradientFlatList

View File

@ -1,16 +1,11 @@
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { ViewStyle } from 'react-native'
import ImageColors from 'react-native-image-colors' import ImageColors from 'react-native-image-colors'
import { AndroidImageColors } from 'react-native-image-colors/lib/typescript/types' import { AndroidImageColors } from 'react-native-image-colors/lib/typescript/types'
import colors from '@app/styles/colors' import colors from '@app/styles/colors'
import GradientBackground from '@app/components/GradientBackground' import GradientBackground, { GradientBackgroundPropsBase } from '@app/components/GradientBackground'
export type ImageGradientBackgroundProps = { export type ImageGradientBackgroundProps = GradientBackgroundPropsBase & {
height?: number | string
width?: number | string
position?: 'relative' | 'absolute'
style?: ViewStyle
imagePath?: string imagePath?: string
onGetColor?: (color: string) => void onGetColor?: (color: string) => void
} }

View File

@ -0,0 +1,13 @@
import React from 'react'
import BackgroundHeaderFlatList, { BackgroundHeaderFlatListPropsBase } from './BackgroundHeaderFlatList'
import ImageGradientBackground, { ImageGradientBackgroundProps } from './ImageGradientBackground'
export type ImageGradientFlatListProps<ItemT> = BackgroundHeaderFlatListPropsBase<ItemT> & {
backgroundProps?: ImageGradientBackgroundProps
}
function ImageGradientFlatList<ItemT>(props: ImageGradientFlatListProps<ItemT>) {
return <BackgroundHeaderFlatList BackgroundComponent={ImageGradientBackground} {...props} />
}
export default ImageGradientFlatList

View File

@ -7,7 +7,7 @@ import colors from '@app/styles/colors'
import font from '@app/styles/font' import font from '@app/styles/font'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { StyleSheet, Text, View } from 'react-native' import { StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native'
import IconFA5 from 'react-native-vector-icons/FontAwesome5' import IconFA5 from 'react-native-vector-icons/FontAwesome5'
import IconMat from 'react-native-vector-icons/MaterialIcons' import IconMat from 'react-native-vector-icons/MaterialIcons'
import { AlbumContextPressable, ArtistContextPressable, SongContextPressable } from './ContextMenu' import { AlbumContextPressable, ArtistContextPressable, SongContextPressable } from './ContextMenu'
@ -55,7 +55,8 @@ const ListItem: React.FC<{
showStar?: boolean showStar?: boolean
listStyle?: 'big' | 'small' listStyle?: 'big' | 'small'
subtitle?: string subtitle?: string
}> = ({ item, contextId, queueId, onPress, showArt, showStar, subtitle, listStyle }) => { style?: StyleProp<ViewStyle>
}> = ({ item, contextId, queueId, onPress, showArt, showStar, subtitle, listStyle, style }) => {
const navigation = useNavigation() const navigation = useNavigation()
const starred = useStarred(item.id, item.itemType) const starred = useStarred(item.id, item.itemType)
@ -156,7 +157,7 @@ const ListItem: React.FC<{
} }
return ( return (
<View style={[styles.container, sizeStyle.container]}> <View style={[styles.container, sizeStyle.container, style]}>
<PressableComponent> <PressableComponent>
{showArt && coverArt} {showArt && coverArt}
<View style={styles.text}> <View style={styles.text}>

View File

@ -72,7 +72,6 @@ const AlbumsList = () => {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<GradientFlatList <GradientFlatList
contentContainerStyle={styles.listContent}
data={list.map(album => ({ album, size, height }))} data={list.map(album => ({ album, size, height }))}
renderItem={AlbumListRenderItem} renderItem={AlbumListRenderItem}
keyExtractor={item => item.album.id} keyExtractor={item => item.album.id}
@ -100,9 +99,6 @@ const AlbumsList = () => {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
listContent: {
minHeight: '100%',
},
container: { container: {
flex: 1, flex: 1,
}, },

View File

@ -11,7 +11,7 @@ import React, { useEffect, useState } from 'react'
import { StyleSheet, View } from 'react-native' import { StyleSheet, View } from 'react-native'
const ArtistRenderItem: React.FC<{ item: Artist }> = ({ item }) => ( const ArtistRenderItem: React.FC<{ item: Artist }> = ({ item }) => (
<ListItem item={item} showArt={true} showStar={false} listStyle="big" /> <ListItem item={item} showArt={true} showStar={false} listStyle="big" style={styles.listItem} />
) )
const filterOptions: OptionData[] = [ const filterOptions: OptionData[] = [
@ -44,7 +44,6 @@ const ArtistsList = () => {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<GradientFlatList <GradientFlatList
contentContainerStyle={styles.listContent}
data={sortedList} data={sortedList}
renderItem={ArtistRenderItem} renderItem={ArtistRenderItem}
keyExtractor={item => item.id} keyExtractor={item => item.id}
@ -52,6 +51,7 @@ const ArtistsList = () => {
refreshing={refreshing} refreshing={refreshing}
overScrollMode="never" overScrollMode="never"
windowSize={3} windowSize={3}
contentMarginTop={6}
/> />
<FilterButton <FilterButton
data={filterOptions} data={filterOptions}
@ -71,10 +71,8 @@ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
}, },
listContent: { listItem: {
minHeight: '100%',
paddingHorizontal: 10, paddingHorizontal: 10,
paddingTop: 6,
}, },
}) })

View File

@ -8,7 +8,7 @@ import React from 'react'
import { StyleSheet } from 'react-native' import { StyleSheet } from 'react-native'
const PlaylistRenderItem: React.FC<{ item: PlaylistListItem }> = ({ item }) => ( const PlaylistRenderItem: React.FC<{ item: PlaylistListItem }> = ({ item }) => (
<ListItem item={item} showArt={true} showStar={false} listStyle="big" /> <ListItem item={item} showArt={true} showStar={false} listStyle="big" style={styles.listItem} />
) )
const PlaylistsList = () => { const PlaylistsList = () => {
@ -17,7 +17,6 @@ const PlaylistsList = () => {
return ( return (
<GradientFlatList <GradientFlatList
contentContainerStyle={styles.listContent}
data={list} data={list}
renderItem={PlaylistRenderItem} renderItem={PlaylistRenderItem}
keyExtractor={item => item.id} keyExtractor={item => item.id}
@ -25,15 +24,14 @@ const PlaylistsList = () => {
refreshing={refreshing} refreshing={refreshing}
overScrollMode="never" overScrollMode="never"
windowSize={5} windowSize={5}
contentMarginTop={6}
/> />
) )
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
listContent: { listItem: {
minHeight: '100%',
paddingHorizontal: 10, paddingHorizontal: 10,
paddingTop: 6,
}, },
}) })

View File

@ -1,49 +1,56 @@
import GradientScrollView from '@app/components/GradientScrollView' import GradientFlatList from '@app/components/GradientFlatList'
import ListItem from '@app/components/ListItem' import ListItem from '@app/components/ListItem'
import NowPlayingBar from '@app/components/NowPlayingBar' import NowPlayingBar from '@app/components/NowPlayingBar'
import { useSkipTo } from '@app/hooks/trackplayer' import { useSkipTo } from '@app/hooks/trackplayer'
import { Song } from '@app/models/music'
import { useStore } from '@app/state/store' import { useStore } from '@app/state/store'
import { selectTrackPlayer } from '@app/state/trackplayer' import { selectTrackPlayer } from '@app/state/trackplayer'
import { selectTrackPlayerMap } from '@app/state/trackplayermap' import { selectTrackPlayerMap } from '@app/state/trackplayermap'
import React from 'react' import React from 'react'
import { StyleSheet, View } from 'react-native' import { StyleSheet, View } from 'react-native'
const SongRenderItem: React.FC<{
item: {
song: Song
i: number
onPress: () => void
}
}> = ({ item }) => (
<ListItem
item={item.song}
queueId={item.i}
onPress={item.onPress}
showArt={true}
style={styles.listItem}
subtitle={`${item.song.artist}${item.song.album}`}
/>
)
const NowPlayingQueue = React.memo<{}>(() => { const NowPlayingQueue = React.memo<{}>(() => {
const queue = useStore(selectTrackPlayer.queue) const queue = useStore(selectTrackPlayer.queue)
const mapTrackExtToSong = useStore(selectTrackPlayerMap.mapTrackExtToSong) const mapTrackExtToSong = useStore(selectTrackPlayerMap.mapTrackExtToSong)
const skipTo = useSkipTo() const skipTo = useSkipTo()
return ( return (
<View style={styles.outerContainer}> <View style={styles.container}>
<GradientScrollView style={styles.container}> <GradientFlatList
<View style={styles.content}> data={queue.map(mapTrackExtToSong).map((song, i) => ({ song, i, onPress: () => skipTo(i) }))}
{queue.map(mapTrackExtToSong).map((song, i) => ( renderItem={SongRenderItem}
<ListItem keyExtractor={(item, i) => i.toString()}
key={i} overScrollMode="never"
item={song} windowSize={7}
queueId={i} contentMarginTop={10}
onPress={() => skipTo(i)} />
showArt={true}
subtitle={`${song.artist}${song.album}`}
/>
))}
</View>
</GradientScrollView>
<NowPlayingBar /> <NowPlayingBar />
</View> </View>
) )
}) })
const styles = StyleSheet.create({ const styles = StyleSheet.create({
outerContainer: {
flex: 1,
},
container: { container: {
flex: 1, flex: 1,
}, },
content: { listItem: {
alignItems: 'center',
paddingTop: 10,
paddingHorizontal: 20, paddingHorizontal: 20,
}, },
}) })

View File

@ -28,6 +28,7 @@ const ResultsListItem: React.FC<{ item: SearchListItemType }> = ({ item }) => {
showStar={false} showStar={false}
listStyle="small" listStyle="small"
onPress={onPress} onPress={onPress}
style={styles.listItem}
/> />
) )
} }
@ -69,25 +70,24 @@ const SearchResultsView: React.FC<{
return ( return (
<GradientFlatList <GradientFlatList
contentContainerStyle={styles.listContent}
data={list} data={list}
renderItem={SearchResultsRenderItem} renderItem={SearchResultsRenderItem}
keyExtractor={item => item.id} keyExtractor={(item, i) => i.toString()}
onRefresh={refresh} onRefresh={refresh}
refreshing={refreshing} refreshing={refreshing}
overScrollMode="never" overScrollMode="never"
onEndReached={fetchNextPage} onEndReached={fetchNextPage}
removeClippedSubviews={true} removeClippedSubviews={true}
onEndReachedThreshold={2} onEndReachedThreshold={2}
contentMarginTop={6}
windowSize={5}
/> />
) )
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
listContent: { listItem: {
minHeight: '100%',
paddingHorizontal: 10, paddingHorizontal: 10,
paddingTop: 6,
}, },
}) })

View File

@ -1,10 +1,9 @@
import CoverArt from '@app/components/CoverArt' import CoverArt from '@app/components/CoverArt'
import GradientBackground from '@app/components/GradientBackground' import GradientBackground from '@app/components/GradientBackground'
import HeaderBar from '@app/components/HeaderBar' import HeaderBar from '@app/components/HeaderBar'
import ImageGradientScrollView from '@app/components/ImageGradientScrollView' import ImageGradientFlatList from '@app/components/ImageGradientFlatList'
import ListItem from '@app/components/ListItem' import ListItem from '@app/components/ListItem'
import ListPlayerControls from '@app/components/ListPlayerControls' import ListPlayerControls from '@app/components/ListPlayerControls'
import NothingHere from '@app/components/NothingHere'
import { useCoverArtFile } from '@app/hooks/cache' import { useCoverArtFile } from '@app/hooks/cache'
import { useAlbumWithSongs, usePlaylistWithSongs } from '@app/hooks/music' import { useAlbumWithSongs, usePlaylistWithSongs } from '@app/hooks/music'
import { AlbumWithSongs, PlaylistWithSongs, Song } from '@app/models/music' import { AlbumWithSongs, PlaylistWithSongs, Song } from '@app/models/music'
@ -23,15 +22,42 @@ const SongListDetailsFallback = React.memo(() => (
</GradientBackground> </GradientBackground>
)) ))
const Songs = React.memo<{ const SongRenderItem: React.FC<{
songs: Song[] item: {
name: string song: Song
contextId?: string
queueId?: number
subtitle?: string
onPress?: () => void
showArt?: boolean
}
}> = ({ item }) => (
<ListItem
item={item.song}
contextId={item.contextId}
queueId={item.queueId}
subtitle={item.subtitle}
onPress={item.onPress}
showArt={item.showArt}
style={styles.listItem}
/>
)
const SongListDetails = React.memo<{
title: string
type: SongListType type: SongListType
itemId: string songList?: AlbumWithSongs | PlaylistWithSongs
}>(({ songs, name, type, itemId }) => { subtitle?: string
}>(({ title, songList, subtitle, type }) => {
const coverArtFile = useCoverArtFile(songList?.coverArt, 'thumbnail')
const [headerColor, setHeaderColor] = useState<string | undefined>(undefined)
const setQueue = useStore(selectTrackPlayer.setQueue) const setQueue = useStore(selectTrackPlayer.setQueue)
const _songs = [...songs] if (!songList) {
return <SongListDetailsFallback />
}
const _songs = [...songList.songs]
let typeName = '' let typeName = ''
if (type === 'album') { if (type === 'album') {
@ -49,46 +75,6 @@ const Songs = React.memo<{
typeName = 'Playlist' typeName = 'Playlist'
} }
return (
<>
<ListPlayerControls
style={styles.controls}
songs={_songs}
typeName={typeName}
queueName={name}
queueContextId={itemId}
queueContextType={type}
/>
<View style={styles.songs}>
{_songs.map((s, i) => (
<ListItem
key={i}
item={s}
contextId={itemId}
queueId={i}
subtitle={s.artist}
onPress={() => setQueue(_songs, name, type, itemId, i)}
showArt={false}
/>
))}
</View>
</>
)
})
const SongListDetails = React.memo<{
title: string
type: SongListType
songList?: AlbumWithSongs | PlaylistWithSongs
subtitle?: string
}>(({ title, songList, subtitle, type }) => {
const coverArtFile = useCoverArtFile(songList?.coverArt, 'thumbnail')
const [headerColor, setHeaderColor] = useState<string | undefined>(undefined)
if (!songList) {
return <SongListDetailsFallback />
}
return ( return (
<View style={styles.container}> <View style={styles.container}>
<HeaderBar <HeaderBar
@ -96,21 +82,43 @@ const SongListDetails = React.memo<{
title={title} title={title}
contextItem={songList.itemType === 'album' ? songList : undefined} contextItem={songList.itemType === 'album' ? songList : undefined}
/> />
<ImageGradientScrollView <ImageGradientFlatList
imagePath={coverArtFile?.file?.path} data={_songs.map((s, i) => ({
style={styles.container} song: s,
onGetColor={setHeaderColor}> contextId: songList.id,
<View style={styles.content}> queueId: i,
<CoverArt type="cover" size="original" coverArt={songList.coverArt} style={styles.cover} /> subtitle: s.artist,
<Text style={styles.title}>{songList.name}</Text> onPress: () => setQueue(_songs, songList.name, type, songList.id, i),
{subtitle ? <Text style={styles.subtitle}>{subtitle}</Text> : <></>} showArt: songList.itemType === 'playlist',
{songList.songs.length > 0 ? ( }))}
<Songs songs={songList.songs} name={songList.name} type={type} itemId={songList.id} /> renderItem={SongRenderItem}
) : ( keyExtractor={(item, i) => i.toString()}
<NothingHere height={300} width={250} /> backgroundProps={{
)} imagePath: coverArtFile?.file?.path,
</View> style: styles.container,
</ImageGradientScrollView> onGetColor: setHeaderColor,
}}
overScrollMode="never"
windowSize={7}
contentMarginTop={26}
ListHeaderComponent={
<View style={styles.content}>
<CoverArt type="cover" size="original" coverArt={songList.coverArt} style={styles.cover} />
<Text style={styles.title}>{songList.name}</Text>
{subtitle ? <Text style={styles.subtitle}>{subtitle}</Text> : <></>}
{songList.songs.length > 0 && (
<ListPlayerControls
style={styles.controls}
songs={_songs}
typeName={typeName}
queueName={songList.name}
queueContextId={songList.id}
queueContextType={type}
/>
)}
</View>
}
/>
</View> </View>
) )
}) })
@ -185,6 +193,9 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
paddingTop: 100, paddingTop: 100,
}, },
listItem: {
paddingHorizontal: 20,
},
}) })
export default SongListView export default SongListView

View File

@ -135,8 +135,7 @@ export const createMusicMapSlice = (set: SetState<Store>, get: GetState<Store>):
mapPlaylistWithSongs: async playlist => { mapPlaylistWithSongs: async playlist => {
return { return {
...get().mapPlaylistListItem(playlist), ...get().mapPlaylistListItem(playlist),
// passing cover art here is a temp fix to improve large playlist performance songs: await get().mapChildrenToSongs(playlist.songs),
songs: await get().mapChildrenToSongs(playlist.songs, playlist.coverArt),
coverArt: playlist.coverArt, coverArt: playlist.coverArt,
} }
}, },