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 import android.widget.FrameLayout; 29 30 import com.android.setupwizardlib.DividerItemDecoration; 31 import 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 */ 38 public 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 * @paramType 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