Rewrite catalogue adapter

This commit is contained in:
len 2017-01-17 20:13:29 +01:00
parent f86c3c81bf
commit 871e17c2f5
12 changed files with 207 additions and 213 deletions

View File

@ -1,107 +0,0 @@
package eu.kanade.tachiyomi.ui.catalogue
import android.view.Gravity
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout
import eu.davidea.flexibleadapter4.FlexibleAdapter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.util.inflate
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
import java.util.*
/**
* Adapter storing a list of manga from the catalogue.
*
* @param fragment the fragment containing this adapter.
*/
class CatalogueAdapter(val fragment: CatalogueFragment) : FlexibleAdapter<CatalogueHolder, Manga>() {
/**
* Property to get the list of manga in the adapter.
*/
val items: List<Manga>
get() = mItems
init {
mItems = ArrayList()
setHasStableIds(true)
}
/**
* Adds a list of manga to the adapter.
*
* @param list the list to add.
*/
fun addItems(list: List<Manga>) {
if (list.isNotEmpty()) {
val sizeBeforeAdding = mItems.size
mItems.addAll(list)
notifyItemRangeInserted(sizeBeforeAdding, list.size)
}
}
/**
* Clears the list of manga from the adapter.
*/
fun clear() {
val sizeBeforeRemoving = mItems.size
mItems.clear()
notifyItemRangeRemoved(0, sizeBeforeRemoving)
}
/**
* Returns the identifier for a manga.
*
* @param position the position in the adapter.
* @return an identifier for the item.
*/
override fun getItemId(position: Int): Long {
return mItems[position].id!!
}
/**
* Used to filter the list. Required but not used.
*/
override fun updateDataSet(param: String) {}
/**
* Creates a new view holder.
*
* @param parent the parent view.
* @param viewType the type of the holder.
* @return a new view holder for a manga.
*/
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CatalogueHolder {
if (parent.id == R.id.catalogue_grid) {
val view = parent.inflate(R.layout.item_catalogue_grid).apply {
card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight)
gradient.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM)
}
return CatalogueGridHolder(view, this, fragment)
} else {
val view = parent.inflate(R.layout.item_catalogue_list)
return CatalogueListHolder(view, this, fragment)
}
}
/**
* Binds a holder with a new position.
*
* @param holder the holder to bind.
* @param position the position to bind.
*/
override fun onBindViewHolder(holder: CatalogueHolder, position: Int) {
val manga = getItem(position)
holder.onSetValues(manga)
}
/**
* Property to return the height for the covers based on the width to keep an aspect ratio.
*/
val coverHeight: Int
get() = (fragment.recycler as AutofitRecyclerView).itemWidth / 3 * 4
}

View File

