Observable collection adapter in release candidate state
This commit is contained in:
parent
1406eb73fa
commit
71f84db9ca
|
|
@ -3,7 +3,7 @@ apply plugin: 'me.tatarka.retrolambda'
|
|||
|
||||
android {
|
||||
compileSdkVersion 24
|
||||
buildToolsVersion "24.0.1"
|
||||
buildToolsVersion "24.0.2"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
|
|
@ -19,8 +19,8 @@ android {
|
|||
dependencies {
|
||||
compile project(':libraries:core')
|
||||
|
||||
provided 'com.android.support:appcompat-v7:24.1.1'
|
||||
compile 'com.android.support:recyclerview-v7:24.1.1'
|
||||
provided 'com.android.support:appcompat-v7:24.2.0'
|
||||
provided 'com.android.support:recyclerview-v7:24.2.0'
|
||||
|
||||
provided 'io.reactivex:rxandroid:1.2.1'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,11 +37,14 @@ import rx.subjects.BehaviorSubject;
|
|||
|
||||
/**
|
||||
* Created by Gavriil Sitnikov on 12/8/2016.
|
||||
* TODO: fill description
|
||||
* ViewHolder that implements {@link LifecycleBindable} and uses parent bindable object as bridge (Activity, ViewController etc.).
|
||||
* It is important to use such ViewHolder to avoid endless bindings when parent bindable have started but ViewHolder already detached from window.
|
||||
* So inside method {@link RecyclerView.Adapter#onBindViewHolder(RecyclerView.ViewHolder, int)}
|
||||
* use only inner {@link #bind(Observable, Action1)} method but not parent's bind method.
|
||||
*/
|
||||
public class BindableViewHolder extends RecyclerView.ViewHolder implements LifecycleBindable {
|
||||
|
||||
// it is needed to delay detach to avoid re-subscriptions on fast scroll
|
||||
//HACK: it is needed to delay detach to avoid re-subscriptions on fast scroll
|
||||
private static final long DETACH_DELAY = TimeUnit.SECONDS.toMillis(1);
|
||||
|
||||
@NonNull
|
||||
|
|
@ -62,15 +65,26 @@ public class BindableViewHolder extends RecyclerView.ViewHolder implements Lifec
|
|||
.refCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls when {@link RecyclerView.ViewHolder} have attached to window.
|
||||
*/
|
||||
@CallSuper
|
||||
public void onAttachedToWindow() {
|
||||
attachedToWindowSubject.onNext(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if {@link RecyclerView.ViewHolder} attached to window or not.
|
||||
*
|
||||
* @return True if {@link RecyclerView.ViewHolder} attached to window.
|
||||
*/
|
||||
public boolean isAttachedToWindow() {
|
||||
return attachedToWindowSubject.hasValue() && attachedToWindowSubject.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls when {@link RecyclerView.ViewHolder} have detached from window.
|
||||
*/
|
||||
@CallSuper
|
||||
public void onDetachedFromWindow() {
|
||||
attachedToWindowSubject.onNext(false);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import java.util.Collection;
|
|||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import ru.touchin.roboswag.components.R;
|
||||
import ru.touchin.roboswag.components.utils.LifecycleBindable;
|
||||
import ru.touchin.roboswag.components.utils.UiUtils;
|
||||
import ru.touchin.roboswag.core.log.Lc;
|
||||
|
|
@ -45,15 +46,20 @@ import rx.subjects.BehaviorSubject;
|
|||
|
||||
/**
|
||||
* Created by Gavriil Sitnikov on 20/11/2015.
|
||||
* TODO: fill description
|
||||
* Adapter based on {@link ObservableCollection} and providing some useful features like:
|
||||
* - item-based binding by {@link #onCreateItemViewHolder(ViewGroup, int)} and {@link #onBindItemToViewHolder(ViewHolder, int, Object)}} methods;
|
||||
* - item click listener setup by {@link #setOnItemClickListener(OnItemClickListener)};
|
||||
* - allows to inform about footers/headers by overriding base create/bind methods and {@link #getHeadersCount()} plus {@link #getFootersCount()};
|
||||
* - by default it is pre-loading items for collections like {@link ru.touchin.roboswag.core.observables.collections.loadable.LoadingMoreList}.
|
||||
*
|
||||
* @param <TItem> Type of items to bind to ViewHolders;
|
||||
* @param <TViewHolder> Type of ViewHolders to show items.
|
||||
*/
|
||||
public abstract class ObservableCollectionAdapter<TItem, TViewHolder extends ObservableCollectionAdapter.ViewHolder>
|
||||
extends RecyclerView.Adapter<BindableViewHolder> {
|
||||
|
||||
private static final int PRE_LOADING_COUNT = 10;
|
||||
|
||||
private static final int LIST_ITEM_TYPE = 12313212;
|
||||
|
||||
@NonNull
|
||||
private final BehaviorSubject<ObservableCollection<TItem>> observableCollectionSubject
|
||||
= BehaviorSubject.create((ObservableCollection<TItem>) null);
|
||||
|
|
@ -112,53 +118,6 @@ public abstract class ObservableCollectionAdapter<TItem, TViewHolder extends Obs
|
|||
attachedRecyclerViews.add(recyclerView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromRecyclerView(@NonNull final RecyclerView recyclerView) {
|
||||
super.onDetachedFromRecyclerView(recyclerView);
|
||||
attachedRecyclerViews.remove(recyclerView);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public LifecycleBindable getLifecycleBindable() {
|
||||
return lifecycleBindable;
|
||||
}
|
||||
|
||||
protected long getItemClickDelay() {
|
||||
return UiUtils.RIPPLE_EFFECT_DELAY;
|
||||
}
|
||||
|
||||
public void setItems(@NonNull final List<TItem> items) {
|
||||
setObservableCollection(new ObservableList<>(items));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ObservableCollection<TItem> getObservableCollection() {
|
||||
return observableCollectionSubject.getValue();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Observable<ObservableCollection<TItem>> observeObservableCollection() {
|
||||
return observableCollectionSubject.distinctUntilChanged();
|
||||
}
|
||||
|
||||
protected int getHeadersCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected int getFootersCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void refreshUpdate() {
|
||||
notifyDataSetChanged();
|
||||
lastUpdatedChangeNumber = innerCollection.getChangesCount();
|
||||
}
|
||||
|
||||
public void setObservableCollection(@Nullable final ObservableCollection<TItem> observableCollection) {
|
||||
this.observableCollectionSubject.onNext(observableCollection);
|
||||
refreshUpdate();
|
||||
}
|
||||
|
||||
private boolean anyRecyclerViewShown() {
|
||||
for (final RecyclerView recyclerView : attachedRecyclerViews) {
|
||||
if (recyclerView.isShown()) {
|
||||
|
|
@ -168,6 +127,66 @@ public abstract class ObservableCollectionAdapter<TItem, TViewHolder extends Obs
|
|||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromRecyclerView(@NonNull final RecyclerView recyclerView) {
|
||||
super.onDetachedFromRecyclerView(recyclerView);
|
||||
attachedRecyclerViews.remove(recyclerView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns parent {@link LifecycleBindable} (Activity/ViewController etc.).
|
||||
*
|
||||
* @return Parent {@link LifecycleBindable}.
|
||||
*/
|
||||
@NonNull
|
||||
public LifecycleBindable getLifecycleBindable() {
|
||||
return lifecycleBindable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@link ObservableCollection} which provides items and it's changes.
|
||||
*
|
||||
* @return Inner {@link ObservableCollection}.
|
||||
*/
|
||||
@Nullable
|
||||
public ObservableCollection<TItem> getObservableCollection() {
|
||||
return observableCollectionSubject.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to observe {@link ObservableCollection} which provides items and it's changes.
|
||||
*
|
||||
* @return Observable of inner {@link ObservableCollection}.
|
||||
*/
|
||||
@NonNull
|
||||
public Observable<ObservableCollection<TItem>> observeObservableCollection() {
|
||||
return observableCollectionSubject.distinctUntilChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets {@link ObservableCollection} which will provide items and it's changes.
|
||||
*
|
||||
* @param observableCollection Inner {@link ObservableCollection}.
|
||||
*/
|
||||
public void setObservableCollection(@Nullable final ObservableCollection<TItem> observableCollection) {
|
||||
this.observableCollectionSubject.onNext(observableCollection);
|
||||
refreshUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Simply sets items.
|
||||
*
|
||||
* @param items Items to set.
|
||||
*/
|
||||
public void setItems(@NonNull final Collection<TItem> items) {
|
||||
setObservableCollection(new ObservableList<>(items));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls when collection changes.
|
||||
*
|
||||
* @param collectionChange Changes of collection.
|
||||
*/
|
||||
protected void onItemsChanged(@NonNull final ObservableCollection.CollectionChange<TItem> collectionChange) {
|
||||
if (Looper.myLooper() != Looper.getMainLooper()) {
|
||||
Lc.assertion("Items changes called on not main thread");
|
||||
|
|
@ -189,6 +208,11 @@ public abstract class ObservableCollectionAdapter<TItem, TViewHolder extends Obs
|
|||
lastUpdatedChangeNumber = innerCollection.getChangesCount();
|
||||
}
|
||||
|
||||
private void refreshUpdate() {
|
||||
notifyDataSetChanged();
|
||||
lastUpdatedChangeNumber = innerCollection.getChangesCount();
|
||||
}
|
||||
|
||||
private void notifyAboutChanges(@NonNull final Collection<Change<TItem>> changes) {
|
||||
for (final Change change : changes) {
|
||||
switch (change.getType()) {
|
||||
|
|
@ -213,14 +237,32 @@ public abstract class ObservableCollectionAdapter<TItem, TViewHolder extends Obs
|
|||
}
|
||||
}
|
||||
|
||||
public void setOnItemClickListener(@Nullable final OnItemClickListener<TItem> onItemClickListener) {
|
||||
this.onItemClickListener = onItemClickListener;
|
||||
refreshUpdate();
|
||||
/**
|
||||
* Returns headers count goes before items.
|
||||
*
|
||||
* @return Headers count.
|
||||
*/
|
||||
protected int getHeadersCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns footers count goes after items and headers.
|
||||
*
|
||||
* @return Footers count.
|
||||
*/
|
||||
protected int getFootersCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return getHeadersCount() + innerCollection.size() + getFootersCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(final int position) {
|
||||
return LIST_ITEM_TYPE;
|
||||
return R.id.OBSERVABLE_COLLECTION_ITEM_VIEW_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -228,18 +270,25 @@ public abstract class ObservableCollectionAdapter<TItem, TViewHolder extends Obs
|
|||
return onCreateItemViewHolder(parent, viewType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to create ViewHolder for item (from {@link #getObservableCollection()}).
|
||||
*
|
||||
* @param parent Parent to inflate ViewHolder into;
|
||||
* @param viewType Type of ViewHolder;
|
||||
* @return Item-specific ViewHolder.
|
||||
*/
|
||||
public abstract TViewHolder onCreateItemViewHolder(@NonNull ViewGroup parent, int viewType);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public void onBindViewHolder(final BindableViewHolder holder, final int position) {
|
||||
public void onBindViewHolder(@NonNull final BindableViewHolder holder, final int position) {
|
||||
lastUpdatedChangeNumber = innerCollection.getChangesCount();
|
||||
|
||||
if (position - getHeadersCount() >= innerCollection.size()) {
|
||||
return;
|
||||
}
|
||||
final TItem item = innerCollection.get(position - getHeadersCount());
|
||||
|
||||
final TItem item = innerCollection.get(position - getHeadersCount());
|
||||
onBindItemToViewHolder((TViewHolder) holder, position, item);
|
||||
((TViewHolder) holder).bindPosition(position);
|
||||
if (onItemClickListener != null && !isOnClickListenerDisabled(item)) {
|
||||
|
|
@ -247,6 +296,14 @@ public abstract class ObservableCollectionAdapter<TItem, TViewHolder extends Obs
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to bind item (from {@link #getObservableCollection()}) to item-specific ViewHolder.
|
||||
* It is not calling for headers and footer which counts are returned by {@link #getHeadersCount()} and @link #getFootersCount()}.
|
||||
*
|
||||
* @param holder ViewHolder to bind item to;
|
||||
* @param position Position of ViewHolder (NOT item!);
|
||||
* @param item Item returned by position (WITH HEADER OFFSET!).
|
||||
*/
|
||||
protected abstract void onBindItemToViewHolder(@NonNull TViewHolder holder, int position, @NonNull TItem item);
|
||||
|
||||
@Nullable
|
||||
|
|
@ -255,11 +312,6 @@ public abstract class ObservableCollectionAdapter<TItem, TViewHolder extends Obs
|
|||
return positionInList < 0 || positionInList >= innerCollection.size() ? null : innerCollection.get(positionInList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return getHeadersCount() + innerCollection.size() + getFootersCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewAttachedToWindow(@NonNull final BindableViewHolder holder) {
|
||||
super.onViewAttachedToWindow(holder);
|
||||
|
|
@ -272,16 +324,55 @@ public abstract class ObservableCollectionAdapter<TItem, TViewHolder extends Obs
|
|||
super.onViewDetachedFromWindow(holder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets item click listener.
|
||||
*
|
||||
* @param onItemClickListener Item click listener.
|
||||
*/
|
||||
public void setOnItemClickListener(@Nullable final OnItemClickListener<TItem> onItemClickListener) {
|
||||
this.onItemClickListener = onItemClickListener;
|
||||
refreshUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns delay of item click. By default returns delay of ripple effect duration.
|
||||
*
|
||||
* @return Milliseconds delay of click.
|
||||
*/
|
||||
protected long getItemClickDelay() {
|
||||
return UiUtils.RIPPLE_EFFECT_DELAY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if click listening disabled or not for specific item.
|
||||
*
|
||||
* @param item Item to check click availability;
|
||||
* @return True if click listener enabled for such item.
|
||||
*/
|
||||
public boolean isOnClickListenerDisabled(@NonNull final TItem item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface to simply add item click listener.
|
||||
*
|
||||
* @param <TItem> Type of item
|
||||
*/
|
||||
public interface OnItemClickListener<TItem> {
|
||||
|
||||
/**
|
||||
* Calls when item have clicked.
|
||||
*
|
||||
* @param item Clicked item;
|
||||
* @param position Position of clicked item.
|
||||
*/
|
||||
void onItemClicked(@NonNull TItem item, int position);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Base item ViewHolder that have included pre-loading logic.
|
||||
*/
|
||||
public class ViewHolder extends BindableViewHolder {
|
||||
|
||||
@Nullable
|
||||
|
|
@ -291,7 +382,11 @@ public abstract class ObservableCollectionAdapter<TItem, TViewHolder extends Obs
|
|||
super(baseBindable, itemView);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
/**
|
||||
* Bind position to enable pre-loading for connected {@link ObservableCollection}.
|
||||
*
|
||||
* @param position Position of ViewHolder.
|
||||
*/
|
||||
public void bindPosition(final int position) {
|
||||
if (historyPreLoadingSubscription != null) {
|
||||
historyPreLoadingSubscription.unsubscribe();
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ import rx.functions.Action0;
|
|||
public final class UiUtils {
|
||||
|
||||
/**
|
||||
* Delay to let user view rippleeffect before screen changed.
|
||||
* Delay to let user view ripple effect before screen changed.
|
||||
*/
|
||||
public static final long RIPPLE_EFFECT_DELAY = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? 150 : 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -19,4 +19,6 @@
|
|||
<attr name="isMultiline" format="boolean"/>
|
||||
</declare-styleable>
|
||||
|
||||
<item name="OBSERVABLE_COLLECTION_ITEM_VIEW_TYPE" type="id"/>
|
||||
|
||||
</resources>
|
||||
Loading…
Reference in New Issue