Click here to Skip to main content
15,886,065 members
Articles / Web Development / HTML

Customized StickyGridHeaders

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
13 Sep 2013CPOL1 min read 12.2K   6  
I customized StickyGridHeaders to work on any data and group by any data.
/*
 Copyright 2013 Tonic Artos

 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.example.stickyheadertest;

import com.example.stickyheadertest.StickyGridHeadersBaseAdapterWrapper.HeaderFillerView;
import java.util.ArrayList;
import java.util.List;

import android.content.Context;
import android.database.DataSetObserver;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Build;
import android.os.Handler;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.SoundEffectConstants;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.GridView;
import android.widget.ListAdapter;

/**
 * GridView that displays items in sections with headers that stick to the top
 * of the view.
 * 
 * @author Tonic Artos, Emil Sjölander
 */
public class StickyGridHeadersGridView extends GridView implements OnScrollListener,
        OnItemClickListener, OnItemSelectedListener, OnItemLongClickListener {
    private static final int MATCHED_STICKIED_HEADER = -2;

    private static final int NO_MATCHED_HEADER = -1;

    protected static final int TOUCH_MODE_DONE_WAITING = 2;

    protected static final int TOUCH_MODE_DOWN = 0;

    protected static final int TOUCH_MODE_FINISHED_LONG_PRESS = -2;

    protected static final int TOUCH_MODE_REST = -1;

    protected static final int TOUCH_MODE_TAP = 1;

    public CheckForHeaderLongPress mPendingCheckForLongPress;

    public CheckForHeaderTap mPendingCheckForTap;

    private boolean mAreHeadersSticky = true;

    private final Rect mClippingRect = new Rect();

    private boolean mClippingToPadding;

    private boolean mClipToPaddingHasBeenSet;

    private int mColumnWidth;

    private long mCurrentHeaderId = -1;

    private DataSetObserver mDataSetObserver = new DataSetObserver() {
        @Override
        public void onChanged() {
            reset();
        }

        @Override
        public void onInvalidated() {
            reset();
        }
    };

    private int mHeaderBottomPosition;

    private int mHorizontalSpacing;

    private float mMotionY;

    /**
     * Must be set from the wrapped GridView in the constructor.
     */
    private int mNumColumns;

    private boolean mNumColumnsSet;

    private int mNumMeasuredColumns = 1;

    private OnHeaderClickListener mOnHeaderClickListener;

    private OnHeaderLongClickListener mOnHeaderLongClickListener;

    private OnItemClickListener mOnItemClickListener;

    private OnItemLongClickListener mOnItemLongClickListener;

    private OnItemSelectedListener mOnItemSelectedListener;

    private PerformHeaderClick mPerformHeaderClick;

    private OnScrollListener mScrollListener;

    private int mScrollState = SCROLL_STATE_IDLE;

    private View mStickiedHeader;

    private Runnable mTouchModeReset;

    private int mTouchSlop;

    private int mVerticalSpacing;

    protected StickyGridHeadersBaseAdapterWrapper mAdapter;

    protected boolean mDataChanged;

    protected int mMotionHeaderPosition;

    protected int mTouchMode;

    private boolean mMaskStickyHeaderRegion = true;

    private boolean mHeadersIgnorePadding;

    public StickyGridHeadersGridView(Context context) {
        this(context, null);
    }

    public StickyGridHeadersGridView(Context context, AttributeSet attrs) {
        this(context, attrs, android.R.attr.gridViewStyle);
    }

    public StickyGridHeadersGridView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        super.setOnScrollListener(this);
        setVerticalFadingEdgeEnabled(false);

        if (!mNumColumnsSet) {
            mNumColumns = AUTO_FIT;
        }

        ViewConfiguration vc = ViewConfiguration.get(context);
        mTouchSlop = vc.getScaledTouchSlop();
    }

    public boolean areHeadersSticky() {
        return mAreHeadersSticky;
    }

    /**
     * Gets the header at an item position. However, the position must be that
     * of a HeaderFiller.
     * 
     * @param position Position of HeaderFiller.
     * @return Header View wrapped in HeaderFiller or null if no header was
     *         found.
     */
    public View getHeaderAt(int position) {
        if (position == MATCHED_STICKIED_HEADER) {
            return mStickiedHeader;
        }

        try {
            return (View)getChildAt(position).getTag();
        } catch (Exception e) {
        }
        return null;
    }

    /**
     * Get the currently stickied header.
     * 
     * @return Current stickied header.
     */
    public View getStickiedHeader() {
        return mStickiedHeader;
    }

    public boolean getStickyHeaderIsTranscluent() {
        return !mMaskStickyHeaderRegion;
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        mOnItemClickListener.onItemClick(parent, view,
                mAdapter.translatePosition(position).mPosition, id);
    }

    @Override
    public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
        return mOnItemLongClickListener.onItemLongClick(parent, view,
                mAdapter.translatePosition(position).mPosition, id);
    }

    @Override
    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
        mOnItemSelectedListener.onItemSelected(parent, view,
                mAdapter.translatePosition(position).mPosition, id);
    }

    @Override
    public void onNothingSelected(AdapterView<?> parent) {
        mOnItemSelectedListener.onNothingSelected(parent);
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        SavedState ss = (SavedState)state;

        super.onRestoreInstanceState(ss.getSuperState());
        mAreHeadersSticky = ss.areHeadersSticky;

        requestLayout();
    }

    @Override
    public Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();

        SavedState ss = new SavedState(superState);
        ss.areHeadersSticky = mAreHeadersSticky;
        return ss;
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
            int totalItemCount) {
        if (mScrollListener != null) {
            mScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {
            scrollChanged(firstVisibleItem);
        }
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        if (mScrollListener != null) {
            mScrollListener.onScrollStateChanged(view, scrollState);
        }

        mScrollState = scrollState;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                if (mPendingCheckForTap == null) {
                    mPendingCheckForTap = new CheckForHeaderTap();
                }
                postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());

                final int y = (int)ev.getY();
                mMotionY = y;
                mMotionHeaderPosition = findMotionHeader(y);
                if (mMotionHeaderPosition == NO_MATCHED_HEADER
                        || mScrollState == SCROLL_STATE_FLING) {
                    // Don't consume the event and pass it to super because we
                    // can't handle it yet.
                    break;
                }
                mTouchMode = TOUCH_MODE_DOWN;
                return true;
            case MotionEvent.ACTION_MOVE:
                if (mMotionHeaderPosition != NO_MATCHED_HEADER
                        && Math.abs(ev.getY() - mMotionY) > mTouchSlop) {
                    // Detected scroll initiation so cancel touch completion on
                    // header.
                    mTouchMode = TOUCH_MODE_REST;
                    final View header = getHeaderAt(mMotionHeaderPosition);
                    if (header != null) {
                        header.setPressed(false);
                    }
                    final Handler handler = getHandler();
                    if (handler != null) {
                        handler.removeCallbacks(mPendingCheckForLongPress);
                    }
                    mMotionHeaderPosition = NO_MATCHED_HEADER;
                }
                break;
            case MotionEvent.ACTION_UP:
                if (mTouchMode == TOUCH_MODE_FINISHED_LONG_PRESS) {
                    return true;
                }
                if (mTouchMode == TOUCH_MODE_REST || mMotionHeaderPosition == NO_MATCHED_HEADER) {
                    break;
                }

                final View header = getHeaderAt(mMotionHeaderPosition);
                if (header != null && !header.hasFocusable()) {
                    if (mTouchMode != TOUCH_MODE_DOWN) {
                        header.setPressed(false);
                    }

                    if (mPerformHeaderClick == null) {
                        mPerformHeaderClick = new PerformHeaderClick();
                    }

                    final PerformHeaderClick performHeaderClick = mPerformHeaderClick;
                    performHeaderClick.mClickMotionPosition = mMotionHeaderPosition;
                    performHeaderClick.rememberWindowAttachCount();

                    if (mTouchMode != TOUCH_MODE_DOWN || mTouchMode != TOUCH_MODE_TAP) {
                        final Handler handler = getHandler();
                        if (handler != null) {
                            handler.removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ? mPendingCheckForTap
                                    : mPendingCheckForLongPress);
                        }

                        if (!mDataChanged) {
                            // Got here so must be a tap. The long press would
                            // have trigger on the callback handler. Probably.
                            mTouchMode = TOUCH_MODE_TAP;
                            header.setPressed(true);
                            setPressed(true);
                            if (mTouchModeReset != null) {
                                removeCallbacks(mTouchModeReset);
                            }
                            mTouchModeReset = new Runnable() {
                                @Override
                                public void run() {
                                    mTouchMode = TOUCH_MODE_REST;
                                    header.setPressed(false);
                                    setPressed(false);
                                    if (!mDataChanged) {
                                        performHeaderClick.run();
                                    }
                                }
                            };
                            postDelayed(mTouchModeReset,
                                    ViewConfiguration.getPressedStateDuration());
                        } else {
                            mTouchMode = TOUCH_MODE_REST;
                        }
                    } else if (!mDataChanged) {
                        performHeaderClick.run();
                    }
                }
                mTouchMode = TOUCH_MODE_REST;
                return true;
        }
        return super.onTouchEvent(ev);
    }

    public boolean performHeaderClick(View view, long id) {
        if (mOnHeaderClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            if (view != null) {
                view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
            }
            mOnHeaderClickListener.onHeaderClick(this, view, id);
            return true;
        }

        return false;
    }

    public boolean performHeaderLongPress(View view, long id) {
        boolean handled = false;
        if (mOnHeaderLongClickListener != null) {
            handled = mOnHeaderLongClickListener.onHeaderLongClick(this, view, id);
        }

        if (handled) {
            if (view != null) {
                view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
            }
            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
        }

        return handled;
    }

    @Override
    public void setAdapter(ListAdapter adapter) {
        if (mAdapter != null && mDataSetObserver != null) {
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        }

        if (!mClipToPaddingHasBeenSet) {
            mClippingToPadding = true;
        }

        StickyGridHeadersBaseAdapter baseAdapter;
        if (adapter instanceof StickyGridHeadersBaseAdapter) {
            baseAdapter = (StickyGridHeadersBaseAdapter)adapter;
        } else if (adapter instanceof StickyGridHeadersSimpleAdapter) {
            // Wrap up simple adapter to auto-generate the data we need.
            baseAdapter = new StickyGridHeadersSimpleAdapterWrapper(
                    (StickyGridHeadersSimpleAdapter)adapter);
        } else {
            // Wrap up a list adapter so it is an adapter with zero headers.
            baseAdapter = new StickyGridHeadersListAdapterWrapper(adapter);
        }

        this.mAdapter = new StickyGridHeadersBaseAdapterWrapper(getContext(), this, baseAdapter);
        this.mAdapter.registerDataSetObserver(mDataSetObserver);
        reset();
        super.setAdapter(this.mAdapter);
    }

    public void setAreHeadersSticky(boolean useStickyHeaders) {
        if (useStickyHeaders != mAreHeadersSticky) {
            mAreHeadersSticky = useStickyHeaders;
            requestLayout();
        }
    }

    @Override
    public void setClipToPadding(boolean clipToPadding) {
        super.setClipToPadding(clipToPadding);
        mClippingToPadding = clipToPadding;
        mClipToPaddingHasBeenSet = true;
    }

    @Override
    public void setColumnWidth(int columnWidth) {
        super.setColumnWidth(columnWidth);
        mColumnWidth = columnWidth;
    }

    @Override
    public void setHorizontalSpacing(int horizontalSpacing) {
        super.setHorizontalSpacing(horizontalSpacing);
        mHorizontalSpacing = horizontalSpacing;
    }

    @Override
    public void setNumColumns(int numColumns) {
        super.setNumColumns(numColumns);
        mNumColumnsSet = true;
        this.mNumColumns = numColumns;
        if (numColumns != AUTO_FIT && mAdapter != null) {
            mAdapter.setNumColumns(numColumns);
        }
    }

    public void setOnHeaderClickListener(OnHeaderClickListener listener) {
        mOnHeaderClickListener = listener;
    }

    public void setOnHeaderLongClickListener(OnHeaderLongClickListener listener) {
        if (!isLongClickable()) {
            setLongClickable(true);
        }
        mOnHeaderLongClickListener = listener;
    }

    @Override
    public void setOnItemClickListener(android.widget.AdapterView.OnItemClickListener listener) {
        this.mOnItemClickListener = listener;
        super.setOnItemClickListener(this);
    }

    @Override
    public void setOnItemLongClickListener(
            android.widget.AdapterView.OnItemLongClickListener listener) {
        this.mOnItemLongClickListener = listener;
        super.setOnItemLongClickListener(this);
    }

    @Override
    public void setOnItemSelectedListener(android.widget.AdapterView.OnItemSelectedListener listener) {
        this.mOnItemSelectedListener = listener;
        super.setOnItemSelectedListener(this);
    }

    @Override
    public void setOnScrollListener(OnScrollListener listener) {
        this.mScrollListener = listener;
    }

    public void setStickyHeaderIsTranscluent(boolean isTranscluent) {
        mMaskStickyHeaderRegion = !isTranscluent;
    }

    @Override
    public void setVerticalSpacing(int verticalSpacing) {
        super.setVerticalSpacing(verticalSpacing);
        mVerticalSpacing = verticalSpacing;
    }

    private int findMotionHeader(float y) {
        if (mStickiedHeader != null && y <= mStickiedHeader.getBottom()) {
            return MATCHED_STICKIED_HEADER;
        }

        int vi = 0;
        for (int i = getFirstVisiblePosition(); i <= getLastVisiblePosition();) {
            long id = getItemIdAtPosition(i);
            if (id == StickyGridHeadersBaseAdapterWrapper.ID_HEADER) {
                View headerWrapper = getChildAt(vi);

                int bottom = headerWrapper.getBottom();
                int top = headerWrapper.getTop();
                if (y <= bottom && y >= top) {
                    return vi;
                }
            }
            i += mNumMeasuredColumns;
            vi += mNumMeasuredColumns;
        }

        return NO_MATCHED_HEADER;
    }

    private int getHeaderHeight() {
        if (mStickiedHeader != null) {
            return mStickiedHeader.getMeasuredHeight();
        }
        return 0;
    }

    private long headerViewPositionToId(int pos) {
        if (pos == MATCHED_STICKIED_HEADER) {
            return mCurrentHeaderId;
        }
        return mAdapter.getHeaderId(getFirstVisiblePosition() + pos);
    }

    private void measureHeader() {
        if (mStickiedHeader == null) {
            return;
        }

        int widthMeasureSpec;
        if (mHeadersIgnorePadding) {
            widthMeasureSpec = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY);
        } else {
            widthMeasureSpec = MeasureSpec.makeMeasureSpec(getWidth() - getPaddingLeft()
                    - getPaddingRight(), MeasureSpec.EXACTLY);
        }

        int heightMeasureSpec = 0;

        ViewGroup.LayoutParams params = mStickiedHeader.getLayoutParams();
        if (params != null && params.height > 0) {
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(params.height, MeasureSpec.EXACTLY);
        } else {
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        }
        mStickiedHeader.measure(widthMeasureSpec, heightMeasureSpec);

        if (mHeadersIgnorePadding) {
            mStickiedHeader.layout(getLeft(), 0, getRight(), mStickiedHeader.getMeasuredHeight());
        } else {
            mStickiedHeader.layout(getLeft() + getPaddingLeft(), 0, getRight() - getPaddingRight(),
                    mStickiedHeader.getMeasuredHeight());
        }
    }

    /**
     * If set to true, headers will ignore horizontal padding.
     * 
     * @param b if true, horizontal padding is ignored by headers
     */
    public void setHeadersIgnorePadding(boolean b) {
        mHeadersIgnorePadding = b;
    }

    private void reset() {
        mHeaderBottomPosition = 0;
        mStickiedHeader = null;
        mCurrentHeaderId = INVALID_ROW_ID;
    }

    private void scrollChanged(int firstVisibleItem) {
        if (mAdapter == null || mAdapter.getCount() == 0 || !mAreHeadersSticky) {
            return;
        }

        View firstItem = getChildAt(0);
        if (firstItem == null) {
            return;
        }

        long newHeaderId;
        int selectedHeaderPosition = firstVisibleItem;

        int beforeRowPosition = firstVisibleItem - mNumMeasuredColumns;
        if (beforeRowPosition < 0) {
            beforeRowPosition = firstVisibleItem;
        }

        int secondRowPosition = firstVisibleItem + mNumMeasuredColumns;
        if (secondRowPosition >= mAdapter.getCount()) {
            secondRowPosition = firstVisibleItem;
        }

        if (mVerticalSpacing == 0) {
            newHeaderId = mAdapter.getHeaderId(firstVisibleItem);
        } else if (mVerticalSpacing < 0) {
            newHeaderId = mAdapter.getHeaderId(firstVisibleItem);
            View firstSecondRowView = getChildAt(mNumMeasuredColumns);
            if (firstSecondRowView.getTop() <= 0) {
                newHeaderId = mAdapter.getHeaderId(secondRowPosition);
                selectedHeaderPosition = secondRowPosition;
            } else {
                newHeaderId = mAdapter.getHeaderId(firstVisibleItem);
            }
        } else {
            int margin = getChildAt(0).getTop();
            if (0 < margin && margin < mVerticalSpacing) {
                newHeaderId = mAdapter.getHeaderId(beforeRowPosition);
                selectedHeaderPosition = beforeRowPosition;
            } else {
                newHeaderId = mAdapter.getHeaderId(firstVisibleItem);
            }
        }

        if (mCurrentHeaderId != newHeaderId) {
            mStickiedHeader = mAdapter.getHeaderView(selectedHeaderPosition, mStickiedHeader, this);
            measureHeader();
            mCurrentHeaderId = newHeaderId;
        }

        final int childCount = getChildCount();
        if (childCount != 0) {
            View viewToWatch = null;
            int watchingChildDistance = 99999;

            // Find the next header after the stickied one.
            for (int i = 0; i < childCount; i += mNumMeasuredColumns) {
                View child = super.getChildAt(i);

                int childDistance;
                if (mClippingToPadding) {
                    childDistance = child.getTop() - getPaddingTop();
                } else {
                    childDistance = child.getTop();
                }

                if (childDistance < 0) {
                    continue;
                }

                if (mAdapter.getItemId(getPositionForView(child)) == StickyGridHeadersBaseAdapterWrapper.ID_HEADER
                        && childDistance < watchingChildDistance) {
                    viewToWatch = child;
                    watchingChildDistance = childDistance;
                }
            }

            int headerHeight = getHeaderHeight();

            // Work out where to draw stickied header using synchronised
            // scrolling.
            if (viewToWatch != null) {
                if (firstVisibleItem == 0 && super.getChildAt(0).getTop() > 0
                        && !mClippingToPadding) {
                    mHeaderBottomPosition = 0;
                } else {
                    if (mClippingToPadding) {
                        mHeaderBottomPosition = Math.min(viewToWatch.getTop(), headerHeight
                                + getPaddingTop());
                        mHeaderBottomPosition = mHeaderBottomPosition < getPaddingTop() ? headerHeight
                                + getPaddingTop()
                                : mHeaderBottomPosition;
                    } else {
                        mHeaderBottomPosition = Math.min(viewToWatch.getTop(), headerHeight);
                        mHeaderBottomPosition = mHeaderBottomPosition < 0 ? headerHeight
                                : mHeaderBottomPosition;
                    }
                }
            } else {
                mHeaderBottomPosition = headerHeight;
                if (mClippingToPadding) {
                    mHeaderBottomPosition += getPaddingTop();
                }
            }
        }
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) {
            scrollChanged(getFirstVisiblePosition());
        }

        boolean drawStickiedHeader = mStickiedHeader != null && mAreHeadersSticky
                && mStickiedHeader.getVisibility() == View.VISIBLE;
        int headerHeight = getHeaderHeight();
        int top = mHeaderBottomPosition - headerHeight;

        // Mask the region where we will draw the header later, but only if we
        // will draw a header and masking is requested.
        if (drawStickiedHeader && mMaskStickyHeaderRegion) {
            if (mHeadersIgnorePadding) {
                mClippingRect.left = 0;
                mClippingRect.right = getWidth();
            } else {
                mClippingRect.left = getPaddingLeft();
                mClippingRect.right = getWidth() - getPaddingRight();
            }
            mClippingRect.top = mHeaderBottomPosition;
            mClippingRect.bottom = getHeight();

            canvas.save();
            canvas.clipRect(mClippingRect);
        }

        // ...and draw the grid view.
        super.dispatchDraw(canvas);

        // Find headers.
        List<Integer> headerPositions = new ArrayList<Integer>();
        int vi = 0;
        for (int i = getFirstVisiblePosition(); i <= getLastVisiblePosition();) {
            long id = getItemIdAtPosition(i);
            if (id == StickyGridHeadersBaseAdapterWrapper.ID_HEADER) {
                headerPositions.add(vi);
            }
            i += mNumMeasuredColumns;
            vi += mNumMeasuredColumns;
        }

        // Draw headers in list.
        for (int i = 0; i < headerPositions.size(); i++) {
            View frame = getChildAt(headerPositions.get(i));
            View header;
            try {
                header = (View)frame.getTag();
            } catch (Exception e) {
                return;
            }

            boolean headerIsStickied = ((HeaderFillerView)frame).getHeaderId() == mCurrentHeaderId
                    && frame.getTop() < 0 && mAreHeadersSticky;
            if (header.getVisibility() != View.VISIBLE || headerIsStickied) {
                continue;
            }

            int widthMeasureSpec;
            if (mHeadersIgnorePadding) {
                widthMeasureSpec = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY);
            } else {
                widthMeasureSpec = MeasureSpec.makeMeasureSpec(getWidth() - getPaddingLeft()
                        - getPaddingRight(), MeasureSpec.EXACTLY);
            }

            int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
            header.measure(widthMeasureSpec, heightMeasureSpec);

            if (mHeadersIgnorePadding) {
                header.layout(getLeft(), 0, getRight(), frame.getHeight());
            } else {
                header.layout(getLeft() + getPaddingLeft(), 0, getRight() - getPaddingRight(),
                        frame.getHeight());
            }

            if (mHeadersIgnorePadding) {
                mClippingRect.left = 0;
                mClippingRect.right = getWidth();
            } else {
                mClippingRect.left = getPaddingLeft();
                mClippingRect.right = getWidth() - getPaddingRight();
            }

            mClippingRect.bottom = frame.getBottom();
            mClippingRect.top = frame.getTop();
            canvas.save();
            canvas.clipRect(mClippingRect);
            if (mHeadersIgnorePadding) {
                canvas.translate(0, frame.getTop());
            } else {
                canvas.translate(getPaddingLeft(), frame.getTop());
            }
            header.draw(canvas);
            canvas.restore();
        }

        if (drawStickiedHeader && mMaskStickyHeaderRegion) {
            canvas.restore();
        } else if (!drawStickiedHeader) {
            // Done.
            return;
        }

        // Draw stickied header.
        int wantedWidth;
        if (mHeadersIgnorePadding) {
            wantedWidth = getWidth();
        } else {
            wantedWidth = getWidth() - getPaddingLeft() - getPaddingRight();
        }
        if (mStickiedHeader.getWidth() != wantedWidth) {
            int widthMeasureSpec;
            if (mHeadersIgnorePadding) {
                widthMeasureSpec = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY);
            } else {
                widthMeasureSpec = MeasureSpec.makeMeasureSpec(getWidth() - getPaddingLeft()
                        - getPaddingRight(), MeasureSpec.EXACTLY); // Bug here
            }
            int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
            mStickiedHeader.measure(widthMeasureSpec, heightMeasureSpec);
            if (mHeadersIgnorePadding) {
                mStickiedHeader.layout(getLeft(), 0, getRight(), mStickiedHeader.getHeight());
            } else {
                mStickiedHeader.layout(getLeft() + getPaddingLeft(), 0, getRight()
                        - getPaddingRight(), mStickiedHeader.getHeight());
            }
        }

        if (mHeadersIgnorePadding) {
            mClippingRect.left = 0;
            mClippingRect.right = getWidth();
        } else {
            mClippingRect.left = getPaddingLeft();
            mClippingRect.right = getWidth() - getPaddingRight();
        }
        mClippingRect.bottom = top + headerHeight;
        if (mClippingToPadding) {
            mClippingRect.top = getPaddingTop();
        } else {
            mClippingRect.top = 0;
        }

        canvas.save();
        canvas.clipRect(mClippingRect);

        if (mHeadersIgnorePadding) {
            canvas.translate(0, top);
        } else {
            canvas.translate(getPaddingLeft(), top);
        }

        if (mHeaderBottomPosition != headerHeight) {
            canvas.saveLayerAlpha(0, 0, canvas.getWidth(), canvas.getHeight(), 255
                    * mHeaderBottomPosition / headerHeight, Canvas.ALL_SAVE_FLAG);
        }

        mStickiedHeader.draw(canvas);
        canvas.restore();
        canvas.restore();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mNumColumns == AUTO_FIT) {
            int numFittedColumns;
            if (mColumnWidth > 0) {
                int gridWidth = Math.max(MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft()
                        - getPaddingRight(), 0);
                numFittedColumns = gridWidth / mColumnWidth;
                // Calculate measured columns accounting for requested grid
                // spacing.
                if (numFittedColumns > 0) {
                    while (numFittedColumns != 1) {
                        if (numFittedColumns * mColumnWidth + (numFittedColumns - 1)
                                * mHorizontalSpacing > gridWidth) {
                            numFittedColumns--;
                        } else {
                            break;
                        }
                    }
                } else {
                    // Could not fit any columns in grid width, so default to a
                    // single column.
                    numFittedColumns = 1;
                }
            } else {
                // Mimic vanilla GridView behaviour where there is not enough
                // information to auto-fit columns.
                numFittedColumns = 2;
            }
            mNumMeasuredColumns = numFittedColumns;
        } else {
            // There were some number of columns requested so we will try to
            // fulfil the request.
            mNumMeasuredColumns = mNumColumns;
        }

        // Update adapter with number of columns.
        if (mAdapter != null) {
            mAdapter.setNumColumns(mNumMeasuredColumns);
        }

        measureHeader();

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    public interface OnHeaderClickListener {
        void onHeaderClick(AdapterView<?> parent, View view, long id);
    }

    public interface OnHeaderLongClickListener {
        boolean onHeaderLongClick(AdapterView<?> parent, View view, long id);
    }

    private class CheckForHeaderLongPress extends WindowRunnable implements Runnable {
        @Override
        public void run() {
            final View child = getHeaderAt(mMotionHeaderPosition);
            if (child != null) {
                final long longPressId = headerViewPositionToId(mMotionHeaderPosition);

                boolean handled = false;
                if (sameWindow() && !mDataChanged) {
                    handled = performHeaderLongPress(child, longPressId);
                }
                if (handled) {
                    mTouchMode = TOUCH_MODE_FINISHED_LONG_PRESS;
                    setPressed(false);
                    child.setPressed(false);
                } else {
                    mTouchMode = TOUCH_MODE_DONE_WAITING;
                }
            }
        }
    }

    private class PerformHeaderClick extends WindowRunnable implements Runnable {
        int mClickMotionPosition;

        @Override
        public void run() {
            // The data has changed since we posted this action to the event
            // queue, bail out before bad things happen.
            if (mDataChanged)
                return;

            if (mAdapter != null && mAdapter.getCount() > 0
                    && mClickMotionPosition != INVALID_POSITION
                    && mClickMotionPosition < mAdapter.getCount() && sameWindow()) {
                final View view = getHeaderAt(mClickMotionPosition);
                // If there is no view then something bad happened, the view
                // probably scrolled off the screen, and we should cancel the
                // click.
                if (view != null) {
                    performHeaderClick(view, headerViewPositionToId(mClickMotionPosition));
                }
            }
        }
    }

    /**
     * A base class for Runnables that will check that their view is still
     * attached to the original window as when the Runnable was created.
     */
    private class WindowRunnable {
        private int mOriginalAttachCount;

        public void rememberWindowAttachCount() {
            mOriginalAttachCount = getWindowAttachCount();
        }

        public boolean sameWindow() {
            return hasWindowFocus() && getWindowAttachCount() == mOriginalAttachCount;
        }
    }

    final class CheckForHeaderTap implements Runnable {
        @Override
        public void run() {
            if (mTouchMode == TOUCH_MODE_DOWN) {
                mTouchMode = TOUCH_MODE_TAP;
                final View header = getHeaderAt(mMotionHeaderPosition);
                if (header != null && !header.hasFocusable()) {
                    if (!mDataChanged) {
                        header.setPressed(true);
                        setPressed(true);
                        refreshDrawableState();

                        final int longPressTimeout = ViewConfiguration.getLongPressTimeout();
                        final boolean longClickable = isLongClickable();

                        if (longClickable) {
                            if (mPendingCheckForLongPress == null) {
                                mPendingCheckForLongPress = new CheckForHeaderLongPress();
                            }
                            mPendingCheckForLongPress.rememberWindowAttachCount();
                            postDelayed(mPendingCheckForLongPress, longPressTimeout);
                        } else {
                            mTouchMode = TOUCH_MODE_DONE_WAITING;
                        }
                    } else {
                        mTouchMode = TOUCH_MODE_DONE_WAITING;
                    }
                }
            }
        }
    }

    /**
     * Constructor called from {@link #CREATOR}
     */
    static class SavedState extends BaseSavedState {
        public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
            @Override
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            @Override
            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };

        boolean areHeadersSticky;

        public SavedState(Parcelable superState) {
            super(superState);
        }

        /**
         * Constructor called from {@link #CREATOR}
         */
        private SavedState(Parcel in) {
            super(in);
            areHeadersSticky = in.readByte() != 0;
        }

        @Override
        public String toString() {
            return "StickyGridHeadersGridView.SavedState{"
                    + Integer.toHexString(System.identityHashCode(this)) + " areHeadersSticky="
                    + areHeadersSticky + "}";
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeByte((byte)(areHeadersSticky ? 1 : 0));
        }
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer eSpace Software Company
Egypt Egypt
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions