/* 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.tonicartos.widget.stickygridheaders; import com.tonicartos.stickygridheaders.R; import com.tonicartos.widget.stickygridheaders.StickyGridHeadersBaseAdapterWrapper.HeaderFillerView; import com.tonicartos.widget.stickygridheaders.StickyGridHeadersBaseAdapterWrapper.ReferenceView; import android.content.Context; import android.content.res.TypedArray; 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; import android.widget.Toast; import java.util.ArrayList; import java.util.List; /** * 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 Toast mToast; private Runnable mTouchModeReset; private int mTouchSlop; private int mVerticalSpacing; protected StickyGridHeadersBaseAdapterWrapper mAdapter; protected boolean mDataChanged; protected int mMotionHeaderPosition; protected int mTouchMode; 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; } TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.StickyGridHeadersGridView); for (int i = 0; i < a.getIndexCount(); i++) { final int attr = a.getIndex(i); if (attr == R.styleable.StickyGridHeadersGridView_areHeadersSticky) { mAreHeadersSticky = a.getBoolean(attr, true); } } a.recycle(); 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; } @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; } @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 = 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); mStickiedHeader.layout(getLeft() + getPaddingLeft(), 0, getRight() - getPaddingRight(), mStickiedHeader.getMeasuredHeight()); } private void reset() { mHeaderBottomPosition = 0; mStickiedHeader = null; mCurrentHeaderId = INVALID_ROW_ID; } private void scrollChanged(int firstVisibleItem) { if (mAdapter == null || mAdapter.getCount() == 0 || !mAreHeadersSticky) { return; } ReferenceView firstItem = (ReferenceView)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) { ReferenceView child = (ReferenceView)super.getChildAt(i); int childDistance; if (mClippingToPadding) { childDistance = child.getTop() - getPaddingTop(); } else { childDistance = child.getTop(); } if (childDistance < 0) { continue; } if (child.getView() instanceof HeaderFillerView && 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(); } } } } /** * Use for interactive debugging on screen. * * @param text Text to toast to see exactly what is happening where. */ private void showToast(String text) { if (mToast == null) { mToast = Toast.makeText(getContext(), text, Toast.LENGTH_SHORT); } else { mToast.setText(text); } mToast.show(); } @Override protected void dispatchDraw(Canvas canvas) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) { scrollChanged(getFirstVisiblePosition()); } // Mask the region where we will draw the header later, but only if we // will draw a header. boolean willDrawStickiedHeader = mStickiedHeader != null && mAreHeadersSticky; int headerHeight = getHeaderHeight(); int top = mHeaderBottomPosition - headerHeight; if (willDrawStickiedHeader) { 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; } int widthMeasureSpec = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY - getPaddingLeft() - getPaddingRight()); int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); header.measure(widthMeasureSpec, heightMeasureSpec); header.layout(getLeft() + getPaddingLeft(), 0, getRight() - getPaddingRight(), frame.getHeight()); mClippingRect.left = getPaddingLeft(); mClippingRect.right = getWidth() - getPaddingRight(); mClippingRect.bottom = frame.getBottom(); mClippingRect.top = frame.getTop(); canvas.save(); canvas.clipRect(mClippingRect); canvas.translate(getPaddingLeft(), frame.getTop()); header.draw(canvas); canvas.restore(); } if (willDrawStickiedHeader) { canvas.restore(); } else { // Done. return; } // Draw stickied header. if (mStickiedHeader.getWidth() != getWidth() - getPaddingLeft() - getPaddingRight()) { int widthMeasureSpec = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY - getPaddingLeft() - getPaddingRight()); int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); mStickiedHeader.measure(widthMeasureSpec, heightMeasureSpec); mStickiedHeader.layout(getLeft() + getPaddingLeft(), 0, getRight() - getPaddingRight(), mStickiedHeader.getHeight()); } 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); canvas.translate(getPaddingLeft(), top); canvas.saveLayerAlpha(0, 0, canvas.getWidth(), canvas.getHeight(), (int)(0xff * (float)mHeaderBottomPosition / headerHeight), Canvas.HAS_ALPHA_LAYER_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)); } } }