Use RecyclerView for catalogue

This commit is contained in:
inorichi 2016-01-03 00:30:20 +01:00
parent 453a187938
commit 868058a50b
8 changed files with 186 additions and 123 deletions

View File

@ -77,32 +77,31 @@ public class Manga implements Serializable {
this.url = UrlUtil.getPath(url); this.url = UrlUtil.getPath(url);
} }
public static void copyFromNetwork(Manga local, Manga network) { public void copyFrom(Manga other) {
if (network.title != null) if (other.title != null)
local.title = network.title; title = other.title;
if (network.author != null) if (other.author != null)
local.author = network.author; author = other.author;
if (network.artist != null) if (other.artist != null)
local.artist = network.artist; artist = other.artist;
if (network.url != null) if (other.url != null)
local.url = network.url; url = other.url;
if (network.description != null) if (other.description != null)
local.description = network.description; description = other.description;
if (network.genre != null) if (other.genre != null)
local.genre = network.genre; genre = other.genre;
if (network.thumbnail_url != null) if (other.thumbnail_url != null)
local.thumbnail_url = network.thumbnail_url; thumbnail_url = other.thumbnail_url;
local.status = network.status; status = other.status;
local.initialized = true;
initialized = true;
} }
@Override @Override

View File

@ -3,71 +3,58 @@ package eu.kanade.mangafeed.ui.catalogue;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
import butterknife.Bind; import eu.davidea.flexibleadapter.FlexibleAdapter;
import butterknife.ButterKnife;
import eu.kanade.mangafeed.R; import eu.kanade.mangafeed.R;
import eu.kanade.mangafeed.data.database.models.Manga; import eu.kanade.mangafeed.data.database.models.Manga;
public class CatalogueAdapter extends ArrayAdapter<Manga> { public class CatalogueAdapter extends FlexibleAdapter<CatalogueHolder, Manga> {
private CatalogueFragment fragment; private CatalogueFragment fragment;
private LayoutInflater inflater;
public CatalogueAdapter(CatalogueFragment fragment) { public CatalogueAdapter(CatalogueFragment fragment) {
super(fragment.getActivity(), 0, new ArrayList<>());
this.fragment = fragment; this.fragment = fragment;
inflater = fragment.getActivity().getLayoutInflater(); mItems = new ArrayList<>();
setHasStableIds(true);
}
public void addItems(List<Manga> list) {
mItems.addAll(list);
notifyDataSetChanged();
}
public void clear() {
mItems.clear();
notifyDataSetChanged();
} }
@Override @Override
public View getView(int position, View view, ViewGroup parent) { public long getItemId(int position) {
Manga manga = getItem(position); return mItems.get(position).id;
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;
} }
static class ViewHolder { @Override
@Bind(R.id.title) TextView title; public void updateDataSet(String param) {
@Bind(R.id.thumbnail) ImageView thumbnail;
@Bind(R.id.favorite_sticker) ImageView favorite_sticker;
CataloguePresenter presenter;
public ViewHolder(View view, CatalogueFragment fragment) {
ButterKnife.bind(this, view);
presenter = fragment.getPresenter();
} }
public void onSetValues(Manga manga) { @Override
title.setText(manga.title); public CatalogueHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = fragment.getActivity().getLayoutInflater();
if (manga.thumbnail_url != null) { View v = inflater.inflate(R.layout.item_catalogue, parent, false);
presenter.coverCache.loadFromCacheOrNetwork(thumbnail, manga.thumbnail_url, return new CatalogueHolder(v, this, fragment);
presenter.getSource().getGlideHeaders());
} else {
thumbnail.setImageResource(android.R.color.transparent);
} }
if (manga.favorite) { @Override
favorite_sticker.setVisibility(View.VISIBLE); public void onBindViewHolder(CatalogueHolder holder, int position) {
} else { final Manga manga = getItem(position);
favorite_sticker.setVisibility(View.INVISIBLE); holder.onSetValues(manga, fragment.getPresenter());
}
} //When user scrolls this bind the correct selection status
//holder.itemView.setActivated(isSelected(position));
} }
} }

View File

