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.android.setupwizardlib.view;
18 
19 import android.content.Context;
20 import android.content.res.TypedArray;
21 import android.os.Build;
22 import android.support.v7.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 
29 import com.android.setupwizardlib.DividerItemDecoration;
30 import com.android.setupwizardlib.R;
31 import com.android.setupwizardlib.annotations.VisibleForTesting;
32 
33 /**
34  * A RecyclerView that can display a header item at the start of the list. The header can be set by
35  * {@code app:suwHeader} in XML. Note that the header will not be inflated until a layout manager
36  * is set.
37  */
38 public class HeaderRecyclerView extends RecyclerView {
39 
40     private static class HeaderViewHolder extends ViewHolder
41             implements DividerItemDecoration.DividedViewHolder {
42 
HeaderViewHolder(View itemView)43         public HeaderViewHolder(View itemView) {
44             super(itemView);
45         }
46 
47         @Override
isDividerAllowedAbove()48         public boolean isDividerAllowedAbove() {
49             return false;
50         }
51 
52         @Override
isDividerAllowedBelow()53         public boolean isDividerAllowedBelow() {
54             return false;
55         }
56     }
57 
58     /**
59      * An adapter that can optionally add one header item to the RecyclerView.
60      */
61     public static class HeaderAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
62 
63         private static final int HEADER_VIEW_TYPE = Integer.MAX_VALUE;
64 
65         private RecyclerView.Adapter mAdapter;
66         private View mHeader;
67 
68         private final AdapterDataObserver mObserver = new AdapterDataObserver() {
69 
70             @Override
71             public void onChanged() {
72                 notifyDataSetChanged();
73             }
74 
75             @Override
76             public void onItemRangeChanged(int positionStart, int itemCount) {
77                 notifyItemRangeChanged(positionStart, itemCount);
78             }
79 
80             @Override
81             public void onItemRangeInserted(int positionStart, int itemCount) {
82                 notifyItemRangeInserted(positionStart, itemCount);
83             }
84 
85             @Override
86             public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
87                 // Why is there no notifyItemRangeMoved?
88                 notifyDataSetChanged();
89             }
90 
91             @Override
92             public void onItemRangeRemoved(int positionStart, int itemCount) {
93                 notifyItemRangeRemoved(positionStart, itemCount);
94             }
95         };
96 
HeaderAdapter(RecyclerView.Adapter adapter)97         public HeaderAdapter(RecyclerView.Adapter adapter) {
98             mAdapter = adapter;
99             mAdapter.registerAdapterDataObserver(mObserver);
100             setHasStableIds(mAdapter.hasStableIds());
101         }
102 
103         @Override
onCreateViewHolder(ViewGroup parent, int viewType)104         public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
105             if (viewType == HEADER_VIEW_TYPE) {
106                 return new HeaderViewHolder(mHeader);
107             } else {
108                 return mAdapter.onCreateViewHolder(parent, viewType);
109             }
110         }
111 
112         @Override
113         @SuppressWarnings("unchecked")
onBindViewHolder(ViewHolder holder, int position)114         public void onBindViewHolder(ViewHolder holder, int position) {
115             if (mHeader != null) {
116                 position--;
117             }
118             if (position >= 0) {
119                 mAdapter.onBindViewHolder(holder, position);
120             }
121         }
122 
123         @Override
getItemViewType(int position)124         public int getItemViewType(int position) {
125             if (mHeader != null) {
126                 position--;
127             }
128             if (position < 0) {
129                 return HEADER_VIEW_TYPE;
130             }
131             return mAdapter.getItemViewType(position);
132         }
133 
134         @Override
getItemCount()135         public int getItemCount() {
136             int count = mAdapter.getItemCount();
137             if (mHeader != null) {
138                 count++;
139             }
140             return count;
141         }
142 
143         @Override
getItemId(int position)144         public long getItemId(int position) {
145             if (mHeader != null) {
146                 position--;
147             }
148             if (position < 0) {
149                 return Long.MAX_VALUE;
150             }
151             return mAdapter.getItemId(position);
152         }
153 
setHeader(View header)154         public void setHeader(View header) {
155             mHeader = header;
156         }
157 
158         @VisibleForTesting
getWrappedAdapter()159         public RecyclerView.Adapter getWrappedAdapter() {
160             return mAdapter;
161         }
162     }
163 
164     private View mHeader;
165     private int mHeaderRes;
166 
HeaderRecyclerView(Context context)167     public HeaderRecyclerView(Context context) {
168         super(context);
169         init(null, 0);
170     }
171 
HeaderRecyclerView(Context context, AttributeSet attrs)172     public HeaderRecyclerView(Context context, AttributeSet attrs) {
173         super(context, attrs);
174         init(attrs, 0);
175     }
176 
HeaderRecyclerView(Context context, AttributeSet attrs, int defStyleAttr)177     public HeaderRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
178         super(context, attrs, defStyleAttr);
179         init(attrs, defStyleAttr);
180     }
181 
init(AttributeSet attrs, int defStyleAttr)182     private void init(AttributeSet attrs, int defStyleAttr) {
183         final TypedArray a = getContext().obtainStyledAttributes(attrs,
184                 R.styleable.SuwHeaderRecyclerView, defStyleAttr, 0);
185         mHeaderRes = a.getResourceId(R.styleable.SuwHeaderRecyclerView_suwHeader, 0);
186         a.recycle();
187     }
188 
189     @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)190     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
191         super.onInitializeAccessibilityEvent(event);
192 
193         // Decoration-only headers should not count as an item for accessibility, adjust the
194         // accessibility event to account for that.
195         final int numberOfHeaders = mHeader != null ? 1 : 0;
196         event.setItemCount(event.getItemCount() - numberOfHeaders);
197         event.setFromIndex(Math.max(event.getFromIndex() - numberOfHeaders, 0));
198         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
199             event.setToIndex(Math.max(event.getToIndex() - numberOfHeaders, 0));
200         }
201     }
202 
203     /**
204      * Gets the header view of this RecyclerView, or {@code null} if there are no headers.
205      */
getHeader()206     public View getHeader() {
207         return mHeader;
208     }
209 
210     /**
211      * Set the view to use as the header of this recycler view.
212      * Note: This must be called before setAdapter.
213      */
setHeader(View header)214     public void setHeader(View header) {
215         mHeader = header;
216     }
217 
218     @Override
setLayoutManager(LayoutManager layout)219     public void setLayoutManager(LayoutManager layout) {
220         super.setLayoutManager(layout);
221         if (layout != null && mHeader == null && mHeaderRes != 0) {
222             // Inflating a child view requires the layout manager to be set. Check here to see if
223             // any header item is specified in XML and inflate them.
224             final LayoutInflater inflater = LayoutInflater.from(getContext());
225             mHeader = inflater.inflate(mHeaderRes, this, false);
226         }
227     }
228 
229     @Override
setAdapter(Adapter adapter)230     public void setAdapter(Adapter adapter) {
231         if (mHeader != null && adapter != null) {
232             final HeaderAdapter headerAdapter = new HeaderAdapter(adapter);
233             headerAdapter.setHeader(mHeader);
234             adapter = headerAdapter;
235         }
236         super.setAdapter(adapter);
237     }
238 }
239