Skip to content

Commit dd9f301

Browse files
author
Hamid Palo
committed
Add ViewObservables.listViewScroll(AbsListView).
1 parent aa1f2aa commit dd9f301

File tree

6 files changed

+372
-19
lines changed

6 files changed

+372
-19
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
15+
package rx.android.events;
16+
17+
import android.widget.AbsListView;
18+
19+
public class OnListViewScrollEvent {
20+
public final AbsListView listView;
21+
public final int scrollState;
22+
public final int firstVisibleItem;
23+
public final int visibleItemCount;
24+
public final int totalItemCount;
25+
26+
public OnListViewScrollEvent(
27+
AbsListView listView, int scrollState, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
28+
this.listView = listView;
29+
this.scrollState = scrollState;
30+
this.firstVisibleItem = firstVisibleItem;
31+
this.visibleItemCount = visibleItemCount;
32+
this.totalItemCount = totalItemCount;
33+
}
34+
35+
@Override
36+
public boolean equals(Object o) {
37+
if (this == o) {
38+
return true;
39+
}
40+
if (o == null || getClass() != o.getClass()) {
41+
return false;
42+
}
43+
44+
OnListViewScrollEvent that = (OnListViewScrollEvent) o;
45+
46+
if (firstVisibleItem != that.firstVisibleItem) {
47+
return false;
48+
}
49+
if (scrollState != that.scrollState) {
50+
return false;
51+
}
52+
if (totalItemCount != that.totalItemCount) {
53+
return false;
54+
}
55+
if (visibleItemCount != that.visibleItemCount) {
56+
return false;
57+
}
58+
if (!listView.equals(that.listView)) {
59+
return false;
60+
}
61+
62+
return true;
63+
}
64+
65+
@Override
66+
public int hashCode() {
67+
int result = listView.hashCode();
68+
result = 31 * result + scrollState;
69+
result = 31 * result + firstVisibleItem;
70+
result = 31 * result + visibleItemCount;
71+
result = 31 * result + totalItemCount;
72+
return result;
73+
}
74+
75+
@Override
76+
public String toString() {
77+
return "OnListViewScrollEvent{" +
78+
"listView=" + listView +
79+
", scrollState=" + scrollState +
80+
", firstVisibleItem=" + firstVisibleItem +
81+
", visibleItemCount=" + visibleItemCount +
82+
", totalItemCount=" + totalItemCount +
83+
'}';
84+
}
85+
}

library/src/main/java/rx/android/observables/ViewObservable.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
package rx.android.observables;
1515

1616
import android.view.View;
17+
import android.widget.AbsListView;
1718
import android.widget.AdapterView;
1819
import android.widget.CompoundButton;
1920
import android.widget.TextView;
@@ -22,9 +23,11 @@
2223
import rx.android.events.OnCheckedChangeEvent;
2324
import rx.android.events.OnClickEvent;
2425
import rx.android.events.OnItemClickEvent;
26+
import rx.android.events.OnListViewScrollEvent;
2527
import rx.android.events.OnTextChangeEvent;
2628
import rx.android.operators.OperatorAdapterViewOnItemClick;
2729
import rx.android.operators.OperatorCompoundButtonInput;
30+
import rx.android.operators.OnSubscribeListViewScroll;
2831
import rx.android.operators.OperatorTextViewInput;
2932
import rx.android.operators.OperatorViewClick;
3033

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

