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