@ -3,6 +3,8 @@ package eu.kanade.mangafeed.ui.catalogue;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; 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.SearchView;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.text.TextUtils; import android.text.TextUtils;
@ -14,8 +16,6 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.Spinner; import android.widget.Spinner;
@ -24,15 +24,16 @@ import java.util.concurrent.TimeUnit;
import butterknife.Bind; import butterknife.Bind;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import butterknife.OnItemClick;
import eu.kanade.mangafeed.R; import eu.kanade.mangafeed.R;
import eu.kanade.mangafeed.data.database.models.Manga; import eu.kanade.mangafeed.data.database.models.Manga;
import eu.kanade.mangafeed.data.source.base.Source; 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.base.fragment.BaseRxFragment;
import eu.kanade.mangafeed.ui.main.MainActivity; import eu.kanade.mangafeed.ui.main.MainActivity;
import eu.kanade.mangafeed.ui.manga.MangaActivity; import eu.kanade.mangafeed.ui.manga.MangaActivity;
import eu.kanade.mangafeed.util.ToastUtil; 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 icepick.State;
import nucleus.factory.RequiresPresenter; import nucleus.factory.RequiresPresenter;
import rx.Subscription; import rx.Subscription;
@ -40,16 +41,16 @@ import rx.android.schedulers.AndroidSchedulers;
import rx.subjects.PublishSubject; import rx.subjects.PublishSubject;
@RequiresPresenter(CataloguePresenter.class) @RequiresPresenter(CataloguePresenter.class)
public class CatalogueFragment extends BaseRxFragment<CataloguePresenter> { public class CatalogueFragment extends BaseRxFragment<CataloguePresenter> 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) ProgressBar progress;
@Bind(R.id.progress_grid) ProgressBar progressGrid; @Bind(R.id.progress_grid) ProgressBar progressGrid;
private Toolbar toolbar; private Toolbar toolbar;
private Spinner spinner; private Spinner spinner;
private CatalogueAdapter adapter; private CatalogueAdapter adapter;
private EndlessScrollListener scrollListener; private EndlessRecyclerScrollListener scrollListener;
@State String query = ""; @State String query = "";
@State int selectedIndex = -1; @State int selectedIndex = -1;
@ -75,10 +76,12 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter> {
ButterKnife.bind(this, view); ButterKnife.bind(this, view);
// Initialize adapter and scroll listener // Initialize adapter and scroll listener
GridLayoutManager layoutManager = (GridLayoutManager) recycler.getLayoutManager();
adapter = new CatalogueAdapter(this); adapter = new CatalogueAdapter(this);
scrollListener = new EndlessScrollListener(this::requestNextPage); scrollListener = new EndlessRecyclerScrollListener(layoutManager, this::requestNextPage);
gridView.setAdapter(adapter); recycler.setHasFixedSize(true);
gridView.setOnScrollListener(scrollListener); recycler.setAdapter(adapter);
recycler.addOnScrollListener(scrollListener);
// Create toolbar spinner // Create toolbar spinner
Context themedContext = getBaseActivity().getSupportActionBar() != null ? Context themedContext = getBaseActivity().getSupportActionBar() != null ?
@ -192,9 +195,7 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter> {
query = newQuery; query = newQuery;
showProgressBar(); showProgressBar();
// Set adapter again for scrolling to top: http://stackoverflow.com/a/17577981/3263582 recycler.getLayoutManager().scrollToPosition(0);
gridView.setAdapter(adapter);
gridView.setSelection(0);
getPresenter().restartRequest(query); getPresenter().restartRequest(query);
} }
@ -212,48 +213,23 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter> {
adapter.clear(); adapter.clear();
scrollListener.resetScroll(); scrollListener.resetScroll();
} }
adapter.addAll(pair.second); adapter.addItems(pair.second);
} }
public void onAddPageError() { public void onAddPageError() {
hideProgressBar(); 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) { public void updateImage(Manga manga) {
ImageView imageView = getImageView(getMangaIndex(manga)); CatalogueHolder holder = getHolder(manga);
if (imageView != null && manga.thumbnail_url != null) { if (holder != null) {
getPresenter().coverCache.loadFromNetwork(imageView, manga.thumbnail_url, holder.setImage(manga, getPresenter());
getPresenter().getSource().getGlideHeaders());
} }
} }
private ImageView getImageView(int position) { @Nullable
if (position == -1) return null; private CatalogueHolder getHolder(Manga manga) {
return (CatalogueHolder) recycler.findViewHolderForItemId(manga.id);
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;
} }
private void showProgressBar() { private void showProgressBar() {
@ -269,4 +245,18 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter> {
progressGrid.setVisibility(ProgressBar.GONE); 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
}
} }

View File

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

View File

@ -60,12 +60,12 @@ public class CataloguePresenter extends BasePresenter<CatalogueFragment> {
() -> pager.pages().concatMap( () -> pager.pages().concatMap(
page -> getMangaObs(page + 1) page -> getMangaObs(page + 1)
.map(mangas -> Pair.create(page, mangas)) .map(mangas -> Pair.create(page, mangas))
.observeOn(AndroidSchedulers.mainThread())), .doOnNext(pair -> {
(view, page) -> {
view.onAddPage(page);
if (mangaDetailSubject != null) if (mangaDetailSubject != null)
mangaDetailSubject.onNext(page.second); mangaDetailSubject.onNext(pair.second);
}, })
.observeOn(AndroidSchedulers.mainThread())),
CatalogueFragment::onAddPage,
(view, error) -> { (view, error) -> {
view.onAddPageError(); view.onAddPageError();
Timber.e(error.getMessage()); Timber.e(error.getMessage());
@ -73,14 +73,14 @@ public class CataloguePresenter extends BasePresenter<CatalogueFragment> {
restartableLatestCache(GET_MANGA_DETAIL, restartableLatestCache(GET_MANGA_DETAIL,
() -> mangaDetailSubject () -> mangaDetailSubject
.observeOn(Schedulers.io())
.flatMap(Observable::from) .flatMap(Observable::from)
.filter(manga -> !manga.initialized) .filter(manga -> !manga.initialized)
.window(3) .window(3)
.concatMap(pack -> pack.concatMap(this::getMangaDetails)) .concatMap(pack -> pack.concatMap(this::getMangaDetails))
.filter(manga -> manga.initialized)
.onBackpressureBuffer() .onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread()), .observeOn(AndroidSchedulers.mainThread()),
(view, manga) -> view.updateImage(manga), CatalogueFragment::updateImage,
(view, error) -> Timber.e(error.getMessage())); (view, error) -> Timber.e(error.getMessage()));
} }
@ -147,7 +147,7 @@ public class CataloguePresenter extends BasePresenter<CatalogueFragment> {
return source.pullMangaFromNetwork(manga.url) return source.pullMangaFromNetwork(manga.url)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.flatMap(networkManga -> { .flatMap(networkManga -> {
Manga.copyFromNetwork(manga, networkManga); manga.copyFrom(networkManga);
db.insertManga(manga).executeAsBlocking(); db.insertManga(manga).executeAsBlocking();
return Observable.just(manga); return Observable.just(manga);
}) })

View File

@ -93,7 +93,7 @@ public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
private Observable<Manga> fetchMangaObs() { private Observable<Manga> fetchMangaObs() {
return source.pullMangaFromNetwork(manga.url) return source.pullMangaFromNetwork(manga.url)
.flatMap(networkManga -> { .flatMap(networkManga -> {
Manga.copyFromNetwork(manga, networkManga); manga.copyFrom(networkManga);
db.insertManga(manga).executeAsBlocking(); db.insertManga(manga).executeAsBlocking();
return Observable.just(manga); return Observable.just(manga);
}) })

View File

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

View File

@ -15,8 +15,8 @@
android:layout_gravity="center_vertical|center_horizontal" android:layout_gravity="center_vertical|center_horizontal"
android:visibility="gone"/> android:visibility="gone"/>
<GridView <eu.kanade.mangafeed.widget.AutofitRecyclerView
android:id="@+id/gridView" android:id="@+id/recycler"
style="@style/AppTheme.GridView" style="@style/AppTheme.GridView"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" android:layout_weight="1"