/* * DragSortListView. A subclass of the Android ListView component that enables * drag and drop re-ordering of list items. Copyright 2012 Carl Bauer 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.cyanogenmod.eleven.dragdrop; import android.content.Context; import android.database.DataSetObserver; import android.graphics.Canvas; import android.graphics.Point; import android.graphics.drawable.Drawable; import android.os.SystemClock; import android.util.AttributeSet; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.BaseAdapter; import android.widget.HeaderViewListAdapter; import android.widget.ListAdapter; import android.widget.ListView; import android.widget.RelativeLayout; import com.cyanogenmod.eleven.R; import java.io.File; import java.io.FileWriter; import java.io.IOException; /** * ListView subclass that mediates drag and drop resorting of items. * * @author heycosmo */ public class DragSortListView extends ListView { /** * The View that floats above the ListView and represents the dragged item. */ private View mFloatView; /** * A proposed float View location based on touch location and given deltaX * and deltaY. */ private final Point mFloatLoc = new Point(); /** * The middle (in the y-direction) of the floating View. */ private int mFloatViewMid; /** * Left edge of floating View. */ private int mFloatViewLeft; /** * Top edge of floating View. */ private int mFloatViewTop; /** * Watch the Adapter for data changes. Cancel a drag if coincident with a * change. */ private final DataSetObserver mObserver; /** * Transparency for the floating View (XML attribute). */ private final float mFloatAlpha = 1.0f; private float mCurrFloatAlpha = 1.0f; /** * While drag-sorting, the current position of the floating View. If * dropped, the dragged item will land in this position. */ private int mFloatPos; /** * The amount to scroll during the next layout pass. Used only for * drag-scrolling, not standard ListView scrolling. */ private int mScrollY = 0; /** * The first expanded ListView position that helps represent the drop slot * tracking the floating View. */ private int mFirstExpPos; /** * The second expanded ListView position that helps represent the drop slot * tracking the floating View. This can equal mFirstExpPos if there is no * slide shuffle occurring; otherwise it is equal to mFirstExpPos + 1. */ private int mSecondExpPos; /** * Flag set if slide shuffling is enabled. */ private boolean mAnimate = false; /** * The user dragged from this position. */ private int mSrcPos; /** * Offset (in x) within the dragged item at which the user picked it up (or * first touched down with the digitalis). */ private int mDragDeltaX; /** * Offset (in y) within the dragged item at which the user picked it up (or * first touched down with the digitalis). */ private int mDragDeltaY; /** * A listener that receives callbacks whenever the floating View hovers over * a new position. */ private DragListener mDragListener; /** * A listener that receives a callback when the floating View is dropped. */ private DropListener mDropListener; /** * A listener that receives a callback when the floating View (or more * precisely the originally dragged item) is removed by one of the provided * gestures. */ private RemoveListener mRemoveListener; /** * Enable/Disable item dragging */ private boolean mDragEnabled = true; /** * Drag state enum. */ private final static int IDLE = 0; private final static int STOPPED = 1; private final static int DRAGGING = 2; private int mDragState = IDLE; /** * Height in pixels to which the originally dragged item is collapsed during * a drag-sort. Currently, this value must be greater than zero. */ private int mItemHeightCollapsed = 1; /** * Height of the floating View. Stored for the purpose of providing the * tracking drop slot. */ private int mFloatViewHeight; /** * Convenience member. See above. */ private int mFloatViewHeightHalf; /** * Save the given width spec for use in measuring children */ private int mWidthMeasureSpec = 0; /** * Sample Views ultimately used for calculating the height of ListView items * that are off-screen. */ private View[] mSampleViewTypes = new View[1]; /** * Drag-scroll encapsulator! */ private final DragScroller mDragScroller; /** * Determines the start of the upward drag-scroll region at the top of the * ListView. Specified by a fraction of the ListView height, thus screen * resolution agnostic. */ private float mDragUpScrollStartFrac = 1.0f / 3.0f; /** * Determines the start of the downward drag-scroll region at the bottom of * the ListView. Specified by a fraction of the ListView height, thus screen * resolution agnostic. */ private float mDragDownScrollStartFrac = 1.0f / 3.0f; /** * The following are calculated from the above fracs. */ private int mUpScrollStartY; private int mDownScrollStartY; private float mDownScrollStartYF; private float mUpScrollStartYF; /** * Calculated from above above and current ListView height. */ private float mDragUpScrollHeight; /** * Calculated from above above and current ListView height. */ private float mDragDownScrollHeight; /** * Maximum drag-scroll speed in pixels per ms. Only used with default linear * drag-scroll profile. */ private float mMaxScrollSpeed = 0.3f; /** * Defines the scroll speed during a drag-scroll. User can provide their * own; this default is a simple linear profile where scroll speed increases * linearly as the floating View nears the top/bottom of the ListView. */ private DragScrollProfile mScrollProfile = new DragScrollProfile() { /** * {@inheritDoc} */ @Override public float getSpeed(final float w, final long t) { return mMaxScrollSpeed * w; } }; /** * Current touch x. */ private int mX; /** * Current touch y. */ private int mY; /** * Last touch y. */ private int mLastY; /** * Drag flag bit. Floating View can move in the positive x direction. */ public final static int DRAG_POS_X = 0x1; /** * Drag flag bit. Floating View can move in the negative x direction. */ public final static int DRAG_NEG_X = 0x2; /** * Drag flag bit. Floating View can move in the positive y direction. This * is subtle. What this actually means is that, if enabled, the floating * View can be dragged below its starting position. Remove in favor of * upper-bounding item position? */ public final static int DRAG_POS_Y = 0x4; /** * Drag flag bit. Floating View can move in the negative y direction. This * is subtle. What this actually means is that the floating View can be * dragged above its starting position. Remove in favor of lower-bounding * item position? */ public final static int DRAG_NEG_Y = 0x8; /** * Flags that determine limits on the motion of the floating View. See flags * above. */ private int mDragFlags = 0; /** * Last call to an on*TouchEvent was a call to onInterceptTouchEvent. */ private boolean mLastCallWasIntercept = false; /** * A touch event is in progress. */ private boolean mInTouchEvent = false; /** * Let the user customize the floating View. */ private FloatViewManager mFloatViewManager = null; /** * Given to ListView to cancel its action when a drag-sort begins. */ private final MotionEvent mCancelEvent; /** * Enum telling where to cancel the ListView action when a drag-sort begins */ private static final int NO_CANCEL = 0; private static final int ON_TOUCH_EVENT = 1; private static final int ON_INTERCEPT_TOUCH_EVENT = 2; /** * Where to cancel the ListView action when a drag-sort begins */ private int mCancelMethod = NO_CANCEL; /** * Determines when a slide shuffle animation starts. That is, defines how * close to the edge of the drop slot the floating View must be to initiate * the slide. */ private float mSlideRegionFrac = 0.25f; /** * Number between 0 and 1 indicating the relative location of a sliding item * (only used if drag-sort animations are turned on). Nearly 1 means the * item is at the top of the slide region (nearly full blank item is * directly below). */ private float mSlideFrac = 0.0f; /** * Wraps the user-provided ListAdapter. This is used to wrap each item View * given by the user inside another View (currenly a RelativeLayout) which * expands and collapses to simulate the item shuffling. */ private AdapterWrapper mAdapterWrapper; /** * Turn on custom debugger. */ private final boolean mTrackDragSort = false; /** * Debugging class. */ private DragSortTracker mDragSortTracker; /** * Needed for adjusting item heights from within layoutChildren */ private boolean mBlockLayoutRequests = false; private final DragSortController mController; /** * @param context The {@link Context} to use * @param attrs The attributes of the XML tag that is inflating the view. */ public DragSortListView(final Context context, final AttributeSet attrs) { super(context, attrs); mItemHeightCollapsed = 1; mCurrFloatAlpha = mFloatAlpha; mSlideRegionFrac = 0.75f; mAnimate = mSlideRegionFrac > 0.0f; setDragScrollStart(mDragUpScrollStartFrac); mController = new DragSortController(this, R.id.edit_track_list_item_handle, DragSortController.ON_DOWN, DragSortController.FLING_RIGHT_REMOVE); mController.setRemoveEnabled(true); mController.setSortEnabled(true); /* Transparent holo light blue */ mController .setBackgroundColor(getResources().getColor(R.color.holo_blue_light_transparent)); mFloatViewManager = mController; setOnTouchListener(mController); mDragScroller = new DragScroller(); setOnScrollListener(mDragScroller); mCancelEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0f, 0f, 0, 0f, 0f, 0, 0); mObserver = new DataSetObserver() { private void cancel() { if (mDragState == DRAGGING) { stopDrag(false); } } /** * {@inheritDoc} */ @Override public void onChanged() { cancel(); } /** * {@inheritDoc} */ @Override public void onInvalidated() { cancel(); } }; } /** * Usually called from a FloatViewManager. The float alpha will be reset to * the xml-defined value every time a drag is stopped. */ public void setFloatAlpha(final float alpha) { mCurrFloatAlpha = alpha; } public float getFloatAlpha() { return mCurrFloatAlpha; } /** * Set maximum drag scroll speed in positions/second. Only applies if using * default ScrollSpeedProfile. * * @param max Maximum scroll speed. */ public void setMaxScrollSpeed(final float max) { mMaxScrollSpeed = max; } /** * {@inheritDoc} */ @Override public void setAdapter(final ListAdapter adapter) { mAdapterWrapper = new AdapterWrapper(adapter); adapter.registerDataSetObserver(mObserver); 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 HeaderViewListAdapter { private final ListAdapter mAdapter; public AdapterWrapper(final ListAdapter adapter) { super(null, null, adapter); mAdapter = adapter; } public ListAdapter getAdapter() { return mAdapter; } /** * {@inheritDoc} */ @Override public View getView(final int position, final View convertView, final ViewGroup parent) { RelativeLayout v; View child; if (convertView != null) { v = (RelativeLayout)convertView; final View oldChild = v.getChildAt(0); try { child = mAdapter.getView(position, oldChild, v); if (child != oldChild) { v.removeViewAt(0); v.addView(child); } } catch (final Exception nullz) { } } else { final AbsListView.LayoutParams params = new AbsListView.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); v = new RelativeLayout(getContext()); v.setLayoutParams(params); try { child = mAdapter.getView(position, null, v); v.addView(child); } catch (final Exception todo) { } } adjustItem(position + getHeaderViewsCount(), v, true); return v; } } private void drawDivider(final int expPosition, final Canvas canvas) { final Drawable divider = getDivider(); final int dividerHeight = getDividerHeight(); 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; } divider.setBounds(l, t, r, b); divider.draw(canvas); } } } /** * {@inheritDoc} */ @Override protected void dispatchDraw(final Canvas canvas) { super.dispatchDraw(canvas); if (mFloatView != null) { if (mFirstExpPos != mSrcPos) { drawDivider(mFirstExpPos, canvas); } if (mSecondExpPos != mFirstExpPos && mSecondExpPos != mSrcPos) { drawDivider(mSecondExpPos, canvas); } final int w = mFloatView.getWidth(); final int h = mFloatView.getHeight(); final int alpha = (int)(255f * mCurrFloatAlpha); canvas.save(); canvas.translate(mFloatViewLeft, mFloatViewTop); canvas.clipRect(0, 0, w, h); canvas.saveLayerAlpha(0, 0, w, h, alpha, Canvas.ALL_SAVE_FLAG); mFloatView.draw(canvas); canvas.restore(); canvas.restore(); } } private class ItemHeights { int item; int child; } private void measureItemAndGetHeights(final int position, final View item, final ItemHeights heights) { ViewGroup.LayoutParams lp = item.getLayoutParams(); final boolean isHeadFoot = position < getHeaderViewsCount() || position >= getCount() - getFooterViewsCount(); int height = lp == null ? 0 : lp.height; if (height > 0) { heights.item = height; // get height of child, measure if we have to if (isHeadFoot) { heights.child = heights.item; } else if (position == mSrcPos) { heights.child = 0; } else { final View child = ((ViewGroup)item).getChildAt(0); lp = child.getLayoutParams(); height = lp == null ? 0 : lp.height; if (height > 0) { heights.child = height; } else { final int hspec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); final int wspec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, getListPaddingLeft() + getListPaddingRight(), lp.width); child.measure(wspec, hspec); heights.child = child.getMeasuredHeight(); } } } else { final int hspec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); final int wspec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, getListPaddingLeft() + getListPaddingRight(), lp == null ? ViewGroup.LayoutParams.MATCH_PARENT : lp.width); item.measure(wspec, hspec); heights.item = item.getMeasuredHeight(); if (isHeadFoot) { heights.child = heights.item; } else if (position == mSrcPos) { heights.child = 0; } else { heights.child = ((ViewGroup)item).getChildAt(0).getMeasuredHeight(); } } } /** * Get the height of the given wrapped item and its child. * * @param position Position from which item was obtained. * @param item List item (usually obtained from * {@link ListView#getChildAt()}). * @param heights Object to fill with heights of item. */ private void getItemHeights(final int position, final View item, final ItemHeights heights) { final boolean isHeadFoot = position < getHeaderViewsCount() || position >= getCount() - getFooterViewsCount(); heights.item = item.getHeight(); if (isHeadFoot) { heights.child = heights.item; } else if (position == mSrcPos) { heights.child = 0; } else { heights.child = ((ViewGroup)item).getChildAt(0).getHeight(); } } /** * This function works for arbitrary positions (could be off-screen). If * requested position is off-screen, this function calls * getView to get height information. * * @param position ListView position. * @param heights Object to fill with heights of item at * position. */ private void getItemHeights(final int position, final ItemHeights heights) { final int first = getFirstVisiblePosition(); final int last = getLastVisiblePosition(); if (position >= first && position <= last) { getItemHeights(position, getChildAt(position - first), heights); } else { // Log.d("mobeta", "getView for height"); final ListAdapter adapter = getAdapter(); final 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]; } View v; 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); } measureItemAndGetHeights(position, v, heights); } } private int getShuffleEdge(final int position, final int top) { return getShuffleEdge(position, top, null); } /** * Get the shuffle edge for item at position when top of item is at y-coord * top * * @param position * @param top * @param height Height of item at position. If -1, this function calculates * this height. * @return Shuffle line between position-1 and position (for the given view * of the list; that is, for when top of item at position has * y-coord of given `top`). If floating View (treated as horizontal * line) is dropped immediately above this line, it lands in * position-1. If dropped immediately below this line, it lands in * position. */ private int getShuffleEdge(final int position, final int top, ItemHeights heights) { final int numHeaders = getHeaderViewsCount(); final int numFooters = getFooterViewsCount(); // shuffle edges are defined between items that can be // dragged; there are N-1 of them if there are N draggable // items. if (position <= numHeaders || position >= getCount() - numFooters) { return top; } final int divHeight = getDividerHeight(); int edge; final int maxBlankHeight = mFloatViewHeight - mItemHeightCollapsed; if (heights == null) { heights = new ItemHeights(); getItemHeights(position, heights); } // first calculate top of item given that floating View is // centered over src position int otop = top; if (mSecondExpPos <= mSrcPos) { // items are expanded on and/or above the source position if (position == mSecondExpPos && mFirstExpPos != mSecondExpPos) { if (position == mSrcPos) { otop = top + heights.item - mFloatViewHeight; } else { final int blankHeight = heights.item - heights.child; otop = top + blankHeight - maxBlankHeight; } } else if (position > mSecondExpPos && position <= mSrcPos) { otop = top - maxBlankHeight; } } else { // items are expanded on and/or below the source position if (position > mSrcPos && position <= mFirstExpPos) { otop = top + maxBlankHeight; } else if (position == mSecondExpPos && mFirstExpPos != mSecondExpPos) { final int blankHeight = heights.item - heights.child; otop = top + blankHeight; } } // otop is set if (position <= mSrcPos) { final ItemHeights tmpHeights = new ItemHeights(); getItemHeights(position - 1, tmpHeights); edge = otop + (mFloatViewHeight - divHeight - tmpHeights.child) / 2; } else { edge = otop + (heights.child - divHeight - mFloatViewHeight) / 2; } return edge; } private boolean updatePositions() { final int first = getFirstVisiblePosition(); int startPos = mFirstExpPos; View startView = getChildAt(startPos - first); if (startView == null) { startPos = first + getChildCount() / 2; startView = getChildAt(startPos - first); } final int startTop = startView.getTop() + mScrollY; final ItemHeights itemHeights = new ItemHeights(); getItemHeights(startPos, startView, itemHeights); int edge = getShuffleEdge(startPos, startTop, itemHeights); int lastEdge = edge; final int divHeight = getDividerHeight(); // Log.d("mobeta", "float mid="+mFloatViewMid); int itemPos = startPos; int itemTop = startTop; if (mFloatViewMid < edge) { // scanning up for float position // Log.d("mobeta", " edge="+edge); while (itemPos >= 0) { itemPos--; getItemHeights(itemPos, itemHeights); // if (itemPos <= 0) if (itemPos == 0) { edge = itemTop - divHeight - itemHeights.item; // itemPos = 0; break; } itemTop -= itemHeights.item + divHeight; edge = getShuffleEdge(itemPos, itemTop, itemHeights); // Log.d("mobeta", " edge="+edge); if (mFloatViewMid >= edge) { break; } lastEdge = edge; } } else { // scanning down for float position // Log.d("mobeta", " edge="+edge); final int count = getCount(); while (itemPos < count) { if (itemPos == count - 1) { edge = itemTop + divHeight + itemHeights.item; break; } itemTop += divHeight + itemHeights.item; getItemHeights(itemPos + 1, itemHeights); edge = getShuffleEdge(itemPos + 1, itemTop, itemHeights); // Log.d("mobeta", " edge="+edge); // test for hit if (mFloatViewMid < edge) { break; } lastEdge = edge; itemPos++; } } final int numHeaders = getHeaderViewsCount(); final int numFooters = getFooterViewsCount(); boolean updated = false; final int oldFirstExpPos = mFirstExpPos; final int oldSecondExpPos = mSecondExpPos; final float oldSlideFrac = mSlideFrac; if (mAnimate) { final int edgeToEdge = Math.abs(edge - lastEdge); int edgeTop, edgeBottom; if (mFloatViewMid < edge) { edgeBottom = edge; edgeTop = lastEdge; } else { edgeTop = edge; edgeBottom = lastEdge; } // Log.d("mobeta", "edgeTop="+edgeTop+" edgeBot="+edgeBottom); final int slideRgnHeight = (int)(0.5f * mSlideRegionFrac * edgeToEdge); final int slideEdgeTop = edgeTop + slideRgnHeight; final int slideEdgeBottom = edgeBottom - slideRgnHeight; // Three regions if (mFloatViewMid < slideEdgeTop) { mFirstExpPos = itemPos - 1; mSecondExpPos = itemPos; mSlideFrac = 0.5f * (slideEdgeTop - mFloatViewMid) / (float) slideRgnHeight; // Log.d("mobeta", // "firstExp="+mFirstExpPos+" secExp="+mSecondExpPos+" slideFrac="+mSlideFrac); } else if (mFloatViewMid < slideEdgeBottom) { mFirstExpPos = itemPos; mSecondExpPos = itemPos; } else { mFirstExpPos = itemPos; mSecondExpPos = itemPos + 1; mSlideFrac = 0.5f * (1.0f + (edgeBottom - mFloatViewMid) / (float) slideRgnHeight); // Log.d("mobeta", // "firstExp="+mFirstExpPos+" secExp="+mSecondExpPos+" slideFrac="+mSlideFrac); } } else { mFirstExpPos = itemPos; mSecondExpPos = itemPos; } // correct for headers and footers if (mFirstExpPos < numHeaders) { itemPos = numHeaders; mFirstExpPos = itemPos; mSecondExpPos = itemPos; } else if (mSecondExpPos >= getCount() - numFooters) { itemPos = getCount() - numFooters - 1; mFirstExpPos = itemPos; mSecondExpPos = itemPos; } if (mFirstExpPos != oldFirstExpPos || mSecondExpPos != oldSecondExpPos || mSlideFrac != oldSlideFrac) { updated = true; } if (itemPos != mFloatPos) { if (mDragListener != null) { mDragListener.drag(mFloatPos - numHeaders, itemPos - numHeaders); } mFloatPos = itemPos; updated = true; } return updated; } /** * {@inheritDoc} */ @Override protected void onDraw(final Canvas canvas) { super.onDraw(canvas); if (mTrackDragSort) { mDragSortTracker.appendState(); } } /** * 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 * DropListener, if one exists. * @return True if the stop was successful. */ public boolean stopDrag(final boolean remove) { if (mFloatView != null) { mDragState = STOPPED; // stop the drag dropFloatView(remove); return true; } else { // stop failed return false; } } /** * {@inheritDoc} */ @Override public boolean onTouchEvent(final MotionEvent ev) { if (!mDragEnabled) { return super.onTouchEvent(ev); } boolean more = false; final boolean lastCallWasIntercept = mLastCallWasIntercept; mLastCallWasIntercept = false; if (!lastCallWasIntercept) { saveTouchCoords(ev); } if (mFloatView != null) { 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 (super.onTouchEvent(ev)) { more = true; } } final 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; mDragState = IDLE; mCurrFloatAlpha = mFloatAlpha; } private void saveTouchCoords(final MotionEvent ev) { final int action = ev.getAction() & MotionEvent.ACTION_MASK; if (action != MotionEvent.ACTION_DOWN) { mLastY = mY; } mX = (int)ev.getX(); mY = (int)ev.getY(); if (action == MotionEvent.ACTION_DOWN) { mLastY = mY; } } /** * {@inheritDoc} */ @Override public boolean onInterceptTouchEvent(final MotionEvent ev) { if (!mDragEnabled) { return super.onInterceptTouchEvent(ev); } saveTouchCoords(ev); mLastCallWasIntercept = true; boolean intercept = false; final int action = ev.getAction() & MotionEvent.ACTION_MASK; if (action == MotionEvent.ACTION_DOWN) { mInTouchEvent = true; } // 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)) { 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; } } } // check for startDragging 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(final 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(final float upperFrac, final 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(final int x, final int y) { // Log.d("mobeta", "move"); dragView(x, y); // if (mTrackDragSort) { // mDragSortTracker.appendState(); // } requestLayout(); final int minY = Math.min(y, mFloatViewMid + mFloatViewHeightHalf); final int maxY = Math.max(y, mFloatViewMid - mFloatViewHeightHalf); // get the current scroll direction final 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(); mUpScrollStartYF = padTop + mDragUpScrollStartFrac * (float) listHeight; mDownScrollStartYF = padTop + (1.0f - mDragDownScrollStartFrac) * (float) listHeight; mUpScrollStartY = (int)mUpScrollStartYF; mDownScrollStartY = (int)mDownScrollStartYF; mDragUpScrollHeight = mUpScrollStartYF - padTop; mDragDownScrollHeight = padTop + listHeight - mDownScrollStartYF; } /** * {@inheritDoc} */ @Override protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) { super.onSizeChanged(w, h, oldw, oldh); updateScrollStarts(); } private void dropFloatView(final boolean removeSrcItem) { mDragScroller.stopScrolling(true); if (removeSrcItem) { if (mRemoveListener != null) { mRemoveListener.remove(mSrcPos - getHeaderViewsCount()); } } else { if (mDropListener != null && mFloatPos >= 0 && mFloatPos < getCount()) { final int numHeaders = getHeaderViewsCount(); mDropListener.drop(mSrcPos - numHeaders, mFloatPos - numHeaders); } // adjustAllItems(); final int firstPos = getFirstVisiblePosition(); if (mSrcPos < firstPos) { // collapsed src item is off screen; // adjust the scroll after item heights have been fixed final 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()); } } mSrcPos = -1; mFirstExpPos = -1; mSecondExpPos = -1; mFloatPos = -1; removeFloatView(); if (mTrackDragSort) { mDragSortTracker.stopTracking(); } } private void adjustAllItems() { final int first = getFirstVisiblePosition(); final int last = getLastVisiblePosition(); final int begin = Math.max(0, getHeaderViewsCount() - first); final int end = Math.min(last - first, getCount() - 1 - getFooterViewsCount() - first); for (int i = begin; i <= end; ++i) { final View v = getChildAt(i); if (v != null) { adjustItem(first + i, v, false); } } } private void adjustItem(final int position, final View v, final boolean needsMeasure) { final ViewGroup.LayoutParams lp = v.getLayoutParams(); final int oldHeight = lp.height; int height = oldHeight; getDividerHeight(); final boolean isSliding = mAnimate && mFirstExpPos != mSecondExpPos; final int maxNonSrcBlankHeight = mFloatViewHeight - mItemHeightCollapsed; final int slideHeight = (int)(mSlideFrac * maxNonSrcBlankHeight); 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 || position == mSecondExpPos) { // position is not src final ItemHeights itemHeights = new ItemHeights(); if (needsMeasure) { measureItemAndGetHeights(position, v, itemHeights); } else { getItemHeights(position, v, itemHeights); } if (position == mFirstExpPos) { if (isSliding) { height = itemHeights.child + slideHeight; } else { height = itemHeights.child + maxNonSrcBlankHeight; } } else { // position=mSecondExpPos // we know an item is sliding (b/c 2ndPos != 1stPos) height = itemHeights.child + maxNonSrcBlankHeight - slideHeight; } } else { height = ViewGroup.LayoutParams.WRAP_CONTENT; } if (height != oldHeight) { lp.height = height; v.setLayoutParams(lp); } // Adjust item gravity if (position == mFirstExpPos || position == mSecondExpPos) { if (position < mSrcPos) { ((RelativeLayout)v).setGravity(Gravity.BOTTOM); } else if (position > mSrcPos) { ((RelativeLayout)v).setGravity(Gravity.TOP); } } // Finally adjust item visibility final int oldVis = v.getVisibility(); int vis = View.VISIBLE; if (position == mSrcPos && mFloatView != null) { vis = View.INVISIBLE; } if (vis != oldVis) { v.setVisibility(vis); } } /** * {@inheritDoc} */ @Override public void requestLayout() { if (!mBlockLayoutRequests) { super.requestLayout(); } } private void doDragScroll(final int oldFirstExpPos, final int oldSecondExpPos) { if (mScrollY == 0) { return; } final int padTop = getPaddingTop(); final int listHeight = getHeight() - padTop - getPaddingBottom(); final int first = getFirstVisiblePosition(); final int last = getLastVisiblePosition(); int movePos; if (mScrollY >= 0) { mScrollY = Math.min(listHeight, mScrollY); movePos = first; } else { mScrollY = Math.max(-listHeight, mScrollY); movePos = last; } final View moveItem = getChildAt(movePos - first); int top = moveItem.getTop() + mScrollY; if (movePos == 0 && top > padTop) { top = padTop; } final ItemHeights itemHeightsBefore = new ItemHeights(); getItemHeights(movePos, moveItem, itemHeightsBefore); final int moveHeightBefore = itemHeightsBefore.item; final int moveBlankBefore = moveHeightBefore - itemHeightsBefore.child; final ItemHeights itemHeightsAfter = new ItemHeights(); measureItemAndGetHeights(movePos, moveItem, itemHeightsAfter); final int moveHeightAfter = itemHeightsAfter.item; final int moveBlankAfter = moveHeightAfter - itemHeightsAfter.child; if (movePos <= oldFirstExpPos) { if (movePos > mFirstExpPos) { top += mFloatViewHeight - moveBlankAfter; } } else if (movePos == oldSecondExpPos) { if (movePos <= mFirstExpPos) { top += moveBlankBefore - mFloatViewHeight; } else if (movePos == mSecondExpPos) { top += moveHeightBefore - moveHeightAfter; } else { top += moveBlankBefore; } } else { if (movePos <= mFirstExpPos) { top -= mFloatViewHeight; } else if (movePos == mSecondExpPos) { top -= moveBlankAfter; } } setSelectionFromTop(movePos, top - padTop); mScrollY = 0; } private void measureFloatView() { if (mFloatView != null) { ViewGroup.LayoutParams lp = mFloatView.getLayoutParams(); if (lp == null) { lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } final 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); } mFloatView.measure(wspec, hspec); mFloatViewHeight = mFloatView.getMeasuredHeight(); mFloatViewHeightHalf = mFloatViewHeight / 2; } } /** * {@inheritDoc} */ @Override protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mFloatView != null) { if (mFloatView.isLayoutRequested()) { measureFloatView(); } } mWidthMeasureSpec = widthMeasureSpec; mDragScroller.setListHeight(getHeight()); } /** * {@inheritDoc} */ @Override protected void layoutChildren() { if (mFloatView != null) { mFloatView.layout(0, 0, mFloatView.getMeasuredWidth(), mFloatView.getMeasuredHeight()); // Log.d("mobeta", "layout children"); final int oldFirstExpPos = mFirstExpPos; final int oldSecondExpPos = mSecondExpPos; mBlockLayoutRequests = true; if (getChildCount() > 0 && updatePositions()) { adjustAllItems(); } if (mScrollY != 0) { doDragScroll(oldFirstExpPos, oldSecondExpPos); } mBlockLayoutRequests = false; } super.layoutChildren(); } protected boolean onDragTouchEvent(final MotionEvent ev) { switch (ev.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: stopDrag(false); doActionUpOrCancel(); break; case MotionEvent.ACTION_MOVE: continueDrag((int)ev.getX(), (int)ev.getY()); break; } return 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(final int position, final int dragFlags, final int deltaX, final int deltaY) { if (!mInTouchEvent || mFloatViewManager == null) { return false; } final 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(final int position, final View floatView, final int dragFlags, final int deltaX, final int deltaY) { if (!mInTouchEvent || mFloatView != null || floatView == null) { return false; } if (getParent() != null) { getParent().requestDisallowInterceptTouchEvent(true); } final 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; updateFloatView(mX - mDragDeltaX, 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(); return true; } /** * Sets float View location based on suggested values and constraints set in * mDragFlags. */ private void updateFloatView(final int floatX, final int floatY) { // restrict x motion final int padLeft = getPaddingLeft(); if ((mDragFlags & DRAG_POS_X) == 0 && floatX > padLeft) { mFloatViewLeft = padLeft; } else if ((mDragFlags & DRAG_NEG_X) == 0 && floatX < padLeft) { mFloatViewLeft = padLeft; } else { mFloatViewLeft = floatX; } // 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) { mFloatViewTop = topLimit; } else if (floatY + mFloatViewHeight > bottomLimit) { mFloatViewTop = bottomLimit - mFloatViewHeight; } else { mFloatViewTop = floatY; } // get y-midpoint of floating view (constrained to ListView bounds) mFloatViewMid = mFloatViewTop + mFloatViewHeightHalf; } private void dragView(final int x, final int y) { // Log.d("mobeta", "float view pure x=" + x + " y=" + y); // proposed position mFloatLoc.x = x - mDragDeltaX; mFloatLoc.y = y - mDragDeltaY; final Point touch = new Point(x, y); // let manager adjust proposed position first if (mFloatViewManager != null) { mFloatViewManager.onDragFloatView(mFloatView, mFloatLoc, touch); } // then we override if manager gives an unsatisfactory // position (e.g. over a header/footer view). Also, // dragFlags override manager adjustments. updateFloatView(mFloatLoc.x, mFloatLoc.y); } private void removeFloatView() { if (mFloatView != null) { mFloatView.setVisibility(GONE); if (mFloatViewManager != null) { mFloatViewManager.onDestroyFloatView(mFloatView); } mFloatView = null; } } /** * 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). */ 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(final FloatViewManager manager) { mFloatViewManager = manager; } public void setDragListener(final 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(final 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. * * @param l */ public void setDropListener(final 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(final RemoveListener l) { if (mController != null && l == null) { mController.setRemoveEnabled(false); } 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(final 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(final DragScrollProfile ssp) { if (ssp != null) { mScrollProfile = ssp; } } /** * 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, AbsListView.OnScrollListener { private boolean mAbort; private long mPrevTime; 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 mMaxScrollSpeed; public boolean isScrolling() { return mScrolling; } public int getScrollDir() { return mScrolling ? scrollDir : STOP; } public DragScroller() { } public void startScrolling(final int dir) { if (!mScrolling) { // Debug.startMethodTracing("dslv-scroll"); mAbort = false; mScrolling = true; tStart = SystemClock.uptimeMillis(); mPrevTime = tStart; scrollDir = dir; post(this); } } public void stopScrolling(final boolean now) { if (now) { removeCallbacks(this); mScrolling = false; } else { mAbort = true; } } public void setListHeight(final int height) { // cap the max scroll speed per frame to be 1/5 of the list height mMaxScrollSpeed = height / 5; } /** * {@inheritDoc} */ @Override public void run() { if (mAbort) { mScrolling = false; return; } final int first = getFirstVisiblePosition(); final int last = getLastVisiblePosition(); final int count = getCount(); final int padTop = getPaddingTop(); final int listHeight = getHeight() - padTop - getPaddingBottom(); final int minY = Math.min(mY, mFloatViewMid + mFloatViewHeightHalf); final int maxY = Math.max(mY, mFloatViewMid - mFloatViewHeightHalf); if (scrollDir == UP) { final View v = getChildAt(0); if (v == null) { mScrolling = false; return; } else { if (first == 0 && v.getTop() == padTop) { mScrolling = false; return; } } mScrollSpeed = mScrollProfile.getSpeed((mUpScrollStartYF - maxY) / mDragUpScrollHeight, mPrevTime); } else { final 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); } dt = SystemClock.uptimeMillis() - 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 = Math.round(mScrollSpeed * dt); mScrollY += dy; // cap the scroll speed mScrollY = Math.max(Math.min(mScrollY, mMaxScrollSpeed), -mMaxScrollSpeed); requestLayout(); mPrevTime += dt; post(this); } /** * {@inheritDoc} */ @Override public void onScroll(final AbsListView view, final int firstVisibleItem, final int visibleItemCount, final int totalItemCount) { if (mScrolling && visibleItemCount != 0) { dragView(mX, mY); } } /** * {@inheritDoc} */ @Override public void onScrollStateChanged(final AbsListView view, final int scrollState) { } } private class DragSortTracker { StringBuilder mBuilder = new StringBuilder(); File mFile; private int mNumInBuffer = 0; private int mNumFlushes = 0; private boolean mTracking = false; public void startTracking() { mBuilder.append("\n"); mNumFlushes = 0; mTracking = true; } public void appendState() { if (!mTracking) { return; } mBuilder.append("\n"); final int children = getChildCount(); final int first = getFirstVisiblePosition(); final ItemHeights itemHeights = new ItemHeights(); mBuilder.append(" "); for (int i = 0; i < children; ++i) { mBuilder.append(first + i).append(","); } mBuilder.append("\n"); mBuilder.append(" "); for (int i = 0; i < children; ++i) { mBuilder.append(getChildAt(i).getTop()).append(","); } mBuilder.append("\n"); mBuilder.append(" "); for (int i = 0; i < children; ++i) { mBuilder.append(getChildAt(i).getBottom()).append(","); } mBuilder.append("\n"); mBuilder.append(" ").append(mFirstExpPos).append("\n"); getItemHeights(mFirstExpPos, itemHeights); mBuilder.append(" ") .append(itemHeights.item - itemHeights.child) .append("\n"); mBuilder.append(" ").append(mSecondExpPos).append("\n"); getItemHeights(mSecondExpPos, itemHeights); mBuilder.append(" ") .append(itemHeights.item - itemHeights.child) .append("\n"); mBuilder.append(" ").append(mSrcPos).append("\n"); mBuilder.append(" ").append(mFloatViewHeight + getDividerHeight()) .append("\n"); mBuilder.append(" ").append(getHeight()).append("\n"); mBuilder.append(" ").append(mLastY).append("\n"); mBuilder.append(" ").append(mFloatViewMid).append("\n"); mBuilder.append(" "); for (int i = 0; i < children; ++i) { mBuilder.append(getShuffleEdge(first + i, getChildAt(i).getTop())).append(","); } mBuilder.append("\n"); mBuilder.append("\n"); mNumInBuffer++; if (mNumInBuffer > 1000) { flush(); mNumInBuffer = 0; } } public void flush() { if (!mTracking) { return; } // save to file on sdcard try { boolean append = true; if (mNumFlushes == 0) { append = false; } final FileWriter writer = new FileWriter(mFile, append); writer.write(mBuilder.toString()); mBuilder.delete(0, mBuilder.length()); writer.flush(); writer.close(); mNumFlushes++; } catch (final IOException e) { // do nothing } } public void stopTracking() { if (mTracking) { mBuilder.append("\n"); flush(); mTracking = false; } } } }