• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.android.setupdesign.view;
18 
19 import android.content.Context;
20 import android.content.res.TypedArray;
21 import android.os.Build;
22 import androidx.recyclerview.widget.RecyclerView;
23 import android.util.AttributeSet;
24 import android.view.LayoutInflater;
25 import android.view.View;
26 import android.view.ViewGroup;
27 import android.view.accessibility.AccessibilityEvent;
28 import android.widget.FrameLayout;
29 import com.google.android.setupdesign.DividerItemDecoration;
30 import com.google.android.setupdesign.R;
31 
32 /**
33  * A RecyclerView that can display a header item at the start of the list. The header can be set by
34  * {@code app:sudHeader} in XML. Note that the header will not be inflated until a layout manager is
35  * set.
36  */
37 public class HeaderRecyclerView extends RecyclerView {
38 
39   private static class HeaderViewHolder extends ViewHolder
40       implements DividerItemDecoration.DividedViewHolder {
41 
HeaderViewHolder(View itemView)42     HeaderViewHolder(View itemView) {
43       super(itemView);
44     }
45 
46     @Override
isDividerAllowedAbove()47     public boolean isDividerAllowedAbove() {
48       return false;
49     }
50 
51     @Override
isDividerAllowedBelow()52     public boolean isDividerAllowedBelow() {
53       return false;
54     }
55   }
56 
57   /**
58    * An adapter that can optionally add one header item to the RecyclerView.
59    *
60    * @param <CVH> Type of the content view holder. i.e. view holder type of the wrapped adapter.
61    */
62   public static class HeaderAdapter<CVH extends ViewHolder>
63       extends RecyclerView.Adapter<ViewHolder> {
64 
65     private static final int HEADER_VIEW_TYPE = Integer.MAX_VALUE;
66 
67     private final RecyclerView.Adapter<CVH> adapter;
68     private View header;
69 
70     private final AdapterDataObserver observer =
71         new AdapterDataObserver() {
72 
73           @Override
74           public void onChanged() {
75             notifyDataSetChanged();
76           }
77 
78           @Override
79           public void onItemRangeChanged(int positionStart, int itemCount) {
80             if (header != null) {
81               positionStart++;
82             }
83             notifyItemRangeChanged(positionStart, itemCount);
84           }
85 
86           @Override
87           public void onItemRangeInserted(int positionStart, int itemCount) {
88             if (header != null) {
89               positionStart++;
90             }
91             notifyItemRangeInserted(positionStart, itemCount);
92           }
93 
94           @Override
95           public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
96             if (header != null) {
97               fromPosition++;
98               toPosition++;
99             }
100             // Why is there no notifyItemRangeMoved?
101             for (int i = 0; i < itemCount; i++) {
102               notifyItemMoved(fromPosition + i, toPosition + i);
103             }
104           }
105 
106           @Override
107           public void onItemRangeRemoved(int positionStart, int itemCount) {
108             if (header != null) {
109               positionStart++;
110             }
111             notifyItemRangeRemoved(positionStart, itemCount);
112           }
113         };
114 
HeaderAdapter(RecyclerView.Adapter<CVH> adapter)115     public HeaderAdapter(RecyclerView.Adapter<CVH> adapter) {
116       this.adapter = adapter;
117       this.adapter.registerAdapterDataObserver(observer);
118       setHasStableIds(this.adapter.hasStableIds());
119     }
120 
121     @Override
onCreateViewHolder(ViewGroup parent, int viewType)122     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
123       // Returning the same view (header) results in crash ".. but view is not a real child."
124       // The framework creates more than one instance of header because of "disappear"
125       // animations applied on the header and this necessitates creation of another header
126       // view to use after the animation. We work around this restriction by returning an
127       // empty FrameLayout to which the header is attached using #onBindViewHolder method.
128       if (viewType == HEADER_VIEW_TYPE) {
129         FrameLayout frameLayout = new FrameLayout(parent.getContext());
130         FrameLayout.LayoutParams params =
131             new FrameLayout.LayoutParams(
132                 FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT);
133         frameLayout.setLayoutParams(params);
134         return new HeaderViewHolder(frameLayout);
135       } else {
136         return adapter.onCreateViewHolder(parent, viewType);
137       }
138     }
139 
140     @Override
141     @SuppressWarnings("unchecked") // Non-header position always return type CVH
onBindViewHolder(ViewHolder holder, int position)142     public void onBindViewHolder(ViewHolder holder, int position) {
143       if (header != null) {
144         position--;
145       }
146 
147       if (holder instanceof HeaderViewHolder) {
148         if (header == null) {
149           throw new IllegalStateException("HeaderViewHolder cannot find mHeader");
150         }
151         if (header.getParent() != null) {
152           ((ViewGroup) header.getParent()).removeView(header);
153         }
154         FrameLayout mHeaderParent = (FrameLayout) holder.itemView;
155         mHeaderParent.addView(header);
156       } else {
157         adapter.onBindViewHolder((CVH) holder, position);
158       }
159     }
160 
161     @Override
getItemViewType(int position)162     public int getItemViewType(int position) {
163       if (header != null) {
164         position--;
165       }
166       if (position < 0) {
167         return HEADER_VIEW_TYPE;
168       }
169       return adapter.getItemViewType(position);
170     }
171 
172     @Override
getItemCount()173     public int getItemCount() {
174       int count = adapter.getItemCount();
175       if (header != null) {
176         count++;
177       }
178       return count;
179     }
180 
181     @Override
getItemId(int position)182     public long getItemId(int position) {
183       if (header != null) {
184         position--;
185       }
186       if (position < 0) {
187         return Long.MAX_VALUE;
188       }
189       return adapter.getItemId(position);
190     }
191 
setHeader(View header)192     public void setHeader(View header) {
193       this.header = header;
194     }
195 
getWrappedAdapter()196     public RecyclerView.Adapter<CVH> getWrappedAdapter() {
197       return adapter;
198     }
199   }
200 
201   private View header;
202   private int headerRes;
203 
HeaderRecyclerView(Context context)204   public HeaderRecyclerView(Context context) {
205     super(context);
206     init(null, 0);
207   }
208 
HeaderRecyclerView(Context context, AttributeSet attrs)209   public HeaderRecyclerView(Context context, AttributeSet attrs) {
210     super(context, attrs);
211     init(attrs, 0);
212   }
213 
HeaderRecyclerView(Context context, AttributeSet attrs, int defStyleAttr)214   public HeaderRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
215     super(context, attrs, defStyleAttr);
216     init(attrs, defStyleAttr);
217   }
218 
init(AttributeSet attrs, int defStyleAttr)219   private void init(AttributeSet attrs, int defStyleAttr) {
220     final TypedArray a =
221         getContext()
222             .obtainStyledAttributes(attrs, R.styleable.SudHeaderRecyclerView, defStyleAttr, 0);
223     headerRes = a.getResourceId(R.styleable.SudHeaderRecyclerView_sudHeader, 0);
224     a.recycle();
225   }
226 
227   @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)228   public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
229     super.onInitializeAccessibilityEvent(event);
230 
231     // Decoration-only headers should not count as an item for accessibility, adjust the
232     // accessibility event to account for that.
233     final int numberOfHeaders = header != null ? 1 : 0;
234     event.setItemCount(event.getItemCount() - numberOfHeaders);
235     event.setFromIndex(Math.max(event.getFromIndex() - numberOfHeaders, 0));
236     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
237       event.setToIndex(Math.max(event.getToIndex() - numberOfHeaders, 0));
238     }
239   }
240 
241   /** Gets the header view of this RecyclerView, or {@code null} if there are no headers. */
getHeader()242   public View getHeader() {
243     return header;
244   }
245 
246   /**
247    * Set the view to use as the header of this recycler view. Note: This must be called before
248    * setAdapter.
249    */
setHeader(View header)250   public void setHeader(View header) {
251     this.header = header;
252   }
253 
254   @Override
setLayoutManager(LayoutManager layout)255   public void setLayoutManager(LayoutManager layout) {
256     super.setLayoutManager(layout);
257     if (layout != null && header == null && headerRes != 0) {
258       // Inflating a child view requires the layout manager to be set. Check here to see if
259       // any header item is specified in XML and inflate them.
260       final LayoutInflater inflater = LayoutInflater.from(getContext());
261       header = inflater.inflate(headerRes, this, false);
262     }
263   }
264 
265   @Override
266   @SuppressWarnings("rawtypes,unchecked") // RecyclerView.setAdapter uses raw type :(
setAdapter(Adapter adapter)267   public void setAdapter(Adapter adapter) {
268     if (header != null && adapter != null) {
269       final HeaderAdapter headerAdapter = new HeaderAdapter(adapter);
270       headerAdapter.setHeader(header);
271       adapter = headerAdapter;
272     }
273     super.setAdapter(adapter);
274   }
275 }
276