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