Index: chrome/android/java/src/org/chromium/chrome/browser/widget/DateDividedAdapter.java |
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/widget/DateDividedAdapter.java b/chrome/android/java/src/org/chromium/chrome/browser/widget/DateDividedAdapter.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..3974d6bcfad1c2eadbe6224cd7e484af82bea10d |
--- /dev/null |
+++ b/chrome/android/java/src/org/chromium/chrome/browser/widget/DateDividedAdapter.java |
@@ -0,0 +1,307 @@ |
+// Copyright 2016 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+package org.chromium.chrome.browser.widget; |
+ |
+import android.os.AsyncTask; |
+import android.support.v7.widget.RecyclerView; |
+import android.support.v7.widget.RecyclerView.Adapter; |
+import android.support.v7.widget.RecyclerView.ViewHolder; |
+import android.text.format.DateUtils; |
+import android.util.Pair; |
+import android.view.LayoutInflater; |
+import android.view.View; |
+import android.view.ViewGroup; |
+import android.widget.TextView; |
+ |
+import org.chromium.chrome.R; |
+ |
+import java.util.ArrayList; |
+import java.util.Calendar; |
+import java.util.Comparator; |
+import java.util.Date; |
+import java.util.List; |
+import java.util.SortedSet; |
+import java.util.TreeSet; |
+import java.util.concurrent.ExecutionException; |
+ |
+/** |
+ * An {@link Adapter} that works with a {@link RecyclerView}. It sorts the given {@link List} of |
+ * {@link TimedItem}s according to their date, and divides them into sub lists and displays them in |
+ * different sections. |
+ * <p> |
+ * Subclasses should not care about the how date headers are placed in the list. Instead, they |
+ * should call {@link #loadItems(List)} with a list of {@link TimedItem}, and this adapter will |
+ * insert the headers automatically. |
+ */ |
+public abstract class DateDividedAdapter extends Adapter<RecyclerView.ViewHolder> { |
+ |
+ /** |
+ * Interface that the {@link Adapter} uses to interact with the items it manages. |
+ */ |
+ public interface TimedItem { |
+ /** |
+ * @return The timestamp of this item. |
+ */ |
+ long getStartTime(); |
+ } |
+ |
+ private static class DateViewHolder extends RecyclerView.ViewHolder { |
+ private TextView mTextView; |
+ |
+ public DateViewHolder(View view) { |
+ super(view); |
+ if (view instanceof TextView) mTextView = (TextView) view; |
+ } |
+ |
+ public void setDate(Date date) { |
+ // Calender.getInstance() may take long time to run, so Calendar object should be reused |
+ // as much as possible. |
+ Pair<Calendar, Calendar> pair = getCachedCalendars(); |
+ Calendar cal1 = pair.first, cal2 = pair.second; |
+ cal1.setTimeInMillis(System.currentTimeMillis()); |
+ cal2.setTime(date); |
+ |
+ StringBuilder builder = new StringBuilder(); |
+ if (compareCalendar(cal1, cal2) == 0) { |
+ builder.append(mTextView.getContext().getString(R.string.today)); |
+ builder.append(" - "); |
+ } else { |
+ // Set cal1 to yesterday. |
+ cal1.add(Calendar.DATE, -1); |
+ if (compareCalendar(cal1, cal2) == 0) { |
+ builder.append(mTextView.getContext().getString(R.string.yesterday)); |
+ builder.append(" - "); |
+ } |
+ } |
+ builder.append(DateUtils.formatDateTime(mTextView.getContext(), date.getTime(), |
+ DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_DATE |
+ | DateUtils.FORMAT_SHOW_YEAR)); |
+ mTextView.setText(builder); |
+ } |
+ } |
+ |
+ /** |
+ * A bucket of items with the same date. |
+ */ |
+ private static class ItemGroup { |
+ private Date mDate; |
+ private List<TimedItem> mItems; |
+ |
+ public ItemGroup(TimedItem item) { |
+ mItems = new ArrayList<>(); |
+ mItems.add(item); |
+ mDate = new Date(item.getStartTime()); |
+ } |
+ |
+ public void addItem(TimedItem item) { |
+ mItems.add(item); |
+ } |
+ |
+ /** |
+ * @return Whether the given date happens in the same day as the items in this group. |
+ */ |
+ public boolean isSameDay(Date otherDate) { |
+ return compareDate(mDate, otherDate) == 0; |
+ } |
+ |
+ /** |
+ * @return The size of this group. |
+ */ |
+ public int size() { |
+ // Plus 1 to account for the date header. |
+ return mItems.size() + 1; |
+ } |
+ |
+ public TimedItem getItemAt(int index) { |
+ // 0 is allocated to the date header. |
+ if (index == 0) return null; |
+ return mItems.get(index - 1); |
+ } |
+ } |
+ |
+ // Cached async tasks to get the two Calendar objects, which are used when comparing dates. |
+ private static final AsyncTask<Void, Void, Calendar> sCal1 = createCalendar(); |
+ private static final AsyncTask<Void, Void, Calendar> sCal2 = createCalendar(); |
+ |
+ public static final int TYPE_DATE = 0; |
+ public static final int TYPE_NORMAL = 1; |
+ |
+ private int mSize; |
+ private SortedSet<ItemGroup> mItems = new TreeSet<>(new Comparator<ItemGroup>() { |
+ @Override |
+ public int compare(ItemGroup lhs, ItemGroup rhs) { |
+ return compareDate(lhs.mDate, rhs.mDate); |
+ } |
+ }); |
+ |
+ /** |
+ * Creates a {@link ViewHolder} in the given view parent. |
+ * @see #onCreateViewHolder(ViewGroup, int) |
+ */ |
+ protected abstract ViewHolder createViewHolder(ViewGroup parent); |
+ |
+ /** |
+ * Binds the {@link ViewHolder} with the given {@link TimedItem}. |
+ * @see #onBindViewHolder(ViewHolder, int) |
+ */ |
+ protected abstract void bindViewHolderForTimedItem(ViewHolder viewHolder, TimedItem item); |
+ |
+ /** |
+ * Gets the resource id of the view showing the date header. |
+ * Contract for subclasses: this view should be a {@link TextView}. |
+ */ |
+ protected abstract int getTimedItemViewResId(); |
+ |
+ /** |
+ * Loads a list of {@link TimedItem}s to this adapter. Any previous data will be removed. |
+ */ |
+ public void loadItems(List<? extends TimedItem> timedItems) { |
+ mSize = 0; |
+ mItems.clear(); |
+ for (TimedItem timedItem : timedItems) { |
+ Date date = new Date(timedItem.getStartTime()); |
+ boolean found = false; |
+ for (ItemGroup item : mItems) { |
+ if (item.isSameDay(date)) { |
+ found = true; |
+ item.addItem(timedItem); |
+ mSize++; |
+ break; |
+ } |
+ } |
+ if (!found) { |
+ // Create a new ItemGroup with the date for the new item. This increases the |
+ // size by two because we add new views for the date and the item itself. |
+ mItems.add(new ItemGroup(timedItem)); |
+ mSize += 2; |
+ } |
+ } |
+ notifyDataSetChanged(); |
+ } |
+ |
+ /** |
+ * Removes all items from this adapter. |
+ */ |
+ public void clear() { |
+ mSize = 0; |
+ mItems.clear(); |
+ notifyDataSetChanged(); |
+ } |
+ |
+ /** |
+ * Gets the item at the given position. For date headers, {@link TimedItem} will be null; for |
+ * normal items, {@link Date} will be null. |
+ */ |
+ private Pair<Date, TimedItem> getItemAt(int position) { |
+ Pair<ItemGroup, Integer> pair = getGroupAt(position); |
+ ItemGroup group = pair.first; |
+ return new Pair<>(group.mDate, group.getItemAt(pair.second)); |
+ } |
+ |
+ @Override |
+ public final int getItemViewType(int position) { |
+ Pair<ItemGroup, Integer> pair = getGroupAt(position); |
+ return pair.second == 0 ? TYPE_DATE : TYPE_NORMAL; |
+ } |
+ |
+ @Override |
+ public final RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { |
+ if (viewType == TYPE_DATE) { |
+ return new DateViewHolder(LayoutInflater.from(parent.getContext()).inflate( |
+ getTimedItemViewResId(), parent, false)); |
+ } else if (viewType == TYPE_NORMAL) { |
+ return createViewHolder(parent); |
+ } |
+ return null; |
+ } |
+ |
+ @Override |
+ public final void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { |
+ Pair<Date, TimedItem> pair = getItemAt(position); |
+ if (holder instanceof DateViewHolder) { |
+ ((DateViewHolder) holder).setDate(pair.first); |
+ } else { |
+ bindViewHolderForTimedItem(holder, pair.second); |
+ } |
+ } |
+ |
+ @Override |
+ public final int getItemCount() { |
+ return mSize; |
+ } |
+ |
+ /** |
+ * Utility method to traverse all groups and find the {@link ItemGroup} for the given position. |
+ */ |
+ private Pair<ItemGroup, Integer> getGroupAt(int position) { |
+ // TODO(ianwen): Optimize the performance if the number of groups becomes too large. |
+ int i = position; |
+ for (ItemGroup group : mItems) { |
+ if (i >= group.size()) { |
+ i -= group.size(); |
+ } else { |
+ return new Pair<>(group, i); |
+ } |
+ } |
+ assert false; |
+ return null; |
+ } |
+ |
+ /** |
+ * Compares two {@link Date}s. Note if you already have two {@link Calendar} objects, use |
+ * {@link #compareCalendar(Calendar, Calendar)} instead. |
+ * @return 0 if date1 and date2 are in the same day; 1 if date1 is before date2; -1 otherwise. |
+ */ |
+ private static int compareDate(Date date1, Date date2) { |
+ Pair<Calendar, Calendar> pair = getCachedCalendars(); |
+ Calendar cal1 = pair.first, cal2 = pair.second; |
+ cal1.setTime(date1); |
+ cal2.setTime(date2); |
+ return compareCalendar(cal1, cal2); |
+ } |
+ |
+ /** |
+ * @return 0 if cal1 and cal2 are in the same day; 1 if cal1 happens before cal2; -1 otherwise. |
+ */ |
+ private static int compareCalendar(Calendar cal1, Calendar cal2) { |
+ boolean sameDay = cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) |
+ && cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR); |
+ if (sameDay) { |
+ return 0; |
+ } else if (cal1.before(cal2)) { |
+ return 1; |
+ } else { |
+ return -1; |
+ } |
+ } |
+ |
+ /** |
+ * Convenient getter for {@link #sCal1} and {@link #sCal2}. |
+ */ |
+ private static Pair<Calendar, Calendar> getCachedCalendars() { |
+ Calendar cal1, cal2; |
+ try { |
+ cal1 = sCal1.get(); |
+ cal2 = sCal2.get(); |
+ } catch (InterruptedException | ExecutionException e) { |
+ // We've tried our best. If AsyncTask really does not work, we give up. :( |
+ cal1 = Calendar.getInstance(); |
+ cal2 = Calendar.getInstance(); |
+ } |
+ return new Pair<>(cal1, cal2); |
+ } |
+ |
+ /** |
+ * Wraps {@link Calendar#getInstance()} in an {@link AsyncTask} to avoid Strict mode violation. |
+ */ |
+ private static AsyncTask<Void, Void, Calendar> createCalendar() { |
+ return new AsyncTask<Void, Void, Calendar>() { |
+ @Override |
+ protected Calendar doInBackground(Void... unused) { |
+ return Calendar.getInstance(); |
+ } |
+ }.execute(); |
+ } |
+} |