/* * A modified version of the Android GridView that can be configured to * scroll vertically or horizontally * * Copyright 2012 Jess Anders * * 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. */ /* * Copyright (C) 2006 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.ijoomer.customviews; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; 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; import android.widget.ListAdapter; import com.ijoomer.src.R; /** * A view that shows items in two-dimensional scrolling grid. The items in the * grid come from the {@link android.widget.ListAdapter} associated with this view. * */ public class TwoWayGridView extends TwoWayAbsListView { 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 STRETCH_SPACING_UNIFORM = 3; public static final int AUTO_FIT = -1; public static final String TAG = "TwoWayGridView"; public static final boolean DEBUG = false; private int mNumColumns = AUTO_FIT; private int mNumRows = AUTO_FIT; private int mHorizontalSpacing = 0; private int mRequestedHorizontalSpacing; private int mVerticalSpacing = 0; private int mRequestedVerticalSpacing; private int mStretchMode = STRETCH_COLUMN_WIDTH; private int mColumnWidth; private int mRequestedColumnWidth; private int mRequestedNumColumns; private int mRowHeight; private int mRequestedRowHeight; private int mRequestedNumRows; private View mReferenceView = null; private View mReferenceViewInSelectedRow = null; private int mGravity = Gravity.LEFT; private final Rect mTempRect = new Rect(); protected GridBuilder mGridBuilder = null; public TwoWayGridView(Context context) { super(context); setupGridType(); } public TwoWayGridView(Context context, AttributeSet attrs) { this(context, attrs, android.R.attr.gridViewStyle); } public TwoWayGridView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TwoWayGridView, defStyle, 0); int hSpacing = a.getDimensionPixelOffset( R.styleable.TwoWayGridView_horizontalSpacing, 0); setHorizontalSpacing(hSpacing); int vSpacing = a.getDimensionPixelOffset( R.styleable.TwoWayGridView_verticalSpacing, 0); setVerticalSpacing(vSpacing); int index = a.getInt(R.styleable.TwoWayGridView_stretchMode, STRETCH_COLUMN_WIDTH); if (index >= 0) { setStretchMode(index); } int columnWidth = a.getDimensionPixelOffset(R.styleable.TwoWayGridView_columnWidth, -1); if (columnWidth > 0) { setColumnWidth(columnWidth); } int rowHeight = a.getDimensionPixelOffset(R.styleable.TwoWayGridView_rowHeight, -1); if (rowHeight > 0) { setRowHeight(rowHeight); } int numColumns = a.getInt(R.styleable.TwoWayGridView_numColumns, 1); setNumColumns(numColumns); int numRows = a.getInt(R.styleable.TwoWayGridView_numRows, 1); setNumRows(numRows); index = a.getInt(R.styleable.TwoWayGridView_gravity, -1); if (index >= 0) { setGravity(index); } a.recycle(); setupGridType(); } private void setupGridType() { if (mScrollVertically) { mGridBuilder = new VerticalGridBuilder(); } else { mGridBuilder = new HorizontalGridBuilder(); } } @Override public ListAdapter getAdapter() { return mAdapter; } /** * Sets the data behind this TwoWayGridView. * * @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) { if (DEBUG) Log.i(TAG, "fillGap() down: " + down); mGridBuilder.fillGap(down); } @Override int findMotionRowY(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 INVALID_POSITION; } @Override int findMotionRowX(int x) { final int childCount = getChildCount(); if (childCount > 0) { final int numRows = mNumRows; if (!mStackFromBottom) { for (int i = 0; i < childCount; i += numRows) { if (x <= getChildAt(i).getRight()) { return mFirstPosition + i; } } } else { for (int i = childCount - 1; i >= 0; i -= numRows) { if (x >= getChildAt(i).getLeft()) { return mFirstPosition + i; } } } } return INVALID_POSITION; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if ((mScrollVertically && !(mGridBuilder instanceof VerticalGridBuilder)) || (!mScrollVertically && !(mGridBuilder instanceof HorizontalGridBuilder)) ) { setupGridType(); } // Sets up mListPadding mGridBuilder.onMeasure(widthMeasureSpec, heightMeasureSpec); } //TODO implement horizontal support @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; } mGridBuilder.layoutChildren(); } finally { if (!blockLayoutRequests) { mBlockLayoutRequests = false; } } } /** * 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) { 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) { mGridBuilder.setSelectionInt(position); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { return commonKey(keyCode, 1, event); } @Override public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { return commonKey(keyCode, repeatCount, event); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { return commonKey(keyCode, 1, event); } private boolean commonKey(int keyCode, int count, KeyEvent event) { if (mAdapter == null) { return false; } if (mDataChanged) { layoutChildren(); } boolean handled = false; int action = event.getAction(); if (action != KeyEvent.ACTION_UP) { if (mSelectedPosition < 0) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_DPAD_DOWN: case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_RIGHT: case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_SPACE: case KeyEvent.KEYCODE_ENTER: resurrectSelection(); return true; } } switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: if (!event.isAltPressed()) { handled = mGridBuilder.arrowScroll(FOCUS_LEFT); } else { handled = fullScroll(FOCUS_UP); } break; case KeyEvent.KEYCODE_DPAD_RIGHT: if (!event.isAltPressed()) { handled = mGridBuilder.arrowScroll(FOCUS_RIGHT); } else { handled = fullScroll(FOCUS_DOWN); } break; case KeyEvent.KEYCODE_DPAD_UP: if (!event.isAltPressed()) { handled = mGridBuilder.arrowScroll(FOCUS_UP); } else { handled = fullScroll(FOCUS_UP); } break; case KeyEvent.KEYCODE_DPAD_DOWN: if (!event.isAltPressed()) { handled = mGridBuilder.arrowScroll(FOCUS_DOWN); } else { handled = fullScroll(FOCUS_DOWN); } break; case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_ENTER: { if (getChildCount() > 0 && event.getRepeatCount() == 0) { keyPressed(); } return true; } case KeyEvent.KEYCODE_SPACE: //if (mPopup == null || !mPopup.isShowing()) { if (!event.isShiftPressed()) { handled = pageScroll(FOCUS_DOWN); } else { handled = pageScroll(FOCUS_UP); } //} break; } } //if (!handled) { // handled = sendToTextFilter(keyCode, count, event); //} if (handled) { return true; } else { switch (action) { case KeyEvent.ACTION_DOWN: return super.onKeyDown(keyCode, event); case KeyEvent.ACTION_UP: return super.onKeyUp(keyCode, event); case KeyEvent.ACTION_MULTIPLE: return super.onKeyMultiple(keyCode, count, event); default: return false; } } } /** * Scrolls up or down by the number of items currently present on screen. * * @param direction either {@link android.view.View#FOCUS_UP} or {@link android.view.View#FOCUS_DOWN} * @return whether selection was moved */ boolean pageScroll(int direction) { int nextPage = -1; //TODO this doesn't look correct... 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); invokeOnItemScrollListener(); //awakenScrollBars(); return true; } return false; } /** * Go to the last or first item if possible. * * @param direction either {@link android.view.View#FOCUS_UP} or {@link android.view.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); invokeOnItemScrollListener(); moved = true; } else if (direction == FOCUS_DOWN) { mLayoutMode = LAYOUT_SET_SELECTION; setSelectionInt(mItemCount - 1); invokeOnItemScrollListener(); moved = true; } if (moved) { //awakenScrollBars(); } 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 (!mGridBuilder.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(); } } /** * 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#JessGridView_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#JessGridView_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#JessGridView_verticalSpacing */ public void setVerticalSpacing(int verticalSpacing) { if (verticalSpacing != mRequestedVerticalSpacing) { mRequestedVerticalSpacing = verticalSpacing; requestLayoutIfNecessary(); } } /** * Control how items are stretched to fill their space. * * @param stretchMode Either {@link #NO_STRETCH}, * {@link #STRETCH_SPACING}, {@link #STRETCH_SPACING_UNIFORM}, or {@link #STRETCH_COLUMN_WIDTH}. * * @attr ref android.R.styleable#JessGridView_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. (Only used in vertical scroll mode) * * @param columnWidth The column width, in pixels. * * @attr ref android.R.styleable#JessGridView_columnWidth */ public void setColumnWidth(int columnWidth) { if (columnWidth != mRequestedColumnWidth) { mRequestedColumnWidth = columnWidth; requestLayoutIfNecessary(); } } /** * Set the height of rows in the grid. (Only used in horizontal scroll mode) * * @param rowHeight The row height, in pixels. * * @attr ref android.R.styleable#JessGridView_rowHeight */ public void setRowHeight(int rowHeight) { if (rowHeight != mRequestedRowHeight) { mRequestedRowHeight = rowHeight; requestLayoutIfNecessary(); } } /** * Set the number of columns in the grid * * @param numColumns The desired number of columns. * * @attr ref android.R.styleable#JessGridView_numColumns */ public void setNumColumns(int numColumns) { if (numColumns != mRequestedNumColumns) { mRequestedNumColumns = numColumns; requestLayoutIfNecessary(); } } /** * Set the number of rows in the grid * * @param numRows The desired number of rows. * * @attr ref android.R.styleable#JessGridView_numRows */ public void setNumRows(int numRows) { if (numRows != mRequestedNumRows) { mRequestedNumRows = numRows; requestLayoutIfNecessary(); } } @Override protected int computeVerticalScrollExtent() { final int count = getChildCount(); if (count > 0 && mScrollVertically) { 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 && mScrollVertically) { final View view = getChildAt(0); final int top = view.getTop(); int height = view.getHeight(); if (height > 0) { final int numColumns = mNumColumns; final int whichRow = mFirstPosition / numColumns; final int rowCount = (mItemCount + numColumns - 1) / numColumns; return Math.max(whichRow * 100 - (top * 100) / height + (int) ((float) getScrollY() / getHeight() * rowCount * 100), 0); } } return 0; } @Override protected int computeVerticalScrollRange() { // TODO: Account for vertical spacing too if (!mScrollVertically) { return 0; } final int numColumns = mNumColumns; final int rowCount = (mItemCount + numColumns - 1) / numColumns; return Math.max(rowCount * 100, 0); } @Override protected int computeHorizontalScrollExtent() { final int count = getChildCount(); if (count > 0 && !mScrollVertically) { final int numRows = mNumRows; final int columnCount = (count + numRows - 1) / numRows; int extent = columnCount * 100; View view = getChildAt(0); final int left = view.getLeft(); int width = view.getWidth(); if (width > 0) { extent += (left * 100) / width; } view = getChildAt(count - 1); final int right = view.getRight(); width = view.getWidth(); if (width > 0) { extent -= ((right - getWidth()) * 100) / width; } return extent; } return 0; } @Override protected int computeHorizontalScrollOffset() { if (mFirstPosition >= 0 && getChildCount() > 0 && !mScrollVertically) { final View view = getChildAt(0); final int left = view.getLeft(); int width = view.getWidth(); if (width > 0) { final int numRows = mNumRows; final int whichColumn = mFirstPosition / numRows; final int columnCount = (mItemCount + numRows - 1) / numRows; return Math.max(whichColumn * 100 - (left * 100) / width + (int) ((float) getScrollX() / getWidth() * columnCount * 100), 0); } } return 0; } @Override protected int computeHorizontalScrollRange() { // TODO: Account for horizontal spacing too if (mScrollVertically) { return 0; } final int numRows = mNumRows; final int columnCount = (mItemCount + numRows - 1) / numRows; return Math.max(columnCount * 100, 0); } private abstract class GridBuilder { protected abstract View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected, int where); protected abstract void fillGap(boolean down); protected abstract void onMeasure(int widthMeasureSpec, int heightMeasureSpec); protected abstract void layoutChildren(); protected abstract void setSelectionInt(int position); protected abstract boolean arrowScroll(int direction); protected abstract boolean isCandidateSelection(int childIndex, int direction); } private class VerticalGridBuilder extends GridBuilder { /** * 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 */ @Override protected View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected, int where) { View child; if (DEBUG) Log.i(TAG, "makeAndAddView() - start - position: " + position + " mFirstPosition: " + mFirstPosition); if (!mDataChanged) { // Try to use an existing 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); if (DEBUG) Log.i(TAG, "makeAndAddView() - end - position: " + position + "reused a view"); 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); if (DEBUG) Log.i(TAG, "makeAndAddView() - end - position: " + position + "did NOT reuse a view - scrap: " + mIsScrap[0]); return child; } @Override protected 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) { if (DEBUG) Log.i(TAG, "fillDown() pos: " + pos + " nextTop: " + nextTop + " mFirstPosition: " + mFirstPosition); 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; } // mReferenceView will change with each call to makeRow() // do not cache in a local variable outside of this loop nextTop = mReferenceView.getBottom() + mVerticalSpacing; pos += mNumColumns; } return selectedView; } private View makeRow(int startPos, int y, boolean flow) { if (DEBUG) Log.i(TAG, "makeRow() startPos: " + startPos + " y: " + y + " flow: " + flow + " mFirstPosition: " + mFirstPosition); final int columnWidth = mColumnWidth; final int horizontalSpacing = mHorizontalSpacing; int last; int nextLeft = mListPadding.left + ((mStretchMode == STRETCH_SPACING_UNIFORM) ? horizontalSpacing : 0); 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; View child = 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; child = makeAndAddView(pos, y, flow, nextLeft, selected, where); nextLeft += columnWidth; if (pos < last - 1) { nextLeft += horizontalSpacing; } if (selected && (hasFocus || inClick)) { selectedView = child; } } mReferenceView = 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) { if (DEBUG) Log.i(TAG, "fillLeft() pos: " + pos + " nextBottom: " + nextBottom + " mFirstPosition: " + mFirstPosition); 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) { if (DEBUG) Log.i(TAG, "fillFromTop() nextLeft: " + nextTop + " mFirstPosition: " + mFirstPosition); 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) { if (DEBUG) Log.i(TAG, "fillFromBotom() lastPosition: " + lastPosition + " nextBottom: " + nextBottom + " mFirstPosition: " + mFirstPosition); 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) { if (DEBUG) Log.i(TAG, "fillSelection() childrenTop: " + childrenTop + " childrenBottom: " + childrenBottom + " mFirstPosition: " + mFirstPosition); 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; } /** * 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) { if (DEBUG) Log.i(TAG, "fillSpecific() position: " + position + " top: " + top + " mFirstPosition: " + mFirstPosition); 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; // We didn't have anything to layout, bail out if (referenceView == null) { return null; } 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) { if (DEBUG) Log.i(TAG, "correctTooLeft() numColumns: " + numColumns + " verticalSpacing: " + verticalSpacing + " mFirstPosition: " + mFirstPosition); // 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 (DEBUG) Log.i(TAG, "correctTooLow() numColumns: " + numColumns + " verticalSpacing: " + verticalSpacing + " mFirstPosition: " + mFirstPosition); 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) { if (DEBUG) Log.i(TAG, "fillFromSelection() selectedTop: " + selectedTop + " childrenTop: " + childrenTop + " childrenBottom: " + childrenBottom + " mFirstPosition: " + mFirstPosition); 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); } } private void determineColumns(int availableSpace) { final int requestedHorizontalSpacing = mRequestedHorizontalSpacing; final int stretchMode = mStretchMode; final int requestedColumnWidth = mRequestedColumnWidth; mVerticalSpacing = mRequestedVerticalSpacing; 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 = 0; switch (stretchMode) { case STRETCH_COLUMN_WIDTH: // Stretch the columns spaceLeftOver = availableSpace - (mNumColumns * requestedColumnWidth) - ((mNumColumns - 1) * requestedHorizontalSpacing); mColumnWidth = requestedColumnWidth + spaceLeftOver / mNumColumns; mHorizontalSpacing = requestedHorizontalSpacing; break; case STRETCH_SPACING: // Stretch the spacing between columns spaceLeftOver = availableSpace - (mNumColumns * requestedColumnWidth) - ((mNumColumns - 1) * requestedHorizontalSpacing); mColumnWidth = requestedColumnWidth; if (mNumColumns > 1) { mHorizontalSpacing = requestedHorizontalSpacing + spaceLeftOver / (mNumColumns - 1); } else { mHorizontalSpacing = requestedHorizontalSpacing + spaceLeftOver; } break; case STRETCH_SPACING_UNIFORM: // Stretch the spacing between columns spaceLeftOver = availableSpace - (mNumColumns * requestedColumnWidth) - ((mNumColumns + 1) * requestedHorizontalSpacing); mColumnWidth = requestedColumnWidth; if (mNumColumns > 1) { mHorizontalSpacing = requestedHorizontalSpacing + spaceLeftOver / (mNumColumns + 1); } else { mHorizontalSpacing = ((requestedHorizontalSpacing * 2) + spaceLeftOver) / 2; } break; } break; } } /** * 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) { if (DEBUG) Log.i(TAG, "moveSelection() delta: " + delta + " childrenTop: " + childrenTop + " childrenBottom: " + childrenBottom + " mFirstPosition: " + mFirstPosition); 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; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (DEBUG) Log.i(TAG, "vertical onMeasure heightMode: " + heightMode); 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); TwoWayAbsListView.LayoutParams p = (TwoWayAbsListView.LayoutParams)child.getLayoutParams(); if (p == null) { p = new TwoWayAbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0); child.setLayoutParams(p); } p.viewType = mAdapter.getItemViewType(0); p.forceAdd = true; int childHeightSpec = getChildMeasureSpec( MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, p.height); int childWidthSpec = getChildMeasureSpec( MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width); child.measure(childWidthSpec, childHeightSpec); childHeight = child.getMeasuredHeight(); if (mRecycler.shouldRecycleViewType(p.viewType)) { mRecycler.addScrapView(child); } } 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; if (DEBUG) Log.i(TAG, "Vertical onMeasure widthSize: " + widthSize + " heightSize: " + heightSize); } @Override protected void layoutChildren() { 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)); } } else { recycleBin.fillActiveViews(childCount, firstPosition); } // Clear out old views //removeAllViewsInLayout(); detachAllViewsFromParent(); 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(mAdapter == null || isInTouchMode() ? INVALID_POSITION : 0); sel = fillFromTop(childrenTop); } else { final int last = mItemCount - 1; setSelectedPositionInt(mAdapter == null || isInTouchMode() ? INVALID_POSITION : 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(sel); mSelectedTop = sel.getTop(); } else if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) { View child = getChildAt(mMotionPosition - mFirstPosition); if (child != null) positionSelector(child); } else { mSelectedTop = 0; mSelectorRect.setEmpty(); } mLayoutMode = LAYOUT_NORMAL; mDataChanged = false; mNeedSync = false; setNextSelectedPositionInt(mSelectedPosition); updateScrollIndicators(); if (mItemCount > 0) { checkSelectionChanged(); } invokeOnItemScrollListener(); } /** * 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); } } } /** * 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(); final int mode = mTouchMode; final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL && mMotionPosition == position; final boolean updateChildPressed = isPressed != child.isPressed(); boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested(); // Respect layout params that are already in the view. Otherwise make // some up... TwoWayAbsListView.LayoutParams p = (TwoWayAbsListView.LayoutParams)child.getLayoutParams(); if (p == null) { p = new TwoWayAbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0); } p.viewType = mAdapter.getItemViewType(position); if (recycled && !p.forceAdd) { attachViewToParent(child, where, p); } else { p.forceAdd = false; addViewInLayout(child, where, p, true); } if (updateChildSelected) { child.setSelected(isSelected); if (isSelected) { requestFocus(); } } if (updateChildPressed) { child.setPressed(isPressed); } 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()); } if (mCachingStarted) { child.setDrawingCacheEnabled(true); } } 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); } } } /** * Makes the item at the supplied position selected. * * @param position the position of the new selection */ @Override protected void setSelectionInt(int position) { int previousSelectedPosition = mNextSelectedPosition; setNextSelectedPositionInt(position); TwoWayGridView.this.layoutChildren(); final int next = mStackFromBottom ? mItemCount - 1 - mNextSelectedPosition : mNextSelectedPosition; final int previous = mStackFromBottom ? mItemCount - 1 - previousSelectedPosition : previousSelectedPosition; final int nextRow = next / mNumColumns; final int previousRow = previous / mNumColumns; if (nextRow != previousRow) { //awakenScrollBars(); } } /** * Scrolls to the next or previous item, horizontally or vertically. * * @param direction either {@link android.view.View#FOCUS_LEFT}, {@link android.view.View#FOCUS_RIGHT}, * {@link android.view.View#FOCUS_UP} or {@link android.view.View#FOCUS_DOWN} * * @return whether selection was moved */ @Override protected 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(Math.max(0, selectedPosition - 1)); moved = true; } break; case FOCUS_RIGHT: if (selectedPosition < endOfRowPos) { mLayoutMode = LAYOUT_MOVE_SELECTION; setSelectionInt(Math.min(selectedPosition + 1, mItemCount - 1)); moved = true; } break; } if (moved) { playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); invokeOnItemScrollListener(); } if (moved) { //awakenScrollBars(); } return moved; } /** * 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. */ @Override protected 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; case View.FOCUS_FORWARD: // coming from top-left, need to be first in top row return childIndex == rowStart && rowStart == 0; case View.FOCUS_BACKWARD: // coming from bottom-right, need to be last in bottom row return childIndex == rowEnd && rowEnd == count - 1; default: throw new IllegalArgumentException("direction must be one of " + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, " + "FOCUS_FORWARD, FOCUS_BACKWARD}"); } } } ///////////////////////////////////////////////////////////////////////////////////////////////////// // // Horizontal Grid Builder // ///////////////////////////////////////////////////////////////////////////////////////////////////// private class HorizontalGridBuilder extends GridBuilder { /** * 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 x Left or Right edge of the view to add * @param flow if true, align left edge to x. If false, align right edge to * x. * @param childrenTop Top 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 */ @Override protected View makeAndAddView(int position, int x, boolean flow, int childrenTop, boolean selected, int where) { View child; if (!mDataChanged) { // Try to use an existing 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, x, flow, childrenTop, selected, true, where); if (DEBUG) Log.i(TAG, "makeAndAddView() - end - position: " + position + " reused a view"); 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, x, flow, childrenTop, selected, mIsScrap[0], where); if (DEBUG) Log.i(TAG, "makeAndAddView() - end - position: " + position + "did NOT reuse a view - scrap: " + mIsScrap[0]); return child; } @Override protected void fillGap(boolean right) { final int numRows = mNumRows; final int horizontalSpacing = mHorizontalSpacing; final int count = getChildCount(); if (right) { final int startOffset = count > 0 ? getChildAt(count - 1).getRight() + horizontalSpacing : getListPaddingLeft(); int position = mFirstPosition + count; if (mStackFromBottom) { position += numRows - 1; } fillRight(position, startOffset); correctTooLeft(numRows, horizontalSpacing, getChildCount()); } else { final int startOffset = count > 0 ? getChildAt(0).getLeft() - horizontalSpacing : getWidth() - getListPaddingRight(); int position = mFirstPosition; if (!mStackFromBottom) { position -= numRows; } else { position--; } fillLeft(position, startOffset); correctTooRight(numRows, horizontalSpacing, getChildCount()); } } /** * Fills the list from pos right to the end of the list view. * * @param pos The first position to put in the list * * @param nextLeft The location where the left 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 fillRight(int pos, int nextLeft) { if (DEBUG) Log.i(TAG, "fillRight() pos: " + pos + " nextLeft: " + nextLeft + " mFirstPosition: " + mFirstPosition); View selectedView = null; final int end = (getRight() - getLeft()) - mListPadding.right; while (nextLeft < end && pos < mItemCount) { View temp = makeColumn(pos, nextLeft, true); if (temp != null) { selectedView = temp; } // mReferenceView will change with each call to makeRow() // do not cache in a local variable outside of this loop nextLeft = mReferenceView.getRight() + mHorizontalSpacing; pos += mNumRows; } return selectedView; } private View makeColumn(int startPos, int x, boolean flow) { if (DEBUG) Log.i(TAG, "makeColumn() startPos: " + startPos + " x: " + x + " flow: " + flow + " mFirstPosition: " + mFirstPosition); final int rowHeight = mRowHeight; final int verticalSpacing = mVerticalSpacing; int last; int nextTop = mListPadding.top + ((mStretchMode == STRETCH_SPACING_UNIFORM) ? verticalSpacing : 0); if (!mStackFromBottom) { last = Math.min(startPos + mNumRows, mItemCount); } else { last = startPos + 1; startPos = Math.max(0, startPos - mNumRows + 1); if (last - startPos < mNumRows) { nextTop += (mNumRows - (last - startPos)) * (rowHeight + verticalSpacing); } } View selectedView = null; final boolean hasFocus = shouldShowSelector(); final boolean inClick = touchModeDrawsInPressedState(); final int selectedPosition = mSelectedPosition; View child = 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; child = makeAndAddView(pos, x, flow, nextTop, selected, where); nextTop += rowHeight; if (pos < last - 1) { nextTop += verticalSpacing; } if (selected && (hasFocus || inClick)) { selectedView = child; } } mReferenceView = child; if (selectedView != null) { mReferenceViewInSelectedRow = mReferenceView; } return selectedView; } /** * Fills the list from pos to the left of the list view. * * @param pos The first position to put in the list * * @param nextRight The location where the right of the item associated * with pos should be drawn * * @return The view that is currently selected */ private View fillLeft(int pos, int nextRight) { if (DEBUG) Log.i(TAG, "fillLeft() pos: " + pos + " nextRight: " + nextRight + " mFirstPosition: " + mFirstPosition); View selectedView = null; final int end = mListPadding.left; while (nextRight > end && pos >= 0) { View temp = makeColumn(pos, nextRight, false); if (temp != null) { selectedView = temp; } nextRight = mReferenceView.getLeft() - mHorizontalSpacing; mFirstPosition = pos; pos -= mNumRows; } if (mStackFromBottom) { mFirstPosition = Math.max(0, pos + 1); } return selectedView; } /** * Fills the list from left to right, starting with mFirstPosition * * @param nextLeft The location where the left of the first item should be * drawn * * @return The view that is currently selected */ private View fillFromTop(int nextLeft) { if (DEBUG) Log.i(TAG, "fillFromTop() nextLeft: " + nextLeft + " mFirstPosition: " + mFirstPosition); mFirstPosition = Math.min(mFirstPosition, mSelectedPosition); mFirstPosition = Math.min(mFirstPosition, mItemCount - 1); if (mFirstPosition < 0) { mFirstPosition = 0; } mFirstPosition -= mFirstPosition % mNumRows; return fillRight(mFirstPosition, nextLeft); } private View fillFromBottom(int lastPosition, int nextRight) { if (DEBUG) Log.i(TAG, "fillFromBotom() lastPosition: " + lastPosition + " nextRight: " + nextRight + " mFirstPosition: " + mFirstPosition); lastPosition = Math.max(lastPosition, mSelectedPosition); lastPosition = Math.min(lastPosition, mItemCount - 1); final int invertedPosition = mItemCount - 1 - lastPosition; lastPosition = mItemCount - 1 - (invertedPosition - (invertedPosition % mNumRows)); return fillLeft(lastPosition, nextRight); } private View fillSelection(int childrenLeft, int childrenRight) { if (DEBUG) Log.i(TAG, "fillSelection() childrenLeft: " + childrenLeft + " childrenRight: " + childrenRight + " mFirstPosition: " + mFirstPosition); final int selectedPosition = reconcileSelectedPosition(); final int numRows = mNumRows; final int horizontalSpacing = mHorizontalSpacing; int columnStart; int columnEnd = -1; if (!mStackFromBottom) { columnStart = selectedPosition - (selectedPosition % numRows); } else { final int invertedSelection = mItemCount - 1 - selectedPosition; columnEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numRows)); columnStart = Math.max(0, columnEnd - numRows + 1); } final int fadingEdgeLength = getHorizontalFadingEdgeLength(); final int leftSelectionPixel = getLeftSelectionPixel(childrenLeft, fadingEdgeLength, columnStart); final View sel = makeColumn(mStackFromBottom ? columnEnd : columnStart, leftSelectionPixel, true); mFirstPosition = columnStart; final View referenceView = mReferenceView; if (!mStackFromBottom) { fillRight(columnStart + numRows, referenceView.getRight() + horizontalSpacing); pinToRight(childrenRight); fillLeft(columnStart - numRows, referenceView.getLeft() - horizontalSpacing); adjustViewsLeftOrRight(); } else { final int rightSelectionPixel = getRightSelectionPixel(childrenRight, fadingEdgeLength, numRows, columnStart); final int offset = rightSelectionPixel - referenceView.getRight(); offsetChildrenLeftAndRight(offset); fillLeft(columnStart - 1, referenceView.getLeft() - horizontalSpacing); pinToLeft(childrenLeft); fillRight(columnEnd + numRows, referenceView.getRight() + horizontalSpacing); adjustViewsLeftOrRight(); } return sel; } /** * Layout during a scroll that results from tracking motion events. Places * the mMotionPosition view at the offset specified by mMotionViewLeft, and * then build surrounding views from there. * * @param position the position at which to start filling * @param left the left 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 left) { if (DEBUG) Log.i(TAG, "fillSpecific() position: " + position + " left: " + left + " mFirstPosition: " + mFirstPosition); final int numRows = mNumRows; int motionColumnStart; int motionColumnEnd = -1; if (!mStackFromBottom) { //TODO don't understand what this is doing.... motionColumnStart = position - (position % numRows); } else { //TODO don't understand what this is doing.... final int invertedSelection = mItemCount - 1 - position; motionColumnEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numRows)); motionColumnStart = Math.max(0, motionColumnEnd - numRows + 1); } final View temp = makeColumn(mStackFromBottom ? motionColumnEnd : motionColumnStart, left, true); // Possibly changed again in fillUp if we add rows above this one. mFirstPosition = motionColumnStart; final View referenceView = mReferenceView; // We didn't have anything to layout, bail out if (referenceView == null) { return null; } final int horizontalSpacing = mHorizontalSpacing; View leftOf; View rightOf; if (!mStackFromBottom) { leftOf = fillLeft(motionColumnStart - numRows, referenceView.getLeft() - horizontalSpacing); adjustViewsLeftOrRight(); rightOf = fillRight(motionColumnStart + numRows, referenceView.getRight() + horizontalSpacing); // Check if we have dragged the bottom of the grid too high final int childCount = getChildCount(); if (childCount > 0) { correctTooLeft(numRows, horizontalSpacing, childCount); } } else { rightOf = fillRight(motionColumnEnd + numRows, referenceView.getRight() + horizontalSpacing); adjustViewsLeftOrRight(); leftOf = fillLeft(motionColumnStart - 1, referenceView.getLeft() - horizontalSpacing); // Check if we have dragged the right of the grid too high final int childCount = getChildCount(); if (childCount > 0) { correctTooRight(numRows, horizontalSpacing, childCount); } } if (temp != null) { return temp; } else if (leftOf != null) { return leftOf; } else { return rightOf; } } private void correctTooLeft(int numRows, int horizontalSpacing, int childCount) { if (DEBUG) Log.i(TAG, "correctTooLeft() numRows: " + numRows + " horizontalSpacing: " + horizontalSpacing + " mFirstPosition: " + mFirstPosition); // 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 right edge final int lastRight = lastChild.getRight(); // This is end of our drawable area final int end = (getRight() - getLeft()) - mListPadding.right; // This is how far the right edge of the last view is from the right // edge of the drawable area int rightOffset = end - lastRight; final View firstChild = getChildAt(0); final int firstLeft = firstChild.getLeft(); // Make sure we are 1) Too Left, and 2) Either there are more columns to left of // the first column or the first column is scrolled off the top of the drawable area if (rightOffset > 0 && (mFirstPosition > 0 || firstLeft < mListPadding.left)) { if (mFirstPosition == 0) { // Don't pull the left too far right rightOffset = Math.min(rightOffset, mListPadding.left - firstLeft); } // Move everything right offsetChildrenLeftAndRight(rightOffset); if (mFirstPosition > 0) { // Fill the gap that was opened above mFirstPosition with more columns, if // possible fillLeft(mFirstPosition - (mStackFromBottom ? 1 : numRows), firstChild.getLeft() - horizontalSpacing); // Close up the remaining gap adjustViewsLeftOrRight(); } } } } private void correctTooRight(int numRows, int horizontalSpacing, int childCount) { if (DEBUG) Log.i(TAG, "correctTooRight() numRows: " + numRows + " horizontalSpacing: " + horizontalSpacing + " mFirstPosition: " + mFirstPosition); if (mFirstPosition == 0 && childCount > 0) { // Get the first child ... final View firstChild = getChildAt(0); // ... and its left edge final int firstLeft = firstChild.getLeft(); // This is left of our drawable area final int start = mListPadding.left; // This is right of our drawable area final int end = (getRight() - getLeft()) - mListPadding.right; // This is how far the left edge of the first view is from the left of the // drawable area int leftOffset = firstLeft - start; final View lastChild = getChildAt(childCount - 1); final int lastRight = lastChild.getRight(); final int lastPosition = mFirstPosition + childCount - 1; // Make sure we are 1) Too right, and 2) Either there are more columns to right of the // last column or the last column is scrolled off the right of the drawable area if (leftOffset > 0 && (lastPosition < mItemCount - 1 || lastRight > end)) { if (lastPosition == mItemCount - 1 ) { // Don't pull the right too far left leftOffset = Math.min(leftOffset, lastRight - end); } // Move everything left offsetChildrenLeftAndRight(-leftOffset); if (lastPosition < mItemCount - 1) { // Fill the gap that was opened to right of the last position with // more columns, if possible fillRight(lastPosition + (!mStackFromBottom ? 1 : numRows), lastChild.getRight() + horizontalSpacing); // Close up the remaining gap adjustViewsLeftOrRight(); } } } } /** * Fills the grid based on positioning the new selection relative to the old * selection. The new selection will be placed at, to left of, or to right of 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 leftwards and * rightwards from there. * * @param delta Which way we are moving * @param childrenLeft Where to start drawing children * @param childrenRight Last pixel where children can be drawn * @return The view that currently has selection */ private View moveSelection(int delta, int childrenLeft, int childrenRight) { if (DEBUG) Log.i(TAG, "moveSelection() delta: " + delta + " childrenLeft: " + childrenLeft + " childrenRight: " + childrenRight + " mFirstPosition: " + mFirstPosition); final int fadingEdgeLength = getHorizontalFadingEdgeLength(); final int selectedPosition = mSelectedPosition; final int numRows = mNumRows; final int horizontalSpacing = mHorizontalSpacing; int oldColumnStart; int columnStart; int columnEnd = -1; if (!mStackFromBottom) { oldColumnStart = (selectedPosition - delta) - ((selectedPosition - delta) % numRows); columnStart = selectedPosition - (selectedPosition % numRows); } else { int invertedSelection = mItemCount - 1 - selectedPosition; columnEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numRows)); columnStart = Math.max(0, columnEnd - numRows + 1); invertedSelection = mItemCount - 1 - (selectedPosition - delta); oldColumnStart = mItemCount - 1 - (invertedSelection - (invertedSelection % numRows)); oldColumnStart = Math.max(0, oldColumnStart - numRows + 1); } final int rowDelta = columnStart - oldColumnStart; final int leftSelectionPixel = getLeftSelectionPixel(childrenLeft, fadingEdgeLength, columnStart); final int rightSelectionPixel = getRightSelectionPixel(childrenRight, fadingEdgeLength, numRows, columnStart); // Possibly changed again in fillLeft if we add rows above this one. mFirstPosition = columnStart; View sel; View referenceView; if (rowDelta > 0) { /* * Case 1: Scrolling right. */ final int oldRight = mReferenceViewInSelectedRow == null ? 0 : mReferenceViewInSelectedRow.getRight(); sel = makeColumn(mStackFromBottom ? columnEnd : columnStart, oldRight + horizontalSpacing, true); referenceView = mReferenceView; adjustForRightFadingEdge(referenceView, leftSelectionPixel, rightSelectionPixel); } else if (rowDelta < 0) { /* * Case 2: Scrolling left. */ final int oldTop = mReferenceViewInSelectedRow == null ? 0 : mReferenceViewInSelectedRow.getLeft(); sel = makeColumn(mStackFromBottom ? columnEnd : columnStart, oldTop - horizontalSpacing, false); referenceView = mReferenceView; adjustForLeftFadingEdge(referenceView, leftSelectionPixel, rightSelectionPixel); } else { /* * Keep selection where it was */ final int oldTop = mReferenceViewInSelectedRow == null ? 0 : mReferenceViewInSelectedRow.getLeft(); sel = makeColumn(mStackFromBottom ? columnEnd : columnStart, oldTop, true); referenceView = mReferenceView; } if (!mStackFromBottom) { fillLeft(columnStart - numRows, referenceView.getLeft() - horizontalSpacing); adjustViewsLeftOrRight(); fillRight(columnStart + numRows, referenceView.getRight() + horizontalSpacing); } else { fillRight(columnStart + numRows, referenceView.getRight() + horizontalSpacing); adjustViewsLeftOrRight(); fillLeft(columnStart - 1, referenceView.getLeft() - horizontalSpacing); } return sel; } @Override protected void layoutChildren() { final int childrenLeft = mListPadding.left; final int childrenRight = getRight() - getLeft() - mListPadding.right; 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)); } } else { recycleBin.fillActiveViews(childCount, firstPosition); } // Clear out old views //removeAllViewsInLayout(); detachAllViewsFromParent(); switch (mLayoutMode) { case LAYOUT_SET_SELECTION: if (newSel != null) { sel = fillFromSelection(newSel.getLeft(), childrenLeft, childrenRight); } else { sel = fillSelection(childrenLeft, childrenRight); } break; case LAYOUT_FORCE_TOP: mFirstPosition = 0; sel = fillFromTop(childrenLeft); adjustViewsLeftOrRight(); break; case LAYOUT_FORCE_BOTTOM: sel = fillRight(mItemCount - 1, childrenRight); adjustViewsLeftOrRight(); 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, childrenLeft, childrenRight); break; default: if (childCount == 0) { if (!mStackFromBottom) { setSelectedPositionInt(mAdapter == null || isInTouchMode() ? INVALID_POSITION : 0); sel = fillFromTop(childrenLeft); } else { final int last = mItemCount - 1; setSelectedPositionInt(mAdapter == null || isInTouchMode() ? INVALID_POSITION : last); sel = fillFromBottom(last, childrenRight); } } else { if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) { sel = fillSpecific(mSelectedPosition, oldSel == null ? childrenLeft : oldSel.getLeft()); } else if (mFirstPosition < mItemCount) { sel = fillSpecific(mFirstPosition, oldFirst == null ? childrenLeft : oldFirst.getLeft()); } else { sel = fillSpecific(0, childrenLeft); } } break; } // Flush any cached views that did not get reused above recycleBin.scrapActiveViews(); if (sel != null) { positionSelector(sel); mSelectedTop = sel.getLeft(); } else if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) { View child = getChildAt(mMotionPosition - mFirstPosition); if (child != null) positionSelector(child); } else { mSelectedTop = 0; mSelectorRect.setEmpty(); } mLayoutMode = LAYOUT_NORMAL; mDataChanged = false; mNeedSync = false; setNextSelectedPositionInt(mSelectedPosition); updateScrollIndicators(); if (mItemCount > 0) { checkSelectionChanged(); } invokeOnItemScrollListener(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (DEBUG) Log.i(TAG, "horizontal onMeasure heightMode: " + heightMode); if (heightMode == MeasureSpec.UNSPECIFIED) { if (mRowHeight > 0) { heightSize = mRowHeight + mListPadding.top + mListPadding.bottom; } else { heightSize = mListPadding.top + mListPadding.bottom; } heightSize += getHorizontalScrollbarHeight(); } int childHeight = heightSize - mListPadding.top - mListPadding.bottom; determineRows(childHeight); int childWidth = 0; mItemCount = mAdapter == null ? 0 : mAdapter.getCount(); final int count = mItemCount; if (count > 0) { final View child = obtainView(0, mIsScrap); TwoWayAbsListView.LayoutParams p = (TwoWayAbsListView.LayoutParams)child.getLayoutParams(); if (p == null) { p = new TwoWayAbsListView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.FILL_PARENT, 0); child.setLayoutParams(p); } p.viewType = mAdapter.getItemViewType(0); p.forceAdd = true; int childHeightSpec = getChildMeasureSpec( MeasureSpec.makeMeasureSpec(mRowHeight, MeasureSpec.UNSPECIFIED), 0, p.height); int childWidthSpec = getChildMeasureSpec( MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY), 0, p.width); child.measure(childWidthSpec, childHeightSpec); childWidth = child.getMeasuredWidth(); if (mRecycler.shouldRecycleViewType(p.viewType)) { mRecycler.addScrapView(child); } } if (widthMode == MeasureSpec.UNSPECIFIED) { widthSize = mListPadding.left + mListPadding.right + childWidth + getHorizontalFadingEdgeLength() * 2; } if (widthMode == MeasureSpec.AT_MOST) { int ourSize = mListPadding.left + mListPadding.right; final int numRows = mNumRows; for (int i = 0; i < count; i += numRows) { ourSize += childWidth; if (i + numRows < count) { ourSize += mHorizontalSpacing; } if (ourSize >= widthSize) { ourSize = widthSize; break; } } widthSize = ourSize; } setMeasuredDimension(widthSize, heightSize); mWidthMeasureSpec = widthMeasureSpec; if (DEBUG) Log.i(TAG, "Horizontal onMeasure widthSize: " + widthSize + " heightSize: " + heightSize); } private void determineRows(int availableSpace) { final int requestedVerticalSpacing = mRequestedVerticalSpacing; final int stretchMode = mStretchMode; final int requestedRowHeight = mRequestedRowHeight; mHorizontalSpacing = mRequestedHorizontalSpacing; if (mRequestedNumRows == AUTO_FIT) { if (requestedRowHeight > 0) { // Client told us to pick the number of rows mNumRows = (availableSpace + mRequestedVerticalSpacing) / (requestedRowHeight + mRequestedVerticalSpacing); } else { // Just make up a number if we don't have enough info mNumRows = 2; } } else { // We picked the rows mNumRows = mRequestedNumRows; } if (mNumRows <= 0) { mNumRows = 1; } switch (stretchMode) { case NO_STRETCH: // Nobody stretches mRowHeight = mRequestedRowHeight; mVerticalSpacing = mRequestedVerticalSpacing; break; default: int spaceLeftOver = 0; switch (stretchMode) { case STRETCH_COLUMN_WIDTH: spaceLeftOver = availableSpace - (mNumRows * requestedRowHeight) - ((mNumRows - 1) * requestedVerticalSpacing); // Stretch the rows mRowHeight = requestedRowHeight + spaceLeftOver / mNumRows; mVerticalSpacing = requestedVerticalSpacing; break; case STRETCH_SPACING: spaceLeftOver = availableSpace - (mNumRows * requestedRowHeight) - ((mNumRows - 1) * requestedVerticalSpacing); // Stretch the spacing between rows mRowHeight = requestedRowHeight; if (mNumRows > 1) { mVerticalSpacing = requestedVerticalSpacing + spaceLeftOver / (mNumRows - 1); } else { mVerticalSpacing = requestedVerticalSpacing + spaceLeftOver; } break; case STRETCH_SPACING_UNIFORM: // Stretch the spacing between rows spaceLeftOver = availableSpace - (mNumRows * requestedRowHeight) - ((mNumRows + 1) * requestedVerticalSpacing); mRowHeight = requestedRowHeight; if (mNumRows > 1) { mVerticalSpacing = requestedVerticalSpacing + spaceLeftOver / (mNumRows + 1); } else { mVerticalSpacing = ((requestedVerticalSpacing * 2) + spaceLeftOver) / 2; } break; } break; } if (DEBUG) Log.i(TAG, "determineRows() mRowHeight: " + mRowHeight + " mVerticalSpacing: " + mVerticalSpacing + " mStretchMode: " + mStretchMode); } /** * 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 selectedLeft Where the selected item should be * @param childrenLeft Where to start drawing children * @param childrenRight Last pixel where children can be drawn * @return The view that currently has selection */ private View fillFromSelection(int selectedLeft, int childrenLeft, int childrenRight) { if (DEBUG) Log.i(TAG, "fillFromSelection() selectedLeft: " + selectedLeft + " childrenLeft: " + childrenLeft + " childrenRight: " + childrenRight + " mFirstPosition: " + mFirstPosition); final int fadingEdgeLength = getHorizontalFadingEdgeLength(); final int selectedPosition = mSelectedPosition; final int numRows = mNumRows; final int horizontalSpacing = mHorizontalSpacing; int columnStart; int columnEnd = -1; if (!mStackFromBottom) { columnStart = selectedPosition - (selectedPosition % numRows); } else { int invertedSelection = mItemCount - 1 - selectedPosition; columnEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numRows)); columnStart = Math.max(0, columnEnd - numRows + 1); } View sel; View referenceView; int leftSelectionPixel = getLeftSelectionPixel(childrenLeft, fadingEdgeLength, columnStart); int rightSelectionPixel = getRightSelectionPixel(childrenRight, fadingEdgeLength, numRows, columnStart); sel = makeColumn(mStackFromBottom ? columnEnd : columnStart, selectedLeft, true); // Possibly changed again in fillLeft if we add rows above this one. mFirstPosition = columnStart; referenceView = mReferenceView; adjustForLeftFadingEdge(referenceView, leftSelectionPixel, rightSelectionPixel); adjustForRightFadingEdge(referenceView, leftSelectionPixel, rightSelectionPixel); if (!mStackFromBottom) { fillLeft(columnStart - numRows, referenceView.getLeft() - horizontalSpacing); adjustViewsLeftOrRight(); fillRight(columnStart + numRows, referenceView.getRight() + horizontalSpacing); } else { fillRight(columnEnd + numRows, referenceView.getRight() + horizontalSpacing); adjustViewsLeftOrRight(); fillLeft(columnStart - 1, referenceView.getLeft() - horizontalSpacing); } return sel; } /** * Calculate the right-most pixel we can draw the selection into * * @param childrenRight Right 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 right-most pixel we can draw the selection into */ private int getRightSelectionPixel(int childrenRight, int fadingEdgeLength, int numColumns, int rowStart) { // Last pixel we can draw the selection into int rightSelectionPixel = childrenRight; if (rowStart + numColumns - 1 < mItemCount - 1) { rightSelectionPixel -= fadingEdgeLength; } return rightSelectionPixel; } /** * Calculate the left-most pixel we can draw the selection into * * @param childrenLeft Left 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 left-most pixel we can draw the selection into */ private int getLeftSelectionPixel(int childrenLeft, int fadingEdgeLength, int rowStart) { // first pixel we can draw the selection into int leftSelectionPixel = childrenLeft; if (rowStart > 0) { leftSelectionPixel += fadingEdgeLength; } return leftSelectionPixel; } /** * Move all views left so the selected row does not interesect the right * fading edge (if necessary). * * @param childInSelectedRow A child in the row that contains the selection * @param leftSelectionPixel The leftmost pixel we can draw the selection into * @param rightSelectionPixel The rightmost pixel we can draw the * selection into */ private void adjustForRightFadingEdge(View childInSelectedRow, int leftSelectionPixel, int rightSelectionPixel) { // Some of the newly selected item extends below the bottom of the // list if (childInSelectedRow.getRight() > rightSelectionPixel) { // Find space available to the left the selection into which we can // scroll upwards int spaceToLeft = childInSelectedRow.getLeft() - leftSelectionPixel; // Find space required to bring the right of the selected item // fully into view int spaceToRight = childInSelectedRow.getRight() - rightSelectionPixel; int offset = Math.min(spaceToLeft, spaceToRight); // Now offset the selected item to get it into view offsetChildrenLeftAndRight(-offset); } } /** * Move all views right so the selected row does not interesect the left * fading edge (if necessary). * * @param childInSelectedRow A child in the row that contains the selection * @param leftSelectionPixel The leftmost pixel we can draw the selection into * @param rightSelectionPixel The rightmost pixel we can draw the * selection into */ private void adjustForLeftFadingEdge(View childInSelectedRow, int leftSelectionPixel, int rightSelectionPixel) { // Some of the newly selected item extends above the top of the list if (childInSelectedRow.getLeft() < leftSelectionPixel) { // Find space required to bring the top of the selected item // fully into view int spaceToLeft = leftSelectionPixel - childInSelectedRow.getLeft(); // Find space available below the selection into which we can // scroll downwards int spaceToRight = rightSelectionPixel - childInSelectedRow.getRight(); int offset = Math.min(spaceToLeft, spaceToRight); // Now offset the selected item to get it into view offsetChildrenLeftAndRight(offset); } } /** * Make sure views are touching the top or bottom edge, as appropriate for * our gravity */ private void adjustViewsLeftOrRight() { final int childCount = getChildCount(); if (childCount > 0) { int delta; View child; if (!mStackFromBottom) { // Uh-oh -- we came up short. Slide all views left to make them // align with the left child = getChildAt(0); delta = child.getLeft() - mListPadding.left; if (mFirstPosition != 0) { // It's OK to have some space to left the first item if it is // part of the horizontal spacing delta -= mHorizontalSpacing; } if (delta < 0) { // We only are looking to see if we are too right, not too left delta = 0; } } else { // we are too left, slide all views right to align with right child = getChildAt(childCount - 1); delta = child.getRight() - (getWidth() - mListPadding.right); if (mFirstPosition + childCount < mItemCount) { // It's OK to have some space to right of the last item if it is // part of the horizontal spacing delta += mHorizontalSpacing; } if (delta > 0) { // We only are looking to see if we are too left, not too right delta = 0; } } if (delta != 0) { offsetChildrenLeftAndRight(-delta); } } } /** * 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 x The x position relative to which this view will be positioned * @param flow if true, align left edge to x. If false, align right edge * to x. * @param childrenTop Top 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 x, boolean flow, int childrenTop, boolean selected, boolean recycled, int where) { boolean isSelected = selected && shouldShowSelector(); final boolean updateChildSelected = isSelected != child.isSelected(); final int mode = mTouchMode; final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL && mMotionPosition == position; final boolean updateChildPressed = isPressed != child.isPressed(); boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested(); // Respect layout params that are already in the view. Otherwise make // some up... TwoWayAbsListView.LayoutParams p = (TwoWayAbsListView.LayoutParams)child.getLayoutParams(); if (p == null) { p = new TwoWayAbsListView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.FILL_PARENT, 0); } p.viewType = mAdapter.getItemViewType(position); if (recycled && !p.forceAdd) { attachViewToParent(child, where, p); } else { p.forceAdd = false; addViewInLayout(child, where, p, true); } if (updateChildSelected) { child.setSelected(isSelected); if (isSelected) { requestFocus(); } } if (updateChildPressed) { child.setPressed(isPressed); } if (needToMeasure) { int childWidthSpec = ViewGroup.getChildMeasureSpec( MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, p.width); int childHeightSpec = ViewGroup.getChildMeasureSpec( MeasureSpec.makeMeasureSpec(mRowHeight, MeasureSpec.EXACTLY), 0, p.height); child.measure(childWidthSpec, childHeightSpec); } else { cleanupLayoutState(child); } final int w = child.getMeasuredWidth(); final int h = child.getMeasuredHeight(); final int childLeft = flow ? x : x - w; int childTop; switch (mGravity & Gravity.VERTICAL_GRAVITY_MASK) { case Gravity.TOP: childTop = childrenTop; break; case Gravity.CENTER_HORIZONTAL: childTop = childrenTop + ((mRowHeight - h) / 2); break; case Gravity.RIGHT: childTop = childrenTop + mRowHeight - h; break; default: childTop = childrenTop; 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()); } if (mCachingStarted) { child.setDrawingCacheEnabled(true); } } private void pinToLeft(int childrenLeft) { if (mFirstPosition == 0) { final int left = getChildAt(0).getLeft(); final int offset = childrenLeft - left; if (offset < 0) { offsetChildrenLeftAndRight(offset); } } } private void pinToRight(int childrenRight) { final int count = getChildCount(); if (mFirstPosition + count == mItemCount) { final int right = getChildAt(count - 1).getRight(); final int offset = childrenRight - right; if (offset > 0) { offsetChildrenLeftAndRight(offset); } } } /** * Makes the item at the supplied position selected. * * @param position the position of the new selection */ @Override protected void setSelectionInt(int position) { int previousSelectedPosition = mNextSelectedPosition; setNextSelectedPositionInt(position); TwoWayGridView.this.layoutChildren(); final int next = mStackFromBottom ? mItemCount - 1 - mNextSelectedPosition : mNextSelectedPosition; final int previous = mStackFromBottom ? mItemCount - 1 - previousSelectedPosition : previousSelectedPosition; final int nextColumn = next / mNumRows; final int previousColumn = previous / mNumRows; if (nextColumn != previousColumn) { //awakenScrollBars(); } } /** * Scrolls to the next or previous item, horizontally or vertically. * * @param direction either {@link android.view.View#FOCUS_LEFT}, {@link android.view.View#FOCUS_RIGHT}, * {@link android.view.View#FOCUS_UP} or {@link android.view.View#FOCUS_DOWN} * * @return whether selection was moved */ @Override protected boolean arrowScroll(int direction) { final int selectedPosition = mSelectedPosition; final int numRows = mNumRows; int startOfColumnPos; int endOfColumnPos; boolean moved = false; if (!mStackFromBottom) { startOfColumnPos = (selectedPosition / numRows) * numRows; endOfColumnPos = Math.min(startOfColumnPos + numRows - 1, mItemCount - 1); } else { final int invertedSelection = mItemCount - 1 - selectedPosition; endOfColumnPos = mItemCount - 1 - (invertedSelection / numRows) * numRows; startOfColumnPos = Math.max(0, endOfColumnPos - numRows + 1); } switch (direction) { case FOCUS_LEFT: if (startOfColumnPos > 0) { mLayoutMode = LAYOUT_MOVE_SELECTION; setSelectionInt(Math.max(0, selectedPosition - numRows)); moved = true; } break; case FOCUS_RIGHT: if (startOfColumnPos < mItemCount - 1) { mLayoutMode = LAYOUT_MOVE_SELECTION; setSelectionInt(Math.min(selectedPosition + numRows, mItemCount - 1)); moved = true; } break; case FOCUS_UP: if (selectedPosition > startOfColumnPos) { mLayoutMode = LAYOUT_MOVE_SELECTION; setSelectionInt(Math.max(0, selectedPosition - 1)); moved = true; } break; case FOCUS_DOWN: if (selectedPosition < endOfColumnPos) { mLayoutMode = LAYOUT_MOVE_SELECTION; setSelectionInt(Math.min(selectedPosition + 1, mItemCount - 1)); moved = true; } break; } if (moved) { playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); invokeOnItemScrollListener(); } if (moved) { //awakenScrollBars(); } return moved; } /** * 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. */ @Override protected boolean isCandidateSelection(int childIndex, int direction) { final int count = getChildCount(); final int invertedIndex = count - 1 - childIndex; final int numRows = mNumRows; int columnStart; int columnEnd; if (!mStackFromBottom) { columnStart = childIndex - (childIndex % numRows); columnEnd = Math.max(columnStart + numRows - 1, count); } else { columnEnd = count - 1 - (invertedIndex - (invertedIndex % numRows)); columnStart = Math.max(0, columnEnd - numRows + 1); } switch (direction) { case View.FOCUS_RIGHT: // coming from left, selection is only valid if it is on left // edge return childIndex == columnStart; case View.FOCUS_DOWN: // coming from top; only valid if in top row return columnStart == 0; case View.FOCUS_LEFT: // coming from right, must be on right edge return childIndex == columnStart; case View.FOCUS_UP: // coming from bottom, need to be in last row return columnStart == count - 1; case View.FOCUS_FORWARD: // coming from top-left, need to be first in top row return childIndex == columnStart && columnStart == 0; case View.FOCUS_BACKWARD: // coming from bottom-right, need to be last in bottom row return childIndex == columnEnd && columnEnd == count - 1; default: throw new IllegalArgumentException("direction must be one of " + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, " + "FOCUS_FORWARD, FOCUS_BACKWARD}."); } } } }