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