Add a Header to a GridView (Android)
Asked Answered
F

10

34

I'm aware that the GridView does NOT support a header or footer. I'm extensively using GridViews and I would like to have headers that scroll with it.

What is the best way to approach the problem? Extending the GridView? Extending the ScrollView or ListView?

Any pointer or suggestion would be really appreciated! Thanks!

Fabre answered 4/11, 2012 at 9:19 Comment(1)
Use this library github.com/liaohuqiu/android-GridViewWithHeaderAndFooterPodesta
S
68

Google's implementation of HeaderGridView addresses this problem. They are subclassing GridView.

HeaderGridView

I believe this is either part of the Google+ Photos app or the Gallery native app.

/*
 * 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();
        }
    }
}
Stomatitis answered 7/7, 2014 at 18:13 Comment(16)
Here is the link if you'd like it: android.googlesource.com/platform/packages/apps/Gallery2/+/…Expositor
this code is working better than others but in my case it just shows half of header view and the other half is off the screen , whats the problem ?Liber
getNumColumns() in HeaderGridView says it requires API 11 and above but my app supports API 10 and above. So how do I get around this?Nisse
Hi. I'm using HeaderGridView, but i need to center the header. How can i make this?Jerrold
Hi, i have used this. I have to hide header on filtering and re-show on removing filtering. Scrollable height is reduced to barely 20-40 dp when header view visibility is made to GONE. Any idea how header view can be hidden (I dont want to remove header view as it cant be added again upon removing filter).Small
@MohamadGhafourian That's because you have gravity set to enter on the GridView. Remove the gravity specification.Chondroma
This classe is very useful. Please note that it will not handle well the item click listener : you have to ignore the headers count by yourself.Molotov
add header view and set your HeaderGridView gravity to center_horizontal and the header gets messed up!Orr
thanks , above code works fine, just remove gravity from gridview and put central_horizontal in headerview. all that it works.Kelseykelsi
I think the 'gravity' issue is a bug. Also I found that if I place ImageViews in the items , the CENTER_CROP property is not applied (take the example of Developers GridView).Culex
How can I add this widget to XML layout?Eke
How do you add header string to it?Eke
Crash on clicking items and multiple headers do not work and not explained. Also arranging items under headers is not possible.Authorize
Really amazing you saved my day <3 <3 thanks so so sooo muchRessieressler
this library (y) github.com/liaohuqiu/android-GridViewWithHeaderAndFooterPodesta
It was giving me wrong position for each item click. so updated the code to add pastebin.com/VjQEwP5y it works fine now. Thanks @toobsco42Posset
W
9

I used Stickygridheaders from github ,it's very beautiful and simple ,try it.

Warbler answered 28/12, 2013 at 11:42 Comment(4)
yes , it seems like a good library . here's the github link btw: github.com/TonicArtos/StickyGridHeadersSeawards
hello android developer,i mention this link AFAIK !Warbler
I've tested the library and it still has some issues.Seawards
I would advise against using stickygridheaders, the performance is horrible. Edit: I'm talking about version 1.0.1, a future update might make it usableInterpellate
T
1

I would go for extending GridView in this case as it seems the easiest. If you decided to extend ListView or ScrollView you would have to implement all GridView functions first, which is unnecessary for your case.

Tymon answered 4/11, 2012 at 9:33 Comment(1)
I didn't get any pointer how to extend the GridView, so I think I will just use a ListView and place X items inside each row to make it look like a grid. Makes sense?Fabre
H
1

After implementing it myself, I can say that the easiest way is to make an Adapter that handles the columns and use a ListView with the default header

I published the code with an example here: https://github.com/plattysoft/grid-with-header-list-adapter/

Harilda answered 22/7, 2013 at 7:36 Comment(0)
J
1

You have to add header/footer view before calling setAdapter(new Your_Adapter);

  Try below code:



  LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
  View footerView = layoutInflater.inflate(R.layout.grid_view_footer, null);
  myListViewOrGridView.addFooterView(footerView);
  YourAdapter mAdapter = new YourAdapter(getActivity(), Your_Argument_Here);
  myListViewOrGridView.setAdapter(mAdapter);
Joannjoanna answered 4/12, 2014 at 9:23 Comment(2)
I haven't heard any method named addFooterView(), please elaborate your answer.Nisse
@Ramswaroop Can you please check toobsco42 answer for Custom gridview ?, It has method with myListViewOrGridView.addFooterView(footerView); Thanks.Joannjoanna
D
1

I know this is very old but if anyone runs into a bug that displays a white container at the bottom of the screen when using one of the custom GridView classes :

set the layouts heights above the GridView to match_parent instead of wrap_content

Deputize answered 10/5, 2020 at 2:46 Comment(0)
F
0

You may add the header view right above your GridView in a layout file. Like:

<LinearLayout>
...
    <LinearLayout
        android:id="@+id/header" />
    <com.sample.MyGridView />
...
</LinearLayout>

Then generate the header view and add it the LinearLayout with id header

View header = inflater.inflate(R.layout.head_view, null);
LinearLayout headerContainer = (LinearLayout) findViewById(R.id.header);
headerContainer.addView(header);
Fraud answered 4/11, 2012 at 9:37 Comment(1)
@vasart no. you have to override onActionMove to implement this if your want it to scroll with the GridView.Fraud
H
0

It is possible to use the HeaderViewListAdapter which is used internally by ListView. It has the restrictions that you must have the same number of headers as columns and that headers can't span columns (although you can play with the look so they appear to).

On the plus side it's very easy to wrap your existing adaptor and add in some extra header cells and you don't need to write any new code.

Hodge answered 19/11, 2012 at 11:37 Comment(0)
S
0

I've found another library that allows having headers on a gridView.

I't a bit annoying to import it and it has many un-needed resources, but it works fine: AStickyHeader

EDIT: it seems to have very annoying flexibility in regard to putting clickable views on the headers.

I think the best is to extend from GridView or implement it in a different way (as shown here, yet it supports only a single header, on top of the gridView) or use a listView with linearLayouts as the rows.

Seawards answered 28/1, 2014 at 14:40 Comment(0)
P
0

My code in c# looks like this

((HeaderViewGridAdapter)Adapter).NumColumns = NumColumnsCompatible;

private int NumColumnsCompatible
    {
        get
        {
            if (Build.VERSION.SdkInt >= BuildVersionCodes.Honeycomb)
            {
                return base.NumColumns;
            }
            else
            {
                try
                {
                    Field numColumns = this.Class.GetDeclaredField("numColumns");
                    numColumns.Accessible = true;

                    return numColumns.GetInt(this);
                }
                catch (Exception e)
                {
                    if (numColumns != -1)
                    {
                        return numColumns;
                    }
                    throw new Exception("Can not determine the NumColumns for this API platform, please call setNumColumns to set it.");
                }
            }
        }
    }
Prudential answered 9/2, 2015 at 21:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.