From e01b74ee3d1219724b5dbe96aa42c3d051d89864 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 31 May 2020 12:10:43 +0300 Subject: [PATCH] Pagination loading indicator --- .../ui/common/list/BoundsScrollListener.kt | 6 ++- .../common/list/PaginationScrollListener.kt | 15 +++++-- .../ui/common/list/ProgressBarAdapter.kt | 4 +- .../ui/common/list/ProgressBarHolder.kt | 18 +++++++- .../kotatsu/ui/list/MangaListFragment.kt | 42 ++++++++++++++----- .../kotatsu/ui/list/MangaListSheet.kt | 29 ++++++++++--- .../kotatsu/ui/list/feed/FeedFragment.kt | 4 ++ .../kotatsu/ui/list/feed/FeedPresenter.kt | 12 +++++- .../ui/list/local/LocalListFragment.kt | 4 +- .../ui/list/local/LocalListPresenter.kt | 6 ++- .../ui/search/global/GlobalSearchPresenter.kt | 26 ++++++------ app/src/main/res/layout/item_progress.xml | 1 + 12 files changed, 123 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/common/list/BoundsScrollListener.kt b/app/src/main/java/org/koitharu/kotatsu/ui/common/list/BoundsScrollListener.kt index 6a3290c9c..a97dfce74 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/common/list/BoundsScrollListener.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/common/list/BoundsScrollListener.kt @@ -12,13 +12,15 @@ abstract class BoundsScrollListener(private val offsetTop: Int, private val offs super.onScrolled(recyclerView, dx, dy) val layoutManager = (recyclerView.layoutManager as? LinearLayoutManager) ?: return val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() + if (firstVisibleItemPosition == RecyclerView.NO_POSITION) { + return + } if (firstVisibleItemPosition <= offsetTop) { onScrolledToStart(recyclerView) - return } val visibleItemCount = layoutManager.childCount val totalItemCount = layoutManager.itemCount - if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - offsetBottom && firstVisibleItemPosition >= 0) { + if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - offsetBottom) { onScrolledToEnd(recyclerView) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/common/list/PaginationScrollListener.kt b/app/src/main/java/org/koitharu/kotatsu/ui/common/list/PaginationScrollListener.kt index 18ca9f6aa..0ffa5353f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/common/list/PaginationScrollListener.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/common/list/PaginationScrollListener.kt @@ -2,24 +2,31 @@ package org.koitharu.kotatsu.ui.common.list import androidx.recyclerview.widget.RecyclerView -class PaginationScrollListener(offset: Int, private val callback: Callback) : BoundsScrollListener(0, offset) { +class PaginationScrollListener(offset: Int, private val callback: Callback) : + BoundsScrollListener(0, offset) { private var lastTotalCount = 0 - override fun onScrolledToStart(recyclerView: RecyclerView) = Unit + override fun onScrolledToStart(recyclerView: RecyclerView) = Unit override fun onScrolledToEnd(recyclerView: RecyclerView) { - val total = recyclerView.adapter?.itemCount ?: 0 + val total = callback.getItemsCount() if (total > lastTotalCount) { - callback.onRequestMoreItems(total) lastTotalCount = total + callback.onRequestMoreItems(total) } else if (total < lastTotalCount) { lastTotalCount = total } } + fun reset() { + lastTotalCount = 0 + } + interface Callback { fun onRequestMoreItems(offset: Int) + + fun getItemsCount(): Int } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/common/list/ProgressBarAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/ui/common/list/ProgressBarAdapter.kt index 8e6ca78f0..b3adbaf23 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/common/list/ProgressBarAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/common/list/ProgressBarAdapter.kt @@ -4,7 +4,7 @@ import android.view.ViewGroup class ProgressBarAdapter : BaseRecyclerAdapter() { - var isVisible: Boolean + var isProgressVisible: Boolean get() = dataSet.isNotEmpty() set(value) { if (value == dataSet.isEmpty()) { @@ -20,5 +20,5 @@ class ProgressBarAdapter : BaseRecyclerAdapter() { override fun onCreateViewHolder(parent: ViewGroup) = ProgressBarHolder(parent) - override fun onGetItemId(item: Boolean) = 1L + override fun onGetItemId(item: Boolean) = -1L } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/common/list/ProgressBarHolder.kt b/app/src/main/java/org/koitharu/kotatsu/ui/common/list/ProgressBarHolder.kt index 0b21be9a7..6cc0f5e46 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/common/list/ProgressBarHolder.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/common/list/ProgressBarHolder.kt @@ -8,11 +8,27 @@ import org.koitharu.kotatsu.R class ProgressBarHolder(parent: ViewGroup) : BaseViewHolder(parent, R.layout.item_progress) { + private var pendingVisibility: Int = View.GONE + private val action = Runnable { + progressBar?.visibility = pendingVisibility + pendingVisibility = View.GONE + } + override fun onBind(data: Boolean, extra: Unit) { - progressBar.visibility = if (data) { + val visibility = if (data) { View.VISIBLE } else { View.INVISIBLE } + if (visibility != progressBar.visibility && visibility != pendingVisibility) { + progressBar.removeCallbacks(action) + pendingVisibility = visibility + progressBar.postDelayed(action, 400) + } + } + + override fun onRecycled() { + progressBar.removeCallbacks(action) + super.onRecycled() } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/list/MangaListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/ui/list/MangaListFragment.kt index 2e291db54..360d7e520 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/list/MangaListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/list/MangaListFragment.kt @@ -9,10 +9,7 @@ import androidx.core.view.GravityCompat import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.drawerlayout.widget.DrawerLayout -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.* import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.fragment_list.* @@ -28,6 +25,7 @@ import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.ui.common.BaseFragment import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener import org.koitharu.kotatsu.ui.common.list.PaginationScrollListener +import org.koitharu.kotatsu.ui.common.list.ProgressBarAdapter import org.koitharu.kotatsu.ui.common.list.decor.ItemTypeDividerDecoration import org.koitharu.kotatsu.ui.common.list.decor.SectionItemDecoration import org.koitharu.kotatsu.ui.common.list.decor.SpacingItemDecoration @@ -38,14 +36,19 @@ import org.koitharu.kotatsu.utils.UiUtils import org.koitharu.kotatsu.utils.ext.* abstract class MangaListFragment : BaseFragment(R.layout.fragment_list), - MangaListView, - PaginationScrollListener.Callback, OnRecyclerItemClickListener, + MangaListView, PaginationScrollListener.Callback, OnRecyclerItemClickListener, SharedPreferences.OnSharedPreferenceChangeListener, OnFilterChangedListener, SectionItemDecoration.Callback, SwipeRefreshLayout.OnRefreshListener { private val settings by inject() + private val adapterConfig = MergeAdapter.Config.Builder() + .setIsolateViewTypes(true) + .setStableIdMode(MergeAdapter.Config.StableIdMode.SHARED_STABLE_IDS) + .build() private var adapter: MangaListAdapter? = null + private var progressAdapter: ProgressBarAdapter? = null + private var paginationListener : PaginationScrollListener? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -56,10 +59,11 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list), super.onViewCreated(view, savedInstanceState) drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) adapter = MangaListAdapter(this) + progressAdapter = ProgressBarAdapter() + paginationListener = PaginationScrollListener(4, this) recyclerView.setHasFixedSize(true) initListMode(settings.listMode) - // recyclerView.adapter = adapter - recyclerView.addOnScrollListener(PaginationScrollListener(4, this)) + recyclerView.addOnScrollListener(paginationListener!!) swipeRefreshLayout.setOnRefreshListener(this) recyclerView_filter.setHasFixedSize(true) recyclerView_filter.addItemDecoration(ItemTypeDividerDecoration(view.context)) @@ -72,6 +76,8 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list), override fun onDestroyView() { adapter = null + progressAdapter = null + paginationListener = null settings.unsubscribe(this) super.onDestroyView() } @@ -123,6 +129,7 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list), } override fun onListChanged(list: List) { + paginationListener?.reset() adapter?.replaceData(list) if (list.isEmpty()) { setUpEmptyListHolder() @@ -130,11 +137,16 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list), } else { layout_holder.isVisible = false } + progressAdapter?.isProgressVisible = list.isNotEmpty() recyclerView.callOnScrollListeners() } override fun onListAppended(list: List) { adapter?.appendData(list) + progressAdapter?.isProgressVisible = list.isNotEmpty() + if (list.isNotEmpty()) { + layout_holder.isVisible = false + } recyclerView.callOnScrollListeners() } @@ -231,11 +243,19 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list), recyclerView.removeOnLayoutChangeListener(UiUtils.SpanCountResolver) adapter?.listMode = mode recyclerView.layoutManager = when (mode) { - ListMode.GRID -> GridLayoutManager(ctx, UiUtils.resolveGridSpanCount(ctx)) + ListMode.GRID -> { + val spanCount = UiUtils.resolveGridSpanCount(ctx) + GridLayoutManager(ctx, spanCount).apply { + spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int) = if (position < getItemsCount()) + 1 else spanCount + } + } + } else -> LinearLayoutManager(ctx) } recyclerView.recycledViewPool.clear() - recyclerView.adapter = adapter + recyclerView.adapter = MergeAdapter(adapterConfig, adapter, progressAdapter) recyclerView.addItemDecoration( when (mode) { ListMode.LIST -> DividerItemDecoration(ctx, RecyclerView.VERTICAL) @@ -252,6 +272,8 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list), recyclerView.firstItem = position } + override fun getItemsCount() = adapter?.itemCount ?: 0 + final override fun isSection(position: Int): Boolean { return position == 0 || recyclerView_filter.adapter?.run { getItemViewType(position) != getItemViewType(position - 1) diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/list/MangaListSheet.kt b/app/src/main/java/org/koitharu/kotatsu/ui/list/MangaListSheet.kt index 85d95bcb5..ddddee7db 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/list/MangaListSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/list/MangaListSheet.kt @@ -7,10 +7,7 @@ import android.view.View import androidx.appcompat.widget.Toolbar import androidx.core.view.isGone import androidx.core.view.isVisible -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.* import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.snackbar.Snackbar @@ -27,6 +24,7 @@ import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.ui.common.BaseBottomSheet import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener import org.koitharu.kotatsu.ui.common.list.PaginationScrollListener +import org.koitharu.kotatsu.ui.common.list.ProgressBarAdapter import org.koitharu.kotatsu.ui.common.list.decor.SpacingItemDecoration import org.koitharu.kotatsu.ui.details.MangaDetailsActivity import org.koitharu.kotatsu.utils.UiUtils @@ -38,12 +36,18 @@ abstract class MangaListSheet : BaseBottomSheet(R.layout.sheet_list), SharedPreferences.OnSharedPreferenceChangeListener, Toolbar.OnMenuItemClickListener { private val settings by inject() + private val adapterConfig = MergeAdapter.Config.Builder() + .setIsolateViewTypes(true) + .setStableIdMode(MergeAdapter.Config.StableIdMode.SHARED_STABLE_IDS) + .build() private var adapter: MangaListAdapter? = null + private var progressAdapter: ProgressBarAdapter? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) adapter = MangaListAdapter(this) + progressAdapter = ProgressBarAdapter() initListMode(settings.listMode) recyclerView.adapter = adapter recyclerView.addOnScrollListener(PaginationScrollListener(4, this)) @@ -66,6 +70,7 @@ abstract class MangaListSheet : BaseBottomSheet(R.layout.sheet_list), override fun onDestroyView() { settings.unsubscribe(this) adapter = null + progressAdapter = null super.onDestroyView() } @@ -123,6 +128,7 @@ abstract class MangaListSheet : BaseBottomSheet(R.layout.sheet_list), override fun onListChanged(list: List) { adapter?.replaceData(list) textView_holder.isVisible = list.isEmpty() + progressAdapter?.isProgressVisible = list.isNotEmpty() recyclerView.callOnScrollListeners() } @@ -131,6 +137,7 @@ abstract class MangaListSheet : BaseBottomSheet(R.layout.sheet_list), if (list.isNotEmpty()) { textView_holder.isVisible = false } + progressAdapter?.isProgressVisible = list.isNotEmpty() recyclerView.callOnScrollListeners() } @@ -138,6 +145,8 @@ abstract class MangaListSheet : BaseBottomSheet(R.layout.sheet_list), Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT).show() } + override fun getItemsCount() = adapter?.itemCount ?: 0 + override fun onInitFilter( sortOrders: List, tags: List, @@ -171,10 +180,18 @@ abstract class MangaListSheet : BaseBottomSheet(R.layout.sheet_list), recyclerView.removeOnLayoutChangeListener(UiUtils.SpanCountResolver) adapter?.listMode = mode recyclerView.layoutManager = when (mode) { - ListMode.GRID -> GridLayoutManager(ctx, UiUtils.resolveGridSpanCount(ctx)) + ListMode.GRID -> { + val spanCount = UiUtils.resolveGridSpanCount(ctx) + GridLayoutManager(ctx, spanCount).apply { + spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int) = if (position < getItemsCount()) + 1 else spanCount + } + } + } else -> LinearLayoutManager(ctx) } - recyclerView.adapter = adapter + recyclerView.adapter = MergeAdapter(adapterConfig, adapter, progressAdapter) recyclerView.addItemDecoration( when (mode) { ListMode.LIST -> DividerItemDecoration(ctx, RecyclerView.VERTICAL) diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/list/feed/FeedFragment.kt b/app/src/main/java/org/koitharu/kotatsu/ui/list/feed/FeedFragment.kt index 0194adac1..7004e86cf 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/list/feed/FeedFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/list/feed/FeedFragment.kt @@ -102,6 +102,10 @@ class FeedFragment : BaseFragment(R.layout.fragment_tracklogs), FeedView, } } + override fun getItemsCount(): Int { + return adapter?.itemCount ?: 0 + } + override fun onRequestMoreItems(offset: Int) { presenter.loadList(offset) } diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/list/feed/FeedPresenter.kt b/app/src/main/java/org/koitharu/kotatsu/ui/list/feed/FeedPresenter.kt index 0ac7c8b2b..5c778a0c0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/list/feed/FeedPresenter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/list/feed/FeedPresenter.kt @@ -25,13 +25,21 @@ class FeedPresenter : BasePresenter() { val list = withContext(Dispatchers.IO) { repository.getTrackingLog(offset, 20) } - viewState.onListChanged(list) + if (offset == 0) { + viewState.onListChanged(list) + } else { + viewState.onListAppended(list) + } } catch (e: CancellationException) { } catch (e: Throwable) { if (BuildConfig.DEBUG) { e.printStackTrace() } - viewState.onListError(e) + if (offset == 0) { + viewState.onListError(e) + } else { + viewState.onError(e) + } } finally { viewState.onLoadingStateChanged(false) } diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/list/local/LocalListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/ui/list/local/LocalListFragment.kt index 7c55fc083..7023f81ba 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/list/local/LocalListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/list/local/LocalListFragment.kt @@ -23,9 +23,7 @@ class LocalListFragment : MangaListFragment(), ActivityResultCallback private val presenter by moxyPresenter(factory = ::LocalListPresenter) override fun onRequestMoreItems(offset: Int) { - if (offset == 0) { - presenter.loadList() - } + presenter.loadList(offset) } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/list/local/LocalListPresenter.kt b/app/src/main/java/org/koitharu/kotatsu/ui/list/local/LocalListPresenter.kt index 444c50825..53cbe4123 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/list/local/LocalListPresenter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/list/local/LocalListPresenter.kt @@ -37,7 +37,11 @@ class LocalListPresenter : BasePresenter>() { super.onFirstViewAttach() } - fun loadList() { + fun loadList(offset: Int) { + if (offset != 0) { + viewState.onListAppended(emptyList()) + return + } presenterScope.launch { viewState.onLoadingStateChanged(true) try { diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/search/global/GlobalSearchPresenter.kt b/app/src/main/java/org/koitharu/kotatsu/ui/search/global/GlobalSearchPresenter.kt index 730dc9ec8..5e39b8cf2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/search/global/GlobalSearchPresenter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/search/global/GlobalSearchPresenter.kt @@ -1,16 +1,12 @@ package org.koitharu.kotatsu.ui.search.global import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.onEmpty +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import moxy.presenterScope import org.koitharu.kotatsu.domain.MangaSearchRepository import org.koitharu.kotatsu.ui.common.BasePresenter import org.koitharu.kotatsu.ui.list.MangaListView -import org.koitharu.kotatsu.utils.ext.onFirst import java.io.IOException class GlobalSearchPresenter : BasePresenter>() { @@ -26,23 +22,27 @@ class GlobalSearchPresenter : BasePresenter>() { fun startSearch(query: String) { presenterScope.launch { viewState.onLoadingStateChanged(isLoading = true) + var isFirstCall = true repository.globalSearch(query) .flowOn(Dispatchers.IO) .catch { e -> if (e is IOException) { viewState.onError(e) } - } - .onFirst { - viewState.onListChanged(emptyList()) - viewState.onLoadingStateChanged(isLoading = false) - } + }.filterNot { x -> x.isEmpty() } .onEmpty { viewState.onListChanged(emptyList()) viewState.onLoadingStateChanged(isLoading = false) - } - .collect { - viewState.onListAppended(it) + }.onCompletion { + viewState.onListAppended(emptyList()) + }.collect { + if (isFirstCall) { + isFirstCall = false + viewState.onListChanged(it) + viewState.onLoadingStateChanged(isLoading = false) + } else { + viewState.onListAppended(it) + } } } } diff --git a/app/src/main/res/layout/item_progress.xml b/app/src/main/res/layout/item_progress.xml index 2ca64e856..d060af2fb 100644 --- a/app/src/main/res/layout/item_progress.xml +++ b/app/src/main/res/layout/item_progress.xml @@ -9,6 +9,7 @@ android:id="@+id/progressBar" style="@style/Widget.AppCompat.ProgressBar" android:layout_width="wrap_content" + android:visibility="invisible" android:layout_height="wrap_content" android:layout_gravity="center" />