true
to enable list
+ * item sorting.
+ */
+ public void setSortEnabled(boolean enabled) {
+ mSortEnabled = enabled;
+ }
+
+ public boolean isSortEnabled() {
+ return mSortEnabled;
+ }
+
+ /**
+ * One of {@link CLICK_REMOVE}, {@link FLING_RIGHT_REMOVE},
+ * {@link FLING_LEFT_REMOVE},
+ * {@link SLIDE_RIGHT_REMOVE}, or {@link SLIDE_LEFT_REMOVE}.
+ */
+ public void setRemoveMode(int mode) {
+ mRemoveMode = mode;
+ }
+
+ public int getRemoveMode() {
+ return mRemoveMode;
+ }
+
+ /**
+ * Enable/Disable item removal without affecting remove mode.
+ */
+ public void setRemoveEnabled(boolean enabled) {
+ mRemoveEnabled = enabled;
+ }
+
+ public boolean isRemoveEnabled() {
+ return mRemoveEnabled;
+ }
+
+ /**
+ * Set the resource id for the View that represents the drag
+ * handle in a list item.
+ *
+ * @param id An android resource id.
+ */
+ public void setDragHandleId(int id) {
+ mDragHandleId = id;
+ }
+
+ /**
+ * Set the resource id for the View that represents the fling
+ * handle in a list item.
+ *
+ * @param id An android resource id.
+ */
+ public void setFlingHandleId(int id) {
+ mFlingHandleId = id;
+ }
+
+ /**
+ * Set the resource id for the View that represents click
+ * removal button.
+ *
+ * @param id An android resource id.
+ */
+ public void setClickRemoveId(int id) {
+ mClickRemoveId = id;
+ }
+
+ /**
+ * Sets flags to restrict certain motions of the floating View
+ * based on DragSortController settings (such as remove mode).
+ * Starts the drag on the DragSortListView.
+ *
+ * @param position The list item position (includes headers).
+ * @param deltaX Touch x-coord minus left edge of floating View.
+ * @param deltaY Touch y-coord minus top edge of floating View.
+ *
+ * @return True if drag started, false otherwise.
+ */
+ public boolean startDrag(int position, int deltaX, int deltaY) {
+
+ int dragFlags = 0;
+ if (mSortEnabled && !mIsRemoving) {
+ dragFlags |= DragSortListView.DRAG_POS_Y | DragSortListView.DRAG_NEG_Y;
+ }
+ if (mRemoveEnabled && mIsRemoving) {
+ dragFlags |= DragSortListView.DRAG_POS_X;
+ dragFlags |= DragSortListView.DRAG_NEG_X;
+ }
+
+ mDragging = mDslv.startDrag(position - mDslv.getHeaderViewsCount(), dragFlags, deltaX,
+ deltaY);
+ return mDragging;
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent ev) {
+ if (!mDslv.isDragEnabled() || mDslv.listViewIntercepted()) {
+ return false;
+ }
+
+ mDetector.onTouchEvent(ev);
+ if (mRemoveEnabled && mDragging && mRemoveMode == FLING_REMOVE) {
+ mFlingRemoveDetector.onTouchEvent(ev);
+ }
+
+ int action = ev.getAction() & MotionEvent.ACTION_MASK;
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mCurrX = (int) ev.getX();
+ mCurrY = (int) ev.getY();
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mRemoveEnabled && mIsRemoving) {
+ int x = mPositionX >= 0 ? mPositionX : -mPositionX;
+ int removePoint = mDslv.getWidth() / 2;
+ if (x > removePoint) {
+ mDslv.stopDragWithVelocity(true, 0);
+ }
+ }
+ case MotionEvent.ACTION_CANCEL:
+ mIsRemoving = false;
+ mDragging = false;
+ break;
+ }
+
+ return false;
+ }
+
+ /**
+ * Overrides to provide fading when slide removal is enabled.
+ */
+ @Override
+ public void onDragFloatView(View floatView, Point position, Point touch) {
+
+ if (mRemoveEnabled && mIsRemoving) {
+ mPositionX = position.x;
+ }
+ }
+
+ /**
+ * Get the position to start dragging based on the ACTION_DOWN
+ * MotionEvent. This function simply calls
+ * {@link #dragHandleHitPosition(MotionEvent)}. Override
+ * to change drag handle behavior;
+ * this function is called internally when an ACTION_DOWN
+ * event is detected.
+ *
+ * @param ev The ACTION_DOWN MotionEvent.
+ *
+ * @return The list position to drag if a drag-init gesture is
+ * detected; MISS if unsuccessful.
+ */
+ public int startDragPosition(MotionEvent ev) {
+ return dragHandleHitPosition(ev);
+ }
+
+ public int startFlingPosition(MotionEvent ev) {
+ return mRemoveMode == FLING_REMOVE ? flingHandleHitPosition(ev) : MISS;
+ }
+
+ /**
+ * Checks for the touch of an item's drag handle (specified by
+ * {@link #setDragHandleId(int)}), and returns that item's position
+ * if a drag handle touch was detected.
+ *
+ * @param ev The ACTION_DOWN MotionEvent.
+
+ * @return The list position of the item whose drag handle was
+ * touched; MISS if unsuccessful.
+ */
+ public int dragHandleHitPosition(MotionEvent ev) {
+ return viewIdHitPosition(ev, mDragHandleId);
+ }
+
+ public int flingHandleHitPosition(MotionEvent ev) {
+ return viewIdHitPosition(ev, mFlingHandleId);
+ }
+
+ public int viewIdHitPosition(MotionEvent ev, int id) {
+ final int x = (int) ev.getX();
+ final int y = (int) ev.getY();
+
+ int touchPos = mDslv.pointToPosition(x, y); // includes headers/footers
+
+ final int numHeaders = mDslv.getHeaderViewsCount();
+ final int numFooters = mDslv.getFooterViewsCount();
+ final int count = mDslv.getCount();
+
+ // Log.d("mobeta", "touch down on position " + itemnum);
+ // We're only interested if the touch was on an
+ // item that's not a header or footer.
+ if (touchPos != AdapterView.INVALID_POSITION && touchPos >= numHeaders
+ && touchPos < (count - numFooters)) {
+ final View item = mDslv.getChildAt(touchPos - mDslv.getFirstVisiblePosition());
+ final int rawX = (int) ev.getRawX();
+ final int rawY = (int) ev.getRawY();
+
+ View dragBox = id == 0 ? item : (View) item.findViewById(id);
+ if (dragBox != null) {
+ dragBox.getLocationOnScreen(mTempLoc);
+
+ if (rawX > mTempLoc[0] && rawY > mTempLoc[1] &&
+ rawX < mTempLoc[0] + dragBox.getWidth() &&
+ rawY < mTempLoc[1] + dragBox.getHeight()) {
+
+ mItemX = item.getLeft();
+ mItemY = item.getTop();
+
+ return touchPos;
+ }
+ }
+ }
+
+ return MISS;
+ }
+
+ @Override
+ public boolean onDown(MotionEvent ev) {
+ if (mRemoveEnabled && mRemoveMode == CLICK_REMOVE) {
+ mClickRemoveHitPos = viewIdHitPosition(ev, mClickRemoveId);
+ }
+
+ mHitPos = startDragPosition(ev);
+ if (mHitPos != MISS && mDragInitMode == ON_DOWN) {
+ startDrag(mHitPos, (int) ev.getX() - mItemX, (int) ev.getY() - mItemY);
+ }
+
+ mIsRemoving = false;
+ mCanDrag = true;
+ mPositionX = 0;
+ mFlingHitPos = startFlingPosition(ev);
+
+ return true;
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+
+ final int x1 = (int) e1.getX();
+ final int y1 = (int) e1.getY();
+ final int x2 = (int) e2.getX();
+ final int y2 = (int) e2.getY();
+ final int deltaX = x2 - mItemX;
+ final int deltaY = y2 - mItemY;
+
+ if (mCanDrag && !mDragging && (mHitPos != MISS || mFlingHitPos != MISS)) {
+ if (mHitPos != MISS) {
+ if (mDragInitMode == ON_DRAG && Math.abs(y2 - y1) > mTouchSlop && mSortEnabled) {
+ startDrag(mHitPos, deltaX, deltaY);
+ }
+ else if (mDragInitMode != ON_DOWN && Math.abs(x2 - x1) > mTouchSlop && mRemoveEnabled)
+ {
+ mIsRemoving = true;
+ startDrag(mFlingHitPos, deltaX, deltaY);
+ }
+ } else if (mFlingHitPos != MISS) {
+ if (Math.abs(x2 - x1) > mTouchSlop && mRemoveEnabled) {
+ mIsRemoving = true;
+ startDrag(mFlingHitPos, deltaX, deltaY);
+ } else if (Math.abs(y2 - y1) > mTouchSlop) {
+ mCanDrag = false; // if started to scroll the list then
+ // don't allow sorting nor fling-removing
+ }
+ }
+ }
+ // return whatever
+ return false;
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ // Log.d("mobeta", "lift listener long pressed");
+ if (mHitPos != MISS && mDragInitMode == ON_LONG_PRESS) {
+ mDslv.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ startDrag(mHitPos, mCurrX - mItemX, mCurrY - mItemY);
+ }
+ }
+
+ // complete the OnGestureListener interface
+ @Override
+ public final boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ return false;
+ }
+
+ // complete the OnGestureListener interface
+ @Override
+ public boolean onSingleTapUp(MotionEvent ev) {
+ if (mRemoveEnabled && mRemoveMode == CLICK_REMOVE) {
+ if (mClickRemoveHitPos != MISS) {
+ mDslv.removeItem(mClickRemoveHitPos - mDslv.getHeaderViewsCount());
+ }
+ }
+ return true;
+ }
+
+ // complete the OnGestureListener interface
+ @Override
+ public void onShowPress(MotionEvent ev) {
+ // do nothing
+ }
+
+ private GestureDetector.OnGestureListener mFlingRemoveListener =
+ new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public final boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+ float velocityY) {
+ ViewConfiguration vc = ViewConfiguration.get(mDslv.getContext());
+ int minSwipeVelocity = vc.getScaledMinimumFlingVelocity();
+ int maxSwipeVelocity = vc.getScaledMaximumFlingVelocity();
+ if (mRemoveEnabled && mIsRemoving) {
+ int w = mDslv.getWidth();
+ if(mPositionX >= w/2) {
+ mDslv.stopDragWithVelocity(true, velocityX);
+ } else if(mPositionX >= w/5 && minSwipeVelocity <= velocityX && velocityX <= maxSwipeVelocity) {
+ mDslv.stopDragWithVelocity(true, velocityX);
+ }
+ mIsRemoving = false;
+ }
+ return false;
+ }
+ };
+
+}
diff --git a/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortCursorAdapter.java b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortCursorAdapter.java
new file mode 100644
index 000000000..267c6f869
--- /dev/null
+++ b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortCursorAdapter.java
@@ -0,0 +1,241 @@
+package com.mobeta.android.dslv;
+
+import java.util.ArrayList;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.util.SparseIntArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.support.v4.widget.CursorAdapter;
+
+
+/**
+ * A subclass of {@link android.widget.CursorAdapter} that provides
+ * reordering of the elements in the Cursor based on completed
+ * drag-sort operations. The reordering is a simple mapping of
+ * list positions into Cursor positions (the Cursor is unchanged).
+ * To persist changes made by drag-sorts, one can retrieve the
+ * mapping with the {@link #getCursorPositions()} method, which
+ * returns the reordered list of Cursor positions.
+ *
+ * An instance of this class is passed
+ * to {@link DragSortListView#setAdapter(ListAdapter)} and, since
+ * this class implements the {@link DragSortListView.DragSortListener}
+ * interface, it is automatically set as the DragSortListener for
+ * the DragSortListView instance.
+ */
+public abstract class DragSortCursorAdapter extends CursorAdapter implements DragSortListView.DragSortListener {
+
+ public static final int REMOVED = -1;
+
+ /**
+ * Key is ListView position, value is Cursor position
+ */
+ private SparseIntArray mListMapping = new SparseIntArray();
+
+ private ArrayListadapter
, this method calls the appropriate
+ * set*Listener method with adapter
as the argument.
+ *
+ * @param adapter The ListAdapter providing data to back
+ * DragSortListView.
+ *
+ * @see android.widget.ListView#setAdapter(android.widget.ListAdapter)
+ */
+ @Override
+ public void setAdapter(ListAdapter adapter) {
+ if (adapter != null) {
+ mAdapterWrapper = new AdapterWrapper(adapter);
+ adapter.registerDataSetObserver(mObserver);
+
+ if (adapter instanceof DropListener) {
+ setDropListener((DropListener) adapter);
+ }
+ if (adapter instanceof DragListener) {
+ setDragListener((DragListener) adapter);
+ }
+ if (adapter instanceof RemoveListener) {
+ setRemoveListener((RemoveListener) adapter);
+ }
+ } else {
+ mAdapterWrapper = null;
+ }
+
+ super.setAdapter(mAdapterWrapper);
+ }
+
+ /**
+ * As opposed to {@link ListView#getAdapter()}, which returns
+ * a heavily wrapped ListAdapter (DragSortListView wraps the
+ * input ListAdapter {\emph and} ListView wraps the wrapped one).
+ *
+ * @return The ListAdapter set as the argument of {@link setAdapter()}
+ */
+ public ListAdapter getInputAdapter() {
+ if (mAdapterWrapper == null) {
+ return null;
+ } else {
+ return mAdapterWrapper.getAdapter();
+ }
+ }
+
+ private class AdapterWrapper extends BaseAdapter {
+ private ListAdapter mAdapter;
+
+ public AdapterWrapper(ListAdapter adapter) {
+ super();
+ mAdapter = adapter;
+
+ mAdapter.registerDataSetObserver(new DataSetObserver() {
+ public void onChanged() {
+ notifyDataSetChanged();
+ }
+
+ public void onInvalidated() {
+ notifyDataSetInvalidated();
+ }
+ });
+ }
+
+ public ListAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mAdapter.getItemId(position);
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mAdapter.getItem(position);
+ }
+
+ @Override
+ public int getCount() {
+ return mAdapter.getCount();
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return mAdapter.areAllItemsEnabled();
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return mAdapter.isEnabled(position);
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return mAdapter.getItemViewType(position);
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return mAdapter.getViewTypeCount();
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return mAdapter.hasStableIds();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return mAdapter.isEmpty();
+ }
+
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+
+ DragSortItemView v;
+ View child;
+ // Log.d("mobeta",
+ // "getView: position="+position+" convertView="+convertView);
+ if (convertView != null) {
+ v = (DragSortItemView) convertView;
+ View oldChild = v.getChildAt(0);
+
+ child = mAdapter.getView(position, oldChild, DragSortListView.this);
+ if (child != oldChild) {
+ // shouldn't get here if user is reusing convertViews
+ // properly
+ if (oldChild != null) {
+ v.removeViewAt(0);
+ }
+ v.addView(child);
+ }
+ } else {
+ child = mAdapter.getView(position, null, DragSortListView.this);
+ if (child instanceof Checkable) {
+ v = new DragSortItemViewCheckable(getContext());
+ } else {
+ v = new DragSortItemView(getContext());
+ }
+ v.setLayoutParams(new AbsListView.LayoutParams(
+ ViewGroup.LayoutParams.FILL_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+ v.addView(child);
+ }
+
+ // Set the correct item height given drag state; passed
+ // View needs to be measured if measurement is required.
+ adjustItem(position + getHeaderViewsCount(), v, true);
+
+ return v;
+ }
+ }
+
+ private void drawDivider(int expPosition, Canvas canvas) {
+
+ final Drawable divider = getDivider();
+ final int dividerHeight = getDividerHeight();
+ // Log.d("mobeta", "div="+divider+" divH="+dividerHeight);
+
+ if (divider != null && dividerHeight != 0) {
+ final ViewGroup expItem = (ViewGroup) getChildAt(expPosition
+ - getFirstVisiblePosition());
+ if (expItem != null) {
+ final int l = getPaddingLeft();
+ final int r = getWidth() - getPaddingRight();
+ final int t;
+ final int b;
+
+ final int childHeight = expItem.getChildAt(0).getHeight();
+
+ if (expPosition > mSrcPos) {
+ t = expItem.getTop() + childHeight;
+ b = t + dividerHeight;
+ } else {
+ b = expItem.getBottom() - childHeight;
+ t = b - dividerHeight;
+ }
+ // Log.d("mobeta", "l="+l+" t="+t+" r="+r+" b="+b);
+
+ // Have to clip to support ColorDrawable on <= Gingerbread
+ canvas.save();
+ canvas.clipRect(l, t, r, b);
+ divider.setBounds(l, t, r, b);
+ divider.draw(canvas);
+ canvas.restore();
+ }
+ }
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ super.dispatchDraw(canvas);
+
+ if (mDragState != IDLE) {
+ // draw the divider over the expanded item
+ if (mFirstExpPos != mSrcPos) {
+ drawDivider(mFirstExpPos, canvas);
+ }
+ if (mSecondExpPos != mFirstExpPos && mSecondExpPos != mSrcPos) {
+ drawDivider(mSecondExpPos, canvas);
+ }
+ }
+
+ if (mFloatView != null) {
+ // draw the float view over everything
+ final int w = mFloatView.getWidth();
+ final int h = mFloatView.getHeight();
+
+ int x = mFloatLoc.x;
+
+ int width = getWidth();
+ if (x < 0)
+ x = -x;
+ float alphaMod;
+ if (x < width) {
+ alphaMod = ((float) (width - x)) / ((float) width);
+ alphaMod *= alphaMod;
+ } else {
+ alphaMod = 0;
+ }
+
+ final int alpha = (int) (255f * mCurrFloatAlpha * alphaMod);
+
+ canvas.save();
+ // Log.d("mobeta", "clip rect bounds: " + canvas.getClipBounds());
+ canvas.translate(mFloatLoc.x, mFloatLoc.y);
+ canvas.clipRect(0, 0, w, h);
+
+ // Log.d("mobeta", "clip rect bounds: " + canvas.getClipBounds());
+ canvas.saveLayerAlpha(0, 0, w, h, alpha, Canvas.ALL_SAVE_FLAG);
+ mFloatView.draw(canvas);
+ canvas.restore();
+ canvas.restore();
+ }
+ }
+
+ private int getItemHeight(int position) {
+ View v = getChildAt(position - getFirstVisiblePosition());
+
+ if (v != null) {
+ // item is onscreen, just get the height of the View
+ return v.getHeight();
+ } else {
+ // item is offscreen. get child height and calculate
+ // item height based on current shuffle state
+ return calcItemHeight(position, getChildHeight(position));
+ }
+ }
+
+ private void printPosData() {
+ Log.d("mobeta", "mSrcPos=" + mSrcPos + " mFirstExpPos=" + mFirstExpPos + " mSecondExpPos="
+ + mSecondExpPos);
+ }
+
+ private class HeightCache {
+
+ private SparseIntArray mMap;
+ private ArrayListtrue
as the first argument.
+ */
+ public void cancelDrag() {
+ if (mDragState == DRAGGING) {
+ mDragScroller.stopScrolling(true);
+ destroyFloatView();
+ clearPositions();
+ adjustAllItems();
+
+ if (mInTouchEvent) {
+ mDragState = STOPPED;
+ } else {
+ mDragState = IDLE;
+ }
+ }
+ }
+
+ private void clearPositions() {
+ mSrcPos = -1;
+ mFirstExpPos = -1;
+ mSecondExpPos = -1;
+ mFloatPos = -1;
+ }
+
+ private void dropFloatView() {
+ // must set to avoid cancelDrag being called from the
+ // DataSetObserver
+ mDragState = DROPPING;
+
+ if (mDropListener != null && mFloatPos >= 0 && mFloatPos < getCount()) {
+ final int numHeaders = getHeaderViewsCount();
+ mDropListener.drop(mSrcPos - numHeaders, mFloatPos - numHeaders);
+ }
+
+ destroyFloatView();
+
+ adjustOnReorder();
+ clearPositions();
+ adjustAllItems();
+
+ // now the drag is done
+ if (mInTouchEvent) {
+ mDragState = STOPPED;
+ } else {
+ mDragState = IDLE;
+ }
+ }
+
+ private void doRemoveItem() {
+ doRemoveItem(mSrcPos - getHeaderViewsCount());
+ }
+
+ /**
+ * Removes dragged item from the list. Calls RemoveListener.
+ */
+ private void doRemoveItem(int which) {
+ // must set to avoid cancelDrag being called from the
+ // DataSetObserver
+ mDragState = REMOVING;
+
+ // end it
+ if (mRemoveListener != null) {
+ mRemoveListener.remove(which);
+ }
+
+ destroyFloatView();
+
+ adjustOnReorder();
+ clearPositions();
+
+ // now the drag is done
+ if (mInTouchEvent) {
+ mDragState = STOPPED;
+ } else {
+ mDragState = IDLE;
+ }
+ }
+
+ private void adjustOnReorder() {
+ final int firstPos = getFirstVisiblePosition();
+ // Log.d("mobeta", "first="+firstPos+" src="+mSrcPos);
+ if (mSrcPos < firstPos) {
+ // collapsed src item is off screen;
+ // adjust the scroll after item heights have been fixed
+ View v = getChildAt(0);
+ int top = 0;
+ if (v != null) {
+ top = v.getTop();
+ }
+ // Log.d("mobeta", "top="+top+" fvh="+mFloatViewHeight);
+ setSelectionFromTop(firstPos - 1, top - getPaddingTop());
+ }
+ }
+
+ /**
+ * Stop a drag in progress. Pass true
if you would
+ * like to remove the dragged item from the list.
+ *
+ * @param remove Remove the dragged item from the list. Calls
+ * a registered RemoveListener, if one exists. Otherwise, calls
+ * the DropListener, if one exists.
+ *
+ * @return True if the stop was successful. False if there is
+ * no floating View.
+ */
+ public boolean stopDrag(boolean remove) {
+ mUseRemoveVelocity = false;
+ return stopDrag(remove, 0);
+ }
+
+ public boolean stopDragWithVelocity(boolean remove, float velocityX) {
+
+ mUseRemoveVelocity = true;
+ return stopDrag(remove, velocityX);
+ }
+
+ public boolean stopDrag(boolean remove, float velocityX) {
+ if (mFloatView != null) {
+ mDragScroller.stopScrolling(true);
+
+ if (remove) {
+ removeItem(mSrcPos - getHeaderViewsCount(), velocityX);
+ } else {
+ if (mDropAnimator != null) {
+ mDropAnimator.start();
+ } else {
+ dropFloatView();
+ }
+ }
+
+ if (mTrackDragSort) {
+ mDragSortTracker.stopTracking();
+ }
+
+ return true;
+ } else {
+ // stop failed
+ return false;
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (mIgnoreTouchEvent) {
+ mIgnoreTouchEvent = false;
+ return false;
+ }
+
+ if (!mDragEnabled) {
+ return super.onTouchEvent(ev);
+ }
+
+ boolean more = false;
+
+ boolean lastCallWasIntercept = mLastCallWasIntercept;
+ mLastCallWasIntercept = false;
+
+ if (!lastCallWasIntercept) {
+ saveTouchCoords(ev);
+ }
+
+ // if (mFloatView != null) {
+ if (mDragState == DRAGGING) {
+ onDragTouchEvent(ev);
+ more = true; // give us more!
+ } else {
+ // what if float view is null b/c we dropped in middle
+ // of drag touch event?
+
+ // if (mDragState != STOPPED) {
+ if (mDragState == IDLE) {
+ if (super.onTouchEvent(ev)) {
+ more = true;
+ }
+ }
+
+ int action = ev.getAction() & MotionEvent.ACTION_MASK;
+
+ switch (action) {
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ doActionUpOrCancel();
+ break;
+ default:
+ if (more) {
+ mCancelMethod = ON_TOUCH_EVENT;
+ }
+ }
+ }
+
+ return more;
+ }
+
+ private void doActionUpOrCancel() {
+ mCancelMethod = NO_CANCEL;
+ mInTouchEvent = false;
+ if (mDragState == STOPPED) {
+ mDragState = IDLE;
+ }
+ mCurrFloatAlpha = mFloatAlpha;
+ mListViewIntercepted = false;
+ mChildHeightCache.clear();
+ }
+
+ private void saveTouchCoords(MotionEvent ev) {
+ int action = ev.getAction() & MotionEvent.ACTION_MASK;
+ if (action != MotionEvent.ACTION_DOWN) {
+ mLastX = mX;
+ mLastY = mY;
+ }
+ mX = (int) ev.getX();
+ mY = (int) ev.getY();
+ if (action == MotionEvent.ACTION_DOWN) {
+ mLastX = mX;
+ mLastY = mY;
+ }
+ mOffsetX = (int) ev.getRawX() - mX;
+ mOffsetY = (int) ev.getRawY() - mY;
+ }
+
+ public boolean listViewIntercepted() {
+ return mListViewIntercepted;
+ }
+
+ private boolean mListViewIntercepted = false;
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (!mDragEnabled) {
+ return super.onInterceptTouchEvent(ev);
+ }
+
+ saveTouchCoords(ev);
+ mLastCallWasIntercept = true;
+
+ int action = ev.getAction() & MotionEvent.ACTION_MASK;
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ if (mDragState != IDLE) {
+ // intercept and ignore
+ mIgnoreTouchEvent = true;
+ return true;
+ }
+ mInTouchEvent = true;
+ }
+
+ boolean intercept = false;
+
+ // the following deals with calls to super.onInterceptTouchEvent
+ if (mFloatView != null) {
+ // super's touch event canceled in startDrag
+ intercept = true;
+ } else {
+ if (super.onInterceptTouchEvent(ev)) {
+ mListViewIntercepted = true;
+ intercept = true;
+ }
+
+ switch (action) {
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ doActionUpOrCancel();
+ break;
+ default:
+ if (intercept) {
+ mCancelMethod = ON_TOUCH_EVENT;
+ } else {
+ mCancelMethod = ON_INTERCEPT_TOUCH_EVENT;
+ }
+ }
+ }
+
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ mInTouchEvent = false;
+ }
+
+ return intercept;
+ }
+
+ /**
+ * Set the width of each drag scroll region by specifying
+ * a fraction of the ListView height.
+ *
+ * @param heightFraction Fraction of ListView height. Capped at
+ * 0.5f.
+ *
+ */
+ public void setDragScrollStart(float heightFraction) {
+ setDragScrollStarts(heightFraction, heightFraction);
+ }
+
+ /**
+ * Set the width of each drag scroll region by specifying
+ * a fraction of the ListView height.
+ *
+ * @param upperFrac Fraction of ListView height for up-scroll bound.
+ * Capped at 0.5f.
+ * @param lowerFrac Fraction of ListView height for down-scroll bound.
+ * Capped at 0.5f.
+ *
+ */
+ public void setDragScrollStarts(float upperFrac, float lowerFrac) {
+ if (lowerFrac > 0.5f) {
+ mDragDownScrollStartFrac = 0.5f;
+ } else {
+ mDragDownScrollStartFrac = lowerFrac;
+ }
+
+ if (upperFrac > 0.5f) {
+ mDragUpScrollStartFrac = 0.5f;
+ } else {
+ mDragUpScrollStartFrac = upperFrac;
+ }
+
+ if (getHeight() != 0) {
+ updateScrollStarts();
+ }
+ }
+
+ private void continueDrag(int x, int y) {
+
+ // proposed position
+ mFloatLoc.x = x - mDragDeltaX;
+ mFloatLoc.y = y - mDragDeltaY;
+
+ doDragFloatView(true);
+
+ int minY = Math.min(y, mFloatViewMid + mFloatViewHeightHalf);
+ int maxY = Math.max(y, mFloatViewMid - mFloatViewHeightHalf);
+
+ // get the current scroll direction
+ int currentScrollDir = mDragScroller.getScrollDir();
+
+ if (minY > mLastY && minY > mDownScrollStartY && currentScrollDir != DragScroller.DOWN) {
+ // dragged down, it is below the down scroll start and it is not
+ // scrolling up
+
+ if (currentScrollDir != DragScroller.STOP) {
+ // moved directly from up scroll to down scroll
+ mDragScroller.stopScrolling(true);
+ }
+
+ // start scrolling down
+ mDragScroller.startScrolling(DragScroller.DOWN);
+ } else if (maxY < mLastY && maxY < mUpScrollStartY && currentScrollDir != DragScroller.UP) {
+ // dragged up, it is above the up scroll start and it is not
+ // scrolling up
+
+ if (currentScrollDir != DragScroller.STOP) {
+ // moved directly from down scroll to up scroll
+ mDragScroller.stopScrolling(true);
+ }
+
+ // start scrolling up
+ mDragScroller.startScrolling(DragScroller.UP);
+ }
+ else if (maxY >= mUpScrollStartY && minY <= mDownScrollStartY
+ && mDragScroller.isScrolling()) {
+ // not in the upper nor in the lower drag-scroll regions but it is
+ // still scrolling
+
+ mDragScroller.stopScrolling(true);
+ }
+ }
+
+ private void updateScrollStarts() {
+ final int padTop = getPaddingTop();
+ final int listHeight = getHeight() - padTop - getPaddingBottom();
+ float heightF = (float) listHeight;
+
+ mUpScrollStartYF = padTop + mDragUpScrollStartFrac * heightF;
+ mDownScrollStartYF = padTop + (1.0f - mDragDownScrollStartFrac) * heightF;
+
+ mUpScrollStartY = (int) mUpScrollStartYF;
+ mDownScrollStartY = (int) mDownScrollStartYF;
+
+ mDragUpScrollHeight = mUpScrollStartYF - padTop;
+ mDragDownScrollHeight = padTop + listHeight - mDownScrollStartYF;
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ updateScrollStarts();
+ }
+
+ private void adjustAllItems() {
+ final int first = getFirstVisiblePosition();
+ final int last = getLastVisiblePosition();
+
+ int begin = Math.max(0, getHeaderViewsCount() - first);
+ int end = Math.min(last - first, getCount() - 1 - getFooterViewsCount() - first);
+
+ for (int i = begin; i <= end; ++i) {
+ View v = getChildAt(i);
+ if (v != null) {
+ adjustItem(first + i, v, false);
+ }
+ }
+ }
+
+ private void adjustItem(int position) {
+ View v = getChildAt(position - getFirstVisiblePosition());
+
+ if (v != null) {
+ adjustItem(position, v, false);
+ }
+ }
+
+ /**
+ * Sets layout param height, gravity, and visibility on
+ * wrapped item.
+ */
+ private void adjustItem(int position, View v, boolean invalidChildHeight) {
+
+ // Adjust item height
+ ViewGroup.LayoutParams lp = v.getLayoutParams();
+ int height;
+ if (position != mSrcPos && position != mFirstExpPos && position != mSecondExpPos) {
+ height = ViewGroup.LayoutParams.WRAP_CONTENT;
+ } else {
+ height = calcItemHeight(position, v, invalidChildHeight);
+ }
+
+ if (height != lp.height) {
+ lp.height = height;
+ v.setLayoutParams(lp);
+ }
+
+ // Adjust item gravity
+ if (position == mFirstExpPos || position == mSecondExpPos) {
+ if (position < mSrcPos) {
+ ((DragSortItemView) v).setGravity(Gravity.BOTTOM);
+ } else if (position > mSrcPos) {
+ ((DragSortItemView) v).setGravity(Gravity.TOP);
+ }
+ }
+
+ // Finally adjust item visibility
+
+ int oldVis = v.getVisibility();
+ int vis = View.VISIBLE;
+
+ if (position == mSrcPos && mFloatView != null) {
+ vis = View.INVISIBLE;
+ }
+
+ if (vis != oldVis) {
+ v.setVisibility(vis);
+ }
+ }
+
+ private int getChildHeight(int position) {
+ if (position == mSrcPos) {
+ return 0;
+ }
+
+ View v = getChildAt(position - getFirstVisiblePosition());
+
+ if (v != null) {
+ // item is onscreen, therefore child height is valid,
+ // hence the "true"
+ return getChildHeight(position, v, false);
+ } else {
+ // item is offscreen
+ // first check cache for child height at this position
+ int childHeight = mChildHeightCache.get(position);
+ if (childHeight != -1) {
+ // Log.d("mobeta", "found child height in cache!");
+ return childHeight;
+ }
+
+ final ListAdapter adapter = getAdapter();
+ int type = adapter.getItemViewType(position);
+
+ // There might be a better place for checking for the following
+ final int typeCount = adapter.getViewTypeCount();
+ if (typeCount != mSampleViewTypes.length) {
+ mSampleViewTypes = new View[typeCount];
+ }
+
+ if (type >= 0) {
+ if (mSampleViewTypes[type] == null) {
+ v = adapter.getView(position, null, this);
+ mSampleViewTypes[type] = v;
+ } else {
+ v = adapter.getView(position, mSampleViewTypes[type], this);
+ }
+ } else {
+ // type is HEADER_OR_FOOTER or IGNORE
+ v = adapter.getView(position, null, this);
+ }
+
+ // current child height is invalid, hence "true" below
+ childHeight = getChildHeight(position, v, true);
+
+ // cache it because this could have been expensive
+ mChildHeightCache.add(position, childHeight);
+
+ return childHeight;
+ }
+ }
+
+ private int getChildHeight(int position, View item, boolean invalidChildHeight) {
+ if (position == mSrcPos) {
+ return 0;
+ }
+
+ View child;
+ if (position < getHeaderViewsCount() || position >= getCount() - getFooterViewsCount()) {
+ child = item;
+ } else {
+ child = ((ViewGroup) item).getChildAt(0);
+ }
+
+ ViewGroup.LayoutParams lp = child.getLayoutParams();
+
+ if (lp != null) {
+ if (lp.height > 0) {
+ return lp.height;
+ }
+ }
+
+ int childHeight = child.getHeight();
+
+ if (childHeight == 0 || invalidChildHeight) {
+ measureItem(child);
+ childHeight = child.getMeasuredHeight();
+ }
+
+ return childHeight;
+ }
+
+ private int calcItemHeight(int position, View item, boolean invalidChildHeight) {
+ return calcItemHeight(position, getChildHeight(position, item, invalidChildHeight));
+ }
+
+ private int calcItemHeight(int position, int childHeight) {
+
+ int divHeight = getDividerHeight();
+
+ boolean isSliding = mAnimate && mFirstExpPos != mSecondExpPos;
+ int maxNonSrcBlankHeight = mFloatViewHeight - mItemHeightCollapsed;
+ int slideHeight = (int) (mSlideFrac * maxNonSrcBlankHeight);
+
+ int height;
+
+ if (position == mSrcPos) {
+ if (mSrcPos == mFirstExpPos) {
+ if (isSliding) {
+ height = slideHeight + mItemHeightCollapsed;
+ } else {
+ height = mFloatViewHeight;
+ }
+ } else if (mSrcPos == mSecondExpPos) {
+ // if gets here, we know an item is sliding
+ height = mFloatViewHeight - slideHeight;
+ } else {
+ height = mItemHeightCollapsed;
+ }
+ } else if (position == mFirstExpPos) {
+ if (isSliding) {
+ height = childHeight + slideHeight;
+ } else {
+ height = childHeight + maxNonSrcBlankHeight;
+ }
+ } else if (position == mSecondExpPos) {
+ // we know an item is sliding (b/c 2ndPos != 1stPos)
+ height = childHeight + maxNonSrcBlankHeight - slideHeight;
+ } else {
+ height = childHeight;
+ }
+
+ return height;
+ }
+
+ @Override
+ public void requestLayout() {
+ if (!mBlockLayoutRequests) {
+ super.requestLayout();
+ }
+ }
+
+ private int adjustScroll(int movePos, View moveItem, int oldFirstExpPos, int oldSecondExpPos) {
+ int adjust = 0;
+
+ final int childHeight = getChildHeight(movePos);
+
+ int moveHeightBefore = moveItem.getHeight();
+ int moveHeightAfter = calcItemHeight(movePos, childHeight);
+
+ int moveBlankBefore = moveHeightBefore;
+ int moveBlankAfter = moveHeightAfter;
+ if (movePos != mSrcPos) {
+ moveBlankBefore -= childHeight;
+ moveBlankAfter -= childHeight;
+ }
+
+ int maxBlank = mFloatViewHeight;
+ if (mSrcPos != mFirstExpPos && mSrcPos != mSecondExpPos) {
+ maxBlank -= mItemHeightCollapsed;
+ }
+
+ if (movePos <= oldFirstExpPos) {
+ if (movePos > mFirstExpPos) {
+ adjust += maxBlank - moveBlankAfter;
+ }
+ } else if (movePos == oldSecondExpPos) {
+ if (movePos <= mFirstExpPos) {
+ adjust += moveBlankBefore - maxBlank;
+ } else if (movePos == mSecondExpPos) {
+ adjust += moveHeightBefore - moveHeightAfter;
+ } else {
+ adjust += moveBlankBefore;
+ }
+ } else {
+ if (movePos <= mFirstExpPos) {
+ adjust -= maxBlank;
+ } else if (movePos == mSecondExpPos) {
+ adjust -= moveBlankAfter;
+ }
+ }
+
+ return adjust;
+ }
+
+ private void measureItem(View item) {
+ ViewGroup.LayoutParams lp = item.getLayoutParams();
+ if (lp == null) {
+ lp = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ item.setLayoutParams(lp);
+ }
+ int wspec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, getListPaddingLeft()
+ + getListPaddingRight(), lp.width);
+ int hspec;
+ if (lp.height > 0) {
+ hspec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
+ } else {
+ hspec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ }
+ item.measure(wspec, hspec);
+ }
+
+ private void measureFloatView() {
+ if (mFloatView != null) {
+ measureItem(mFloatView);
+ mFloatViewHeight = mFloatView.getMeasuredHeight();
+ mFloatViewHeightHalf = mFloatViewHeight / 2;
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ // Log.d("mobeta", "onMeasure called");
+ if (mFloatView != null) {
+ if (mFloatView.isLayoutRequested()) {
+ measureFloatView();
+ }
+ mFloatViewOnMeasured = true; // set to false after layout
+ }
+ mWidthMeasureSpec = widthMeasureSpec;
+ }
+
+ @Override
+ protected void layoutChildren() {
+ super.layoutChildren();
+
+ if (mFloatView != null) {
+ if (mFloatView.isLayoutRequested() && !mFloatViewOnMeasured) {
+ // Have to measure here when usual android measure
+ // pass is skipped. This happens during a drag-sort
+ // when layoutChildren is called directly.
+ measureFloatView();
+ }
+ mFloatView.layout(0, 0, mFloatView.getMeasuredWidth(), mFloatView.getMeasuredHeight());
+ mFloatViewOnMeasured = false;
+ }
+ }
+
+ protected boolean onDragTouchEvent(MotionEvent ev) {
+ // we are in a drag
+ int action = ev.getAction() & MotionEvent.ACTION_MASK;
+
+ switch (ev.getAction() & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_CANCEL:
+ if (mDragState == DRAGGING) {
+ cancelDrag();
+ }
+ doActionUpOrCancel();
+ break;
+ case MotionEvent.ACTION_UP:
+ // Log.d("mobeta", "calling stopDrag from onDragTouchEvent");
+ if (mDragState == DRAGGING) {
+ stopDrag(false);
+ }
+ doActionUpOrCancel();
+ break;
+ case MotionEvent.ACTION_MOVE:
+ continueDrag((int) ev.getX(), (int) ev.getY());
+ break;
+ }
+
+ return true;
+ }
+
+ private boolean mFloatViewInvalidated = false;
+
+ private void invalidateFloatView() {
+ mFloatViewInvalidated = true;
+ }
+
+ /**
+ * Start a drag of item at position
using the
+ * registered FloatViewManager. Calls through
+ * to {@link #startDrag(int,View,int,int,int)} after obtaining
+ * the floating View from the FloatViewManager.
+ *
+ * @param position Item to drag.
+ * @param dragFlags Flags that restrict some movements of the
+ * floating View. For example, set dragFlags |=
+ * ~{@link #DRAG_NEG_X}
to allow dragging the floating
+ * View in all directions except off the screen to the left.
+ * @param deltaX Offset in x of the touch coordinate from the
+ * left edge of the floating View (i.e. touch-x minus float View
+ * left).
+ * @param deltaY Offset in y of the touch coordinate from the
+ * top edge of the floating View (i.e. touch-y minus float View
+ * top).
+ *
+ * @return True if the drag was started, false otherwise. This
+ * startDrag
will fail if we are not currently in
+ * a touch event, there is no registered FloatViewManager,
+ * or the FloatViewManager returns a null View.
+ */
+ public boolean startDrag(int position, int dragFlags, int deltaX, int deltaY) {
+ if (!mInTouchEvent || mFloatViewManager == null) {
+ return false;
+ }
+
+ View v = mFloatViewManager.onCreateFloatView(position);
+
+ if (v == null) {
+ return false;
+ } else {
+ return startDrag(position, v, dragFlags, deltaX, deltaY);
+ }
+
+ }
+
+ /**
+ * Start a drag of item at position
without using
+ * a FloatViewManager.
+ *
+ * @param position Item to drag.
+ * @param floatView Floating View.
+ * @param dragFlags Flags that restrict some movements of the
+ * floating View. For example, set dragFlags |=
+ * ~{@link #DRAG_NEG_X}
to allow dragging the floating
+ * View in all directions except off the screen to the left.
+ * @param deltaX Offset in x of the touch coordinate from the
+ * left edge of the floating View (i.e. touch-x minus float View
+ * left).
+ * @param deltaY Offset in y of the touch coordinate from the
+ * top edge of the floating View (i.e. touch-y minus float View
+ * top).
+ *
+ * @return True if the drag was started, false otherwise. This
+ * startDrag
will fail if we are not currently in
+ * a touch event, floatView
is null, or there is
+ * a drag in progress.
+ */
+ public boolean startDrag(int position, View floatView, int dragFlags, int deltaX, int deltaY) {
+ if (mDragState != IDLE || !mInTouchEvent || mFloatView != null || floatView == null
+ || !mDragEnabled) {
+ return false;
+ }
+
+ if (getParent() != null) {
+ getParent().requestDisallowInterceptTouchEvent(true);
+ }
+
+ int pos = position + getHeaderViewsCount();
+ mFirstExpPos = pos;
+ mSecondExpPos = pos;
+ mSrcPos = pos;
+ mFloatPos = pos;
+
+ // mDragState = dragType;
+ mDragState = DRAGGING;
+ mDragFlags = 0;
+ mDragFlags |= dragFlags;
+
+ mFloatView = floatView;
+ measureFloatView(); // sets mFloatViewHeight
+
+ mDragDeltaX = deltaX;
+ mDragDeltaY = deltaY;
+ mDragStartY = mY;
+
+ // updateFloatView(mX - mDragDeltaX, mY - mDragDeltaY);
+ mFloatLoc.x = mX - mDragDeltaX;
+ mFloatLoc.y = mY - mDragDeltaY;
+
+ // set src item invisible
+ final View srcItem = getChildAt(mSrcPos - getFirstVisiblePosition());
+
+ if (srcItem != null) {
+ srcItem.setVisibility(View.INVISIBLE);
+ }
+
+ if (mTrackDragSort) {
+ mDragSortTracker.startTracking();
+ }
+
+ // once float view is created, events are no longer passed
+ // to ListView
+ switch (mCancelMethod) {
+ case ON_TOUCH_EVENT:
+ super.onTouchEvent(mCancelEvent);
+ break;
+ case ON_INTERCEPT_TOUCH_EVENT:
+ super.onInterceptTouchEvent(mCancelEvent);
+ break;
+ }
+
+ requestLayout();
+
+ if (mLiftAnimator != null) {
+ mLiftAnimator.start();
+ }
+
+ return true;
+ }
+
+ private void doDragFloatView(boolean forceInvalidate) {
+ int movePos = getFirstVisiblePosition() + getChildCount() / 2;
+ View moveItem = getChildAt(getChildCount() / 2);
+
+ if (moveItem == null) {
+ return;
+ }
+
+ doDragFloatView(movePos, moveItem, forceInvalidate);
+ }
+
+ private void doDragFloatView(int movePos, View moveItem, boolean forceInvalidate) {
+ mBlockLayoutRequests = true;
+
+ updateFloatView();
+
+ int oldFirstExpPos = mFirstExpPos;
+ int oldSecondExpPos = mSecondExpPos;
+
+ boolean updated = updatePositions();
+
+ if (updated) {
+ adjustAllItems();
+ int scroll = adjustScroll(movePos, moveItem, oldFirstExpPos, oldSecondExpPos);
+ // Log.d("mobeta", " adjust scroll="+scroll);
+
+ setSelectionFromTop(movePos, moveItem.getTop() + scroll - getPaddingTop());
+ layoutChildren();
+ }
+
+ if (updated || forceInvalidate) {
+ invalidate();
+ }
+
+ mBlockLayoutRequests = false;
+ }
+
+ /**
+ * Sets float View location based on suggested values and
+ * constraints set in mDragFlags.
+ */
+ private void updateFloatView() {
+
+ if (mFloatViewManager != null) {
+ mTouchLoc.set(mX, mY);
+ mFloatViewManager.onDragFloatView(mFloatView, mFloatLoc, mTouchLoc);
+ }
+
+ final int floatX = mFloatLoc.x;
+ final int floatY = mFloatLoc.y;
+
+ // restrict x motion
+ int padLeft = getPaddingLeft();
+ if ((mDragFlags & DRAG_POS_X) == 0 && floatX > padLeft) {
+ mFloatLoc.x = padLeft;
+ } else if ((mDragFlags & DRAG_NEG_X) == 0 && floatX < padLeft) {
+ mFloatLoc.x = padLeft;
+ }
+
+ // keep floating view from going past bottom of last header view
+ final int numHeaders = getHeaderViewsCount();
+ final int numFooters = getFooterViewsCount();
+ final int firstPos = getFirstVisiblePosition();
+ final int lastPos = getLastVisiblePosition();
+
+ // Log.d("mobeta",
+ // "nHead="+numHeaders+" nFoot="+numFooters+" first="+firstPos+" last="+lastPos);
+ int topLimit = getPaddingTop();
+ if (firstPos < numHeaders) {
+ topLimit = getChildAt(numHeaders - firstPos - 1).getBottom();
+ }
+ if ((mDragFlags & DRAG_NEG_Y) == 0) {
+ if (firstPos <= mSrcPos) {
+ topLimit = Math.max(getChildAt(mSrcPos - firstPos).getTop(), topLimit);
+ }
+ }
+ // bottom limit is top of first footer View or
+ // bottom of last item in list
+ int bottomLimit = getHeight() - getPaddingBottom();
+ if (lastPos >= getCount() - numFooters - 1) {
+ bottomLimit = getChildAt(getCount() - numFooters - 1 - firstPos).getBottom();
+ }
+ if ((mDragFlags & DRAG_POS_Y) == 0) {
+ if (lastPos >= mSrcPos) {
+ bottomLimit = Math.min(getChildAt(mSrcPos - firstPos).getBottom(), bottomLimit);
+ }
+ }
+
+ // Log.d("mobeta", "dragView top=" + (y - mDragDeltaY));
+ // Log.d("mobeta", "limit=" + limit);
+ // Log.d("mobeta", "mDragDeltaY=" + mDragDeltaY);
+
+ if (floatY < topLimit) {
+ mFloatLoc.y = topLimit;
+ } else if (floatY + mFloatViewHeight > bottomLimit) {
+ mFloatLoc.y = bottomLimit - mFloatViewHeight;
+ }
+
+ // get y-midpoint of floating view (constrained to ListView bounds)
+ mFloatViewMid = mFloatLoc.y + mFloatViewHeightHalf;
+ }
+
+ private void destroyFloatView() {
+ if (mFloatView != null) {
+ mFloatView.setVisibility(GONE);
+ if (mFloatViewManager != null) {
+ mFloatViewManager.onDestroyFloatView(mFloatView);
+ }
+ mFloatView = null;
+ invalidate();
+ }
+ }
+
+ /**
+ * Interface for customization of the floating View appearance
+ * and dragging behavior. Implement
+ * your own and pass it to {@link #setFloatViewManager}. If
+ * your own is not passed, the default {@link SimpleFloatViewManager}
+ * implementation is used.
+ */
+ public interface FloatViewManager {
+ /**
+ * Return the floating View for item at position
.
+ * DragSortListView will measure and layout this View for you,
+ * so feel free to just inflate it. You can help DSLV by
+ * setting some {@link ViewGroup.LayoutParams} on this View;
+ * otherwise it will set some for you (with a width of FILL_PARENT
+ * and a height of WRAP_CONTENT).
+ *
+ * @param position Position of item to drag (NOTE:
+ * position
excludes header Views; thus, if you
+ * want to call {@link ListView#getChildAt(int)}, you will need
+ * to add {@link ListView#getHeaderViewsCount()} to the index).
+ *
+ * @return The View you wish to display as the floating View.
+ */
+ public View onCreateFloatView(int position);
+
+ /**
+ * Called whenever the floating View is dragged. Float View
+ * properties can be changed here. Also, the upcoming location
+ * of the float View can be altered by setting
+ * location.x
and location.y
.
+ *
+ * @param floatView The floating View.
+ * @param location The location (top-left; relative to DSLV
+ * top-left) at which the float
+ * View would like to appear, given the current touch location
+ * and the offset provided in {@link DragSortListView#startDrag}.
+ * @param touch The current touch location (relative to DSLV
+ * top-left).
+ * @param pendingScroll
+ */
+ public void onDragFloatView(View floatView, Point location, Point touch);
+
+ /**
+ * Called when the float View is dropped; lets you perform
+ * any necessary cleanup. The internal DSLV floating View
+ * reference is set to null immediately after this is called.
+ *
+ * @param floatView The floating View passed to
+ * {@link #onCreateFloatView(int)}.
+ */
+ public void onDestroyFloatView(View floatView);
+ }
+
+ public void setFloatViewManager(FloatViewManager manager) {
+ mFloatViewManager = manager;
+ }
+
+ public void setDragListener(DragListener l) {
+ mDragListener = l;
+ }
+
+ /**
+ * Allows for easy toggling between a DragSortListView
+ * and a regular old ListView. If enabled, items are
+ * draggable, where the drag init mode determines how
+ * items are lifted (see {@link setDragInitMode(int)}).
+ * If disabled, items cannot be dragged.
+ *
+ * @param enabled Set true
to enable list
+ * item dragging
+ */
+ public void setDragEnabled(boolean enabled) {
+ mDragEnabled = enabled;
+ }
+
+ public boolean isDragEnabled() {
+ return mDragEnabled;
+ }
+
+ /**
+ * This better reorder your ListAdapter! DragSortListView does not do this
+ * for you; doesn't make sense to. Make sure
+ * {@link BaseAdapter#notifyDataSetChanged()} or something like it is called
+ * in your implementation. Furthermore, if you have a choiceMode other than
+ * none and the ListAdapter does not return true for
+ * {@link ListAdapter#hasStableIds()}, you will need to call
+ * {@link #moveCheckState(int, int)} to move the check boxes along with the
+ * list items.
+ *
+ * @param l
+ */
+ public void setDropListener(DropListener l) {
+ mDropListener = l;
+ }
+
+ /**
+ * Probably a no-brainer, but make sure that your remove listener
+ * calls {@link BaseAdapter#notifyDataSetChanged()} or something like it.
+ * When an item removal occurs, DragSortListView
+ * relies on a redraw of all the items to recover invisible views
+ * and such. Strictly speaking, if you remove something, your dataset
+ * has changed...
+ *
+ * @param l
+ */
+ public void setRemoveListener(RemoveListener l) {
+ mRemoveListener = l;
+ }
+
+ public interface DragListener {
+ public void drag(int from, int to);
+ }
+
+ /**
+ * Your implementation of this has to reorder your ListAdapter!
+ * Make sure to call
+ * {@link BaseAdapter#notifyDataSetChanged()} or something like it
+ * in your implementation.
+ *
+ * @author heycosmo
+ *
+ */
+ public interface DropListener {
+ public void drop(int from, int to);
+ }
+
+ /**
+ * Make sure to call
+ * {@link BaseAdapter#notifyDataSetChanged()} or something like it
+ * in your implementation.
+ *
+ * @author heycosmo
+ *
+ */
+ public interface RemoveListener {
+ public void remove(int which);
+ }
+
+ public interface DragSortListener extends DropListener, DragListener, RemoveListener {
+ }
+
+ public void setDragSortListener(DragSortListener l) {
+ setDropListener(l);
+ setDragListener(l);
+ setRemoveListener(l);
+ }
+
+ /**
+ * Completely custom scroll speed profile. Default increases linearly
+ * with position and is constant in time. Create your own by implementing
+ * {@link DragSortListView.DragScrollProfile}.
+ *
+ * @param ssp
+ */
+ public void setDragScrollProfile(DragScrollProfile ssp) {
+ if (ssp != null) {
+ mScrollProfile = ssp;
+ }
+ }
+
+ /**
+ * Use this to move the check state of an item from one position to another
+ * in a drop operation. If you have a choiceMode which is not none, this
+ * method must be called when the order of items changes in an underlying
+ * adapter which does not have stable IDs (see
+ * {@link ListAdapter#hasStableIds()}). This is because without IDs, the
+ * ListView has no way of knowing which items have moved where, and cannot
+ * update the check state accordingly.
+ *
+ * A word of warning about a "feature" in Android that you may run into when
+ * dealing with movable list items: for an adapter that does have
+ * stable IDs, ListView will attempt to locate each item based on its ID and
+ * move the check state from the item's old position to the new position —
+ * which is all fine and good (and removes the need for calling this
+ * function), except for the half-baked approach. Apparently to save time in
+ * the naive algorithm used, ListView will only search for an ID in the
+ * close neighborhood of the old position. If the user moves an item too far
+ * (specifically, more than 20 rows away), ListView will give up and just
+ * force the item to be unchecked. So if there is a reasonable chance that
+ * the user will move items more than 20 rows away from the original
+ * position, you may wish to use an adapter with unstable IDs and call this
+ * method manually instead.
+ *
+ * @param from
+ * @param to
+ */
+ public void moveCheckState(int from, int to) {
+ // This method runs in O(n log n) time (n being the number of list
+ // items). The bottleneck is the call to AbsListView.setItemChecked,
+ // which is O(log n) because of the binary search involved in calling
+ // SparseBooleanArray.put().
+ //
+ // To improve on the average time, we minimize the number of calls to
+ // setItemChecked by only calling it for items that actually have a
+ // changed state. This is achieved by building a list containing the
+ // start and end of the "runs" of checked items, and then moving the
+ // runs. Note that moving an item from A to B is essentially a rotation
+ // of the range of items in [A, B]. Let's say we have
+ // . . U V X Y Z . .
+ // and move U after Z. This is equivalent to a rotation one step to the
+ // left within the range you are moving across:
+ // . . V X Y Z U . .
+ //
+ // So, to perform the move we enumerate all the runs within the move
+ // range, then rotate each run one step to the left or right (depending
+ // on move direction). For example, in the list:
+ // X X . X X X . X
+ // we have two runs. One begins at the last item of the list and wraps
+ // around to the beginning, ending at position 1. The second begins at
+ // position 3 and ends at position 5. To rotate a run, regardless of
+ // length, we only need to set a check mark at one end of the run, and
+ // clear a check mark at the other end:
+ // X . X X X . X X
+ SparseBooleanArray cip = getCheckedItemPositions();
+ int rangeStart = from;
+ int rangeEnd = to;
+ if (to < from) {
+ rangeStart = to;
+ rangeEnd = from;
+ }
+ rangeEnd += 1;
+
+ int[] runStart = new int[cip.size()];
+ int[] runEnd = new int[cip.size()];
+ int runCount = buildRunList(cip, rangeStart, rangeEnd, runStart, runEnd);
+ if (runCount == 1 && (runStart[0] == runEnd[0])) {
+ // Special case where all items are checked, we can never set any
+ // item to false like we do below.
+ return;
+ }
+
+ if (from < to) {
+ for (int i = 0; i != runCount; i++) {
+ setItemChecked(rotate(runStart[i], -1, rangeStart, rangeEnd), true);
+ setItemChecked(rotate(runEnd[i], -1, rangeStart, rangeEnd), false);
+ }
+
+ } else {
+ for (int i = 0; i != runCount; i++) {
+ setItemChecked(runStart[i], false);
+ setItemChecked(runEnd[i], true);
+ }
+ }
+ }
+
+ /**
+ * Use this when an item has been deleted, to move the check state of all
+ * following items up one step. If you have a choiceMode which is not none,
+ * this method must be called when the order of items changes in an
+ * underlying adapter which does not have stable IDs (see
+ * {@link ListAdapter#hasStableIds()}). This is because without IDs, the
+ * ListView has no way of knowing which items have moved where, and cannot
+ * update the check state accordingly.
+ *
+ * See also further comments on {@link #moveCheckState(int, int)}.
+ *
+ * @param position
+ */
+ public void removeCheckState(int position) {
+ SparseBooleanArray cip = getCheckedItemPositions();
+
+ if (cip.size() == 0)
+ return;
+ int[] runStart = new int[cip.size()];
+ int[] runEnd = new int[cip.size()];
+ int rangeStart = position;
+ int rangeEnd = cip.keyAt(cip.size() - 1) + 1;
+ int runCount = buildRunList(cip, rangeStart, rangeEnd, runStart, runEnd);
+ for (int i = 0; i != runCount; i++) {
+ if (!(runStart[i] == position || (runEnd[i] < runStart[i] && runEnd[i] > position))) {
+ // Only set a new check mark in front of this run if it does
+ // not contain the deleted position. If it does, we only need
+ // to make it one check mark shorter at the end.
+ setItemChecked(rotate(runStart[i], -1, rangeStart, rangeEnd), true);
+ }
+ setItemChecked(rotate(runEnd[i], -1, rangeStart, rangeEnd), false);
+ }
+ }
+
+ private static int buildRunList(SparseBooleanArray cip, int rangeStart,
+ int rangeEnd, int[] runStart, int[] runEnd) {
+ int runCount = 0;
+
+ int i = findFirstSetIndex(cip, rangeStart, rangeEnd);
+ if (i == -1)
+ return 0;
+
+ int position = cip.keyAt(i);
+ int currentRunStart = position;
+ int currentRunEnd = currentRunStart + 1;
+ for (i++; i < cip.size() && (position = cip.keyAt(i)) < rangeEnd; i++) {
+ if (!cip.valueAt(i)) // not checked => not interesting
+ continue;
+ if (position == currentRunEnd) {
+ currentRunEnd++;
+ } else {
+ runStart[runCount] = currentRunStart;
+ runEnd[runCount] = currentRunEnd;
+ runCount++;
+ currentRunStart = position;
+ currentRunEnd = position + 1;
+ }
+ }
+
+ if (currentRunEnd == rangeEnd) {
+ // rangeStart and rangeEnd are equivalent positions so to be
+ // consistent we translate them to the same integer value. That way
+ // we can check whether a run covers the entire range by just
+ // checking if the start equals the end position.
+ currentRunEnd = rangeStart;
+ }
+ runStart[runCount] = currentRunStart;
+ runEnd[runCount] = currentRunEnd;
+ runCount++;
+
+ if (runCount > 1) {
+ if (runStart[0] == rangeStart && runEnd[runCount - 1] == rangeStart) {
+ // The last run ends at the end of the range, and the first run
+ // starts at the beginning of the range. So they are actually
+ // part of the same run, except they wrap around the end of the
+ // range. To avoid adjacent runs, we need to merge them.
+ runStart[0] = runStart[runCount - 1];
+ runCount--;
+ }
+ }
+ return runCount;
+ }
+
+ private static int rotate(int value, int offset, int lowerBound, int upperBound) {
+ int windowSize = upperBound - lowerBound;
+
+ value += offset;
+ if (value < lowerBound) {
+ value += windowSize;
+ } else if (value >= upperBound) {
+ value -= windowSize;
+ }
+ return value;
+ }
+
+ private static int findFirstSetIndex(SparseBooleanArray sba, int rangeStart, int rangeEnd) {
+ int size = sba.size();
+ int i = insertionIndexForKey(sba, rangeStart);
+ while (i < size && sba.keyAt(i) < rangeEnd && !sba.valueAt(i))
+ i++;
+ if (i == size || sba.keyAt(i) >= rangeEnd)
+ return -1;
+ return i;
+ }
+
+ private static int insertionIndexForKey(SparseBooleanArray sba, int key) {
+ int low = 0;
+ int high = sba.size();
+ while (high - low > 0) {
+ int middle = (low + high) >> 1;
+ if (sba.keyAt(middle) < key)
+ low = middle + 1;
+ else
+ high = middle;
+ }
+ return low;
+ }
+
+ /**
+ * Interface for controlling
+ * scroll speed as a function of touch position and time. Use
+ * {@link DragSortListView#setDragScrollProfile(DragScrollProfile)} to
+ * set custom profile.
+ *
+ * @author heycosmo
+ *
+ */
+ public interface DragScrollProfile {
+ /**
+ * Return a scroll speed in pixels/millisecond. Always return a
+ * positive number.
+ *
+ * @param w Normalized position in scroll region (i.e. w \in [0,1]).
+ * Small w typically means slow scrolling.
+ * @param t Time (in milliseconds) since start of scroll (handy if you
+ * want scroll acceleration).
+ * @return Scroll speed at position w and time t in pixels/ms.
+ */
+ float getSpeed(float w, long t);
+ }
+
+ private class DragScroller implements Runnable {
+
+ private boolean mAbort;
+
+ private long mPrevTime;
+ private long mCurrTime;
+
+ private int dy;
+ private float dt;
+ private long tStart;
+ private int scrollDir;
+
+ public final static int STOP = -1;
+ public final static int UP = 0;
+ public final static int DOWN = 1;
+
+ private float mScrollSpeed; // pixels per ms
+
+ private boolean mScrolling = false;
+
+ private int mLastHeader;
+ private int mFirstFooter;
+
+ public boolean isScrolling() {
+ return mScrolling;
+ }
+
+ public int getScrollDir() {
+ return mScrolling ? scrollDir : STOP;
+ }
+
+ public DragScroller() {
+ }
+
+ public void startScrolling(int dir) {
+ if (!mScrolling) {
+ // Debug.startMethodTracing("dslv-scroll");
+ mAbort = false;
+ mScrolling = true;
+ tStart = SystemClock.uptimeMillis();
+ mPrevTime = tStart;
+ scrollDir = dir;
+ post(this);
+ }
+ }
+
+ public void stopScrolling(boolean now) {
+ if (now) {
+ DragSortListView.this.removeCallbacks(this);
+ mScrolling = false;
+ } else {
+ mAbort = true;
+ }
+
+ // Debug.stopMethodTracing();
+ }
+
+ @Override
+ public void run() {
+ if (mAbort) {
+ mScrolling = false;
+ return;
+ }
+
+ // Log.d("mobeta", "scroll");
+
+ final int first = getFirstVisiblePosition();
+ final int last = getLastVisiblePosition();
+ final int count = getCount();
+ final int padTop = getPaddingTop();
+ final int listHeight = getHeight() - padTop - getPaddingBottom();
+
+ int minY = Math.min(mY, mFloatViewMid + mFloatViewHeightHalf);
+ int maxY = Math.max(mY, mFloatViewMid - mFloatViewHeightHalf);
+
+ if (scrollDir == UP) {
+ View v = getChildAt(0);
+ // Log.d("mobeta", "vtop="+v.getTop()+" padtop="+padTop);
+ if (v == null) {
+ mScrolling = false;
+ return;
+ } else {
+ if (first == 0 && v.getTop() == padTop) {
+ mScrolling = false;
+ return;
+ }
+ }
+ mScrollSpeed = mScrollProfile.getSpeed((mUpScrollStartYF - maxY)
+ / mDragUpScrollHeight, mPrevTime);
+ } else {
+ View v = getChildAt(last - first);
+ if (v == null) {
+ mScrolling = false;
+ return;
+ } else {
+ if (last == count - 1 && v.getBottom() <= listHeight + padTop) {
+ mScrolling = false;
+ return;
+ }
+ }
+ mScrollSpeed = -mScrollProfile.getSpeed((minY - mDownScrollStartYF)
+ / mDragDownScrollHeight, mPrevTime);
+ }
+
+ mCurrTime = SystemClock.uptimeMillis();
+ dt = (float) (mCurrTime - mPrevTime);
+
+ // dy is change in View position of a list item; i.e. positive dy
+ // means user is scrolling up (list item moves down the screen,
+ // remember
+ // y=0 is at top of View).
+ dy = (int) Math.round(mScrollSpeed * dt);
+
+ int movePos;
+ if (dy >= 0) {
+ dy = Math.min(listHeight, dy);
+ movePos = first;
+ } else {
+ dy = Math.max(-listHeight, dy);
+ movePos = last;
+ }
+
+ final View moveItem = getChildAt(movePos - first);
+ int top = moveItem.getTop() + dy;
+
+ if (movePos == 0 && top > padTop) {
+ top = padTop;
+ }
+
+ // always do scroll
+ mBlockLayoutRequests = true;
+
+ setSelectionFromTop(movePos, top - padTop);
+ DragSortListView.this.layoutChildren();
+ invalidate();
+
+ mBlockLayoutRequests = false;
+
+ // scroll means relative float View movement
+ doDragFloatView(movePos, moveItem, false);
+
+ mPrevTime = mCurrTime;
+ // Log.d("mobeta", " updated prevTime="+mPrevTime);
+
+ post(this);
+ }
+ }
+
+ private class DragSortTracker {
+ StringBuilder mBuilder = new StringBuilder();
+
+ File mFile;
+
+ private int mNumInBuffer = 0;
+ private int mNumFlushes = 0;
+
+ private boolean mTracking = false;
+
+ public DragSortTracker() {
+ File root = Environment.getExternalStorageDirectory();
+ mFile = new File(root, "dslv_state.txt");
+
+ if (!mFile.exists()) {
+ try {
+ mFile.createNewFile();
+ Log.d("mobeta", "file created");
+ } catch (IOException e) {
+ Log.w("mobeta", "Could not create dslv_state.txt");
+ Log.d("mobeta", e.getMessage());
+ }
+ }
+
+ }
+
+ public void startTracking() {
+ mBuilder.append("
Sets the layout resource of the item views.
+ * + * @param layout the layout resources used to create item views + */ + public void setViewResource(int layout) { + mLayout = layout; + } + + /** + *Sets the layout resource of the drop down views.
+ * + * @param dropDownLayout the layout resources used to create drop down views + */ + public void setDropDownViewResource(int dropDownLayout) { + mDropDownLayout = dropDownLayout; + } +} diff --git a/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/SimpleDragSortCursorAdapter.java b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/SimpleDragSortCursorAdapter.java new file mode 100644 index 000000000..7a76ea9d3 --- /dev/null +++ b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/SimpleDragSortCursorAdapter.java @@ -0,0 +1,422 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mobeta.android.dslv; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.view.View; +import android.widget.TextView; +import android.widget.ImageView; + +// taken from sdk/sources/android-16/android/widget/SimpleCursorAdapter.java + +/** + * An easy adapter to map columns from a cursor to TextViews or ImageViews + * defined in an XML file. You can specify which columns you want, which + * views you want to display the columns, and the XML file that defines + * the appearance of these views. + * + * Binding occurs in two phases. First, if a + * {@link android.widget.SimpleCursorAdapter.ViewBinder} is available, + * {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)} + * is invoked. If the returned value is true, binding has occured. If the + * returned value is false and the view to bind is a TextView, + * {@link #setViewText(TextView, String)} is invoked. If the returned value + * is false and the view to bind is an ImageView, + * {@link #setViewImage(ImageView, String)} is invoked. If no appropriate + * binding can be found, an {@link IllegalStateException} is thrown. + * + * If this adapter is used with filtering, for instance in an + * {@link android.widget.AutoCompleteTextView}, you can use the + * {@link android.widget.SimpleCursorAdapter.CursorToStringConverter} and the + * {@link android.widget.FilterQueryProvider} interfaces + * to get control over the filtering process. You can refer to + * {@link #convertToString(android.database.Cursor)} and + * {@link #runQueryOnBackgroundThread(CharSequence)} for more information. + */ +public class SimpleDragSortCursorAdapter extends ResourceDragSortCursorAdapter { + /** + * A list of columns containing the data to bind to the UI. + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected int[] mFrom; + /** + * A list of View ids representing the views to which the data must be bound. + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected int[] mTo; + + private int mStringConversionColumn = -1; + private CursorToStringConverter mCursorToStringConverter; + private ViewBinder mViewBinder; + + String[] mOriginalFrom; + + /** + * Constructor the enables auto-requery. + * + * @deprecated This option is discouraged, as it results in Cursor queries + * being performed on the application's UI thread and thus can cause poor + * responsiveness or even Application Not Responding errors. As an alternative, + * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}. + */ + @Deprecated + public SimpleDragSortCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to) { + super(context, layout, c); + mTo = to; + mOriginalFrom = from; + findColumns(c, from); + } + + /** + * Standard constructor. + * + * @param context The context where the ListView associated with this + * SimpleListItemFactory is running + * @param layout resource identifier of a layout file that defines the views + * for this list item. The layout file should include at least + * those named views defined in "to" + * @param c The database cursor. Can be null if the cursor is not available yet. + * @param from A list of column names representing the data to bind to the UI. Can be null + * if the cursor is not available yet. + * @param to The views that should display column in the "from" parameter. + * These should all be TextViews. The first N views in this list + * are given the values of the first N columns in the from + * parameter. Can be null if the cursor is not available yet. + * @param flags Flags used to determine the behavior of the adapter, + * as per {@link CursorAdapter#CursorAdapter(Context, Cursor, int)}. + */ + public SimpleDragSortCursorAdapter(Context context, int layout, + Cursor c, String[] from, int[] to, int flags) { + super(context, layout, c, flags); + mTo = to; + mOriginalFrom = from; + findColumns(c, from); + } + + /** + * Binds all of the field names passed into the "to" parameter of the + * constructor with their corresponding cursor columns as specified in the + * "from" parameter. + * + * Binding occurs in two phases. First, if a + * {@link android.widget.SimpleCursorAdapter.ViewBinder} is available, + * {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)} + * is invoked. If the returned value is true, binding has occured. If the + * returned value is false and the view to bind is a TextView, + * {@link #setViewText(TextView, String)} is invoked. If the returned value is + * false and the view to bind is an ImageView, + * {@link #setViewImage(ImageView, String)} is invoked. If no appropriate + * binding can be found, an {@link IllegalStateException} is thrown. + * + * @throws IllegalStateException if binding cannot occur + * + * @see android.widget.CursorAdapter#bindView(android.view.View, + * android.content.Context, android.database.Cursor) + * @see #getViewBinder() + * @see #setViewBinder(android.widget.SimpleCursorAdapter.ViewBinder) + * @see #setViewImage(ImageView, String) + * @see #setViewText(TextView, String) + */ + @Override + public void bindView(View view, Context context, Cursor cursor) { + final ViewBinder binder = mViewBinder; + final int count = mTo.length; + final int[] from = mFrom; + final int[] to = mTo; + + for (int i = 0; i < count; i++) { + final View v = view.findViewById(to[i]); + if (v != null) { + boolean bound = false; + if (binder != null) { + bound = binder.setViewValue(v, cursor, from[i]); + } + + if (!bound) { + String text = cursor.getString(from[i]); + if (text == null) { + text = ""; + } + + if (v instanceof TextView) { + setViewText((TextView) v, text); + } else if (v instanceof ImageView) { + setViewImage((ImageView) v, text); + } else { + throw new IllegalStateException(v.getClass().getName() + " is not a " + + " view that can be bounds by this SimpleCursorAdapter"); + } + } + } + } + } + + /** + * Returns the {@link ViewBinder} used to bind data to views. + * + * @return a ViewBinder or null if the binder does not exist + * + * @see #bindView(android.view.View, android.content.Context, android.database.Cursor) + * @see #setViewBinder(android.widget.SimpleCursorAdapter.ViewBinder) + */ + public ViewBinder getViewBinder() { + return mViewBinder; + } + + /** + * Sets the binder used to bind data to views. + * + * @param viewBinder the binder used to bind data to views, can be null to + * remove the existing binder + * + * @see #bindView(android.view.View, android.content.Context, android.database.Cursor) + * @see #getViewBinder() + */ + public void setViewBinder(ViewBinder viewBinder) { + mViewBinder = viewBinder; + } + + /** + * Called by bindView() to set the image for an ImageView but only if + * there is no existing ViewBinder or if the existing ViewBinder cannot + * handle binding to an ImageView. + * + * By default, the value will be treated as an image resource. If the + * value cannot be used as an image resource, the value is used as an + * image Uri. + * + * Intended to be overridden by Adapters that need to filter strings + * retrieved from the database. + * + * @param v ImageView to receive an image + * @param value the value retrieved from the cursor + */ + public void setViewImage(ImageView v, String value) { + try { + v.setImageResource(Integer.parseInt(value)); + } catch (NumberFormatException nfe) { + v.setImageURI(Uri.parse(value)); + } + } + + /** + * Called by bindView() to set the text for a TextView but only if + * there is no existing ViewBinder or if the existing ViewBinder cannot + * handle binding to a TextView. + * + * Intended to be overridden by Adapters that need to filter strings + * retrieved from the database. + * + * @param v TextView to receive text + * @param text the text to be set for the TextView + */ + public void setViewText(TextView v, String text) { + v.setText(text); + } + + /** + * Return the index of the column used to get a String representation + * of the Cursor. + * + * @return a valid index in the current Cursor or -1 + * + * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) + * @see #setStringConversionColumn(int) + * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter) + * @see #getCursorToStringConverter() + */ + public int getStringConversionColumn() { + return mStringConversionColumn; + } + + /** + * Defines the index of the column in the Cursor used to get a String + * representation of that Cursor. The column is used to convert the + * Cursor to a String only when the current CursorToStringConverter + * is null. + * + * @param stringConversionColumn a valid index in the current Cursor or -1 to use the default + * conversion mechanism + * + * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) + * @see #getStringConversionColumn() + * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter) + * @see #getCursorToStringConverter() + */ + public void setStringConversionColumn(int stringConversionColumn) { + mStringConversionColumn = stringConversionColumn; + } + + /** + * Returns the converter used to convert the filtering Cursor + * into a String. + * + * @return null if the converter does not exist or an instance of + * {@link android.widget.SimpleCursorAdapter.CursorToStringConverter} + * + * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter) + * @see #getStringConversionColumn() + * @see #setStringConversionColumn(int) + * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) + */ + public CursorToStringConverter getCursorToStringConverter() { + return mCursorToStringConverter; + } + + /** + * Sets the converter used to convert the filtering Cursor + * into a String. + * + * @param cursorToStringConverter the Cursor to String converter, or + * null to remove the converter + * + * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter) + * @see #getStringConversionColumn() + * @see #setStringConversionColumn(int) + * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) + */ + public void setCursorToStringConverter(CursorToStringConverter cursorToStringConverter) { + mCursorToStringConverter = cursorToStringConverter; + } + + /** + * Returns a CharSequence representation of the specified Cursor as defined + * by the current CursorToStringConverter. If no CursorToStringConverter + * has been set, the String conversion column is used instead. If the + * conversion column is -1, the returned String is empty if the cursor + * is null or Cursor.toString(). + * + * @param cursor the Cursor to convert to a CharSequence + * + * @return a non-null CharSequence representing the cursor + */ + @Override + public CharSequence convertToString(Cursor cursor) { + if (mCursorToStringConverter != null) { + return mCursorToStringConverter.convertToString(cursor); + } else if (mStringConversionColumn > -1) { + return cursor.getString(mStringConversionColumn); + } + + return super.convertToString(cursor); + } + + /** + * Create a map from an array of strings to an array of column-id integers in cursor c. + * If c is null, the array will be discarded. + * + * @param c the cursor to find the columns from + * @param from the Strings naming the columns of interest + */ + private void findColumns(Cursor c, String[] from) { + if (c != null) { + int i; + int count = from.length; + if (mFrom == null || mFrom.length != count) { + mFrom = new int[count]; + } + for (i = 0; i < count; i++) { + mFrom[i] = c.getColumnIndexOrThrow(from[i]); + } + } else { + mFrom = null; + } + } + + @Override + public Cursor swapCursor(Cursor c) { + // super.swapCursor() will notify observers before we have + // a valid mapping, make sure we have a mapping before this + // happens + findColumns(c, mOriginalFrom); + return super.swapCursor(c); + } + + /** + * Change the cursor and change the column-to-view mappings at the same time. + * + * @param c The database cursor. Can be null if the cursor is not available yet. + * @param from A list of column names representing the data to bind to the UI. Can be null + * if the cursor is not available yet. + * @param to The views that should display column in the "from" parameter. + * These should all be TextViews. The first N views in this list + * are given the values of the first N columns in the from + * parameter. Can be null if the cursor is not available yet. + */ + public void changeCursorAndColumns(Cursor c, String[] from, int[] to) { + mOriginalFrom = from; + mTo = to; + // super.changeCursor() will notify observers before we have + // a valid mapping, make sure we have a mapping before this + // happens + findColumns(c, mOriginalFrom); + super.changeCursor(c); + } + + /** + * This class can be used by external clients of SimpleCursorAdapter + * to bind values fom the Cursor to views. + * + * You should use this class to bind values from the Cursor to views + * that are not directly supported by SimpleCursorAdapter or to + * change the way binding occurs for views supported by + * SimpleCursorAdapter. + * + * @see SimpleCursorAdapter#bindView(android.view.View, android.content.Context, android.database.Cursor) + * @see SimpleCursorAdapter#setViewImage(ImageView, String) + * @see SimpleCursorAdapter#setViewText(TextView, String) + */ + public static interface ViewBinder { + /** + * Binds the Cursor column defined by the specified index to the specified view. + * + * When binding is handled by this ViewBinder, this method must return true. + * If this method returns false, SimpleCursorAdapter will attempts to handle + * the binding on its own. + * + * @param view the view to bind the data to + * @param cursor the cursor to get the data from + * @param columnIndex the column at which the data can be found in the cursor + * + * @return true if the data was bound to the view, false otherwise + */ + boolean setViewValue(View view, Cursor cursor, int columnIndex); + } + + /** + * This class can be used by external clients of SimpleCursorAdapter + * to define how the Cursor should be converted to a String. + * + * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) + */ + public static interface CursorToStringConverter { + /** + * Returns a CharSequence representing the specified Cursor. + * + * @param cursor the cursor for which a CharSequence representation + * is requested + * + * @return a non-null CharSequence representing the cursor + */ + CharSequence convertToString(Cursor cursor); + } + +} diff --git a/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/SimpleFloatViewManager.java b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/SimpleFloatViewManager.java new file mode 100644 index 000000000..af1df01c0 --- /dev/null +++ b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/SimpleFloatViewManager.java @@ -0,0 +1,89 @@ +package com.mobeta.android.dslv; + +import android.graphics.Bitmap; +import android.graphics.Point; +import android.graphics.Color; +import android.widget.ListView; +import android.widget.ImageView; +import android.view.View; +import android.view.ViewGroup; +import android.util.Log; + +/** + * Simple implementation of the FloatViewManager class. Uses list + * items as they appear in the ListView to create the floating View. + */ +public class SimpleFloatViewManager implements DragSortListView.FloatViewManager { + + private Bitmap mFloatBitmap; + + private ImageView mImageView; + + private int mFloatBGColor = Color.BLACK; + + private ListView mListView; + + public SimpleFloatViewManager(ListView lv) { + mListView = lv; + } + + public void setBackgroundColor(int color) { + mFloatBGColor = color; + } + + /** + * This simple implementation creates a Bitmap copy of the + * list item currently shown at ListViewposition
.
+ */
+ @Override
+ public View onCreateFloatView(int position) {
+ // Guaranteed that this will not be null? I think so. Nope, got
+ // a NullPointerException once...
+ View v = mListView.getChildAt(position + mListView.getHeaderViewsCount() - mListView.getFirstVisiblePosition());
+
+ if (v == null) {
+ return null;
+ }
+
+ v.setPressed(false);
+
+ // Create a copy of the drawing cache so that it does not get
+ // recycled by the framework when the list tries to clean up memory
+ //v.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);
+ v.setDrawingCacheEnabled(true);
+ mFloatBitmap = Bitmap.createBitmap(v.getDrawingCache());
+ v.setDrawingCacheEnabled(false);
+
+ if (mImageView == null) {
+ mImageView = new ImageView(mListView.getContext());
+ }
+ mImageView.setBackgroundColor(mFloatBGColor);
+ mImageView.setPadding(0, 0, 0, 0);
+ mImageView.setImageBitmap(mFloatBitmap);
+ mImageView.setLayoutParams(new ViewGroup.LayoutParams(v.getWidth(), v.getHeight()));
+
+ return mImageView;
+ }
+
+ /**
+ * This does nothing
+ */
+ @Override
+ public void onDragFloatView(View floatView, Point position, Point touch) {
+ // do nothing
+ }
+
+ /**
+ * Removes the Bitmap from the ImageView created in
+ * onCreateFloatView() and tells the system to recycle it.
+ */
+ @Override
+ public void onDestroyFloatView(View floatView) {
+ ((ImageView) floatView).setImageDrawable(null);
+
+ mFloatBitmap.recycle();
+ mFloatBitmap = null;
+ }
+
+}
+
diff --git a/library/drag-sort-listview/src/main/res/values/dslv_attrs.xml b/library/drag-sort-listview/src/main/res/values/dslv_attrs.xml
new file mode 100644
index 000000000..8c779c9bf
--- /dev/null
+++ b/library/drag-sort-listview/src/main/res/values/dslv_attrs.xml
@@ -0,0 +1,30 @@
+
+