Pagination loading indicator
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user