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