64+
/**
65+
* Returns an observable that emits all the scroll events from the provided ListView.
66+
* Note that this will replace any listeners previously set through
67+
* {@link android.widget.AbsListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)} unless those
68+
* were set by this method or {@link rx.android.operators.OnSubscribeListViewScroll}.
69+
*/
70+
public static Observable<OnListViewScrollEvent> listScrollEvents(final AbsListView listView) {
71+
return Observable.create(new OnSubscribeListViewScroll(listView));
72+
}
6173
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
15+
package rx.android.operators;
16+
17+
import android.widget.AbsListView;
18+
import android.widget.AdapterView;
19+
import rx.Observable;
20+
import rx.Subscriber;
21+
import rx.android.events.OnListViewScrollEvent;
22+
import rx.android.observables.Assertions;
23+
import rx.android.subscriptions.AndroidSubscriptions;
24+
import rx.functions.Action0;
25+
26+
import java.util.ArrayList;
27+
import java.util.List;
28+
import java.util.Map;
29+
import java.util.WeakHashMap;
30+
31+
public class OnSubscribeListViewScroll implements Observable.OnSubscribe<OnListViewScrollEvent> {
32+
33+
private final AbsListView listView;
34+
35+
public OnSubscribeListViewScroll(AbsListView listView) {
36+
this.listView = listView;
37+
}
38+
39+
@Override
40+
public void call(final Subscriber<? super OnListViewScrollEvent> observer) {
41+
Assertions.assertUiThread();
42+
43+
final CompositeOnScrollListener composite = CachedListeners.getFromViewOrCreate(listView);
44+
final AbsListView.OnScrollListener listener = new AbsListView.OnScrollListener() {
45+
int currentScrollState = SCROLL_STATE_IDLE;
46+
47+
@Override
48+
public void onScrollStateChanged(AbsListView view, int scrollState) {
49+
this.currentScrollState = scrollState;
50+
}
51+
52+
@Override
53+
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
54+
OnListViewScrollEvent event = new OnListViewScrollEvent(view, this.currentScrollState, firstVisibleItem,
55+
visibleItemCount, totalItemCount);
56+
observer.onNext(event);
57+
}
58+
};
59+
60+
composite.addOnScrollListener(listener);
61+
observer.add(AndroidSubscriptions.unsubscribeInUiThread(new Action0() {
62+
@Override
63+
public void call() {
64+
composite.removeOnScrollListener(listener);
65+
}
66+
}));
67+
}
68+
69+
private static class CompositeOnScrollListener implements AbsListView.OnScrollListener {
70+
71+
private final List<AbsListView.OnScrollListener> listeners = new ArrayList<AbsListView.OnScrollListener>();
72+
73+
public boolean addOnScrollListener(final AbsListView.OnScrollListener listener) {
74+
return listeners.add(listener);
75+
}
76+
77+
public boolean removeOnScrollListener(final AbsListView.OnScrollListener listener) {
78+
return listeners.remove(listener);
79+
}
80+
81+
@Override
82+
public void onScrollStateChanged(AbsListView view, int scrollState) {
83+
for (AbsListView.OnScrollListener listener : listeners) {
84+
listener.onScrollStateChanged(view, scrollState);
85+
}
86+
}
87+
88+
@Override
89+
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
90+
for (AbsListView.OnScrollListener listener : listeners) {
91+
listener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
92+
}
93+
}
94+
}
95+
96+
private static class CachedListeners {
97+
98+
private static final Map<AdapterView<?>, CompositeOnScrollListener> sCachedListeners =
99+
new WeakHashMap<AdapterView<?>, CompositeOnScrollListener>();
100+
101+
public static CompositeOnScrollListener getFromViewOrCreate(final AbsListView view) {
102+
final CompositeOnScrollListener cached = sCachedListeners.get(view);
103+
if (cached != null) {
104+
return cached;
105+
}
106+
107+
final CompositeOnScrollListener listener = new CompositeOnScrollListener();
108+
109+
sCachedListeners.put(view, listener);
110+
view.setOnScrollListener(listener);
111+
112+
return listener;
113+
}
114+
}
115+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package rx.android.operators;
2+
3+
import android.widget.AbsListView;
4+
import android.widget.ListView;
5+
import org.junit.Before;
6+
import org.junit.Test;
7+
import org.junit.runner.RunWith;
8+
import org.mockito.ArgumentCaptor;
9+
import org.mockito.Captor;
10+
import org.mockito.InOrder;
11+
import org.mockito.Mock;
12+
import org.mockito.MockitoAnnotations;
13+
import org.robolectric.RobolectricTestRunner;
14+
import rx.Observer;
15+
import rx.Subscription;
16+
import rx.android.events.OnListViewScrollEvent;
17+
import rx.android.observables.ViewObservable;
18+
import rx.observers.TestObserver;
19+
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
23+
import static org.mockito.Mockito.*;
24+
25+
@RunWith(RobolectricTestRunner.class)
26+
public class OnSubscribeListViewScrollTest {
27+
28+
@Mock
29+
private ListView listView;
30+
31+
@Captor
32+
private ArgumentCaptor<AbsListView.OnScrollListener> captor;
33+
34+
private List<OnListViewScrollEvent> events;
35+
36+
@Before
37+
public void setup() {
38+
MockitoAnnotations.initMocks(this);
39+
40+
events = new ArrayList<OnListViewScrollEvent>();
41+
for (int i = 0; i < 10; i++) {
42+
events.add(
43+
new OnListViewScrollEvent(listView, AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL, i, 2, 10));
44+
}
45+
}
46+
47+
@Test
48+
@SuppressWarnings("unchecked")
49+
public void testEventsEmitted() {
50+
Observer<OnListViewScrollEvent> observer = mock(Observer.class);
51+
Subscription subscription =
52+
ViewObservable.listScrollEvents(listView).subscribe(new TestObserver<OnListViewScrollEvent>(observer));
53+
54+
verify(observer, never()).onNext(any(OnListViewScrollEvent.class));
55+
56+
verify(listView, times(1)).setOnScrollListener(captor.capture());
57+
captor.getValue().onScrollStateChanged(listView, AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
58+
59+
InOrder inOrder = inOrder(observer);
60+
61+
verifyObserversGetEvents(inOrder, observer);
62+
63+
subscription.unsubscribe();
64+
65+
verifyNoInteractionsOnEventEmit(inOrder);
66+
}
67+
68+
@SuppressWarnings("unchecked")
69+
public void testMultipleObservablesOnSameViewGetCorrectEvents() {
70+
Observer<OnListViewScrollEvent> observerA = mock(Observer.class);
71+
Subscription subscriptionA = ViewObservable.listScrollEvents(listView)
72+
.subscribe(new TestObserver<OnListViewScrollEvent>(observerA));
73+
74+
Observer<OnListViewScrollEvent> observerB = mock(Observer.class);
75+
Subscription subscriptionB = ViewObservable.listScrollEvents(listView)
76+
.subscribe(new TestObserver<OnListViewScrollEvent>(observerB));
77+
78+
verify(listView, times(2)).setOnScrollListener(captor.capture());
79+
80+
InOrder inOrder = inOrder(observerA, observerB);
81+
verifyObserversGetEvents(inOrder, observerA, observerB);
82+
83+
subscriptionA.unsubscribe();
84+
85+
verifyObserversGetEvents(inOrder, observerB);
86+
87+
subscriptionB.unsubscribe();
88+
verifyNoInteractionsOnEventEmit(inOrder);
89+
}
90+
91+
private void verifyNoInteractionsOnEventEmit(InOrder inOrder) {
92+
OnListViewScrollEvent lastEvent = events.get(0);
93+
captor.getValue().onScroll(listView,
94+
lastEvent.firstVisibleItem,
95+
lastEvent.visibleItemCount,
96+
lastEvent.totalItemCount);
97+
inOrder.verifyNoMoreInteractions();
98+
}
99+
100+
private void verifyObserversGetEvents(InOrder inOrder, Observer<OnListViewScrollEvent>... observers) {
101+
for (OnListViewScrollEvent event : events) {
102+
captor.getValue().onScroll(event.listView,
103+
event.firstVisibleItem,
104+
event.visibleItemCount,
105+
event.totalItemCount);
106+
for (Observer<OnListViewScrollEvent> observer : observers) {
107+
inOrder.verify(observer, times(1)).onNext(event);
108+
}
109+
}
110+
111+
inOrder.verifyNoMoreInteractions();
112+
}
113+
}

0 commit comments

Comments
 (0)