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