From 868058a50b673f9b41ebd2a131cb96dd04feb30b Mon Sep 17 00:00:00 2001 From: inorichi Date: Sun, 3 Jan 2016 00:30:20 +0100 Subject: [PATCH] Use RecyclerView for catalogue --- .../mangafeed/data/database/models/Manga.java | 35 ++++---- .../ui/catalogue/CatalogueAdapter.java | 85 ++++++++----------- .../ui/catalogue/CatalogueFragment.java | 80 ++++++++--------- .../ui/catalogue/CatalogueHolder.java | 38 +++++++++ .../ui/catalogue/CataloguePresenter.java | 16 ++-- .../ui/manga/info/MangaInfoPresenter.java | 2 +- .../widget/EndlessRecyclerScrollListener.java | 49 +++++++++++ .../main/res/layout/fragment_catalogue.xml | 4 +- 8 files changed, 186 insertions(+), 123 deletions(-) create mode 100644 app/src/main/java/eu/kanade/mangafeed/ui/catalogue/CatalogueHolder.java create mode 100644 app/src/main/java/eu/kanade/mangafeed/widget/EndlessRecyclerScrollListener.java diff --git a/app/src/main/java/eu/kanade/mangafeed/data/database/models/Manga.java b/app/src/main/java/eu/kanade/mangafeed/data/database/models/Manga.java index 7342262dd..81dd90261 100644 --- a/app/src/main/java/eu/kanade/mangafeed/data/database/models/Manga.java +++ b/app/src/main/java/eu/kanade/mangafeed/data/database/models/Manga.java @@ -77,32 +77,31 @@ public class Manga implements Serializable { this.url = UrlUtil.getPath(url); } - public static void copyFromNetwork(Manga local, Manga network) { - if (network.title != null) - local.title = network.title; + public void copyFrom(Manga other) { + if (other.title != null) + title = other.title; - if (network.author != null) - local.author = network.author; + if (other.author != null) + author = other.author; - if (network.artist != null) - local.artist = network.artist; + if (other.artist != null) + artist = other.artist; - if (network.url != null) - local.url = network.url; + if (other.url != null) + url = other.url; - if (network.description != null) - local.description = network.description; + if (other.description != null) + description = other.description; - if (network.genre != null) - local.genre = network.genre; + if (other.genre != null) + genre = other.genre; - if (network.thumbnail_url != null) - local.thumbnail_url = network.thumbnail_url; + if (other.thumbnail_url != null) + thumbnail_url = other.thumbnail_url; - local.status = network.status; - - local.initialized = true; + status = other.status; + initialized = true; } @Override diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/catalogue/CatalogueAdapter.java b/app/src/main/java/eu/kanade/mangafeed/ui/catalogue/CatalogueAdapter.java index 10958b262..1ec457b61 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/catalogue/CatalogueAdapter.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/catalogue/CatalogueAdapter.java @@ -3,71 +3,58 @@ package eu.kanade.mangafeed.ui.catalogue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.TextView; import java.util.ArrayList; +import java.util.List; -import butterknife.Bind; -import butterknife.ButterKnife; +import eu.davidea.flexibleadapter.FlexibleAdapter; import eu.kanade.mangafeed.R; import eu.kanade.mangafeed.data.database.models.Manga; -public class CatalogueAdapter extends ArrayAdapter { +public class CatalogueAdapter extends FlexibleAdapter { private CatalogueFragment fragment; - private LayoutInflater inflater; public CatalogueAdapter(CatalogueFragment fragment) { - super(fragment.getActivity(), 0, new ArrayList<>()); this.fragment = fragment; - inflater = fragment.getActivity().getLayoutInflater(); + mItems = new ArrayList<>(); + setHasStableIds(true); + } + + public void addItems(List list) { + mItems.addAll(list); + notifyDataSetChanged(); + } + + public void clear() { + mItems.clear(); + notifyDataSetChanged(); } @Override - public View getView(int position, View view, ViewGroup parent) { - Manga manga = getItem(position); - - ViewHolder holder; - if (view != null) { - holder = (ViewHolder) view.getTag(); - } else { - view = inflater.inflate(R.layout.item_catalogue, parent, false); - holder = new ViewHolder(view, fragment); - view.setTag(holder); - } - holder.onSetValues(manga); - return view; + public long getItemId(int position) { + return mItems.get(position).id; } - static class ViewHolder { - @Bind(R.id.title) TextView title; - @Bind(R.id.thumbnail) ImageView thumbnail; - @Bind(R.id.favorite_sticker) ImageView favorite_sticker; + @Override + public void updateDataSet(String param) { - CataloguePresenter presenter; - - public ViewHolder(View view, CatalogueFragment fragment) { - ButterKnife.bind(this, view); - presenter = fragment.getPresenter(); - } - - public void onSetValues(Manga manga) { - title.setText(manga.title); - - if (manga.thumbnail_url != null) { - presenter.coverCache.loadFromCacheOrNetwork(thumbnail, manga.thumbnail_url, - presenter.getSource().getGlideHeaders()); - } else { - thumbnail.setImageResource(android.R.color.transparent); - } - - if (manga.favorite) { - favorite_sticker.setVisibility(View.VISIBLE); - } else { - favorite_sticker.setVisibility(View.INVISIBLE); - } - } } + + @Override + public CatalogueHolder onCreateViewHolder(ViewGroup parent, int viewType) { + LayoutInflater inflater = fragment.getActivity().getLayoutInflater(); + View v = inflater.inflate(R.layout.item_catalogue, parent, false); + return new CatalogueHolder(v, this, fragment); + } + + @Override + public void onBindViewHolder(CatalogueHolder holder, int position) { + final Manga manga = getItem(position); + holder.onSetValues(manga, fragment.getPresenter()); + + //When user scrolls this bind the correct selection status + //holder.itemView.setActivated(isSelected(position)); + } + } diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/catalogue/CatalogueFragment.java b/app/src/main/java/eu/kanade/mangafeed/ui/catalogue/CatalogueFragment.java index f9c56be26..2abf00cd8 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/catalogue/CatalogueFragment.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/catalogue/CatalogueFragment.java @@ -3,6 +3,8 @@ package eu.kanade.mangafeed.ui.catalogue; import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.SearchView; import android.support.v7.widget.Toolbar; import android.text.TextUtils; @@ -14,8 +16,6 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; -import android.widget.GridView; -import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.Spinner; @@ -24,15 +24,16 @@ import java.util.concurrent.TimeUnit; import butterknife.Bind; import butterknife.ButterKnife; -import butterknife.OnItemClick; import eu.kanade.mangafeed.R; import eu.kanade.mangafeed.data.database.models.Manga; import eu.kanade.mangafeed.data.source.base.Source; +import eu.kanade.mangafeed.ui.base.adapter.FlexibleViewHolder; import eu.kanade.mangafeed.ui.base.fragment.BaseRxFragment; import eu.kanade.mangafeed.ui.main.MainActivity; import eu.kanade.mangafeed.ui.manga.MangaActivity; import eu.kanade.mangafeed.util.ToastUtil; -import eu.kanade.mangafeed.widget.EndlessScrollListener; +import eu.kanade.mangafeed.widget.AutofitRecyclerView; +import eu.kanade.mangafeed.widget.EndlessRecyclerScrollListener; import icepick.State; import nucleus.factory.RequiresPresenter; import rx.Subscription; @@ -40,16 +41,16 @@ import rx.android.schedulers.AndroidSchedulers; import rx.subjects.PublishSubject; @RequiresPresenter(CataloguePresenter.class) -public class CatalogueFragment extends BaseRxFragment { +public class CatalogueFragment extends BaseRxFragment implements FlexibleViewHolder.OnListItemClickListener { - @Bind(R.id.gridView) GridView gridView; + @Bind(R.id.recycler) AutofitRecyclerView recycler; @Bind(R.id.progress) ProgressBar progress; @Bind(R.id.progress_grid) ProgressBar progressGrid; private Toolbar toolbar; private Spinner spinner; private CatalogueAdapter adapter; - private EndlessScrollListener scrollListener; + private EndlessRecyclerScrollListener scrollListener; @State String query = ""; @State int selectedIndex = -1; @@ -75,10 +76,12 @@ public class CatalogueFragment extends BaseRxFragment { ButterKnife.bind(this, view); // Initialize adapter and scroll listener + GridLayoutManager layoutManager = (GridLayoutManager) recycler.getLayoutManager(); adapter = new CatalogueAdapter(this); - scrollListener = new EndlessScrollListener(this::requestNextPage); - gridView.setAdapter(adapter); - gridView.setOnScrollListener(scrollListener); + scrollListener = new EndlessRecyclerScrollListener(layoutManager, this::requestNextPage); + recycler.setHasFixedSize(true); + recycler.setAdapter(adapter); + recycler.addOnScrollListener(scrollListener); // Create toolbar spinner Context themedContext = getBaseActivity().getSupportActionBar() != null ? @@ -192,9 +195,7 @@ public class CatalogueFragment extends BaseRxFragment { query = newQuery; showProgressBar(); - // Set adapter again for scrolling to top: http://stackoverflow.com/a/17577981/3263582 - gridView.setAdapter(adapter); - gridView.setSelection(0); + recycler.getLayoutManager().scrollToPosition(0); getPresenter().restartRequest(query); } @@ -212,48 +213,23 @@ public class CatalogueFragment extends BaseRxFragment { adapter.clear(); scrollListener.resetScroll(); } - adapter.addAll(pair.second); + adapter.addItems(pair.second); } public void onAddPageError() { hideProgressBar(); } - @OnItemClick(R.id.gridView) - public void onMangaClick(int position) { - Manga selectedManga = adapter.getItem(position); - - Intent intent = MangaActivity.newIntent(getActivity(), selectedManga); - intent.putExtra(MangaActivity.MANGA_ONLINE, true); - startActivity(intent); - } - public void updateImage(Manga manga) { - ImageView imageView = getImageView(getMangaIndex(manga)); - if (imageView != null && manga.thumbnail_url != null) { - getPresenter().coverCache.loadFromNetwork(imageView, manga.thumbnail_url, - getPresenter().getSource().getGlideHeaders()); + CatalogueHolder holder = getHolder(manga); + if (holder != null) { + holder.setImage(manga, getPresenter()); } } - private ImageView getImageView(int position) { - if (position == -1) return null; - - View v = gridView.getChildAt(position - - gridView.getFirstVisiblePosition()); - - if (v == null) return null; - - return (ImageView) v.findViewById(R.id.thumbnail); - } - - private int getMangaIndex(Manga manga) { - for (int i = adapter.getCount() - 1; i >= 0; i--) { - if (manga.id.equals(adapter.getItem(i).id)) { - return i; - } - } - return -1; + @Nullable + private CatalogueHolder getHolder(Manga manga) { + return (CatalogueHolder) recycler.findViewHolderForItemId(manga.id); } private void showProgressBar() { @@ -269,4 +245,18 @@ public class CatalogueFragment extends BaseRxFragment { progressGrid.setVisibility(ProgressBar.GONE); } + @Override + public boolean onListItemClick(int position) { + final Manga selectedManga = adapter.getItem(position); + + Intent intent = MangaActivity.newIntent(getActivity(), selectedManga); + intent.putExtra(MangaActivity.MANGA_ONLINE, true); + startActivity(intent); + return false; + } + + @Override + public void onListItemLongClick(int position) { + // Do nothing + } } diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/catalogue/CatalogueHolder.java b/app/src/main/java/eu/kanade/mangafeed/ui/catalogue/CatalogueHolder.java new file mode 100644 index 000000000..620771b08 --- /dev/null +++ b/app/src/main/java/eu/kanade/mangafeed/ui/catalogue/CatalogueHolder.java @@ -0,0 +1,38 @@ +package eu.kanade.mangafeed.ui.catalogue; + +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import butterknife.Bind; +import butterknife.ButterKnife; +import eu.kanade.mangafeed.R; +import eu.kanade.mangafeed.data.database.models.Manga; +import eu.kanade.mangafeed.ui.base.adapter.FlexibleViewHolder; + +public class CatalogueHolder extends FlexibleViewHolder { + + @Bind(R.id.title) TextView title; + @Bind(R.id.thumbnail) ImageView thumbnail; + @Bind(R.id.favorite_sticker) ImageView favoriteSticker; + + public CatalogueHolder(View view, CatalogueAdapter adapter, OnListItemClickListener listener) { + super(view, adapter, listener); + ButterKnife.bind(this, view); + } + + public void onSetValues(Manga manga, CataloguePresenter presenter) { + title.setText(manga.title); + favoriteSticker.setVisibility(manga.favorite ? View.VISIBLE : View.GONE); + setImage(manga, presenter); + } + + public void setImage(Manga manga, CataloguePresenter presenter) { + if (manga.thumbnail_url != null) { + presenter.coverCache.loadFromNetwork(thumbnail, manga.thumbnail_url, + presenter.getSource().getGlideHeaders()); + } else { + thumbnail.setImageResource(android.R.color.transparent); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/catalogue/CataloguePresenter.java b/app/src/main/java/eu/kanade/mangafeed/ui/catalogue/CataloguePresenter.java index 208e60337..ab649c1f3 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/catalogue/CataloguePresenter.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/catalogue/CataloguePresenter.java @@ -60,12 +60,12 @@ public class CataloguePresenter extends BasePresenter { () -> pager.pages().concatMap( page -> getMangaObs(page + 1) .map(mangas -> Pair.create(page, mangas)) + .doOnNext(pair -> { + if (mangaDetailSubject != null) + mangaDetailSubject.onNext(pair.second); + }) .observeOn(AndroidSchedulers.mainThread())), - (view, page) -> { - view.onAddPage(page); - if (mangaDetailSubject != null) - mangaDetailSubject.onNext(page.second); - }, + CatalogueFragment::onAddPage, (view, error) -> { view.onAddPageError(); Timber.e(error.getMessage()); @@ -73,14 +73,14 @@ public class CataloguePresenter extends BasePresenter { restartableLatestCache(GET_MANGA_DETAIL, () -> mangaDetailSubject + .observeOn(Schedulers.io()) .flatMap(Observable::from) .filter(manga -> !manga.initialized) .window(3) .concatMap(pack -> pack.concatMap(this::getMangaDetails)) - .filter(manga -> manga.initialized) .onBackpressureBuffer() .observeOn(AndroidSchedulers.mainThread()), - (view, manga) -> view.updateImage(manga), + CatalogueFragment::updateImage, (view, error) -> Timber.e(error.getMessage())); } @@ -147,7 +147,7 @@ public class CataloguePresenter extends BasePresenter { return source.pullMangaFromNetwork(manga.url) .subscribeOn(Schedulers.io()) .flatMap(networkManga -> { - Manga.copyFromNetwork(manga, networkManga); + manga.copyFrom(networkManga); db.insertManga(manga).executeAsBlocking(); return Observable.just(manga); }) diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/manga/info/MangaInfoPresenter.java b/app/src/main/java/eu/kanade/mangafeed/ui/manga/info/MangaInfoPresenter.java index a58c54dfd..b26989500 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/manga/info/MangaInfoPresenter.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/manga/info/MangaInfoPresenter.java @@ -93,7 +93,7 @@ public class MangaInfoPresenter extends BasePresenter { private Observable fetchMangaObs() { return source.pullMangaFromNetwork(manga.url) .flatMap(networkManga -> { - Manga.copyFromNetwork(manga, networkManga); + manga.copyFrom(networkManga); db.insertManga(manga).executeAsBlocking(); return Observable.just(manga); }) diff --git a/app/src/main/java/eu/kanade/mangafeed/widget/EndlessRecyclerScrollListener.java b/app/src/main/java/eu/kanade/mangafeed/widget/EndlessRecyclerScrollListener.java new file mode 100644 index 000000000..05f5435b4 --- /dev/null +++ b/app/src/main/java/eu/kanade/mangafeed/widget/EndlessRecyclerScrollListener.java @@ -0,0 +1,49 @@ +package eu.kanade.mangafeed.widget; + +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.RecyclerView; + +import rx.functions.Action0; + +public class EndlessRecyclerScrollListener extends RecyclerView.OnScrollListener { + + private int previousTotal = 0; // The total number of items in the dataset after the last load + private boolean loading = true; // True if we are still waiting for the last set of data to load. + private int visibleThreshold = 5; // The minimum amount of items to have below your current scroll position before loading more. + int firstVisibleItem, visibleItemCount, totalItemCount; + + private GridLayoutManager layoutManager; + + private Action0 requestNext; + + public EndlessRecyclerScrollListener(GridLayoutManager layoutManager, Action0 requestNext) { + this.layoutManager = layoutManager; + this.requestNext = requestNext; + } + + public void resetScroll() { + previousTotal = 0; + loading = true; + } + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + + visibleItemCount = recyclerView.getChildCount(); + totalItemCount = layoutManager.getItemCount(); + firstVisibleItem = layoutManager.findFirstVisibleItemPosition(); + + if (loading && (totalItemCount > previousTotal)) { + loading = false; + previousTotal = totalItemCount; + } + if (!loading && (totalItemCount - visibleItemCount) + <= (firstVisibleItem + visibleThreshold)) { + // End has been reached + requestNext.call(); + loading = true; + } + } + +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_catalogue.xml b/app/src/main/res/layout/fragment_catalogue.xml index 7ae149d6a..d2b9a1bb3 100644 --- a/app/src/main/res/layout/fragment_catalogue.xml +++ b/app/src/main/res/layout/fragment_catalogue.xml @@ -15,8 +15,8 @@ android:layout_gravity="center_vertical|center_horizontal" android:visibility="gone"/> -