Index: chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/NewTabPageAdapter.java |
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/NewTabPageAdapter.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/NewTabPageAdapter.java |
index 99dd3baa71222bc6da6e0728de6b2a8038a6b7a0..f512c57635992162082f5ecaa59683bd3f3202fd 100644 |
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/NewTabPageAdapter.java |
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/NewTabPageAdapter.java |
@@ -42,7 +42,7 @@ |
* elements will be the cards shown to the user |
*/ |
public class NewTabPageAdapter extends Adapter<NewTabPageViewHolder> |
- implements SuggestionsSource.Observer { |
+ implements SuggestionsSource.Observer, ItemGroup.Observer { |
private static final String TAG = "Ntp"; |
private final NewTabPageManager mNewTabPageManager; |
@@ -127,19 +127,38 @@ public void onChildDraw(Canvas c, RecyclerView recyclerView, ViewHolder viewHold |
} |
/** |
- * Constructor to create the manager for all the cards to display on the NTP |
+ * Creates the adapter that will manage all the cards to display on the NTP. |
* |
* @param manager the NewTabPageManager to use to interact with the rest of the system. |
* @param aboveTheFoldView the layout encapsulating all the above-the-fold elements |
* (logo, search box, most visited tiles) |
- * @param suggestionsSource the bridge to interact with the content suggestions service. |
* @param uiConfig the NTP UI configuration, to be passed to created views. |
*/ |
- public NewTabPageAdapter(NewTabPageManager manager, View aboveTheFoldView, UiConfig uiConfig) { |
+ public static NewTabPageAdapter create( |
+ NewTabPageManager manager, View aboveTheFoldView, UiConfig uiConfig) { |
+ NewTabPageAdapter adapter = new NewTabPageAdapter(manager, aboveTheFoldView, uiConfig); |
+ adapter.initializeSections(); |
+ return adapter; |
+ } |
+ |
+ /** |
+ * Constructor for {@link NewTabPageAdapter}. The object is not completely ready to be used |
+ * until {@link #initializeSections()} is called. Usage reserved for testing, prefer calling |
+ * {@link NewTabPageAdapter#create(NewTabPageManager, View, UiConfig)} in production code. |
+ */ |
+ @VisibleForTesting |
+ NewTabPageAdapter(NewTabPageManager manager, View aboveTheFoldView, UiConfig uiConfig) { |
mNewTabPageManager = manager; |
mAboveTheFoldView = aboveTheFoldView; |
mUiConfig = uiConfig; |
+ } |
+ /** |
+ * Initialises the sections to be handled by this adapter. Events about categories for which |
+ * a section has not been registered at this point will be ignored. |
+ */ |
+ @VisibleForTesting |
+ void initializeSections() { |
SuggestionsSource suggestionsSource = mNewTabPageManager.getSuggestionsSource(); |
int[] categories = suggestionsSource.getCategories(); |
@@ -203,7 +222,6 @@ public void onNewSuggestions(@CategoryInt int category) { |
if (suggestions.isEmpty()) return; |
setSuggestions(category, suggestions, status); |
- updateGroups(); |
NewTabPageUma.recordSnippetAction(NewTabPageUma.SNIPPETS_ACTION_SHOWN); |
} |
@@ -222,18 +240,16 @@ public void onCategoryStatusChanged(@CategoryInt int category, @CategoryStatusEn |
if (status == CategoryStatus.CATEGORY_EXPLICITLY_DISABLED |
|| status == CategoryStatus.LOADING_ERROR) { |
// Need to remove the entire section from the UI immediately. |
- mSections.remove(category); |
+ removeSection(mSections.get(category)); |
} else { |
mSections.get(category).setStatus(status); |
} |
- updateGroups(); |
} |
@Override |
public void onSuggestionInvalidated(@CategoryInt int category, String idWithinCategory) { |
if (!mSections.containsKey(category)) return; |
mSections.get(category).removeSuggestionById(idWithinCategory); |
- updateGroups(); |
} |
@Override |
@@ -358,10 +374,58 @@ private void updateGroups() { |
mGroups.add(mBottomSpacer); |
} |
- // TODO(bauerb): Notify about a smaller range: https://crbug.com/627512 |
notifyDataSetChanged(); |
} |
+ private void removeSection(SuggestionsSection section) { |
+ mSections.remove(section.getCategory()); |
+ int startPos = getGroupPositionOffset(section); |
+ mGroups.remove(section); |
+ int removedItems = section.getItems().size(); |
+ |
+ if (mSections.isEmpty()) { |
+ if (mGroups.remove(mFooter)) ++removedItems; |
+ if (mGroups.remove(mBottomSpacer)) ++removedItems; |
+ } |
+ |
+ notifyItemRangeRemoved(startPos, removedItems); |
+ notifyItemChanged(getItems().size() - 1); // Refresh the spacer too. |
+ } |
+ |
+ @Override |
+ public void notifyGroupChanged(ItemGroup group, int itemCountBefore, int itemCountAfter) { |
+ int startPos = getGroupPositionOffset(group); |
+ |
+ if (group instanceof SuggestionsSection) { |
+ // The header is stable in sections. Don't notify about it. |
+ ++startPos; |
+ --itemCountBefore; |
+ --itemCountAfter; |
+ } |
+ |
+ if (itemCountBefore < itemCountAfter) { |
+ notifyItemRangeChanged(startPos, itemCountBefore); |
+ notifyItemRangeInserted(startPos + itemCountBefore, itemCountAfter - itemCountBefore); |
+ } else { |
+ notifyItemRangeChanged(startPos, itemCountAfter); |
+ notifyItemRangeRemoved(startPos + itemCountAfter, itemCountBefore - itemCountAfter); |
+ } |
+ |
+ notifyItemChanged(getItems().size() - 1); // Refresh the spacer too. |
+ } |
+ |
+ @Override |
+ public void notifyItemInserted(ItemGroup group, int itemPosition) { |
+ notifyItemInserted(getGroupPositionOffset(group) + itemPosition); |
+ notifyItemChanged(getItems().size() - 1); // Refresh the spacer too. |
+ } |
+ |
+ @Override |
+ public void notifyItemRemoved(ItemGroup group, int itemPosition) { |
+ notifyItemRemoved(getGroupPositionOffset(group) + itemPosition); |
+ notifyItemChanged(getItems().size() - 1); // Refresh the spacer too. |
+ } |
+ |
@Override |
public void onAttachedToRecyclerView(RecyclerView recyclerView) { |
super.onAttachedToRecyclerView(recyclerView); |
@@ -392,9 +456,7 @@ public void dismissItem(int position) { |
private void dismissSection(SuggestionsSection section) { |
mNewTabPageManager.getSuggestionsSource().dismissCategory(section.getCategory()); |
- |
- mSections.remove(section.getCategory()); |
- updateGroups(); |
+ removeSection(section); |
} |
private void dismissSuggestion(int position) { |
@@ -418,22 +480,11 @@ public void onResult(Boolean result) { |
} |
}); |
- mRecyclerView.announceForAccessibility(mRecyclerView.getResources().getString( |
- R.string.ntp_accessibility_item_removed, suggestion.mTitle)); |
+ announceItemRemoved(suggestion.mTitle); |
suggestionsSource.dismissSuggestion(suggestion); |
SuggestionsSection section = (SuggestionsSection) getGroup(position); |
section.removeSuggestion(suggestion); |
- |
- if (section.hasSuggestions()) { |
- // If one of many suggestions was dismissed, it's a simple item removal, which can be |
- // animated smoothly by the RecyclerView. |
- notifyItemRemoved(position); |
- } else { |
- // If the last suggestion was dismissed, multiple items will have changed, so mark |
- // everything as changed. |
- notifyDataSetChanged(); |
- } |
} |
/** |
@@ -477,4 +528,10 @@ int getGroupPositionOffset(ItemGroup group) { |
SnippetArticle getSuggestionAt(int position) { |
return (SnippetArticle) getItems().get(position); |
} |
+ |
+ @VisibleForTesting |
+ void announceItemRemoved(String suggestionTitle) { |
+ mRecyclerView.announceForAccessibility(mRecyclerView.getResources().getString( |
+ R.string.ntp_accessibility_item_removed, suggestionTitle)); |
+ } |
} |