Skip to content

Commit

Permalink
Add ViewObservables.listViewScroll(AbsListView).
Browse files Browse the repository at this point in the history
  • Loading branch information
Hamid Palo committed Nov 20, 2014
1 parent aa1f2aa commit dd9f301
Show file tree
Hide file tree
Showing 6 changed files with 372 additions and 19 deletions.
85 changes: 85 additions & 0 deletions library/src/main/java/rx/android/events/OnListViewScrollEvent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* 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 rx.android.events;

import android.widget.AbsListView;

public class OnListViewScrollEvent {
public final AbsListView listView;
public final int scrollState;
public final int firstVisibleItem;
public final int visibleItemCount;
public final int totalItemCount;

public OnListViewScrollEvent(
AbsListView listView, int scrollState, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
this.listView = listView;
this.scrollState = scrollState;
this.firstVisibleItem = firstVisibleItem;
this.visibleItemCount = visibleItemCount;
this.totalItemCount = totalItemCount;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}

OnListViewScrollEvent that = (OnListViewScrollEvent) o;

if (firstVisibleItem != that.firstVisibleItem) {
return false;
}
if (scrollState != that.scrollState) {
return false;
}
if (totalItemCount != that.totalItemCount) {
return false;
}
if (visibleItemCount != that.visibleItemCount) {
return false;
}
if (!listView.equals(that.listView)) {
return false;
}

return true;
}

@Override
public int hashCode() {
int result = listView.hashCode();
result = 31 * result + scrollState;
result = 31 * result + firstVisibleItem;
result = 31 * result + visibleItemCount;
result = 31 * result + totalItemCount;
return result;
}

@Override
public String toString() {
return "OnListViewScrollEvent{" +
"listView=" + listView +
", scrollState=" + scrollState +
", firstVisibleItem=" + firstVisibleItem +
", visibleItemCount=" + visibleItemCount +
", totalItemCount=" + totalItemCount +
'}';
}
}
12 changes: 12 additions & 0 deletions library/src/main/java/rx/android/observables/ViewObservable.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
package rx.android.observables;

import android.view.View;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.CompoundButton;
import android.widget.TextView;
Expand All @@ -22,9 +23,11 @@
import rx.android.events.OnCheckedChangeEvent;
import rx.android.events.OnClickEvent;
import rx.android.events.OnItemClickEvent;
import rx.android.events.OnListViewScrollEvent;
import rx.android.events.OnTextChangeEvent;
import rx.android.operators.OperatorAdapterViewOnItemClick;
import rx.android.operators.OperatorCompoundButtonInput;
import rx.android.operators.OnSubscribeListViewScroll;
import rx.android.operators.OperatorTextViewInput;
import rx.android.operators.OperatorViewClick;

Expand Down Expand Up @@ -58,4 +61,13 @@ public static Observable<OnItemClickEvent> itemClicks(final AdapterView<?> adapt
return Observable.create(new OperatorAdapterViewOnItemClick(adapterView));
}

/**
* Returns an observable that emits all the scroll events from the provided ListView.
* Note that this will replace any listeners previously set through
* {@link android.widget.AbsListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)} unless those
* were set by this method or {@link rx.android.operators.OnSubscribeListViewScroll}.
*/
public static Observable<OnListViewScrollEvent> listScrollEvents(final AbsListView listView) {
return Observable.create(new OnSubscribeListViewScroll(listView));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* 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 rx.android.operators;

import android.widget.AbsListView;
import android.widget.AdapterView;
import rx.Observable;
import rx.Subscriber;
import rx.android.events.OnListViewScrollEvent;
import rx.android.observables.Assertions;
import rx.android.subscriptions.AndroidSubscriptions;
import rx.functions.Action0;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;

public class OnSubscribeListViewScroll implements Observable.OnSubscribe<OnListViewScrollEvent> {

private final AbsListView listView;

public OnSubscribeListViewScroll(AbsListView listView) {
this.listView = listView;
}

@Override
public void call(final Subscriber<? super OnListViewScrollEvent> observer) {
Assertions.assertUiThread();

final CompositeOnScrollListener composite = CachedListeners.getFromViewOrCreate(listView);
final AbsListView.OnScrollListener listener = new AbsListView.OnScrollListener() {
int currentScrollState = SCROLL_STATE_IDLE;

@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
this.currentScrollState = scrollState;
}

@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
OnListViewScrollEvent event = new OnListViewScrollEvent(view, this.currentScrollState, firstVisibleItem,
visibleItemCount, totalItemCount);
observer.onNext(event);
}
};

composite.addOnScrollListener(listener);
observer.add(AndroidSubscriptions.unsubscribeInUiThread(new Action0() {
@Override
public void call() {
composite.removeOnScrollListener(listener);
}
}));
}

