MangaScreen: Improve chapter list scrolling performance (#7491)

* MangaScreen: Improve chapter list scrolling performance

Process chapter title, date and read progress string ahead of time

* Use enum for contentType and add key

(cherry picked from commit 1551891c15eb5d323cb1d425794876a753316091)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt
This commit is contained in:
Ivan Iskandar 2022-07-10 03:20:40 +07:00 committed by Jobobby04
parent d3e8ae54d9
commit d3b59768d4
4 changed files with 121 additions and 78 deletions

View File

@ -44,7 +44,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
@ -52,7 +51,6 @@ import androidx.compose.ui.res.stringResource
import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import eu.kanade.domain.chapter.model.Chapter import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.manga.model.Manga.Companion.CHAPTER_DISPLAY_NUMBER
import eu.kanade.presentation.components.ExtendedFloatingActionButton import eu.kanade.presentation.components.ExtendedFloatingActionButton
import eu.kanade.presentation.components.Scaffold import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.SwipeRefreshIndicator import eu.kanade.presentation.components.SwipeRefreshIndicator
@ -76,18 +74,8 @@ import eu.kanade.tachiyomi.source.getNameForMangaInfo
import eu.kanade.tachiyomi.source.online.MetadataSource import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.ui.manga.ChapterItem import eu.kanade.tachiyomi.ui.manga.ChapterItem
import eu.kanade.tachiyomi.ui.manga.MangaScreenState import eu.kanade.tachiyomi.ui.manga.MangaScreenState
import eu.kanade.tachiyomi.util.lang.toRelativeString
import exh.source.MERGED_SOURCE_ID import exh.source.MERGED_SOURCE_ID
import exh.source.getMainSource import exh.source.getMainSource
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.Date
private val chapterDecimalFormat = DecimalFormat(
"#.###",
DecimalFormatSymbols()
.apply { decimalSeparator = '.' },
)
@Composable @Composable
fun MangaScreen( fun MangaScreen(
@ -361,7 +349,10 @@ private fun MangaScreenSmallImpl(
state = chapterListState, state = chapterListState,
contentPadding = noTopContentPadding, contentPadding = noTopContentPadding,
) { ) {
item(contentType = "info_box") { item(
key = MangaScreenItem.INFO_BOX,
contentType = MangaScreenItem.INFO_BOX,
) {
MangaInfoBox( MangaInfoBox(
windowWidthSizeClass = WindowWidthSizeClass.Compact, windowWidthSizeClass = WindowWidthSizeClass.Compact,
appBarPadding = topPadding, appBarPadding = topPadding,
@ -377,7 +368,10 @@ private fun MangaScreenSmallImpl(
) )
} }
item(contentType = "action_row") { item(
key = MangaScreenItem.ACTION_ROW,
contentType = MangaScreenItem.ACTION_ROW,
) {
MangaActionRow( MangaActionRow(
favorite = state.manga.favorite, favorite = state.manga.favorite,
trackingCount = state.trackingCount, trackingCount = state.trackingCount,
@ -393,7 +387,10 @@ private fun MangaScreenSmallImpl(
// SY --> // SY -->
if (metadataSource != null) { if (metadataSource != null) {
item(contentType = "metadata_info") { item(
key = MangaScreenItem.METADATA_INFO,
contentType = MangaScreenItem.METADATA_INFO,
) {
metadataSource.DescriptionComposable( metadataSource.DescriptionComposable(
state = state, state = state,
openMetadataViewer = onMetadataViewerClicked, openMetadataViewer = onMetadataViewerClicked,
@ -403,7 +400,10 @@ private fun MangaScreenSmallImpl(
} }
// SY <-- // SY <--
item(contentType = "desc") { item(
key = MangaScreenItem.DESCRIPTION_WITH_TAG,
contentType = MangaScreenItem.DESCRIPTION_WITH_TAG,
) {
ExpandableMangaDescription( ExpandableMangaDescription(
defaultExpandState = state.isFromSource, defaultExpandState = state.isFromSource,
description = state.manga.description, description = state.manga.description,
@ -420,7 +420,10 @@ private fun MangaScreenSmallImpl(
// SY --> // SY -->
if (!state.showRecommendationsInOverflow || state.showMergeWithAnother) { if (!state.showRecommendationsInOverflow || state.showMergeWithAnother) {
item(contentType = "info_buttons") { item(
key = MangaScreenItem.INFO_BUTTONS,
contentType = MangaScreenItem.INFO_BUTTONS,
) {
MangaInfoButtons( MangaInfoButtons(
showRecommendsButton = !state.showRecommendationsInOverflow, showRecommendsButton = !state.showRecommendationsInOverflow,
showMergeWithAnotherButton = state.showMergeWithAnother, showMergeWithAnotherButton = state.showMergeWithAnother,
@ -431,7 +434,10 @@ private fun MangaScreenSmallImpl(
} }
// SY <-- // SY <--
item(contentType = "header") { item(
key = MangaScreenItem.CHAPTER_HEADER,
contentType = MangaScreenItem.CHAPTER_HEADER,
) {
ChapterHeader( ChapterHeader(
chapterCount = chapters.size, chapterCount = chapters.size,
isChapterFiltered = state.manga.chaptersFiltered(), isChapterFiltered = state.manga.chaptersFiltered(),
@ -441,7 +447,6 @@ private fun MangaScreenSmallImpl(
sharedChapterItems( sharedChapterItems(
chapters = chapters, chapters = chapters,
state = state,
selected = selected, selected = selected,
selectedPositions = selectedPositions, selectedPositions = selectedPositions,
onChapterClicked = onChapterClicked, onChapterClicked = onChapterClicked,
@ -680,7 +685,10 @@ fun MangaScreenLargeImpl(
state = chapterListState, state = chapterListState,
contentPadding = withNavBarContentPadding, contentPadding = withNavBarContentPadding,
) { ) {
item(contentType = "header") { item(
key = MangaScreenItem.CHAPTER_HEADER,
contentType = MangaScreenItem.CHAPTER_HEADER,
) {
ChapterHeader( ChapterHeader(
chapterCount = chapters.size, chapterCount = chapters.size,
isChapterFiltered = state.manga.chaptersFiltered(), isChapterFiltered = state.manga.chaptersFiltered(),
@ -690,7 +698,6 @@ fun MangaScreenLargeImpl(
sharedChapterItems( sharedChapterItems(
chapters = chapters, chapters = chapters,
state = state,
selected = selected, selected = selected,
selectedPositions = selectedPositions, selectedPositions = selectedPositions,
onChapterClicked = onChapterClicked, onChapterClicked = onChapterClicked,
@ -753,56 +760,27 @@ private fun SharedMangaBottomActionMenu(
private fun LazyListScope.sharedChapterItems( private fun LazyListScope.sharedChapterItems(
chapters: List<ChapterItem>, chapters: List<ChapterItem>,
state: MangaScreenState.Success,
selected: SnapshotStateList<ChapterItem>, selected: SnapshotStateList<ChapterItem>,
selectedPositions: Array<Int>, selectedPositions: Array<Int>,
onChapterClicked: (Chapter) -> Unit, onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?, onDownloadChapter: ((List<ChapterItem>, ChapterDownloadAction) -> Unit)?,
) { ) {
items(items = chapters) { chapterItem -> items(
val context = LocalContext.current items = chapters,
key = { it.chapter.id },
contentType = { MangaScreenItem.CHAPTER },
) { chapterItem ->
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val (chapter, downloadState, downloadProgress) = chapterItem
val chapterTitle = if (state.manga.displayMode == CHAPTER_DISPLAY_NUMBER) {
stringResource(
id = R.string.display_mode_chapter,
chapterDecimalFormat.format(chapter.chapterNumber.toDouble()),
)
} else {
chapter.name
}
val date = remember(chapter.dateUpload) {
chapter.dateUpload
.takeIf { it > 0 }
?.let {
Date(it).toRelativeString(
context,
state.dateRelativeTime,
state.dateFormat,
)
}
}
val lastPageRead = remember(chapter.lastPageRead) {
chapter.lastPageRead.takeIf { /* SY --> */(!chapter.read || state.alwaysShowPageProgress)/* SY <-- */ && it > 0 }
}
val scanlator = remember(chapter.scanlator) { chapter.scanlator.takeIf { !it.isNullOrBlank() } }
MangaChapterListItem( MangaChapterListItem(
title = chapterTitle, title = chapterItem.chapterTitleString,
date = date, date = chapterItem.dateUploadString,
readProgress = lastPageRead?.let { readProgress = chapterItem.readProgressString,
stringResource( scanlator = chapterItem.chapter.scanlator.takeIf { !it.isNullOrBlank() },
id = R.string.chapter_progress, read = chapterItem.chapter.read,
it + 1, bookmark = chapterItem.chapter.bookmark,
)
},
scanlator = scanlator,
read = chapter.read,
bookmark = chapter.bookmark,
selected = selected.contains(chapterItem), selected = selected.contains(chapterItem),
downloadStateProvider = { downloadState }, downloadStateProvider = { chapterItem.downloadState },
downloadProgressProvider = { downloadProgress }, downloadProgressProvider = { chapterItem.downloadProgress },
onLongClick = { onLongClick = {
val dispatched = onChapterItemLongClick( val dispatched = onChapterItemLongClick(
chapterItem = chapterItem, chapterItem = chapterItem,

View File

@ -20,3 +20,21 @@ enum class EditCoverAction {
EDIT, EDIT,
DELETE, DELETE,
} }
enum class MangaScreenItem {
INFO_BOX,
ACTION_ROW,
// SY -->
METADATA_INFO,
// SY <--
DESCRIPTION_WITH_TAG,
// SY -->
INFO_BUTTONS,
// SY <--
CHAPTER_HEADER,
CHAPTER,
}

View File

@ -81,8 +81,8 @@ fun MangaChapterListItem(
} }
Text( Text(
text = title, text = title,
style = MaterialTheme.typography.bodyMedium color = textColor,
.copy(color = textColor), style = MaterialTheme.typography.bodyMedium,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
onTextLayout = { textHeight = it.size.height }, onTextLayout = { textHeight = it.size.height },

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.ui.manga package eu.kanade.tachiyomi.ui.manga
import android.app.Application
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
@ -60,6 +61,7 @@ import eu.kanade.tachiyomi.util.chapter.ChapterSettingsHelper
import eu.kanade.tachiyomi.util.chapter.getChapterSort import eu.kanade.tachiyomi.util.chapter.getChapterSort
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.toRelativeString
import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.preference.asImmediateFlow import eu.kanade.tachiyomi.util.preference.asImmediateFlow
import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.util.removeCovers
@ -80,7 +82,6 @@ import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.source.MERGED_SOURCE_ID import exh.source.MERGED_SOURCE_ID
import exh.source.getMainSource import exh.source.getMainSource
import exh.source.isEhBasedManga import exh.source.isEhBasedManga
import exh.source.isEhBasedSource
import exh.source.mangaDexSourceIds import exh.source.mangaDexSourceIds
import exh.util.nullIfEmpty import exh.util.nullIfEmpty
import exh.util.trimOrNull import exh.util.trimOrNull
@ -110,6 +111,9 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.text.DateFormat import java.text.DateFormat
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.Date
import eu.kanade.domain.chapter.model.Chapter as DomainChapter import eu.kanade.domain.chapter.model.Chapter as DomainChapter
import eu.kanade.domain.manga.model.Manga as DomainManga import eu.kanade.domain.manga.model.Manga as DomainManga
import eu.kanade.tachiyomi.data.database.models.Manga.Companion as DbManga import eu.kanade.tachiyomi.data.database.models.Manga.Companion as DbManga
@ -284,7 +288,20 @@ class MangaPresenter(
} }
// SY <-- // SY <--
.collectLatest { (manga, chapters, flatMetadata, mergedData) -> .collectLatest { (manga, chapters, flatMetadata, mergedData) ->
val chapterItems = chapters.toChapterItems(manga, mergedData) val chapterItems = chapters.toChapterItems(
context = view?.activity ?: Injekt.get<Application>(),
manga = manga,
// SY -->
dateRelativeTime = if (manga.isEhBasedManga()) 0 else preferences.relativeTime().get(),
dateFormat = if (manga.isEhBasedManga()) {
MetadataUtil.EX_DATE_FORMAT
} else {
preferences.dateFormat()
},
mergedData = mergedData,
alwaysShowReadingProgress = preferences.preserveReadingPosition().get() && manga.isEhBasedManga(),
// SY <--
)
_state.update { currentState -> _state.update { currentState ->
when (currentState) { when (currentState) {
// Initialize success state // Initialize success state
@ -293,12 +310,6 @@ class MangaPresenter(
MangaScreenState.Success( MangaScreenState.Success(
manga = manga, manga = manga,
source = source, source = source,
dateRelativeTime = if (source.isEhBasedSource()) 0 else preferences.relativeTime().get(),
dateFormat = if (source.isEhBasedSource()) {
MetadataUtil.EX_DATE_FORMAT
} else {
preferences.dateFormat()
},
isFromSource = isFromSource, isFromSource = isFromSource,
trackingAvailable = trackManager.hasLoggedServices(), trackingAvailable = trackManager.hasLoggedServices(),
chapters = chapterItems, chapters = chapterItems,
@ -306,7 +317,6 @@ class MangaPresenter(
mergedData = mergedData, mergedData = mergedData,
showRecommendationsInOverflow = preferences.recommendsInOverflow().get(), showRecommendationsInOverflow = preferences.recommendsInOverflow().get(),
showMergeWithAnother = smartSearched, showMergeWithAnother = smartSearched,
alwaysShowPageProgress = preferences.preserveReadingPosition().get() && manga.isEhBasedManga(),
) )
} }
@ -818,7 +828,14 @@ class MangaPresenter(
} }
} }
private fun List<DomainChapter>.toChapterItems(manga: DomainManga, mergedData: MergedMangaData?): List<ChapterItem> { private fun List<DomainChapter>.toChapterItems(
context: Context,
manga: DomainManga,
dateRelativeTime: Int,
dateFormat: DateFormat,
mergedData: MergedMangaData?,
alwaysShowReadingProgress: Boolean,
): List<ChapterItem> {
return map { chapter -> return map { chapter ->
val activeDownload = downloadManager.queue.find { chapter.id == it.chapter.id } val activeDownload = downloadManager.queue.find { chapter.id == it.chapter.id }
val chapter = chapter.let { if (mergedData != null) it.toMergedDownloadedChapter() else it } val chapter = chapter.let { if (mergedData != null) it.toMergedDownloadedChapter() else it }
@ -840,6 +857,29 @@ class MangaPresenter(
chapter = chapter, chapter = chapter,
downloadState = downloadState, downloadState = downloadState,
downloadProgress = activeDownload?.progress ?: 0, downloadProgress = activeDownload?.progress ?: 0,
chapterTitleString = if (manga.displayMode == DomainManga.CHAPTER_DISPLAY_NUMBER) {
context.getString(
R.string.display_mode_chapter,
chapterDecimalFormat.format(chapter.chapterNumber.toDouble()),
)
} else {
chapter.name
},
dateUploadString = chapter.dateUpload
.takeIf { it > 0 }
?.let {
Date(it).toRelativeString(
context,
dateRelativeTime,
dateFormat,
)
},
readProgressString = chapter.lastPageRead.takeIf { /* SY --> */(!chapter.read || alwaysShowReadingProgress)/* SY <-- */ && it > 0 }?.let {
context.getString(
R.string.chapter_progress,
it + 1,
)
},
) )
} }
} }
@ -1319,8 +1359,6 @@ sealed class MangaScreenState {
data class Success( data class Success(
val manga: DomainManga, val manga: DomainManga,
val source: Source, val source: Source,
val dateRelativeTime: Int,
val dateFormat: DateFormat,
val isFromSource: Boolean, val isFromSource: Boolean,
val chapters: List<ChapterItem>, val chapters: List<ChapterItem>,
val trackingAvailable: Boolean = false, val trackingAvailable: Boolean = false,
@ -1334,7 +1372,6 @@ sealed class MangaScreenState {
val mergedData: MergedMangaData?, val mergedData: MergedMangaData?,
val showRecommendationsInOverflow: Boolean, val showRecommendationsInOverflow: Boolean,
val showMergeWithAnother: Boolean, val showMergeWithAnother: Boolean,
val alwaysShowPageProgress: Boolean,
// SY <-- // SY <--
) : MangaScreenState() { ) : MangaScreenState() {
@ -1386,6 +1423,16 @@ data class ChapterItem(
val chapter: DomainChapter, val chapter: DomainChapter,
val downloadState: Download.State, val downloadState: Download.State,
val downloadProgress: Int, val downloadProgress: Int,
val chapterTitleString: String,
val dateUploadString: String?,
val readProgressString: String?,
) { ) {
val isDownloaded = downloadState == Download.State.DOWNLOADED val isDownloaded = downloadState == Download.State.DOWNLOADED
} }
private val chapterDecimalFormat = DecimalFormat(
"#.###",
DecimalFormatSymbols()
.apply { decimalSeparator = '.' },
)