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