private static class CompositeOnScrollListener implements AbsListView.OnScrollListener {

private final List<AbsListView.OnScrollListener> listeners = new ArrayList<AbsListView.OnScrollListener>();

public boolean addOnScrollListener(final AbsListView.OnScrollListener listener) {
return listeners.add(listener);
}

public boolean removeOnScrollListener(final AbsListView.OnScrollListener listener) {
return listeners.remove(listener);
}

@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
for (AbsListView.OnScrollListener listener : listeners) {
listener.onScrollStateChanged(view, scrollState);
}
}

@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
for (AbsListView.OnScrollListener listener : listeners) {
listener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
}
}
}

private static class CachedListeners {

private static final Map<AdapterView<?>, CompositeOnScrollListener> sCachedListeners =
new WeakHashMap<AdapterView<?>, CompositeOnScrollListener>();

public static CompositeOnScrollListener getFromViewOrCreate(final AbsListView view) {
final CompositeOnScrollListener cached = sCachedListeners.get(view);
if (cached != null) {
return cached;
}

final CompositeOnScrollListener listener = new CompositeOnScrollListener();

sCachedListeners.put(view, listener);
view.setOnScrollListener(listener);

return listener;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package rx.android.operators;

import android.widget.AbsListView;
import android.widget.ListView;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import rx.Observer;
import rx.Subscription;
import rx.android.events.OnListViewScrollEvent;
import rx.android.observables.ViewObservable;
import rx.observers.TestObserver;

import java.util.ArrayList;
import java.util.List;

import static org.mockito.Mockito.*;

@RunWith(RobolectricTestRunner.class)
public class OnSubscribeListViewScrollTest {

@Mock
private ListView listView;

@Captor
private ArgumentCaptor<AbsListView.OnScrollListener> captor;

private List<OnListViewScrollEvent> events;

@Before
public void setup() {
MockitoAnnotations.initMocks(this);

events = new ArrayList<OnListViewScrollEvent>();
for (int i = 0; i < 10; i++) {
events.add(
new OnListViewScrollEvent(listView, AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL, i, 2, 10));
}
}

@Test
@SuppressWarnings("unchecked")
public void testEventsEmitted() {
Observer<OnListViewScrollEvent> observer = mock(Observer.class);
Subscription subscription =
ViewObservable.listScrollEvents(listView).subscribe(new TestObserver<OnListViewScrollEvent>(observer));

verify(observer, never()).onNext(any(OnListViewScrollEvent.class));

verify(listView, times(1)).setOnScrollListener(captor.capture());
captor.getValue().onScrollStateChanged(listView, AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);

InOrder inOrder = inOrder(observer);

verifyObserversGetEvents(inOrder, observer);

subscription.unsubscribe();

verifyNoInteractionsOnEventEmit(inOrder);
}

@SuppressWarnings("unchecked")
public void testMultipleObservablesOnSameViewGetCorrectEvents() {
Observer<OnListViewScrollEvent> observerA = mock(Observer.class);
Subscription subscriptionA = ViewObservable.listScrollEvents(listView)
.subscribe(new TestObserver<OnListViewScrollEvent>(observerA));

Observer<OnListViewScrollEvent> observerB = mock(Observer.class);
Subscription subscriptionB = ViewObservable.listScrollEvents(listView)
.subscribe(new TestObserver<OnListViewScrollEvent>(observerB));

verify(listView, times(2)).setOnScrollListener(captor.capture());

InOrder inOrder = inOrder(observerA, observerB);
verifyObserversGetEvents(inOrder, observerA, observerB);

subscriptionA.unsubscribe();

verifyObserversGetEvents(inOrder, observerB);

subscriptionB.unsubscribe();
verifyNoInteractionsOnEventEmit(inOrder);
}

private void verifyNoInteractionsOnEventEmit(InOrder inOrder) {
OnListViewScrollEvent lastEvent = events.get(0);
captor.getValue().onScroll(listView,
lastEvent.firstVisibleItem,
lastEvent.visibleItemCount,
lastEvent.totalItemCount);
inOrder.verifyNoMoreInteractions();
}

private void verifyObserversGetEvents(InOrder inOrder, Observer<OnListViewScrollEvent>... observers) {
for (OnListViewScrollEvent event : events) {
captor.getValue().onScroll(event.listView,
event.firstVisibleItem,
event.visibleItemCount,
event.totalItemCount);
for (Observer<OnListViewScrollEvent> observer : observers) {
inOrder.verify(observer, times(1)).onNext(event);
}
}

inOrder.verifyNoMoreInteractions();
}
}
Loading

0 comments on commit dd9f301

Please sign in to comment.