1 package android.support.v17.leanback.app; 2 3 import android.support.v17.leanback.widget.ObjectAdapter; 4 import android.support.v17.leanback.widget.Row; 5 6 /** 7 * Wrapper class for {@link ObjectAdapter} used by {@link BrowseFragment} to initialize 8 * {@link RowsFragment}. We use invisible rows to represent 9 * {@link android.support.v17.leanback.widget.DividerRow}, 10 * {@link android.support.v17.leanback.widget.SectionRow} and 11 * {@link android.support.v17.leanback.widget.PageRow} in RowsFragment. In case we have an 12 * invisible row at the end of a RowsFragment, it creates a jumping effect as the layout manager 13 * thinks there are items even though they're invisible. This class takes care of filtering out 14 * the invisible rows at the end. In case the data inside the adapter changes, it adjusts the 15 * bounds to reflect the latest data. 16 */ 17 class ListRowDataAdapter extends ObjectAdapter { 18 public static final int ON_ITEM_RANGE_CHANGED = 2; 19 public static final int ON_ITEM_RANGE_INSERTED = 4; 20 public static final int ON_ITEM_RANGE_REMOVED = 8; 21 public static final int ON_CHANGED = 16; 22 23 private final ObjectAdapter mAdapter; 24 int mLastVisibleRowIndex; 25 ListRowDataAdapter(ObjectAdapter adapter)26 public ListRowDataAdapter(ObjectAdapter adapter) { 27 super(adapter.getPresenterSelector()); 28 this.mAdapter = adapter; 29 initialize(); 30 31 // If an user implements its own ObjectAdapter, notification corresponding to data 32 // updates can be batched e.g. remove, add might be followed by notifyRemove, notifyAdd. 33 // But underlying data would have changed during the notifyRemove call by the previous add 34 // operation. To handle this case, we use QueueBasedDataObserver which forces 35 // recyclerview to do a full data refresh after each update operation. 36 if (adapter.isImmediateNotifySupported()) { 37 mAdapter.registerObserver(new SimpleDataObserver()); 38 } else { 39 mAdapter.registerObserver(new QueueBasedDataObserver()); 40 } 41 } 42 initialize()43 void initialize() { 44 mLastVisibleRowIndex = -1; 45 int i = mAdapter.size() - 1; 46 while (i >= 0) { 47 Row item = (Row) mAdapter.get(i); 48 if (item.isRenderedAsRowView()) { 49 mLastVisibleRowIndex = i; 50 break; 51 } 52 i--; 53 } 54 } 55 56 @Override size()57 public int size() { 58 return mLastVisibleRowIndex + 1; 59 } 60 61 @Override get(int index)62 public Object get(int index) { 63 return mAdapter.get(index); 64 } 65 doNotify(int eventType, int positionStart, int itemCount)66 void doNotify(int eventType, int positionStart, int itemCount) { 67 switch (eventType) { 68 case ON_ITEM_RANGE_CHANGED: 69 notifyItemRangeChanged(positionStart, itemCount); 70 break; 71 case ON_ITEM_RANGE_INSERTED: 72 notifyItemRangeInserted(positionStart, itemCount); 73 break; 74 case ON_ITEM_RANGE_REMOVED: 75 notifyItemRangeRemoved(positionStart, itemCount); 76 break; 77 case ON_CHANGED: 78 notifyChanged(); 79 break; 80 default: 81 throw new IllegalArgumentException("Invalid event type " + eventType); 82 } 83 } 84 85 private class SimpleDataObserver extends DataObserver { 86 SimpleDataObserver()87 SimpleDataObserver() { 88 } 89 90 @Override onItemRangeChanged(int positionStart, int itemCount)91 public void onItemRangeChanged(int positionStart, int itemCount) { 92 if (positionStart <= mLastVisibleRowIndex) { 93 onEventFired(ON_ITEM_RANGE_CHANGED, positionStart, 94 Math.min(itemCount, mLastVisibleRowIndex - positionStart + 1)); 95 } 96 } 97 98 @Override onItemRangeInserted(int positionStart, int itemCount)99 public void onItemRangeInserted(int positionStart, int itemCount) { 100 if (positionStart <= mLastVisibleRowIndex) { 101 mLastVisibleRowIndex += itemCount; 102 onEventFired(ON_ITEM_RANGE_INSERTED, positionStart, itemCount); 103 return; 104 } 105 106 int lastVisibleRowIndex = mLastVisibleRowIndex; 107 initialize(); 108 if (mLastVisibleRowIndex > lastVisibleRowIndex) { 109 int totalItems = mLastVisibleRowIndex - lastVisibleRowIndex; 110 onEventFired(ON_ITEM_RANGE_INSERTED, lastVisibleRowIndex + 1, totalItems); 111 } 112 } 113 114 @Override onItemRangeRemoved(int positionStart, int itemCount)115 public void onItemRangeRemoved(int positionStart, int itemCount) { 116 if (positionStart + itemCount - 1 < mLastVisibleRowIndex) { 117 mLastVisibleRowIndex -= itemCount; 118 onEventFired(ON_ITEM_RANGE_REMOVED, positionStart, itemCount); 119 return; 120 } 121 122 int lastVisibleRowIndex = mLastVisibleRowIndex; 123 initialize(); 124 int totalItems = lastVisibleRowIndex - mLastVisibleRowIndex; 125 if (totalItems > 0) { 126 onEventFired(ON_ITEM_RANGE_REMOVED, 127 Math.min(mLastVisibleRowIndex + 1, positionStart), 128 totalItems); 129 } 130 } 131 132 @Override onChanged()133 public void onChanged() { 134 initialize(); 135 onEventFired(ON_CHANGED, -1, -1); 136 } 137 onEventFired(int eventType, int positionStart, int itemCount)138 protected void onEventFired(int eventType, int positionStart, int itemCount) { 139 doNotify(eventType, positionStart, itemCount); 140 } 141 } 142 143 144 /** 145 * When using custom {@link ObjectAdapter}, it's possible that the user may make multiple 146 * changes to the underlying data at once. The notifications about those updates may be 147 * batched and the underlying data would have changed to reflect latest updates as opposed 148 * to intermediate changes. In order to force RecyclerView to refresh the view with access 149 * only to the final data, we call notifyChange(). 150 */ 151 private class QueueBasedDataObserver extends DataObserver { 152 QueueBasedDataObserver()153 QueueBasedDataObserver() { 154 } 155 156 @Override onChanged()157 public void onChanged() { 158 initialize(); 159 notifyChanged(); 160 } 161 } 162 } 163