/*
* Copyright (c) 2013 Etsy
* 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.marshalchen.common.uimodule.staggeredgridview;
import android.content.Context;
import android.database.DataSetObserver;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.util.SparseArrayCompat;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.*;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.ListAdapter;
import android.widget.Scroller;
import java.util.ArrayList;
/**
* An extendable implementation of the Android {@link android.widget.ListView}
* <p/>
* This is partly inspired by the incomplete StaggeredGridView supplied in the
* Android 4.2+ source & the {@link android.widget.AbsListView} & {@link android.widget.ListView} source;
* however this is intended to have a smaller simplified
* scope of functionality and hopefully therefore be a workable solution.
* <p/>
* Some things that this doesn't support (yet)
* - Dividers (We don't use them in our Etsy grid)
* - Edge effect
* - Fading edge - yuck
* - Item selection
* - Focus
* <p/>
* Note: we only really extend {@link android.widget.AbsListView} so we can appear to be one of its direct subclasses.
* However most of the code we need to modify is either 1. hidden or 2. package private
* So a lot of it's code and some {@link android.widget.AdapterView} code is repeated here
* Be careful with this - not everything may be how you expect if you assume this to be
* a regular old {@link android.widget.ListView}
*/
public abstract class ExtendableListView extends AbsListView {
private static final String TAG = "ExtendableListView";
private static final boolean DBG = false;
private static final int TOUCH_MODE_IDLE = 0;
private static final int TOUCH_MODE_SCROLLING = 1;
private static final int TOUCH_MODE_FLINGING = 2;
private static final int TOUCH_MODE_DOWN = 3;
private static final int TOUCH_MODE_TAP = 4;
private static final int TOUCH_MODE_DONE_WAITING = 5;
private static final int INVALID_POINTER = -1;
// Layout using our default existing state
private static final int LAYOUT_NORMAL = 0;
// Layout from the first item down
private static final int LAYOUT_FORCE_TOP = 1;
// Layout from the saved instance state data
private static final int LAYOUT_SYNC = 2;
private int mLayoutMode;
private int mTouchMode;
private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE;
// Rectangle used for hit testing children
// private Rect mTouchFrame;
// TODO : ItemClick support from AdapterView
// For managing scrolling
private VelocityTracker mVelocityTracker = null;
private int mTouchSlop;
private int mMaximumVelocity;
private int mFlingVelocity;
// TODO : Edge effect handling
// private EdgeEffectCompat mEdgeGlowTop;
// private EdgeEffectCompat mEdgeGlowBottom;
// blocker for when we're in a layout pass
private boolean mInLayout;
ListAdapter mAdapter;
private int mMotionY;
private int mMotionX;
private int mMotionCorrection;
private int mMotionPosition;
private int mLastY;
private int mActivePointerId = INVALID_POINTER;
protected int mFirstPosition;
// are we attached to a window - we shouldn't handle any touch events if we're not!
private boolean mIsAttached;
/**
* 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.
*/
private boolean mBlockLayoutRequests = false;
// has our data changed - and should we react to it
private boolean mDataChanged;
private int mItemCount;
private int mOldItemCount;
final boolean[] mIsScrap = new boolean[1];
private RecycleBin mRecycleBin;
private AdapterDataSetObserver mObserver;
private int mWidthMeasureSpec;
private FlingRunnable mFlingRunnable;
protected boolean mClipToPadding;
private PerformClick mPerformClick;
private Runnable mPendingCheckForTap;
private CheckForLongPress mPendingCheckForLongPress;
private class CheckForLongPress extends WindowRunnnable implements Runnable {
public void run() {
final int motionPosition = mMotionPosition;
final View child = getChildAt(motionPosition);
if (child != null) {
final int longPressPosition = mMotionPosition;
final long longPressId = mAdapter.getItemId(mMotionPosition + mFirstPosition);
boolean handled = false;
if (sameWindow() && !mDataChanged) {
handled = performLongPress(child, longPressPosition + mFirstPosition, longPressId);
}
if (handled) {
mTouchMode = TOUCH_MODE_IDLE;
setPressed(false);
child.setPressed(false);
} else {
mTouchMode = TOUCH_MODE_DONE_WAITING;
}
}
}
}
/**
* A class that represents a fixed view in a list, for example a header at the top
* or a footer at the bottom.
*/
public class FixedViewInfo {
/**
* The view to add to the list
*/
public View view;
/**
* The data backing the view. This is returned from {@link android.widget.ListAdapter#getItem(int)}.
*/
public Object data;
/**
* <code>true</code> if the fixed view should be selectable in the list
*/
public boolean isSelectable;
}
private ArrayList<FixedViewInfo> mHeaderViewInfos;
private ArrayList<FixedViewInfo> mFooterViewInfos;
public ExtendableListView(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
// setting up to be a scrollable view group
setWillNotDraw(false);
setClipToPadding(false);
setFocusableInTouchMode(false);
final ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
mTouchSlop = viewConfiguration.getScaledTouchSlop();
mMaximumVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
mFlingVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
mRecycleBin = new RecycleBin();
mObserver = new AdapterDataSetObserver();
mHeaderViewInfos = new ArrayList<FixedViewInfo>();
mFooterViewInfos = new ArrayList<FixedViewInfo>();
// start our layout mode drawing from the top
mLayoutMode = LAYOUT_NORMAL;
}
// //////////////////////////////////////////////////////////////////////////////////////////
// MAINTAINING SOME STATE
//
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (mAdapter != null) {
// Data may have changed while we were detached. Refresh.
mDataChanged = true;
mOldItemCount = mItemCount;
mItemCount = mAdapter.getCount();
}
mIsAttached = true;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
// Detach any view left in the scrap heap
mRecycleBin.clear();
if (mFlingRunnable != null) {
removeCallbacks(mFlingRunnable);
}
mIsAttached = false;
}
@Override
protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
// TODO : handle focus and its impact on selection - if we add item selection support
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
// TODO : handle focus and its impact on selection - if we add item selection support
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
onSizeChanged(w, h);
}
protected void onSizeChanged(int w, int h) {
if (getChildCount() > 0) {
stopFlingRunnable();
mRecycleBin.clear();
mDataChanged = true;
rememberSyncState();
}
}
// //////////////////////////////////////////////////////////////////////////////////////////
// ADAPTER
//
@Override
public ListAdapter getAdapter() {
return mAdapter;
}
@Override
public void setAdapter(final ListAdapter adapter) {
if (mAdapter != null) {
mAdapter.unregisterDataSetObserver(mObserver);
}
// use a wrapper list adapter if we have a header or footer
if (mHeaderViewInfos.size() > 0 || mFooterViewInfos.size() > 0) {
mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
}
else {
mAdapter = adapter;
}
mDataChanged = true;
mItemCount = mAdapter != null ? mAdapter.getCount() : 0;
if (mAdapter != null) {
mAdapter.registerDataSetObserver(mObserver);
mRecycleBin.setViewTypeCount(mAdapter.getViewTypeCount());
}
requestLayout();
}
@Override
public int getCount() {
return mItemCount;
}
// //////////////////////////////////////////////////////////////////////////////////////////
// ADAPTER VIEW - UNSUPPORTED
//
@Override
public View getSelectedView() {
if (DBG) Log.e(TAG, "getSelectedView() is not supported in ExtendableListView yet");
return null;
}
@Override
public void setSelection(final int position) {
if (position >= 0) {
mLayoutMode = LAYOUT_SYNC;
mSpecificTop = getListPaddingTop();
mFirstPosition = 0;
if (mNeedSync) {
mSyncPosition = position;
mSyncRowId = mAdapter.getItemId(position);
}
requestLayout();
}
}
// //////////////////////////////////////////////////////////////////////////////////////////
// HEADER & FOOTER
//
/**
* Add a fixed view to appear at the top of the list. If addHeaderView is
* called more than once, the views will appear in the order they were
* added. Views added using this call can take focus if they want.
* <p/>
* NOTE: Call this before calling setAdapter. This is so ListView can wrap
* the supplied cursor with one that will also account for header and footer
* views.
*
* @param v The view to add.
* @param data Data to associate with this view
* @param isSelectable whether the item is selectable
*/
public void addHeaderView(View v, Object data, boolean isSelectable) {
if (mAdapter != null && !(mAdapter instanceof HeaderViewListAdapter)) {
throw new IllegalStateException(
"Cannot add header view to list -- setAdapter has already been called.");
}
FixedViewInfo info = new FixedViewInfo();
info.view = v;
info.data = data;
info.isSelectable = isSelectable;
mHeaderViewInfos.add(info);
// in the case of re-adding a header view, or adding one later on,
// we need to notify the observer
if (mAdapter != null && mObserver != null) {
mObserver.onChanged();
}
}
/**
* Add a fixed view to appear at the top of the list. If addHeaderView is
* called more than once, the views will appear in the order they were
* added. Views added using this call can take focus if they want.
* <p/>
* NOTE: Call this before calling setAdapter. This is so ListView can wrap
* the supplied cursor with one that will also account for header and footer
* views.
*
* @param v The view to add.
*/
public void addHeaderView(View v) {
addHeaderView(v, null, true);
}
public int getHeaderViewsCount() {
return mHeaderViewInfos.size();
}
/**
* Removes a previously-added header view.
*
* @param v The view to remove
* @return true if the view was removed, false if the view was not a header
* view
*/
public boolean removeHeaderView(View v) {
if (mHeaderViewInfos.size() > 0) {
boolean result = false;
if (mAdapter != null && ((HeaderViewListAdapter) mAdapter).removeHeader(v)) {
if (mObserver != null) {
mObserver.onChanged();
}
result = true;
}
removeFixedViewInfo(v, mHeaderViewInfos);
return result;
}
return false;
}
private void removeFixedViewInfo(View v, ArrayList<FixedViewInfo> where) {
int len = where.size();
for (int i = 0; i < len; ++i) {
FixedViewInfo info = where.get(i);
if (info.view == v) {
where.remove(i);
break;
}
}
}
/**
* Add a fixed view to appear at the bottom of the list. If addFooterView is
* called more than once, the views will appear in the order they were
* added. Views added using this call can take focus if they want.
* <p/>
* NOTE: Call this before calling setAdapter. This is so ListView can wrap
* the supplied cursor with one that will also account for header and footer
* views.
*
* @param v The view to add.
* @param data Data to associate with this view
* @param isSelectable true if the footer view can be selected
*/
public void addFooterView(View v, Object data, boolean isSelectable) {
// NOTE: do not enforce the adapter being null here, since unlike in
// addHeaderView, it was never enforced here, and so existing apps are
// relying on being able to add a footer and then calling setAdapter to
// force creation of the HeaderViewListAdapter wrapper
FixedViewInfo info = new FixedViewInfo();
info.view = v;
info.data = data;
info.isSelectable = isSelectable;
mFooterViewInfos.add(info);
// in the case of re-adding a footer view, or adding one later on,
// we need to notify the observer
if (mAdapter != null && mObserver != null) {
mObserver.onChanged();
}
}
/**
* Add a fixed view to appear at the bottom of the list. If addFooterView is called more
* than once, the views will appear in the order they were added. Views added using
* this call can take focus if they want.
* <p>NOTE: Call this before calling setAdapter. This is so ListView can wrap the supplied
* cursor with one that will also account for header and footer views.
*
* @param v The view to add.
*/
public void addFooterView(View v) {
addFooterView(v, null, true);
}
public int getFooterViewsCount() {
return mFooterViewInfos.size();
}
/**
* Removes a previously-added footer view.
*
* @param v The view to remove
* @return true if the view was removed, false if the view was not a footer view
*/
public boolean removeFooterView(View v) {
if (mFooterViewInfos.size() > 0) {
boolean result = false;
if (mAdapter != null && ((HeaderViewListAdapter) mAdapter).removeFooter(v)) {
if (mObserver != null) {
mObserver.onChanged();
}
result = true;
}
removeFixedViewInfo(v, mFooterViewInfos);
return result;
}
return false;
}
// //////////////////////////////////////////////////////////////////////////////////////////
// Property Overrides
//
@Override
public void setClipToPadding(final boolean clipToPadding) {
super.setClipToPadding(clipToPadding);
mClipToPadding = clipToPadding;
}
// //////////////////////////////////////////////////////////////////////////////////////////
// LAYOUT
//
/**
* {@inheritDoc}
*/
@Override
public void requestLayout() {
if (!mBlockLayoutRequests && !mInLayout) {
super.requestLayout();
}
}
/**
* {@inheritDoc}
*/
@Override
protected void onLayout(final boolean changed, final int l, final int t, final int r, final int b) {
// super.onLayout(changed, l, t, r, b); - skipping base AbsListView implementation on purpose
// haven't set an adapter yet? get to it
if (mAdapter == null) {
return;
}
if (changed) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
getChildAt(i).forceLayout();
}
mRecycleBin.markChildrenDirty();
}
// TODO get the height of the view??
mInLayout = true;
layoutChildren();
mInLayout = false;
}
/**
* {@inheritDoc}
*/
@Override
protected void layoutChildren() {
if (mBlockLayoutRequests) return;
mBlockLayoutRequests = true;
try {
super.layoutChildren();
invalidate();
if (mAdapter == null) {
clearState();
invokeOnItemScrollListener();
return;
}
int childrenTop = getListPaddingTop();
int childCount = getChildCount();
View oldFirst = null;
// our last state so we keep our position
if (mLayoutMode == LAYOUT_NORMAL) {
oldFirst = getChildAt(0);
}
boolean dataChanged = mDataChanged;
if (dataChanged) {
handleDataChanged();
}
// safety check!
// Handle the empty set by removing all views that are visible
// and calling it a day
if (mItemCount == 0) {
clearState();
invokeOnItemScrollListener();
return;
}
else if (mItemCount != mAdapter.getCount()) {
throw new IllegalStateException("The content of the adapter has changed but "
+ "ExtendableListView did not receive a notification. Make sure the content of "
+ "your adapter is not modified from a background thread, but only "
+ "from the UI thread. [in ExtendableListView(" + getId() + ", " + getClass()
+ ") with Adapter(" + mAdapter.getClass() + ")]");
}
// Pull all children into the RecycleBin.
// These views will be reused if possible
final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycleBin;
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition + i);
}
}
else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
// Clear out old views
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();
switch (mLayoutMode) {
case LAYOUT_FORCE_TOP: {
mFirstPosition = 0;
resetToTop();
adjustViewsUpOrDown();
fillFromTop(childrenTop);
adjustViewsUpOrDown();
break;
}
case LAYOUT_SYNC: {
fillSpecific(mSyncPosition, mSpecificTop);
break;
}
case LAYOUT_NORMAL:
default: {
if (childCount == 0) {
fillFromTop(childrenTop);
}
else if (mFirstPosition < mItemCount) {
fillSpecific(mFirstPosition,
oldFirst == null ? childrenTop : oldFirst.getTop());
}
else {
fillSpecific(0, childrenTop);
}
break;
}
}
// Flush any cached views that did not get reused above
recycleBin.scrapActiveViews();
mDataChanged = false;
mNeedSync = false;
mLayoutMode = LAYOUT_NORMAL;
invokeOnItemScrollListener();
} finally {
mBlockLayoutRequests = false;
}
}
@Override
protected void handleDataChanged() {
super.handleDataChanged();
final int count = mItemCount;
if (count > 0 && mNeedSync) {
mNeedSync = false;
mSyncState = null;
mLayoutMode = LAYOUT_SYNC;
mSyncPosition = Math.min(Math.max(0, mSyncPosition), count - 1);
return;
}
mLayoutMode = LAYOUT_FORCE_TOP;
mNeedSync = false;
mSyncState = null;
// TODO : add selection handling here
}
public void resetToTop() {
// TO override
}
// //////////////////////////////////////////////////////////////////////////////////////////
// MEASUREMENT
//
/**
* {@inheritDoc}
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(widthSize, heightSize);
mWidthMeasureSpec = widthMeasureSpec;
}
// //////////////////////////////////////////////////////////////////////////////////////////
// ON TOUCH
//
/**
* {@inheritDoc}
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
// we're not passing this down as
// all the touch handling is right here
// super.onTouchEvent(event);
if (!isEnabled()) {
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return isClickable() || isLongClickable();
}
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(event);
if (!hasChildren()) return false;
boolean handled;
final int action = event.getAction() & MotionEventCompat.ACTION_MASK;
switch (action) {
case MotionEvent.ACTION_DOWN:
handled = onTouchDown(event);
break;
case MotionEvent.ACTION_MOVE:
handled = onTouchMove(event);
break;
case MotionEvent.ACTION_CANCEL:
handled = onTouchCancel(event);
break;
case MotionEvent.ACTION_POINTER_UP:
handled = onTouchPointerUp(event);
break;
case MotionEvent.ACTION_UP:
handled = onTouchUp(event);
break;
default:
handled = false;
break;
}
notifyTouchMode();
return handled;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if (!mIsAttached) {
// Something isn't right.
// Since we rely on being attached to get data set change notifications,
// don't risk doing anything where we might try to resync and find things
// in a bogus state.
return false;
}
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
int touchMode = mTouchMode;
// TODO : overscroll
// if (touchMode == TOUCH_MODE_OVERFLING || touchMode == TOUCH_MODE_OVERSCROLL) {
// mMotionCorrection = 0;
// return true;
// }
final int x = (int) ev.getX();
final int y = (int) ev.getY();
mActivePointerId = ev.getPointerId(0);
int motionPosition = findMotionRow(y);
if (touchMode != TOUCH_MODE_FLINGING && motionPosition >= 0) {
// User clicked on an actual view (and was not stopping a fling).
// Remember where the motion event started
mMotionX = x;
mMotionY = y;
mMotionPosition = motionPosition;
mTouchMode = TOUCH_MODE_DOWN;
}
mLastY = Integer.MIN_VALUE;
initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);
if (touchMode == TOUCH_MODE_FLINGING) {
return true;
}
break;
}
case MotionEvent.ACTION_MOVE: {
switch (mTouchMode) {
case TOUCH_MODE_DOWN:
int pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex == -1) {
pointerIndex = 0;
mActivePointerId = ev.getPointerId(pointerIndex);
}
final int y = (int) ev.getY(pointerIndex);
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
if (startScrollIfNeeded(y)) {
return true;
}
break;
}
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
mTouchMode = TOUCH_MODE_IDLE;
mActivePointerId = INVALID_POINTER;
recycleVelocityTracker();
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
break;
}
case MotionEvent.ACTION_POINTER_UP: {
onSecondaryPointerUp(ev);
break;
}
}
return false;
}
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept) {
recycleVelocityTracker();
}
super.requestDisallowInterceptTouchEvent(disallowIntercept);
}
final class CheckForTap implements Runnable {
public void run() {
if (mTouchMode == TOUCH_MODE_DOWN) {
mTouchMode = TOUCH_MODE_TAP;
final View child = getChildAt(mMotionPosition);
if (child != null && !child.hasFocusable()) {
mLayoutMode = LAYOUT_NORMAL;
if (!mDataChanged) {
layoutChildren();
child.setPressed(true);
setPressed(true);
final int longPressTimeout = ViewConfiguration.getLongPressTimeout();
final boolean longClickable = isLongClickable();
if (longClickable) {
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.rememberWindowAttachCount();
postDelayed(mPendingCheckForLongPress, longPressTimeout);
} else {
mTouchMode = TOUCH_MODE_DONE_WAITING;
}
} else {
mTouchMode = TOUCH_MODE_DONE_WAITING;
}
}
}
}
}
private boolean onTouchDown(final MotionEvent event) {
final int x = (int) event.getX();
final int y = (int) event.getY();
int motionPosition = pointToPosition(x, y);
mVelocityTracker.clear();
mActivePointerId = MotionEventCompat.getPointerId(event, 0);
// TODO : use the motion position for fling support
// TODO : support long press!
// startLongPressCheck();
if ((mTouchMode != TOUCH_MODE_FLINGING) &&
!mDataChanged &&
motionPosition >= 0 &&
getAdapter().isEnabled(motionPosition)) {
// is it a tap or a scroll .. we don't know yet!
mTouchMode = TOUCH_MODE_DOWN;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
if (event.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;
}
}
else if (mTouchMode == TOUCH_MODE_FLINGING) {
mTouchMode = TOUCH_MODE_SCROLLING;
mMotionCorrection = 0;
motionPosition = findMotionRow(y);
}
mMotionX = x;
mMotionY = y;
mMotionPosition = motionPosition;
mLastY = Integer.MIN_VALUE;
return true;
}
private boolean onTouchMove(final MotionEvent event) {
final int index = MotionEventCompat.findPointerIndex(event, mActivePointerId);
if (index < 0) {
Log.e(TAG, "onTouchMove could not find pointer with id " +
mActivePointerId + " - did ExtendableListView receive an inconsistent " +
"event stream?");
return false;
}
final int y = (int) MotionEventCompat.getY(event, index);
// our data's changed so we need to do a layout before moving any further
if (mDataChanged) {
layoutChildren();
}
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(y);
break;
case TOUCH_MODE_SCROLLING:
// case TOUCH_MODE_OVERSCROLL:
scrollIfNeeded(y);
break;
}
return true;
}
private boolean onTouchCancel(final MotionEvent event) {
mTouchMode = TOUCH_MODE_IDLE;
setPressed(false);
invalidate(); // redraw selector
final Handler handler = getHandler();
if (handler != null) {
handler.removeCallbacks(mPendingCheckForLongPress);
}
recycleVelocityTracker();
mActivePointerId = INVALID_POINTER;
return true;
}
private boolean onTouchUp(final MotionEvent event) {
switch (mTouchMode) {
case TOUCH_MODE_DOWN:
case TOUCH_MODE_TAP:
case TOUCH_MODE_DONE_WAITING:
return onTouchUpTap(event);
case TOUCH_MODE_SCROLLING:
return onTouchUpScrolling(event);
}
setPressed(false);
invalidate(); // redraw selector
final Handler handler = getHandler();
if (handler != null) {
handler.removeCallbacks(mPendingCheckForLongPress);
}
recycleVelocityTracker();
mActivePointerId = INVALID_POINTER;
return true;
}
private boolean onTouchUpScrolling(final MotionEvent event) {
if (hasChildren()) {
// 2 - Are we at the top or bottom?
int top = getFirstChildTop();
int bottom = getLastChildBottom();
final boolean atEdge = mFirstPosition == 0 &&
top >= getListPaddingTop() &&
mFirstPosition + getChildCount() < mItemCount &&
bottom <= getHeight() - getListPaddingBottom();
if (!atEdge) {
mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
final float velocity = mVelocityTracker.getYVelocity(mActivePointerId);
if (Math.abs(velocity) > mFlingVelocity) {
startFlingRunnable(velocity);
mTouchMode = TOUCH_MODE_FLINGING;
mMotionY = 0;
invalidate();
return true;
}
}
}
stopFlingRunnable();
recycleVelocityTracker();
mTouchMode = TOUCH_MODE_IDLE;
return true;
}
private boolean onTouchUpTap(final MotionEvent event) {
final int motionPosition = mMotionPosition;
if (motionPosition >= 0) {
final View child = getChildAt(motionPosition);
if (child != null && !child.hasFocusable()) {
if (mTouchMode != TOUCH_MODE_DOWN) {
child.setPressed(false);
}
if (mPerformClick == null) {
invalidate();
mPerformClick = new PerformClick();
}
final PerformClick performClick = mPerformClick;
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 && motionPosition >= 0 && mAdapter.isEnabled(motionPosition)) {
mTouchMode = TOUCH_MODE_TAP;
layoutChildren();
child.setPressed(true);
setPressed(true);
postDelayed(new Runnable() {
public void run() {
child.setPressed(false);
setPressed(false);
if (!mDataChanged) {
post(performClick);
}
mTouchMode = TOUCH_MODE_IDLE;
}
}, ViewConfiguration.getPressedStateDuration());
} else {
mTouchMode = TOUCH_MODE_IDLE;
}
return true;
} else if (!mDataChanged && motionPosition >= 0 && mAdapter.isEnabled(motionPosition)) {
post(performClick);
}
}
}
mTouchMode = TOUCH_MODE_IDLE;
return true;
}
private boolean onTouchPointerUp(final MotionEvent event) {
onSecondaryPointerUp(event);
final int x = mMotionX;
final int y = mMotionY;
final int motionPosition = pointToPosition(x, y);
if (motionPosition >= 0) {
mMotionPosition = motionPosition;
}
mLastY = y;
return true;
}
private void onSecondaryPointerUp(MotionEvent event) {
final int pointerIndex = (event.getAction() &
MotionEventCompat.ACTION_POINTER_INDEX_MASK) >>
MotionEventCompat.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = event.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
// TODO: Make this decision more intelligent.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mMotionX = (int) event.getX(newPointerIndex);
mMotionY = (int) event.getY(newPointerIndex);
mActivePointerId = event.getPointerId(newPointerIndex);
recycleVelocityTracker();
}
}
// //////////////////////////////////////////////////////////////////////////////////////////
// SCROLL HELPERS
//
/**
* Starts a scroll that moves the difference between y and our last motions y
* if it's a movement that represents a big enough scroll.
*/
private boolean startScrollIfNeeded(final int y) {
final int deltaY = y - mMotionY;
final int distance = Math.abs(deltaY);
// TODO : Overscroll?
// final boolean overscroll = mScrollY != 0;
final boolean overscroll = false;
if (overscroll || distance > mTouchSlop) {
if (overscroll) {
mMotionCorrection = 0;
}
else {
mTouchMode = TOUCH_MODE_SCROLLING;
mMotionCorrection = deltaY > 0 ? mTouchSlop : -mTouchSlop;
}
final Handler handler = getHandler();
if (handler != null) {
handler.removeCallbacks(mPendingCheckForLongPress);
}
setPressed(false);
View motionView = getChildAt(mMotionPosition - mFirstPosition);
if (motionView != null) {
motionView.setPressed(false);
}
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
scrollIfNeeded(y);
return true;
}
return false;
}
private void scrollIfNeeded(final int y) {
if (DBG) Log.d(TAG, "scrollIfNeeded y: " + y);
final int rawDeltaY = y - mMotionY;
final int deltaY = rawDeltaY - mMotionCorrection;
int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY;
if (mTouchMode == TOUCH_MODE_SCROLLING) {
if (DBG) Log.d(TAG, "scrollIfNeeded TOUCH_MODE_SCROLLING");
if (y != mLastY) {
// stop our parent
if (Math.abs(rawDeltaY) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
final int motionIndex;
if (mMotionPosition >= 0) {
motionIndex = mMotionPosition - mFirstPosition;
}
else {
// If we don't have a motion position that we can reliably track,
// pick something in the middle to make a best guess at things below.
motionIndex = getChildCount() / 2;
}
// No need to do all this work if we're not going to move anyway
boolean atEdge = false;
if (incrementalDeltaY != 0) {
atEdge = moveTheChildren(deltaY, incrementalDeltaY);
}
// Check to see if we have bumped into the scroll limit
View motionView = this.getChildAt(motionIndex);
if (motionView != null) {
if (atEdge) {
// TODO : edge effect & overscroll
}
mMotionY = y;
}
mLastY = y;
}
}
// TODO : ELSE SUPPORT OVERSCROLL!
}
private int findMotionRow(int y) {
int childCount = getChildCount();
if (childCount > 0) {
// always from the top
for (int i = 0; i < childCount; i++) {
View v = getChildAt(i);
if (y <= v.getBottom()) {
return mFirstPosition + i;
}
}
}
return INVALID_POSITION;
}
// //////////////////////////////////////////////////////////////////////////////////////////
// MOVING STUFF!
//
// It's not scrolling - we're just moving views!
// Move our views and implement view recycling to show new views if necessary
// move our views by deltaY - what's the incrementalDeltaY?
private boolean moveTheChildren(int deltaY, int incrementalDeltaY) {
if (DBG) Log.d(TAG, "moveTheChildren deltaY: " + deltaY + "incrementalDeltaY: " + incrementalDeltaY);
// there's nothing to move!
if (!hasChildren()) return true;
final int firstTop = getHighestChildTop();
final int lastBottom = getLowestChildBottom();
// "effective padding" In this case is the amount of padding that affects
// how much space should not be filled by items. If we don't clip to padding
// there is no effective padding.
int effectivePaddingTop = 0;
int effectivePaddingBottom = 0;
if (mClipToPadding) {
effectivePaddingTop = getListPaddingTop();
effectivePaddingBottom = getListPaddingBottom();
}
final int gridHeight = getHeight();
final int spaceAbove = effectivePaddingTop - getFirstChildTop();
final int end = gridHeight - effectivePaddingBottom;
final int spaceBelow = getLastChildBottom() - end;
final int height = gridHeight - getListPaddingBottom() - getListPaddingTop();
if (incrementalDeltaY < 0) {
incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
}
else {
incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
}
final int firstPosition = mFirstPosition;
int maxTop = getListPaddingTop();
int maxBottom = gridHeight - getListPaddingBottom();
int childCount = getChildCount();
final boolean cannotScrollDown = (firstPosition == 0 &&
firstTop >= maxTop && incrementalDeltaY >= 0);
final boolean cannotScrollUp = (firstPosition + childCount == mItemCount &&
lastBottom <= maxBottom && incrementalDeltaY <= 0);
if (DBG) {
Log.d(TAG, "moveTheChildren " +
" firstTop " + firstTop +
" maxTop " + maxTop +
" incrementalDeltaY " + incrementalDeltaY);
Log.d(TAG, "moveTheChildren " +
" lastBottom " + lastBottom +
" maxBottom " + maxBottom +
" incrementalDeltaY " + incrementalDeltaY);
}
if (cannotScrollDown) {
if (DBG) Log.d(TAG, "moveTheChildren cannotScrollDown " + cannotScrollDown);
return incrementalDeltaY != 0;
}
if (cannotScrollUp) {
if (DBG) Log.d(TAG, "moveTheChildren cannotScrollUp " + cannotScrollUp);
return incrementalDeltaY != 0;
}
final boolean isDown = incrementalDeltaY < 0;
final int headerViewsCount = getHeaderViewsCount();
final int footerViewsStart = mItemCount - getFooterViewsCount();
int start = 0;
int count = 0;
if (isDown) {
int top = -incrementalDeltaY;
if (mClipToPadding) {
top += getListPaddingTop();
}
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) {
mRecycleBin.addScrapView(child, position);
}
}
}
}
else {
int bottom = gridHeight - incrementalDeltaY;
if (mClipToPadding) {
bottom -= getListPaddingBottom();
}
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) {
mRecycleBin.addScrapView(child, position);
}
}
}
}
mBlockLayoutRequests = true;
if (count > 0) {
if (DBG) Log.d(TAG, "scrap - detachViewsFromParent start:" + start + " count:" + count);
detachViewsFromParent(start, count);
mRecycleBin.removeSkippedScrap();
onChildrenDetached(start, count);
}
// invalidate before moving the children to avoid unnecessary invalidate
// calls to bubble up from the children all the way to the top
if (!awakenScrollBars()) {
invalidate();
}
offsetChildrenTopAndBottom(incrementalDeltaY);
if (isDown) {
mFirstPosition += count;
}
final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
fillGap(isDown);
}
// TODO : touch mode selector handling
mBlockLayoutRequests = false;
invokeOnItemScrollListener();
return false;
}
protected void onChildrenDetached(final int start, final int count) {
}
// //////////////////////////////////////////////////////////////////////////////////////////
// FILLING THE GRID!
//
/**
* As we move and scroll and recycle views we want to fill the gap created with new views
*/
protected void fillGap(boolean down) {
final int count = getChildCount();
if (down) {
// fill down from the top of the position below our last
int position = mFirstPosition + count;
final int startOffset = getChildTop(position);
fillDown(position, startOffset);
}
else {
// fill up from the bottom of the position above our first.
int position = mFirstPosition - 1;
final int startOffset = getChildBottom(position);
fillUp(position, startOffset);
}
adjustViewsAfterFillGap(down);
}
protected void adjustViewsAfterFillGap(boolean down) {
if (down) {
correctTooHigh(getChildCount());
}
else {
correctTooLow(getChildCount());
}
}
private View fillDown(int pos, int nextTop) {
if (DBG) Log.d(TAG, "fillDown - pos:" + pos + " nextTop:" + nextTop);
View selectedView = null;
int end = getHeight();
if (mClipToPadding) {
end -= getListPaddingBottom();
}
while ((nextTop < end || hasSpaceDown()) && pos < mItemCount) {
// TODO : add selection support
makeAndAddView(pos, nextTop, true, false);
pos++;
nextTop = getNextChildDownsTop(pos); // = child.getBottom();
}
return selectedView;
}
/***
* Override to tell filling flow to continue to fill up as we have space.
*/
protected boolean hasSpaceDown() {
return false;
}
private View fillUp(int pos, int nextBottom) {
if (DBG) Log.d(TAG, "fillUp - position:" + pos + " nextBottom:" + nextBottom);
View selectedView = null;
int end = mClipToPadding ? getListPaddingTop() : 0;
while ((nextBottom > end || hasSpaceUp()) && pos >= 0) {
// TODO : add selection support
makeAndAddView(pos, nextBottom, false, false);
pos--;
nextBottom = getNextChildUpsBottom(pos);
if (DBG) Log.d(TAG, "fillUp next - position:" + pos + " nextBottom:" + nextBottom);
}
mFirstPosition = pos + 1;
return selectedView;
}
/***
* Override to tell filling flow to continue to fill up as we have space.
*/
protected boolean hasSpaceUp() {
return false;
}
/**
* Fills the list from top to bottom, starting with mFirstPosition
*/
private View fillFromTop(int nextTop) {
mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
if (mFirstPosition < 0) {
mFirstPosition = 0;
}
return fillDown(mFirstPosition, nextTop);
}
/**
* Put a specific item at a specific location on the screen and then build
* up and down from there.
*
* @param position The reference view to use as the starting point
* @param top Pixel offset from the top of this view to the top of the
* reference view.
* @return The selected view, or null if the selected view is outside the
* visible area.
*/
private View fillSpecific(int position, int top) {
boolean tempIsSelected = false; // ain't no body got time for that @ Etsy
View temp = makeAndAddView(position, top, true, tempIsSelected);
// Possibly changed again in fillUp if we add rows above this one.
mFirstPosition = position;
View above;
View below;
int nextBottom = getNextChildUpsBottom(position - 1);
int nextTop = getNextChildDownsTop(position + 1);
above = fillUp(position - 1, nextBottom);
// This will correct for the top of the first view not touching the top of the list
adjustViewsUpOrDown();
below = fillDown(position + 1, nextTop);
int childCount = getChildCount();
if (childCount > 0) {
correctTooHigh(childCount);
}
if (tempIsSelected) {
return temp;
}
else if (above != null) {
return above;
}
else {
return below;
}
}
/**
* Gets a view either a new view an unused view?? or a recycled view and adds it to our children
*/
private View makeAndAddView(int position, int y, boolean flowDown, boolean selected) {
View child;
onChildCreated(position, flowDown);
if (!mDataChanged) {
// Try to use an existing view for this position
child = mRecycleBin.getActiveView(position);
if (child != null) {
// Found it -- we're using an existing child
// This just needs to be positioned
setupChild(child, position, y, flowDown, selected, true);
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, flowDown, selected, mIsScrap[0]);
return child;
}
/**
* Add a view as a child and make sure it is measured (if necessary) and
* positioned properly.
*
* @param child The view to add
* @param position The position of this child
* @param y The y position relative to which this view will be positioned
* @param flowDown If true, align top edge to y. If false, align bottom
* edge to y.
* @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.
*/
private void setupChild(View child, int position, int y, boolean flowDown,
boolean selected, boolean recycled) {
final boolean isSelected = false; // TODO : selected && shouldShowSelector();
final boolean updateChildSelected = isSelected != child.isSelected();
final int mode = mTouchMode;
final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLLING &&
mMotionPosition == position;
final boolean updateChildPressed = isPressed != child.isPressed();
final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
int itemViewType = mAdapter.getItemViewType(position);
LayoutParams layoutParams;
if (itemViewType == ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
layoutParams = generateWrapperLayoutParams(child);
}
else {
layoutParams = generateChildLayoutParams(child);
}
layoutParams.viewType = itemViewType;
layoutParams.position = position;
if (recycled || (layoutParams.recycledHeaderFooter &&
layoutParams.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
if (DBG) Log.d(TAG, "setupChild attachViewToParent position:" + position);
attachViewToParent(child, flowDown ? -1 : 0, layoutParams);
}
else {
if (DBG) Log.d(TAG, "setupChild addViewInLayout position:" + position);
if (layoutParams.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
layoutParams.recycledHeaderFooter = true;
}
addViewInLayout(child, flowDown ? -1 : 0, layoutParams, true);
}
if (updateChildSelected) {
child.setSelected(isSelected);
}
if (updateChildPressed) {
child.setPressed(isPressed);
}
if (needToMeasure) {
if (DBG) Log.d(TAG, "setupChild onMeasureChild position:" + position);
onMeasureChild(child, layoutParams);
}
else {
if (DBG) Log.d(TAG, "setupChild cleanupLayoutState position:" + position);
cleanupLayoutState(child);
}
final int w = child.getMeasuredWidth();
final int h = child.getMeasuredHeight();
final int childTop = flowDown ? y : y - h;
if (DBG) {
Log.d(TAG, "setupChild position:" + position + " h:" + h + " w:" + w);
}
final int childrenLeft = getChildLeft(position);
if (needToMeasure) {
final int childRight = childrenLeft + w;
final int childBottom = childTop + h;
onLayoutChild(child, position, flowDown, childrenLeft, childTop, childRight, childBottom);
}
else {
onOffsetChild(child, position, flowDown, childrenLeft, childTop);
}
}
protected LayoutParams generateChildLayoutParams(final View child) {
return generateWrapperLayoutParams(child);
}
protected LayoutParams generateWrapperLayoutParams(final View child) {
LayoutParams layoutParams = null;
final ViewGroup.LayoutParams childParams = child.getLayoutParams();
if (childParams != null) {
if (childParams instanceof LayoutParams) {
layoutParams = (LayoutParams) childParams;
}
else {
layoutParams = new LayoutParams(childParams);
}
}
if (layoutParams == null) {
layoutParams = generateDefaultLayoutParams();
}
return layoutParams;
}
/**
* Measures a child view in the list. Should call
*/
protected void onMeasureChild(final View child, final LayoutParams layoutParams) {
int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
getListPaddingLeft() + getListPaddingRight(), layoutParams.width);
int lpHeight = layoutParams.height;
int childHeightSpec;
if (lpHeight > 0) {
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
}
else {
childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
}
child.measure(childWidthSpec, childHeightSpec);
}
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT, 0);
}
protected LayoutParams generateHeaderFooterLayoutParams(final View child) {
return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT, 0);
}
/**
* Get a view and have it show the data associated with the specified
* position. This is called when we have already discovered that the view is
* not available for reuse in the recycle bin. The only choices left are
* converting an old view or making a new one.
*
* @param position The position to display
* @param isScrap Array of at least 1 boolean, the first entry will become true if
* the returned view was taken from the scrap heap, false if otherwise.
* @return A view displaying the data associated with the specified position
*/
private View obtainView(int position, boolean[] isScrap) {
isScrap[0] = false;
View scrapView;
scrapView = mRecycleBin.getScrapView(position);
View child;
if (scrapView != null) {
if (DBG) Log.d(TAG, "getView from scrap position:" + position);
child = mAdapter.getView(position, scrapView, this);
if (child != scrapView) {
mRecycleBin.addScrapView(scrapView, position);
}
else {
isScrap[0] = true;
}
}
else {
if (DBG) Log.d(TAG, "getView position:" + position);
child = mAdapter.getView(position, null, this);
}
return child;
}
/**
* Check if we have dragged the bottom of the list too high (we have pushed the
* top element off the top of the screen when we did not need to). Correct by sliding
* everything back down.
*
* @param childCount Number of children
*/
private void correctTooHigh(int childCount) {
// First see if the last item is visible. If it is not, it is OK for the
// top of the list to be pushed up.
int lastPosition = mFirstPosition + childCount - 1;
if (lastPosition == mItemCount - 1 && childCount > 0) {
// ... and its bottom edge
final int lastBottom = getLowestChildBottom();
// This is bottom of our drawable area
final int end = (getBottom() - getTop()) - getListPaddingBottom();
// This is how far the bottom edge of the last view is from the bottom of the
// drawable area
int bottomOffset = end - lastBottom;
final int firstTop = getHighestChildTop();
// 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 < getListPaddingTop())) {
if (mFirstPosition == 0) {
// Don't pull the top too far down
bottomOffset = Math.min(bottomOffset, getListPaddingTop() - firstTop);
}
// Move everything down
offsetChildrenTopAndBottom(bottomOffset);
if (mFirstPosition > 0) {
// Fill the gap that was opened above mFirstPosition with more rows, if
// possible
int previousPosition = mFirstPosition - 1;
fillUp(previousPosition, getNextChildUpsBottom(previousPosition));
// Close up the remaining gap
adjustViewsUpOrDown();
}
}
}
}
/**
* Check if we have dragged the bottom of the list too low (we have pushed the
* bottom element off the bottom of the screen when we did not need to). Correct by sliding
* everything back up.
*
* @param childCount Number of children
*/
private void correctTooLow(int childCount) {
// First see if the first item is visible. If it is not, it is OK for the
// bottom of the list to be pushed down.
if (mFirstPosition == 0 && childCount > 0) {
// ... and its top edge
final int firstTop = getHighestChildTop();
// This is top of our drawable area
final int start = getListPaddingTop();
// This is bottom of our drawable area
final int end = (getTop() - getBottom()) - getListPaddingBottom();
// This is how far the top edge of the first view is from the top of the
// drawable area
int topOffset = firstTop - start;
final int lastBottom = getLowestChildBottom();
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) {
if (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
int nextPosition = lastPosition + 1;
fillDown(nextPosition, getNextChildDownsTop(nextPosition));
// Close up the remaining gap
adjustViewsUpOrDown();
}
}
else if (lastPosition == mItemCount - 1) {
adjustViewsUpOrDown();
}
}
}
}
/**
* Make sure views are touching the top or bottom edge, as appropriate for
* our gravity
*/
private void adjustViewsUpOrDown() {
final int childCount = getChildCount();
int delta;
if (childCount > 0) {
// Uh-oh -- we came up short. Slide all views up to make them
// align with the top
delta = getHighestChildTop() - getListPaddingTop();
if (delta < 0) {
// We only are looking to see if we are too low, not too high
delta = 0;
}
if (delta != 0) {
offsetChildrenTopAndBottom(-delta);
}
}
}
// //////////////////////////////////////////////////////////////////////////////////////////
// PROTECTED POSITIONING EXTENSABLES
//
/**
* Override
*/
protected void onChildCreated(final int position, final boolean flowDown) {
}
/**
* Override to position the child as you so wish
*/
protected void onLayoutChild(final View child, final int position,
final boolean flowDown, final int childrenLeft, final int childTop,
final int childRight, final int childBottom) {
child.layout(childrenLeft, childTop, childRight, childBottom);
}
/**
* Override to offset the child as you so wish
*/
protected void onOffsetChild(final View child, final int position,
final boolean flowDown, final int childrenLeft, final int childTop) {
child.offsetLeftAndRight(childrenLeft - child.getLeft());
child.offsetTopAndBottom(childTop - child.getTop());
}
/**
* Override to set you custom listviews child to a specific left location
*
* @return the left location to layout the child for the given position
*/
protected int getChildLeft(final int position) {
return getListPaddingLeft();
}
/**
* Override to set you custom listviews child to a specific top location
*
* @return the top location to layout the child for the given position
*/
protected int getChildTop(final int position) {
int count = getChildCount();
int paddingTop = 0;
if (mClipToPadding) {
paddingTop = getListPaddingTop();
}
return count > 0 ? getChildAt(count - 1).getBottom() : paddingTop;
}
/**
* Override to set you custom listviews child to a bottom top location
*
* @return the bottom location to layout the child for the given position
*/
protected int getChildBottom(final int position) {
int count = getChildCount();
int paddingBottom = 0;
if (mClipToPadding) {
paddingBottom = getListPaddingBottom();
}
return count > 0 ? getChildAt(0).getTop() : getHeight() - paddingBottom;
}
protected int getNextChildDownsTop(final int position) {
final int count = getChildCount();
return count > 0 ? getChildAt(count - 1).getBottom() : 0;
}
protected int getNextChildUpsBottom(final int position) {
final int count = getChildCount();
if (count == 0) {
return 0;
}
return count > 0 ? getChildAt(0).getTop() : 0;
}
protected int getFirstChildTop() {
return hasChildren() ? getChildAt(0).getTop() : 0;
}
protected int getHighestChildTop() {
return hasChildren() ? getChildAt(0).getTop() : 0;
}
protected int getLastChildBottom() {
return hasChildren() ? getChildAt(getChildCount() - 1).getBottom() : 0;
}
protected int getLowestChildBottom() {
return hasChildren() ? getChildAt(getChildCount() - 1).getBottom() : 0;
}
protected boolean hasChildren() {
return getChildCount() > 0;
}
protected void offsetChildrenTopAndBottom(int offset) {
if (DBG) Log.d(TAG, "offsetChildrenTopAndBottom: " + offset);
final int count = getChildCount();
for (int i = 0; i < count; i++) {
final View v = getChildAt(i);
v.offsetTopAndBottom(offset);
}
}
@Override
public int getFirstVisiblePosition() {
return Math.max(0, mFirstPosition - getHeaderViewsCount());
}
@Override
public int getLastVisiblePosition() {
return Math.min(mFirstPosition + getChildCount() - 1, mAdapter != null ? mAdapter.getCount() - 1 : 0);
}
// //////////////////////////////////////////////////////////////////////////////////////////
// FLING
//
private void initOrResetVelocityTracker() {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
else {
mVelocityTracker.clear();
}
}
private void initVelocityTrackerIfNotExists() {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
}
private void recycleVelocityTracker() {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
private void startFlingRunnable(final float velocity) {
if (mFlingRunnable == null) {
mFlingRunnable = new FlingRunnable();
}
mFlingRunnable.start((int) -velocity);
}
private void stopFlingRunnable() {
if (mFlingRunnable != null) {
mFlingRunnable.endFling();
}
}
// //////////////////////////////////////////////////////////////////////////////////////////
// FLING RUNNABLE
//
/**
* 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 FlingRunnable implements Runnable {
/**
* Tracks the decay of a fling scroll
*/
private final Scroller mScroller;
/**
* Y value reported by mScroller on the previous fling
*/
private int mLastFlingY;
FlingRunnable() {
mScroller = new Scroller(getContext());
}
void start(int initialVelocity) {
int initialY = initialVelocity < 0 ? Integer.MAX_VALUE : 0;
mLastFlingY = initialY;
mScroller.forceFinished(true);
mScroller.fling(0, initialY, 0, initialVelocity, 0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE);
mTouchMode = TOUCH_MODE_FLINGING;
postOnAnimate(this);
}
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_FLINGING;
postOnAnimate(this);
}
private void endFling() {
mLastFlingY = 0;
mTouchMode = TOUCH_MODE_IDLE;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
removeCallbacks(this);
mScroller.forceFinished(true);
}
public void run() {
switch (mTouchMode) {
default:
return;
case TOUCH_MODE_FLINGING: {
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;
// 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;
// Don't fling more than 1 screen
delta = Math.max(-(getHeight() - getPaddingBottom() - getPaddingTop() - 1), delta);
}
final boolean atEnd = moveTheChildren(delta, delta);
if (more && !atEnd) {
invalidate();
mLastFlingY = y;
postOnAnimate(this);
}
else {
endFling();
}
break;
}
}
}
}
private void postOnAnimate(Runnable runnable) {
ViewCompat.postOnAnimation(this, runnable);
}
// //////////////////////////////////////////////////////////////////////////////////////////
// SCROLL LISTENER
//
/**
* Notify any scroll listeners of our current touch mode
*/
public void notifyTouchMode() {
// only tell the scroll listener about some things we want it to know
switch (mTouchMode) {
case TOUCH_MODE_SCROLLING:
reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
break;
case TOUCH_MODE_FLINGING:
reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
break;
case TOUCH_MODE_IDLE:
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
break;
}
}
private OnScrollListener mOnScrollListener;
public void setOnScrollListener(OnScrollListener scrollListener) {
super.setOnScrollListener(scrollListener);
mOnScrollListener = scrollListener;
}
void reportScrollStateChange(int newState) {
if (newState != mScrollState) {
mScrollState = newState;
if (mOnScrollListener != null) {
mOnScrollListener.onScrollStateChanged(this, newState);
}
}
}
void invokeOnItemScrollListener() {
if (mOnScrollListener != null) {
mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount);
}
}
/**
* 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 = getAdapter() == null || getAdapter().isEmpty();
if (isInFilterMode()) {
empty = false;
}
View emptyView = getEmptyView();
if (empty) {
if (emptyView != null) {
emptyView.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 (emptyView != null) {
emptyView.setVisibility(View.GONE);
}
setVisibility(View.VISIBLE);
}
}
// //////////////////////////////////////////////////////////////////////////////////////////
// ADAPTER OBSERVER
//
class AdapterDataSetObserver extends DataSetObserver {
private Parcelable mInstanceState = null;
@Override
public void onChanged() {
mDataChanged = true;
mOldItemCount = mItemCount;
mItemCount = getAdapter().getCount();
mRecycleBin.clearTransientStateViews();
// Detect the case where a cursor that was previously invalidated has
// been repopulated with new data.
if (ExtendableListView.this.getAdapter().hasStableIds() && mInstanceState != null
&& mOldItemCount == 0 && mItemCount > 0) {
ExtendableListView.this.onRestoreInstanceState(mInstanceState);
mInstanceState = null;
}
else {
rememberSyncState();
}
updateEmptyStatus();
requestLayout();
}
@Override
public void onInvalidated() {
mDataChanged = true;
if (ExtendableListView.this.getAdapter().hasStableIds()) {
// Remember the current state for the case where our hosting activity is being
// stopped and later restarted
mInstanceState = ExtendableListView.this.onSaveInstanceState();
}
// Data is invalid so we should reset our state
mOldItemCount = mItemCount;
mItemCount = 0;
mNeedSync = false;
updateEmptyStatus();
requestLayout();
}
public void clearSavedState() {
mInstanceState = null;
}
}
// //////////////////////////////////////////////////////////////////////////////////////////
// LAYOUT PARAMS
//
/**
* Re-implementing some properties in {@link android.view.ViewGroup.LayoutParams} since they're package
* private but we want to appear to be an extension of the existing class.
*/
public static class LayoutParams extends AbsListView.LayoutParams {
boolean recycledHeaderFooter;
// Position of the view in the data
int position;
// adapter ID the view represents fetched from the adapter if it's stable
long itemId = -1;
// adapter view type
int viewType;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int w, int h) {
super(w, h);
}
public LayoutParams(int w, int h, int viewType) {
super(w, h);
this.viewType = viewType;
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
// //////////////////////////////////////////////////////////////////////////////////////////
// RecycleBin
//
/**
* Note there's no RecyclerListener. The caller shouldn't have a need and we can add it later.
*/
class RecycleBin {
/**
* The position of the first view stored in mActiveViews.
*/
private int mFirstActivePosition;
/**
* Views that were on screen at the start of layout. This array is populated at the start of
* layout, and at the end of layout all view in mActiveViews are moved to mScrapViews.
* Views in mActiveViews represent a contiguous range of Views, with position of the first
* view store in mFirstActivePosition.
*/
private View[] mActiveViews = new View[0];
/**
* Unsorted views that can be used by the adapter as a convert view.
*/
private ArrayList<View>[] mScrapViews;
private int mViewTypeCount;
private ArrayList<View> mCurrentScrap;
private ArrayList<View> mSkippedScrap;
private SparseArrayCompat<View> mTransientStateViews;
public void setViewTypeCount(int viewTypeCount) {
if (viewTypeCount < 1) {
throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
}
//noinspection unchecked
ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
for (int i = 0; i < viewTypeCount; i++) {
scrapViews[i] = new ArrayList<View>();
}
mViewTypeCount = viewTypeCount;
mCurrentScrap = scrapViews[0];
mScrapViews = scrapViews;
}
public void markChildrenDirty() {
if (mViewTypeCount == 1) {
final ArrayList<View> scrap = mCurrentScrap;
final int scrapCount = scrap.size();
for (int i = 0; i < scrapCount; i++) {
scrap.get(i).forceLayout();
}
}
else {
final int typeCount = mViewTypeCount;
for (int i = 0; i < typeCount; i++) {
final ArrayList<View> scrap = mScrapViews[i];
final int scrapCount = scrap.size();
for (int j = 0; j < scrapCount; j++) {
scrap.get(j).forceLayout();
}
}
}
if (mTransientStateViews != null) {
final int count = mTransientStateViews.size();
for (int i = 0; i < count; i++) {
mTransientStateViews.valueAt(i).forceLayout();
}
}
}
public boolean shouldRecycleViewType(int viewType) {
return viewType >= 0;
}
/**
* Clears the scrap heap.
*/
void clear() {
if (mViewTypeCount == 1) {
final ArrayList<View> scrap = mCurrentScrap;
final int scrapCount = scrap.size();
for (int i = 0; i < scrapCount; i++) {
removeDetachedView(scrap.remove(scrapCount - 1 - i), false);
}
}
else {
final int typeCount = mViewTypeCount;
for (int i = 0; i < typeCount; i++) {
final ArrayList<View> scrap = mScrapViews[i];
final int scrapCount = scrap.size();
for (int j = 0; j < scrapCount; j++) {
removeDetachedView(scrap.remove(scrapCount - 1 - j), false);
}
}
}
if (mTransientStateViews != null) {
mTransientStateViews.clear();
}
}
/**
* Fill ActiveViews with all of the children of the AbsListView.
*
* @param childCount The minimum number of views mActiveViews should hold
* @param firstActivePosition The position of the first view that will be stored in
* mActiveViews
*/
void fillActiveViews(int childCount, int firstActivePosition) {
if (mActiveViews.length < childCount) {
mActiveViews = new View[childCount];
}
mFirstActivePosition = firstActivePosition;
final View[] activeViews = mActiveViews;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
// Don't put header or footer views into the scrap heap
if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
// Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views.
// However, we will NOT place them into scrap views.
activeViews[i] = child;
}
}
}
/**
* Get the view corresponding to the specified position. The view will be removed from
* mActiveViews if it is found.
*
* @param position The position to look up in mActiveViews
* @return The view if it is found, null otherwise
*/
View getActiveView(int position) {
int index = position - mFirstActivePosition;
final View[] activeViews = mActiveViews;
if (index >= 0 && index < activeViews.length) {
final View match = activeViews[index];
activeViews[index] = null;
return match;
}
return null;
}
View getTransientStateView(int position) {
if (mTransientStateViews == null) {
return null;
}
final int index = mTransientStateViews.indexOfKey(position);
if (index < 0) {
return null;
}
final View result = mTransientStateViews.valueAt(index);
mTransientStateViews.removeAt(index);
return result;
}
/**
* Dump any currently saved views with transient state.
*/
void clearTransientStateViews() {
if (mTransientStateViews != null) {
mTransientStateViews.clear();
}
}
/**
* @return A view from the ScrapViews collection. These are unordered.
*/
View getScrapView(int position) {
if (mViewTypeCount == 1) {
return retrieveFromScrap(mCurrentScrap, position);
}
else {
int whichScrap = mAdapter.getItemViewType(position);
if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
return retrieveFromScrap(mScrapViews[whichScrap], position);
}
}
return null;
}
/**
* Put a view into the ScrapViews list. These views are unordered.
*
* @param scrap The view to add
*/
void addScrapView(View scrap, int position) {
if (DBG) Log.d(TAG, "addScrapView position = " + position);
LayoutParams lp = (LayoutParams) scrap.getLayoutParams();
if (lp == null) {
return;
}
lp.position = position;
// Don't put header or footer views or views that should be ignored
// into the scrap heap
int viewType = lp.viewType;
final boolean scrapHasTransientState = ViewCompat.hasTransientState(scrap);
if (!shouldRecycleViewType(viewType) || scrapHasTransientState) {
if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER || scrapHasTransientState) {
if (mSkippedScrap == null) {
mSkippedScrap = new ArrayList<View>();
}
mSkippedScrap.add(scrap);
}
if (scrapHasTransientState) {
if (mTransientStateViews == null) {
mTransientStateViews = new SparseArrayCompat<View>();
}
mTransientStateViews.put(position, scrap);
}
return;
}
if (mViewTypeCount == 1) {
mCurrentScrap.add(scrap);
}
else {
mScrapViews[viewType].add(scrap);
}
}
/**
* Finish the removal of any views that skipped the scrap heap.
*/
void removeSkippedScrap() {
if (mSkippedScrap == null) {
return;
}
final int count = mSkippedScrap.size();
for (int i = 0; i < count; i++) {
removeDetachedView(mSkippedScrap.get(i), false);
}
mSkippedScrap.clear();
}
/**
* Move all views remaining in mActiveViews to mScrapViews.
*/
void scrapActiveViews() {
final View[] activeViews = mActiveViews;
final boolean multipleScraps = mViewTypeCount > 1;
ArrayList<View> scrapViews = mCurrentScrap;
final int count = activeViews.length;
for (int i = count - 1; i >= 0; i--) {
final View victim = activeViews[i];
if (victim != null) {
final LayoutParams lp = (LayoutParams) victim.getLayoutParams();
activeViews[i] = null;
final boolean scrapHasTransientState = ViewCompat.hasTransientState(victim);
int viewType = lp.viewType;
if (!shouldRecycleViewType(viewType) || scrapHasTransientState) {
// Do not move views that should be ignored
if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER || scrapHasTransientState) {
removeDetachedView(victim, false);
}
if (scrapHasTransientState) {
if (mTransientStateViews == null) {
mTransientStateViews = new SparseArrayCompat<View>();
}
mTransientStateViews.put(mFirstActivePosition + i, victim);
}
continue;
}
if (multipleScraps) {
scrapViews = mScrapViews[viewType];
}
lp.position = mFirstActivePosition + i;
scrapViews.add(victim);
}
}
pruneScrapViews();
}
/**
* Makes sure that the size of mScrapViews does not exceed the size of mActiveViews.
* (This can happen if an adapter does not recycle its views).
*/
private void pruneScrapViews() {
final int maxViews = mActiveViews.length;
final int viewTypeCount = mViewTypeCount;
final ArrayList<View>[] scrapViews = mScrapViews;
for (int i = 0; i < viewTypeCount; ++i) {
final ArrayList<View> scrapPile = scrapViews[i];
int size = scrapPile.size();
final int extras = size - maxViews;
size--;
for (int j = 0; j < extras; j++) {
removeDetachedView(scrapPile.remove(size--), false);
}
}
if (mTransientStateViews != null) {
for (int i = 0; i < mTransientStateViews.size(); i++) {
final View v = mTransientStateViews.valueAt(i);
if (!ViewCompat.hasTransientState(v)) {
mTransientStateViews.removeAt(i);
i--;
}
}
}
}
/**
* Updates the cache color hint of all known views.
*
* @param color The new cache color hint.
*/
void setCacheColorHint(int color) {
if (mViewTypeCount == 1) {
final ArrayList<View> scrap = mCurrentScrap;
final int scrapCount = scrap.size();
for (int i = 0; i < scrapCount; i++) {
scrap.get(i).setDrawingCacheBackgroundColor(color);
}
}
else {
final int typeCount = mViewTypeCount;
for (int i = 0; i < typeCount; i++) {
final ArrayList<View> scrap = mScrapViews[i];
final int scrapCount = scrap.size();
for (int j = 0; j < scrapCount; j++) {
scrap.get(j).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);
}
}
}
}
static View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
int size = scrapViews.size();
if (size > 0) {
// See if we still have a view for this position.
for (int i = 0; i < size; i++) {
View view = scrapViews.get(i);
if (((LayoutParams) view.getLayoutParams()).position == position) {
scrapViews.remove(i);
return view;
}
}
return scrapViews.remove(size - 1);
}
else {
return null;
}
}
// //////////////////////////////////////////////////////////////////////////////////////////
// OUR STATE
//
/**
* Position from which to start looking for mSyncRowId
*/
protected int mSyncPosition;
/**
* The offset in pixels from the top of the AdapterView to the top
* of the view to select during the next layout.
*/
protected int mSpecificTop;
/**
* Row id to look for when data has changed
*/
long mSyncRowId = INVALID_ROW_ID;
/**
* Height of the view when mSyncPosition and mSyncRowId where set
*/
long mSyncHeight;
/**
* True if we need to sync to mSyncRowId
*/
boolean mNeedSync = false;
private ListSavedState mSyncState;
/**
* Remember enough information to restore the screen state when the data has
* changed.
*/
void rememberSyncState() {
if (getChildCount() > 0) {
mNeedSync = true;
mSyncHeight = getHeight();
// Sync the based on the offset of the first view
View v = getChildAt(0);
ListAdapter adapter = getAdapter();
if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) {
mSyncRowId = adapter.getItemId(mFirstPosition);
}
else {
mSyncRowId = NO_ID;
}
if (v != null) {
mSpecificTop = v.getTop();
}
mSyncPosition = mFirstPosition;
}
}
private void clearState() {
// cleanup headers and footers before removing the views
clearRecycledState(mHeaderViewInfos);
clearRecycledState(mFooterViewInfos);
removeAllViewsInLayout();
mFirstPosition = 0;
mDataChanged = false;
mRecycleBin.clear();
mNeedSync = false;
mSyncState = null;
mLayoutMode = LAYOUT_NORMAL;
invalidate();
}
private void clearRecycledState(ArrayList<FixedViewInfo> infos) {
if (infos == null) return;
for (FixedViewInfo info : infos) {
final View child = info.view;
final ViewGroup.LayoutParams p = child.getLayoutParams();
if (p instanceof LayoutParams) {
((LayoutParams) p).recycledHeaderFooter = false;
}
}
}
public static class ListSavedState extends ClassLoaderSavedState {
protected long selectedId;
protected long firstId;
protected int viewTop;
protected int position;
protected int height;
/**
* Constructor called from {@link android.widget.AbsListView#onSaveInstanceState()}
*/
public ListSavedState(Parcelable superState) {
super(superState, AbsListView.class.getClassLoader());
}
/**
* Constructor called from {@link #CREATOR}
*/
public ListSavedState(Parcel in) {
super(in);
selectedId = in.readLong();
firstId = in.readLong();
viewTop = in.readInt();
position = in.readInt();
height = in.readInt();
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeLong(selectedId);
out.writeLong(firstId);
out.writeInt(viewTop);
out.writeInt(position);
out.writeInt(height);
}
@Override
public String toString() {
return "ExtendableListView.ListSavedState{"
+ Integer.toHexString(System.identityHashCode(this))
+ " selectedId=" + selectedId
+ " firstId=" + firstId
+ " viewTop=" + viewTop
+ " position=" + position
+ " height=" + height + "}";
}
public static final Creator<ListSavedState> CREATOR
= new Creator<ListSavedState>() {
public ListSavedState createFromParcel(Parcel in) {
return new ListSavedState(in);
}
public ListSavedState[] newArray(int size) {
return new ListSavedState[size];
}
};
}
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
ListSavedState ss = new ListSavedState(superState);
if (mSyncState != null) {
// Just keep what we last restored.
ss.selectedId = mSyncState.selectedId;
ss.firstId = mSyncState.firstId;
ss.viewTop = mSyncState.viewTop;
ss.position = mSyncState.position;
ss.height = mSyncState.height;
return ss;
}
boolean haveChildren = getChildCount() > 0 && mItemCount > 0;
ss.selectedId = getSelectedItemId();
ss.height = getHeight();
// TODO : sync selection when we handle it
if (haveChildren && mFirstPosition > 0) {
// Remember the position of the first child.
// We only do this if we are not currently at the top of
// the list, for two reasons:
// (1) The list may be in the process of becoming empty, in
// which case mItemCount may not be 0, but if we try to
// ask for any information about position 0 we will crash.
// (2) Being "at the top" seems like a special case, anyway,
// and the user wouldn't expect to end up somewhere else when
// they revisit the list even if its content has changed.
View v = getChildAt(0);
ss.viewTop = v.getTop();
int firstPos = mFirstPosition;
if (firstPos >= mItemCount) {
firstPos = mItemCount - 1;
}
ss.position = firstPos;
ss.firstId = mAdapter.getItemId(firstPos);
}
else {
ss.viewTop = 0;
ss.firstId = INVALID_POSITION;
ss.position = 0;
}
return ss;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
ListSavedState ss = (ListSavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
mDataChanged = true;
mSyncHeight = ss.height;
if (ss.firstId >= 0) {
mNeedSync = true;
mSyncState = ss;
mSyncRowId = ss.firstId;
mSyncPosition = ss.position;
mSpecificTop = ss.viewTop;
}
requestLayout();
}
private class PerformClick extends WindowRunnnable implements Runnable {
int mClickMotionPosition;
public void run() {
if (mDataChanged) return;
final ListAdapter adapter = mAdapter;
final int motionPosition = mClickMotionPosition;
if (adapter != null && mItemCount > 0 &&
motionPosition != INVALID_POSITION &&
motionPosition < adapter.getCount() && sameWindow()) {
final View view = getChildAt(motionPosition); // a fix by @pboos
if (view != null) {
final int clickPosition = motionPosition + mFirstPosition;
performItemClick(view, clickPosition, adapter.getItemId(clickPosition));
}
}
}
}
private boolean performLongPress(final View child,
final int longPressPosition, final long longPressId) {
boolean handled = false;
OnItemLongClickListener onItemLongClickListener = getOnItemLongClickListener();
if (onItemLongClickListener != null) {
handled = onItemLongClickListener.onItemLongClick(ExtendableListView.this, child,
longPressPosition, longPressId);
}
// if (!handled) {
// mContextMenuInfo = createContextMenuInfo(child, longPressPosition, longPressId);
// handled = super.showContextMenuForChild(AbsListView.this);
// }
if (handled) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
return handled;
}
/**
* A base class for Runnables that will check that their view is still attached to
* the original window as when the Runnable was created.
*/
private class WindowRunnnable {
private int mOriginalAttachCount;
public void rememberWindowAttachCount() {
mOriginalAttachCount = getWindowAttachCount();
}
public boolean sameWindow() {
return hasWindowFocus() && getWindowAttachCount() == mOriginalAttachCount;
}
}
}