providers changed
This commit is contained in:
parent
d9729f1373
commit
3faa0cb9db
|
|
@ -35,10 +35,10 @@ import rx.subjects.PublishSubject;
|
|||
*/
|
||||
public abstract class ItemsProvider<T> {
|
||||
|
||||
private final PublishSubject<List<ListChange>> listChangesSubject = PublishSubject.create();
|
||||
private final PublishSubject<List<Change>> listChangesSubject = PublishSubject.create();
|
||||
|
||||
protected void notifyChanges(@NonNull final List<ListChange> listChanges) {
|
||||
listChangesSubject.onNext(listChanges);
|
||||
protected void notifyChanges(@NonNull final List<Change> changes) {
|
||||
listChangesSubject.onNext(changes);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
|
@ -49,16 +49,16 @@ public abstract class ItemsProvider<T> {
|
|||
public abstract int getSize();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Observable<List<T>> loadRange(int first, int last) {
|
||||
public Observable<List<T>> loadRange(final int first, final int last) {
|
||||
final List<Observable<List<T>>> itemsRequests = new ArrayList<>();
|
||||
|
||||
int i = first;
|
||||
while (i <= last) {
|
||||
int index = first;
|
||||
while (index <= last) {
|
||||
final List<Observable<T>> limitedPageRequests = new ArrayList<>();
|
||||
final int maxIndex = i + RxRingBuffer.SIZE - 1;
|
||||
while (i <= Math.min(last, maxIndex)) {
|
||||
limitedPageRequests.add(loadItem(i));
|
||||
i++;
|
||||
final int maxIndex = index + RxRingBuffer.SIZE - 1;
|
||||
while (index <= Math.min(last, maxIndex)) {
|
||||
limitedPageRequests.add(loadItem(index));
|
||||
index++;
|
||||
}
|
||||
itemsRequests.add(Observable.combineLatest(limitedPageRequests, args -> {
|
||||
final List<T> resultPart = new ArrayList<>(args.length);
|
||||
|
|
@ -79,18 +79,18 @@ public abstract class ItemsProvider<T> {
|
|||
}
|
||||
|
||||
@NonNull
|
||||
public Observable<List<ListChange>> observeListChanges() {
|
||||
public Observable<List<Change>> observeListChanges() {
|
||||
return listChangesSubject;
|
||||
}
|
||||
|
||||
public static class ListChange {
|
||||
public static class Change {
|
||||
|
||||
@NonNull
|
||||
private final Type type;
|
||||
private final int start;
|
||||
private final int count;
|
||||
|
||||
public ListChange(final @NonNull Type type, final int start, final int count) {
|
||||
public Change(@NonNull final Type type, final int start, final int count) {
|
||||
this.type = type;
|
||||
this.start = start;
|
||||
this.count = count;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ public class ListProvider<T> extends ItemsProvider<T> {
|
|||
private final List<T> items;
|
||||
|
||||
public ListProvider(@NonNull final Collection<T> collection) {
|
||||
super();
|
||||
items = Collections.unmodifiableList(new ArrayList<>(collection));
|
||||
}
|
||||
|
||||
|
|
@ -49,7 +50,7 @@ public class ListProvider<T> extends ItemsProvider<T> {
|
|||
|
||||
@Override
|
||||
public Observable<T> loadItem(final int position) {
|
||||
return Observable.just(items.get(position));
|
||||
return Observable.just(items.size() > position ? items.get(position) : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -0,0 +1,217 @@
|
|||
/*
|
||||
* Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
|
||||
*
|
||||
* This file is part of RoboSwag library.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.roboswag.components.listing;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import org.roboswag.core.log.Lc;
|
||||
import org.roboswag.core.utils.android.RxAndroidUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import rx.Observable;
|
||||
import rx.Scheduler;
|
||||
|
||||
/**
|
||||
* Created by Gavriil Sitnikov on 07/12/2015.
|
||||
* TODO: fill description
|
||||
*/
|
||||
public class SimplePagingProvider<T> extends ItemsProvider<T> {
|
||||
|
||||
public static final int DEFAULT_PAGE_SIZE = 25;
|
||||
|
||||
private final Scheduler scheduler = RxAndroidUtils.createLooperScheduler();
|
||||
private final Object lock = new Object();
|
||||
private final int pageSize;
|
||||
private final SparseArray<List<T>> loadedPages = new SparseArray<>();
|
||||
@Nullable
|
||||
private Integer maxLoadedPage;
|
||||
private boolean isLastPageLoaded;
|
||||
private final SparseArray<Observable<List<T>>> loadingPages = new SparseArray<>();
|
||||
@NonNull
|
||||
private final PagesProvider<T> pagesProvider;
|
||||
|
||||
public SimplePagingProvider(@NonNull final PagesProvider<T> pagesProvider) {
|
||||
this(pagesProvider, DEFAULT_PAGE_SIZE);
|
||||
}
|
||||
|
||||
public SimplePagingProvider(@NonNull final PagesProvider<T> pagesProvider, final int pageSize) {
|
||||
super();
|
||||
this.pagesProvider = pagesProvider;
|
||||
this.pageSize = pageSize;
|
||||
}
|
||||
|
||||
public int getPageSize() {
|
||||
return pageSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize() {
|
||||
synchronized (lock) {
|
||||
return (maxLoadedPage != null ? maxLoadedPage * pageSize + loadedPages.get(maxLoadedPage).size() : 0)
|
||||
+ (isLastPageLoaded ? 0 : 1);
|
||||
}
|
||||
}
|
||||
|
||||
private int pageIndexOf(final int position) {
|
||||
return position / pageSize;
|
||||
}
|
||||
|
||||
private int indexOnPage(final int position) {
|
||||
return position % pageSize;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public T getItem(final int position) {
|
||||
synchronized (lock) {
|
||||
final List<T> page = loadedPages.get(pageIndexOf(position));
|
||||
final int indexOnPage = indexOnPage(position);
|
||||
return page != null && page.size() > indexOnPage ? page.get(indexOnPage) : null;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Observable<T> loadItem(final int position) {
|
||||
final int indexOfPage = pageIndexOf(position);
|
||||
final int indexOnPage = indexOnPage(position);
|
||||
return Observable.<T>create(subscriber -> {
|
||||
final List<T> page = loadedPages.get(indexOfPage);
|
||||
subscriber.onNext(page != null && page.size() > indexOnPage ? page.get(indexOnPage) : null);
|
||||
subscriber.onCompleted();
|
||||
}).switchMap(item -> {
|
||||
if (item != null || (isLastPageLoaded && maxLoadedPage != null && maxLoadedPage <= indexOfPage)) {
|
||||
return Observable.just(item);
|
||||
}
|
||||
Observable<List<T>> loadingPage = loadingPages.get(indexOfPage);
|
||||
if (loadingPage == null) {
|
||||
loadingPage = createPageLoadingObservable(indexOfPage);
|
||||
loadingPages.put(indexOfPage, loadingPage);
|
||||
}
|
||||
return loadingPage.map(page -> {
|
||||
int index = 0;
|
||||
for (final T nextItem : page) {
|
||||
if (index == indexOnPage) {
|
||||
return nextItem;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}).subscribeOn(scheduler);
|
||||
}
|
||||
|
||||
private boolean shouldReplaceMaxLoaded(@NonNull final List<T> pageItems, final int indexOfPage) {
|
||||
return (indexOfPage == 0 || !pageItems.isEmpty()) && (maxLoadedPage == null || maxLoadedPage < indexOfPage);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Observable<List<T>> createPageLoadingObservable(final int indexOfPage) {
|
||||
return pagesProvider
|
||||
.loadPage(indexOfPage * pageSize, pageSize)
|
||||
.observeOn(scheduler)
|
||||
.map(this::validatePageItems)
|
||||
.doOnNext(pageItems -> {
|
||||
synchronized (lock) {
|
||||
final int oldSize = getSize();
|
||||
final boolean oldIsLastPageLoaded = isLastPageLoaded;
|
||||
if (pageItems.size() < pageSize) {
|
||||
if (maxLoadedPage != null && maxLoadedPage > indexOfPage) {
|
||||
maxLoadedPage = indexOfPage == 0 || !pageItems.isEmpty() ? indexOfPage : null;
|
||||
downgradeMaxLoadedPages(indexOfPage);
|
||||
isLastPageLoaded = false;
|
||||
}
|
||||
if (shouldReplaceMaxLoaded(pageItems, indexOfPage)) {
|
||||
maxLoadedPage = indexOfPage;
|
||||
}
|
||||
isLastPageLoaded = isLastPageLoaded
|
||||
|| (maxLoadedPage != null
|
||||
&& (maxLoadedPage == indexOfPage || maxLoadedPage == indexOfPage - 1));
|
||||
} else if (shouldReplaceMaxLoaded(pageItems, indexOfPage)) {
|
||||
maxLoadedPage = indexOfPage;
|
||||
}
|
||||
if (indexOfPage == 0 || !pageItems.isEmpty()) {
|
||||
loadedPages.put(indexOfPage, pageItems);
|
||||
}
|
||||
loadingPages.remove(indexOfPage);
|
||||
updateItemsChanges(indexOfPage, pageItems, oldSize, oldIsLastPageLoaded);
|
||||
}
|
||||
}).replay(1)
|
||||
.refCount();
|
||||
}
|
||||
|
||||
private void downgradeMaxLoadedPages(final int maximum) {
|
||||
for (int i = 0; i <= loadedPages.size(); i++) {
|
||||
final int key = loadedPages.keyAt(i);
|
||||
if (key > maximum || loadedPages.get(key).size() < pageSize) {
|
||||
loadedPages.remove(key);
|
||||
} else if (maxLoadedPage == null || maxLoadedPage < key) {
|
||||
maxLoadedPage = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateItemsChanges(final int indexOfPage, @NonNull final List<T> pageItems, final int oldSize, final boolean oldIsLastPageLoaded) {
|
||||
final List<Change> changes = new ArrayList<>();
|
||||
final int size = getSize();
|
||||
|
||||
if (size == oldSize) {
|
||||
if (!pageItems.isEmpty()) {
|
||||
changes.add(new Change(Change.Type.CHANGED, indexOfPage * pageSize, pageItems.size()));
|
||||
}
|
||||
} else if (size > oldSize) {
|
||||
if (!oldIsLastPageLoaded) {
|
||||
changes.add(new Change(Change.Type.CHANGED, oldSize - 1, 1));
|
||||
}
|
||||
changes.add(new Change(Change.Type.INSERTED, oldSize, size - oldSize));
|
||||
} else {
|
||||
changes.add(new Change(Change.Type.REMOVED, size, oldSize - size));
|
||||
if (!isLastPageLoaded) {
|
||||
changes.add(new Change(Change.Type.CHANGED, size - 1, 1));
|
||||
}
|
||||
}
|
||||
if (!changes.isEmpty()) {
|
||||
notifyChanges(changes);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private List<T> validatePageItems(@NonNull final Collection<T> pageItems) {
|
||||
if (pageItems.size() > pageSize) {
|
||||
Lc.assertion("Unexpectedly big pageItems: " + pageItems.size() + '/' + pageSize);
|
||||
return Collections.unmodifiableList(new ArrayList<>(pageItems).subList(0, pageSize));
|
||||
} else {
|
||||
return Collections.unmodifiableList(new ArrayList<>(pageItems));
|
||||
}
|
||||
}
|
||||
|
||||
public interface PagesProvider<T> {
|
||||
|
||||
@NonNull
|
||||
Observable<Collection<T>> loadPage(int offset, int limit);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -48,8 +48,6 @@ import rx.functions.Actions;
|
|||
public abstract class AbstractItemsAdapter<TItem, TViewHolder extends RecyclerView.ViewHolder>
|
||||
extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
|
||||
private static final int PRE_LOADING_COUNT = 2;
|
||||
|
||||
private static final int LOADED_ITEM_TYPE = R.id.LOADED_ITEM_TYPE;
|
||||
private static final int NOT_LOADED_ITEM_TYPE = R.id.NOT_LOADED_ITEM_TYPE;
|
||||
|
||||
|
|
@ -58,31 +56,48 @@ public abstract class AbstractItemsAdapter<TItem, TViewHolder extends RecyclerVi
|
|||
private OnItemClickListener<TItem> onItemClickListener;
|
||||
@Nullable
|
||||
private ItemsProvider<TItem> itemsProvider;
|
||||
@Nullable
|
||||
private Subscription itemsProviderSubscription;
|
||||
|
||||
public void setItems(@NonNull final List<TItem> items) {
|
||||
setItemsProvider(new ListProvider<>(items));
|
||||
}
|
||||
|
||||
protected int itemsOffset() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void setItemsProvider(@NonNull final ItemsProvider<TItem> itemsProvider) {
|
||||
if (itemsProviderSubscription != null) {
|
||||
itemsProviderSubscription.unsubscribe();
|
||||
itemsProviderSubscription = null;
|
||||
}
|
||||
this.itemsProvider = itemsProvider;
|
||||
notifyDataSetChanged();
|
||||
itemsProvider.observeListChanges()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(listChanges -> {
|
||||
for (final ItemsProvider.ListChange listChange : listChanges) {
|
||||
switch (listChange.getType()) {
|
||||
case INSERTED:
|
||||
notifyItemRangeInserted(listChange.getStart(), listChange.getCount());
|
||||
break;
|
||||
case CHANGED:
|
||||
notifyItemRangeChanged(listChange.getStart(), listChange.getCount());
|
||||
break;
|
||||
case REMOVED:
|
||||
notifyItemRangeRemoved(listChange.getStart(), listChange.getCount());
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (this.itemsProvider != null) {
|
||||
itemsProviderSubscription = itemsProvider.observeListChanges()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::onItemsChanged);
|
||||
}
|
||||
}
|
||||
|
||||
protected void onItemsChanged(@NonNull final List<ItemsProvider.Change> changes) {
|
||||
for (final ItemsProvider.Change change : changes) {
|
||||
switch (change.getType()) {
|
||||
case INSERTED:
|
||||
notifyItemRangeInserted(change.getStart() + itemsOffset(), change.getCount());
|
||||
break;
|
||||
case CHANGED:
|
||||
notifyItemRangeChanged(change.getStart() + itemsOffset(), change.getCount());
|
||||
break;
|
||||
case REMOVED:
|
||||
notifyItemRangeRemoved(change.getStart() + itemsOffset(), change.getCount());
|
||||
break;
|
||||
default:
|
||||
Lc.assertion("Not supported " + change.getType());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setOnItemClickListener(@Nullable final OnItemClickListener<TItem> onItemClickListener) {
|
||||
|
|
@ -116,11 +131,13 @@ public abstract class AbstractItemsAdapter<TItem, TViewHolder extends RecyclerVi
|
|||
}
|
||||
((NotLoadedItemViewHolder) holder).bindItem(position, itemsProvider);
|
||||
} else {
|
||||
if (item == null) {
|
||||
if (item == null || itemsProvider == null) {
|
||||
Lc.assertion(new ShouldNotHappenException("Item at" + position + " should not be null"));
|
||||
return;
|
||||
}
|
||||
onBindItemToViewHolder((TViewHolder) holder, position, item);
|
||||
itemsProvider.loadRange(Math.max(0, position - itemsProvider.getSize() / 2), position + itemsProvider.getSize() / 2).first()
|
||||
.subscribe(Actions.empty(), Actions.empty());
|
||||
if (onItemClickListener != null && !isOnClickListenerDisabled(item)) {
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
//TODO: fix multitap
|
||||
|
|
@ -179,7 +196,9 @@ public abstract class AbstractItemsAdapter<TItem, TViewHolder extends RecyclerVi
|
|||
}
|
||||
retryButton.setVisibility(View.INVISIBLE);
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
subscription = itemsProvider.loadRange(Math.max(0, position - PRE_LOADING_COUNT), position + PRE_LOADING_COUNT)
|
||||
subscription = itemsProvider
|
||||
.loadRange(Math.max(0, position - itemsProvider.getSize() / 2), position + itemsProvider.getSize() / 2)
|
||||
.first()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(Actions.empty(),
|
||||
throwable -> {
|
||||
|
|
|
|||
Loading…
Reference in New Issue