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;
28
29import com.android.setupwizardlib.DividerItemDecoration;
30import com.android.setupwizardlib.R;
31import 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 */
38public class HeaderRecyclerView extends RecyclerView {
39
40    private static class HeaderViewHolder extends ViewHolder
41            implements DividerItemDecoration.DividedViewHolder {
42
43        public 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    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
97        public HeaderAdapter(RecyclerView.Adapter adapter) {
98            mAdapter = adapter;
99            mAdapter.registerAdapterDataObserver(mObserver);
100            setHasStableIds(mAdapter.hasStableIds());
101        }
102
103        @Override
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")
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
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
135        public int getItemCount() {
136            int count = mAdapter.getItemCount();
137            if (mHeader != null) {
138                count++;
139            }
140            return count;
141        }
142
143        @Override
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
154        public void setHeader(View header) {
155            mHeader = header;
156        }
157
158        @VisibleForTesting
159        public RecyclerView.Adapter getWrappedAdapter() {
160            return mAdapter;
161        }
162    }
163
164    private View mHeader;
165    private int mHeaderRes;
166
167    public HeaderRecyclerView(Context context) {
168        super(context);
169        init(null, 0);
170    }
171
172    public HeaderRecyclerView(Context context, AttributeSet attrs) {
173        super(context, attrs);
174        init(attrs, 0);
175    }
176
177    public HeaderRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
178        super(context, attrs, defStyleAttr);
179        init(attrs, defStyleAttr);
180    }
181
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
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     */
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     */
214    public void setHeader(View header) {
215        mHeader = header;
216    }
217
218    @Override
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
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