Pagination loading indicator

This commit is contained in:
Koitharu
2020-05-31 12:10:43 +03:00
parent 3539e6a892
commit e01b74ee3d
12 changed files with 123 additions and 44 deletions

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -4,7 +4,7 @@ import android.view.ViewGroup
class ProgressBarAdapter : BaseRecyclerAdapter<Boolean, Unit>() {
var isVisible: Boolean
var isProgressVisible: Boolean
get() = dataSet.isNotEmpty()
set(value) {
if (value == dataSet.isEmpty()) {
@@ -20,5 +20,5 @@ class ProgressBarAdapter : BaseRecyclerAdapter<Boolean, Unit>() {
override fun onCreateViewHolder(parent: ViewGroup) = ProgressBarHolder(parent)
override fun onGetItemId(item: Boolean) = 1L
override fun onGetItemId(item: Boolean) = -1L
}

View File

@@ -8,11 +8,27 @@ import org.koitharu.kotatsu.R
class ProgressBarHolder(parent: ViewGroup) :
BaseViewHolder<Boolean, Unit>(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()
}
}

View File

@@ -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<E> : BaseFragment(R.layout.fragment_list),
MangaListView<E>,
PaginationScrollListener.Callback, OnRecyclerItemClickListener<Manga>,
MangaListView<E>, PaginationScrollListener.Callback, OnRecyclerItemClickListener<Manga>,
SharedPreferences.OnSharedPreferenceChangeListener, OnFilterChangedListener,
SectionItemDecoration.Callback, SwipeRefreshLayout.OnRefreshListener {
private val settings by inject<AppSettings>()
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<E> : 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<E> : 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<E> : BaseFragment(R.layout.fragment_list),
}
override fun onListChanged(list: List<Manga>) {
paginationListener?.reset()
adapter?.replaceData(list)
if (list.isEmpty()) {
setUpEmptyListHolder()
@@ -130,11 +137,16 @@ abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list),
} else {
layout_holder.isVisible = false
}
progressAdapter?.isProgressVisible = list.isNotEmpty()
recyclerView.callOnScrollListeners()
}
override fun onListAppended(list: List<Manga>) {
adapter?.appendData(list)
progressAdapter?.isProgressVisible = list.isNotEmpty()
if (list.isNotEmpty()) {
layout_holder.isVisible = false
}
recyclerView.callOnScrollListeners()
}
@@ -231,11 +243,19 @@ abstract class MangaListFragment<E> : 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<E> : 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)

View File

@@ -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<E> : BaseBottomSheet(R.layout.sheet_list),
SharedPreferences.OnSharedPreferenceChangeListener, Toolbar.OnMenuItemClickListener {
private val settings by inject<AppSettings>()
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<E> : BaseBottomSheet(R.layout.sheet_list),
override fun onDestroyView() {
settings.unsubscribe(this)
adapter = null
progressAdapter = null
super.onDestroyView()
}
@@ -123,6 +128,7 @@ abstract class MangaListSheet<E> : BaseBottomSheet(R.layout.sheet_list),
override fun onListChanged(list: List<Manga>) {
adapter?.replaceData(list)
textView_holder.isVisible = list.isEmpty()
progressAdapter?.isProgressVisible = list.isNotEmpty()
recyclerView.callOnScrollListeners()
}
@@ -131,6 +137,7 @@ abstract class MangaListSheet<E> : 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<E> : 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<SortOrder>,
tags: List<MangaTag>,
@@ -171,10 +180,18 @@ abstract class MangaListSheet<E> : 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)

View File

@@ -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)
}

View File

@@ -25,13 +25,21 @@ class FeedPresenter : BasePresenter<FeedView>() {
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)
}

View File

@@ -23,9 +23,7 @@ class LocalListFragment : MangaListFragment<File>(), ActivityResultCallback<Uri>
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) {

View File

@@ -37,7 +37,11 @@ class LocalListPresenter : BasePresenter<MangaListView<File>>() {
super.onFirstViewAttach()
}
fun loadList() {
fun loadList(offset: Int) {
if (offset != 0) {
viewState.onListAppended(emptyList())
return
}
presenterScope.launch {
viewState.onLoadingStateChanged(true)
try {

View File

@@ -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<MangaListView<Unit>>() {
@@ -26,23 +22,27 @@ class GlobalSearchPresenter : BasePresenter<MangaListView<Unit>>() {
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)
}
}
}
}

View File

@@ -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" />