diff --git a/src/screens/Music/stacks/Albums.tsx b/src/screens/Music/stacks/Albums.tsx
index 492e7f1..c5bdb68 100644
--- a/src/screens/Music/stacks/Albums.tsx
+++ b/src/screens/Music/stacks/Albums.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useEffect, useRef, ReactText } from 'react';
+import React, { useCallback, useEffect, useRef, ReactText, useMemo } from 'react';
import { useGetImage } from 'utility/JellyfinApi';
import { MusicNavigationProp } from '../types';
import { SafeAreaView, SectionList, View } from 'react-native';
@@ -20,22 +20,6 @@ import { ShadowWrapper } from 'components/Shadow';
const HeadingHeight = 50;
-interface VirtualizedItemInfo {
- section: SectionedId,
- // Key of the section or combined key for section + item
- key: string,
- // Relative index within the section
- index: number,
- // True if this is the section header
- header?: boolean,
- leadingItem?: EntityId,
- leadingSection?: SectionedId,
- trailingItem?: EntityId,
- trailingSection?: SectionedId,
-}
-
-type VirtualizedSectionList = { _subExtractor: (index: number) => VirtualizedItemInfo };
-
function generateSection({ section }: { section: SectionedId }) {
return (
@@ -105,42 +89,52 @@ const Albums: React.FC = () => {
const dispatch = useAppDispatch();
const navigation = useNavigation();
const getImage = useGetImage();
- const listRef = useRef>(null);
+ const listRef = useRef>(null);
- const getItemLayout = useCallback((data: SectionedId[] | null, index: number): { offset: number, length: number, index: number } => {
- // We must wait for the ref to become available before we can use the
- // native item retriever in VirtualizedSectionList
- if (!listRef.current) {
- return { offset: 0, length: 0, index };
- }
+ // Create an array that computes all the height data for the entire list in
+ // advance. We can then use this pre-computed data to respond to
+ // `getItemLayout` calls, without having to compute things in place (and
+ // fail horribly).
+ // This approach was inspired by https://gist.github.com/RaphBlanchet/472ed013e05398c083caae6216b598b5
+ const itemLayouts = useMemo(() => {
+ // Create an array in which we will store all possible outputs for
+ // `getItemLayout`. We will loop through each potential album and add
+ // items that will be in the list
+ const layouts: Array<{ length: number; offset: number; index: number }> = [];
+
+ // Keep track of both the index of items and the offset (in pixels) from
+ // the top
+ let index = 0;
+ let offset = 0;
- // Retrieve the right item info
- // @ts-ignore
- const wrapperListRef = (listRef.current?._wrapperListRef) as VirtualizedSectionList;
- const info: VirtualizedItemInfo = wrapperListRef._subExtractor(index);
- const { index: itemIndex, header, key } = info;
- const sectionIndex = parseInt(key.split(':')[0]);
+ // Loop through each individual section (i.e. alphabet letter) and add
+ // all items in that particular section.
+ sections.forEach((section) => {
+ // Each section starts with a header, so we'll need to add the item,
+ // as well as the offset.
+ layouts[index] = ({ length: HeadingHeight, offset, index });
+ index++;
+ offset += HeadingHeight;
- // We can then determine the "length" (=height) of this item. Header items
- // end up with an itemIndex of -1, thus are easy to identify.
- const length = header ? 50 : (itemIndex % 2 === 0 ? AlbumHeight : 0);
-
- // We'll also need to account for any unevenly-ended lists up until the
- // current item.
- const previousRows = data?.filter((row, i) => i < sectionIndex)
- .reduce((sum, row) => sum + Math.ceil(row.data.length / 2), 0) || 0;
+ // Then, loop through all the rows (sets of two albums) and add
+ // items for those as well.
+ section.data.forEach(() => {
+ layouts[index] = ({ length: AlbumHeight, offset, index });
+ index++;
+ offset += AlbumHeight;
+ });
- // We must also calcuate the offset, total distance from the top of the
- // screen. First off, we'll account for each sectionIndex that is shown up
- // until now. This only includes the heading for the current section if the
- // item is not the section header
- const headingOffset = HeadingHeight * (header ? sectionIndex : sectionIndex + 1);
- const currentRows = itemIndex > 1 ? Math.ceil((itemIndex + 1) / 2) : 0;
- const itemOffset = AlbumHeight * (previousRows + currentRows);
- const offset = headingOffset + itemOffset;
-
- return { index, length, offset };
- }, [listRef]);
+ // The way SectionList works is that you get an item for a
+ // SectionHeader and a SectionFooter, no matter if you've specified
+ // whether you want them or not. Thus, we will need to add an empty
+ // footer as an item, so that we don't mismatch our indexes
+ layouts[index] = { length: 0, offset, index };
+ index++;
+ });
+
+ // Then, store and memoize the output
+ return layouts;
+ }, [sections]);
// Set callbacks
const retrieveData = useCallback(() => dispatch(fetchAllAlbums()), [dispatch]);
@@ -148,30 +142,19 @@ const Albums: React.FC = () => {
const selectLetter = useCallback((sectionIndex: number) => {
listRef.current?.scrollToLocation({ sectionIndex, itemIndex: 0, animated: false, });
}, [listRef]);
- const generateItem = useCallback(({ item, index, section }: { item: EntityId, index: number, section: SectionedId }) => {
- if (index % 2 === 1) {
- return ;
- }
- const nextItem = section.data[index + 1];
-
+ const generateItem = useCallback(({ item }: { item: EntityId[] }) => {
return (
-
-
- {albums[nextItem] &&
+
+ {item.map((id) => (
- }
+ ))}
);
}, [albums, getImage, selectAlbum]);
@@ -191,9 +174,9 @@ const Albums: React.FC = () => {
sections={sections}
refreshing={isLoading}
onRefresh={retrieveData}
- getItemLayout={getItemLayout}
+ getItemLayout={(_, i) => itemLayouts[i] ?? { length: 0, offset: 0, index: i }}
ref={listRef}
- keyExtractor={(item) => item as string}
+ keyExtractor={(item) => item.join('-')}
renderSectionHeader={generateSection}
renderItem={generateItem}
/>
diff --git a/src/store/music/selectors.ts b/src/store/music/selectors.ts
index 43b38ef..ddb9586 100644
--- a/src/store/music/selectors.ts
+++ b/src/store/music/selectors.ts
@@ -50,7 +50,7 @@ export const selectAlbumsByArtist = createSelector(
albumsByArtist,
);
-export type SectionedId = SectionListData;
+export type SectionedId = SectionListData;
/**
* Splits a set of albums into a list that is split by alphabet letters
@@ -58,13 +58,26 @@ export type SectionedId = SectionListData;
function splitAlbumsByAlphabet(state: AppState['music']['albums']): SectionedId[] {
const { entities: albums } = state;
const albumIds = albumsByArtist(state);
- const sections: SectionedId[] = ALPHABET_LETTERS.split('').map((l) => ({ label: l, data: [] }));
+ const sections: SectionedId[] = ALPHABET_LETTERS.split('').map((l) => ({ label: l, data: [[]] }));
albumIds.forEach((id) => {
+ // Retrieve the album letter and corresponding letter index
const album = albums[id];
const letter = album?.AlbumArtist?.toUpperCase().charAt(0);
const index = letter ? ALPHABET_LETTERS.indexOf(letter) : 26;
- (sections[index >= 0 ? index : 26].data as Array).push(id);
+
+ // Then find the current row in this section (note that albums are
+ // grouped in pairs so we can render them more easily).
+ const section = sections[index >= 0 ? index : 26];
+ const row = section.data.length - 1;
+
+ // Add the album to the row
+ section.data[row].push(id);
+
+ // GUARD: Check if the row is overflowing. If so, add a new row.
+ if (section.data[row].length >= 2) {
+ (section.data as EntityId[][]).push([]);
+ }
});
return sections;