Home | History | Annotate | Download | only in views
      1 /*
      2  * Copyright (C) 2013 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.photos.views;
     18 
     19 import android.content.Context;
     20 import android.database.DataSetObservable;
     21 import android.database.DataSetObserver;
     22 import android.util.AttributeSet;
     23 import android.view.View;
     24 import android.view.ViewGroup;
     25 import android.widget.AdapterView;
     26 import android.widget.Filter;
     27 import android.widget.Filterable;
     28 import android.widget.FrameLayout;
     29 import android.widget.GridView;
     30 import android.widget.ListAdapter;
     31 import android.widget.WrapperListAdapter;
     32 
     33 import java.util.ArrayList;
     34 
     35 /**
     36  * A {@link GridView} that supports adding header rows in a
     37  * very similar way to {@link ListView}.
     38  * See {@link HeaderGridView#addHeaderView(View, Object, boolean)}
     39  */
     40 public class HeaderGridView extends GridView {
     41     private static final String TAG = "HeaderGridView";
     42 
     43     /**
     44      * A class that represents a fixed view in a list, for example a header at the top
     45      * or a footer at the bottom.
     46      */
     47     private static class FixedViewInfo {
     48         /** The view to add to the grid */
     49         public View view;
     50         public ViewGroup viewContainer;
     51         /** The data backing the view. This is returned from {@link ListAdapter#getItem(int)}. */
     52         public Object data;
     53         /** <code>true</code> if the fixed view should be selectable in the grid */
     54         public boolean isSelectable;
     55     }
     56 
     57     private ArrayList<FixedViewInfo> mHeaderViewInfos = new ArrayList<FixedViewInfo>();
     58 
     59     private void initHeaderGridView() {
     60         super.setClipChildren(false);
     61     }
     62 
     63     public HeaderGridView(Context context) {
     64         super(context);
     65         initHeaderGridView();
     66     }
     67 
     68     public HeaderGridView(Context context, AttributeSet attrs) {
     69         super(context, attrs);
     70         initHeaderGridView();
     71     }
     72 
     73     public HeaderGridView(Context context, AttributeSet attrs, int defStyle) {
     74         super(context, attrs, defStyle);
     75         initHeaderGridView();
     76     }
     77 
     78     @Override
     79     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     80         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
     81         ListAdapter adapter = getAdapter();
     82         if (adapter != null && adapter instanceof HeaderViewGridAdapter) {
     83             ((HeaderViewGridAdapter) adapter).setNumColumns(getNumColumns());
     84         }
     85     }
     86 
     87     @Override
     88     public void setClipChildren(boolean clipChildren) {
     89        // Ignore, since the header rows depend on not being clipped
     90     }
     91 
     92     /**
     93      * Add a fixed view to appear at the top of the grid. If addHeaderView is
     94      * called more than once, the views will appear in the order they were
     95      * added. Views added using this call can take focus if they want.
     96      * <p>
     97      * NOTE: Call this before calling setAdapter. This is so HeaderGridView can wrap
     98      * the supplied cursor with one that will also account for header views.
     99      *
    100      * @param v The view to add.
    101      * @param data Data to associate with this view
    102      * @param isSelectable whether the item is selectable
    103      */
    104     public void addHeaderView(View v, Object data, boolean isSelectable) {
    105         ListAdapter adapter = getAdapter();
    106 
    107         if (adapter != null && ! (adapter instanceof HeaderViewGridAdapter)) {
    108             throw new IllegalStateException(
    109                     "Cannot add header view to grid -- setAdapter has already been called.");
    110         }
    111 
    112         FixedViewInfo info = new FixedViewInfo();
    113         FrameLayout fl = new FullWidthFixedViewLayout(getContext());
    114         fl.addView(v);
    115         info.view = v;
    116         info.viewContainer = fl;
    117         info.data = data;
    118         info.isSelectable = isSelectable;
    119         mHeaderViewInfos.add(info);
    120 
    121         // in the case of re-adding a header view, or adding one later on,
    122         // we need to notify the observer
    123         if (adapter != null) {
    124             ((HeaderViewGridAdapter) adapter).notifyDataSetChanged();
    125         }
    126     }
    127 
    128     /**
    129      * Add a fixed view to appear at the top of the grid. If addHeaderView is
    130      * called more than once, the views will appear in the order they were
    131      * added. Views added using this call can take focus if they want.
    132      * <p>
    133      * NOTE: Call this before calling setAdapter. This is so HeaderGridView can wrap
    134      * the supplied cursor with one that will also account for header views.
    135      *
    136      * @param v The view to add.
    137      */
    138     public void addHeaderView(View v) {
    139         addHeaderView(v, null, true);
    140     }
    141 
    142     public int getHeaderViewCount() {
    143         return mHeaderViewInfos.size();
    144     }
    145 
    146     /**
    147      * Removes a previously-added header view.
    148      *
    149      * @param v The view to remove
    150      * @return true if the view was removed, false if the view was not a header
    151      *         view
    152      */
    153     public boolean removeHeaderView(View v) {
    154         if (mHeaderViewInfos.size() > 0) {
    155             boolean result = false;
    156             ListAdapter adapter = getAdapter();
    157             if (adapter != null && ((HeaderViewGridAdapter) adapter).removeHeader(v)) {
    158                 result = true;
    159             }
    160             removeFixedViewInfo(v, mHeaderViewInfos);
    161             return result;
    162         }
    163         return false;
    164     }
    165 
    166     private void removeFixedViewInfo(View v, ArrayList<FixedViewInfo> where) {
    167         int len = where.size();
    168         for (int i = 0; i < len; ++i) {
    169             FixedViewInfo info = where.get(i);
    170             if (info.view == v) {
    171                 where.remove(i);
    172                 break;
    173             }
    174         }
    175     }
    176 
    177     @Override
    178     public void setAdapter(ListAdapter adapter) {
    179         if (mHeaderViewInfos.size() > 0) {
    180             HeaderViewGridAdapter hadapter = new HeaderViewGridAdapter(mHeaderViewInfos, adapter);
    181             int numColumns = getNumColumns();
    182             if (numColumns > 1) {
    183                 hadapter.setNumColumns(numColumns);
    184             }
    185             super.setAdapter(hadapter);
    186         } else {
    187             super.setAdapter(adapter);
    188         }
    189     }
    190 
    191     private class FullWidthFixedViewLayout extends FrameLayout {
    192         public FullWidthFixedViewLayout(Context context) {
    193             super(context);
    194         }
    195 
    196         @Override
    197         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    198             int targetWidth = HeaderGridView.this.getMeasuredWidth()
    199                     - HeaderGridView.this.getPaddingLeft()
    200                     - HeaderGridView.this.getPaddingRight();
    201             widthMeasureSpec = MeasureSpec.makeMeasureSpec(targetWidth,
    202                     MeasureSpec.getMode(widthMeasureSpec));
    203             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    204         }
    205     }
    206 
    207     /**
    208      * ListAdapter used when a HeaderGridView has header views. This ListAdapter
    209      * wraps another one and also keeps track of the header views and their
    210      * associated data objects.
    211      *<p>This is intended as a base class; you will probably not need to
    212      * use this class directly in your own code.
    213      */
    214     private static class HeaderViewGridAdapter implements WrapperListAdapter, Filterable {
    215 
    216         // This is used to notify the container of updates relating to number of columns
    217         // or headers changing, which changes the number of placeholders needed
    218         private final DataSetObservable mDataSetObservable = new DataSetObservable();
    219 
    220         private final ListAdapter mAdapter;
    221         private int mNumColumns = 1;
    222 
    223         // This ArrayList is assumed to NOT be null.
    224         ArrayList<FixedViewInfo> mHeaderViewInfos;
    225 
    226         boolean mAreAllFixedViewsSelectable;
    227 
    228         private final boolean mIsFilterable;
    229 
    230         public HeaderViewGridAdapter(ArrayList<FixedViewInfo> headerViewInfos, ListAdapter adapter) {
    231             mAdapter = adapter;
    232             mIsFilterable = adapter instanceof Filterable;
    233 
    234             if (headerViewInfos == null) {
    235                 throw new IllegalArgumentException("headerViewInfos cannot be null");
    236             }
    237             mHeaderViewInfos = headerViewInfos;
    238 
    239             mAreAllFixedViewsSelectable = areAllListInfosSelectable(mHeaderViewInfos);
    240         }
    241 
    242         public int getHeadersCount() {
    243             return mHeaderViewInfos.size();
    244         }
    245 
    246         @Override
    247         public boolean isEmpty() {
    248             return (mAdapter == null || mAdapter.isEmpty()) && getHeadersCount() == 0;
    249         }
    250 
    251         public void setNumColumns(int numColumns) {
    252             if (numColumns < 1) {
    253                 throw new IllegalArgumentException("Number of columns must be 1 or more");
    254             }
    255             if (mNumColumns != numColumns) {
    256                 mNumColumns = numColumns;
    257                 notifyDataSetChanged();
    258             }
    259         }
    260 
    261         private boolean areAllListInfosSelectable(ArrayList<FixedViewInfo> infos) {
    262             if (infos != null) {
    263                 for (FixedViewInfo info : infos) {
    264                     if (!info.isSelectable) {
    265                         return false;
    266                     }
    267                 }
    268             }
    269             return true;
    270         }
    271 
    272         public boolean removeHeader(View v) {
    273             for (int i = 0; i < mHeaderViewInfos.size(); i++) {
    274                 FixedViewInfo info = mHeaderViewInfos.get(i);
    275                 if (info.view == v) {
    276                     mHeaderViewInfos.remove(i);
    277 
    278                     mAreAllFixedViewsSelectable = areAllListInfosSelectable(mHeaderViewInfos);
    279 
    280                     mDataSetObservable.notifyChanged();
    281                     return true;
    282                 }
    283             }
    284 
    285             return false;
    286         }
    287 
    288         @Override
    289         public int getCount() {
    290             if (mAdapter != null) {
    291                 return getHeadersCount() * mNumColumns + mAdapter.getCount();
    292             } else {
    293                 return getHeadersCount() * mNumColumns;
    294             }
    295         }
    296 
    297         @Override
    298         public boolean areAllItemsEnabled() {
    299             if (mAdapter != null) {
    300                 return mAreAllFixedViewsSelectable && mAdapter.areAllItemsEnabled();
    301             } else {
    302                 return true;
    303             }
    304         }
    305 
    306         @Override
    307         public boolean isEnabled(int position) {
    308             // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
    309             int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
    310             if (position < numHeadersAndPlaceholders) {
    311                 return (position % mNumColumns == 0)
    312                         && mHeaderViewInfos.get(position / mNumColumns).isSelectable;
    313             }
    314 
    315             // Adapter
    316             final int adjPosition = position - numHeadersAndPlaceholders;
    317             int adapterCount = 0;
    318             if (mAdapter != null) {
    319                 adapterCount = mAdapter.getCount();
    320                 if (adjPosition < adapterCount) {
    321                     return mAdapter.isEnabled(adjPosition);
    322                 }
    323             }
    324 
    325             throw new ArrayIndexOutOfBoundsException(position);
    326         }
    327 
    328         @Override
    329         public Object getItem(int position) {
    330             // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
    331             int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
    332             if (position < numHeadersAndPlaceholders) {
    333                 if (position % mNumColumns == 0) {
    334                     return mHeaderViewInfos.get(position / mNumColumns).data;
    335                 }
    336                 return null;
    337             }
    338 
    339             // Adapter
    340             final int adjPosition = position - numHeadersAndPlaceholders;
    341             int adapterCount = 0;
    342             if (mAdapter != null) {
    343                 adapterCount = mAdapter.getCount();
    344                 if (adjPosition < adapterCount) {
    345                     return mAdapter.getItem(adjPosition);
    346                 }
    347             }
    348 
    349             throw new ArrayIndexOutOfBoundsException(position);
    350         }
    351 
    352         @Override
    353         public long getItemId(int position) {
    354             int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
    355             if (mAdapter != null && position >= numHeadersAndPlaceholders) {
    356                 int adjPosition = position - numHeadersAndPlaceholders;
    357                 int adapterCount = mAdapter.getCount();
    358                 if (adjPosition < adapterCount) {
    359                     return mAdapter.getItemId(adjPosition);
    360                 }
    361             }
    362             return -1;
    363         }
    364 
    365         @Override
    366         public boolean hasStableIds() {
    367             if (mAdapter != null) {
    368                 return mAdapter.hasStableIds();
    369             }
    370             return false;
    371         }
    372 
    373         @Override
    374         public View getView(int position, View convertView, ViewGroup parent) {
    375             // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
    376             int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns ;
    377             if (position < numHeadersAndPlaceholders) {
    378                 View headerViewContainer = mHeaderViewInfos
    379                         .get(position / mNumColumns).viewContainer;
    380                 if (position % mNumColumns == 0) {
    381                     return headerViewContainer;
    382                 } else {
    383                     if (convertView == null) {
    384                         convertView = new View(parent.getContext());
    385                     }
    386                     // We need to do this because GridView uses the height of the last item
    387                     // in a row to determine the height for the entire row.
    388                     convertView.setVisibility(View.INVISIBLE);
    389                     convertView.setMinimumHeight(headerViewContainer.getHeight());
    390                     return convertView;
    391                 }
    392             }
    393 
    394             // Adapter
    395             final int adjPosition = position - numHeadersAndPlaceholders;
    396             int adapterCount = 0;
    397             if (mAdapter != null) {
    398                 adapterCount = mAdapter.getCount();
    399                 if (adjPosition < adapterCount) {
    400                     return mAdapter.getView(adjPosition, convertView, parent);
    401                 }
    402             }
    403 
    404             throw new ArrayIndexOutOfBoundsException(position);
    405         }
    406 
    407         @Override
    408         public int getItemViewType(int position) {
    409             int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
    410             if (position < numHeadersAndPlaceholders && (position % mNumColumns != 0)) {
    411                 // Placeholders get the last view type number
    412                 return mAdapter != null ? mAdapter.getViewTypeCount() : 1;
    413             }
    414             if (mAdapter != null && position >= numHeadersAndPlaceholders) {
    415                 int adjPosition = position - numHeadersAndPlaceholders;
    416                 int adapterCount = mAdapter.getCount();
    417                 if (adjPosition < adapterCount) {
    418                     return mAdapter.getItemViewType(adjPosition);
    419                 }
    420             }
    421 
    422             return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
    423         }
    424 
    425         @Override
    426         public int getViewTypeCount() {
    427             if (mAdapter != null) {
    428                 return mAdapter.getViewTypeCount() + 1;
    429             }
    430             return 2;
    431         }
    432 
    433         @Override
    434         public void registerDataSetObserver(DataSetObserver observer) {
    435             mDataSetObservable.registerObserver(observer);
    436             if (mAdapter != null) {
    437                 mAdapter.registerDataSetObserver(observer);
    438             }
    439         }
    440 
    441         @Override
    442         public void unregisterDataSetObserver(DataSetObserver observer) {
    443             mDataSetObservable.unregisterObserver(observer);
    444             if (mAdapter != null) {
    445                 mAdapter.unregisterDataSetObserver(observer);
    446             }
    447         }
    448 
    449         @Override
    450         public Filter getFilter() {
    451             if (mIsFilterable) {
    452                 return ((Filterable) mAdapter).getFilter();
    453             }
    454             return null;
    455         }
    456 
    457         @Override
    458         public ListAdapter getWrappedAdapter() {
    459             return mAdapter;
    460         }
    461 
    462         public void notifyDataSetChanged() {
    463             mDataSetObservable.notifyChanged();
    464         }
    465     }
    466 }
    467