adapter
, this method calls the appropriate
* set*Listener method with adapter
as the argument.
*
* @param adapter The ListAdapter providing data to back
* DragSortListView.
*
* @see android.widget.ListView#setAdapter(android.widget.ListAdapter)
*/
@Override
public void setAdapter(ListAdapter adapter) {
if (adapter != null) {
mAdapterWrapper = new AdapterWrapper(adapter);
adapter.registerDataSetObserver(mObserver);
if (adapter instanceof DropListener) {
setDropListener((DropListener) adapter);
}
if (adapter instanceof DragListener) {
setDragListener((DragListener) adapter);
}
if (adapter instanceof RemoveListener) {
setRemoveListener((RemoveListener) adapter);
}
} else {
mAdapterWrapper = null;
}
super.setAdapter(mAdapterWrapper);
}
/**
* As opposed to {@link ListView#getAdapter()}, which returns
* a heavily wrapped ListAdapter (DragSortListView wraps the
* input ListAdapter {\emph and} ListView wraps the wrapped one).
*
* @return The ListAdapter set as the argument of {@link setAdapter()}
*/
public ListAdapter getInputAdapter() {
if (mAdapterWrapper == null) {
return null;
} else {
return mAdapterWrapper.getAdapter();
}
}
private class AdapterWrapper extends BaseAdapter {
private ListAdapter mAdapter;
public AdapterWrapper(ListAdapter adapter) {
super();
mAdapter = adapter;
mAdapter.registerDataSetObserver(new DataSetObserver() {
public void onChanged() {
notifyDataSetChanged();
}
public void onInvalidated() {
notifyDataSetInvalidated();
}
});
}
public ListAdapter getAdapter() {
return mAdapter;
}
@Override
public long getItemId(int position) {
return mAdapter.getItemId(position);
}
@Override
public Object getItem(int position) {
return mAdapter.getItem(position);
}
@Override
public int getCount() {
return mAdapter.getCount();
}
@Override
public boolean areAllItemsEnabled() {
return mAdapter.areAllItemsEnabled();
}
@Override
public boolean isEnabled(int position) {
return mAdapter.isEnabled(position);
}
@Override
public int getItemViewType(int position) {
return mAdapter.getItemViewType(position);
}
@Override
public int getViewTypeCount() {
return mAdapter.getViewTypeCount();
}
@Override
public boolean hasStableIds() {
return mAdapter.hasStableIds();
}
@Override
public boolean isEmpty() {
return mAdapter.isEmpty();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
DragSortItemView v;
View child;
// Log.d("mobeta",
// "getView: position="+position+" convertView="+convertView);
if (convertView != null) {
v = (DragSortItemView) convertView;
View oldChild = v.getChildAt(0);
child = mAdapter.getView(position, oldChild, DragSortListView.this);
if (child != oldChild) {
// shouldn't get here if user is reusing convertViews
// properly
if (oldChild != null) {
v.removeViewAt(0);
}
v.addView(child);
}
} else {
child = mAdapter.getView(position, null, DragSortListView.this);
if (child instanceof Checkable) {
v = new DragSortItemViewCheckable(getContext());
} else {
v = new DragSortItemView(getContext());
}
v.setLayoutParams(new AbsListView.LayoutParams(
ViewGroup.LayoutParams.FILL_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
v.addView(child);
}
// Set the correct item height given drag state; passed
// View needs to be measured if measurement is required.
adjustItem(position + getHeaderViewsCount(), v, true);
return v;
}
}
private void drawDivider(int expPosition, Canvas canvas) {
final Drawable divider = getDivider();
final int dividerHeight = getDividerHeight();
// Log.d("mobeta", "div="+divider+" divH="+dividerHeight);
if (divider != null && dividerHeight != 0) {
final ViewGroup expItem = (ViewGroup) getChildAt(expPosition
- getFirstVisiblePosition());
if (expItem != null) {
final int l = getPaddingLeft();
final int r = getWidth() - getPaddingRight();
final int t;
final int b;
final int childHeight = expItem.getChildAt(0).getHeight();
if (expPosition > mSrcPos) {
t = expItem.getTop() + childHeight;
b = t + dividerHeight;
} else {
b = expItem.getBottom() - childHeight;
t = b - dividerHeight;
}
// Log.d("mobeta", "l="+l+" t="+t+" r="+r+" b="+b);
// Have to clip to support ColorDrawable on <= Gingerbread
canvas.save();
canvas.clipRect(l, t, r, b);
divider.setBounds(l, t, r, b);
divider.draw(canvas);
canvas.restore();
}
}
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mDragState != IDLE) {
// draw the divider over the expanded item
if (mFirstExpPos != mSrcPos) {
drawDivider(mFirstExpPos, canvas);
}
if (mSecondExpPos != mFirstExpPos && mSecondExpPos != mSrcPos) {
drawDivider(mSecondExpPos, canvas);
}
}
if (mFloatView != null) {
// draw the float view over everything
final int w = mFloatView.getWidth();
final int h = mFloatView.getHeight();
int x = mFloatLoc.x;
int width = getWidth();
if (x < 0)
x = -x;
float alphaMod;
if (x < width) {
alphaMod = ((float) (width - x)) / ((float) width);
alphaMod *= alphaMod;
} else {
alphaMod = 0;
}
final int alpha = (int) (255f * mCurrFloatAlpha * alphaMod);
canvas.save();
// Log.d("mobeta", "clip rect bounds: " + canvas.getClipBounds());
canvas.translate(mFloatLoc.x, mFloatLoc.y);
canvas.clipRect(0, 0, w, h);
// Log.d("mobeta", "clip rect bounds: " + canvas.getClipBounds());
canvas.saveLayerAlpha(0, 0, w, h, alpha, Canvas.ALL_SAVE_FLAG);
mFloatView.draw(canvas);
canvas.restore();
canvas.restore();
}
}
private int getItemHeight(int position) {
View v = getChildAt(position - getFirstVisiblePosition());
if (v != null) {
// item is onscreen, just get the height of the View
return v.getHeight();
} else {
// item is offscreen. get child height and calculate
// item height based on current shuffle state
return calcItemHeight(position, getChildHeight(position));
}
}
private void printPosData() {
Log.d("mobeta", "mSrcPos=" + mSrcPos + " mFirstExpPos=" + mFirstExpPos + " mSecondExpPos="
+ mSecondExpPos);
}
private class HeightCache {
private SparseIntArray mMap;
private ArrayListtrue
as the first argument.
*/
public void cancelDrag() {
if (mDragState == DRAGGING) {
mDragScroller.stopScrolling(true);
destroyFloatView();
clearPositions();
adjustAllItems();
if (mInTouchEvent) {
mDragState = STOPPED;
} else {
mDragState = IDLE;
}
}
}
private void clearPositions() {
mSrcPos = -1;
mFirstExpPos = -1;
mSecondExpPos = -1;
mFloatPos = -1;
}
private void dropFloatView() {
// must set to avoid cancelDrag being called from the
// DataSetObserver
mDragState = DROPPING;
if (mDropListener != null && mFloatPos >= 0 && mFloatPos < getCount()) {
final int numHeaders = getHeaderViewsCount();
mDropListener.drop(mSrcPos - numHeaders, mFloatPos - numHeaders);
}
destroyFloatView();
adjustOnReorder();
clearPositions();
adjustAllItems();
// now the drag is done
if (mInTouchEvent) {
mDragState = STOPPED;
} else {
mDragState = IDLE;
}
}
private void doRemoveItem() {
doRemoveItem(mSrcPos - getHeaderViewsCount());
}
/**
* Removes dragged item from the list. Calls RemoveListener.
*/
private void doRemoveItem(int which) {
// must set to avoid cancelDrag being called from the
// DataSetObserver
mDragState = REMOVING;
// end it
if (mRemoveListener != null) {
mRemoveListener.remove(which);
}
destroyFloatView();
adjustOnReorder();
clearPositions();
// now the drag is done
if (mInTouchEvent) {
mDragState = STOPPED;
} else {
mDragState = IDLE;
}
}
private void adjustOnReorder() {
final int firstPos = getFirstVisiblePosition();
// Log.d("mobeta", "first="+firstPos+" src="+mSrcPos);
if (mSrcPos < firstPos) {
// collapsed src item is off screen;
// adjust the scroll after item heights have been fixed
View v = getChildAt(0);
int top = 0;
if (v != null) {
top = v.getTop();
}
// Log.d("mobeta", "top="+top+" fvh="+mFloatViewHeight);
setSelectionFromTop(firstPos - 1, top - getPaddingTop());
}
}
/**
* Stop a drag in progress. Pass true
if you would
* like to remove the dragged item from the list.
*
* @param remove Remove the dragged item from the list. Calls
* a registered RemoveListener, if one exists. Otherwise, calls
* the DropListener, if one exists.
*
* @return True if the stop was successful. False if there is
* no floating View.
*/
public boolean stopDrag(boolean remove) {
mUseRemoveVelocity = false;
return stopDrag(remove, 0);
}
public boolean stopDragWithVelocity(boolean remove, float velocityX) {
mUseRemoveVelocity = true;
return stopDrag(remove, velocityX);
}
public boolean stopDrag(boolean remove, float velocityX) {
if (mFloatView != null) {
mDragScroller.stopScrolling(true);
if (remove) {
removeItem(mSrcPos - getHeaderViewsCount(), velocityX);
} else {
if (mDropAnimator != null) {
mDropAnimator.start();
} else {
dropFloatView();
}
}
if (mTrackDragSort) {
mDragSortTracker.stopTracking();
}
return true;
} else {
// stop failed
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mIgnoreTouchEvent) {
mIgnoreTouchEvent = false;
return false;
}
if (!mDragEnabled) {
return super.onTouchEvent(ev);
}
boolean more = false;
boolean lastCallWasIntercept = mLastCallWasIntercept;
mLastCallWasIntercept = false;
if (!lastCallWasIntercept) {
saveTouchCoords(ev);
}
// if (mFloatView != null) {
if (mDragState == DRAGGING) {
onDragTouchEvent(ev);
more = true; // give us more!
} else {
// what if float view is null b/c we dropped in middle
// of drag touch event?
// if (mDragState != STOPPED) {
if (mDragState == IDLE) {
if (super.onTouchEvent(ev)) {
more = true;
}
}
int action = ev.getAction() & MotionEvent.ACTION_MASK;
switch (action) {
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
doActionUpOrCancel();
break;
default:
if (more) {
mCancelMethod = ON_TOUCH_EVENT;
}
}
}
return more;
}
private void doActionUpOrCancel() {
mCancelMethod = NO_CANCEL;
mInTouchEvent = false;
if (mDragState == STOPPED) {
mDragState = IDLE;
}
mCurrFloatAlpha = mFloatAlpha;
mListViewIntercepted = false;
mChildHeightCache.clear();
}
private void saveTouchCoords(MotionEvent ev) {
int action = ev.getAction() & MotionEvent.ACTION_MASK;
if (action != MotionEvent.ACTION_DOWN) {
mLastX = mX;
mLastY = mY;
}
mX = (int) ev.getX();
mY = (int) ev.getY();
if (action == MotionEvent.ACTION_DOWN) {
mLastX = mX;
mLastY = mY;
}
mOffsetX = (int) ev.getRawX() - mX;
mOffsetY = (int) ev.getRawY() - mY;
}
public boolean listViewIntercepted() {
return mListViewIntercepted;
}
private boolean mListViewIntercepted = false;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (!mDragEnabled) {
return super.onInterceptTouchEvent(ev);
}
saveTouchCoords(ev);
mLastCallWasIntercept = true;
int action = ev.getAction() & MotionEvent.ACTION_MASK;
if (action == MotionEvent.ACTION_DOWN) {
if (mDragState != IDLE) {
// intercept and ignore
mIgnoreTouchEvent = true;
return true;
}
mInTouchEvent = true;
}
boolean intercept = false;
// the following deals with calls to super.onInterceptTouchEvent
if (mFloatView != null) {
// super's touch event canceled in startDrag
intercept = true;
} else {
if (super.onInterceptTouchEvent(ev)) {
mListViewIntercepted = true;
intercept = true;
}
switch (action) {
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
doActionUpOrCancel();
break;
default:
if (intercept) {
mCancelMethod = ON_TOUCH_EVENT;
} else {
mCancelMethod = ON_INTERCEPT_TOUCH_EVENT;
}
}
}
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
mInTouchEvent = false;
}
return intercept;
}
/**
* Set the width of each drag scroll region by specifying
* a fraction of the ListView height.
*
* @param heightFraction Fraction of ListView height. Capped at
* 0.5f.
*
*/
public void setDragScrollStart(float heightFraction) {
setDragScrollStarts(heightFraction, heightFraction);
}
/**
* Set the width of each drag scroll region by specifying
* a fraction of the ListView height.
*
* @param upperFrac Fraction of ListView height for up-scroll bound.
* Capped at 0.5f.
* @param lowerFrac Fraction of ListView height for down-scroll bound.
* Capped at 0.5f.
*
*/
public void setDragScrollStarts(float upperFrac, float lowerFrac) {
if (lowerFrac > 0.5f) {
mDragDownScrollStartFrac = 0.5f;
} else {
mDragDownScrollStartFrac = lowerFrac;
}
if (upperFrac > 0.5f) {
mDragUpScrollStartFrac = 0.5f;
} else {
mDragUpScrollStartFrac = upperFrac;
}
if (getHeight() != 0) {
updateScrollStarts();
}
}
private void continueDrag(int x, int y) {
// proposed position
mFloatLoc.x = x - mDragDeltaX;
mFloatLoc.y = y - mDragDeltaY;
doDragFloatView(true);
int minY = Math.min(y, mFloatViewMid + mFloatViewHeightHalf);
int maxY = Math.max(y, mFloatViewMid - mFloatViewHeightHalf);
// get the current scroll direction
int currentScrollDir = mDragScroller.getScrollDir();
if (minY > mLastY && minY > mDownScrollStartY && currentScrollDir != DragScroller.DOWN) {
// dragged down, it is below the down scroll start and it is not
// scrolling up
if (currentScrollDir != DragScroller.STOP) {
// moved directly from up scroll to down scroll
mDragScroller.stopScrolling(true);
}
// start scrolling down
mDragScroller.startScrolling(DragScroller.DOWN);
} else if (maxY < mLastY && maxY < mUpScrollStartY && currentScrollDir != DragScroller.UP) {
// dragged up, it is above the up scroll start and it is not
// scrolling up
if (currentScrollDir != DragScroller.STOP) {
// moved directly from down scroll to up scroll
mDragScroller.stopScrolling(true);
}
// start scrolling up
mDragScroller.startScrolling(DragScroller.UP);
}
else if (maxY >= mUpScrollStartY && minY <= mDownScrollStartY
&& mDragScroller.isScrolling()) {
// not in the upper nor in the lower drag-scroll regions but it is
// still scrolling
mDragScroller.stopScrolling(true);
}
}
private void updateScrollStarts() {
final int padTop = getPaddingTop();
final int listHeight = getHeight() - padTop - getPaddingBottom();
float heightF = (float) listHeight;
mUpScrollStartYF = padTop + mDragUpScrollStartFrac * heightF;
mDownScrollStartYF = padTop + (1.0f - mDragDownScrollStartFrac) * heightF;
mUpScrollStartY = (int) mUpScrollStartYF;
mDownScrollStartY = (int) mDownScrollStartYF;
mDragUpScrollHeight = mUpScrollStartYF - padTop;
mDragDownScrollHeight = padTop + listHeight - mDownScrollStartYF;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
updateScrollStarts();
}
private void adjustAllItems() {
final int first = getFirstVisiblePosition();
final int last = getLastVisiblePosition();
int begin = Math.max(0, getHeaderViewsCount() - first);
int end = Math.min(last - first, getCount() - 1 - getFooterViewsCount() - first);
for (int i = begin; i <= end; ++i) {
View v = getChildAt(i);
if (v != null) {
adjustItem(first + i, v, false);
}
}
}
private void adjustItem(int position) {
View v = getChildAt(position - getFirstVisiblePosition());
if (v != null) {
adjustItem(position, v, false);
}
}
/**
* Sets layout param height, gravity, and visibility on
* wrapped item.
*/
private void adjustItem(int position, View v, boolean invalidChildHeight) {
// Adjust item height
ViewGroup.LayoutParams lp = v.getLayoutParams();
int height;
if (position != mSrcPos && position != mFirstExpPos && position != mSecondExpPos) {
height = ViewGroup.LayoutParams.WRAP_CONTENT;
} else {
height = calcItemHeight(position, v, invalidChildHeight);
}
if (height != lp.height) {
lp.height = height;
v.setLayoutParams(lp);
}
// Adjust item gravity
if (position == mFirstExpPos || position == mSecondExpPos) {
if (position < mSrcPos) {
((DragSortItemView) v).setGravity(Gravity.BOTTOM);
} else if (position > mSrcPos) {
((DragSortItemView) v).setGravity(Gravity.TOP);
}
}
// Finally adjust item visibility
int oldVis = v.getVisibility();
int vis = View.VISIBLE;
if (position == mSrcPos && mFloatView != null) {
vis = View.INVISIBLE;
}
if (vis != oldVis) {
v.setVisibility(vis);
}
}
private int getChildHeight(int position) {
if (position == mSrcPos) {
return 0;
}
View v = getChildAt(position - getFirstVisiblePosition());
if (v != null) {
// item is onscreen, therefore child height is valid,
// hence the "true"
return getChildHeight(position, v, false);
} else {
// item is offscreen
// first check cache for child height at this position
int childHeight = mChildHeightCache.get(position);
if (childHeight != -1) {
// Log.d("mobeta", "found child height in cache!");
return childHeight;
}
final ListAdapter adapter = getAdapter();
int type = adapter.getItemViewType(position);
// There might be a better place for checking for the following
final int typeCount = adapter.getViewTypeCount();
if (typeCount != mSampleViewTypes.length) {
mSampleViewTypes = new View[typeCount];
}
if (type >= 0) {
if (mSampleViewTypes[type] == null) {
v = adapter.getView(position, null, this);
mSampleViewTypes[type] = v;
} else {
v = adapter.getView(position, mSampleViewTypes[type], this);
}
} else {
// type is HEADER_OR_FOOTER or IGNORE
v = adapter.getView(position, null, this);
}
// current child height is invalid, hence "true" below
childHeight = getChildHeight(position, v, true);
// cache it because this could have been expensive
mChildHeightCache.add(position, childHeight);
return childHeight;
}
}
private int getChildHeight(int position, View item, boolean invalidChildHeight) {
if (position == mSrcPos) {
return 0;
}
View child;
if (position < getHeaderViewsCount() || position >= getCount() - getFooterViewsCount()) {
child = item;
} else {
child = ((ViewGroup) item).getChildAt(0);
}
ViewGroup.LayoutParams lp = child.getLayoutParams();
if (lp != null) {
if (lp.height > 0) {
return lp.height;
}
}
int childHeight = child.getHeight();
if (childHeight == 0 || invalidChildHeight) {
measureItem(child);
childHeight = child.getMeasuredHeight();
}
return childHeight;
}
private int calcItemHeight(int position, View item, boolean invalidChildHeight) {
return calcItemHeight(position, getChildHeight(position, item, invalidChildHeight));
}
private int calcItemHeight(int position, int childHeight) {
int divHeight = getDividerHeight();
boolean isSliding = mAnimate && mFirstExpPos != mSecondExpPos;
int maxNonSrcBlankHeight = mFloatViewHeight - mItemHeightCollapsed;
int slideHeight = (int) (mSlideFrac * maxNonSrcBlankHeight);
int height;
if (position == mSrcPos) {
if (mSrcPos == mFirstExpPos) {
if (isSliding) {
height = slideHeight + mItemHeightCollapsed;
} else {
height = mFloatViewHeight;
}
} else if (mSrcPos == mSecondExpPos) {
// if gets here, we know an item is sliding
height = mFloatViewHeight - slideHeight;
} else {
height = mItemHeightCollapsed;
}
} else if (position == mFirstExpPos) {
if (isSliding) {
height = childHeight + slideHeight;
} else {
height = childHeight + maxNonSrcBlankHeight;
}
} else if (position == mSecondExpPos) {
// we know an item is sliding (b/c 2ndPos != 1stPos)
height = childHeight + maxNonSrcBlankHeight - slideHeight;
} else {
height = childHeight;
}
return height;
}
@Override
public void requestLayout() {
if (!mBlockLayoutRequests) {
super.requestLayout();
}
}
private int adjustScroll(int movePos, View moveItem, int oldFirstExpPos, int oldSecondExpPos) {
int adjust = 0;
final int childHeight = getChildHeight(movePos);
int moveHeightBefore = moveItem.getHeight();
int moveHeightAfter = calcItemHeight(movePos, childHeight);
int moveBlankBefore = moveHeightBefore;
int moveBlankAfter = moveHeightAfter;
if (movePos != mSrcPos) {
moveBlankBefore -= childHeight;
moveBlankAfter -= childHeight;
}
int maxBlank = mFloatViewHeight;
if (mSrcPos != mFirstExpPos && mSrcPos != mSecondExpPos) {
maxBlank -= mItemHeightCollapsed;
}
if (movePos <= oldFirstExpPos) {
if (movePos > mFirstExpPos) {
adjust += maxBlank - moveBlankAfter;
}
} else if (movePos == oldSecondExpPos) {
if (movePos <= mFirstExpPos) {
adjust += moveBlankBefore - maxBlank;
} else if (movePos == mSecondExpPos) {
adjust += moveHeightBefore - moveHeightAfter;
} else {
adjust += moveBlankBefore;
}
} else {
if (movePos <= mFirstExpPos) {
adjust -= maxBlank;
} else if (movePos == mSecondExpPos) {
adjust -= moveBlankAfter;
}
}
return adjust;
}
private void measureItem(View item) {
ViewGroup.LayoutParams lp = item.getLayoutParams();
if (lp == null) {
lp = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
item.setLayoutParams(lp);
}
int wspec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, getListPaddingLeft()
+ getListPaddingRight(), lp.width);
int hspec;
if (lp.height > 0) {
hspec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
} else {
hspec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
}
item.measure(wspec, hspec);
}
private void measureFloatView() {
if (mFloatView != null) {
measureItem(mFloatView);
mFloatViewHeight = mFloatView.getMeasuredHeight();
mFloatViewHeightHalf = mFloatViewHeight / 2;
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// Log.d("mobeta", "onMeasure called");
if (mFloatView != null) {
if (mFloatView.isLayoutRequested()) {
measureFloatView();
}
mFloatViewOnMeasured = true; // set to false after layout
}
mWidthMeasureSpec = widthMeasureSpec;
}
@Override
protected void layoutChildren() {
super.layoutChildren();
if (mFloatView != null) {
if (mFloatView.isLayoutRequested() && !mFloatViewOnMeasured) {
// Have to measure here when usual android measure
// pass is skipped. This happens during a drag-sort
// when layoutChildren is called directly.
measureFloatView();
}
mFloatView.layout(0, 0, mFloatView.getMeasuredWidth(), mFloatView.getMeasuredHeight());
mFloatViewOnMeasured = false;
}
}
protected boolean onDragTouchEvent(MotionEvent ev) {
// we are in a drag
int action = ev.getAction() & MotionEvent.ACTION_MASK;
switch (ev.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_CANCEL:
if (mDragState == DRAGGING) {
cancelDrag();
}
doActionUpOrCancel();
break;
case MotionEvent.ACTION_UP:
// Log.d("mobeta", "calling stopDrag from onDragTouchEvent");
if (mDragState == DRAGGING) {
stopDrag(false);
}
doActionUpOrCancel();
break;
case MotionEvent.ACTION_MOVE:
continueDrag((int) ev.getX(), (int) ev.getY());
break;
}
return true;
}
private boolean mFloatViewInvalidated = false;
private void invalidateFloatView() {
mFloatViewInvalidated = true;
}
/**
* Start a drag of item at position
using the
* registered FloatViewManager. Calls through
* to {@link #startDrag(int,View,int,int,int)} after obtaining
* the floating View from the FloatViewManager.
*
* @param position Item to drag.
* @param dragFlags Flags that restrict some movements of the
* floating View. For example, set dragFlags |=
* ~{@link #DRAG_NEG_X}
to allow dragging the floating
* View in all directions except off the screen to the left.
* @param deltaX Offset in x of the touch coordinate from the
* left edge of the floating View (i.e. touch-x minus float View
* left).
* @param deltaY Offset in y of the touch coordinate from the
* top edge of the floating View (i.e. touch-y minus float View
* top).
*
* @return True if the drag was started, false otherwise. This
* startDrag
will fail if we are not currently in
* a touch event, there is no registered FloatViewManager,
* or the FloatViewManager returns a null View.
*/
public boolean startDrag(int position, int dragFlags, int deltaX, int deltaY) {
if (!mInTouchEvent || mFloatViewManager == null) {
return false;
}
View v = mFloatViewManager.onCreateFloatView(position);
if (v == null) {
return false;
} else {
return startDrag(position, v, dragFlags, deltaX, deltaY);
}
}
/**
* Start a drag of item at position
without using
* a FloatViewManager.
*
* @param position Item to drag.
* @param floatView Floating View.
* @param dragFlags Flags that restrict some movements of the
* floating View. For example, set dragFlags |=
* ~{@link #DRAG_NEG_X}
to allow dragging the floating
* View in all directions except off the screen to the left.
* @param deltaX Offset in x of the touch coordinate from the
* left edge of the floating View (i.e. touch-x minus float View
* left).
* @param deltaY Offset in y of the touch coordinate from the
* top edge of the floating View (i.e. touch-y minus float View
* top).
*
* @return True if the drag was started, false otherwise. This
* startDrag
will fail if we are not currently in
* a touch event, floatView
is null, or there is
* a drag in progress.
*/
public boolean startDrag(int position, View floatView, int dragFlags, int deltaX, int deltaY) {
if (mDragState != IDLE || !mInTouchEvent || mFloatView != null || floatView == null
|| !mDragEnabled) {
return false;
}
if (getParent() != null) {
getParent().requestDisallowInterceptTouchEvent(true);
}
int pos = position + getHeaderViewsCount();
mFirstExpPos = pos;
mSecondExpPos = pos;
mSrcPos = pos;
mFloatPos = pos;
// mDragState = dragType;
mDragState = DRAGGING;
mDragFlags = 0;
mDragFlags |= dragFlags;
mFloatView = floatView;
measureFloatView(); // sets mFloatViewHeight
mDragDeltaX = deltaX;
mDragDeltaY = deltaY;
mDragStartY = mY;
// updateFloatView(mX - mDragDeltaX, mY - mDragDeltaY);
mFloatLoc.x = mX - mDragDeltaX;
mFloatLoc.y = mY - mDragDeltaY;
// set src item invisible
final View srcItem = getChildAt(mSrcPos - getFirstVisiblePosition());
if (srcItem != null) {
srcItem.setVisibility(View.INVISIBLE);
}
if (mTrackDragSort) {
mDragSortTracker.startTracking();
}
// once float view is created, events are no longer passed
// to ListView
switch (mCancelMethod) {
case ON_TOUCH_EVENT:
super.onTouchEvent(mCancelEvent);
break;
case ON_INTERCEPT_TOUCH_EVENT:
super.onInterceptTouchEvent(mCancelEvent);
break;
}
requestLayout();
if (mLiftAnimator != null) {
mLiftAnimator.start();
}
return true;
}
private void doDragFloatView(boolean forceInvalidate) {
int movePos = getFirstVisiblePosition() + getChildCount() / 2;
View moveItem = getChildAt(getChildCount() / 2);
if (moveItem == null) {
return;
}
doDragFloatView(movePos, moveItem, forceInvalidate);
}
private void doDragFloatView(int movePos, View moveItem, boolean forceInvalidate) {
mBlockLayoutRequests = true;
updateFloatView();
int oldFirstExpPos = mFirstExpPos;
int oldSecondExpPos = mSecondExpPos;
boolean updated = updatePositions();
if (updated) {
adjustAllItems();
int scroll = adjustScroll(movePos, moveItem, oldFirstExpPos, oldSecondExpPos);
// Log.d("mobeta", " adjust scroll="+scroll);
setSelectionFromTop(movePos, moveItem.getTop() + scroll - getPaddingTop());
layoutChildren();
}
if (updated || forceInvalidate) {
invalidate();
}
mBlockLayoutRequests = false;
}
/**
* Sets float View location based on suggested values and
* constraints set in mDragFlags.
*/
private void updateFloatView() {
if (mFloatViewManager != null) {
mTouchLoc.set(mX, mY);
mFloatViewManager.onDragFloatView(mFloatView, mFloatLoc, mTouchLoc);
}
final int floatX = mFloatLoc.x;
final int floatY = mFloatLoc.y;
// restrict x motion
int padLeft = getPaddingLeft();
if ((mDragFlags & DRAG_POS_X) == 0 && floatX > padLeft) {
mFloatLoc.x = padLeft;
} else if ((mDragFlags & DRAG_NEG_X) == 0 && floatX < padLeft) {
mFloatLoc.x = padLeft;
}
// keep floating view from going past bottom of last header view
final int numHeaders = getHeaderViewsCount();
final int numFooters = getFooterViewsCount();
final int firstPos = getFirstVisiblePosition();
final int lastPos = getLastVisiblePosition();
// Log.d("mobeta",
// "nHead="+numHeaders+" nFoot="+numFooters+" first="+firstPos+" last="+lastPos);
int topLimit = getPaddingTop();
if (firstPos < numHeaders) {
topLimit = getChildAt(numHeaders - firstPos - 1).getBottom();
}
if ((mDragFlags & DRAG_NEG_Y) == 0) {
if (firstPos <= mSrcPos) {
topLimit = Math.max(getChildAt(mSrcPos - firstPos).getTop(), topLimit);
}
}
// bottom limit is top of first footer View or
// bottom of last item in list
int bottomLimit = getHeight() - getPaddingBottom();
if (lastPos >= getCount() - numFooters - 1) {
bottomLimit = getChildAt(getCount() - numFooters - 1 - firstPos).getBottom();
}
if ((mDragFlags & DRAG_POS_Y) == 0) {
if (lastPos >= mSrcPos) {
bottomLimit = Math.min(getChildAt(mSrcPos - firstPos).getBottom(), bottomLimit);
}
}
// Log.d("mobeta", "dragView top=" + (y - mDragDeltaY));
// Log.d("mobeta", "limit=" + limit);
// Log.d("mobeta", "mDragDeltaY=" + mDragDeltaY);
if (floatY < topLimit) {
mFloatLoc.y = topLimit;
} else if (floatY + mFloatViewHeight > bottomLimit) {
mFloatLoc.y = bottomLimit - mFloatViewHeight;
}
// get y-midpoint of floating view (constrained to ListView bounds)
mFloatViewMid = mFloatLoc.y + mFloatViewHeightHalf;
}
private void destroyFloatView() {
if (mFloatView != null) {
mFloatView.setVisibility(GONE);
if (mFloatViewManager != null) {
mFloatViewManager.onDestroyFloatView(mFloatView);
}
mFloatView = null;
invalidate();
}
}
/**
* Interface for customization of the floating View appearance
* and dragging behavior. Implement
* your own and pass it to {@link #setFloatViewManager}. If
* your own is not passed, the default {@link SimpleFloatViewManager}
* implementation is used.
*/
public interface FloatViewManager {
/**
* Return the floating View for item at position
.
* DragSortListView will measure and layout this View for you,
* so feel free to just inflate it. You can help DSLV by
* setting some {@link ViewGroup.LayoutParams} on this View;
* otherwise it will set some for you (with a width of FILL_PARENT
* and a height of WRAP_CONTENT).
*
* @param position Position of item to drag (NOTE:
* position
excludes header Views; thus, if you
* want to call {@link ListView#getChildAt(int)}, you will need
* to add {@link ListView#getHeaderViewsCount()} to the index).
*
* @return The View you wish to display as the floating View.
*/
public View onCreateFloatView(int position);
/**
* Called whenever the floating View is dragged. Float View
* properties can be changed here. Also, the upcoming location
* of the float View can be altered by setting
* location.x
and location.y
.
*
* @param floatView The floating View.
* @param location The location (top-left; relative to DSLV
* top-left) at which the float
* View would like to appear, given the current touch location
* and the offset provided in {@link DragSortListView#startDrag}.
* @param touch The current touch location (relative to DSLV
* top-left).
* @param pendingScroll
*/
public void onDragFloatView(View floatView, Point location, Point touch);
/**
* Called when the float View is dropped; lets you perform
* any necessary cleanup. The internal DSLV floating View
* reference is set to null immediately after this is called.
*
* @param floatView The floating View passed to
* {@link #onCreateFloatView(int)}.
*/
public void onDestroyFloatView(View floatView);
}
public void setFloatViewManager(FloatViewManager manager) {
mFloatViewManager = manager;
}
public void setDragListener(DragListener l) {
mDragListener = l;
}
/**
* Allows for easy toggling between a DragSortListView
* and a regular old ListView. If enabled, items are
* draggable, where the drag init mode determines how
* items are lifted (see {@link setDragInitMode(int)}).
* If disabled, items cannot be dragged.
*
* @param enabled Set true
to enable list
* item dragging
*/
public void setDragEnabled(boolean enabled) {
mDragEnabled = enabled;
}
public boolean isDragEnabled() {
return mDragEnabled;
}
/**
* This better reorder your ListAdapter! DragSortListView does not do this
* for you; doesn't make sense to. Make sure
* {@link BaseAdapter#notifyDataSetChanged()} or something like it is called
* in your implementation. Furthermore, if you have a choiceMode other than
* none and the ListAdapter does not return true for
* {@link ListAdapter#hasStableIds()}, you will need to call
* {@link #moveCheckState(int, int)} to move the check boxes along with the
* list items.
*
* @param l
*/
public void setDropListener(DropListener l) {
mDropListener = l;
}
/**
* Probably a no-brainer, but make sure that your remove listener
* calls {@link BaseAdapter#notifyDataSetChanged()} or something like it.
* When an item removal occurs, DragSortListView
* relies on a redraw of all the items to recover invisible views
* and such. Strictly speaking, if you remove something, your dataset
* has changed...
*
* @param l
*/
public void setRemoveListener(RemoveListener l) {
mRemoveListener = l;
}
public interface DragListener {
public void drag(int from, int to);
}
/**
* Your implementation of this has to reorder your ListAdapter!
* Make sure to call
* {@link BaseAdapter#notifyDataSetChanged()} or something like it
* in your implementation.
*
* @author heycosmo
*
*/
public interface DropListener {
public void drop(int from, int to);
}
/**
* Make sure to call
* {@link BaseAdapter#notifyDataSetChanged()} or something like it
* in your implementation.
*
* @author heycosmo
*
*/
public interface RemoveListener {
public void remove(int which);
}
public interface DragSortListener extends DropListener, DragListener, RemoveListener {
}
public void setDragSortListener(DragSortListener l) {
setDropListener(l);
setDragListener(l);
setRemoveListener(l);
}
/**
* Completely custom scroll speed profile. Default increases linearly
* with position and is constant in time. Create your own by implementing
* {@link DragSortListView.DragScrollProfile}.
*
* @param ssp
*/
public void setDragScrollProfile(DragScrollProfile ssp) {
if (ssp != null) {
mScrollProfile = ssp;
}
}
/**
* Use this to move the check state of an item from one position to another
* in a drop operation. If you have a choiceMode which is not none, this
* method must be called when the order of items changes in an underlying
* adapter which does not have stable IDs (see
* {@link ListAdapter#hasStableIds()}). This is because without IDs, the
* ListView has no way of knowing which items have moved where, and cannot
* update the check state accordingly.
*
* A word of warning about a "feature" in Android that you may run into when
* dealing with movable list items: for an adapter that does have
* stable IDs, ListView will attempt to locate each item based on its ID and
* move the check state from the item's old position to the new position —
* which is all fine and good (and removes the need for calling this
* function), except for the half-baked approach. Apparently to save time in
* the naive algorithm used, ListView will only search for an ID in the
* close neighborhood of the old position. If the user moves an item too far
* (specifically, more than 20 rows away), ListView will give up and just
* force the item to be unchecked. So if there is a reasonable chance that
* the user will move items more than 20 rows away from the original
* position, you may wish to use an adapter with unstable IDs and call this
* method manually instead.
*
* @param from
* @param to
*/
public void moveCheckState(int from, int to) {
// This method runs in O(n log n) time (n being the number of list
// items). The bottleneck is the call to AbsListView.setItemChecked,
// which is O(log n) because of the binary search involved in calling
// SparseBooleanArray.put().
//
// To improve on the average time, we minimize the number of calls to
// setItemChecked by only calling it for items that actually have a
// changed state. This is achieved by building a list containing the
// start and end of the "runs" of checked items, and then moving the
// runs. Note that moving an item from A to B is essentially a rotation
// of the range of items in [A, B]. Let's say we have
// . . U V X Y Z . .
// and move U after Z. This is equivalent to a rotation one step to the
// left within the range you are moving across:
// . . V X Y Z U . .
//
// So, to perform the move we enumerate all the runs within the move
// range, then rotate each run one step to the left or right (depending
// on move direction). For example, in the list:
// X X . X X X . X
// we have two runs. One begins at the last item of the list and wraps
// around to the beginning, ending at position 1. The second begins at
// position 3 and ends at position 5. To rotate a run, regardless of
// length, we only need to set a check mark at one end of the run, and
// clear a check mark at the other end:
// X . X X X . X X
SparseBooleanArray cip = getCheckedItemPositions();
int rangeStart = from;
int rangeEnd = to;
if (to < from) {
rangeStart = to;
rangeEnd = from;
}
rangeEnd += 1;
int[] runStart = new int[cip.size()];
int[] runEnd = new int[cip.size()];
int runCount = buildRunList(cip, rangeStart, rangeEnd, runStart, runEnd);
if (runCount == 1 && (runStart[0] == runEnd[0])) {
// Special case where all items are checked, we can never set any
// item to false like we do below.
return;
}
if (from < to) {
for (int i = 0; i != runCount; i++) {
setItemChecked(rotate(runStart[i], -1, rangeStart, rangeEnd), true);
setItemChecked(rotate(runEnd[i], -1, rangeStart, rangeEnd), false);
}
} else {
for (int i = 0; i != runCount; i++) {
setItemChecked(runStart[i], false);
setItemChecked(runEnd[i], true);
}
}
}
/**
* Use this when an item has been deleted, to move the check state of all
* following items up one step. If you have a choiceMode which is not none,
* this method must be called when the order of items changes in an
* underlying adapter which does not have stable IDs (see
* {@link ListAdapter#hasStableIds()}). This is because without IDs, the
* ListView has no way of knowing which items have moved where, and cannot
* update the check state accordingly.
*
* See also further comments on {@link #moveCheckState(int, int)}.
*
* @param position
*/
public void removeCheckState(int position) {
SparseBooleanArray cip = getCheckedItemPositions();
if (cip.size() == 0)
return;
int[] runStart = new int[cip.size()];
int[] runEnd = new int[cip.size()];
int rangeStart = position;
int rangeEnd = cip.keyAt(cip.size() - 1) + 1;
int runCount = buildRunList(cip, rangeStart, rangeEnd, runStart, runEnd);
for (int i = 0; i != runCount; i++) {
if (!(runStart[i] == position || (runEnd[i] < runStart[i] && runEnd[i] > position))) {
// Only set a new check mark in front of this run if it does
// not contain the deleted position. If it does, we only need
// to make it one check mark shorter at the end.
setItemChecked(rotate(runStart[i], -1, rangeStart, rangeEnd), true);
}
setItemChecked(rotate(runEnd[i], -1, rangeStart, rangeEnd), false);
}
}
private static int buildRunList(SparseBooleanArray cip, int rangeStart,
int rangeEnd, int[] runStart, int[] runEnd) {
int runCount = 0;
int i = findFirstSetIndex(cip, rangeStart, rangeEnd);
if (i == -1)
return 0;
int position = cip.keyAt(i);
int currentRunStart = position;
int currentRunEnd = currentRunStart + 1;
for (i++; i < cip.size() && (position = cip.keyAt(i)) < rangeEnd; i++) {
if (!cip.valueAt(i)) // not checked => not interesting
continue;
if (position == currentRunEnd) {
currentRunEnd++;
} else {
runStart[runCount] = currentRunStart;
runEnd[runCount] = currentRunEnd;
runCount++;
currentRunStart = position;
currentRunEnd = position + 1;
}
}
if (currentRunEnd == rangeEnd) {
// rangeStart and rangeEnd are equivalent positions so to be
// consistent we translate them to the same integer value. That way
// we can check whether a run covers the entire range by just
// checking if the start equals the end position.
currentRunEnd = rangeStart;
}
runStart[runCount] = currentRunStart;
runEnd[runCount] = currentRunEnd;
runCount++;
if (runCount > 1) {
if (runStart[0] == rangeStart && runEnd[runCount - 1] == rangeStart) {
// The last run ends at the end of the range, and the first run
// starts at the beginning of the range. So they are actually
// part of the same run, except they wrap around the end of the
// range. To avoid adjacent runs, we need to merge them.
runStart[0] = runStart[runCount - 1];
runCount--;
}
}
return runCount;
}
private static int rotate(int value, int offset, int lowerBound, int upperBound) {
int windowSize = upperBound - lowerBound;
value += offset;
if (value < lowerBound) {
value += windowSize;
} else if (value >= upperBound) {
value -= windowSize;
}
return value;
}
private static int findFirstSetIndex(SparseBooleanArray sba, int rangeStart, int rangeEnd) {
int size = sba.size();
int i = insertionIndexForKey(sba, rangeStart);
while (i < size && sba.keyAt(i) < rangeEnd && !sba.valueAt(i))
i++;
if (i == size || sba.keyAt(i) >= rangeEnd)
return -1;
return i;
}
private static int insertionIndexForKey(SparseBooleanArray sba, int key) {
int low = 0;
int high = sba.size();
while (high - low > 0) {
int middle = (low + high) >> 1;
if (sba.keyAt(middle) < key)
low = middle + 1;
else
high = middle;
}
return low;
}
/**
* Interface for controlling
* scroll speed as a function of touch position and time. Use
* {@link DragSortListView#setDragScrollProfile(DragScrollProfile)} to
* set custom profile.
*
* @author heycosmo
*
*/
public interface DragScrollProfile {
/**
* Return a scroll speed in pixels/millisecond. Always return a
* positive number.
*
* @param w Normalized position in scroll region (i.e. w \in [0,1]).
* Small w typically means slow scrolling.
* @param t Time (in milliseconds) since start of scroll (handy if you
* want scroll acceleration).
* @return Scroll speed at position w and time t in pixels/ms.
*/
float getSpeed(float w, long t);
}
private class DragScroller implements Runnable {
private boolean mAbort;
private long mPrevTime;
private long mCurrTime;
private int dy;
private float dt;
private long tStart;
private int scrollDir;
public final static int STOP = -1;
public final static int UP = 0;
public final static int DOWN = 1;
private float mScrollSpeed; // pixels per ms
private boolean mScrolling = false;
private int mLastHeader;
private int mFirstFooter;
public boolean isScrolling() {
return mScrolling;
}
public int getScrollDir() {
return mScrolling ? scrollDir : STOP;
}
public DragScroller() {
}
public void startScrolling(int dir) {
if (!mScrolling) {
// Debug.startMethodTracing("dslv-scroll");
mAbort = false;
mScrolling = true;
tStart = SystemClock.uptimeMillis();
mPrevTime = tStart;
scrollDir = dir;
post(this);
}
}
public void stopScrolling(boolean now) {
if (now) {
DragSortListView.this.removeCallbacks(this);
mScrolling = false;
} else {
mAbort = true;
}
// Debug.stopMethodTracing();
}
@Override
public void run() {
if (mAbort) {
mScrolling = false;
return;
}
// Log.d("mobeta", "scroll");
final int first = getFirstVisiblePosition();
final int last = getLastVisiblePosition();
final int count = getCount();
final int padTop = getPaddingTop();
final int listHeight = getHeight() - padTop - getPaddingBottom();
int minY = Math.min(mY, mFloatViewMid + mFloatViewHeightHalf);
int maxY = Math.max(mY, mFloatViewMid - mFloatViewHeightHalf);
if (scrollDir == UP) {
View v = getChildAt(0);
// Log.d("mobeta", "vtop="+v.getTop()+" padtop="+padTop);
if (v == null) {
mScrolling = false;
return;
} else {
if (first == 0 && v.getTop() == padTop) {
mScrolling = false;
return;
}
}
mScrollSpeed = mScrollProfile.getSpeed((mUpScrollStartYF - maxY)
/ mDragUpScrollHeight, mPrevTime);
} else {
View v = getChildAt(last - first);
if (v == null) {
mScrolling = false;
return;
} else {
if (last == count - 1 && v.getBottom() <= listHeight + padTop) {
mScrolling = false;
return;
}
}
mScrollSpeed = -mScrollProfile.getSpeed((minY - mDownScrollStartYF)
/ mDragDownScrollHeight, mPrevTime);
}
mCurrTime = SystemClock.uptimeMillis();
dt = (float) (mCurrTime - mPrevTime);
// dy is change in View position of a list item; i.e. positive dy
// means user is scrolling up (list item moves down the screen,
// remember
// y=0 is at top of View).
dy = (int) Math.round(mScrollSpeed * dt);
int movePos;
if (dy >= 0) {
dy = Math.min(listHeight, dy);
movePos = first;
} else {
dy = Math.max(-listHeight, dy);
movePos = last;
}
final View moveItem = getChildAt(movePos - first);
int top = moveItem.getTop() + dy;
if (movePos == 0 && top > padTop) {
top = padTop;
}
// always do scroll
mBlockLayoutRequests = true;
setSelectionFromTop(movePos, top - padTop);
DragSortListView.this.layoutChildren();
invalidate();
mBlockLayoutRequests = false;
// scroll means relative float View movement
doDragFloatView(movePos, moveItem, false);
mPrevTime = mCurrTime;
// Log.d("mobeta", " updated prevTime="+mPrevTime);
post(this);
}
}
private class DragSortTracker {
StringBuilder mBuilder = new StringBuilder();
File mFile;
private int mNumInBuffer = 0;
private int mNumFlushes = 0;
private boolean mTracking = false;
public DragSortTracker() {
File root = Environment.getExternalStorageDirectory();
mFile = new File(root, "dslv_state.txt");
if (!mFile.exists()) {
try {
mFile.createNewFile();
Log.d("mobeta", "file created");
} catch (IOException e) {
Log.w("mobeta", "Could not create dslv_state.txt");
Log.d("mobeta", e.getMessage());
}
}
}
public void startTracking() {
mBuilder.append("