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
17package com.android.setupwizardlib.view;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.os.Build;
22import android.support.v7.widget.RecyclerView;
23import android.util.AttributeSet;
24import android.view.LayoutInflater;
25import android.view.View;
26import android.view.ViewGroup;
27import android.view.accessibility.AccessibilityEvent;
28import android.widget.FrameLayout;
29
30import com.android.setupwizardlib.DividerItemDecoration;
31import com.android.setupwizardlib.R;
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 */
38public class HeaderRecyclerView extends RecyclerView {
39
40    private static class HeaderViewHolder extends ViewHolder
41            implements DividerItemDecoration.DividedViewHolder {
42
43        HeaderViewHolder(View itemView) {
44            super(itemView);
45        }
46
47        @Override
48        public boolean isDividerAllowedAbove() {
49            return false;
50        }
51
52        @Override
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     * @param  Type of the content view holder. i.e. view holder type of the wrapped adapter.
62     */
63    public static class HeaderAdapter<CVH extends ViewHolder>
64            extends RecyclerView.Adapter<ViewHolder> {
65
66        private static final int HEADER_VIEW_TYPE = Integer.MAX_VALUE;
67
68        private RecyclerView.Adapter<CVH> mAdapter;
69        private View mHeader;
70
71        private final AdapterDataObserver mObserver = 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 (mHeader != null) {
81                    positionStart++;
82                }
83                notifyItemRangeChanged(positionStart, itemCount);
84            }
85
86            @Override
87            public void onItemRangeInserted(int positionStart, int itemCount) {
88                if (mHeader != null) {
89                    positionStart++;
90                }
91                notifyItemRangeInserted(positionStart, itemCount);
92            }
93
94            @Override
95            public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
96                if (mHeader != 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 (mHeader != null) {
109                    positionStart++;
110                }
111                notifyItemRangeRemoved(positionStart, itemCount);
112            }
113        };
114
115        public HeaderAdapter(RecyclerView.Adapter<CVH> adapter) {
116            mAdapter = adapter;
117            mAdapter.registerAdapterDataObserver(mObserver);
118            setHasStableIds(mAdapter.hasStableIds());
119        }
120
121        @Override
122        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
123            // Returning the same view (mHeader) 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 = new FrameLayout.LayoutParams(
131                        FrameLayout.LayoutParams.MATCH_PARENT,
132                        FrameLayout.LayoutParams.WRAP_CONTENT);
133                frameLayout.setLayoutParams(params);
134                return new HeaderViewHolder(frameLayout);
135            } else {
136                return mAdapter.onCreateViewHolder(parent, viewType);
137            }
138        }
139
140        @Override
141        @SuppressWarnings("unchecked") // Non-header position always return type CVH
142        public void onBindViewHolder(ViewHolder holder, int position) {
143            if (mHeader != null) {
144                position--;
145            }
146
147            if (holder instanceof HeaderViewHolder) {
148                if (mHeader == null) {
149                    throw new IllegalStateException("HeaderViewHolder cannot find mHeader");
150                }
151                if (mHeader.getParent() != null) {
152                    ((ViewGroup) mHeader.getParent()).removeView(mHeader);
153                }
154                FrameLayout mHeaderParent = (FrameLayout) holder.itemView;
155                mHeaderParent.addView(mHeader);
156            } else {
157                mAdapter.onBindViewHolder((CVH) holder, position);
158            }
159        }
160
161        @Override
162        public int getItemViewType(int position) {
163            if (mHeader != null) {
164                position--;
165            }
166            if (position < 0) {
167                return HEADER_VIEW_TYPE;
168            }
169            return mAdapter.getItemViewType(position);
170        }
171
172        @Override
173        public int getItemCount() {
174            int count = mAdapter.getItemCount();
175            if (mHeader != null) {
176                count++;
177            }
178            return count;
179        }
180
181        @Override
182        public long getItemId(int position) {
183            if (mHeader != null) {
184                position--;
185            }
186            if (position < 0) {
187                return Long.MAX_VALUE;
188            }
189            return mAdapter.getItemId(position);
190        }
191
192        public void setHeader(View header) {
193            mHeader = header;
194        }
195
196        public RecyclerView.Adapter<CVH> getWrappedAdapter() {
197            return mAdapter;
198        }
199    }
200
201    private View mHeader;
202    private int mHeaderRes;
203
204    public HeaderRecyclerView(Context context) {
205        super(context);
206        init(null, 0);
207    }
208
209    public HeaderRecyclerView(Context context, AttributeSet attrs) {
210        super(context, attrs);
211        init(attrs, 0);
212    }
213
214    public HeaderRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
215        super(context, attrs, defStyleAttr);
216        init(attrs, defStyleAttr);
217    }
218
219    private void init(AttributeSet attrs, int defStyleAttr) {
220        final TypedArray a = getContext().obtainStyledAttributes(attrs,
221                R.styleable.SuwHeaderRecyclerView, defStyleAttr, 0);
222        mHeaderRes = a.getResourceId(R.styleable.SuwHeaderRecyclerView_suwHeader, 0);
223        a.recycle();
224    }
225
226    @Override
227    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
228        super.onInitializeAccessibilityEvent(event);
229
230        // Decoration-only headers should not count as an item for accessibility, adjust the
231        // accessibility event to account for that.
232        final int numberOfHeaders = mHeader != null ? 1 : 0;
233        event.setItemCount(event.getItemCount() - numberOfHeaders);
234        event.setFromIndex(Math.max(event.getFromIndex() - numberOfHeaders, 0));
235        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
236            event.setToIndex(Math.max(event.getToIndex() - numberOfHeaders, 0));
237        }
238    }
239
240    /**
241     * Gets the header view of this RecyclerView, or {@code null} if there are no headers.
242     */
243    public View getHeader() {
244        return mHeader;
245    }
246
247    /**
248     * Set the view to use as the header of this recycler view.
249     * Note: This must be called before setAdapter.
250     */
251    public void setHeader(View header) {
252        mHeader = header;
253    }
254
255    @Override
256    public void setLayoutManager(LayoutManager layout) {
257        super.setLayoutManager(layout);
258        if (layout != null && mHeader == null && mHeaderRes != 0) {
259            // Inflating a child view requires the layout manager to be set. Check here to see if
260            // any header item is specified in XML and inflate them.
261            final LayoutInflater inflater = LayoutInflater.from(getContext());
262            mHeader = inflater.inflate(mHeaderRes, this, false);
263        }
264    }
265
266    @Override
267    @SuppressWarnings("rawtypes,unchecked") // RecyclerView.setAdapter uses raw type :(
268    public void setAdapter(Adapter adapter) {
269        if (mHeader != null && adapter != null) {
270            final HeaderAdapter headerAdapter = new HeaderAdapter(adapter);
271            headerAdapter.setHeader(mHeader);
272            adapter = headerAdapter;
273        }
274        super.setAdapter(adapter);
275    }
276}
277