diff --git a/ParseUI-Widget-Sample/build.gradle b/ParseUI-Widget-Sample/build.gradle
index 883e312..e87cab4 100644
--- a/ParseUI-Widget-Sample/build.gradle
+++ b/ParseUI-Widget-Sample/build.gradle
@@ -21,6 +21,7 @@ android {
dependencies {
compile 'com.android.support:appcompat-v7:23.1.1'
+ compile 'com.android.support:recyclerview-v7:23.1.0'
compile project(':ParseUI-Widget')
testCompile 'junit:junit:4.12'
diff --git a/ParseUI-Widget-Sample/src/main/AndroidManifest.xml b/ParseUI-Widget-Sample/src/main/AndroidManifest.xml
index 37ac933..e80db45 100644
--- a/ParseUI-Widget-Sample/src/main/AndroidManifest.xml
+++ b/ParseUI-Widget-Sample/src/main/AndroidManifest.xml
@@ -28,6 +28,10 @@
+
+
diff --git a/ParseUI-Widget-Sample/src/main/java/com/parse/ui/widget/sample/ListActivity.java b/ParseUI-Widget-Sample/src/main/java/com/parse/ui/widget/sample/ListActivity.java
index 08a0595..59d8cc5 100644
--- a/ParseUI-Widget-Sample/src/main/java/com/parse/ui/widget/sample/ListActivity.java
+++ b/ParseUI-Widget-Sample/src/main/java/com/parse/ui/widget/sample/ListActivity.java
@@ -1,23 +1,30 @@
package com.parse.ui.widget.sample;
+import android.database.DataSetObserver;
import android.os.Bundle;
import android.support.annotation.Nullable;
+import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.AppCompatActivity;
-import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
import android.widget.ListView;
-import android.widget.Toast;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import com.parse.FindCallback;
import com.parse.ParseException;
import com.parse.ParseObject;
import com.parse.ParseQuery;
-import com.parse.ParseQueryAdapter;
+import com.parse.widget.util.ParseQueryPager;
import java.util.List;
+import bolts.CancellationTokenSource;
-public class ListActivity extends AppCompatActivity {
- private static final String TAG = "ListActivity";
+public class ListActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -26,32 +33,230 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
ListView listView = (ListView) findViewById(R.id.list);
- ParseQueryAdapter adapter = new ParseQueryAdapter<>(this,
- new ParseQueryAdapter.QueryFactory() {
+ final MyAdapter adapter = new MyAdapter<>(createPager());
+ listView.setAdapter(adapter);
+
+ final SwipeRefreshLayout refreshLayout = (SwipeRefreshLayout) findViewById(R.id.refresh);
+ refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
+ @Override
+ public void onRefresh() {
+ final ParseQueryPager pager = createPager();
+ pager.loadNextPage(new FindCallback() {
@Override
- public ParseQuery create() {
- return ParseQuery.getQuery("Contact")
- .orderByAscending("name")
- .setCachePolicy(ParseQuery.CachePolicy.CACHE_THEN_NETWORK);
+ public void done(List objects, ParseException e) {
+ refreshLayout.setRefreshing(false);
+
+ if (objects == null && e == null) { // cancelled
+ return;
+ }
+
+ if (e != null) {
+ return;
+ }
+
+ adapter.swap(pager);
+ adapter.notifyDataSetChanged();
}
- }, android.R.layout.simple_list_item_1);
- adapter.setTextKey("name");
- adapter.addOnQueryLoadListener(new ParseQueryAdapter.OnQueryLoadListener() {
- @Override
- public void onLoading() {
- Log.d(TAG, "loading");
+ });
}
+ });
+ }
- @Override
- public void onLoaded(List objects, Exception e) {
- Log.d(TAG, "loaded");
- if (e != null
- && e instanceof ParseException
- && ((ParseException) e).getCode() != ParseException.CACHE_MISS) {
- Toast.makeText(ListActivity.this, "Error: " + e.getMessage(), Toast.LENGTH_LONG).show();
+ private ParseQueryPager createPager() {
+ ParseQuery query = ParseQuery.getQuery("TestObject");
+ query.orderByAscending("name");
+ query.setCachePolicy(ParseQuery.CachePolicy.CACHE_THEN_NETWORK);
+ return new ParseQueryPager<>(query, 25);
+ }
+
+ public static class MyAdapter extends BaseAdapter {
+
+ public static final int TYPE_ITEM = 0;
+ public static final int TYPE_NEXT = 1;
+
+ private static class ItemViewHolder extends ViewHolder {
+ TextView textView;
+
+ public ItemViewHolder(View itemView) {
+ super(itemView);
+ textView = (TextView) itemView;
+ }
+ }
+
+ private static class NextViewHolder extends ViewHolder {
+ TextView textView;
+ ProgressBar progressBar;
+
+ public NextViewHolder(View itemView) {
+ super(itemView);
+ textView = (TextView) itemView.findViewById(android.R.id.text1);
+ progressBar = (ProgressBar) itemView.findViewById(android.R.id.progress);
+ }
+
+ public void setLoading(boolean loading) {
+ if (loading) {
+ textView.setVisibility(View.INVISIBLE);
+ progressBar.setVisibility(View.VISIBLE);
+ progressBar.setIndeterminate(true);
+ } else {
+ textView.setVisibility(View.VISIBLE);
+ progressBar.setVisibility(View.INVISIBLE);
+ progressBar.setIndeterminate(false);
}
}
- });
- listView.setAdapter(adapter);
+ }
+
+ private final Object lock = new Object();
+ private ParseQueryPager pager;
+ private CancellationTokenSource cts;
+
+ public MyAdapter(ParseQueryPager pager) {
+ swap(pager);
+ }
+
+ public ParseQueryPager getPager() {
+ synchronized (lock) {
+ return pager;
+ }
+ }
+
+ public void swap(ParseQueryPager pager) {
+ synchronized (lock) {
+ if (cts != null) {
+ cts.cancel();
+ }
+ this.pager = pager;
+ this.cts = new CancellationTokenSource();
+ }
+ }
+
+ private void loadNextPage() {
+ final ParseQueryPager pager;
+ final CancellationTokenSource cts;
+
+ synchronized (lock) {
+ pager = this.pager;
+ cts = this.cts;
+ }
+
+ // Utilizing callbacks to support CACHE_THEN_NETWORK
+ pager.loadNextPage(new FindCallback() {
+ @Override
+ public void done(List results, ParseException e) {
+ if (results == null && e == null) { // cancelled
+ return;
+ }
+
+ if (e != null) {
+ notifyDataSetChanged();
+ return;
+ }
+
+ notifyDataSetChanged();
+ }
+ }, cts.getToken());
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getCount() {
+ ParseQueryPager pager = getPager();
+ return pager.getObjects().size() + (pager.hasNextPage() ? 1 : 0);
+ }
+
+ @Override
+ public T getItem(int position) {
+ List objects = getPager().getObjects();
+ return position < objects.size() ? objects.get(position) : null;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public final View getView(int position, View convertView, ViewGroup parent) {
+ ViewHolder holder;
+ View view;
+ if (convertView == null) {
+ holder = onCreateViewHolder(parent, getItemViewType(position));
+ view = holder.itemView;
+ view.setTag(holder);
+ } else {
+ view = convertView;
+ holder = (ViewHolder) view.getTag();
+ }
+ onBindViewHolder(holder, position);
+ return view;
+ }
+
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+ switch (viewType) {
+ case TYPE_ITEM: {
+ View v = inflater.inflate(android.R.layout.simple_list_item_1, parent, false);
+ return new ItemViewHolder(v);
+ }
+ case TYPE_NEXT: {
+ View v = inflater.inflate(R.layout.load_more_list_item, parent, false);
+ v.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (!getPager().isLoadingNextPage()) {
+ loadNextPage();
+ }
+ }
+ });
+ NextViewHolder vh = new NextViewHolder(v);
+ vh.textView.setText(R.string.load_more);
+ return vh;
+ }
+ default:
+ throw new IllegalStateException("Invalid view type: " + viewType);
+ }
+ }
+
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ switch (getItemViewType(position)) {
+ case TYPE_ITEM: {
+ ParseObject item = getItem(position);
+
+ ItemViewHolder vh = (ItemViewHolder) holder;
+ vh.textView.setText(item.getString("name"));
+ }
+ break;
+ case TYPE_NEXT: {
+ NextViewHolder vh = (NextViewHolder) holder;
+ vh.setLoading(getPager().isLoadingNextPage());
+ }
+ break;
+ }
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 2;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return position < getPager().getObjects().size() ? TYPE_ITEM : TYPE_NEXT;
+ }
+
+ @Override
+ public void registerDataSetObserver(DataSetObserver observer) {
+ super.registerDataSetObserver(observer);
+ // We use this method as a notification that the ListView is bound to the adapter.
+ loadNextPage();
+ }
+
+ public static class ViewHolder {
+ private View itemView;
+
+ public ViewHolder(View itemView) {
+ this.itemView = itemView;
+ }
+ }
}
}
diff --git a/ParseUI-Widget-Sample/src/main/java/com/parse/ui/widget/sample/MainActivity.java b/ParseUI-Widget-Sample/src/main/java/com/parse/ui/widget/sample/MainActivity.java
index be09b6e..a5bf8b3 100644
--- a/ParseUI-Widget-Sample/src/main/java/com/parse/ui/widget/sample/MainActivity.java
+++ b/ParseUI-Widget-Sample/src/main/java/com/parse/ui/widget/sample/MainActivity.java
@@ -12,6 +12,7 @@ protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
+ findViewById(R.id.sample_recycler).setOnClickListener(this);
findViewById(R.id.sample_list).setOnClickListener(this);
}
@@ -21,6 +22,11 @@ protected void onCreate(Bundle savedInstanceState) {
public void onClick(View v) {
int id = v.getId();
switch (id) {
+ case R.id.sample_recycler: {
+ Intent intent = new Intent(this, RecyclerActivity.class);
+ startActivity(intent);
+ break;
+ }
case R.id.sample_list: {
Intent intent = new Intent(this, ListActivity.class);
startActivity(intent);
diff --git a/ParseUI-Widget-Sample/src/main/java/com/parse/ui/widget/sample/RecyclerActivity.java b/ParseUI-Widget-Sample/src/main/java/com/parse/ui/widget/sample/RecyclerActivity.java
new file mode 100644
index 0000000..fc893dd
--- /dev/null
+++ b/ParseUI-Widget-Sample/src/main/java/com/parse/ui/widget/sample/RecyclerActivity.java
@@ -0,0 +1,244 @@
+package com.parse.ui.widget.sample;
+
+import android.os.Bundle;
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.parse.ParseObject;
+import com.parse.ParseQuery;
+import com.parse.widget.util.ParseQueryPager;
+
+import java.util.List;
+
+import bolts.CancellationTokenSource;
+import bolts.Continuation;
+import bolts.Task;
+
+public class RecyclerActivity extends AppCompatActivity {
+
+ private SwipeRefreshLayout refreshLayout;
+
+ private MyAdapter adapter;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_recycler);
+
+ RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler);
+
+ RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this);
+ recyclerView.setLayoutManager(layoutManager);
+
+ adapter = new MyAdapter<>(createPager());
+ recyclerView.setAdapter(adapter);
+
+ refreshLayout = (SwipeRefreshLayout) findViewById(R.id.refresh);
+ refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
+ @Override
+ public void onRefresh() {
+ final ParseQueryPager pager = createPager();
+ pager.loadNextPage().continueWith(new Continuation, Void>() {
+ @Override
+ public Void then(Task> task) throws Exception {
+ refreshLayout.setRefreshing(false);
+
+ if (task.isCancelled()) {
+ return null;
+ }
+
+ if (task.isFaulted()) {
+ return null;
+ }
+
+ adapter.swap(pager);
+ adapter.notifyDataSetChanged();
+ return null;
+ }
+ }, Task.UI_THREAD_EXECUTOR);
+ }
+ });
+ }
+
+ private ParseQueryPager createPager() {
+ ParseQuery query = ParseQuery.getQuery("TestObject");
+ query.orderByAscending("name");
+ return new ParseQueryPager<>(query, 25);
+ }
+
+ public static class MyAdapter extends RecyclerView.Adapter {
+
+ private static final int TYPE_ITEM = 0;
+ private static final int TYPE_NEXT = 1;
+
+ private static class ItemViewHolder extends RecyclerView.ViewHolder {
+ TextView textView;
+
+ public ItemViewHolder(View itemView) {
+ super(itemView);
+ textView = (TextView) itemView;
+ }
+ }
+
+ private static class NextViewHolder extends RecyclerView.ViewHolder {
+ TextView textView;
+ ProgressBar progressBar;
+
+ public NextViewHolder(View itemView) {
+ super(itemView);
+ textView = (TextView) itemView.findViewById(android.R.id.text1);
+ progressBar = (ProgressBar) itemView.findViewById(android.R.id.progress);
+ }
+
+ public void setLoading(boolean loading) {
+ if (loading) {
+ textView.setVisibility(View.INVISIBLE);
+ progressBar.setVisibility(View.VISIBLE);
+ progressBar.setIndeterminate(true);
+ } else {
+ textView.setVisibility(View.VISIBLE);
+ progressBar.setVisibility(View.INVISIBLE);
+ progressBar.setIndeterminate(false);
+ }
+ }
+ }
+
+ private final Object lock = new Object();
+ private ParseQueryPager pager;
+ private CancellationTokenSource cts;
+
+ public MyAdapter(ParseQueryPager pager) {
+ swap(pager);
+ }
+
+ public ParseQueryPager getPager() {
+ synchronized (lock) {
+ return pager;
+ }
+ }
+
+ public void swap(ParseQueryPager pager) {
+ synchronized (lock) {
+ if (cts != null) {
+ cts.cancel();
+ }
+ this.pager = pager;
+ this.cts = new CancellationTokenSource();
+ }
+ }
+
+ private void loadNextPage() {
+ final ParseQueryPager pager;
+ final CancellationTokenSource cts;
+
+ synchronized (lock) {
+ pager = this.pager;
+ cts = this.cts;
+ }
+
+ final int oldSize = pager.getObjects().size();
+
+ // Uses Tasks, so it does not support CACHE_THEN_NETWORK. See ListActivity for a sample
+ // with callbacks.
+ pager.loadNextPage(cts.getToken()).continueWith(new Continuation, Task>() {
+ @Override
+ public Task then(Task> task) throws Exception {
+ if (task.isCancelled()) {
+ return null;
+ }
+
+ if (task.isFaulted()) {
+ notifyDataSetChanged();
+ return null;
+ }
+
+ // Remove "Load more..."
+ notifyItemRemoved(oldSize);
+
+ // Insert results
+ List results = task.getResult();
+ if (results.size() > 0) {
+ notifyItemRangeInserted(oldSize, results.size());
+ }
+
+ if (pager.hasNextPage()) {
+ // Add "Load more..."
+ notifyItemInserted(pager.getObjects().size());
+ }
+ return null;
+ }
+ });
+ notifyItemChanged(oldSize);
+ }
+
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+ switch (viewType) {
+ case TYPE_ITEM: {
+ View v = inflater.inflate(android.R.layout.simple_list_item_1, parent, false);
+ return new ItemViewHolder(v);
+ }
+ case TYPE_NEXT: {
+ View v = inflater.inflate(R.layout.load_more_list_item, parent, false);
+ v.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (!getPager().isLoadingNextPage()) {
+ loadNextPage();
+ }
+ }
+ });
+ NextViewHolder vh = new NextViewHolder(v);
+ vh.textView.setText(R.string.load_more);
+ return vh;
+ }
+ default:
+ throw new IllegalStateException("Invalid view type: " + viewType);
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ switch (getItemViewType(position)) {
+ case TYPE_ITEM: {
+ ParseObject item = getPager().getObjects().get(position);
+
+ ItemViewHolder vh = (ItemViewHolder) holder;
+ vh.textView.setText(item.getString("name"));
+ }
+ break;
+ case TYPE_NEXT: {
+ NextViewHolder vh = (NextViewHolder) holder;
+ vh.setLoading(getPager().isLoadingNextPage());
+ }
+ break;
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ ParseQueryPager pager = getPager();
+ return pager.getObjects().size() + (pager.hasNextPage() ? 1 : 0);
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return position < getPager().getObjects().size() ? TYPE_ITEM : TYPE_NEXT;
+ }
+
+ @Override
+ public void registerAdapterDataObserver(RecyclerView.AdapterDataObserver observer) {
+ super.registerAdapterDataObserver(observer);
+ // We use this method as a notification that the RecyclerView is bound to the adapter.
+ loadNextPage();
+ }
+ }
+}
diff --git a/ParseUI-Widget-Sample/src/main/res/layout/activity_list.xml b/ParseUI-Widget-Sample/src/main/res/layout/activity_list.xml
index a570070..55df1d8 100644
--- a/ParseUI-Widget-Sample/src/main/res/layout/activity_list.xml
+++ b/ParseUI-Widget-Sample/src/main/res/layout/activity_list.xml
@@ -1,10 +1,17 @@
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
-
+ android:layout_height="match_parent">
+
+
+
+
\ No newline at end of file
diff --git a/ParseUI-Widget-Sample/src/main/res/layout/activity_main.xml b/ParseUI-Widget-Sample/src/main/res/layout/activity_main.xml
index 87dc282..b8d4586 100644
--- a/ParseUI-Widget-Sample/src/main/res/layout/activity_main.xml
+++ b/ParseUI-Widget-Sample/src/main/res/layout/activity_main.xml
@@ -11,6 +11,12 @@
android:orientation="vertical"
tools:context="com.parse.ui.widget.sample.MainActivity">
+
+