Skip to content

Commit 5109b4c

Browse files
committed
Super basic infinite scroll
Change-Id: I9890f5c629ce972624a5cf0fcd5276e83f48d1e5
1 parent 6fbe224 commit 5109b4c

File tree

3 files changed

+267
-8
lines changed

3 files changed

+267
-8
lines changed

app/src/main/java/com/firebase/uidemo/database/firestore/FirestoreChatActivity.java

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
import android.widget.Toast;
1616

1717
import com.firebase.ui.auth.util.ui.ImeHelper;
18+
import com.firebase.ui.firestore.ClassSnapshotParser;
19+
import com.firebase.ui.firestore.FirestoreInfiniteArray;
20+
import com.firebase.ui.firestore.FirestoreInfiniteScrollListener;
1821
import com.firebase.ui.firestore.FirestoreRecyclerAdapter;
1922
import com.firebase.ui.firestore.FirestoreRecyclerOptions;
2023
import com.firebase.uidemo.R;
@@ -47,6 +50,9 @@ public class FirestoreChatActivity extends AppCompatActivity
4750
/** Get the last 50 chat messages ordered by timestamp . */
4851
private static final Query sChatQuery = sChatCollection.orderBy("timestamp").limit(50);
4952

53+
private LinearLayoutManager mManager;
54+
private FirestoreInfiniteArray<Chat> mArray;
55+
5056
static {
5157
FirebaseFirestore.setLoggingEnabled(true);
5258
}
@@ -69,8 +75,14 @@ protected void onCreate(Bundle savedInstanceState) {
6975
setContentView(R.layout.activity_chat);
7076
ButterKnife.bind(this);
7177

78+
mArray = new FirestoreInfiniteArray<>(
79+
sChatCollection.orderBy("timestamp", Query.Direction.ASCENDING),
80+
sChatCollection.orderBy("timestamp", Query.Direction.DESCENDING),
81+
new ClassSnapshotParser<>(Chat.class));
82+
83+
mManager = new LinearLayoutManager(this);
7284
mRecyclerView.setHasFixedSize(true);
73-
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
85+
mRecyclerView.setLayoutManager(mManager);
7486

7587
ImeHelper.setImeOnDoneListener(mMessageEdit, new ImeHelper.DonePressedListener() {
7688
@Override
@@ -114,12 +126,16 @@ private void attachRecyclerViewAdapter() {
114126
final RecyclerView.Adapter adapter = newAdapter();
115127

116128
// Scroll to bottom on new messages
117-
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
118-
@Override
119-
public void onItemRangeInserted(int positionStart, int itemCount) {
120-
mRecyclerView.smoothScrollToPosition(adapter.getItemCount());
121-
}
122-
});
129+
// TODO
130+
// adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
131+
// @Override
132+
// public void onItemRangeInserted(int positionStart, int itemCount) {
133+
// mRecyclerView.smoothScrollToPosition(adapter.getItemCount());
134+
// }
135+
// });
136+
137+
// Scroll listener for infinite
138+
mRecyclerView.addOnScrollListener(new FirestoreInfiniteScrollListener(mManager, mArray));
123139

124140
mRecyclerView.setAdapter(adapter);
125141
}
@@ -137,7 +153,7 @@ public void onSendClick() {
137153
protected RecyclerView.Adapter newAdapter() {
138154
FirestoreRecyclerOptions<Chat> options =
139155
new FirestoreRecyclerOptions.Builder<Chat>()
140-
.setQuery(sChatQuery, Chat.class)
156+
.setSnapshotArray(mArray)
141157
.setLifecycleOwner(this)
142158
.build();
143159

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
package com.firebase.ui.firestore;
2+
3+
import android.support.annotation.NonNull;
4+
import android.support.annotation.Nullable;
5+
import android.util.Log;
6+
7+
import com.firebase.ui.common.ChangeEventType;
8+
import com.google.firebase.firestore.DocumentSnapshot;
9+
import com.google.firebase.firestore.FirebaseFirestoreException;
10+
import com.google.firebase.firestore.Query;
11+
12+
import java.util.ArrayList;
13+
import java.util.Collections;
14+
import java.util.List;
15+
16+
public class FirestoreInfiniteArray<T> extends ObservableSnapshotArray<T> {
17+
18+
private static final String TAG = "FirestoreInfiniteArray";
19+
20+
private SnapshotParser<T> mParser;
21+
22+
private Query mForwardQuery;
23+
private Query mReverseQuery;
24+
25+
private int mPageSize = 10;
26+
27+
private List<Page> mPages = new ArrayList<>();
28+
29+
public FirestoreInfiniteArray(Query forwardQuery, Query reverseQuery, SnapshotParser<T> parser) {
30+
super(parser);
31+
32+
mParser = parser;
33+
mForwardQuery = forwardQuery;
34+
mReverseQuery = reverseQuery;
35+
36+
Page page = new Page(0, queryAfter(null));
37+
mPages.add(page);
38+
}
39+
40+
public void loadNextPage() {
41+
Page lastPage = getLastPage();
42+
43+
if (lastPage.getState() == PageState.LOADING) {
44+
Log.d(TAG, "Skipping double-load.");
45+
return;
46+
}
47+
48+
int size = size();
49+
DocumentSnapshot lastSnapshot = lastPage.getLast();
50+
51+
if (lastSnapshot == null) {
52+
Log.w(TAG, "Skipping load because last snapshot is null!");
53+
return;
54+
}
55+
56+
Query nextQuery = queryAfter(lastSnapshot);
57+
Log.d(TAG, "loadNextPage: sizeBefore=" + size + ", lastId=" + lastSnapshot.getId());
58+
Page nextPage = new Page(size, nextQuery);
59+
mPages.add(nextPage);
60+
}
61+
62+
63+
@NonNull
64+
@Override
65+
protected List<DocumentSnapshot> getSnapshots() {
66+
// TODO: How to allow outside clearing?
67+
Log.w(TAG, "Called getSnapshots() on an infinite array.");
68+
return Collections.emptyList();
69+
}
70+
71+
@NonNull
72+
@Override
73+
public DocumentSnapshot getSnapshot(int index) {
74+
int remaining = index;
75+
for (Page page : mPages) {
76+
if (remaining < page.size()) {
77+
return page.get(remaining);
78+
}
79+
80+
remaining -= page.size();
81+
}
82+
83+
// TODO
84+
Log.e(TAG, "Requested bad index: " + index);
85+
return null;
86+
}
87+
88+
@NonNull
89+
@Override
90+
public T get(int index) {
91+
return mParser.parseSnapshot(getSnapshot(index));
92+
}
93+
94+
@Override
95+
public int size() {
96+
int size = 0;
97+
98+
for (Page page : mPages) {
99+
size += page.size();
100+
}
101+
102+
return size;
103+
}
104+
105+
private Page getLastPage() {
106+
return mPages.get(mPages.size() - 1);
107+
}
108+
109+
private Query queryAfter(@Nullable DocumentSnapshot snapshot) {
110+
if (snapshot == null) {
111+
return mForwardQuery.limit(mPageSize);
112+
}
113+
114+
return mForwardQuery
115+
.startAfter(snapshot)
116+
.limit(mPageSize);
117+
}
118+
119+
private enum PageState {
120+
LOADING,
121+
LOADED
122+
}
123+
124+
private class Page implements ChangeEventListener {
125+
126+
// TODO: state
127+
128+
private PageState mState = PageState.LOADING;
129+
130+
public final int mStartingPosition;
131+
public final Query mQuery;
132+
public final FirestoreArray<T> mItems;
133+
134+
private List<DocumentSnapshot> mSnapshots = new ArrayList<>();
135+
136+
public Page(int startingPosition, Query query) {
137+
mStartingPosition = startingPosition;
138+
mQuery = query;
139+
140+
mItems = new FirestoreArray<>(query, mParser);
141+
mItems.addChangeEventListener(this);
142+
}
143+
144+
@Override
145+
public void onChildChanged(@NonNull ChangeEventType type,
146+
@NonNull DocumentSnapshot snapshot,
147+
int newIndex,
148+
int oldIndex) {
149+
switch (type) {
150+
// TODO: Implement all types
151+
case ADDED:
152+
int newIndexAdjusted = newIndex + mStartingPosition;
153+
int oldIndexAdjusted = oldIndex == -1
154+
? -1
155+
: oldIndex + mStartingPosition;
156+
157+
Log.d(TAG, "onChildAdded, old=" + oldIndexAdjusted + ", new=" + newIndexAdjusted);
158+
notifyOnChildChanged(type, snapshot, newIndexAdjusted, oldIndexAdjusted);
159+
mSnapshots.add(snapshot);
160+
break;
161+
case MOVED:
162+
break;
163+
case CHANGED:
164+
break;
165+
case REMOVED:
166+
break;
167+
}
168+
}
169+
170+
@Override
171+
public void onDataChanged() {
172+
// TODO
173+
Log.d(TAG, "onDataChanged");
174+
mState = PageState.LOADED;
175+
}
176+
177+
@Override
178+
public void onError(@NonNull FirebaseFirestoreException e) {
179+
// TODO
180+
mState = PageState.LOADED;
181+
}
182+
183+
public PageState getState() {
184+
return mState;
185+
}
186+
187+
public DocumentSnapshot get(int index) {
188+
return mSnapshots.get(index);
189+
}
190+
191+
public int size() {
192+
return mSnapshots.size();
193+
}
194+
195+
@Nullable
196+
public DocumentSnapshot getLast() {
197+
if (mSnapshots == null || mSnapshots.isEmpty()) {
198+
return null;
199+
}
200+
return mSnapshots.get(mSnapshots.size() - 1);
201+
}
202+
}
203+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.firebase.ui.firestore;
2+
3+
import android.support.v7.widget.LinearLayoutManager;
4+
import android.support.v7.widget.RecyclerView;
5+
6+
public class FirestoreInfiniteScrollListener extends RecyclerView.OnScrollListener {
7+
8+
private LinearLayoutManager mManager;
9+
private FirestoreInfiniteArray mArray;
10+
11+
public FirestoreInfiniteScrollListener(LinearLayoutManager manager,
12+
FirestoreInfiniteArray array) {
13+
mManager = manager;
14+
mArray = array;
15+
}
16+
17+
@Override
18+
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
19+
super.onScrollStateChanged(recyclerView, newState);
20+
}
21+
22+
@Override
23+
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
24+
super.onScrolled(recyclerView, dx, dy);
25+
26+
int firstVisible = mManager.findFirstVisibleItemPosition();
27+
int lastVisible = mManager.findLastVisibleItemPosition();
28+
29+
int totalSize = mArray.size();
30+
31+
// TODO: configurable "closeness"
32+
boolean movingDown = dy > 0;
33+
boolean closeToBottom = (totalSize - lastVisible) <= 5;
34+
35+
if (closeToBottom && movingDown) {
36+
mArray.loadNextPage();
37+
}
38+
}
39+
40+
}

0 commit comments

Comments
 (0)