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