android GridView如何像ListView那样使用addHeaderView

GridView和ListView的唯一不同是他不能像ListView 那样添加header,因为他没有addHeaderView方法,不过有时我们的确需要一个带有header的GridView。

不过google已经为我们解决了这个问题,在原生系统的Gallery2应用中google自己写了一个HeaderGridView的类,完全可以实现我们的要求。

HeaderGridView的实现原理很简单,就是在WrapperAdapter根据每一行的列数增加了一列的Item。

/*   * Copyright (C) 2013 The Android Open Source Project   *   * Licensed under the Apache License, Version 2.0 (the "License");   * you may not use this file except in compliance with the License.   * You may obtain a copy of the License at   *   *      http://www.apache.org/licenses/LICENSE-2.0   *   * Unless required by applicable law or agreed to in writing, software   * distributed under the License is distributed on an "AS IS" BASIS,   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   * See the License for the specific language governing permissions and   * limitations under the License.   */  package com.android.photos.views;  import android.content.Context;  import android.database.DataSetObservable;  import android.database.DataSetObserver;  import android.util.AttributeSet;  import android.view.View;  import android.view.ViewGroup;  import android.widget.AdapterView;  import android.widget.Filter;  import android.widget.Filterable;  import android.widget.FrameLayout;  import android.widget.GridView;  import android.widget.ListAdapter;  import android.widget.WrapperListAdapter;  import java.util.ArrayList;  /**   * A {@link GridView} that supports adding header rows in a   * very similar way to {@link ListView}.   * See {@link HeaderGridView#addHeaderView(View, Object, boolean)}   */  public class HeaderGridView extends GridView {      private static final String TAG = "HeaderGridView";      /**       * A class that represents a fixed view in a list, for example a header at the top       * or a footer at the bottom.       */      private static class FixedViewInfo {          /** The view to add to the grid */          public View view;          public ViewGroup viewContainer;          /** The data backing the view. This is returned from {@link ListAdapter#getItem(int)}. */          public Object data;          /** <code>true</code> if the fixed view should be selectable in the grid */          public boolean isSelectable;      }      private ArrayList<FixedViewInfo> mHeaderViewInfos = new ArrayList<FixedViewInfo>();      private void initHeaderGridView() {          super.setClipChildren(false);      }      public HeaderGridView(Context context) {          super(context);          initHeaderGridView();      }      public HeaderGridView(Context context, AttributeSet attrs) {          super(context, attrs);          initHeaderGridView();      }      public HeaderGridView(Context context, AttributeSet attrs, int defStyle) {          super(context, attrs, defStyle);          initHeaderGridView();      }      @Override      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {          super.onMeasure(widthMeasureSpec, heightMeasureSpec);          ListAdapter adapter = getAdapter();          if (adapter != null && adapter instanceof HeaderViewGridAdapter) {              ((HeaderViewGridAdapter) adapter).setNumColumns(getNumColumns());          }      }      @Override      public void setClipChildren(boolean clipChildren) {         // Ignore, since the header rows depend on not being clipped      }      /**       * Add a fixed view to appear at the top of the grid. If addHeaderView is       * called more than once, the views will appear in the order they were       * added. Views added using this call can take focus if they want.       * <p>       * NOTE: Call this before calling setAdapter. This is so HeaderGridView can wrap       * the supplied cursor with one that will also account for header views.       *       * @param v The view to add.       * @param data Data to associate with this view       * @param isSelectable whether the item is selectable       */      public void addHeaderView(View v, Object data, boolean isSelectable) {          ListAdapter adapter = getAdapter();          if (adapter != null && ! (adapter instanceof HeaderViewGridAdapter)) {              throw new IllegalStateException(                      "Cannot add header view to grid -- setAdapter has already been called.");          }          FixedViewInfo info = new FixedViewInfo();          FrameLayout fl = new FullWidthFixedViewLayout(getContext());          fl.addView(v);          info.view = v;          info.viewContainer = fl;          info.data = data;          info.isSelectable = isSelectable;          mHeaderViewInfos.add(info);          // in the case of re-adding a header view, or adding one later on,          // we need to notify the observer          if (adapter != null) {              ((HeaderViewGridAdapter) adapter).notifyDataSetChanged();          }      }      /**       * Add a fixed view to appear at the top of the grid. If addHeaderView is       * called more than once, the views will appear in the order they were       * added. Views added using this call can take focus if they want.       * <p>       * NOTE: Call this before calling setAdapter. This is so HeaderGridView can wrap       * the supplied cursor with one that will also account for header views.       *       * @param v The view to add.       */      public void addHeaderView(View v) {          addHeaderView(v, null, true);      }      public int getHeaderViewCount() {          return mHeaderViewInfos.size();      }      /**       * Removes a previously-added header view.       *       * @param v The view to remove       * @return true if the view was removed, false if the view was not a header       *         view       */      public boolean removeHeaderView(View v) {          if (mHeaderViewInfos.size() > 0) {              boolean result = false;              ListAdapter adapter = getAdapter();              if (adapter != null && ((HeaderViewGridAdapter) adapter).removeHeader(v)) {                  result = true;              }              removeFixedViewInfo(v, mHeaderViewInfos);              return result;          }          return false;      }      private void removeFixedViewInfo(View v, ArrayList<FixedViewInfo> where) {          int len = where.size();          for (int i = 0; i < len; ++i) {              FixedViewInfo info = where.get(i);              if (info.view == v) {                  where.remove(i);                  break;              }          }      }      @Override      public void setAdapter(ListAdapter adapter) {          if (mHeaderViewInfos.size() > 0) {              HeaderViewGridAdapter hadapter = new HeaderViewGridAdapter(mHeaderViewInfos, adapter);              int numColumns = getNumColumns();              if (numColumns > 1) {                  hadapter.setNumColumns(numColumns);              }              super.setAdapter(hadapter);          } else {              super.setAdapter(adapter);          }      }      private class FullWidthFixedViewLayout extends FrameLayout {          public FullWidthFixedViewLayout(Context context) {              super(context);          }          @Override          protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {              int targetWidth = HeaderGridView.this.getMeasuredWidth()                      - HeaderGridView.this.getPaddingLeft()                      - HeaderGridView.this.getPaddingRight();              widthMeasureSpec = MeasureSpec.makeMeasureSpec(targetWidth,                      MeasureSpec.getMode(widthMeasureSpec));              super.onMeasure(widthMeasureSpec, heightMeasureSpec);          }      }      /**       * ListAdapter used when a HeaderGridView has header views. This ListAdapter       * wraps another one and also keeps track of the header views and their       * associated data objects.       *<p>This is intended as a base class; you will probably not need to       * use this class directly in your own code.       */      private static class HeaderViewGridAdapter implements WrapperListAdapter, Filterable {          // This is used to notify the container of updates relating to number of columns          // or headers changing, which changes the number of placeholders needed          private final DataSetObservable mDataSetObservable = new DataSetObservable();          private final ListAdapter mAdapter;          private int mNumColumns = 1;          // This ArrayList is assumed to NOT be null.          ArrayList<FixedViewInfo> mHeaderViewInfos;          boolean mAreAllFixedViewsSelectable;          private final boolean mIsFilterable;          public HeaderViewGridAdapter(ArrayList<FixedViewInfo> headerViewInfos, ListAdapter adapter) {              mAdapter = adapter;              mIsFilterable = adapter instanceof Filterable;              if (headerViewInfos == null) {                  throw new IllegalArgumentException("headerViewInfos cannot be null");              }              mHeaderViewInfos = headerViewInfos;              mAreAllFixedViewsSelectable = areAllListInfosSelectable(mHeaderViewInfos);          }          public int getHeadersCount() {              return mHeaderViewInfos.size();          }          @Override          public boolean isEmpty() {              return (mAdapter == null || mAdapter.isEmpty()) && getHeadersCount() == 0;          }          public void setNumColumns(int numColumns) {              if (numColumns < 1) {                  throw new IllegalArgumentException("Number of columns must be 1 or more");              }              if (mNumColumns != numColumns) {                  mNumColumns = numColumns;                  notifyDataSetChanged();              }          }          private boolean areAllListInfosSelectable(ArrayList<FixedViewInfo> infos) {              if (infos != null) {                  for (FixedViewInfo info : infos) {                      if (!info.isSelectable) {                          return false;                      }                  }              }              return true;          }          public boolean removeHeader(View v) {              for (int i = 0; i < mHeaderViewInfos.size(); i++) {                  FixedViewInfo info = mHeaderViewInfos.get(i);                  if (info.view == v) {                      mHeaderViewInfos.remove(i);                      mAreAllFixedViewsSelectable = areAllListInfosSelectable(mHeaderViewInfos);                      mDataSetObservable.notifyChanged();                      return true;                  }              }              return false;          }          @Override          public int getCount() {              if (mAdapter != null) {                  return getHeadersCount() * mNumColumns + mAdapter.getCount();              } else {                  return getHeadersCount() * mNumColumns;              }          }          @Override          public boolean areAllItemsEnabled() {              if (mAdapter != null) {                  return mAreAllFixedViewsSelectable && mAdapter.areAllItemsEnabled();              } else {                  return true;              }          }          @Override          public boolean isEnabled(int position) {              // Header (negative positions will throw an ArrayIndexOutOfBoundsException)              int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;              if (position < numHeadersAndPlaceholders) {                  return (position % mNumColumns == 0)                          && mHeaderViewInfos.get(position / mNumColumns).isSelectable;              }              // Adapter              final int adjPosition = position - numHeadersAndPlaceholders;              int adapterCount = 0;              if (mAdapter != null) {                  adapterCount = mAdapter.getCount();                  if (adjPosition < adapterCount) {                      return mAdapter.isEnabled(adjPosition);                  }              }              throw new ArrayIndexOutOfBoundsException(position);          }          @Override          public Object getItem(int position) {              // Header (negative positions will throw an ArrayIndexOutOfBoundsException)              int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;              if (position < numHeadersAndPlaceholders) {                  if (position % mNumColumns == 0) {                      return mHeaderViewInfos.get(position / mNumColumns).data;                  }                  return null;              }              // Adapter              final int adjPosition = position - numHeadersAndPlaceholders;              int adapterCount = 0;              if (mAdapter != null) {                  adapterCount = mAdapter.getCount();                  if (adjPosition < adapterCount) {                      return mAdapter.getItem(adjPosition);                  }              }              throw new ArrayIndexOutOfBoundsException(position);          }          @Override          public long getItemId(int position) {              int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;              if (mAdapter != null && position >= numHeadersAndPlaceholders) {                  int adjPosition = position - numHeadersAndPlaceholders;                  int adapterCount = mAdapter.getCount();                  if (adjPosition < adapterCount) {                      return mAdapter.getItemId(adjPosition);                  }              }              return -1;          }          @Override          public boolean hasStableIds() {              if (mAdapter != null) {                  return mAdapter.hasStableIds();              }              return false;          }          @Override          public View getView(int position, View convertView, ViewGroup parent) {              // Header (negative positions will throw an ArrayIndexOutOfBoundsException)              int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns ;              if (position < numHeadersAndPlaceholders) {                  View headerViewContainer = mHeaderViewInfos                          .get(position / mNumColumns).viewContainer;                  if (position % mNumColumns == 0) {                      return headerViewContainer;                  } else {                      if (convertView == null) {                          convertView = new View(parent.getContext());                      }                      // We need to do this because GridView uses the height of the last item                      // in a row to determine the height for the entire row.                      convertView.setVisibility(View.INVISIBLE);                      convertView.setMinimumHeight(headerViewContainer.getHeight());                      return convertView;                  }              }              // Adapter              final int adjPosition = position - numHeadersAndPlaceholders;              int adapterCount = 0;              if (mAdapter != null) {                  adapterCount = mAdapter.getCount();                  if (adjPosition < adapterCount) {                      return mAdapter.getView(adjPosition, convertView, parent);                  }              }              throw new ArrayIndexOutOfBoundsException(position);          }          @Override          public int getItemViewType(int position) {              int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;              if (position < numHeadersAndPlaceholders && (position % mNumColumns != 0)) {                  // Placeholders get the last view type number                  return mAdapter != null ? mAdapter.getViewTypeCount() : 1;              }              if (mAdapter != null && position >= numHeadersAndPlaceholders) {                  int adjPosition = position - numHeadersAndPlaceholders;                  int adapterCount = mAdapter.getCount();                  if (adjPosition < adapterCount) {                      return mAdapter.getItemViewType(adjPosition);                  }              }              return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;          }          @Override          public int getViewTypeCount() {              if (mAdapter != null) {                  return mAdapter.getViewTypeCount() + 1;              }              return 2;          }          @Override          public void registerDataSetObserver(DataSetObserver observer) {              mDataSetObservable.registerObserver(observer);              if (mAdapter != null) {                  mAdapter.registerDataSetObserver(observer);              }          }          @Override          public void unregisterDataSetObserver(DataSetObserver observer) {              mDataSetObservable.unregisterObserver(observer);              if (mAdapter != null) {                  mAdapter.unregisterDataSetObserver(observer);              }          }          @Override          public Filter getFilter() {              if (mIsFilterable) {                  return ((Filterable) mAdapter).getFilter();              }              return null;          }          @Override          public ListAdapter getWrappedAdapter() {              return mAdapter;          }          public void notifyDataSetChanged() {              mDataSetObservable.notifyChanged();          }      }  }

其中可以帮助你理解的代码片段在getView中:

@Override  public View getView(int position, View convertView, ViewGroup parent) {      // Header (negative positions will throw an ArrayIndexOutOfBoundsException)      int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns ;      if (position < numHeadersAndPlaceholders) {          View headerViewContainer = mHeaderViewInfos                  .get(position / mNumColumns).viewContainer;          if (position % mNumColumns == 0) {              return headerViewContainer;          } else {              if (convertView == null) {                  convertView = new View(parent.getContext());              }              // We need to do this because GridView uses the height of the last item              // in a row to determine the height for the entire row.              convertView.setVisibility(View.INVISIBLE);              convertView.setMinimumHeight(headerViewContainer.getHeight());              return convertView;          }      }      // Adapter      final int adjPosition = position - numHeadersAndPlaceholders;      int adapterCount = 0;      if (mAdapter != null) {          adapterCount = mAdapter.getCount();          if (adjPosition < adapterCount) {              return mAdapter.getView(adjPosition, convertView, parent);          }      }      throw new ArrayIndexOutOfBoundsException(position);  }

可以看到再判断position小于列数的时候直接返回了一个headerViewContainer。所以在使用中需要注意的是处理item被点击的时候,position =0的时候代表的并不是第一个item,第mNumColumns个item才是

HeaderGridView的其他方面与ListView中是一样的。