/* * 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.mobeta.android.dslv; import java.io.File; import java.io.FileWriter; import java.io.IOException; import android.content.Context; import android.content.res.TypedArray; import android.database.DataSetObserver; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Point; import android.graphics.drawable.Drawable; import android.os.Environment; import android.os.SystemClock; import android.util.AttributeSet; import android.util.Log; 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.zapta.apps.maniana.R; /** * 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; /** * The float View location. First based on touch location and given deltaX and deltaY. Then * restricted by callback to FloatViewManager.onDragFloatView(). Finally restricted by bounds of * DSLV. */ private Point mFloatLoc = new Point(); /** * The middle (in the y-direction) of the floating View. */ private int mFloatViewMid; /** * Flag to make sure float View isn't measured twice */ private boolean mFloatViewOnMeasured = false; /** * Watch the Adapter for data changes. Cancel a drag if coincident with a change. */ private DataSetObserver mObserver; /** * Transparency for the floating View (XML attribute). */ private 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 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 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.5f; /** * 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() { @Override public float getSpeed(float w, 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 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 boolean mTrackDragSort = false; /** * Debugging class. */ private DragSortTracker mDragSortTracker; /** * Needed for adjusting item heights from within layoutChildren */ private boolean mBlockLayoutRequests = false; public DragSortListView(Context context, AttributeSet attrs) { super(context, attrs); if (attrs != null) { TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.DragSortListView, 0, 0); mItemHeightCollapsed = Math.max(1, a.getDimensionPixelSize(R.styleable.DragSortListView_collapsed_height, 1)); mTrackDragSort = a.getBoolean(R.styleable.DragSortListView_track_drag_sort, false); if (mTrackDragSort) { mDragSortTracker = new DragSortTracker(); } // alpha between 0 and 255, 0=transparent, 255=opaque mFloatAlpha = a.getFloat(R.styleable.DragSortListView_float_alpha, mFloatAlpha); mCurrFloatAlpha = mFloatAlpha; mDragEnabled = a.getBoolean(R.styleable.DragSortListView_drag_enabled, mDragEnabled); mSlideRegionFrac = Math.max(0.0f, Math.min(1.0f, 1.0f - a.getFloat(R.styleable.DragSortListView_slide_shuffle_speed, 0.75f))); mAnimate = mSlideRegionFrac > 0.0f; float frac = a.getFloat(R.styleable.DragSortListView_drag_scroll_start, mDragUpScrollStartFrac); setDragScrollStart(frac); mMaxScrollSpeed = a.getFloat(R.styleable.DragSortListView_max_drag_scroll_speed, mMaxScrollSpeed); boolean useDefault = a.getBoolean(R.styleable.DragSortListView_use_default_controller, true); if (useDefault) { boolean removeEnabled = a.getBoolean(R.styleable.DragSortListView_remove_enabled, false); int removeMode = a.getInt(R.styleable.DragSortListView_remove_mode, DragSortController.FLING_RIGHT_REMOVE); boolean sortEnabled = a.getBoolean(R.styleable.DragSortListView_sort_enabled, true); int dragInitMode = a.getInt(R.styleable.DragSortListView_drag_start_mode, DragSortController.ON_DOWN); int dragHandleId = a.getResourceId(R.styleable.DragSortListView_drag_handle_id, 0); int bgColor = a.getColor(R.styleable.DragSortListView_float_background_color, Color.BLACK); DragSortController controller = new DragSortController(this, dragHandleId, dragInitMode, removeMode); controller.setRemoveEnabled(removeEnabled); controller.setSortEnabled(sortEnabled); controller.setBackgroundColor(bgColor); mFloatViewManager = controller; setOnTouchListener(controller); } a.recycle(); } mDragScroller = new DragScroller(); // setOnScrollListener(mDragScroller); mCancelEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0f, 0f, 0, 0f, 0f, 0, 0); // construct the dataset observer mObserver = new DataSetObserver() { private void cancel() { if (mDragState == DRAGGING) { stopDrag(false); } } @Override public void onChanged() { cancel(); } @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(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(float max) { mMaxScrollSpeed = max; } /** * For each DragSortListView Listener interface implemented by <code>adapter</code>, this method * calls the appropriate set*Listener method with <code>adapter</code> 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) { 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); } 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 ListAdapter mAdapter; public AdapterWrapper(ListAdapter adapter) { super(null, null, adapter); mAdapter = adapter; } public ListAdapter getAdapter() { return mAdapter; } @Override public View getView(int position, View convertView, ViewGroup parent) { RelativeLayout v; View child; // Log.d("mobeta", "getView: position="+position+" convertView="+convertView); if (convertView != null) { v = (RelativeLayout) convertView; View oldChild = v.getChildAt(0); child = mAdapter.getView(position, oldChild, v); if (child != oldChild) { // shouldn't get here if user is reusing convertViews properly v.removeViewAt(0); v.addView(child); } } else { AbsListView.LayoutParams params = new AbsListView.LayoutParams( ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); v = new RelativeLayout(getContext()); v.setLayoutParams(params); child = mAdapter.getView(position, null, v); 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(); 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); } } } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); if (mFloatView != null) { // draw the divider over the expanded item if (mFirstExpPos != mSrcPos) { drawDivider(mFirstExpPos, canvas); } if (mSecondExpPos != mFirstExpPos && mSecondExpPos != mSrcPos) { drawDivider(mSecondExpPos, canvas); } // draw the float view over everything final int w = mFloatView.getWidth(); final int h = mFloatView.getHeight(); final int alpha = (int) (255f * mCurrFloatAlpha); 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 class ItemHeights { int item; int child; } private void measureItemAndGetHeights(int position, View item, ItemHeights heights) { ViewGroup.LayoutParams lp = item.getLayoutParams(); 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 { View child = ((ViewGroup) item).getChildAt(0); lp = child.getLayoutParams(); height = lp == null ? 0 : lp.height; if (height > 0) { heights.child = height; } else { // we have to measure child int hspec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); int wspec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, getListPaddingLeft() + getListPaddingRight(), lp.width); // Log.d("mobeta", "measure child"); child.measure(wspec, hspec); heights.child = child.getMeasuredHeight(); } } } else { // do measure on item int hspec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); int wspec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, getListPaddingLeft() + getListPaddingRight(), lp == null ? ViewGroup.LayoutParams.FILL_PARENT : lp.width); // Log.d("mobeta", "measure item"); item.measure(wspec, hspec); heights.item = item.getMeasuredHeight(); // child gets measured in the process 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(int position, View item, ItemHeights heights) { 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 <code>getView</code> to get height information. * * @param position ListView position. * @param heights Object to fill with heights of item at <code>position</code>. */ private void getItemHeights(int position, 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(); 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(int position, 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(int position, 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; } int divHeight = getDividerHeight(); int edge; 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 { 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) { int blankHeight = heights.item - heights.child; otop = top + blankHeight; } } // otop is set if (position <= mSrcPos) { 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); } int startTop = startView.getTop(); ItemHeights itemHeights = new ItemHeights(); getItemHeights(startPos, startView, itemHeights); int edge = getShuffleEdge(startPos, startTop, itemHeights); int lastEdge = edge; 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; int oldFirstExpPos = mFirstExpPos; int oldSecondExpPos = mSecondExpPos; float oldSlideFrac = mSlideFrac; if (mAnimate) { 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); int slideRgnHeight = (int) (0.5f * mSlideRegionFrac * edgeToEdge); float slideRgnHeightF = (float) slideRgnHeight; int slideEdgeTop = edgeTop + slideRgnHeight; int slideEdgeBottom = edgeBottom - slideRgnHeight; // Three regions if (mFloatViewMid < slideEdgeTop) { mFirstExpPos = itemPos - 1; mSecondExpPos = itemPos; mSlideFrac = 0.5f * ((float) (slideEdgeTop - mFloatViewMid)) / slideRgnHeightF; // 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 + ((float) (edgeBottom - mFloatViewMid)) / slideRgnHeightF); // 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; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mTrackDragSort) { mDragSortTracker.appendState(); } } /** * Stop a drag in progress. Pass <code>true</code> 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(boolean remove) { if (mFloatView != null) { mDragState = STOPPED; // stop the drag dropFloatView(remove); return true; } else { // stop failed return false; } } @Override public boolean onTouchEvent(MotionEvent ev) { if (!mDragEnabled) { return super.onTouchEvent(ev); } boolean more = false; 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; } } 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(MotionEvent ev) { 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; } } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (!mDragEnabled) { return super.onInterceptTouchEvent(ev); } saveTouchCoords(ev); mLastCallWasIntercept = true; boolean intercept = false; 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(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 dropFloatView(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(); int firstPos = getFirstVisiblePosition(); 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()); } } mSrcPos = -1; mFirstExpPos = -1; mSecondExpPos = -1; mFloatPos = -1; removeFloatView(); adjustAllItems(); if (mTrackDragSort) { mDragSortTracker.stopTracking(); } } 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, boolean needsMeasure) { // Adjust item height ViewGroup.LayoutParams lp = v.getLayoutParams(); int oldHeight = lp.height; int height = oldHeight; boolean isSliding = mAnimate && mFirstExpPos != mSecondExpPos; int maxNonSrcBlankHeight = mFloatViewHeight - mItemHeightCollapsed; 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 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 int oldVis = v.getVisibility(); int vis = View.VISIBLE; if (position == mSrcPos && mFloatView != null) { vis = View.INVISIBLE; } if (vis != oldVis) { v.setVisibility(vis); } } @Override public void requestLayout() { if (!mBlockLayoutRequests) { super.requestLayout(); } } private int adjustScroll(int movePos, View moveItem, int oldFirstExpPos, int oldSecondExpPos) { int adjust = 0; ItemHeights itemHeightsBefore = new ItemHeights(); getItemHeights(movePos, moveItem, itemHeightsBefore); int moveHeightBefore = itemHeightsBefore.item; int moveBlankBefore = moveHeightBefore - itemHeightsBefore.child; ItemHeights itemHeightsAfter = new ItemHeights(); measureItemAndGetHeights(movePos, moveItem, itemHeightsAfter); int moveHeightAfter = itemHeightsAfter.item; int moveBlankAfter = moveHeightAfter - itemHeightsAfter.child; if (movePos <= oldFirstExpPos) { if (movePos > mFirstExpPos) { adjust += mFloatViewHeight - moveBlankAfter; } } else if (movePos == oldSecondExpPos) { if (movePos <= mFirstExpPos) { adjust += moveBlankBefore - mFloatViewHeight; } else if (movePos == mSecondExpPos) { adjust += moveHeightBefore - moveHeightAfter; } else { adjust += moveBlankBefore; } } else { if (movePos <= mFirstExpPos) { adjust -= mFloatViewHeight; } else if (movePos == mSecondExpPos) { adjust -= moveBlankAfter; } } return adjust; } private void measureFloatView() { if (mFloatView != null) { ViewGroup.LayoutParams lp = mFloatView.getLayoutParams(); if (lp == null) { lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } 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; } } @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) { 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 <code>position</code> 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 * <code>dragFlags |= * ~{@link #DRAG_NEG_X}</code> 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 <code>startDrag</code> 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 <code>position</code> 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 * <code>dragFlags |= * ~{@link #DRAG_NEG_X}</code> 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 <code>startDrag</code> will fail * if we are not currently in a touch event, <code>floatView</code> is null, or there is * a drag in progress. */ public boolean startDrag(int position, View floatView, int dragFlags, int deltaX, int deltaY) { if (!mInTouchEvent || mFloatView != null || floatView == null) { 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; // 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(); 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) { Point touch = new Point(mX, mY); mFloatViewManager.onDragFloatView(mFloatView, mFloatLoc, touch); } 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 removeFloatView() { 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 <code>position</code>. 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: <code>position</code> 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 * <code>location.x</code> and <code>location.y</code>. * * @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 <code>true</code> 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. * * @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; } } /** * 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 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; 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("<DSLVStates>\n"); mNumFlushes = 0; mTracking = true; } public void appendState() { if (!mTracking) { return; } mBuilder.append("<DSLVState>\n"); final int children = getChildCount(); final int first = getFirstVisiblePosition(); ItemHeights itemHeights = new ItemHeights(); mBuilder.append(" <Positions>"); for (int i = 0; i < children; ++i) { mBuilder.append(first + i).append(","); } mBuilder.append("</Positions>\n"); mBuilder.append(" <Tops>"); for (int i = 0; i < children; ++i) { mBuilder.append(getChildAt(i).getTop()).append(","); } mBuilder.append("</Tops>\n"); mBuilder.append(" <Bottoms>"); for (int i = 0; i < children; ++i) { mBuilder.append(getChildAt(i).getBottom()).append(","); } mBuilder.append("</Bottoms>\n"); mBuilder.append(" <FirstExpPos>").append(mFirstExpPos).append("</FirstExpPos>\n"); getItemHeights(mFirstExpPos, itemHeights); mBuilder.append(" <FirstExpBlankHeight>") .append(itemHeights.item - itemHeights.child) .append("</FirstExpBlankHeight>\n"); mBuilder.append(" <SecondExpPos>").append(mSecondExpPos).append("</SecondExpPos>\n"); getItemHeights(mSecondExpPos, itemHeights); mBuilder.append(" <SecondExpBlankHeight>") .append(itemHeights.item - itemHeights.child) .append("</SecondExpBlankHeight>\n"); mBuilder.append(" <SrcPos>").append(mSrcPos).append("</SrcPos>\n"); mBuilder.append(" <SrcHeight>").append(mFloatViewHeight + getDividerHeight()) .append("</SrcHeight>\n"); mBuilder.append(" <ViewHeight>").append(getHeight()).append("</ViewHeight>\n"); mBuilder.append(" <LastY>").append(mLastY).append("</LastY>\n"); mBuilder.append(" <FloatY>").append(mFloatViewMid).append("</FloatY>\n"); mBuilder.append(" <ShuffleEdges>"); for (int i = 0; i < children; ++i) { mBuilder.append(getShuffleEdge(first + i, getChildAt(i).getTop())).append(","); } mBuilder.append("</ShuffleEdges>\n"); mBuilder.append("</DSLVState>\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; } FileWriter writer = new FileWriter(mFile, append); writer.write(mBuilder.toString()); mBuilder.delete(0, mBuilder.length()); writer.flush(); writer.close(); mNumFlushes++; } catch (IOException e) { // do nothing } } public void stopTracking() { if (mTracking) { mBuilder.append("</DSLVStates>\n"); flush(); mTracking = false; } } } }