/*
* 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 android.content.Context;
import android.content.res.TypedArray;
import android.database.DataSetObserver;
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.graphics.Canvas;
import android.graphics.Point;
import android.os.Environment;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.util.Log;
import android.view.*;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.widget.*;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
/**
* 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 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 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 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;
/**
* The difference (in x) between screen coordinates and coordinates
* in this view.
*/
private int mOffsetX;
/**
* The difference (in y) between screen coordinates and coordinates
* in this view.
*/
private int mOffsetY;
/**
* 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 x.
*/
private int mLastX;
/**
* Last touch y.
*/
private int mLastY;
/**
* The touch y-coord at which drag started
*/
private int mDragStartY;
/**
* 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,
false);
int dragInitMode = a.getInt(
R.styleable.DragSortListView_drag_start_mode,
DragSortController.ON_DOWN);
int dragHandleId = a.getResourceId(
R.styleable.DragSortListView_drag_handle_id,
0);
DragSortController controller = new DragSortController(
this, dragHandleId, dragInitMode, removeMode);
controller.setRemoveEnabled(removeEnabled);
controller.setSortEnabled(sortEnabled);
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;
}
@Override
public void setAdapter(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 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(mFloatViewLeft, mFloatViewTop);
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 void printPosData() {
Log.d("mobeta", "mSrcPos="+mSrcPos+" mFirstExpPos="+mFirstExpPos+" mSecondExpPos="+mSecondExpPos);
}
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() + mScrollY;
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) {
mLastX = mX;
mLastY = mY;
}
mX = (int) ev.getX();
mY = (int) ev.getY();
if (action == MotionEvent.ACTION_DOWN) {
mLastX = mX;
mLastY = mY;
}
mOffsetX = (int) ev.getRawX() - mX;
mOffsetY = (int) ev.getRawY() - mY;
}
@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) {
//Log.d("mobeta", "move");
dragView(x, y);
//if (mTrackDragSort) {
// mDragSortTracker.appendState();
//}
requestLayout();
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 = getChildAt(position - getFirstVisiblePosition());
if (v != null) {
adjustItem(position, 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;
int divHeight = getDividerHeight();
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 void doDragScroll(int oldFirstExpPos, 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;
}
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) {
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.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);
if (mFloatView != null) {
if (mFloatView.isLayoutRequested()) {
measureFloatView();
}
}
mWidthMeasureSpec = widthMeasureSpec;
}
@Override
protected void layoutChildren() {
if (mFloatView != null) {
mFloatView.layout(0, 0, mFloatView.getMeasuredWidth(), mFloatView.getMeasuredHeight());
//Log.d("mobeta", "layout children");
int oldFirstExpPos = mFirstExpPos;
int oldSecondExpPos = mSecondExpPos;
mBlockLayoutRequests = true;
if (updatePositions()) {
adjustAllItems();
}
if (mScrollY != 0) {
doDragScroll(oldFirstExpPos, oldSecondExpPos);
}
mBlockLayoutRequests = false;
}
super.layoutChildren();
}
protected boolean onDragTouchEvent(MotionEvent ev) {
// we are in a drag
int action = ev.getAction() & MotionEvent.ACTION_MASK;
switch (ev.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
stopDrag(false);
doActionUpOrCancel();
break;
case MotionEvent.ACTION_MOVE:
continueDrag((int) ev.getX(), (int) ev.getY());
break;
}
return true;
}
private boolean mFloatViewInvalidated = false;
private void invalidateFloatView() {
mFloatViewInvalidated = true;
}
/**
* Start a drag of item at <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;
mDragStartY = mY;
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(int floatX, int floatY) {
// restrict x motion
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(int x, int y) {
//Log.d("mobeta", "float view pure x=" + x + " y=" + y);
// proposed position
mFloatLoc.x = x - mDragDeltaX;
mFloatLoc.y = y - mDragDeltaY;
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 <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).
*/
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 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 mLastHeader;
private int mFirstFooter;
public boolean isScrolling() {
return mScrolling;
}
public int getScrollDir() {
return mScrolling ? scrollDir : STOP;
}
public DragScroller() {}
public void startScrolling(int dir) {
if (!mScrolling) {
//Debug.startMethodTracing("dslv-scroll");
mAbort = false;
mScrolling = true;
tStart = SystemClock.uptimeMillis();
mPrevTime = tStart;
scrollDir = dir;
post(this);
}
}
public void stopScrolling(boolean now) {
if (now) {
DragSortListView.this.removeCallbacks(this);
mScrolling = false;
} else {
mAbort = true;
}
//Debug.stopMethodTracing();
}
@Override
public void run() {
if (mAbort) {
mScrolling = false;
return;
}
//Log.d("mobeta", "scroll");
final int first = getFirstVisiblePosition();
final int last = getLastVisiblePosition();
final int count = getCount();
final int padTop = getPaddingTop();
final int listHeight = getHeight() - padTop - getPaddingBottom();
int minY = Math.min(mY, mFloatViewMid + mFloatViewHeightHalf);
int maxY = Math.max(mY, mFloatViewMid - mFloatViewHeightHalf);
if (scrollDir == UP) {
View v = getChildAt(0);
//Log.d("mobeta", "vtop="+v.getTop()+" padtop="+padTop);
if (v == null) {
mScrolling = false;
return;
} else {
if (first == 0 && v.getTop() == padTop) {
mScrolling = false;
return;
}
}
mScrollSpeed = mScrollProfile.getSpeed((mUpScrollStartYF - maxY) / mDragUpScrollHeight, mPrevTime);
} else {
View v = getChildAt(last - first);
if (v == null) {
mScrolling = false;
return;
} else {
if (last == count - 1 && v.getBottom() <= listHeight + padTop) {
mScrolling = false;
return;
}
}
mScrollSpeed = -mScrollProfile.getSpeed((minY - mDownScrollStartYF) / mDragDownScrollHeight, mPrevTime);
}
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 = (int) Math.round(mScrollSpeed * dt);
mScrollY += dy;
requestLayout();
mPrevTime += dt;
post(this);
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (mScrolling && visibleItemCount != 0) {
dragView(mX, mY);
}
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {}
}
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;
}
}
}
}