@ -11,11 +11,12 @@ import android.widget.ProgressBar
import android.widget.Spinner
import com.afollestad.materialdialogs.MaterialDialog
import com.f2prateek.rx.preferences.Preference
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.source.model.FilterList
import eu.kanade.tachiyomi.data.source.online.LoginSource
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaActivity
@ -24,7 +25,6 @@ import eu.kanade.tachiyomi.util.inflate
import eu.kanade.tachiyomi.util.snack
import eu.kanade.tachiyomi.util.toast
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import eu.kanade.tachiyomi.widget.EndlessScrollListener
import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.fragment_catalogue.*
@ -40,7 +40,10 @@ import java.util.concurrent.TimeUnit.MILLISECONDS
* Uses R.layout.fragment_catalogue.
*/
@RequiresPresenter(CataloguePresenter::class)
open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHolder.OnListItemClickListener {
open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
FlexibleAdapter.EndlessScrollListener<ProgressItem> {
/**
* Spinner shown in the toolbar to change the selected source.
@ -50,12 +53,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
/**
* Adapter containing the list of manga from the catalogue.
*/
private lateinit var adapter: CatalogueAdapter
/**
* Scroll listener. It loads next pages when the end of the list is reached.
*/
private var scrollListener: EndlessScrollListener? = null
private lateinit var adapter: FlexibleAdapter<IFlexible<*>>
/**
* Query of the search box.
@ -130,6 +128,8 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
lateinit var recycler: RecyclerView
private var progressItem: ProgressItem? = null
companion object {
/**
* Creates a new instance of this fragment.
@ -160,7 +160,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
}
// Initialize adapter, scroll listener and recycler views
adapter = CatalogueAdapter(this)
adapter = FlexibleAdapter(null, this)
setupRecycler()
// Create toolbar spinner
@ -251,11 +251,18 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
.skip(1)
// Set again the adapter to recalculate the covers height
.subscribe { adapter = this@CatalogueFragment.adapter }
(layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return when (adapter.getItemViewType(position)) {
R.layout.item_catalogue_grid -> 1
else -> spanCount
}
}
}
}
}
scrollListener = EndlessScrollListener(recycler.layoutManager as LinearLayoutManager, { requestNextPage() })
recycler.setHasFixedSize(true)
recycler.addOnScrollListener(scrollListener)
recycler.adapter = adapter
catalogue_view.addView(recycler, 1)
@ -376,29 +383,19 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
presenter.restartPager(newQuery)
}
/**
* Requests the next page (if available). Called from scroll listeners when they reach the end.
*/
private fun requestNextPage() {
if (presenter.hasNextPage()) {
showGridProgressBar()
presenter.requestNext()
}
}
/**
* Called from the presenter when the network request is received.
*
* @param page the current page.
* @param mangas the list of manga of the page.
*/
fun onAddPage(page: Int, mangas: List<Manga>) {
fun onAddPage(page: Int, mangas: List<CatalogueItem>) {
hideProgressBar()
if (page == 1) {
adapter.clear()
scrollListener?.resetScroll()
resetProgressItem()
}
adapter.addItems(mangas)
adapter.onLoadMoreComplete(mangas)
}
/**
@ -407,6 +404,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
* @param error the error received.
*/
fun onAddPageError(error: Throwable) {
adapter.onLoadMoreComplete(null)
hideProgressBar()
val message = if (error is NoResultsException) "No results found" else (error.message ?: "")
@ -414,12 +412,42 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
snack?.dismiss()
snack = catalogue_view.snack(message, Snackbar.LENGTH_INDEFINITE) {
setAction(R.string.action_retry) {
showProgressBar()
// If not the first page, show bottom progress bar.
if (adapter.mainItemCount > 0) {
val item = progressItem ?: return@setAction
adapter.addScrollableFooterWithDelay(item, 0, true)
} else {
showProgressBar()
}
presenter.requestNext()
}
}
}
/**
* Sets a new progress item and reenables the scroll listener.
*/
private fun resetProgressItem() {
progressItem = ProgressItem()
adapter.endlessTargetCount = 0
adapter.setEndlessScrollListener(this, progressItem!!)
}
/**
* Called by the adapter when scrolled near the bottom.
*/
override fun onLoadMore(lastPosition: Int, currentPage: Int) {
if (presenter.hasNextPage()) {
presenter.requestNext()
} else {
adapter.onLoadMoreComplete(null)
adapter.endlessTargetCount = 1
}
}
override fun noMoreLoad(newItemsSize: Int) {
}
/**
* Called from the presenter when a manga is initialized.
*
@ -433,13 +461,18 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
* Swaps the current display mode.
*/
fun swapDisplayMode() {
if (!isAdded) return
presenter.swapDisplayMode()
val isListMode = presenter.isListMode
activity.invalidateOptionsMenu()
setupRecycler()
if (!isListMode || !context.connectivityManager.isActiveNetworkMetered) {
// Initialize mangas if going to grid view or if over wifi when going to list view
presenter.initializeMangas(adapter.items)
val mangas = (0..adapter.itemCount-1).mapNotNull {
(adapter.getItem(it) as? CatalogueItem)?.manga
}
presenter.initializeMangas(mangas)
}
}
@ -474,21 +507,11 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
snack = null
}
/**
* Shows the progress bar at the end of the screen.
*/
private fun showGridProgressBar() {
progress_grid.visibility = ProgressBar.VISIBLE
snack?.dismiss()
snack = null
}
/**
* Hides active progress bars.
*/
private fun hideProgressBar() {
progress.visibility = ProgressBar.GONE
progress_grid.visibility = ProgressBar.GONE
}
/**
@ -497,10 +520,10 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
* @param position the position of the element clicked.
* @return true if the item should be selected, false otherwise.
*/
override fun onListItemClick(position: Int): Boolean {
val item = adapter.getItem(position) ?: return false
override fun onItemClick(position: Int): Boolean {
val item = adapter.getItem(position) as? CatalogueItem ?: return false
val intent = MangaActivity.newIntent(activity, item, true)
val intent = MangaActivity.newIntent(activity, item.manga, true)
startActivity(intent)
return false
}
@ -510,8 +533,8 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
*
* @param position the position of the element clicked.
*/
override fun onListItemLongClick(position: Int) {
val manga = adapter.getItem(position) ?: return
override fun onItemLongClick(position: Int) {
val manga = (adapter.getItem(position) as? CatalogueItem?)?.manga ?: return
val textRes = if (manga.favorite) R.string.remove_from_library else R.string.add_to_library

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.catalogue
import android.view.View
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga
import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
@ -12,11 +13,10 @@ import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
*
* @param view the inflated view for this holder.
* @param adapter the adapter handling this holder.
* @param listener a listener to react to single tap and long tap events.
* @constructor creates a new catalogue holder.
*/
class CatalogueGridHolder(private val view: View, private val adapter: CatalogueAdapter, listener: OnListItemClickListener) :
CatalogueHolder(view, adapter, listener) {
class CatalogueGridHolder(private val view: View, private val adapter: FlexibleAdapter<*>) :
CatalogueHolder(view, adapter) {
/**
* Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this

View File

@ -1,18 +1,18 @@
package eu.kanade.tachiyomi.ui.catalogue
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
/**
* Generic class used to hold the displayed data of a manga in the catalogue.
*
* @param view the inflated view for this holder.
* @param adapter the adapter handling this holder.
* @param listener a listener to react to single tap and long tap events.
*/
abstract class CatalogueHolder(view: View, adapter: CatalogueAdapter, listener: OnListItemClickListener) :
FlexibleViewHolder(view, adapter, listener) {
abstract class CatalogueHolder(view: View, adapter: FlexibleAdapter<*>) :
FlexibleViewHolder(view, adapter) {
/**
* Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this

View File

@ -0,0 +1,53 @@
package eu.kanade.tachiyomi.ui.catalogue
import android.view.Gravity
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.FrameLayout
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.util.inflate
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
class CatalogueItem(val manga: Manga) : AbstractFlexibleItem<CatalogueHolder>() {
override fun getLayoutRes(): Int {
return R.layout.item_catalogue_grid
}
override fun createViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, inflater: LayoutInflater, parent: ViewGroup): CatalogueHolder {
if (parent is AutofitRecyclerView) {
val view = parent.inflate(R.layout.item_catalogue_grid).apply {
card.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parent.itemWidth / 3 * 4)
gradient.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parent.itemWidth / 3 * 4 / 2, Gravity.BOTTOM)
}
return CatalogueGridHolder(view, adapter)
} else {
val view = parent.inflate(R.layout.item_catalogue_list)
return CatalogueListHolder(view, adapter)
}
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: CatalogueHolder, position: Int, payloads: List<Any?>?) {
holder.onSetValues(manga)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other is CatalogueItem) {
return manga.id!! == other.manga.id!!
}
return false
}
override fun hashCode(): Int {
return manga.id!!.hashCode()
}
}

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.catalogue
import android.view.View
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.util.getResourceColor
import kotlinx.android.synthetic.main.item_catalogue_list.view.*
@ -13,11 +14,10 @@ import kotlinx.android.synthetic.main.item_catalogue_list.view.*
*
* @param view the inflated view for this holder.
* @param adapter the adapter handling this holder.
* @param listener a listener to react to single tap and long tap events.
* @constructor creates a new catalogue holder.
*/
class CatalogueListHolder(private val view: View, adapter: CatalogueAdapter, listener: OnListItemClickListener) :
CatalogueHolder(view, adapter, listener) {
class CatalogueListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
CatalogueHolder(view, adapter) {
private val favoriteColor = view.context.getResourceColor(android.R.attr.textColorHint)
private val unfavoriteColor = view.context.getResourceColor(android.R.attr.textColorPrimary)

View File

@ -163,6 +163,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
.observeOn(Schedulers.io())
.map { it.first to it.second.map { networkToLocalManga(it, sourceId) } }
.doOnNext { initializeMangas(it.second) }
.map { it.first to it.second.map(::CatalogueItem) }
.observeOn(AndroidSchedulers.mainThread())
.subscribeReplay({ view, pair ->
view.onAddPage(pair.first, pair.second)

View File

@ -0,0 +1,52 @@
package eu.kanade.tachiyomi.ui.catalogue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ProgressBar
import android.widget.TextView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
class ProgressItem : AbstractFlexibleItem<ProgressItem.Holder>() {
var loadMore = true
override fun getLayoutRes(): Int {
return R.layout.progress_item
}
override fun createViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, inflater: LayoutInflater, parent: ViewGroup): Holder {
return Holder(inflater.inflate(layoutRes, parent, false), adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: Holder, position: Int, payloads: List<Any?>) {
holder.progressBar.visibility = View.GONE
holder.progressMessage.visibility = View.GONE
if (!adapter.isEndlessScrollEnabled) {
loadMore = false
}
if (loadMore) {
holder.progressBar.visibility = View.VISIBLE
} else {
holder.progressMessage.visibility = View.VISIBLE
}
}
override fun equals(other: Any?): Boolean {
return this === other
}
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
val progressBar = view.findViewById(R.id.progress_bar) as ProgressBar
val progressMessage = view.findViewById(R.id.progress_message) as TextView
}
}

View File

@ -1,46 +0,0 @@
package eu.kanade.tachiyomi.widget
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
class EndlessScrollListener(
private val layoutManager: LinearLayoutManager,
private val requestNext: () -> Unit)
: RecyclerView.OnScrollListener() {
companion object {
// The minimum amount of items to have below your current scroll position before loading
// more.
private val VISIBLE_THRESHOLD = 5
}
private var previousTotal = 0 // The total number of items in the dataset after the last load
private var loading = true // True if we are still waiting for the last set of data to load.
private var firstVisibleItem = 0
private var visibleItemCount = 0
private var totalItemCount = 0
fun resetScroll() {
previousTotal = 0
loading = true
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
visibleItemCount = recyclerView.childCount
totalItemCount = layoutManager.itemCount
firstVisibleItem = layoutManager.findFirstVisibleItemPosition()
if (loading && totalItemCount > previousTotal) {
loading = false
previousTotal = totalItemCount
}
if (!loading && totalItemCount - visibleItemCount <= firstVisibleItem + VISIBLE_THRESHOLD) {
// End has been reached
requestNext()
loading = true
}
}
}

View File

@ -21,14 +21,6 @@
android:layout_gravity="center_vertical|center_horizontal"
android:visibility="gone"/>
<ProgressBar
android:id="@+id/progress_grid"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|center_horizontal"
android:visibility="gone"/>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
android:padding="8dp">
<ProgressBar
android:id="@+id/progress_bar"
style="@style/Widget.AppCompat.ProgressBar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"/>
<TextView
android:id="@+id/progress_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/no_more_results"
android:layout_gravity="center"
android:visibility="gone"
tools:visibility="visible"/>
</FrameLayout>

View File

@ -223,6 +223,7 @@
<string name="source_requires_login">This source requires you to log in</string>
<string name="select_source">Select a source</string>
<string name="no_valid_sources">Please enable at least one valid source</string>
<string name="no_more_results">No more results</string>
<!-- Manga activity -->
<string name="manga_not_in_db">This manga was removed from the database!</string>