From 4acd99f1e216bd6373773eedf9e78a60b4d0c684 Mon Sep 17 00:00:00 2001 From: Mis012 Date: Fri, 20 Dec 2024 00:00:19 +0100 Subject: [PATCH] GridView: import from AOSP --- src/api-impl/android/widget/GridView.java | 1702 ++++++++++++++++++++- 1 file changed, 1679 insertions(+), 23 deletions(-) diff --git a/src/api-impl/android/widget/GridView.java b/src/api-impl/android/widget/GridView.java index 72285065..5a426408 100644 --- a/src/api-impl/android/widget/GridView.java +++ b/src/api-impl/android/widget/GridView.java @@ -1,48 +1,1704 @@ +/* + * Copyright (C) 2007 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 android.widget; import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Rect; import android.util.AttributeSet; +import android.view.Gravity; +import android.view.KeyEvent; +//import android.view.SoundEffectConstants; +import android.view.View; +import android.view.ViewGroup; +//import android.view.animation.GridLayoutAnimationController; +/** + * A view that shows items in two-dimensional scrolling grid. The items in the + * grid come from the {@link ListAdapter} associated with this view. + */ public class GridView extends AbsListView { + public static final int NO_STRETCH = 0; + public static final int STRETCH_SPACING = 1; + public static final int STRETCH_COLUMN_WIDTH = 2; + + public static final int AUTO_FIT = -1; + + private int mNumColumns = AUTO_FIT; + + private int mHorizontalSpacing = 0; + private int mRequestedHorizontalSpacing; + private int mVerticalSpacing = 0; + private int mStretchMode = STRETCH_COLUMN_WIDTH; + private int mColumnWidth; + private int mRequestedColumnWidth; + private int mRequestedNumColumns; + + private View mReferenceView = null; + private View mReferenceViewInSelectedRow = null; + + private int mGravity = Gravity.LEFT; + + private final Rect mTempRect = new Rect(); + public GridView(Context context) { super(context); } - public GridView(Context context, AttributeSet attributeSet) { - super(context, attributeSet); + public GridView(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.gridViewStyle); } - public GridView(Context context, AttributeSet attributeSet, int defStyleAttr) { - super(context, attributeSet, defStyleAttr); - } + public GridView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); - @Override - void fillGap(boolean down) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'fillGap'"); - } + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.GridView, defStyle, 0); - @Override - int findMotionRow(int y) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'findMotionRow'"); - } + int hSpacing = a.getDimensionPixelOffset( + com.android.internal.R.styleable.GridView_horizontalSpacing, 0); + setHorizontalSpacing(hSpacing); - @Override - void setSelectionInt(int position) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setSelectionInt'"); + int vSpacing = a.getDimensionPixelOffset( + com.android.internal.R.styleable.GridView_verticalSpacing, 0); + setVerticalSpacing(vSpacing); + + int index = a.getInt(com.android.internal.R.styleable.GridView_stretchMode, STRETCH_COLUMN_WIDTH); + if (index >= 0) { + setStretchMode(index); + } + + int columnWidth = a.getDimensionPixelOffset(com.android.internal.R.styleable.GridView_columnWidth, -1); + if (columnWidth > 0) { + setColumnWidth(columnWidth); + } + + int numColumns = a.getInt(com.android.internal.R.styleable.GridView_numColumns, 1); + setNumColumns(numColumns); + + index = a.getInt(com.android.internal.R.styleable.GridView_gravity, -1); + if (index >= 0) { + setGravity(index); + } + + a.recycle(); } @Override public ListAdapter getAdapter() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getAdapter'"); + return mAdapter; + } + + /** + * Sets the data behind this GridView. + * + * @param adapter the adapter providing the grid's data + */ + @Override + public void setAdapter(ListAdapter adapter) { + if (null != mAdapter) { + mAdapter.unregisterDataSetObserver(mDataSetObserver); + } + + resetList(); + mRecycler.clear(); + mAdapter = adapter; + + mOldSelectedPosition = INVALID_POSITION; + mOldSelectedRowId = INVALID_ROW_ID; + + if (mAdapter != null) { + mOldItemCount = mItemCount; + mItemCount = mAdapter.getCount(); + mDataChanged = true; + checkFocus(); + + mDataSetObserver = new AdapterDataSetObserver(); + mAdapter.registerDataSetObserver(mDataSetObserver); + + mRecycler.setViewTypeCount(mAdapter.getViewTypeCount()); + + int position; + if (mStackFromBottom) { + position = lookForSelectablePosition(mItemCount - 1, false); + } else { + position = lookForSelectablePosition(0, true); + } + setSelectedPositionInt(position); + setNextSelectedPositionInt(position); + checkSelectionChanged(); + } else { + checkFocus(); + // Nothing selected + checkSelectionChanged(); + } + + requestLayout(); } + @Override + int lookForSelectablePosition(int position, boolean lookDown) { + final ListAdapter adapter = mAdapter; + if (adapter == null || isInTouchMode()) { + return INVALID_POSITION; + } + + if (position < 0 || position >= mItemCount) { + return INVALID_POSITION; + } + return position; + } + + /** + * {@inheritDoc} + */ + @Override + void fillGap(boolean down) { + final int numColumns = mNumColumns; + final int verticalSpacing = mVerticalSpacing; + + final int count = getChildCount(); + + if (down) { + final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + verticalSpacing : getListPaddingTop(); + int position = mFirstPosition + count; + if (mStackFromBottom) { + position += numColumns - 1; + } + fillDown(position, startOffset); + correctTooHigh(numColumns, verticalSpacing, getChildCount()); + } else { + final int startOffset = count > 0 ? getChildAt(0).getTop() - verticalSpacing : getHeight() - getListPaddingBottom(); + int position = mFirstPosition; + if (!mStackFromBottom) { + position -= numColumns; + } else { + position--; + } + fillUp(position, startOffset); + correctTooLow(numColumns, verticalSpacing, getChildCount()); + } + } + + /** + * Fills the list from pos down to the end of the list view. + * + * @param pos The first position to put in the list + * + * @param nextTop The location where the top of the item associated with pos + * should be drawn + * + * @return The view that is currently selected, if it happens to be in the + * range that we draw. + */ + private View fillDown(int pos, int nextTop) { + View selectedView = null; + + final int end = (getBottom() - getTop()) - mListPadding.bottom; + + while (nextTop < end && pos < mItemCount) { + View temp = makeRow(pos, nextTop, true); + if (temp != null) { + selectedView = temp; + } + + nextTop = mReferenceView.getBottom() + mVerticalSpacing; + + pos += mNumColumns; + } + + return selectedView; + } + + private View makeRow(int startPos, int y, boolean flow) { + int last; + int nextLeft = mListPadding.left; + + final int columnWidth = mColumnWidth; + final int horizontalSpacing = mHorizontalSpacing; + + if (!mStackFromBottom) { + last = Math.min(startPos + mNumColumns, mItemCount); + } else { + last = startPos + 1; + startPos = Math.max(0, startPos - mNumColumns + 1); + + if (last - startPos < mNumColumns) { + nextLeft += (mNumColumns - (last - startPos)) * (columnWidth + horizontalSpacing); + } + } + + View selectedView = null; + + final boolean hasFocus = shouldShowSelector(); + final boolean inClick = touchModeDrawsInPressedState(); + final int selectedPosition = mSelectedPosition; + + mReferenceView = null; + + for (int pos = startPos; pos < last; pos++) { + // is this the selected item? + boolean selected = pos == selectedPosition; + // does the list view have focus or contain focus + + final int where = flow ? -1 : pos - startPos; + final View child = makeAndAddView(pos, y, flow, nextLeft, selected, where); + mReferenceView = child; + + nextLeft += columnWidth; + if (pos < last - 1) { + nextLeft += horizontalSpacing; + } + + if (selected && (hasFocus || inClick)) { + selectedView = child; + } + } + + if (selectedView != null) { + mReferenceViewInSelectedRow = mReferenceView; + } + + return selectedView; + } + + /** + * Fills the list from pos up to the top of the list view. + * + * @param pos The first position to put in the list + * + * @param nextBottom The location where the bottom of the item associated + * with pos should be drawn + * + * @return The view that is currently selected + */ + private View fillUp(int pos, int nextBottom) { + View selectedView = null; + + final int end = mListPadding.top; + + while (nextBottom > end && pos >= 0) { + + View temp = makeRow(pos, nextBottom, false); + if (temp != null) { + selectedView = temp; + } + + nextBottom = mReferenceView.getTop() - mVerticalSpacing; + + mFirstPosition = pos; + + pos -= mNumColumns; + } + + if (mStackFromBottom) { + mFirstPosition = Math.max(0, pos + 1); + } + + return selectedView; + } + + /** + * Fills the list from top to bottom, starting with mFirstPosition + * + * @param nextTop The location where the top of the first item should be + * drawn + * + * @return The view that is currently selected + */ + private View fillFromTop(int nextTop) { + mFirstPosition = Math.min(mFirstPosition, mSelectedPosition); + mFirstPosition = Math.min(mFirstPosition, mItemCount - 1); + if (mFirstPosition < 0) { + mFirstPosition = 0; + } + mFirstPosition -= mFirstPosition % mNumColumns; + return fillDown(mFirstPosition, nextTop); + } + + private View fillFromBottom(int lastPosition, int nextBottom) { + lastPosition = Math.max(lastPosition, mSelectedPosition); + lastPosition = Math.min(lastPosition, mItemCount - 1); + + final int invertedPosition = mItemCount - 1 - lastPosition; + lastPosition = mItemCount - 1 - (invertedPosition - (invertedPosition % mNumColumns)); + + return fillUp(lastPosition, nextBottom); + } + + private View fillSelection(int childrenTop, int childrenBottom) { + final int selectedPosition = reconcileSelectedPosition(); + final int numColumns = mNumColumns; + final int verticalSpacing = mVerticalSpacing; + + int rowStart; + int rowEnd = -1; + + if (!mStackFromBottom) { + rowStart = selectedPosition - (selectedPosition % numColumns); + } else { + final int invertedSelection = mItemCount - 1 - selectedPosition; + + rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); + rowStart = Math.max(0, rowEnd - numColumns + 1); + } + + final int fadingEdgeLength = getVerticalFadingEdgeLength(); + final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart); + + final View sel = makeRow(mStackFromBottom ? rowEnd : rowStart, topSelectionPixel, true); + mFirstPosition = rowStart; + + final View referenceView = mReferenceView; + + if (!mStackFromBottom) { + fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing); + pinToBottom(childrenBottom); + fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing); + adjustViewsUpOrDown(); + } else { + final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, + fadingEdgeLength, numColumns, rowStart); + final int offset = bottomSelectionPixel - referenceView.getBottom(); + offsetChildrenTopAndBottom(offset); + fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing); + pinToTop(childrenTop); + fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing); + adjustViewsUpOrDown(); + } + + return sel; + } + + private void pinToTop(int childrenTop) { + if (mFirstPosition == 0) { + final int top = getChildAt(0).getTop(); + final int offset = childrenTop - top; + if (offset < 0) { + offsetChildrenTopAndBottom(offset); + } + } + } + + private void pinToBottom(int childrenBottom) { + final int count = getChildCount(); + if (mFirstPosition + count == mItemCount) { + final int bottom = getChildAt(count - 1).getBottom(); + final int offset = childrenBottom - bottom; + if (offset > 0) { + offsetChildrenTopAndBottom(offset); + } + } + } + + @Override + int findMotionRow(int y) { + final int childCount = getChildCount(); + if (childCount > 0) { + + final int numColumns = mNumColumns; + if (!mStackFromBottom) { + for (int i = 0; i < childCount; i += numColumns) { + if (y <= getChildAt(i).getBottom()) { + return mFirstPosition + i; + } + } + } else { + for (int i = childCount - 1; i >= 0; i -= numColumns) { + if (y >= getChildAt(i).getTop()) { + return mFirstPosition + i; + } + } + } + + return mFirstPosition + childCount - 1; + } + return INVALID_POSITION; + } + + /** + * Layout during a scroll that results from tracking motion events. Places + * the mMotionPosition view at the offset specified by mMotionViewTop, and + * then build surrounding views from there. + * + * @param position the position at which to start filling + * @param top the top of the view at that position + * @return The selected view, or null if the selected view is outside the + * visible area. + */ + private View fillSpecific(int position, int top) { + final int numColumns = mNumColumns; + + int motionRowStart; + int motionRowEnd = -1; + + if (!mStackFromBottom) { + motionRowStart = position - (position % numColumns); + } else { + final int invertedSelection = mItemCount - 1 - position; + + motionRowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); + motionRowStart = Math.max(0, motionRowEnd - numColumns + 1); + } + + final View temp = makeRow(mStackFromBottom ? motionRowEnd : motionRowStart, top, true); + + // Possibly changed again in fillUp if we add rows above this one. + mFirstPosition = motionRowStart; + + final View referenceView = mReferenceView; + final int verticalSpacing = mVerticalSpacing; + + View above; + View below; + + if (!mStackFromBottom) { + above = fillUp(motionRowStart - numColumns, referenceView.getTop() - verticalSpacing); + adjustViewsUpOrDown(); + below = fillDown(motionRowStart + numColumns, referenceView.getBottom() + verticalSpacing); + // Check if we have dragged the bottom of the grid too high + final int childCount = getChildCount(); + if (childCount > 0) { + correctTooHigh(numColumns, verticalSpacing, childCount); + } + } else { + below = fillDown(motionRowEnd + numColumns, referenceView.getBottom() + verticalSpacing); + adjustViewsUpOrDown(); + above = fillUp(motionRowStart - 1, referenceView.getTop() - verticalSpacing); + // Check if we have dragged the bottom of the grid too high + final int childCount = getChildCount(); + if (childCount > 0) { + correctTooLow(numColumns, verticalSpacing, childCount); + } + } + + if (temp != null) { + return temp; + } else if (above != null) { + return above; + } else { + return below; + } + } + + private void correctTooHigh(int numColumns, int verticalSpacing, int childCount) { + // First see if the last item is visible + final int lastPosition = mFirstPosition + childCount - 1; + if (lastPosition == mItemCount - 1 && childCount > 0) { + // Get the last child ... + final View lastChild = getChildAt(childCount - 1); + + // ... and its bottom edge + final int lastBottom = lastChild.getBottom(); + // This is bottom of our drawable area + final int end = (getBottom() - getTop()) - mListPadding.bottom; + + // This is how far the bottom edge of the last view is from the bottom of the + // drawable area + int bottomOffset = end - lastBottom; + + final View firstChild = getChildAt(0); + final int firstTop = firstChild.getTop(); + + // Make sure we are 1) Too high, and 2) Either there are more rows above the + // first row or the first row is scrolled off the top of the drawable area + if (bottomOffset > 0 && (mFirstPosition > 0 || firstTop < mListPadding.top)) { + if (mFirstPosition == 0) { + // Don't pull the top too far down + bottomOffset = Math.min(bottomOffset, mListPadding.top - firstTop); + } + + // Move everything down + offsetChildrenTopAndBottom(bottomOffset); + if (mFirstPosition > 0) { + // Fill the gap that was opened above mFirstPosition with more rows, if + // possible + fillUp(mFirstPosition - (mStackFromBottom ? 1 : numColumns), + firstChild.getTop() - verticalSpacing); + // Close up the remaining gap + adjustViewsUpOrDown(); + } + } + } + } + + private void correctTooLow(int numColumns, int verticalSpacing, int childCount) { + if (mFirstPosition == 0 && childCount > 0) { + // Get the first child ... + final View firstChild = getChildAt(0); + + // ... and its top edge + final int firstTop = firstChild.getTop(); + + // This is top of our drawable area + final int start = mListPadding.top; + + // This is bottom of our drawable area + final int end = (getBottom() - getTop()) - mListPadding.bottom; + + // This is how far the top edge of the first view is from the top of the + // drawable area + int topOffset = firstTop - start; + final View lastChild = getChildAt(childCount - 1); + final int lastBottom = lastChild.getBottom(); + final int lastPosition = mFirstPosition + childCount - 1; + + // Make sure we are 1) Too low, and 2) Either there are more rows below the + // last row or the last row is scrolled off the bottom of the drawable area + if (topOffset > 0 && (lastPosition < mItemCount - 1 || lastBottom > end)) { + if (lastPosition == mItemCount - 1) { + // Don't pull the bottom too far up + topOffset = Math.min(topOffset, lastBottom - end); + } + + // Move everything up + offsetChildrenTopAndBottom(-topOffset); + if (lastPosition < mItemCount - 1) { + // Fill the gap that was opened below the last position with more rows, if + // possible + fillDown(lastPosition + (!mStackFromBottom ? 1 : numColumns), + lastChild.getBottom() + verticalSpacing); + // Close up the remaining gap + adjustViewsUpOrDown(); + } + } + } + } + + /** + * Fills the grid based on positioning the new selection at a specific + * location. The selection may be moved so that it does not intersect the + * faded edges. The grid is then filled upwards and downwards from there. + * + * @param selectedTop Where the selected item should be + * @param childrenTop Where to start drawing children + * @param childrenBottom Last pixel where children can be drawn + * @return The view that currently has selection + */ + private View fillFromSelection(int selectedTop, int childrenTop, int childrenBottom) { + final int fadingEdgeLength = getVerticalFadingEdgeLength(); + final int selectedPosition = mSelectedPosition; + final int numColumns = mNumColumns; + final int verticalSpacing = mVerticalSpacing; + + int rowStart; + int rowEnd = -1; + + if (!mStackFromBottom) { + rowStart = selectedPosition - (selectedPosition % numColumns); + } else { + int invertedSelection = mItemCount - 1 - selectedPosition; + + rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); + rowStart = Math.max(0, rowEnd - numColumns + 1); + } + + View sel; + View referenceView; + + int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart); + int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength, + numColumns, rowStart); + + sel = makeRow(mStackFromBottom ? rowEnd : rowStart, selectedTop, true); + // Possibly changed again in fillUp if we add rows above this one. + mFirstPosition = rowStart; + + referenceView = mReferenceView; + adjustForTopFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel); + adjustForBottomFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel); + + if (!mStackFromBottom) { + fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing); + adjustViewsUpOrDown(); + fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing); + } else { + fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing); + adjustViewsUpOrDown(); + fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing); + } + + return sel; + } + + /** + * Calculate the bottom-most pixel we can draw the selection into + * + * @param childrenBottom Bottom pixel were children can be drawn + * @param fadingEdgeLength Length of the fading edge in pixels, if present + * @param numColumns Number of columns in the grid + * @param rowStart The start of the row that will contain the selection + * @return The bottom-most pixel we can draw the selection into + */ + private int getBottomSelectionPixel(int childrenBottom, int fadingEdgeLength, + int numColumns, int rowStart) { + // Last pixel we can draw the selection into + int bottomSelectionPixel = childrenBottom; + if (rowStart + numColumns - 1 < mItemCount - 1) { + bottomSelectionPixel -= fadingEdgeLength; + } + return bottomSelectionPixel; + } + + /** + * Calculate the top-most pixel we can draw the selection into + * + * @param childrenTop Top pixel were children can be drawn + * @param fadingEdgeLength Length of the fading edge in pixels, if present + * @param rowStart The start of the row that will contain the selection + * @return The top-most pixel we can draw the selection into + */ + private int getTopSelectionPixel(int childrenTop, int fadingEdgeLength, int rowStart) { + // first pixel we can draw the selection into + int topSelectionPixel = childrenTop; + if (rowStart > 0) { + topSelectionPixel += fadingEdgeLength; + } + return topSelectionPixel; + } + + /** + * Move all views upwards so the selected row does not interesect the bottom + * fading edge (if necessary). + * + * @param childInSelectedRow A child in the row that contains the selection + * @param topSelectionPixel The topmost pixel we can draw the selection into + * @param bottomSelectionPixel The bottommost pixel we can draw the + * selection into + */ + private void adjustForBottomFadingEdge(View childInSelectedRow, + int topSelectionPixel, int bottomSelectionPixel) { + // Some of the newly selected item extends below the bottom of the + // list + if (childInSelectedRow.getBottom() > bottomSelectionPixel) { + + // Find space available above the selection into which we can + // scroll upwards + int spaceAbove = childInSelectedRow.getTop() - topSelectionPixel; + + // Find space required to bring the bottom of the selected item + // fully into view + int spaceBelow = childInSelectedRow.getBottom() - bottomSelectionPixel; + int offset = Math.min(spaceAbove, spaceBelow); + + // Now offset the selected item to get it into view + offsetChildrenTopAndBottom(-offset); + } + } + + /** + * Move all views upwards so the selected row does not interesect the top + * fading edge (if necessary). + * + * @param childInSelectedRow A child in the row that contains the selection + * @param topSelectionPixel The topmost pixel we can draw the selection into + * @param bottomSelectionPixel The bottommost pixel we can draw the + * selection into + */ + private void adjustForTopFadingEdge(View childInSelectedRow, + int topSelectionPixel, int bottomSelectionPixel) { + // Some of the newly selected item extends above the top of the list + if (childInSelectedRow.getTop() < topSelectionPixel) { + // Find space required to bring the top of the selected item + // fully into view + int spaceAbove = topSelectionPixel - childInSelectedRow.getTop(); + + // Find space available below the selection into which we can + // scroll downwards + int spaceBelow = bottomSelectionPixel - childInSelectedRow.getBottom(); + int offset = Math.min(spaceAbove, spaceBelow); + + // Now offset the selected item to get it into view + offsetChildrenTopAndBottom(offset); + } + } + + /** + * Fills the grid based on positioning the new selection relative to the old + * selection. The new selection will be placed at, above, or below the + * location of the new selection depending on how the selection is moving. + * The selection will then be pinned to the visible part of the screen, + * excluding the edges that are faded. The grid is then filled upwards and + * downwards from there. + * + * @param delta Which way we are moving + * @param childrenTop Where to start drawing children + * @param childrenBottom Last pixel where children can be drawn + * @return The view that currently has selection + */ + private View moveSelection(int delta, int childrenTop, int childrenBottom) { + final int fadingEdgeLength = getVerticalFadingEdgeLength(); + final int selectedPosition = mSelectedPosition; + final int numColumns = mNumColumns; + final int verticalSpacing = mVerticalSpacing; + + int oldRowStart; + int rowStart; + int rowEnd = -1; + + if (!mStackFromBottom) { + oldRowStart = (selectedPosition - delta) - ((selectedPosition - delta) % numColumns); + + rowStart = selectedPosition - (selectedPosition % numColumns); + } else { + int invertedSelection = mItemCount - 1 - selectedPosition; + + rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); + rowStart = Math.max(0, rowEnd - numColumns + 1); + + invertedSelection = mItemCount - 1 - (selectedPosition - delta); + oldRowStart = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); + oldRowStart = Math.max(0, oldRowStart - numColumns + 1); + } + + final int rowDelta = rowStart - oldRowStart; + + final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart); + final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength, + numColumns, rowStart); + + // Possibly changed again in fillUp if we add rows above this one. + mFirstPosition = rowStart; + + View sel; + View referenceView; + + if (rowDelta > 0) { + /* + * Case 1: Scrolling down. + */ + + final int oldBottom = mReferenceViewInSelectedRow == null ? 0 : mReferenceViewInSelectedRow.getBottom(); + + sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldBottom + verticalSpacing, true); + referenceView = mReferenceView; + + adjustForBottomFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel); + } else if (rowDelta < 0) { + /* + * Case 2: Scrolling up. + */ + final int oldTop = mReferenceViewInSelectedRow == null ? 0 : mReferenceViewInSelectedRow.getTop(); + + sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldTop - verticalSpacing, false); + referenceView = mReferenceView; + + adjustForTopFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel); + } else { + /* + * Keep selection where it was + */ + final int oldTop = mReferenceViewInSelectedRow == null ? 0 : mReferenceViewInSelectedRow.getTop(); + + sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldTop, true); + referenceView = mReferenceView; + } + + if (!mStackFromBottom) { + fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing); + adjustViewsUpOrDown(); + fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing); + } else { + fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing); + adjustViewsUpOrDown(); + fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing); + } + + return sel; + } + + private void determineColumns(int availableSpace) { + final int requestedHorizontalSpacing = mRequestedHorizontalSpacing; + final int stretchMode = mStretchMode; + final int requestedColumnWidth = mRequestedColumnWidth; + + if (mRequestedNumColumns == AUTO_FIT) { + if (requestedColumnWidth > 0) { + // Client told us to pick the number of columns + mNumColumns = (availableSpace + requestedHorizontalSpacing) / + (requestedColumnWidth + requestedHorizontalSpacing); + } else { + // Just make up a number if we don't have enough info + mNumColumns = 2; + } + } else { + // We picked the columns + mNumColumns = mRequestedNumColumns; + } + + if (mNumColumns <= 0) { + mNumColumns = 1; + } + + switch (stretchMode) { + case NO_STRETCH: + // Nobody stretches + mColumnWidth = requestedColumnWidth; + mHorizontalSpacing = requestedHorizontalSpacing; + break; + + default: + int spaceLeftOver = availableSpace - (mNumColumns * requestedColumnWidth) - + ((mNumColumns - 1) * requestedHorizontalSpacing); + switch (stretchMode) { + case STRETCH_COLUMN_WIDTH: + // Stretch the columns + mColumnWidth = requestedColumnWidth + spaceLeftOver / mNumColumns; + mHorizontalSpacing = requestedHorizontalSpacing; + break; + + case STRETCH_SPACING: + // Stretch the spacing between columns + mColumnWidth = requestedColumnWidth; + if (mNumColumns > 1) { + mHorizontalSpacing = requestedHorizontalSpacing + + spaceLeftOver / (mNumColumns - 1); + } else { + mHorizontalSpacing = requestedHorizontalSpacing + spaceLeftOver; + } + break; + } + + break; + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Sets up mListPadding + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + if (widthMode == MeasureSpec.UNSPECIFIED) { + if (mColumnWidth > 0) { + widthSize = mColumnWidth + mListPadding.left + mListPadding.right; + } else { + widthSize = mListPadding.left + mListPadding.right; + } + widthSize += getVerticalScrollbarWidth(); + } + + int childWidth = widthSize - mListPadding.left - mListPadding.right; + determineColumns(childWidth); + + int childHeight = 0; + + mItemCount = mAdapter == null ? 0 : mAdapter.getCount(); + final int count = mItemCount; + if (count > 0) { + final View child = obtainView(0, mIsScrap); + final int childViewType = mAdapter.getItemViewType(0); + + AbsListView.LayoutParams lp = (AbsListView.LayoutParams)child.getLayoutParams(); + if (lp == null) { + lp = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, 0); + child.setLayoutParams(lp); + } + lp.viewType = childViewType; + + final int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec, + mListPadding.left + mListPadding.right, lp.width); + + int lpHeight = lp.height; + + int childHeightSpec; + if (lpHeight > 0) { + childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); + } else { + childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + } + + child.measure(childWidthSpec, childHeightSpec); + childHeight = child.getMeasuredHeight(); + + if (mRecycler.shouldRecycleViewType(childViewType)) { + mRecycler.addScrapView(child, -1); + } + } + + if (heightMode == MeasureSpec.UNSPECIFIED) { + heightSize = mListPadding.top + mListPadding.bottom + childHeight + + getVerticalFadingEdgeLength() * 2; + } + + if (heightMode == MeasureSpec.AT_MOST) { + int ourSize = mListPadding.top + mListPadding.bottom; + + final int numColumns = mNumColumns; + for (int i = 0; i < count; i += numColumns) { + ourSize += childHeight; + if (i + numColumns < count) { + ourSize += mVerticalSpacing; + } + if (ourSize >= heightSize) { + ourSize = heightSize; + break; + } + } + heightSize = ourSize; + } + + setMeasuredDimension(widthSize, heightSize); + mWidthMeasureSpec = widthMeasureSpec; + } + + /*@Override + protected void attachLayoutAnimationParameters(View child, + ViewGroup.LayoutParams params, int index, int count) { + + GridLayoutAnimationController.AnimationParameters animationParams = + (GridLayoutAnimationController.AnimationParameters)params.layoutAnimationParameters; + + if (animationParams == null) { + animationParams = new GridLayoutAnimationController.AnimationParameters(); + params.layoutAnimationParameters = animationParams; + } + + animationParams.count = count; + animationParams.index = index; + animationParams.columnsCount = mNumColumns; + animationParams.rowsCount = count / mNumColumns; + + if (!mStackFromBottom) { + animationParams.column = index % mNumColumns; + animationParams.row = index / mNumColumns; + } else { + final int invertedIndex = count - 1 - index; + + animationParams.column = mNumColumns - 1 - (invertedIndex % mNumColumns); + animationParams.row = animationParams.rowsCount - 1 - invertedIndex / mNumColumns; + } + }*/ + + @Override + protected void layoutChildren() { + final boolean blockLayoutRequests = mBlockLayoutRequests; + if (!blockLayoutRequests) { + mBlockLayoutRequests = true; + } + + try { + super.layoutChildren(); + + invalidate(); + + if (mAdapter == null) { + resetList(); + invokeOnItemScrollListener(); + return; + } + + final int childrenTop = mListPadding.top; + final int childrenBottom = getBottom() - getTop() - mListPadding.bottom; + + int childCount = getChildCount(); + int index; + int delta = 0; + + View sel; + View oldSel = null; + View oldFirst = null; + View newSel = null; + + // Remember stuff we will need down below + switch (mLayoutMode) { + case LAYOUT_SET_SELECTION: + index = mNextSelectedPosition - mFirstPosition; + if (index >= 0 && index < childCount) { + newSel = getChildAt(index); + } + break; + case LAYOUT_FORCE_TOP: + case LAYOUT_FORCE_BOTTOM: + case LAYOUT_SPECIFIC: + case LAYOUT_SYNC: + break; + case LAYOUT_MOVE_SELECTION: + if (mNextSelectedPosition >= 0) { + delta = mNextSelectedPosition - mSelectedPosition; + } + break; + default: + // Remember the previously selected view + index = mSelectedPosition - mFirstPosition; + if (index >= 0 && index < childCount) { + oldSel = getChildAt(index); + } + + // Remember the previous first child + oldFirst = getChildAt(0); + } + + boolean dataChanged = mDataChanged; + if (dataChanged) { + handleDataChanged(); + } + + // Handle the empty set by removing all views that are visible + // and calling it a day + if (mItemCount == 0) { + resetList(); + invokeOnItemScrollListener(); + return; + } + + setSelectedPositionInt(mNextSelectedPosition); + + // Pull all children into the RecycleBin. + // These views will be reused if possible + final int firstPosition = mFirstPosition; + final RecycleBin recycleBin = mRecycler; + + if (dataChanged) { + for (int i = 0; i < childCount; i++) { + recycleBin.addScrapView(getChildAt(i), firstPosition+i); + } + } else { + recycleBin.fillActiveViews(childCount, firstPosition); + } + + // Clear out old views + // removeAllViewsInLayout(); + //detachAllViewsFromParent(); + for (int i=getChildCount() - 1; i >= 0; i--) { + detachViewFromParent(i); + } + + switch (mLayoutMode) { + case LAYOUT_SET_SELECTION: + if (newSel != null) { + sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom); + } else { + sel = fillSelection(childrenTop, childrenBottom); + } + break; + case LAYOUT_FORCE_TOP: + mFirstPosition = 0; + sel = fillFromTop(childrenTop); + adjustViewsUpOrDown(); + break; + case LAYOUT_FORCE_BOTTOM: + sel = fillUp(mItemCount - 1, childrenBottom); + adjustViewsUpOrDown(); + break; + case LAYOUT_SPECIFIC: + sel = fillSpecific(mSelectedPosition, mSpecificTop); + break; + case LAYOUT_SYNC: + sel = fillSpecific(mSyncPosition, mSpecificTop); + break; + case LAYOUT_MOVE_SELECTION: + // Move the selection relative to its old position + sel = moveSelection(delta, childrenTop, childrenBottom); + break; + default: + if (childCount == 0) { + if (!mStackFromBottom) { + setSelectedPositionInt(0); + sel = fillFromTop(childrenTop); + } else { + final int last = mItemCount - 1; + setSelectedPositionInt(last); + sel = fillFromBottom(last, childrenBottom); + } + } else { + if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) { + sel = fillSpecific(mSelectedPosition, oldSel == null ? childrenTop : oldSel.getTop()); + } else if (mFirstPosition < mItemCount) { + sel = fillSpecific(mFirstPosition, oldFirst == null ? childrenTop : oldFirst.getTop()); + } else { + sel = fillSpecific(0, childrenTop); + } + } + break; + } + + // Flush any cached views that did not get reused above + recycleBin.scrapActiveViews(); + + if (sel != null) { + positionSelector(INVALID_POSITION, sel); + mSelectedTop = sel.getTop(); + } else { + mSelectedTop = 0; + mSelectorRect.setEmpty(); + } + + mLayoutMode = LAYOUT_NORMAL; + mDataChanged = false; + mNeedSync = false; + setNextSelectedPositionInt(mSelectedPosition); + + updateScrollIndicators(); + + if (mItemCount > 0) { + checkSelectionChanged(); + } + + invokeOnItemScrollListener(); + } finally { + if (!blockLayoutRequests) { + mBlockLayoutRequests = false; + } + } + } + + /** + * Obtain the view and add it to our list of children. The view can be made + * fresh, converted from an unused view, or used as is if it was in the + * recycle bin. + * + * @param position Logical position in the list + * @param y Top or bottom edge of the view to add + * @param flow if true, align top edge to y. If false, align bottom edge to + * y. + * @param childrenLeft Left edge where children should be positioned + * @param selected Is this position selected? + * @param where to add new item in the list + * @return View that was added + */ + private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, + boolean selected, int where) { + View child; + + if (!mDataChanged) { + // Try to use an exsiting view for this position + child = mRecycler.getActiveView(position); + if (child != null) { + // Found it -- we're using an existing child + // This just needs to be positioned + setupChild(child, position, y, flow, childrenLeft, selected, true, where); + return child; + } + } + + // Make a new view for this position, or convert an unused view if + // possible + child = obtainView(position, mIsScrap); + + // This needs to be positioned and measured + setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0], where); + + return child; + } + + /** + * Add a view as a child and make sure it is measured (if necessary) and + * positioned properly. + * + * @param child The view to add + * @param position The position of the view + * @param y The y position relative to which this view will be positioned + * @param flow if true, align top edge to y. If false, align bottom edge + * to y. + * @param childrenLeft Left edge where children should be positioned + * @param selected Is this position selected? + * @param recycled Has this view been pulled from the recycle bin? If so it + * does not need to be remeasured. + * @param where Where to add the item in the list + * + */ + private void setupChild(View child, int position, int y, boolean flow, int childrenLeft, + boolean selected, boolean recycled, int where) { + boolean isSelected = selected && shouldShowSelector(); + + final boolean updateChildSelected = isSelected != child.isSelected(); + boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested(); + + // Respect layout params that are already in the view. Otherwise make + // some up... + AbsListView.LayoutParams p = (AbsListView.LayoutParams)child.getLayoutParams(); + if (p == null) { + p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, 0); + } + p.viewType = mAdapter.getItemViewType(position); + + if (recycled) { + attachViewToParent(child, where, p); + } else { + addViewInLayout(child, where, p, true); + } + + if (updateChildSelected) { + child.setSelected(isSelected); + if (isSelected) { + requestFocus(); + } + } + + if (needToMeasure) { + int childHeightSpec = ViewGroup.getChildMeasureSpec( + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, p.height); + + int childWidthSpec = ViewGroup.getChildMeasureSpec( + MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width); + child.measure(childWidthSpec, childHeightSpec); + } else { + cleanupLayoutState(child); + } + + final int w = child.getMeasuredWidth(); + final int h = child.getMeasuredHeight(); + + int childLeft; + final int childTop = flow ? y : y - h; + + switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + case Gravity.LEFT: + childLeft = childrenLeft; + break; + case Gravity.CENTER_HORIZONTAL: + childLeft = childrenLeft + ((mColumnWidth - w) / 2); + break; + case Gravity.RIGHT: + childLeft = childrenLeft + mColumnWidth - w; + break; + default: + childLeft = childrenLeft; + break; + } + + if (needToMeasure) { + final int childRight = childLeft + w; + final int childBottom = childTop + h; + child.layout(childLeft, childTop, childRight, childBottom); + } else { + child.offsetLeftAndRight(childLeft - child.getLeft()); + child.offsetTopAndBottom(childTop - child.getTop()); + } + } + + /** + * Sets the currently selected item + * + * @param position Index (starting at 0) of the data item to be selected. + * + * If in touch mode, the item will not be selected but it will still be positioned + * appropriately. + */ @Override public void setSelection(int position) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'setSelection'"); + if (!isInTouchMode()) { + setNextSelectedPositionInt(position); + } else { + mResurrectToPosition = position; + } + mLayoutMode = LAYOUT_SET_SELECTION; + requestLayout(); + } + + /** + * Makes the item at the supplied position selected. + * + * @param position the position of the new selection + */ + @Override + void setSelectionInt(int position) { + mBlockLayoutRequests = true; + setNextSelectedPositionInt(position); + layoutChildren(); + + mBlockLayoutRequests = false; + } + + /** + * Scrolls up or down by the number of items currently present on screen. + * + * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} + * @return whether selection was moved + */ + boolean pageScroll(int direction) { + int nextPage = -1; + + if (direction == FOCUS_UP) { + nextPage = Math.max(0, mSelectedPosition - getChildCount() - 1); + } else if (direction == FOCUS_DOWN) { + nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount() - 1); + } + + if (nextPage >= 0) { + setSelectionInt(nextPage); + return true; + } + + return false; + } + + /** + * Go to the last or first item if possible. + * + * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}. + * + * @return Whether selection was moved. + */ + boolean fullScroll(int direction) { + boolean moved = false; + if (direction == FOCUS_UP) { + mLayoutMode = LAYOUT_SET_SELECTION; + setSelectionInt(0); + moved = true; + } else if (direction == FOCUS_DOWN) { + mLayoutMode = LAYOUT_SET_SELECTION; + setSelectionInt(mItemCount - 1); + moved = true; + } + + return moved; + } + + /** + * Scrolls to the next or previous item, horizontally or vertically. + * + * @param direction either {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, + * {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} + * + * @return whether selection was moved + */ + boolean arrowScroll(int direction) { + final int selectedPosition = mSelectedPosition; + final int numColumns = mNumColumns; + + int startOfRowPos; + int endOfRowPos; + + boolean moved = false; + + if (!mStackFromBottom) { + startOfRowPos = (selectedPosition / numColumns) * numColumns; + endOfRowPos = Math.min(startOfRowPos + numColumns - 1, mItemCount - 1); + } else { + final int invertedSelection = mItemCount - 1 - selectedPosition; + endOfRowPos = mItemCount - 1 - (invertedSelection / numColumns) * numColumns; + startOfRowPos = Math.max(0, endOfRowPos - numColumns + 1); + } + + switch (direction) { + case FOCUS_UP: + if (startOfRowPos > 0) { + mLayoutMode = LAYOUT_MOVE_SELECTION; + setSelectionInt(Math.max(0, selectedPosition - numColumns)); + moved = true; + } + break; + case FOCUS_DOWN: + if (endOfRowPos < mItemCount - 1) { + mLayoutMode = LAYOUT_MOVE_SELECTION; + setSelectionInt(Math.min(selectedPosition + numColumns, mItemCount - 1)); + moved = true; + } + break; + case FOCUS_LEFT: + if (selectedPosition > startOfRowPos) { + mLayoutMode = LAYOUT_MOVE_SELECTION; + setSelectionInt(selectedPosition - 1); + moved = true; + } + break; + case FOCUS_RIGHT: + if (selectedPosition < endOfRowPos) { + mLayoutMode = LAYOUT_MOVE_SELECTION; + setSelectionInt(selectedPosition + 1); + moved = true; + } + break; + } + + if (moved) { + //playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); + } + + return moved; + } + + @Override + protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + + int closestChildIndex = -1; + if (gainFocus && previouslyFocusedRect != null) { + previouslyFocusedRect.offset(getScrollX(), getScrollY()); + + // figure out which item should be selected based on previously + // focused rect + Rect otherRect = mTempRect; + int minDistance = Integer.MAX_VALUE; + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + // only consider view's on appropriate edge of grid + if (!isCandidateSelection(i, direction)) { + continue; + } + + final View other = getChildAt(i); + other.getDrawingRect(otherRect); + offsetDescendantRectToMyCoords(other, otherRect); + int distance = getDistance(previouslyFocusedRect, otherRect, direction); + + if (distance < minDistance) { + minDistance = distance; + closestChildIndex = i; + } + } + } + + if (closestChildIndex >= 0) { + setSelection(closestChildIndex + mFirstPosition); + } else { + requestLayout(); + } + } + + /** + * Is childIndex a candidate for next focus given the direction the focus + * change is coming from? + * @param childIndex The index to check. + * @param direction The direction, one of + * {FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT} + * @return Whether childIndex is a candidate. + */ + private boolean isCandidateSelection(int childIndex, int direction) { + final int count = getChildCount(); + final int invertedIndex = count - 1 - childIndex; + + int rowStart; + int rowEnd; + + if (!mStackFromBottom) { + rowStart = childIndex - (childIndex % mNumColumns); + rowEnd = Math.max(rowStart + mNumColumns - 1, count); + } else { + rowEnd = count - 1 - (invertedIndex - (invertedIndex % mNumColumns)); + rowStart = Math.max(0, rowEnd - mNumColumns + 1); + } + + switch (direction) { + case View.FOCUS_RIGHT: + // coming from left, selection is only valid if it is on left + // edge + return childIndex == rowStart; + case View.FOCUS_DOWN: + // coming from top; only valid if in top row + return rowStart == 0; + case View.FOCUS_LEFT: + // coming from right, must be on right edge + return childIndex == rowEnd; + case View.FOCUS_UP: + // coming from bottom, need to be in last row + return rowEnd == count - 1; + default: + throw new IllegalArgumentException("direction must be one of " + + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); + } + } + + /** + * Describes how the child views are horizontally aligned. Defaults to Gravity.LEFT + * + * @param gravity the gravity to apply to this grid's children + * + * @attr ref android.R.styleable#GridView_gravity + */ + public void setGravity(int gravity) { + if (mGravity != gravity) { + mGravity = gravity; + requestLayoutIfNecessary(); + } + } + + /** + * Set the amount of horizontal (x) spacing to place between each item + * in the grid. + * + * @param horizontalSpacing The amount of horizontal space between items, + * in pixels. + * + * @attr ref android.R.styleable#GridView_horizontalSpacing + */ + public void setHorizontalSpacing(int horizontalSpacing) { + if (horizontalSpacing != mRequestedHorizontalSpacing) { + mRequestedHorizontalSpacing = horizontalSpacing; + requestLayoutIfNecessary(); + } + } + + /** + * Set the amount of vertical (y) spacing to place between each item + * in the grid. + * + * @param verticalSpacing The amount of vertical space between items, + * in pixels. + * + * @attr ref android.R.styleable#GridView_verticalSpacing + */ + public void setVerticalSpacing(int verticalSpacing) { + if (verticalSpacing != mVerticalSpacing) { + mVerticalSpacing = verticalSpacing; + requestLayoutIfNecessary(); + } + } + + /** + * Control how items are stretched to fill their space. + * + * @param stretchMode Either {@link #NO_STRETCH}, + * {@link #STRETCH_SPACING}, or {@link #STRETCH_COLUMN_WIDTH}. + * + * @attr ref android.R.styleable#GridView_stretchMode + */ + public void setStretchMode(int stretchMode) { + if (stretchMode != mStretchMode) { + mStretchMode = stretchMode; + requestLayoutIfNecessary(); + } + } + + public int getStretchMode() { + return mStretchMode; + } + + /** + * Set the width of columns in the grid. + * + * @param columnWidth The column width, in pixels. + * + * @attr ref android.R.styleable#GridView_columnWidth + */ + public void setColumnWidth(int columnWidth) { + if (columnWidth != mRequestedColumnWidth) { + mRequestedColumnWidth = columnWidth; + requestLayoutIfNecessary(); + } + } + + /** + * Set the number of columns in the grid + * + * @param numColumns The desired number of columns. + * + * @attr ref android.R.styleable#GridView_numColumns + */ + public void setNumColumns(int numColumns) { + if (numColumns != mRequestedNumColumns) { + mRequestedNumColumns = numColumns; + requestLayoutIfNecessary(); + } + } + + /** + * Make sure views are touching the top or bottom edge, as appropriate for + * our gravity + */ + private void adjustViewsUpOrDown() { + final int childCount = getChildCount(); + + if (childCount > 0) { + int delta; + View child; + + if (!mStackFromBottom) { + // Uh-oh -- we came up short. Slide all views up to make them + // align with the top + child = getChildAt(0); + delta = child.getTop() - mListPadding.top; + if (mFirstPosition != 0) { + // It's OK to have some space above the first item if it is + // part of the vertical spacing + delta -= mVerticalSpacing; + } + if (delta < 0) { + // We only are looking to see if we are too low, not too high + delta = 0; + } + } else { + // we are too high, slide all views down to align with bottom + child = getChildAt(childCount - 1); + delta = child.getBottom() - (getHeight() - mListPadding.bottom); + + if (mFirstPosition + childCount < mItemCount) { + // It's OK to have some space below the last item if it is + // part of the vertical spacing + delta += mVerticalSpacing; + } + + if (delta > 0) { + // We only are looking to see if we are too high, not too low + delta = 0; + } + } + + if (delta != 0) { + offsetChildrenTopAndBottom(-delta); + } + } + } + + @Override + protected int computeVerticalScrollExtent() { + final int count = getChildCount(); + if (count > 0) { + final int numColumns = mNumColumns; + final int rowCount = (count + numColumns - 1) / numColumns; + + int extent = rowCount * 100; + + View view = getChildAt(0); + final int top = view.getTop(); + int height = view.getHeight(); + if (height > 0) { + extent += (top * 100) / height; + } + + view = getChildAt(count - 1); + final int bottom = view.getBottom(); + height = view.getHeight(); + if (height > 0) { + extent -= ((bottom - getHeight()) * 100) / height; + } + + return extent; + } + return 0; + } + + @Override + protected int computeVerticalScrollOffset() { + if (mFirstPosition >= 0 && getChildCount() > 0) { + final View view = getChildAt(0); + final int top = view.getTop(); + int height = view.getHeight(); + if (height > 0) { + final int whichRow = mFirstPosition / mNumColumns; + return Math.max(whichRow * 100 - (top * 100) / height, 0); + } + } + return 0; + } + + @Override + protected int computeVerticalScrollRange() { + // TODO: Account for vertical spacing too + final int numColumns = mNumColumns; + final int rowCount = (mItemCount + numColumns - 1) / numColumns; + return Math.max(rowCount * 100, 0); } }