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"> +