scrap = mScrapViews[i];
final int scrapCount = scrap.size();
for (int j = 0; j < scrapCount; j++) {
scrap.get(i).setDrawingCacheBackgroundColor(color);
}
}
}
// Just in case this is called during a layout pass
final View[] activeViews = mActiveViews;
final int count = activeViews.length;
for (int i = 0; i < count; ++i) {
final View victim = activeViews[i];
if (victim != null) {
victim.setDrawingCacheBackgroundColor(color);
}
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Touch Handler
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
abstract class TouchHandler {
/**
* Handles scrolling between positions within the list.
*/
protected PositionScroller mPositionScroller;
/**
* Handles one frame of a fling
*/
protected FlingRunnable mFlingRunnable;
/**
* How far the finger moved before we started scrolling
*/
int mMotionCorrection;
public void onWindowFocusChanged(boolean hasWindowFocus) {
final int touchMode = isInTouchMode() ? TOUCH_MODE_ON : TOUCH_MODE_OFF;
if (!hasWindowFocus) {
setChildrenDrawingCacheEnabled(false);
if (mFlingRunnable != null) {
removeCallbacks(mFlingRunnable);
// let the fling runnable report it's new state which
// should be idle
mFlingRunnable.endFling();
//TODO: this doesn't seem the right way to do this.
if (getScrollY() != 0) {
scrollTo(getScrollX(), 0);
//mScrollY = 0;
invalidate();
}
}
// Always hide the type filter
//dismissPopup();
if (touchMode == TOUCH_MODE_OFF) {
// Remember the last selected element
mResurrectToPosition = mSelectedPosition;
}
} else {
//if (mFiltered && !mPopupHidden) {
// Show the type filter only if a filter is in effect
// showPopup();
//}
// If we changed touch mode since the last time we had focus
if (touchMode != mLastTouchMode && mLastTouchMode != TOUCH_MODE_UNKNOWN) {
// If we come back in trackball mode, we bring the selection back
if (touchMode == TOUCH_MODE_OFF) {
// This will trigger a layout
resurrectSelection();
// If we come back in touch mode, then we want to hide the selector
} else {
hideSelector();
mLayoutMode = LAYOUT_NORMAL;
layoutChildren();
}
}
}
mLastTouchMode = touchMode;
}
public boolean startScrollIfNeeded(int delta) {
// Check if we have moved far enough that it looks more like a
// scroll than a tap
final int distance = Math.abs(delta);
if (distance > mTouchSlop) {
createScrollingCache();
mTouchMode = TOUCH_MODE_SCROLL;
mMotionCorrection = delta;
final Handler handler = getHandler();
// Handler should not be null unless the TwoWayAbsListView is not attached to a
// window, which would make it very hard to scroll it... but the monkeys
// say it's possible.
if (handler != null) {
handler.removeCallbacks(mPendingCheckForLongPress);
}
setPressed(false);
View motionView = getChildAt(mMotionPosition - mFirstPosition);
if (motionView != null) {
motionView.setPressed(false);
}
reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
// Time to start stealing events! Once we've stolen them, don't let anyone
// steal from us
requestDisallowInterceptTouchEvent(true);
return true;
}
return false;
}
public void onTouchModeChanged(boolean isInTouchMode) {
if (isInTouchMode) {
// Get rid of the selection when we enter touch mode
hideSelector();
// Layout, but only if we already have done so previously.
// (Otherwise may clobber a LAYOUT_SYNC layout that was requested to restore
// state.)
if (getHeight() > 0 && getChildCount() > 0) {
// We do not lose focus initiating a touch (since TwoWayAbsListView is focusable in
// touch mode). Force an initial layout to get rid of the selection.
layoutChildren();
}
}
}
/**
* Fires an "on scroll state changed" event to the registered
* {@link com.jess.ui.TwoWayAbsListView.OnScrollListener}, if any. The state change
* is fired only if the specified state is different from the previously known state.
*
* @param newState The new scroll state.
*/
void reportScrollStateChange(int newState) {
if (newState != mLastScrollState) {
if (mOnScrollListener != null) {
mOnScrollListener.onScrollStateChanged(TwoWayAbsListView.this, newState);
mLastScrollState = newState;
}
}
}
/**
* Smoothly scroll to the specified adapter position. The view will
* scroll such that the indicated position is displayed.
* @param position Scroll to this adapter position.
*/
public void smoothScrollToPosition(int position) {
if (mPositionScroller == null) {
mPositionScroller = getPositionScroller();
}
mPositionScroller.start(position);
}
/**
* Smoothly scroll to the specified adapter position. The view will
* scroll such that the indicated position is displayed, but it will
* stop early if scrolling further would scroll boundPosition out of
* view.
* @param position Scroll to this adapter position.
* @param boundPosition Do not scroll if it would move this adapter
* position out of view.
*/
public void smoothScrollToPosition(int position, int boundPosition) {
if (mPositionScroller == null) {
mPositionScroller = getPositionScroller();
}
mPositionScroller.start(position, boundPosition);
}
/**
* Smoothly scroll by distance pixels over duration milliseconds.
* @param distance Distance to scroll in pixels.
* @param duration Duration of the scroll animation in milliseconds.
*/
public void smoothScrollBy(int distance, int duration) {
if (mFlingRunnable == null) {
mFlingRunnable = getFlingRunnable();
} else {
mFlingRunnable.endFling();
}
mFlingRunnable.startScroll(distance, duration);
}
protected void createScrollingCache() {
if (mScrollingCacheEnabled && !mCachingStarted) {
setChildrenDrawnWithCacheEnabled(true);
setChildrenDrawingCacheEnabled(true);
mCachingStarted = true;
}
}
protected void clearScrollingCache() {
if (mClearScrollingCache == null) {
mClearScrollingCache = new Runnable() {
public void run() {
if (mCachingStarted) {
mCachingStarted = false;
setChildrenDrawnWithCacheEnabled(false);
if ((TwoWayAbsListView.this.getPersistentDrawingCache() & PERSISTENT_SCROLLING_CACHE) == 0) {
setChildrenDrawingCacheEnabled(false);
}
if (!isAlwaysDrawnWithCacheEnabled()) {
invalidate();
}
}
}
};
}
post(mClearScrollingCache);
}
/**
* Track a motion scroll
*
* @param delta Amount to offset mMotionView. This is the accumulated delta since the motion
* began. Positive numbers mean the user's finger is moving down or right on the screen.
* @param incrementalDelta Change in delta from the previous event.
* @return true if we're already at the beginning/end of the list and have nothing to do.
*/
abstract boolean trackMotionScroll(int delta, int incrementalDelta);
/**
* Attempt to bring the selection back if the user is switching from touch
* to trackball mode
* @return Whether selection was set to something.
*/
abstract boolean resurrectSelection();
public abstract boolean onTouchEvent(MotionEvent ev);
public abstract boolean onInterceptTouchEvent(MotionEvent ev);
protected abstract PositionScroller getPositionScroller();
protected abstract FlingRunnable getFlingRunnable();
/**
* Responsible for fling behavior. Use {@link #start(int)} to
* initiate a fling. Each frame of the fling is handled in {@link #run()}.
* A FlingRunnable will keep re-posting itself until the fling is done.
*
*/
protected abstract class FlingRunnable implements Runnable {
/**
* Tracks the decay of a fling scroll
*/
protected final Scroller mScroller;
protected Runnable mCheckFlywheel;
public boolean isScrollingInDirection(float xvel, float yvel) {
final int dx = mScroller.getFinalX() - mScroller.getStartX();
final int dy = mScroller.getFinalY() - mScroller.getStartY();
return !mScroller.isFinished() && Math.signum(xvel) == Math.signum(dx) &&
Math.signum(yvel) == Math.signum(dy);
}
FlingRunnable() {
mScroller = new Scroller(getContext());
}
abstract void flywheelTouch();
abstract void start(int initialVelocity);
abstract void startScroll(int distance, int duration);
protected void endFling() {
mTouchMode = TOUCH_MODE_REST;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
clearScrollingCache();
removeCallbacks(this);
if (mCheckFlywheel != null) {
removeCallbacks(mCheckFlywheel);
}
if (mPositionScroller != null) {
removeCallbacks(mPositionScroller);
}
mScroller.abortAnimation();
}
public abstract void run();
}
abstract class PositionScroller implements Runnable {
protected static final int SCROLL_DURATION = 400;
protected static final int MOVE_DOWN_POS = 1;
protected static final int MOVE_UP_POS = 2;
protected static final int MOVE_DOWN_BOUND = 3;
protected static final int MOVE_UP_BOUND = 4;
protected boolean mVertical;
protected int mMode;
protected int mTargetPos;
protected int mBoundPos;
protected int mLastSeenPos;
protected int mScrollDuration;
protected final int mExtraScroll;
PositionScroller() {
mExtraScroll = ViewConfiguration.get(mContext).getScaledFadingEdgeLength();
}
void start(int position) {
final int firstPos = mFirstPosition;
final int lastPos = firstPos + getChildCount() - 1;
int viewTravelCount = 0;
if (position <= firstPos) {
viewTravelCount = firstPos - position + 1;
mMode = MOVE_UP_POS;
} else if (position >= lastPos) {
viewTravelCount = position - lastPos + 1;
mMode = MOVE_DOWN_POS;
} else {
// Already on screen, nothing to do
return;
}
if (viewTravelCount > 0) {
mScrollDuration = SCROLL_DURATION / viewTravelCount;
} else {
mScrollDuration = SCROLL_DURATION;
}
mTargetPos = position;
mBoundPos = INVALID_POSITION;
mLastSeenPos = INVALID_POSITION;
post(this);
}
void start(int position, int boundPosition) {
if (boundPosition == INVALID_POSITION) {
start(position);
return;
}
final int firstPos = mFirstPosition;
final int lastPos = firstPos + getChildCount() - 1;
int viewTravelCount = 0;
if (position <= firstPos) {
final int boundPosFromLast = lastPos - boundPosition;
if (boundPosFromLast < 1) {
// Moving would shift our bound position off the screen. Abort.
return;
}
final int posTravel = firstPos - position + 1;
final int boundTravel = boundPosFromLast - 1;
if (boundTravel < posTravel) {
viewTravelCount = boundTravel;
mMode = MOVE_UP_BOUND;
} else {
viewTravelCount = posTravel;
mMode = MOVE_UP_POS;
}
} else if (position >= lastPos) {
final int boundPosFromFirst = boundPosition - firstPos;
if (boundPosFromFirst < 1) {
// Moving would shift our bound position off the screen. Abort.
return;
}
final int posTravel = position - lastPos + 1;
final int boundTravel = boundPosFromFirst - 1;
if (boundTravel < posTravel) {
viewTravelCount = boundTravel;
mMode = MOVE_DOWN_BOUND;
} else {
viewTravelCount = posTravel;
mMode = MOVE_DOWN_POS;
}
} else {
// Already on screen, nothing to do
return;
}
if (viewTravelCount > 0) {
mScrollDuration = SCROLL_DURATION / viewTravelCount;
} else {
mScrollDuration = SCROLL_DURATION;
}
mTargetPos = position;
mBoundPos = boundPosition;
mLastSeenPos = INVALID_POSITION;
post(this);
}
void stop() {
removeCallbacks(this);
}
public abstract void run();
}
}
////////////////////////////////////////////////////////////////////////////////////////
// Vertical Touch Handler
////////////////////////////////////////////////////////////////////////////////////////
class VerticalTouchHandler extends TouchHandler {
/**
* The offset to the top of the mMotionPosition view when the down motion event was received
*/
int mMotionViewOriginalTop;
/**
* Y value from on the previous motion event (if any)
*/
int mLastY;
/**
* The desired offset to the top of the mMotionPosition view after a scroll
*/
int mMotionViewNewTop;
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (!isEnabled()) {
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return isClickable() || isLongClickable();
}
/*
if (mFastScroller != null) {
boolean intercepted = mFastScroller.onTouchEvent(ev);
if (intercepted) {
return true;
}
}*/
final int action = ev.getAction();
View v;
int deltaY;
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
switch (action) {
case MotionEvent.ACTION_DOWN: {
final int x = (int) ev.getX();
final int y = (int) ev.getY();
int motionPosition = pointToPosition(x, y);
if (!mDataChanged) {
if ((mTouchMode != TOUCH_MODE_FLING) && (motionPosition >= 0)
&& (getAdapter().isEnabled(motionPosition))) {
// User clicked on an actual view (and was not stopping a fling). It might be a
// click or a scroll. Assume it is a click until proven otherwise
mTouchMode = TOUCH_MODE_DOWN;
// FIXME Debounce
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
if (ev.getEdgeFlags() != 0 && motionPosition < 0) {
// If we couldn't find a view to click on, but the down event was touching
// the edge, we will bail out and try again. This allows the edge correcting
// code in ViewRoot to try to find a nearby view to select
return false;
}
if (mTouchMode == TOUCH_MODE_FLING) {
// Stopped a fling. It is a scroll.
createScrollingCache();
mTouchMode = TOUCH_MODE_SCROLL;
mMotionCorrection = 0;
motionPosition = findMotionRowY(y);
mFlingRunnable.flywheelTouch();
}
}
}
if (motionPosition >= 0) {
// Remember where the motion event started
v = getChildAt(motionPosition - mFirstPosition);
mMotionViewOriginalTop = v.getTop();
}
mMotionX = x;
mMotionY = y;
mMotionPosition = motionPosition;
mLastY = Integer.MIN_VALUE;
break;
}
case MotionEvent.ACTION_MOVE: {
final int y = (int) ev.getY();
deltaY = y - mMotionY;
switch (mTouchMode) {
case TOUCH_MODE_DOWN:
case TOUCH_MODE_TAP:
case TOUCH_MODE_DONE_WAITING:
// Check if we have moved far enough that it looks more like a
// scroll than a tap
startScrollIfNeeded(deltaY);
break;
case TOUCH_MODE_SCROLL:
if (PROFILE_SCROLLING) {
if (!mScrollProfilingStarted) {
Debug.startMethodTracing("JessAbsListViewScroll");
mScrollProfilingStarted = true;
}
}
if (y != mLastY) {
deltaY -= mMotionCorrection;
int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY;
// No need to do all this work if we're not going to move anyway
boolean atEdge = false;
if (incrementalDeltaY != 0) {
atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
}
// Check to see if we have bumped into the scroll limit
if (atEdge && getChildCount() > 0) {
// Treat this like we're starting a new scroll from the current
// position. This will let the user start scrolling back into
// content immediately rather than needing to scroll back to the
// point where they hit the limit first.
int motionPosition = findMotionRowY(y);
if (motionPosition >= 0) {
final View motionView = getChildAt(motionPosition - mFirstPosition);
mMotionViewOriginalTop = motionView.getTop();
}
mMotionY = y;
mMotionPosition = motionPosition;
invalidate();
}
mLastY = y;
}
break;
}
break;
}
case MotionEvent.ACTION_UP: {
switch (mTouchMode) {
case TOUCH_MODE_DOWN:
case TOUCH_MODE_TAP:
case TOUCH_MODE_DONE_WAITING:
final int motionPosition = mMotionPosition;
final View child = getChildAt(motionPosition - mFirstPosition);
if (child != null && !child.hasFocusable()) {
if (mTouchMode != TOUCH_MODE_DOWN) {
child.setPressed(false);
}
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
final TwoWayAbsListView.PerformClick performClick = mPerformClick;
performClick.mChild = child;
performClick.mClickMotionPosition = motionPosition;
performClick.rememberWindowAttachCount();
mResurrectToPosition = motionPosition;
if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
final Handler handler = getHandler();
if (handler != null) {
handler.removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ?
mPendingCheckForTap : mPendingCheckForLongPress);
}
mLayoutMode = LAYOUT_NORMAL;
if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
mTouchMode = TOUCH_MODE_TAP;
setSelectedPositionInt(mMotionPosition);
layoutChildren();
child.setPressed(true);
positionSelector(child);
setPressed(true);
if (mSelector != null) {
Drawable d = mSelector.getCurrent();
if (d != null && d instanceof TransitionDrawable) {
((TransitionDrawable) d).resetTransition();
}
}
postDelayed(new Runnable() {
public void run() {
child.setPressed(false);
setPressed(false);
if (!mDataChanged) {
post(performClick);
}
mTouchMode = TOUCH_MODE_REST;
}
}, ViewConfiguration.getPressedStateDuration());
} else {
mTouchMode = TOUCH_MODE_REST;
}
return true;
} else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
post(performClick);
}
}
mTouchMode = TOUCH_MODE_REST;
break;
case TOUCH_MODE_SCROLL:
final int childCount = getChildCount();
if (childCount > 0) {
if (mFirstPosition == 0 && getChildAt(0).getTop() >= mListPadding.top &&
mFirstPosition + childCount < mItemCount &&
getChildAt(childCount - 1).getBottom() <=
getHeight() - mListPadding.bottom) {
mTouchMode = TOUCH_MODE_REST;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
} else {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000);
final int initialVelocity = (int) velocityTracker.getYVelocity();
if (Math.abs(initialVelocity) > mMinimumVelocity) {
if (mFlingRunnable == null) {
mFlingRunnable = new VerticalFlingRunnable();
}
reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
mFlingRunnable.start(-initialVelocity);
} else {
mTouchMode = TOUCH_MODE_REST;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
}
}
} else {
mTouchMode = TOUCH_MODE_REST;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
}
break;
}
setPressed(false);
// Need to redraw since we probably aren't drawing the selector anymore
invalidate();
final Handler handler = getHandler();
if (handler != null) {
handler.removeCallbacks(mPendingCheckForLongPress);
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
mActivePointerId = INVALID_POINTER;
if (PROFILE_SCROLLING) {
if (mScrollProfilingStarted) {
Debug.stopMethodTracing();
mScrollProfilingStarted = false;
}
}
break;
}
case MotionEvent.ACTION_CANCEL: {
mTouchMode = TOUCH_MODE_REST;
setPressed(false);
View motionView = TwoWayAbsListView.this.getChildAt(mMotionPosition - mFirstPosition);
if (motionView != null) {
motionView.setPressed(false);
}
clearScrollingCache();
final Handler handler = getHandler();
if (handler != null) {
handler.removeCallbacks(mPendingCheckForLongPress);
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
mActivePointerId = INVALID_POINTER;
break;
}
}
return true;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
View v;
/*
if (mFastScroller != null) {
boolean intercepted = mFastScroller.onInterceptTouchEvent(ev);
if (intercepted) {
return true;
}
}*/
switch (action) {
case MotionEvent.ACTION_DOWN: {
int touchMode = mTouchMode;
final int x = (int) ev.getX();
final int y = (int) ev.getY();
int motionPosition = findMotionRowY(y);
if (touchMode != TOUCH_MODE_FLING && motionPosition >= 0) {
// User clicked on an actual view (and was not stopping a fling).
// Remember where the motion event started
v = getChildAt(motionPosition - mFirstPosition);
mMotionViewOriginalTop = v.getTop();
mMotionX = x;
mMotionY = y;
mMotionPosition = motionPosition;
mTouchMode = TOUCH_MODE_DOWN;
clearScrollingCache();
}
mLastY = Integer.MIN_VALUE;
initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);
if (touchMode == TOUCH_MODE_FLING) {
return true;
}
break;
}
case MotionEvent.ACTION_MOVE: {
switch (mTouchMode) {
case TOUCH_MODE_DOWN:
final int y = (int) ev.getY();
if (startScrollIfNeeded(y - mMotionY)) {
return true;
}
break;
}
break;
}
case MotionEvent.ACTION_UP: {
mTouchMode = TOUCH_MODE_REST;
mActivePointerId = INVALID_POINTER;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
break;
}
}
return false;
}
/**
* Track a motion scroll
*
* @param deltaY Amount to offset mMotionView. This is the accumulated delta since the motion
* began. Positive numbers mean the user's finger is moving down the screen.
* @param incrementalDeltaY Change in deltaY from the previous event.
* @return true if we're already at the beginning/end of the list and have nothing to do.
*/
@Override
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
if (DEBUG) Log.i(TAG, "trackMotionScroll() - deltaY: " + deltaY + " incrDeltaY: " + incrementalDeltaY);
final int childCount = getChildCount();
if (childCount == 0) {
return true;
}
final int firstTop = getChildAt(0).getTop();
final int lastBottom = getChildAt(childCount - 1).getBottom();
final Rect listPadding = mListPadding;
// FIXME account for grid vertical spacing too?
final int spaceAbove = listPadding.top - firstTop;
final int end = getHeight() - listPadding.bottom;
final int spaceBelow = lastBottom - end;
final int height = getHeight() - getPaddingBottom() - getPaddingTop();
if (deltaY < 0) {
deltaY = Math.max(-(height - 1), deltaY);
} else {
deltaY = Math.min(height - 1, deltaY);
}
if (incrementalDeltaY < 0) {
incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
} else {
incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
}
final int firstPosition = mFirstPosition;
if (firstPosition == 0 && firstTop >= listPadding.top && deltaY >= 0) {
// Don't need to move views down if the top of the first position
// is already visible
return true;
}
if (firstPosition + childCount == mItemCount && lastBottom <= end && deltaY <= 0) {
// Don't need to move views up if the bottom of the last position
// is already visible
return true;
}
final boolean down = incrementalDeltaY < 0;
final boolean inTouchMode = isInTouchMode();
if (inTouchMode) {
hideSelector();
}
final int headerViewsCount = getHeaderViewsCount();
final int footerViewsStart = mItemCount - getFooterViewsCount();
int start = 0;
int count = 0;
if (down) {
final int top = listPadding.top - incrementalDeltaY;
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (child.getBottom() >= top) {
break;
} else {
count++;
int position = firstPosition + i;
if (position >= headerViewsCount && position < footerViewsStart) {
mRecycler.addScrapView(child);
if (ViewDebug.TRACE_RECYCLER) {
ViewDebug.trace(child,
ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP,
firstPosition + i, -1);
}
}
}
}
} else {
final int bottom = getHeight() - listPadding.bottom - incrementalDeltaY;
for (int i = childCount - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (child.getTop() <= bottom) {
break;
} else {
start = i;
count++;
int position = firstPosition + i;
if (position >= headerViewsCount && position < footerViewsStart) {
mRecycler.addScrapView(child);
if (ViewDebug.TRACE_RECYCLER) {
ViewDebug.trace(child,
ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP,
firstPosition + i, -1);
}
}
}
}
}
mMotionViewNewTop = mMotionViewOriginalTop + deltaY;
mBlockLayoutRequests = true;
if (count > 0) {
detachViewsFromParent(start, count);
}
offsetChildrenTopAndBottom(incrementalDeltaY);
if (down) {
mFirstPosition += count;
}
invalidate();
final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
fillGap(down);
}
if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
final int childIndex = mSelectedPosition - mFirstPosition;
if (childIndex >= 0 && childIndex < getChildCount()) {
positionSelector(getChildAt(childIndex));
}
}
mBlockLayoutRequests = false;
invokeOnItemScrollListener();
//awakenScrollBars();
return false;
}
/**
* Attempt to bring the selection back if the user is switching from touch
* to trackball mode
* @return Whether selection was set to something.
*/
@Override
boolean resurrectSelection() {
final int childCount = getChildCount();
if (childCount <= 0) {
return false;
}
int selectedTop = 0;
int selectedPos;
int childrenTop = mListPadding.top;
int childrenBottom = getBottom() - getTop() - mListPadding.bottom;
final int firstPosition = mFirstPosition;
final int toPosition = mResurrectToPosition;
boolean down = true;
if (toPosition >= firstPosition && toPosition < firstPosition + childCount) {
selectedPos = toPosition;
final View selected = getChildAt(selectedPos - mFirstPosition);
selectedTop = selected.getTop();
int selectedBottom = selected.getBottom();
// We are scrolled, don't get in the fade
if (selectedTop < childrenTop) {
selectedTop = childrenTop + getVerticalFadingEdgeLength();
} else if (selectedBottom > childrenBottom) {
selectedTop = childrenBottom - selected.getMeasuredHeight()
- getVerticalFadingEdgeLength();
}
} else {
if (toPosition < firstPosition) {
// Default to selecting whatever is first
selectedPos = firstPosition;
for (int i = 0; i < childCount; i++) {
final View v = getChildAt(i);
final int top = v.getTop();
if (i == 0) {
// Remember the position of the first item
selectedTop = top;
// See if we are scrolled at all
if (firstPosition > 0 || top < childrenTop) {
// If we are scrolled, don't select anything that is
// in the fade region
childrenTop += getVerticalFadingEdgeLength();
}
}
if (top >= childrenTop) {
// Found a view whose top is fully visisble
selectedPos = firstPosition + i;
selectedTop = top;
break;
}
}
} else {
final int itemCount = mItemCount;
down = false;
selectedPos = firstPosition + childCount - 1;
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
final int top = v.getTop();
final int bottom = v.getBottom();
if (i == childCount - 1) {
selectedTop = top;
if (firstPosition + childCount < itemCount || bottom > childrenBottom) {
childrenBottom -= getVerticalFadingEdgeLength();
}
}
if (bottom <= childrenBottom) {
selectedPos = firstPosition + i;
selectedTop = top;
break;
}
}
}
}
mResurrectToPosition = INVALID_POSITION;
removeCallbacks(mFlingRunnable);
mTouchMode = TOUCH_MODE_REST;
clearScrollingCache();
mSpecificTop = selectedTop;
selectedPos = lookForSelectablePosition(selectedPos, down);
if (selectedPos >= firstPosition && selectedPos <= getLastVisiblePosition()) {
mLayoutMode = LAYOUT_SPECIFIC;
setSelectionInt(selectedPos);
invokeOnItemScrollListener();
} else {
selectedPos = INVALID_POSITION;
}
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
return selectedPos >= 0;
}
@Override
protected PositionScroller getPositionScroller() {
return new VerticalPositionScroller();
}
@Override
protected FlingRunnable getFlingRunnable() {
return new VerticalFlingRunnable();
}
/**
* Responsible for fling behavior. Use {@link #start(int)} to
* initiate a fling. Each frame of the fling is handled in {@link #run()}.
* A FlingRunnable will keep re-posting itself until the fling is done.
*
*/
private class VerticalFlingRunnable extends FlingRunnable {
/**
* Y value reported by mScroller on the previous fling
*/
protected int mLastFlingY;
@Override
void start(int initialVelocity) {
int initialY = initialVelocity < 0 ? Integer.MAX_VALUE : 0;
mLastFlingY = initialY;
mScroller.fling(0, initialY, 0, initialVelocity,
0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE);
mTouchMode = TOUCH_MODE_FLING;
post(this);
if (PROFILE_FLINGING) {
if (!mFlingProfilingStarted) {
Debug.startMethodTracing("AbsListViewFling");
mFlingProfilingStarted = true;
}
}
}
@Override
void startScroll(int distance, int duration) {
int initialY = distance < 0 ? Integer.MAX_VALUE : 0;
mLastFlingY = initialY;
mScroller.startScroll(0, initialY, 0, distance, duration);
mTouchMode = TOUCH_MODE_FLING;
post(this);
}
@Override
public void run() {
switch (mTouchMode) {
default:
return;
case TOUCH_MODE_FLING: {
if (mItemCount == 0 || getChildCount() == 0) {
endFling();
return;
}
final Scroller scroller = mScroller;
boolean more = scroller.computeScrollOffset();
final int y = scroller.getCurrY();
// Flip sign to convert finger direction to list items direction
// (e.g. finger moving down means list is moving towards the top)
int delta = mLastFlingY - y;
// Pretend that each frame of a fling scroll is a touch scroll
if (delta > 0) {
// List is moving towards the top. Use first view as mMotionPosition
mMotionPosition = mFirstPosition;
final View firstView = getChildAt(0);
mMotionViewOriginalTop = firstView.getTop();
// Don't fling more than 1 screen
delta = Math.min(getHeight() - getPaddingBottom() - getPaddingTop() - 1, delta);
} else {
// List is moving towards the bottom. Use last view as mMotionPosition
int offsetToLast = getChildCount() - 1;
mMotionPosition = mFirstPosition + offsetToLast;
final View lastView = getChildAt(offsetToLast);
mMotionViewOriginalTop = lastView.getTop();
// Don't fling more than 1 screen
delta = Math.max(-(getHeight() - getPaddingBottom() - getPaddingTop() - 1), delta);
}
final boolean atEnd = trackMotionScroll(delta, delta);
if (more && !atEnd) {
invalidate();
mLastFlingY = y;
post(this);
} else {
endFling();
if (PROFILE_FLINGING) {
if (mFlingProfilingStarted) {
Debug.stopMethodTracing();
mFlingProfilingStarted = false;
}
}
}
break;
}
}
}
private static final int FLYWHEEL_TIMEOUT = 40; // milliseconds
public void flywheelTouch() {
if(mCheckFlywheel == null) {
mCheckFlywheel = new Runnable() {
public void run() {
final VelocityTracker vt = mVelocityTracker;
if (vt == null) {
return;
}
vt.computeCurrentVelocity(1000, mMaximumVelocity);
final float yvel = -vt.getYVelocity();
if (Math.abs(yvel) >= mMinimumVelocity
&& isScrollingInDirection(0, yvel)) {
// Keep the fling alive a little longer
postDelayed(this, FLYWHEEL_TIMEOUT);
} else {
endFling();
mTouchMode = TOUCH_MODE_SCROLL;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
}
}
};
}
postDelayed(mCheckFlywheel, FLYWHEEL_TIMEOUT);
}
}
class VerticalPositionScroller extends PositionScroller {
@Override
public void run() {
final int listHeight = getHeight();
final int firstPos = mFirstPosition;
switch (mMode) {
case MOVE_DOWN_POS: {
final int lastViewIndex = getChildCount() - 1;
final int lastPos = firstPos + lastViewIndex;
if (lastViewIndex < 0) {
return;
}
if (lastPos == mLastSeenPos) {
// No new views, let things keep going.
post(this);
return;
}
final View lastView = getChildAt(lastViewIndex);
final int lastViewHeight = lastView.getHeight();
final int lastViewTop = lastView.getTop();
final int lastViewPixelsShowing = listHeight - lastViewTop;
final int extraScroll = lastPos < mItemCount - 1 ? mExtraScroll : mListPadding.bottom;
smoothScrollBy(lastViewHeight - lastViewPixelsShowing + extraScroll,
mScrollDuration);
mLastSeenPos = lastPos;
if (lastPos < mTargetPos) {
post(this);
}
break;
}
case MOVE_DOWN_BOUND: {
final int nextViewIndex = 1;
final int childCount = getChildCount();
if (firstPos == mBoundPos || childCount <= nextViewIndex
|| firstPos + childCount >= mItemCount) {
return;
}
final int nextPos = firstPos + nextViewIndex;
if (nextPos == mLastSeenPos) {
// No new views, let things keep going.
post(this);
return;
}
final View nextView = getChildAt(nextViewIndex);
final int nextViewHeight = nextView.getHeight();
final int nextViewTop = nextView.getTop();
final int extraScroll = mExtraScroll;
if (nextPos < mBoundPos) {
smoothScrollBy(Math.max(0, nextViewHeight + nextViewTop - extraScroll),
mScrollDuration);
mLastSeenPos = nextPos;
post(this);
} else {
if (nextViewTop > extraScroll) {
smoothScrollBy(nextViewTop - extraScroll, mScrollDuration);
}
}
break;
}
case MOVE_UP_POS: {
if (firstPos == mLastSeenPos) {
// No new views, let things keep going.
post(this);
return;
}
final View firstView = getChildAt(0);
if (firstView == null) {
return;
}
final int firstViewTop = firstView.getTop();
final int extraScroll = firstPos > 0 ? mExtraScroll : mListPadding.top;
smoothScrollBy(firstViewTop - extraScroll, mScrollDuration);
mLastSeenPos = firstPos;
if (firstPos > mTargetPos) {
post(this);
}
break;
}
case MOVE_UP_BOUND: {
final int lastViewIndex = getChildCount() - 2;
if (lastViewIndex < 0) {
return;
}
final int lastPos = firstPos + lastViewIndex;
if (lastPos == mLastSeenPos) {
// No new views, let things keep going.
post(this);
return;
}
final View lastView = getChildAt(lastViewIndex);
final int lastViewHeight = lastView.getHeight();
final int lastViewTop = lastView.getTop();
final int lastViewPixelsShowing = listHeight - lastViewTop;
mLastSeenPos = lastPos;
if (lastPos > mBoundPos) {
smoothScrollBy(-(lastViewPixelsShowing - mExtraScroll), mScrollDuration);
post(this);
} else {
final int bottom = listHeight - mExtraScroll;
final int lastViewBottom = lastViewTop + lastViewHeight;
if (bottom > lastViewBottom) {
smoothScrollBy(-(bottom - lastViewBottom), mScrollDuration);
}
}
break;
}
default:
break;
}
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Horizontal Touch Handler
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
class HorizontalTouchHandler extends TouchHandler {
/**
* The offset to the top of the mMotionPosition view when the down motion event was received
*/
int mMotionViewOriginalLeft;
/**
* X value from on the previous motion event (if any)
*/
int mLastX;
/**
* The desired offset to the top of the mMotionPosition view after a scroll
*/
int mMotionViewNewLeft;
@Override
protected FlingRunnable getFlingRunnable() {
return new HorizontalFlingRunnable();
}
@Override
protected PositionScroller getPositionScroller() {
return new HorizontalPositionScroller();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
View v;
/*
if (mFastScroller != null) {
boolean intercepted = mFastScroller.onInterceptTouchEvent(ev);
if (intercepted) {
return true;
}
}*/
switch (action) {
case MotionEvent.ACTION_DOWN: {
int touchMode = mTouchMode;
final int x = (int) ev.getX();
final int y = (int) ev.getY();
int motionPosition = findMotionRowX(x);
if (touchMode != TOUCH_MODE_FLING && motionPosition >= 0) {
// User clicked on an actual view (and was not stopping a fling).
// Remember where the motion event started
v = getChildAt(motionPosition - mFirstPosition);
mMotionViewOriginalLeft = v.getLeft();
mMotionX = x;
mMotionY = y;
mMotionPosition = motionPosition;
mTouchMode = TOUCH_MODE_DOWN;
clearScrollingCache();
}
mLastX = Integer.MIN_VALUE;
initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);
if (touchMode == TOUCH_MODE_FLING) {
return true;
}
break;
}
case MotionEvent.ACTION_MOVE: {
switch (mTouchMode) {
case TOUCH_MODE_DOWN:
final int x = (int) ev.getX();
if (startScrollIfNeeded(x - mMotionX)) {
return true;
}
break;
}
break;
}
case MotionEvent.ACTION_UP: {
mTouchMode = TOUCH_MODE_REST;
mActivePointerId = INVALID_POINTER;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
break;
}
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (!isEnabled()) {
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return isClickable() || isLongClickable();
}
/*
if (mFastScroller != null) {
boolean intercepted = mFastScroller.onTouchEvent(ev);
if (intercepted) {
return true;
}
}*/
final int action = ev.getAction();
View v;
int deltaX;
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
switch (action) {
case MotionEvent.ACTION_DOWN: {
final int x = (int) ev.getX();
final int y = (int) ev.getY();
int motionPosition = pointToPosition(x, y);
if (!mDataChanged) {
if ((mTouchMode != TOUCH_MODE_FLING) && (motionPosition >= 0)
&& (getAdapter().isEnabled(motionPosition))) {
// User clicked on an actual view (and was not stopping a fling). It might be a
// click or a scroll. Assume it is a click until proven otherwise
mTouchMode = TOUCH_MODE_DOWN;
// FIXME Debounce
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
if (ev.getEdgeFlags() != 0 && motionPosition < 0) {
// If we couldn't find a view to click on, but the down event was touching
// the edge, we will bail out and try again. This allows the edge correcting
// code in ViewRoot to try to find a nearby view to select
return false;
}
if (mTouchMode == TOUCH_MODE_FLING) {
// Stopped a fling. It is a scroll.
createScrollingCache();
mTouchMode = TOUCH_MODE_SCROLL;
mMotionCorrection = 0;
motionPosition = findMotionRowX(x);
mFlingRunnable.flywheelTouch();
}
}
}
if (motionPosition >= 0) {
// Remember where the motion event started
v = getChildAt(motionPosition - mFirstPosition);
mMotionViewOriginalLeft = v.getLeft();
}
mMotionX = x;
mMotionY = y;
mMotionPosition = motionPosition;
mLastX = Integer.MIN_VALUE;
break;
}
case MotionEvent.ACTION_MOVE: {
final int x = (int) ev.getX();
deltaX = x - mMotionX;
switch (mTouchMode) {
case TOUCH_MODE_DOWN:
case TOUCH_MODE_TAP:
case TOUCH_MODE_DONE_WAITING:
// Check if we have moved far enough that it looks more like a
// scroll than a tap
startScrollIfNeeded(deltaX);
break;
case TOUCH_MODE_SCROLL:
if (PROFILE_SCROLLING) {
if (!mScrollProfilingStarted) {
Debug.startMethodTracing("JessAbsListViewScroll");
mScrollProfilingStarted = true;
}
}
if (x != mLastX) {
deltaX -= mMotionCorrection;
int incrementalDeltaX = mLastX != Integer.MIN_VALUE ? x - mLastX : deltaX;
// No need to do all this work if we're not going to move anyway
boolean atEdge = false;
if (incrementalDeltaX != 0) {
atEdge = trackMotionScroll(deltaX, incrementalDeltaX);
}
// Check to see if we have bumped into the scroll limit
if (atEdge && getChildCount() > 0) {
// Treat this like we're starting a new scroll from the current
// position. This will let the user start scrolling back into
// content immediately rather than needing to scroll back to the
// point where they hit the limit first.
int motionPosition = findMotionRowX(x);
if (motionPosition >= 0) {
final View motionView = getChildAt(motionPosition - mFirstPosition);
mMotionViewOriginalLeft = motionView.getLeft();
}
mMotionX = x;
mMotionPosition = motionPosition;
invalidate();
}
mLastX = x;
}
break;
}
break;
}
case MotionEvent.ACTION_UP: {
switch (mTouchMode) {
case TOUCH_MODE_DOWN:
case TOUCH_MODE_TAP:
case TOUCH_MODE_DONE_WAITING:
final int motionPosition = mMotionPosition;
final View child = getChildAt(motionPosition - mFirstPosition);
if (child != null && !child.hasFocusable()) {
if (mTouchMode != TOUCH_MODE_DOWN) {
child.setPressed(false);
}
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
final TwoWayAbsListView.PerformClick performClick = mPerformClick;
performClick.mChild = child;
performClick.mClickMotionPosition = motionPosition;
performClick.rememberWindowAttachCount();
mResurrectToPosition = motionPosition;
if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
final Handler handler = getHandler();
if (handler != null) {
handler.removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ?
mPendingCheckForTap : mPendingCheckForLongPress);
}
mLayoutMode = LAYOUT_NORMAL;
if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
mTouchMode = TOUCH_MODE_TAP;
setSelectedPositionInt(mMotionPosition);
layoutChildren();
child.setPressed(true);
positionSelector(child);
setPressed(true);
if (mSelector != null) {
Drawable d = mSelector.getCurrent();
if (d != null && d instanceof TransitionDrawable) {
((TransitionDrawable) d).resetTransition();
}
}
postDelayed(new Runnable() {
public void run() {
child.setPressed(false);
setPressed(false);
if (!mDataChanged) {
post(performClick);
}
mTouchMode = TOUCH_MODE_REST;
}
}, ViewConfiguration.getPressedStateDuration());
} else {
mTouchMode = TOUCH_MODE_REST;
}
return true;
} else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
post(performClick);
}
}
mTouchMode = TOUCH_MODE_REST;
break;
case TOUCH_MODE_SCROLL:
final int childCount = getChildCount();
if (childCount > 0) {
if (mFirstPosition == 0 && getChildAt(0).getLeft() >= mListPadding.left &&
mFirstPosition + childCount < mItemCount &&
getChildAt(childCount - 1).getRight() <=
getWidth() - mListPadding.right) {
mTouchMode = TOUCH_MODE_REST;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
} else {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000);
final int initialVelocity = (int) velocityTracker.getXVelocity();
if (Math.abs(initialVelocity) > mMinimumVelocity) {
if (mFlingRunnable == null) {
mFlingRunnable = new HorizontalFlingRunnable();
}
reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
mFlingRunnable.start(-initialVelocity);
} else {
mTouchMode = TOUCH_MODE_REST;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
}
}
} else {
mTouchMode = TOUCH_MODE_REST;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
}
break;
}
setPressed(false);
// Need to redraw since we probably aren't drawing the selector anymore
invalidate();
final Handler handler = getHandler();
if (handler != null) {
handler.removeCallbacks(mPendingCheckForLongPress);
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
mActivePointerId = INVALID_POINTER;
if (PROFILE_SCROLLING) {
if (mScrollProfilingStarted) {
Debug.stopMethodTracing();
mScrollProfilingStarted = false;
}
}
break;
}
case MotionEvent.ACTION_CANCEL: {
mTouchMode = TOUCH_MODE_REST;
setPressed(false);
View motionView = TwoWayAbsListView.this.getChildAt(mMotionPosition - mFirstPosition);
if (motionView != null) {
motionView.setPressed(false);
}
clearScrollingCache();
final Handler handler = getHandler();
if (handler != null) {
handler.removeCallbacks(mPendingCheckForLongPress);
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
mActivePointerId = INVALID_POINTER;
break;
}
}
return true;
}
@Override
boolean resurrectSelection() {
final int childCount = getChildCount();
if (childCount <= 0) {
return false;
}
int selectedLeft = 0;
int selectedPos;
int childrenLeft = mListPadding.top;
int childrenRight = getRight() - getLeft() - mListPadding.right;
final int firstPosition = mFirstPosition;
final int toPosition = mResurrectToPosition;
boolean down = true;
if (toPosition >= firstPosition && toPosition < firstPosition + childCount) {
selectedPos = toPosition;
final View selected = getChildAt(selectedPos - mFirstPosition);
selectedLeft = selected.getLeft();
int selectedRight = selected.getRight();
// We are scrolled, don't get in the fade
if (selectedLeft < childrenLeft) {
selectedLeft = childrenLeft + getHorizontalFadingEdgeLength();
} else if (selectedRight > childrenRight) {
selectedLeft = childrenRight - selected.getMeasuredWidth()
- getHorizontalFadingEdgeLength();
}
} else {
if (toPosition < firstPosition) {
// Default to selecting whatever is first
selectedPos = firstPosition;
for (int i = 0; i < childCount; i++) {
final View v = getChildAt(i);
final int left = v.getLeft();
if (i == 0) {
// Remember the position of the first item
selectedLeft = left;
// See if we are scrolled at all
if (firstPosition > 0 || left < childrenLeft) {
// If we are scrolled, don't select anything that is
// in the fade region
childrenLeft += getHorizontalFadingEdgeLength();
}
}
if (left >= childrenLeft) {
// Found a view whose top is fully visisble
selectedPos = firstPosition + i;
selectedLeft = left;
break;
}
}
} else {
final int itemCount = mItemCount;
down = false;
selectedPos = firstPosition + childCount - 1;
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
final int left = v.getLeft();
final int right = v.getRight();
if (i == childCount - 1) {
selectedLeft = left;
if (firstPosition + childCount < itemCount || right > childrenRight) {
childrenRight -= getHorizontalFadingEdgeLength();
}
}
if (right <= childrenRight) {
selectedPos = firstPosition + i;
selectedLeft = left;
break;
}
}
}
}
mResurrectToPosition = INVALID_POSITION;
removeCallbacks(mFlingRunnable);
mTouchMode = TOUCH_MODE_REST;
clearScrollingCache();
mSpecificTop = selectedLeft;
selectedPos = lookForSelectablePosition(selectedPos, down);
if (selectedPos >= firstPosition && selectedPos <= getLastVisiblePosition()) {
mLayoutMode = LAYOUT_SPECIFIC;
setSelectionInt(selectedPos);
invokeOnItemScrollListener();
} else {
selectedPos = INVALID_POSITION;
}
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
return selectedPos >= 0;
}
@Override
boolean trackMotionScroll(int delta, int incrementalDelta) {
if (DEBUG) Log.i(TAG, "trackMotionScroll() - deltaX: " + delta + " incrDeltaX: " + incrementalDelta);
final int childCount = getChildCount();
if (childCount == 0) {
return true;
}
final int firstLeft = getChildAt(0).getLeft();
final int lastRight = getChildAt(childCount - 1).getRight();
final Rect listPadding = mListPadding;
// FIXME account for grid horizontal spacing too?
final int spaceAbove = listPadding.left - firstLeft;
final int end = getWidth() - listPadding.right;
final int spaceBelow = lastRight - end;
final int width = getWidth() - getPaddingRight() - getPaddingLeft();
if (delta < 0) {
delta = Math.max(-(width - 1), delta);
} else {
delta = Math.min(width - 1, delta);
}
if (incrementalDelta < 0) {
incrementalDelta = Math.max(-(width - 1), incrementalDelta);
} else {
incrementalDelta = Math.min(width - 1, incrementalDelta);
}
final int firstPosition = mFirstPosition;
if (firstPosition == 0 && firstLeft >= listPadding.left && delta >= 0) {
// Don't need to move views right if the top of the first position
// is already visible
if (DEBUG) Log.i(TAG, "trackScrollMotion returning true");
return true;
}
if (firstPosition + childCount == mItemCount && lastRight <= end && delta <= 0) {
// Don't need to move views left if the bottom of the last position
// is already visible
if (DEBUG) Log.i(TAG, "trackScrollMotion returning true");
return true;
}
final boolean down = incrementalDelta < 0;
final boolean inTouchMode = isInTouchMode();
if (inTouchMode) {
hideSelector();
}
final int headerViewsCount = getHeaderViewsCount();
final int footerViewsStart = mItemCount - getFooterViewsCount();
int start = 0;
int count = 0;
if (down) {
final int left = listPadding.left - incrementalDelta;
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (child.getRight() >= left) {
break;
} else {
count++;
int position = firstPosition + i;
if (position >= headerViewsCount && position < footerViewsStart) {
mRecycler.addScrapView(child);
if (ViewDebug.TRACE_RECYCLER) {
ViewDebug.trace(child,
ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP,
firstPosition + i, -1);
}
}
}
}
} else {
final int right = getWidth() - listPadding.right - incrementalDelta;
for (int i = childCount - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (child.getLeft() <= right) {
break;
} else {
start = i;
count++;
int position = firstPosition + i;
if (position >= headerViewsCount && position < footerViewsStart) {
mRecycler.addScrapView(child);
if (ViewDebug.TRACE_RECYCLER) {
ViewDebug.trace(child,
ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP,
firstPosition + i, -1);
}
}
}
}
}
mMotionViewNewLeft = mMotionViewOriginalLeft + delta;
mBlockLayoutRequests = true;
if (count > 0) {
detachViewsFromParent(start, count);
}
offsetChildrenLeftAndRight(incrementalDelta);
if (down) {
mFirstPosition += count;
}
invalidate();
final int absIncrementalDelta = Math.abs(incrementalDelta);
if (spaceAbove < absIncrementalDelta|| spaceBelow < absIncrementalDelta) {
fillGap(down);
}
if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
final int childIndex = mSelectedPosition - mFirstPosition;
if (childIndex >= 0 && childIndex < getChildCount()) {
positionSelector(getChildAt(childIndex));
}
}
mBlockLayoutRequests = false;
invokeOnItemScrollListener();
//awakenScrollBars();
if (DEBUG) Log.i(TAG, "trackScrollMotion returning false - mFirstPosition: " + mFirstPosition);
return false;
}
/**
* Responsible for fling behavior. Use {@link #start(int)} to
* initiate a fling. Each frame of the fling is handled in {@link #run()}.
* A FlingRunnable will keep re-posting itself until the fling is done.
*
*/
private class HorizontalFlingRunnable extends FlingRunnable {
/**
* X value reported by mScroller on the previous fling
*/
protected int mLastFlingX;
@Override
void start(int initialVelocity) {
int initialX = initialVelocity < 0 ? Integer.MAX_VALUE : 0;
mLastFlingX = initialX;
mScroller.fling(initialX, 0, initialVelocity, 0,
0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE);
mTouchMode = TOUCH_MODE_FLING;
post(this);
if (PROFILE_FLINGING) {
if (!mFlingProfilingStarted) {
Debug.startMethodTracing("AbsListViewFling");
mFlingProfilingStarted = true;
}
}
}
@Override
void startScroll(int distance, int duration) {
int initialX = distance < 0 ? Integer.MAX_VALUE : 0;
mLastFlingX = initialX;
mScroller.startScroll(initialX, 0, distance, 0, duration);
mTouchMode = TOUCH_MODE_FLING;
post(this);
}
@Override
public void run() {
switch (mTouchMode) {
default:
return;
case TOUCH_MODE_FLING: {
if (mItemCount == 0 || getChildCount() == 0) {
endFling();
return;
}
final Scroller scroller = mScroller;
boolean more = scroller.computeScrollOffset();
final int x = scroller.getCurrX();
// Flip sign to convert finger direction to list items direction
// (e.g. finger moving down means list is moving towards the top)
int delta = mLastFlingX - x;
// Pretend that each frame of a fling scroll is a touch scroll
if (delta > 0) {
// List is moving towards the top. Use first view as mMotionPosition
mMotionPosition = mFirstPosition;
final View firstView = getChildAt(0);
mMotionViewOriginalLeft = firstView.getLeft();
// Don't fling more than 1 screen
delta = Math.min(getWidth() - getPaddingRight() - getPaddingLeft() - 1, delta);
} else {
// List is moving towards the bottom. Use last view as mMotionPosition
int offsetToLast = getChildCount() - 1;
mMotionPosition = mFirstPosition + offsetToLast;
final View lastView = getChildAt(offsetToLast);
mMotionViewOriginalLeft = lastView.getLeft();
// Don't fling more than 1 screen
delta = Math.max(-(getWidth() - getPaddingRight() - getPaddingLeft() - 1), delta);
}
final boolean atEnd = trackMotionScroll(delta, delta);
if (more && !atEnd) {
invalidate();
mLastFlingX = x;
post(this);
} else {
endFling();
if (PROFILE_FLINGING) {
if (mFlingProfilingStarted) {
Debug.stopMethodTracing();
mFlingProfilingStarted = false;
}
}
}
break;
}
}
}
private static final int FLYWHEEL_TIMEOUT = 40; // milliseconds
public void flywheelTouch() {
if(mCheckFlywheel == null) {
mCheckFlywheel = new Runnable() {
public void run() {
final VelocityTracker vt = mVelocityTracker;
if (vt == null) {
return;
}
vt.computeCurrentVelocity(1000, mMaximumVelocity);
final float xvel = -vt.getXVelocity();
if (Math.abs(xvel) >= mMinimumVelocity
&& isScrollingInDirection(0, xvel)) {
// Keep the fling alive a little longer
postDelayed(this, FLYWHEEL_TIMEOUT);
} else {
endFling();
mTouchMode = TOUCH_MODE_SCROLL;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
}
}
};
}
postDelayed(mCheckFlywheel, FLYWHEEL_TIMEOUT);
}
}
class HorizontalPositionScroller extends PositionScroller {
@Override
public void run() {
final int listWidth = getWidth();
final int firstPos = mFirstPosition;
switch (mMode) {
case MOVE_DOWN_POS: {
final int lastViewIndex = getChildCount() - 1;
final int lastPos = firstPos + lastViewIndex;
if (lastViewIndex < 0) {
return;
}
if (lastPos == mLastSeenPos) {
// No new views, let things keep going.
post(this);
return;
}
final View lastView = getChildAt(lastViewIndex);
final int lastViewWidth = lastView.getWidth();
final int lastViewLeft = lastView.getLeft();
final int lastViewPixelsShowing = listWidth - lastViewLeft;
final int extraScroll = lastPos < mItemCount - 1 ? mExtraScroll : mListPadding.right;
smoothScrollBy(lastViewWidth - lastViewPixelsShowing + extraScroll,
mScrollDuration);
mLastSeenPos = lastPos;
if (lastPos < mTargetPos) {
post(this);
}
break;
}
case MOVE_DOWN_BOUND: {
final int nextViewIndex = 1;
final int childCount = getChildCount();
if (firstPos == mBoundPos || childCount <= nextViewIndex
|| firstPos + childCount >= mItemCount) {
return;
}
final int nextPos = firstPos + nextViewIndex;
if (nextPos == mLastSeenPos) {
// No new views, let things keep going.
post(this);
return;
}
final View nextView = getChildAt(nextViewIndex);
final int nextViewWidth = nextView.getWidth();
final int nextViewLeft = nextView.getLeft();
final int extraScroll = mExtraScroll;
if (nextPos < mBoundPos) {
smoothScrollBy(Math.max(0, nextViewWidth + nextViewLeft - extraScroll),
mScrollDuration);
mLastSeenPos = nextPos;
post(this);
} else {
if (nextViewLeft > extraScroll) {
smoothScrollBy(nextViewLeft - extraScroll, mScrollDuration);
}
}
break;
}
case MOVE_UP_POS: {
if (firstPos == mLastSeenPos) {
// No new views, let things keep going.
post(this);
return;
}
final View firstView = getChildAt(0);
if (firstView == null) {
return;
}
final int firstViewLeft = firstView.getLeft();
final int extraScroll = firstPos > 0 ? mExtraScroll : mListPadding.left;
smoothScrollBy(firstViewLeft - extraScroll, mScrollDuration);
mLastSeenPos = firstPos;
if (firstPos > mTargetPos) {
post(this);
}
break;
}
case MOVE_UP_BOUND: {
final int lastViewIndex = getChildCount() - 2;
if (lastViewIndex < 0) {
return;
}
final int lastPos = firstPos + lastViewIndex;
if (lastPos == mLastSeenPos) {
// No new views, let things keep going.
post(this);
return;
}
final View lastView = getChildAt(lastViewIndex);
final int lastViewWidth = lastView.getWidth();
final int lastViewLeft = lastView.getLeft();
final int lastViewPixelsShowing = listWidth - lastViewLeft;
mLastSeenPos = lastPos;
if (lastPos > mBoundPos) {
smoothScrollBy(-(lastViewPixelsShowing - mExtraScroll), mScrollDuration);
post(this);
} else {
final int right = listWidth - mExtraScroll;
final int lastViewRight = lastViewLeft + lastViewWidth;
if (right > lastViewRight) {
smoothScrollBy(-(right - lastViewRight), mScrollDuration);
}
}
break;
}
default:
break;
}
}
}
}
private void initOrResetVelocityTracker() {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
} else {
mVelocityTracker.clear();
}
}
}
================================================
FILE: lib/src/com/jess/ui/TwoWayAdapterView.java
================================================
/*
* A modified version of the Android AdapterView
*
* 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.jess.ui;
import android.content.Context;
import android.database.DataSetObserver;
import android.os.Handler;
import android.os.Parcelable;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.ContextMenu;
import android.view.SoundEffectConstants;
import android.view.View;
import android.view.ViewDebug;
import android.view.ViewGroup;
import android.view.ContextMenu.ContextMenuInfo;
import android.widget.Adapter;
/**
* A TwoWayAdapterView is a view whose children are determined by an {@link Adapter}.
*
*
* See {@link TwoWayAbsListView} and {@link TwoWayGridView} for commonly used subclasses of TwoWayAdapterView.
*/
public abstract class TwoWayAdapterView extends ViewGroup {
/**
* The item view type returned by {@link Adapter#getItemViewType(int)} when
* the adapter does not want the item's view recycled.
*/
public static final int ITEM_VIEW_TYPE_IGNORE = -1;
/**
* The item view type returned by {@link Adapter#getItemViewType(int)} when
* the item is a header or footer.
*/
public static final int ITEM_VIEW_TYPE_HEADER_OR_FOOTER = -2;
protected Context mContext = null;
/**
* Whether the view displays its items vertically or horizontally
*/
protected boolean mIsVertical = true;
/**
* The position of the first child displayed
*/
@ViewDebug.ExportedProperty
int mFirstPosition = 0;
/**
* The offset in pixels from the top of the AdapterView to the top or left
* of the view to select during the next layout.
*/
int mSpecificTop;
/**
* Position from which to start looking for mSyncRowId
*/
int mSyncPosition;
/**
* Row id to look for when data has changed
*/
long mSyncRowId = INVALID_ROW_ID;
/**
* Size of the view when mSyncPosition and mSyncRowId where set
*/
long mSyncSize;
/**
* True if we need to sync to mSyncRowId
*/
boolean mNeedSync = false;
/**
* Indicates whether to sync based on the selection or position. Possible
* values are {@link #SYNC_SELECTED_POSITION} or
* {@link #SYNC_FIRST_POSITION}.
*/
int mSyncMode;
/**
* Our height after the last layout
*/
private int mLayoutHeight;
/**
* Our width after the last layout
*/
private int mLayoutWidth;
/**
* Sync based on the selected child
*/
static final int SYNC_SELECTED_POSITION = 0;
/**
* Sync based on the first child displayed
*/
static final int SYNC_FIRST_POSITION = 1;
/**
* Maximum amount of time to spend in {@link #findSyncPosition()}
*/
static final int SYNC_MAX_DURATION_MILLIS = 100;
/**
* Indicates that this view is currently being laid out.
*/
boolean mInLayout = false;
/**
* The listener that receives notifications when an item is selected.
*/
OnItemSelectedListener mOnItemSelectedListener;
/**
* The listener that receives notifications when an item is clicked.
*/
OnItemClickListener mOnItemClickListener;
/**
* The listener that receives notifications when an item is long clicked.
*/
OnItemLongClickListener mOnItemLongClickListener;
/**
* True if the data has changed since the last layout
*/
boolean mDataChanged;
/**
* The position within the adapter's data set of the item to select
* during the next layout.
*/
@ViewDebug.ExportedProperty
int mNextSelectedPosition = INVALID_POSITION;
/**
* The item id of the item to select during the next layout.
*/
long mNextSelectedRowId = INVALID_ROW_ID;
/**
* The position within the adapter's data set of the currently selected item.
*/
@ViewDebug.ExportedProperty
int mSelectedPosition = INVALID_POSITION;
/**
* The item id of the currently selected item.
*/
long mSelectedRowId = INVALID_ROW_ID;
/**
* View to show if there are no items to show.
*/
private View mEmptyView;
/**
* The number of items in the current adapter.
*/
@ViewDebug.ExportedProperty
int mItemCount;
/**
* The number of items in the adapter before a data changed event occured.
*/
int mOldItemCount;
/**
* Represents an invalid position. All valid positions are in the range 0 to 1 less than the
* number of items in the current adapter.
*/
public static final int INVALID_POSITION = -1;
/**
* Represents an empty or invalid row id
*/
public static final long INVALID_ROW_ID = Long.MIN_VALUE;
/**
* The last selected position we used when notifying
*/
int mOldSelectedPosition = INVALID_POSITION;
/**
* The id of the last selected position we used when notifying
*/
long mOldSelectedRowId = INVALID_ROW_ID;
/**
* Indicates what focusable state is requested when calling setFocusable().
* In addition to this, this view has other criteria for actually
* determining the focusable state (such as whether its empty or the text
* filter is shown).
*
* @see #setFocusable(boolean)
* @see #checkFocus()
*/
private boolean mDesiredFocusableState;
private boolean mDesiredFocusableInTouchModeState;
private SelectionNotifier mSelectionNotifier;
/**
* When set to true, calls to requestLayout() will not propagate up the parent hierarchy.
* This is used to layout the children during a layout pass.
*/
boolean mBlockLayoutRequests = false;
public TwoWayAdapterView(Context context) {
super(context);
mContext = context;
}
public TwoWayAdapterView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
}
public TwoWayAdapterView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mContext = context;
}
/**
* Interface definition for a callback to be invoked when an item in this
* AdapterView has been clicked.
*/
public interface OnItemClickListener {
/**
* Callback method to be invoked when an item in this AdapterView has
* been clicked.
*
* Implementers can call getItemAtPosition(position) if they need
* to access the data associated with the selected item.
*
* @param parent The AdapterView where the click happened.
* @param view The view within the AdapterView that was clicked (this
* will be a view provided by the adapter)
* @param position The position of the view in the adapter.
* @param id The row id of the item that was clicked.
*/
void onItemClick(TwoWayAdapterView> parent, View view, int position, long id);
}
/**
* Register a callback to be invoked when an item in this AdapterView has
* been clicked.
*
* @param listener The callback that will be invoked.
*/
public void setOnItemClickListener(OnItemClickListener listener) {
mOnItemClickListener = listener;
}
/**
* @return The callback to be invoked with an item in this AdapterView has
* been clicked, or null id no callback has been set.
*/
public final OnItemClickListener getOnItemClickListener() {
return mOnItemClickListener;
}
/**
* Call the OnItemClickListener, if it is defined.
*
* @param view The view within the AdapterView that was clicked.
* @param position The position of the view in the adapter.
* @param id The row id of the item that was clicked.
* @return True if there was an assigned OnItemClickListener that was
* called, false otherwise is returned.
*/
public boolean performItemClick(View view, int position, long id) {
if (mOnItemClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnItemClickListener.onItemClick(this, view, position, id);
return true;
}
return false;
}
/**
* Interface definition for a callback to be invoked when an item in this
* view has been clicked and held.
*/
public interface OnItemLongClickListener {
/**
* Callback method to be invoked when an item in this view has been
* clicked and held.
*
* Implementers can call getItemAtPosition(position) if they need to access
* the data associated with the selected item.
*
* @param parent The AbsListView where the click happened
* @param view The view within the AbsListView that was clicked
* @param position The position of the view in the list
* @param id The row id of the item that was clicked
*
* @return true if the callback consumed the long click, false otherwise
*/
boolean onItemLongClick(TwoWayAdapterView> parent, View view, int position, long id);
}
/**
* Register a callback to be invoked when an item in this AdapterView has
* been clicked and held
*
* @param listener The callback that will run
*/
public void setOnItemLongClickListener(OnItemLongClickListener listener) {
if (!isLongClickable()) {
setLongClickable(true);
}
mOnItemLongClickListener = listener;
}
/**
* @return The callback to be invoked with an item in this AdapterView has
* been clicked and held, or null id no callback as been set.
*/
public final OnItemLongClickListener getOnItemLongClickListener() {
return mOnItemLongClickListener;
}
/**
* Interface definition for a callback to be invoked when
* an item in this view has been selected.
*/
public interface OnItemSelectedListener {
/**
* Callback method to be invoked when an item in this view has been
* selected.
*
* Impelmenters can call getItemAtPosition(position) if they need to access the
* data associated with the selected item.
*
* @param parent The AdapterView where the selection happened
* @param view The view within the AdapterView that was clicked
* @param position The position of the view in the adapter
* @param id The row id of the item that is selected
*/
void onItemSelected(TwoWayAdapterView> parent, View view, int position, long id);
/**
* Callback method to be invoked when the selection disappears from this
* view. The selection can disappear for instance when touch is activated
* or when the adapter becomes empty.
*
* @param parent The AdapterView that now contains no selected item.
*/
void onNothingSelected(TwoWayAdapterView> parent);
}
/**
* Register a callback to be invoked when an item in this AdapterView has
* been selected.
*
* @param listener The callback that will run
*/
public void setOnItemSelectedListener(OnItemSelectedListener listener) {
mOnItemSelectedListener = listener;
}
public final OnItemSelectedListener getOnItemSelectedListener() {
return mOnItemSelectedListener;
}
/**
* Extra menu information provided to the
* {@link android.view.View.OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo) }
* callback when a context menu is brought up for this AdapterView.
*
*/
public static class AdapterContextMenuInfo implements ContextMenu.ContextMenuInfo {
public AdapterContextMenuInfo(View targetView, int position, long id) {
this.targetView = targetView;
this.position = position;
this.id = id;
}
/**
* The child view for which the context menu is being displayed. This
* will be one of the children of this AdapterView.
*/
public View targetView;
/**
* The position in the adapter for which the context menu is being
* displayed.
*/
public int position;
/**
* The row id of the item for which the context menu is being displayed.
*/
public long id;
}
/**
* Returns the adapter currently associated with this widget.
*
* @return The adapter used to provide this view's content.
*/
public abstract T getAdapter();
/**
* Sets the adapter that provides the data and the views to represent the data
* in this widget.
*
* @param adapter The adapter to use to create this view's content.
*/
public abstract void setAdapter(T adapter);
/**
* This method is not supported and throws an UnsupportedOperationException when called.
*
* @param child Ignored.
*
* @throws UnsupportedOperationException Every time this method is invoked.
*/
@Override
public void addView(View child) {
throw new UnsupportedOperationException("addView(View) is not supported in AdapterView");
}
/**
* This method is not supported and throws an UnsupportedOperationException when called.
*
* @param child Ignored.
* @param index Ignored.
*
* @throws UnsupportedOperationException Every time this method is invoked.
*/
@Override
public void addView(View child, int index) {
throw new UnsupportedOperationException("addView(View, int) is not supported in AdapterView");
}
/**
* This method is not supported and throws an UnsupportedOperationException when called.
*
* @param child Ignored.
* @param params Ignored.
*
* @throws UnsupportedOperationException Every time this method is invoked.
*/
@Override
public void addView(View child, LayoutParams params) {
throw new UnsupportedOperationException("addView(View, LayoutParams) "
+ "is not supported in AdapterView");
}
/**
* This method is not supported and throws an UnsupportedOperationException when called.
*
* @param child Ignored.
* @param index Ignored.
* @param params Ignored.
*
* @throws UnsupportedOperationException Every time this method is invoked.
*/
@Override
public void addView(View child, int index, LayoutParams params) {
throw new UnsupportedOperationException("addView(View, int, LayoutParams) "
+ "is not supported in AdapterView");
}
/**
* This method is not supported and throws an UnsupportedOperationException when called.
*
* @param child Ignored.
*
* @throws UnsupportedOperationException Every time this method is invoked.
*/
@Override
public void removeView(View child) {
throw new UnsupportedOperationException("removeView(View) is not supported in AdapterView");
}
/**
* This method is not supported and throws an UnsupportedOperationException when called.
*
* @param index Ignored.
*
* @throws UnsupportedOperationException Every time this method is invoked.
*/
@Override
public void removeViewAt(int index) {
throw new UnsupportedOperationException("removeViewAt(int) is not supported in AdapterView");
}
/**
* This method is not supported and throws an UnsupportedOperationException when called.
*
* @throws UnsupportedOperationException Every time this method is invoked.
*/
@Override
public void removeAllViews() {
throw new UnsupportedOperationException("removeAllViews() is not supported in AdapterView");
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
mLayoutHeight = getHeight();
mLayoutWidth = getWidth();
}
/**
* Return the position of the currently selected item within the adapter's data set
*
* @return int Position (starting at 0), or {@link #INVALID_POSITION} if there is nothing selected.
*/
@ViewDebug.CapturedViewProperty
public int getSelectedItemPosition() {
return mNextSelectedPosition;
}
/**
* @return The id corresponding to the currently selected item, or {@link #INVALID_ROW_ID}
* if nothing is selected.
*/
@ViewDebug.CapturedViewProperty
public long getSelectedItemId() {
return mNextSelectedRowId;
}
/**
* @return The view corresponding to the currently selected item, or null
* if nothing is selected
*/
public abstract View getSelectedView();
/**
* @return The data corresponding to the currently selected item, or
* null if there is nothing selected.
*/
public Object getSelectedItem() {
T adapter = getAdapter();
int selection = getSelectedItemPosition();
if (adapter != null && adapter.getCount() > 0 && selection >= 0) {
return adapter.getItem(selection);
} else {
return null;
}
}
/**
* @return The number of items owned by the Adapter associated with this
* AdapterView. (This is the number of data items, which may be
* larger than the number of visible view.)
*/
@ViewDebug.CapturedViewProperty
public int getCount() {
return mItemCount;
}
/**
* Get the position within the adapter's data set for the view, where view is a an adapter item
* or a descendant of an adapter item.
*
* @param view an adapter item, or a descendant of an adapter item. This must be visible in this
* AdapterView at the time of the call.
* @return the position within the adapter's data set of the view, or {@link #INVALID_POSITION}
* if the view does not correspond to a list item (or it is not currently visible).
*/
public int getPositionForView(View view) {
View listItem = view;
try {
View v;
while (!(v = (View) listItem.getParent()).equals(this)) {
listItem = v;
}
} catch (ClassCastException e) {
// We made it up to the window without find this list view
return INVALID_POSITION;
}
// Search the children for the list item
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
if (getChildAt(i).equals(listItem)) {
return mFirstPosition + i;
}
}
// Child not found!
return INVALID_POSITION;
}
/**
* Returns the position within the adapter's data set for the first item
* displayed on screen.
*
* @return The position within the adapter's data set
*/
public int getFirstVisiblePosition() {
return mFirstPosition;
}
/**
* Returns the position within the adapter's data set for the last item
* displayed on screen.
*
* @return The position within the adapter's data set
*/
public int getLastVisiblePosition() {
return mFirstPosition + getChildCount() - 1;
}
/**
* Sets the currently selected item. To support accessibility subclasses that
* override this method must invoke the overriden super method first.
*
* @param position Index (starting at 0) of the data item to be selected.
*/
public abstract void setSelection(int position);
/**
* Sets the view to show if the adapter is empty
*/
public void setEmptyView(View emptyView) {
mEmptyView = emptyView;
final T adapter = getAdapter();
final boolean empty = ((adapter == null) || adapter.isEmpty());
updateEmptyStatus(empty);
}
/**
* When the current adapter is empty, the AdapterView can display a special view
* call the empty view. The empty view is used to provide feedback to the user
* that no data is available in this AdapterView.
*
* @return The view to show if the adapter is empty.
*/
public View getEmptyView() {
return mEmptyView;
}
/**
* Indicates whether this view is in filter mode. Filter mode can for instance
* be enabled by a user when typing on the keyboard.
*
* @return True if the view is in filter mode, false otherwise.
*/
boolean isInFilterMode() {
return false;
}
@Override
public void setFocusable(boolean focusable) {
final T adapter = getAdapter();
final boolean empty = adapter == null || adapter.getCount() == 0;
mDesiredFocusableState = focusable;
if (!focusable) {
mDesiredFocusableInTouchModeState = false;
}
super.setFocusable(focusable && (!empty || isInFilterMode()));
}
@Override
public void setFocusableInTouchMode(boolean focusable) {
final T adapter = getAdapter();
final boolean empty = adapter == null || adapter.getCount() == 0;
mDesiredFocusableInTouchModeState = focusable;
if (focusable) {
mDesiredFocusableState = true;
}
super.setFocusableInTouchMode(focusable && (!empty || isInFilterMode()));
}
void checkFocus() {
final T adapter = getAdapter();
final boolean empty = adapter == null || adapter.getCount() == 0;
final boolean focusable = !empty || isInFilterMode();
// The order in which we set focusable in touch mode/focusable may matter
// for the client, see View.setFocusableInTouchMode() comments for more
// details
super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState);
super.setFocusable(focusable && mDesiredFocusableState);
if (mEmptyView != null) {
updateEmptyStatus((adapter == null) || adapter.isEmpty());
}
}
/**
* Update the status of the list based on the empty parameter. If empty is true and
* we have an empty view, display it. In all the other cases, make sure that the listview
* is VISIBLE and that the empty view is GONE (if it's not null).
*/
private void updateEmptyStatus(boolean empty) {
if (isInFilterMode()) {
empty = false;
}
if (empty) {
if (mEmptyView != null) {
mEmptyView.setVisibility(View.VISIBLE);
setVisibility(View.GONE);
} else {
// If the caller just removed our empty view, make sure the list view is visible
setVisibility(View.VISIBLE);
}
// We are now GONE, so pending layouts will not be dispatched.
// Force one here to make sure that the state of the list matches
// the state of the adapter.
if (mDataChanged) {
this.onLayout(false, getLeft(), getTop(), getRight(), getBottom());
}
} else {
if (mEmptyView != null) mEmptyView.setVisibility(View.GONE);
setVisibility(View.VISIBLE);
}
}
/**
* Gets the data associated with the specified position in the list.
*
* @param position Which data to get
* @return The data associated with the specified position in the list
*/
public Object getItemAtPosition(int position) {
T adapter = getAdapter();
return (adapter == null || position < 0) ? null : adapter.getItem(position);
}
public long getItemIdAtPosition(int position) {
T adapter = getAdapter();
return (adapter == null || position < 0) ? INVALID_ROW_ID : adapter.getItemId(position);
}
@Override
public void setOnClickListener(OnClickListener l) {
throw new RuntimeException("Don't call setOnClickListener for an AdapterView. "
+ "You probably want setOnItemClickListener instead");
}
/**
* Override to prevent freezing of any views created by the adapter.
*/
@Override
protected void dispatchSaveInstanceState(SparseArray container) {
dispatchFreezeSelfOnly(container);
}
/**
* Override to prevent thawing of any views created by the adapter.
*/
@Override
protected void dispatchRestoreInstanceState(SparseArray container) {
dispatchThawSelfOnly(container);
}
class AdapterDataSetObserver extends DataSetObserver {
private Parcelable mInstanceState = null;
@Override
public void onChanged() {
mDataChanged = true;
mOldItemCount = mItemCount;
mItemCount = getAdapter().getCount();
// Detect the case where a cursor that was previously invalidated has
// been repopulated with new data.
if (TwoWayAdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
&& mOldItemCount == 0 && mItemCount > 0) {
TwoWayAdapterView.this.onRestoreInstanceState(mInstanceState);
mInstanceState = null;
} else {
rememberSyncState();
}
checkFocus();
requestLayout();
}
@Override
public void onInvalidated() {
mDataChanged = true;
if (TwoWayAdapterView.this.getAdapter().hasStableIds()) {
// Remember the current state for the case where our hosting activity is being
// stopped and later restarted
mInstanceState = TwoWayAdapterView.this.onSaveInstanceState();
}
// Data is invalid so we should reset our state
mOldItemCount = mItemCount;
mItemCount = 0;
mSelectedPosition = INVALID_POSITION;
mSelectedRowId = INVALID_ROW_ID;
mNextSelectedPosition = INVALID_POSITION;
mNextSelectedRowId = INVALID_ROW_ID;
mNeedSync = false;
checkSelectionChanged();
checkFocus();
requestLayout();
}
public void clearSavedState() {
mInstanceState = null;
}
}
private class SelectionNotifier extends Handler implements Runnable {
public void run() {
if (mDataChanged) {
// Data has changed between when this SelectionNotifier
// was posted and now. We need to wait until the AdapterView
// has been synched to the new data.
post(this);
} else {
fireOnSelected();
}
}
}
void selectionChanged() {
if (mOnItemSelectedListener != null) {
if (mInLayout || mBlockLayoutRequests) {
// If we are in a layout traversal, defer notification
// by posting. This ensures that the view tree is
// in a consistent state and is able to accomodate
// new layout or invalidate requests.
if (mSelectionNotifier == null) {
mSelectionNotifier = new SelectionNotifier();
}
mSelectionNotifier.post(mSelectionNotifier);
} else {
fireOnSelected();
}
}
// we fire selection events here not in View
/* taken out for backward compatibility
if (mSelectedPosition != ListView.INVALID_POSITION && isShown() && !isInTouchMode()) {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
}*/
}
private void fireOnSelected() {
if (mOnItemSelectedListener == null)
return;
int selection = this.getSelectedItemPosition();
if (selection >= 0) {
View v = getSelectedView();
mOnItemSelectedListener.onItemSelected(this, v, selection,
getAdapter().getItemId(selection));
} else {
mOnItemSelectedListener.onNothingSelected(this);
}
}
/* taken out for backward compatibility
@Override
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
boolean populated = false;
// This is an exceptional case which occurs when a window gets the
// focus and sends a focus event via its focused child to announce
// current focus/selection. AdapterView fires selection but not focus
// events so we change the event type here.
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED) {
event.setEventType(AccessibilityEvent.TYPE_VIEW_SELECTED);
}
// we send selection events only from AdapterView to avoid
// generation of such event for each child
View selectedView = getSelectedView();
if (selectedView != null) {
populated = selectedView.dispatchPopulateAccessibilityEvent(event);
}
if (!populated) {
if (selectedView != null) {
event.setEnabled(selectedView.isEnabled());
}
event.setItemCount(getCount());
event.setCurrentItemIndex(getSelectedItemPosition());
}
return populated;
}*/
@Override
protected boolean canAnimate() {
return super.canAnimate() && mItemCount > 0;
}
void handleDataChanged() {
final int count = mItemCount;
boolean found = false;
if (count > 0) {
int newPos;
// Find the row we are supposed to sync to
if (mNeedSync) {
// Update this first, since setNextSelectedPositionInt inspects
// it
mNeedSync = false;
// See if we can find a position in the new data with the same
// id as the old selection
newPos = findSyncPosition();
if (newPos >= 0) {
// Verify that new selection is selectable
int selectablePos = lookForSelectablePosition(newPos, true);
if (selectablePos == newPos) {
// Same row id is selected
setNextSelectedPositionInt(newPos);
found = true;
}
}
}
if (!found) {
// Try to use the same position if we can't find matching data
newPos = getSelectedItemPosition();
// Pin position to the available range
if (newPos >= count) {
newPos = count - 1;
}
if (newPos < 0) {
newPos = 0;
}
// Make sure we select something selectable -- first look down
int selectablePos = lookForSelectablePosition(newPos, true);
if (selectablePos < 0) {
// Looking down didn't work -- try looking up
selectablePos = lookForSelectablePosition(newPos, false);
}
if (selectablePos >= 0) {
setNextSelectedPositionInt(selectablePos);
checkSelectionChanged();
found = true;
}
}
}
if (!found) {
// Nothing is selected
mSelectedPosition = INVALID_POSITION;
mSelectedRowId = INVALID_ROW_ID;
mNextSelectedPosition = INVALID_POSITION;
mNextSelectedRowId = INVALID_ROW_ID;
mNeedSync = false;
checkSelectionChanged();
}
}
void checkSelectionChanged() {
if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) {
selectionChanged();
mOldSelectedPosition = mSelectedPosition;
mOldSelectedRowId = mSelectedRowId;
}
}
/**
* Searches the adapter for a position matching mSyncRowId. The search starts at mSyncPosition
* and then alternates between moving up and moving down until 1) we find the right position, or
* 2) we run out of time, or 3) we have looked at every position
*
* @return Position of the row that matches mSyncRowId, or {@link #INVALID_POSITION} if it can't
* be found
*/
int findSyncPosition() {
int count = mItemCount;
if (count == 0) {
return INVALID_POSITION;
}
long idToMatch = mSyncRowId;
int seed = mSyncPosition;
// If there isn't a selection don't hunt for it
if (idToMatch == INVALID_ROW_ID) {
return INVALID_POSITION;
}
// Pin seed to reasonable values
seed = Math.max(0, seed);
seed = Math.min(count - 1, seed);
long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS;
long rowId;
// first position scanned so far
int first = seed;
// last position scanned so far
int last = seed;
// True if we should move down on the next iteration
boolean next = false;
// True when we have looked at the first item in the data
boolean hitFirst;
// True when we have looked at the last item in the data
boolean hitLast;
// Get the item ID locally (instead of getItemIdAtPosition), so
// we need the adapter
T adapter = getAdapter();
if (adapter == null) {
return INVALID_POSITION;
}
while (SystemClock.uptimeMillis() <= endTime) {
rowId = adapter.getItemId(seed);
if (rowId == idToMatch) {
// Found it!
return seed;
}
hitLast = last == count - 1;
hitFirst = first == 0;
if (hitLast && hitFirst) {
// Looked at everything
break;
}
if (hitFirst || (next && !hitLast)) {
// Either we hit the top, or we are trying to move down
last++;
seed = last;
// Try going up next time
next = false;
} else if (hitLast || (!next && !hitFirst)) {
// Either we hit the bottom, or we are trying to move up
first--;
seed = first;
// Try going down next time
next = true;
}
}
return INVALID_POSITION;
}
/**
* Find a position that can be selected (i.e., is not a separator).
*
* @param position The starting position to look at.
* @param lookDown Whether to look down for other positions.
* @return The next selectable position starting at position and then searching either up or
* down. Returns {@link #INVALID_POSITION} if nothing can be found.
*/
int lookForSelectablePosition(int position, boolean lookDown) {
return position;
}
/**
* Utility to keep mSelectedPosition and mSelectedRowId in sync
* @param position Our current position
*/
void setSelectedPositionInt(int position) {
mSelectedPosition = position;
mSelectedRowId = getItemIdAtPosition(position);
}
/**
* Utility to keep mNextSelectedPosition and mNextSelectedRowId in sync
* @param position Intended value for mSelectedPosition the next time we go
* through layout
*/
void setNextSelectedPositionInt(int position) {
mNextSelectedPosition = position;
mNextSelectedRowId = getItemIdAtPosition(position);
// If we are trying to sync to the selection, update that too
if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) {
mSyncPosition = position;
mSyncRowId = mNextSelectedRowId;
}
}
/**
* Remember enough information to restore the screen state when the data has
* changed.
*
*/
void rememberSyncState() {
if (getChildCount() > 0) {
mNeedSync = true;
if (mIsVertical) {
mSyncSize = mLayoutHeight;
} else {
mSyncSize = mLayoutWidth;
}
if (mSelectedPosition >= 0) {
// Sync the selection state
View v = getChildAt(mSelectedPosition - mFirstPosition);
mSyncRowId = mNextSelectedRowId;
mSyncPosition = mNextSelectedPosition;
if (v != null) {
if (mIsVertical) {
mSpecificTop = v.getTop();
} else {
mSpecificTop = v.getLeft();
}
}
mSyncMode = SYNC_SELECTED_POSITION;
} else {
// Sync the based on the offset of the first view
View v = getChildAt(0);
T adapter = getAdapter();
if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) {
mSyncRowId = adapter.getItemId(mFirstPosition);
} else {
mSyncRowId = NO_ID;
}
mSyncPosition = mFirstPosition;
if (v != null) {
if (mIsVertical) {
mSpecificTop = v.getTop();
} else {
mSpecificTop = v.getLeft();
}
}
mSyncMode = SYNC_FIRST_POSITION;
}
}
}
/**
* Offset the vertical location of all children of this view by the specified number of pixels.
*
* @param offset the number of pixels to offset
*
*/
public void offsetChildrenTopAndBottom(int offset) {
final int count = getChildCount();
for (int i = 0; i < count; i++) {
final View v = getChildAt(i);
v.offsetTopAndBottom(offset);
}
}
/**
* Offset the horizontal location of all children of this view by the specified number of pixels.
*
* @param offset the number of pixels to offset
*
*/
public void offsetChildrenLeftAndRight(int offset) {
final int count = getChildCount();
for (int i = 0; i < count; i++) {
final View v = getChildAt(i);
v.offsetLeftAndRight(offset);
}
}
protected void setIsVertical(boolean vertical) {
mIsVertical = vertical;
}
protected boolean isVertical() {
return mIsVertical;
}
}
================================================
FILE: lib/src/com/jess/ui/TwoWayGridView.java
================================================
/*
* 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.jess.ui;
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;
/**
* A view that shows items in two-dimensional scrolling grid. The items in the
* grid come from the {@link ListAdapter} associated with this view.
*
*/
public class 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 View#FOCUS_UP} or {@link 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 View#FOCUS_UP} or {@link View#FOCUS_DOWN}.
*
* @return Whether selection was moved.
*/
boolean fullScroll(int direction) {
boolean moved = false;
if (direction == FOCUS_UP) {
mLayoutMode = LAYOUT_SET_SELECTION;
setSelectionInt(0);
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 View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT},
* {@link View#FOCUS_UP} or {@link 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 View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT},
* {@link View#FOCUS_UP} or {@link 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}.");
}
}
}
}
================================================
FILE: project.properties
================================================
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system edit
# "ant.properties", and override values to adapt the script to your
# project structure.
#
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Indicates whether an apk should be generated for each density.
split.density=false
# Project target.
target=android-7
android.library=true
================================================
FILE: sample/AndroidManifest.xml
================================================
================================================
FILE: sample/ant.properties
================================================
# This file is used to override default values used by the Ant build system.
#
# This file must be checked into Version Control Systems, as it is
# integral to the build system of your project.
# This file is only used by the Ant script.
# You can use this to override default values such as
# 'source.dir' for the location of your java source folder and
# 'out.dir' for the location of your output folder.
# You can also use it define how the release builds are signed by declaring
# the following properties:
# 'key.store' for the location of your keystore and
# 'key.alias' for the name of the key to use.
# The password will be asked during the build when you use the 'release' target.
================================================
FILE: sample/proguard-project.txt
================================================
# To enable ProGuard in your project, edit project.properties
# to define the proguard.config property as described in that file.
#
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in ${sdk.dir}/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the ProGuard
# include property in project.properties.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
================================================
FILE: sample/project.properties
================================================
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system edit
# "ant.properties", and override values to adapt the script to your
# project structure.
#
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=android-16
android.library.reference.1=../lib
================================================
FILE: sample/res/layout/main.xml
================================================
================================================
FILE: sample/res/values/strings.xml
================================================
EverGrid
================================================
FILE: sample/src/com/jess/demo/BetterImageView.java
================================================
/*
* BetterImageView
*
* 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.
*/
package com.jess.demo;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.ImageView;
public class BetterImageView extends ImageView {
private static final String TAG = "BetterImageView";
private static final boolean DEBUG = false;
public BetterImageView(Context context) {
super(context);
}
public BetterImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BetterImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void invalidateDrawable(Drawable dr) {
Drawable currentDrawable = getDrawable();
if (DEBUG) Log.i(TAG, "invalidateDrawable: " + dr + " current drawable: " + currentDrawable);
if (dr == currentDrawable) {
/* we invalidate the whole view in this case because it's very
* hard to know where the drawable actually is. This is made
* complicated because of the offsets and transformations that
* can be applied. In theory we could get the drawable's bounds
* and run them through the transformation and offsets, but this
* is probably not worth the effort.
*/
Log.i(TAG, "invalidateDrawable - setting imageDrawable");
//destroyDrawingCache();
drawableStateChanged();
forceLayout();
setImageDrawable(currentDrawable);
invalidate();
} else {
super.invalidateDrawable(dr);
}
}
}
================================================
FILE: sample/src/com/jess/demo/ImageThumbnailAdapter.java
================================================
package com.jess.demo;
import java.lang.ref.SoftReference;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.net.Uri;
import android.os.Handler;
import android.provider.MediaStore;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import android.widget.ImageView;
import com.jess.ui.TwoWayAbsListView;
public class ImageThumbnailAdapter extends CursorAdapter {
public static final String[] IMAGE_PROJECTION = {
MediaStore.Images.ImageColumns._ID,
MediaStore.Images.ImageColumns.DISPLAY_NAME,
};
public static final int IMAGE_ID_COLUMN = 0;
public static final int IMAGE_NAME_COLUMN = 1;
public static final boolean DEBUG = false;
private static final String TAG = "ImageThumbnailAdapter";
private static float IMAGE_WIDTH = 80;
private static float IMAGE_HEIGHT = 80;
private static float IMAGE_PADDING = 6;
private static final Map> sImageCache =
new ConcurrentHashMap>();
private static Options sBitmapOptions = new Options();
private final Context mContext;
private Bitmap mDefaultBitmap;
private final ContentResolver mContentResolver;
private final Handler mHandler;
private float mScale;
private int mImageWidth;
private int mImageHeight;
private int mImagePadding;
public ImageThumbnailAdapter(Context context, Cursor c) {
this(context, c, true);
}
public ImageThumbnailAdapter(Context context, Cursor c, boolean autoRequery) {
super(context, c, autoRequery);
mContext = context;
init(c);
mContentResolver = context.getContentResolver();
mHandler = new Handler();
}
private void init(Cursor c) {
mDefaultBitmap = BitmapFactory.decodeResource(mContext.getResources(),
R.drawable.spinner_black_76);
mScale = mContext.getResources().getDisplayMetrics().density;
mImageWidth = (int)(IMAGE_WIDTH * mScale);
mImageHeight = (int)(IMAGE_HEIGHT * mScale);
mImagePadding = (int)(IMAGE_PADDING * mScale);
sBitmapOptions.inSampleSize = 4;
}
@Override
public int getItemViewType(int position) {
return 0;
}
@Override
public int getViewTypeCount() {
return 1;
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
int id = cursor.getInt(IMAGE_ID_COLUMN);
((ImageView)view).setImageDrawable(getCachedThumbnailAsync(
ContentUris.withAppendedId(MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI, id)));
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
ImageView imageView = new BetterImageView(mContext.getApplicationContext());
imageView.setScaleType(ImageView.ScaleType.FIT_XY);
imageView.setLayoutParams(new TwoWayAbsListView.LayoutParams(mImageWidth, mImageHeight));
imageView.setPadding(mImagePadding, mImagePadding, mImagePadding, mImagePadding);
return imageView;
}
public void cleanup() {
cleanupCache();
}
private static Bitmap loadThumbnail(ContentResolver cr, Uri uri) {
return MediaStore.Images.Thumbnails.getThumbnail(
cr, ContentUris.parseId(uri), MediaStore.Images.Thumbnails.MINI_KIND, sBitmapOptions);
}
/**
* Retrieves a drawable from the image cache, identified by the specified id.
* If the drawable does not exist in the cache, it is loaded asynchronously and added to the cache.
* If the drawable cannot be added to the cache, the specified default drawable is
* returned.
*
* @param uri The uri of the drawable to retrieve
*
* @return The drawable identified by id or defaultImage
*/
private ReplaceableBitmapDrawable getCachedThumbnailAsync(Uri uri) {
ReplaceableBitmapDrawable drawable = null;
long id = ContentUris.parseId(uri);
WorkQueue wq = WorkQueue.getInstance();
synchronized(wq.mQueue) {
SoftReference reference = sImageCache.get(id);
if (reference != null) {
drawable = reference.get();
}
if (drawable == null || !drawable.isLoaded()) {
drawable = new ReplaceableBitmapDrawable(mDefaultBitmap);
sImageCache.put(id, new SoftReference(drawable));
ImageLoadingArgs args = new ImageLoadingArgs(mContentResolver, mHandler, drawable, uri);
wq.execute(new ImageLoader(args));
}
}
return drawable;
}
/**
* Removes all the callbacks from the drawables stored in the memory cache. This
* method must be called from the onDestroy() method of any activity using the
* cached drawables. Failure to do so will result in the entire activity being
* leaked.
*/
public static void cleanupCache() {
for (SoftReference reference : sImageCache.values()) {
final ReplaceableBitmapDrawable drawable = reference.get();
if (drawable != null) drawable.setCallback(null);
}
}
/**
* Deletes the specified drawable from the cache.
*
* @param uri The uri of the drawable to delete from the cache
*/
public static void deleteCachedCover(Uri uri) {
sImageCache.remove(ContentUris.parseId(uri));
}
/**
* Class to asynchronously perform the loading of the bitmap
*/
public static class ImageLoader implements Runnable {
protected ImageLoadingArgs mArgs = null;
public ImageLoader(ImageLoadingArgs args) {
mArgs = args;
}
public void run() {
final Bitmap bitmap = loadThumbnail(mArgs.mContentResolver, mArgs.mUri);
if (DEBUG) Log.i(TAG, "run() bitmap: " + bitmap);
if (bitmap != null) {
final ReplaceableBitmapDrawable d = mArgs.mDrawable;
if (d != null) {
mArgs.mHandler.post(new Runnable() {
public void run() {
if (DEBUG) Log.i(TAG, "ImageLoader.run() - setting the bitmap for uri: " + mArgs.mUri);
d.setBitmap(bitmap);
}
});
} else {
Log.e(TAG, "ImageLoader.run() - FastBitmapDrawable is null for uri: " + mArgs.mUri);
}
} else {
Log.e(TAG, "ImageLoader.run() - bitmap is null for uri: " + mArgs.mUri);
}
}
public void cancel() {
sImageCache.remove(mArgs.mUri);
}
@Override
public boolean equals(Object obj) {
if (obj != null && obj instanceof ImageLoader) {
if (mArgs.mUri != null) {
return mArgs.mUri.equals(((ImageLoader)obj).mArgs);
}
}
return false;
}
@Override
public int hashCode() {
return mArgs.mUri.hashCode();
}
}
/**
* Class to hold all the parts necessary to load an image
*/
public static class ImageLoadingArgs {
ContentResolver mContentResolver;
Handler mHandler;
ReplaceableBitmapDrawable mDrawable;
Uri mUri;
/**
* @param contentResolver - ContentResolver to use
* @param drawable - FastBitmapDrawable whose underlying bitmap should be replaced with new bitmap
* @param uri - Uri of image location
*/
public ImageLoadingArgs(ContentResolver contentResolver, Handler handler,
ReplaceableBitmapDrawable drawable, Uri uri) {
mContentResolver = contentResolver;
mHandler = handler;
mDrawable = drawable;
mUri = uri;
}
}
public static class WorkQueue {
private static WorkQueue sInstance = null;
private static final int NUM_OF_THREADS = 1;
private static final int MAX_QUEUE_SIZE = 21;
private final int mNumOfThreads;
private final PoolWorker[] mThreads;
protected final LinkedList mQueue;
public static synchronized WorkQueue getInstance() {
if (sInstance == null) {
sInstance = new WorkQueue(NUM_OF_THREADS);
}
return sInstance;
}
private WorkQueue(int nThreads) {
mNumOfThreads = nThreads;
mQueue = new LinkedList();
mThreads = new PoolWorker[mNumOfThreads];
for (int i=0; i < mNumOfThreads; i++) {
mThreads[i] = new PoolWorker();
mThreads[i].start();
}
}
public void execute(ImageLoader r) {
synchronized(mQueue) {
mQueue.remove(r);
if (mQueue.size() > MAX_QUEUE_SIZE) {
mQueue.removeFirst().cancel();
}
mQueue.addLast(r);
mQueue.notify();
}
}
private class PoolWorker extends Thread {
private boolean mRunning = true;
@Override
public void run() {
Runnable r;
while (mRunning) {
synchronized(mQueue) {
while (mQueue.isEmpty() && mRunning) {
try
{
mQueue.wait();
}
catch (InterruptedException ignored)
{
}
}
r = mQueue.removeFirst();
}
// If we don't catch RuntimeException,
// the pool could leak threads
try {
r.run();
}
catch (RuntimeException e) {
Log.e(TAG, "RuntimeException", e);
}
}
Log.i(TAG, "PoolWorker finished");
}
public void stopWorker() {
mRunning = false;
}
}
}
}
================================================
FILE: sample/src/com/jess/demo/MainActivity.java
================================================
package com.jess.demo;
import android.app.Activity;
import android.content.ContentUris;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;
import android.view.View;
import com.jess.ui.R;
import com.jess.ui.TwoWayAdapterView;
import com.jess.ui.TwoWayAdapterView.OnItemClickListener;
import com.jess.ui.TwoWayGridView;
public class MainActivity extends Activity {
private static final String TAG = "MainActivity";
private Cursor mImageCursor;
private ImageThumbnailAdapter mAdapter;
private TwoWayGridView mImageGrid;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
initGrid();
}
private void initGrid() {
mImageCursor = managedQuery(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
ImageThumbnailAdapter.IMAGE_PROJECTION, null, null,
MediaStore.Images.ImageColumns.DISPLAY_NAME);
mImageGrid = (TwoWayGridView) findViewById(R.id.gridview);
mAdapter = new ImageThumbnailAdapter(this, mImageCursor);
mImageGrid.setAdapter(mAdapter);
mImageGrid.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(TwoWayAdapterView parent, View v, int position, long id) {
Log.i(TAG, "showing image: " + mImageCursor.getString(ImageThumbnailAdapter.IMAGE_NAME_COLUMN));
Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
mAdapter.cleanup();
}
}
================================================
FILE: sample/src/com/jess/demo/ReplaceableBitmapDrawable.java
================================================
/*
* ReplaceableBitmapDrawable
*
* 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.
*/
package com.jess.demo;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.Gravity;
public class ReplaceableBitmapDrawable extends Drawable {
private static final String TAG = "ReplaceableBitmapDrawable";
private static final boolean DEBUG = false;
private Bitmap mBitmap;
private boolean mLoaded;
private boolean mApplyGravity;
private int mGravity;
private final Rect mDstRect = new Rect();
public ReplaceableBitmapDrawable(Bitmap b) {
mBitmap = b;
}
@Override
public void draw(Canvas canvas) {
copyBounds(mDstRect);
if (mBitmap != null) {
if (mApplyGravity) {
Gravity.apply(mGravity, super.getIntrinsicWidth(), super.getIntrinsicHeight(),
getBounds(), mDstRect);
mApplyGravity = false;
}
canvas.drawBitmap(mBitmap, null, mDstRect, null);
}
}
public void setGravity(int gravity) {
mGravity = gravity;
mApplyGravity = true;
}
@Override
public int getOpacity() {
return PixelFormat.OPAQUE;
}
@Override
public void setAlpha(int alpha) {
}
@Override
public void setColorFilter(ColorFilter cf) {
}
@Override
public int getIntrinsicWidth() {
if (mBitmap != null) {
if (DEBUG) Log.i(TAG, "getIntrinsicWidth(): " + mBitmap.getWidth()+ " " + this);
return mBitmap.getWidth();
} else {
return 0;
}
}
@Override
public int getIntrinsicHeight() {
if (mBitmap != null) {
if (DEBUG) Log.i(TAG, "getIntrinsicHeight(): " + mBitmap.getHeight() + " " + this);
return mBitmap.getHeight();
} else {
return 0;
}
}
@Override
public int getMinimumWidth() {
return 0;
}
@Override
public int getMinimumHeight() {
return 0;
}
public Bitmap getBitmap() {
return mBitmap;
}
public void setBitmap(Bitmap bitmap) {
mLoaded = true;
mBitmap = bitmap;
if (DEBUG) Log.i("ReplaceableBitmapDrawable", "setBitmap() " + this);
invalidateSelf();
}
public boolean isLoaded() {
return mLoaded;
}
}