Home | History | Annotate | Download | only in view
      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      * @param  Type 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