From f91c69906f0c1917400d3d938a7b38feeb9aae5a Mon Sep 17 00:00:00 2001 From: tarsin Date: Wed, 10 May 2023 21:37:40 +0800 Subject: [PATCH] Refactor GalleryPreviewsScene with Paging Compose --- app/build.gradle.kts | 1 + .../ehviewer/ui/scene/GalleryPreviewsScene.kt | 78 ++++++++++++------- .../com/hippo/widget/ComposeContentLayout.kt | 32 ++------ 3 files changed, 61 insertions(+), 50 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index de43770d2..6f87a4ae1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -103,6 +103,7 @@ android { "-opt-in=androidx.compose.ui.ExperimentalComposeUiApi", "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi", "-opt-in=androidx.compose.animation.ExperimentalAnimationApi", + "-opt-in=androidx.paging.ExperimentalPagingApi", "-opt-in=kotlin.contracts.ExperimentalContracts", "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.FlowPreview", diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryPreviewsScene.kt b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryPreviewsScene.kt index d61cf78bb..fe0f672fb 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryPreviewsScene.kt +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryPreviewsScene.kt @@ -26,10 +26,8 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.CallMissedOutgoing @@ -39,8 +37,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -51,6 +48,14 @@ import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingSource +import androidx.paging.PagingState +import androidx.paging.cachedIn +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemContentType +import androidx.paging.compose.itemKey import arrow.core.partially1 import coil.imageLoader import com.hippo.app.BaseDialogBuilder @@ -66,30 +71,47 @@ import com.hippo.ehviewer.ui.widget.CrystalCard import com.hippo.ehviewer.ui.widget.EhAsyncPreview import com.hippo.ehviewer.ui.widget.setMD3Content import com.hippo.util.getParcelableCompat -import com.hippo.widget.ContentLayout import com.hippo.widget.recyclerview.getSpanCountForSuitableSize -import com.hippo.widget.rememberContentState import eu.kanade.tachiyomi.util.lang.launchIO +import eu.kanade.tachiyomi.util.lang.withIOContext +import moe.tarsin.coroutines.runSuspendCatching import java.util.Locale class GalleryPreviewsScene : BaseScene() { - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { return ComposeView(inflater.context).apply { setMD3Content { val galleryDetail = rememberSaveable { requireArguments().getParcelableCompat(KEY_GALLERY_DETAIL)!! } fun onPreviewCLick(index: Int) = mainActivity!!.startReaderActivity(galleryDetail, index) val toNextPage = rememberSaveable { requireArguments().getBoolean(KEY_NEXT_PAGE) } val scrollBehaviour = TopAppBarDefaults.pinnedScrollBehavior() - val showJumpMenuButton by rememberSaveable { mutableStateOf(false) } // Padding is not subtracted here to have the same column count as gallery list and preview val totalSpace = LocalConfiguration.current.screenWidthDp val columnCount = getSpanCountForSuitableSize(totalSpace, Settings.thumbSizeDp) - val state = rememberContentState(galleryDetail.previewList) + val pages = galleryDetail.pages + val pgSize = galleryDetail.previewList.size + + // No Refresh support + val source = remember { + object : PagingSource() { + override fun getRefreshKey(state: PagingState) = null + override suspend fun load(params: LoadParams): LoadResult { + val key = params.key + if (key == null) { + return LoadResult.Page(galleryDetail.previewList, null, if (galleryDetail.previewPages == 1) null else pgSize, 0, pages - pgSize) + } else { + val page = key / pgSize + val result = runSuspendCatching { withIOContext { getPreviewListByPage(galleryDetail, page) } }.onFailure { return LoadResult.Error(it) }.getOrThrow() + val more = pages - result.size - key + return LoadResult.Page(result, key - 1, if (more == 0) null else key + result.size, 0, more) + } + } + override val jumpingSupported = true + } + } + + val data = remember { Pager(PagingConfig(pgSize)) { source }.flow.cachedIn(lifecycleScope) }.collectAsLazyPagingItems() Scaffold( topBar = { TopAppBar( @@ -100,7 +122,7 @@ class GalleryPreviewsScene : BaseScene() { } }, actions = { - if (showJumpMenuButton) { + if (galleryDetail.previewPages > 1) { IconButton(onClick = ::showGoto) { Icon(imageVector = Icons.Default.CallMissedOutgoing, contentDescription = null) } @@ -109,29 +131,33 @@ class GalleryPreviewsScene : BaseScene() { scrollBehavior = scrollBehaviour, ) }, - ) { - ContentLayout(state, getData = { page -> getPreviewListByPage(galleryDetail, page) }, modifier = Modifier.padding(top = it.calculateTopPadding())) { data: Array -> - LazyVerticalGrid( - columns = GridCells.Fixed(columnCount), - modifier = Modifier.nestedScroll(scrollBehaviour.nestedScrollConnection), - contentPadding = it, - horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.gallery_grid_margin_h)), - verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.gallery_grid_margin_v)), + ) { paddingValues -> + LazyVerticalGrid( + columns = GridCells.Fixed(columnCount), + modifier = Modifier.nestedScroll(scrollBehaviour.nestedScrollConnection), + contentPadding = paddingValues, + horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.gallery_grid_margin_h)), + verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.gallery_grid_margin_v)), + ) { + items( + count = data.itemCount, + key = data.itemKey { item -> item.position }, + contentType = data.itemContentType(), ) { - items(data) { + data[it]?.run { Column(horizontalAlignment = Alignment.CenterHorizontally) { Box(contentAlignment = Alignment.Center) { CrystalCard( - onClick = ::onPreviewCLick.partially1(it.position), + onClick = ::onPreviewCLick.partially1(position), modifier = Modifier.fillMaxWidth().aspectRatio(0.6666667F), ) { EhAsyncPreview( - model = it, + model = this@run, modifier = Modifier.fillMaxSize(), ) } } - Text((it.position + 1).toString()) + Text((position + 1).toString()) } } } diff --git a/app/src/main/java/com/hippo/widget/ComposeContentLayout.kt b/app/src/main/java/com/hippo/widget/ComposeContentLayout.kt index 3d7ad39ee..db6cc69aa 100644 --- a/app/src/main/java/com/hippo/widget/ComposeContentLayout.kt +++ b/app/src/main/java/com/hippo/widget/ComposeContentLayout.kt @@ -5,29 +5,26 @@ import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.paging.PagingSource +import androidx.paging.compose.LazyPagingItems import com.hippo.widget.pullrefresh.PullRefreshIndicator import com.hippo.widget.pullrefresh.pullRefresh import com.hippo.widget.pullrefresh.rememberPullRefreshState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import moe.tarsin.coroutines.runSuspendCatching @Composable inline fun ContentLayout( state: ContentState, - noinline getData: suspend (Int) -> List, modifier: Modifier = Modifier, - content: @Composable (Array) -> Unit, + content: @Composable (LazyPagingItems) -> Unit, ) { var refreshing by rememberSaveable { mutableStateOf(false) } val coroutineScope = rememberCoroutineScope { Dispatchers.IO } @@ -35,24 +32,18 @@ inline fun ContentLayout( refreshing = true if (state.startPage == 0) { coroutineScope.launch { - val result = runSuspendCatching { getData(0) }.getOrNull() - result?.let { - state.data.clear() - state.data.addAll(it) - } }.invokeOnCompletion { refreshing = false } } else { TODO() } }) Box(modifier = Modifier.pullRefresh(refreshState)) { - content(state.data.toTypedArray()) PullRefreshIndicator(refreshing = refreshing, state = refreshState, modifier = modifier.align(Alignment.TopCenter)) } } -class ContentState( - val data: SnapshotStateList, +class ContentState( + val data: PagingSource, _startPage: MutableState, _endPage: MutableState, ) { @@ -61,17 +52,10 @@ class ContentState( } @Composable -fun rememberContentState( - initialData: List, +fun rememberContentState( + source: PagingSource, ): ContentState { - val snapshotStateListSaver = remember { - listSaver( - save = { it.toList() }, - restore = { mutableStateListOf().apply { addAll(it) } }, - ) - } - val data = rememberSaveable(saver = snapshotStateListSaver) { mutableStateListOf().apply { addAll(initialData) } } val startPage = rememberSaveable { mutableStateOf(0) } val endPage = rememberSaveable { mutableStateOf(0) } - return remember { ContentState(data, startPage, endPage) } + return remember { ContentState(source, startPage, endPage) } }