From 4ecf62ab6a2296e5271807fe925ff401ff7e629a Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Sat, 14 Mar 2015 15:19:06 +0100 Subject: Setup gradle --- .../src/main/AndroidManifest.xml | 8 + .../mobeta/android/dslv/DragSortController.java | 468 +++ .../mobeta/android/dslv/DragSortCursorAdapter.java | 241 ++ .../com/mobeta/android/dslv/DragSortItemView.java | 100 + .../android/dslv/DragSortItemViewCheckable.java | 55 + .../com/mobeta/android/dslv/DragSortListView.java | 3073 ++++++++++++++++++++ .../dslv/ResourceDragSortCursorAdapter.java | 133 + .../android/dslv/SimpleDragSortCursorAdapter.java | 422 +++ .../android/dslv/SimpleFloatViewManager.java | 89 + .../src/main/res/values/dslv_attrs.xml | 30 + 10 files changed, 4619 insertions(+) create mode 100644 library/drag-sort-listview/src/main/AndroidManifest.xml create mode 100644 library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortController.java create mode 100644 library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortCursorAdapter.java create mode 100644 library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortItemView.java create mode 100644 library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortItemViewCheckable.java create mode 100644 library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortListView.java create mode 100644 library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/ResourceDragSortCursorAdapter.java create mode 100644 library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/SimpleDragSortCursorAdapter.java create mode 100644 library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/SimpleFloatViewManager.java create mode 100644 library/drag-sort-listview/src/main/res/values/dslv_attrs.xml (limited to 'library/drag-sort-listview/src/main') diff --git a/library/drag-sort-listview/src/main/AndroidManifest.xml b/library/drag-sort-listview/src/main/AndroidManifest.xml new file mode 100644 index 000000000..f0ef29f3e --- /dev/null +++ b/library/drag-sort-listview/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortController.java b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortController.java new file mode 100644 index 000000000..41d477e92 --- /dev/null +++ b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortController.java @@ -0,0 +1,468 @@ +package com.mobeta.android.dslv; + +import android.graphics.Point; +import android.view.GestureDetector; +import android.view.HapticFeedbackConstants; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.widget.AdapterView; + +/** + * Class that starts and stops item drags on a {@link DragSortListView} + * based on touch gestures. This class also inherits from + * {@link SimpleFloatViewManager}, which provides basic float View + * creation. + * + * An instance of this class is meant to be passed to the methods + * {@link DragSortListView#setTouchListener()} and + * {@link DragSortListView#setFloatViewManager()} of your + * {@link DragSortListView} instance. + */ +public class DragSortController extends SimpleFloatViewManager implements View.OnTouchListener, GestureDetector.OnGestureListener { + + /** + * Drag init mode enum. + */ + public static final int ON_DOWN = 0; + public static final int ON_DRAG = 1; + public static final int ON_LONG_PRESS = 2; + + private int mDragInitMode = ON_DOWN; + + private boolean mSortEnabled = true; + + /** + * Remove mode enum. + */ + public static final int CLICK_REMOVE = 0; + public static final int FLING_REMOVE = 1; + + /** + * The current remove mode. + */ + private int mRemoveMode; + + private boolean mRemoveEnabled = false; + private boolean mIsRemoving = false; + + private GestureDetector mDetector; + + private GestureDetector mFlingRemoveDetector; + + private int mTouchSlop; + + public static final int MISS = -1; + + private int mHitPos = MISS; + private int mFlingHitPos = MISS; + + private int mClickRemoveHitPos = MISS; + + private int[] mTempLoc = new int[2]; + + private int mItemX; + private int mItemY; + + private int mCurrX; + private int mCurrY; + + private boolean mDragging = false; + + private float mFlingSpeed = 500f; + + private int mDragHandleId; + + private int mClickRemoveId; + + private int mFlingHandleId; + private boolean mCanDrag; + + private DragSortListView mDslv; + private int mPositionX; + + /** + * Calls {@link #DragSortController(DragSortListView, int)} with a + * 0 drag handle id, FLING_RIGHT_REMOVE remove mode, + * and ON_DOWN drag init. By default, sorting is enabled, and + * removal is disabled. + * + * @param dslv The DSLV instance + */ + public DragSortController(DragSortListView dslv) { + this(dslv, 0, ON_DOWN, FLING_REMOVE); + } + + public DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode, int removeMode) { + this(dslv, dragHandleId, dragInitMode, removeMode, 0); + } + + public DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode, int removeMode, int clickRemoveId) { + this(dslv, dragHandleId, dragInitMode, removeMode, clickRemoveId, 0); + } + + /** + * By default, sorting is enabled, and removal is disabled. + * + * @param dslv The DSLV instance + * @param dragHandleId The resource id of the View that represents + * the drag handle in a list item. + */ + public DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode, + int removeMode, int clickRemoveId, int flingHandleId) { + super(dslv); + mDslv = dslv; + mDetector = new GestureDetector(dslv.getContext(), this); + mFlingRemoveDetector = new GestureDetector(dslv.getContext(), mFlingRemoveListener); + mFlingRemoveDetector.setIsLongpressEnabled(false); + mTouchSlop = ViewConfiguration.get(dslv.getContext()).getScaledTouchSlop(); + mDragHandleId = dragHandleId; + mClickRemoveId = clickRemoveId; + mFlingHandleId = flingHandleId; + setRemoveMode(removeMode); + setDragInitMode(dragInitMode); + } + + + public int getDragInitMode() { + return mDragInitMode; + } + + /** + * Set how a drag is initiated. Needs to be one of + * {@link ON_DOWN}, {@link ON_DRAG}, or {@link ON_LONG_PRESS}. + * + * @param mode The drag init mode. + */ + public void setDragInitMode(int mode) { + mDragInitMode = mode; + } + + /** + * Enable/Disable list item sorting. Disabling is useful if only item + * removal is desired. Prevents drags in the vertical direction. + * + * @param enabled Set 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) { + // Log.d("mobeta", "on fling remove called"); + if (mRemoveEnabled && mIsRemoving) { + int w = mDslv.getWidth(); + int minPos = w / 5; + if (velocityX > mFlingSpeed) { + if (mPositionX > -minPos) { + mDslv.stopDragWithVelocity(true, velocityX); + } + } else if (velocityX < -mFlingSpeed) { + if (mPositionX < minPos) { + 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 ArrayList mRemovedCursorPositions = new ArrayList(); + + public DragSortCursorAdapter(Context context, Cursor c) { + super(context, c); + } + + public DragSortCursorAdapter(Context context, Cursor c, boolean autoRequery) { + super(context, c, autoRequery); + } + + public DragSortCursorAdapter(Context context, Cursor c, int flags) { + super(context, c, flags); + } + + /** + * Swaps Cursor and clears list-Cursor mapping. + * + * @see android.widget.CursorAdapter#swapCursor(android.database.Cursor) + */ + @Override + public Cursor swapCursor(Cursor newCursor) { + Cursor old = super.swapCursor(newCursor); + resetMappings(); + return old; + } + + /** + * Changes Cursor and clears list-Cursor mapping. + * + * @see android.widget.CursorAdapter#changeCursor(android.database.Cursor) + */ + @Override + public void changeCursor(Cursor cursor) { + super.changeCursor(cursor); + resetMappings(); + } + + /** + * Resets list-cursor mapping. + */ + public void reset() { + resetMappings(); + notifyDataSetChanged(); + } + + private void resetMappings() { + mListMapping.clear(); + mRemovedCursorPositions.clear(); + } + + @Override + public Object getItem(int position) { + return super.getItem(mListMapping.get(position, position)); + } + + @Override + public long getItemId(int position) { + return super.getItemId(mListMapping.get(position, position)); + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + return super.getDropDownView(mListMapping.get(position, position), convertView, parent); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + return super.getView(mListMapping.get(position, position), convertView, parent); + } + + /** + * On drop, this updates the mapping between Cursor positions + * and ListView positions. The Cursor is unchanged. Retrieve + * the current mapping with {@link getCursorPositions()}. + * + * @see DragSortListView.DropListener#drop(int, int) + */ + @Override + public void drop(int from, int to) { + if (from != to) { + int cursorFrom = mListMapping.get(from, from); + + if (from > to) { + for (int i = from; i > to; --i) { + mListMapping.put(i, mListMapping.get(i - 1, i - 1)); + } + } else { + for (int i = from; i < to; ++i) { + mListMapping.put(i, mListMapping.get(i + 1, i + 1)); + } + } + mListMapping.put(to, cursorFrom); + + cleanMapping(); + notifyDataSetChanged(); + } + } + + /** + * On remove, this updates the mapping between Cursor positions + * and ListView positions. The Cursor is unchanged. Retrieve + * the current mapping with {@link getCursorPositions()}. + * + * @see DragSortListView.RemoveListener#remove(int) + */ + @Override + public void remove(int which) { + int cursorPos = mListMapping.get(which, which); + if (!mRemovedCursorPositions.contains(cursorPos)) { + mRemovedCursorPositions.add(cursorPos); + } + + int newCount = getCount(); + for (int i = which; i < newCount; ++i) { + mListMapping.put(i, mListMapping.get(i + 1, i + 1)); + } + + mListMapping.delete(newCount); + + cleanMapping(); + notifyDataSetChanged(); + } + + /** + * Does nothing. Just completes DragSortListener interface. + */ + @Override + public void drag(int from, int to) { + // do nothing + } + + /** + * Remove unnecessary mappings from sparse array. + */ + private void cleanMapping() { + ArrayList toRemove = new ArrayList(); + + int size = mListMapping.size(); + for (int i = 0; i < size; ++i) { + if (mListMapping.keyAt(i) == mListMapping.valueAt(i)) { + toRemove.add(mListMapping.keyAt(i)); + } + } + + size = toRemove.size(); + for (int i = 0; i < size; ++i) { + mListMapping.delete(toRemove.get(i)); + } + } + + @Override + public int getCount() { + return super.getCount() - mRemovedCursorPositions.size(); + } + + /** + * Get the Cursor position mapped to by the provided list position + * (given all previously handled drag-sort + * operations). + * + * @param position List position + * + * @return The mapped-to Cursor position + */ + public int getCursorPosition(int position) { + return mListMapping.get(position, position); + } + + /** + * Get the current order of Cursor positions presented by the + * list. + */ + public ArrayList getCursorPositions() { + ArrayList result = new ArrayList(); + + for (int i = 0; i < getCount(); ++i) { + result.add(mListMapping.get(i, i)); + } + + return result; + } + + /** + * Get the list position mapped to by the provided Cursor position. + * If the provided Cursor position has been removed by a drag-sort, + * this returns {@link #REMOVED}. + * + * @param cursorPosition A Cursor position + * @return The mapped-to list position or REMOVED + */ + public int getListPosition(int cursorPosition) { + if (mRemovedCursorPositions.contains(cursorPosition)) { + return REMOVED; + } + + int index = mListMapping.indexOfValue(cursorPosition); + if (index < 0) { + return cursorPosition; + } else { + return mListMapping.keyAt(index); + } + } + + +} diff --git a/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortItemView.java b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortItemView.java new file mode 100644 index 000000000..cef7b82bb --- /dev/null +++ b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortItemView.java @@ -0,0 +1,100 @@ +package com.mobeta.android.dslv; + +import android.content.Context; +import android.view.Gravity; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.util.Log; + +/** + * Lightweight ViewGroup that wraps list items obtained from user's + * ListAdapter. ItemView expects a single child that has a definite + * height (i.e. the child's layout height is not MATCH_PARENT). + * The width of + * ItemView will always match the width of its child (that is, + * the width MeasureSpec given to ItemView is passed directly + * to the child, and the ItemView measured width is set to the + * child's measured width). The height of ItemView can be anything; + * the + * + * + * The purpose of this class is to optimize slide + * shuffle animations. + */ +public class DragSortItemView extends ViewGroup { + + private int mGravity = Gravity.TOP; + + public DragSortItemView(Context context) { + super(context); + + // always init with standard ListView layout params + setLayoutParams(new AbsListView.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + + //setClipChildren(true); + } + + public void setGravity(int gravity) { + mGravity = gravity; + } + + public int getGravity() { + return mGravity; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + final View child = getChildAt(0); + + if (child == null) { + return; + } + + if (mGravity == Gravity.TOP) { + child.layout(0, 0, getMeasuredWidth(), child.getMeasuredHeight()); + } else { + child.layout(0, getMeasuredHeight() - child.getMeasuredHeight(), getMeasuredWidth(), getMeasuredHeight()); + } + } + + /** + * + */ + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + + int height = MeasureSpec.getSize(heightMeasureSpec); + int width = MeasureSpec.getSize(widthMeasureSpec); + + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + final View child = getChildAt(0); + if (child == null) { + setMeasuredDimension(0, width); + return; + } + + if (child.isLayoutRequested()) { + // Always let child be as tall as it wants. + measureChild(child, widthMeasureSpec, + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + } + + if (heightMode == MeasureSpec.UNSPECIFIED) { + ViewGroup.LayoutParams lp = getLayoutParams(); + + if (lp.height > 0) { + height = lp.height; + } else { + height = child.getMeasuredHeight(); + } + } + + setMeasuredDimension(width, height); + } + +} diff --git a/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortItemViewCheckable.java b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortItemViewCheckable.java new file mode 100644 index 000000000..27d612e01 --- /dev/null +++ b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortItemViewCheckable.java @@ -0,0 +1,55 @@ +package com.mobeta.android.dslv; + +import android.content.Context; +import android.view.Gravity; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.Checkable; +import android.util.Log; + +/** + * Lightweight ViewGroup that wraps list items obtained from user's + * ListAdapter. ItemView expects a single child that has a definite + * height (i.e. the child's layout height is not MATCH_PARENT). + * The width of + * ItemView will always match the width of its child (that is, + * the width MeasureSpec given to ItemView is passed directly + * to the child, and the ItemView measured width is set to the + * child's measured width). The height of ItemView can be anything; + * the + * + * + * The purpose of this class is to optimize slide + * shuffle animations. + */ +public class DragSortItemViewCheckable extends DragSortItemView implements Checkable { + + public DragSortItemViewCheckable(Context context) { + super(context); + } + + @Override + public boolean isChecked() { + View child = getChildAt(0); + if (child instanceof Checkable) + return ((Checkable) child).isChecked(); + else + return false; + } + + @Override + public void setChecked(boolean checked) { + View child = getChildAt(0); + if (child instanceof Checkable) + ((Checkable) child).setChecked(checked); + } + + @Override + public void toggle() { + View child = getChildAt(0); + if (child instanceof Checkable) + ((Checkable) child).toggle(); + } +} diff --git a/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortListView.java b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortListView.java new file mode 100644 index 000000000..4f8ec744d --- /dev/null +++ b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortListView.java @@ -0,0 +1,3073 @@ +/* + * 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.Canvas; +import android.graphics.Color; +import android.graphics.Point; +import android.graphics.drawable.Drawable; +import android.os.Environment; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseBooleanArray; +import android.util.SparseIntArray; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.BaseAdapter; +import android.widget.Checkable; +import android.widget.ListAdapter; +import android.widget.ListView; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; + +/** + * ListView subclass that mediates drag and drop resorting of items. + * + * + * @author heycosmo + * + */ +public class DragSortListView extends ListView { + + + /** + * The View that floats above the ListView and represents + * the dragged item. + */ + private View mFloatView; + + /** + * The float View location. First based on touch location + * and given deltaX and deltaY. Then restricted by callback + * to FloatViewManager.onDragFloatView(). Finally restricted + * by bounds of DSLV. + */ + private Point mFloatLoc = new Point(); + + private Point mTouchLoc = new Point(); + + /** + * The middle (in the y-direction) of the floating View. + */ + private int mFloatViewMid; + + /** + * Flag to make sure float View isn't measured twice + */ + private boolean mFloatViewOnMeasured = false; + + /** + * Watch the Adapter for data changes. Cancel a drag if + * coincident with a change. + */ + private DataSetObserver mObserver; + + /** + * Transparency for the floating View (XML attribute). + */ + private float mFloatAlpha = 1.0f; + private float mCurrFloatAlpha = 1.0f; + + /** + * While drag-sorting, the current position of the floating + * View. If dropped, the dragged item will land in this position. + */ + private int mFloatPos; + + /** + * The first expanded ListView position that helps represent + * the drop slot tracking the floating View. + */ + private int mFirstExpPos; + + /** + * The second expanded ListView position that helps represent + * the drop slot tracking the floating View. This can equal + * mFirstExpPos if there is no slide shuffle occurring; otherwise + * it is equal to mFirstExpPos + 1. + */ + private int mSecondExpPos; + + /** + * Flag set if slide shuffling is enabled. + */ + private boolean mAnimate = false; + + /** + * The user dragged from this position. + */ + private int mSrcPos; + + /** + * Offset (in x) within the dragged item at which the user + * picked it up (or first touched down with the digitalis). + */ + private int mDragDeltaX; + + /** + * Offset (in y) within the dragged item at which the user + * picked it up (or first touched down with the digitalis). + */ + private int mDragDeltaY; + + + /** + * 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 + * + * @attr name dslv:drag_enabled + */ + private boolean mDragEnabled = true; + + /** + * Drag state enum. + */ + private final static int IDLE = 0; + private final static int REMOVING = 1; + private final static int DROPPING = 2; + private final static int STOPPED = 3; + private final static int DRAGGING = 4; + + 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; + + /** + * Set to true when a down event happens during drag sort; + * for example, when drag finish animations are + * playing. + */ + private boolean mIgnoreTouchEvent = false; + + /** + * Caches DragSortItemView child heights. Sometimes DSLV has to + * know the height of an offscreen item. Since ListView virtualizes + * these, DSLV must get the item from the ListAdapter to obtain + * its height. That process can be expensive, but often the same + * offscreen item will be requested many times in a row. Once an + * offscreen item height is calculated, we cache it in this guy. + * Actually, we cache the height of the child of the + * DragSortItemView since the item height changes often during a + * drag-sort. + */ + private static final int sCacheSize = 3; + private HeightCache mChildHeightCache = new HeightCache(sCacheSize); + + private RemoveAnimator mRemoveAnimator; + + private LiftAnimator mLiftAnimator; + + private DropAnimator mDropAnimator; + + private boolean mUseRemoveVelocity; + private float mRemoveVelocityX = 0; + + public DragSortListView(Context context, AttributeSet attrs) { + super(context, attrs); + + int defaultDuration = 150; + int removeAnimDuration = defaultDuration; // ms + int dropAnimDuration = defaultDuration; // ms + + 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); + + removeAnimDuration = a.getInt( + R.styleable.DragSortListView_remove_animation_duration, + removeAnimDuration); + + dropAnimDuration = a.getInt( + R.styleable.DragSortListView_drop_animation_duration, + dropAnimDuration); + + 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_REMOVE); + boolean sortEnabled = a.getBoolean( + R.styleable.DragSortListView_sort_enabled, + true); + int dragInitMode = a.getInt( + R.styleable.DragSortListView_drag_start_mode, + DragSortController.ON_DOWN); + int dragHandleId = a.getResourceId( + R.styleable.DragSortListView_drag_handle_id, + 0); + int flingHandleId = a.getResourceId( + R.styleable.DragSortListView_fling_handle_id, + 0); + int clickRemoveId = a.getResourceId( + R.styleable.DragSortListView_click_remove_id, + 0); + int bgColor = a.getColor( + R.styleable.DragSortListView_float_background_color, + Color.BLACK); + + DragSortController controller = new DragSortController( + this, dragHandleId, dragInitMode, removeMode, + clickRemoveId, flingHandleId); + controller.setRemoveEnabled(removeEnabled); + controller.setSortEnabled(sortEnabled); + controller.setBackgroundColor(bgColor); + + mFloatViewManager = controller; + setOnTouchListener(controller); + } + + a.recycle(); + } + + mDragScroller = new DragScroller(); + + float smoothness = 0.5f; + if (removeAnimDuration > 0) { + mRemoveAnimator = new RemoveAnimator(smoothness, removeAnimDuration); + } + // mLiftAnimator = new LiftAnimator(smoothness, 100); + if (dropAnimDuration > 0) { + mDropAnimator = new DropAnimator(smoothness, dropAnimDuration); + } + + 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) { + cancelDrag(); + } + } + + @Override + public void onChanged() { + cancel(); + } + + @Override + public void onInvalidated() { + cancel(); + } + }; + } + + /** + * Usually called from a FloatViewManager. The float alpha + * will be reset to the xml-defined value every time a drag + * is stopped. + */ + public void setFloatAlpha(float alpha) { + mCurrFloatAlpha = alpha; + } + + public float getFloatAlpha() { + return mCurrFloatAlpha; + } + + /** + * Set maximum drag scroll speed in positions/second. Only applies + * if using default ScrollSpeedProfile. + * + * @param max Maximum scroll speed. + */ + public void setMaxScrollSpeed(float max) { + mMaxScrollSpeed = max; + } + + /** + * For each DragSortListView Listener interface implemented by + * adapter, this method calls the appropriate + * set*Listener method with adapter as the argument. + * + * @param adapter The ListAdapter providing data to back + * DragSortListView. + * + * @see android.widget.ListView#setAdapter(android.widget.ListAdapter) + */ + @Override + public void setAdapter(ListAdapter adapter) { + if (adapter != null) { + mAdapterWrapper = new AdapterWrapper(adapter); + adapter.registerDataSetObserver(mObserver); + + if (adapter instanceof DropListener) { + setDropListener((DropListener) adapter); + } + if (adapter instanceof DragListener) { + setDragListener((DragListener) adapter); + } + if (adapter instanceof RemoveListener) { + setRemoveListener((RemoveListener) adapter); + } + } else { + mAdapterWrapper = null; + } + + super.setAdapter(mAdapterWrapper); + } + + /** + * As opposed to {@link ListView#getAdapter()}, which returns + * a heavily wrapped ListAdapter (DragSortListView wraps the + * input ListAdapter {\emph and} ListView wraps the wrapped one). + * + * @return The ListAdapter set as the argument of {@link setAdapter()} + */ + public ListAdapter getInputAdapter() { + if (mAdapterWrapper == null) { + return null; + } else { + return mAdapterWrapper.getAdapter(); + } + } + + private class AdapterWrapper extends BaseAdapter { + private ListAdapter mAdapter; + + public AdapterWrapper(ListAdapter adapter) { + super(); + mAdapter = adapter; + + mAdapter.registerDataSetObserver(new DataSetObserver() { + public void onChanged() { + notifyDataSetChanged(); + } + + public void onInvalidated() { + notifyDataSetInvalidated(); + } + }); + } + + public ListAdapter getAdapter() { + return mAdapter; + } + + @Override + public long getItemId(int position) { + return mAdapter.getItemId(position); + } + + @Override + public Object getItem(int position) { + return mAdapter.getItem(position); + } + + @Override + public int getCount() { + return mAdapter.getCount(); + } + + @Override + public boolean areAllItemsEnabled() { + return mAdapter.areAllItemsEnabled(); + } + + @Override + public boolean isEnabled(int position) { + return mAdapter.isEnabled(position); + } + + @Override + public int getItemViewType(int position) { + return mAdapter.getItemViewType(position); + } + + @Override + public int getViewTypeCount() { + return mAdapter.getViewTypeCount(); + } + + @Override + public boolean hasStableIds() { + return mAdapter.hasStableIds(); + } + + @Override + public boolean isEmpty() { + return mAdapter.isEmpty(); + } + + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + + DragSortItemView v; + View child; + // Log.d("mobeta", + // "getView: position="+position+" convertView="+convertView); + if (convertView != null) { + v = (DragSortItemView) convertView; + View oldChild = v.getChildAt(0); + + child = mAdapter.getView(position, oldChild, DragSortListView.this); + if (child != oldChild) { + // shouldn't get here if user is reusing convertViews + // properly + if (oldChild != null) { + v.removeViewAt(0); + } + v.addView(child); + } + } else { + child = mAdapter.getView(position, null, DragSortListView.this); + if (child instanceof Checkable) { + v = new DragSortItemViewCheckable(getContext()); + } else { + v = new DragSortItemView(getContext()); + } + v.setLayoutParams(new AbsListView.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + v.addView(child); + } + + // Set the correct item height given drag state; passed + // View needs to be measured if measurement is required. + adjustItem(position + getHeaderViewsCount(), v, true); + + return v; + } + } + + private void drawDivider(int expPosition, Canvas canvas) { + + final Drawable divider = getDivider(); + final int dividerHeight = getDividerHeight(); + // Log.d("mobeta", "div="+divider+" divH="+dividerHeight); + + if (divider != null && dividerHeight != 0) { + final ViewGroup expItem = (ViewGroup) getChildAt(expPosition + - getFirstVisiblePosition()); + if (expItem != null) { + final int l = getPaddingLeft(); + final int r = getWidth() - getPaddingRight(); + final int t; + final int b; + + final int childHeight = expItem.getChildAt(0).getHeight(); + + if (expPosition > mSrcPos) { + t = expItem.getTop() + childHeight; + b = t + dividerHeight; + } else { + b = expItem.getBottom() - childHeight; + t = b - dividerHeight; + } + // Log.d("mobeta", "l="+l+" t="+t+" r="+r+" b="+b); + + // Have to clip to support ColorDrawable on <= Gingerbread + canvas.save(); + canvas.clipRect(l, t, r, b); + divider.setBounds(l, t, r, b); + divider.draw(canvas); + canvas.restore(); + } + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + + if (mDragState != IDLE) { + // draw the divider over the expanded item + if (mFirstExpPos != mSrcPos) { + drawDivider(mFirstExpPos, canvas); + } + if (mSecondExpPos != mFirstExpPos && mSecondExpPos != mSrcPos) { + drawDivider(mSecondExpPos, canvas); + } + } + + if (mFloatView != null) { + // draw the float view over everything + final int w = mFloatView.getWidth(); + final int h = mFloatView.getHeight(); + + int x = mFloatLoc.x; + + int width = getWidth(); + if (x < 0) + x = -x; + float alphaMod; + if (x < width) { + alphaMod = ((float) (width - x)) / ((float) width); + alphaMod *= alphaMod; + } else { + alphaMod = 0; + } + + final int alpha = (int) (255f * mCurrFloatAlpha * alphaMod); + + canvas.save(); + // Log.d("mobeta", "clip rect bounds: " + canvas.getClipBounds()); + canvas.translate(mFloatLoc.x, mFloatLoc.y); + canvas.clipRect(0, 0, w, h); + + // Log.d("mobeta", "clip rect bounds: " + canvas.getClipBounds()); + canvas.saveLayerAlpha(0, 0, w, h, alpha, Canvas.ALL_SAVE_FLAG); + mFloatView.draw(canvas); + canvas.restore(); + canvas.restore(); + } + } + + private int getItemHeight(int position) { + View v = getChildAt(position - getFirstVisiblePosition()); + + if (v != null) { + // item is onscreen, just get the height of the View + return v.getHeight(); + } else { + // item is offscreen. get child height and calculate + // item height based on current shuffle state + return calcItemHeight(position, getChildHeight(position)); + } + } + + private void printPosData() { + Log.d("mobeta", "mSrcPos=" + mSrcPos + " mFirstExpPos=" + mFirstExpPos + " mSecondExpPos=" + + mSecondExpPos); + } + + private class HeightCache { + + private SparseIntArray mMap; + private ArrayList mOrder; + private int mMaxSize; + + public HeightCache(int size) { + mMap = new SparseIntArray(size); + mOrder = new ArrayList(size); + mMaxSize = size; + } + + /** + * Add item height at position if doesn't already exist. + */ + public void add(int position, int height) { + int currHeight = mMap.get(position, -1); + if (currHeight != height) { + if (currHeight == -1) { + if (mMap.size() == mMaxSize) { + // remove oldest entry + mMap.delete(mOrder.remove(0)); + } + } else { + // move position to newest slot + mOrder.remove((Integer) position); + } + mMap.put(position, height); + mOrder.add(position); + } + } + + public int get(int position) { + return mMap.get(position, -1); + } + + public void clear() { + mMap.clear(); + mOrder.clear(); + } + + } + + /** + * Get the shuffle edge for item at position when top of + * item is at y-coord top. Assumes that current item heights + * are consistent with current float view location and + * thus expanded positions and slide fraction. i.e. Should not be + * called between update of expanded positions/slide fraction + * and layoutChildren. + * + * @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) { + + 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; + int childHeight = getChildHeight(position); + int itemHeight = getItemHeight(position); + + // 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 + itemHeight - mFloatViewHeight; + } else { + int blankHeight = itemHeight - childHeight; + 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 = itemHeight - childHeight; + otop = top + blankHeight; + } + } + + // otop is set + if (position <= mSrcPos) { + edge = otop + (mFloatViewHeight - divHeight - getChildHeight(position - 1)) / 2; + } else { + edge = otop + (childHeight - 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(); + + int itemHeight = startView.getHeight(); + + int edge = getShuffleEdge(startPos, startTop); + 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--; + itemHeight = getItemHeight(itemPos); + + if (itemPos == 0) { + edge = itemTop - divHeight - itemHeight; + break; + } + + itemTop -= itemHeight + divHeight; + edge = getShuffleEdge(itemPos, itemTop); + // 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 + itemHeight; + break; + } + + itemTop += divHeight + itemHeight; + itemHeight = getItemHeight(itemPos + 1); + edge = getShuffleEdge(itemPos + 1, itemTop); + // 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(); + } + } + + private class SmoothAnimator implements Runnable { + protected long mStartTime; + + private float mDurationF; + + private float mAlpha; + private float mA, mB, mC, mD; + + private boolean mCanceled; + + public SmoothAnimator(float smoothness, int duration) { + mAlpha = smoothness; + mDurationF = (float) duration; + mA = mD = 1f / (2f * mAlpha * (1f - mAlpha)); + mB = mAlpha / (2f * (mAlpha - 1f)); + mC = 1f / (1f - mAlpha); + } + + public float transform(float frac) { + if (frac < mAlpha) { + return mA * frac * frac; + } else if (frac < 1f - mAlpha) { + return mB + mC * frac; + } else { + return 1f - mD * (frac - 1f) * (frac - 1f); + } + } + + public void start() { + mStartTime = SystemClock.uptimeMillis(); + mCanceled = false; + onStart(); + post(this); + } + + public void cancel() { + mCanceled = true; + } + + public void onStart() { + // stub + } + + public void onUpdate(float frac, float smoothFrac) { + // stub + } + + public void onStop() { + // stub + } + + @Override + public void run() { + if (mCanceled) { + return; + } + + float fraction = ((float) (SystemClock.uptimeMillis() - mStartTime)) / mDurationF; + + if (fraction >= 1f) { + onUpdate(1f, 1f); + onStop(); + } else { + onUpdate(fraction, transform(fraction)); + post(this); + } + } + } + + /** + * Centers floating View under touch point. + */ + private class LiftAnimator extends SmoothAnimator { + + private float mInitDragDeltaY; + private float mFinalDragDeltaY; + + public LiftAnimator(float smoothness, int duration) { + super(smoothness, duration); + } + + @Override + public void onStart() { + mInitDragDeltaY = mDragDeltaY; + mFinalDragDeltaY = mFloatViewHeightHalf; + } + + @Override + public void onUpdate(float frac, float smoothFrac) { + if (mDragState != DRAGGING) { + cancel(); + } else { + mDragDeltaY = (int) (smoothFrac * mFinalDragDeltaY + (1f - smoothFrac) + * mInitDragDeltaY); + mFloatLoc.y = mY - mDragDeltaY; + doDragFloatView(true); + } + } + } + + /** + * Centers floating View over drop slot before destroying. + */ + private class DropAnimator extends SmoothAnimator { + + private int mDropPos; + private int srcPos; + private float mInitDeltaY; + private float mInitDeltaX; + + public DropAnimator(float smoothness, int duration) { + super(smoothness, duration); + } + + @Override + public void onStart() { + mDropPos = mFloatPos; + srcPos = mSrcPos; + mDragState = DROPPING; + mInitDeltaY = mFloatLoc.y - getTargetY(); + mInitDeltaX = mFloatLoc.x - getPaddingLeft(); + } + + private int getTargetY() { + final int first = getFirstVisiblePosition(); + final int otherAdjust = (mItemHeightCollapsed + getDividerHeight()) / 2; + View v = getChildAt(mDropPos - first); + int targetY = -1; + if (v != null) { + if (mDropPos == srcPos) { + targetY = v.getTop(); + } else if (mDropPos < srcPos) { + // expanded down + targetY = v.getTop() - otherAdjust; + } else { + // expanded up + targetY = v.getBottom() + otherAdjust - mFloatViewHeight; + } + } else { + // drop position is not on screen?? no animation + cancel(); + } + + return targetY; + } + + @Override + public void onUpdate(float frac, float smoothFrac) { + final int targetY = getTargetY(); + final int targetX = getPaddingLeft(); + final float deltaY = mFloatLoc.y - targetY; + final float deltaX = mFloatLoc.x - targetX; + final float f = 1f - smoothFrac; + if (f < Math.abs(deltaY / mInitDeltaY) || f < Math.abs(deltaX / mInitDeltaX)) { + mFloatLoc.y = targetY + (int) (mInitDeltaY * f); + mFloatLoc.x = getPaddingLeft() + (int) (mInitDeltaX * f); + doDragFloatView(true); + } + } + + @Override + public void onStop() { + dropFloatView(); + } + + } + + /** + * Collapses expanded items. + */ + private class RemoveAnimator extends SmoothAnimator { + + private float mFloatLocX; + private float mFirstStartBlank; + private float mSecondStartBlank; + + private int mFirstChildHeight = -1; + private int mSecondChildHeight = -1; + + private int mFirstPos; + private int mSecondPos; + private int srcPos; + + public RemoveAnimator(float smoothness, int duration) { + super(smoothness, duration); + } + + @Override + public void onStart() { + mFirstChildHeight = -1; + mSecondChildHeight = -1; + mFirstPos = mFirstExpPos; + mSecondPos = mSecondExpPos; + srcPos = mSrcPos; + mDragState = REMOVING; + + mFloatLocX = mFloatLoc.x; + if (mUseRemoveVelocity) { + float minVelocity = 2f * getWidth(); + if (mRemoveVelocityX == 0) { + mRemoveVelocityX = (mFloatLocX < 0 ? -1 : 1) * minVelocity; + } else { + minVelocity *= 2; + if (mRemoveVelocityX < 0 && mRemoveVelocityX > -minVelocity) + mRemoveVelocityX = -minVelocity; + else if (mRemoveVelocityX > 0 && mRemoveVelocityX < minVelocity) + mRemoveVelocityX = minVelocity; + } + } else { + destroyFloatView(); + } + } + + @Override + public void onUpdate(float frac, float smoothFrac) { + float f = 1f - smoothFrac; + + final int firstVis = getFirstVisiblePosition(); + View item = getChildAt(mFirstPos - firstVis); + ViewGroup.LayoutParams lp; + int blank; + + if (mUseRemoveVelocity) { + float dt = (float) (SystemClock.uptimeMillis() - mStartTime) / 1000; + if (dt == 0) + return; + float dx = mRemoveVelocityX * dt; + int w = getWidth(); + mRemoveVelocityX += (mRemoveVelocityX > 0 ? 1 : -1) * dt * w; + mFloatLocX += dx; + mFloatLoc.x = (int) mFloatLocX; + if (mFloatLocX < w && mFloatLocX > -w) { + mStartTime = SystemClock.uptimeMillis(); + doDragFloatView(true); + return; + } + } + + if (item != null) { + if (mFirstChildHeight == -1) { + mFirstChildHeight = getChildHeight(mFirstPos, item, false); + mFirstStartBlank = (float) (item.getHeight() - mFirstChildHeight); + } + blank = Math.max((int) (f * mFirstStartBlank), 1); + lp = item.getLayoutParams(); + lp.height = mFirstChildHeight + blank; + item.setLayoutParams(lp); + } + if (mSecondPos != mFirstPos) { + item = getChildAt(mSecondPos - firstVis); + if (item != null) { + if (mSecondChildHeight == -1) { + mSecondChildHeight = getChildHeight(mSecondPos, item, false); + mSecondStartBlank = (float) (item.getHeight() - mSecondChildHeight); + } + blank = Math.max((int) (f * mSecondStartBlank), 1); + lp = item.getLayoutParams(); + lp.height = mSecondChildHeight + blank; + item.setLayoutParams(lp); + } + } + } + + @Override + public void onStop() { + doRemoveItem(); + } + } + + public void removeItem(int which) { + + mUseRemoveVelocity = false; + removeItem(which, 0); + } + + /** + * Removes an item from the list and animates the removal. + * + * @param which Position to remove (NOTE: headers/footers ignored! + * this is a position in your input ListAdapter). + * @param velocityX + */ + public void removeItem(int which, float velocityX) { + if (mDragState == IDLE || mDragState == DRAGGING) { + + if (mDragState == IDLE) { + // called from outside drag-sort + mSrcPos = getHeaderViewsCount() + which; + mFirstExpPos = mSrcPos; + mSecondExpPos = mSrcPos; + mFloatPos = mSrcPos; + View v = getChildAt(mSrcPos - getFirstVisiblePosition()); + if (v != null) { + v.setVisibility(View.INVISIBLE); + } + } + + mDragState = REMOVING; + mRemoveVelocityX = velocityX; + + if (mInTouchEvent) { + switch (mCancelMethod) { + case ON_TOUCH_EVENT: + super.onTouchEvent(mCancelEvent); + break; + case ON_INTERCEPT_TOUCH_EVENT: + super.onInterceptTouchEvent(mCancelEvent); + break; + } + } + + if (mRemoveAnimator != null) { + mRemoveAnimator.start(); + } else { + doRemoveItem(which); + } + } + } + + /** + * Move an item, bypassing the drag-sort process. Simply calls + * through to {@link DropListener#drop(int, int)}. + * + * @param from Position to move (NOTE: headers/footers ignored! + * this is a position in your input ListAdapter). + * @param to Target position (NOTE: headers/footers ignored! + * this is a position in your input ListAdapter). + */ + public void moveItem(int from, int to) { + if (mDropListener != null) { + final int count = getInputAdapter().getCount(); + if (from >= 0 && from < count && to >= 0 && to < count) { + mDropListener.drop(from, to); + } + } + } + + /** + * Cancel a drag. Calls {@link #stopDrag(boolean, boolean)} with + * true 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("\n"); + mNumFlushes = 0; + mTracking = true; + } + + public void appendState() { + if (!mTracking) { + return; + } + + mBuilder.append("\n"); + final int children = getChildCount(); + final int first = getFirstVisiblePosition(); + mBuilder.append(" "); + for (int i = 0; i < children; ++i) { + mBuilder.append(first + i).append(","); + } + mBuilder.append("\n"); + + mBuilder.append(" "); + for (int i = 0; i < children; ++i) { + mBuilder.append(getChildAt(i).getTop()).append(","); + } + mBuilder.append("\n"); + mBuilder.append(" "); + for (int i = 0; i < children; ++i) { + mBuilder.append(getChildAt(i).getBottom()).append(","); + } + mBuilder.append("\n"); + + mBuilder.append(" ").append(mFirstExpPos).append("\n"); + mBuilder.append(" ") + .append(getItemHeight(mFirstExpPos) - getChildHeight(mFirstExpPos)) + .append("\n"); + mBuilder.append(" ").append(mSecondExpPos).append("\n"); + mBuilder.append(" ") + .append(getItemHeight(mSecondExpPos) - getChildHeight(mSecondExpPos)) + .append("\n"); + mBuilder.append(" ").append(mSrcPos).append("\n"); + mBuilder.append(" ").append(mFloatViewHeight + getDividerHeight()) + .append("\n"); + mBuilder.append(" ").append(getHeight()).append("\n"); + mBuilder.append(" ").append(mLastY).append("\n"); + mBuilder.append(" ").append(mFloatViewMid).append("\n"); + mBuilder.append(" "); + for (int i = 0; i < children; ++i) { + mBuilder.append(getShuffleEdge(first + i, getChildAt(i).getTop())).append(","); + } + mBuilder.append("\n"); + + mBuilder.append("\n"); + mNumInBuffer++; + + if (mNumInBuffer > 1000) { + flush(); + mNumInBuffer = 0; + } + } + + public void flush() { + if (!mTracking) { + return; + } + + // save to file on sdcard + try { + boolean append = true; + if (mNumFlushes == 0) { + append = false; + } + 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("\n"); + flush(); + mTracking = false; + } + } + + } + +} diff --git a/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/ResourceDragSortCursorAdapter.java b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/ResourceDragSortCursorAdapter.java new file mode 100644 index 000000000..f2d08107f --- /dev/null +++ b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/ResourceDragSortCursorAdapter.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2011 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.view.View; +import android.view.ViewGroup; +import android.view.LayoutInflater; + +// taken from v4 rev. 10 ResourceCursorAdapter.java + +/** + * Static library support version of the framework's {@link android.widget.ResourceCursorAdapter}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework SDK + * documentation for a class overview. + */ +public abstract class ResourceDragSortCursorAdapter extends DragSortCursorAdapter { + private int mLayout; + + private int mDropDownLayout; + + private LayoutInflater mInflater; + + /** + * 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}. + * + * @param context The context where the ListView associated with this adapter is running + * @param layout resource identifier of a layout file that defines the views + * for this list item. Unless you override them later, this will + * define both the item views and the drop down views. + */ + @Deprecated + public ResourceDragSortCursorAdapter(Context context, int layout, Cursor c) { + super(context, c); + mLayout = mDropDownLayout = layout; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + /** + * Constructor with default behavior as per + * {@link CursorAdapter#CursorAdapter(Context, Cursor, boolean)}; it is recommended + * you not use this, but instead {@link #ResourceCursorAdapter(Context, int, Cursor, int)}. + * When using this constructor, {@link #FLAG_REGISTER_CONTENT_OBSERVER} + * will always be set. + * + * @param context The context where the ListView associated with this adapter is running + * @param layout resource identifier of a layout file that defines the views + * for this list item. Unless you override them later, this will + * define both the item views and the drop down views. + * @param c The cursor from which to get the data. + * @param autoRequery If true the adapter will call requery() on the + * cursor whenever it changes so the most recent + * data is always displayed. Using true here is discouraged. + */ + public ResourceDragSortCursorAdapter(Context context, int layout, Cursor c, boolean autoRequery) { + super(context, c, autoRequery); + mLayout = mDropDownLayout = layout; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + /** + * Standard constructor. + * + * @param context The context where the ListView associated with this adapter is running + * @param layout Resource identifier of a layout file that defines the views + * for this list item. Unless you override them later, this will + * define both the item views and the drop down views. + * @param c The cursor from which to get the data. + * @param flags Flags used to determine the behavior of the adapter, + * as per {@link CursorAdapter#CursorAdapter(Context, Cursor, int)}. + */ + public ResourceDragSortCursorAdapter(Context context, int layout, Cursor c, int flags) { + super(context, c, flags); + mLayout = mDropDownLayout = layout; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + /** + * Inflates view(s) from the specified XML file. + * + * @see android.widget.CursorAdapter#newView(android.content.Context, + * android.database.Cursor, ViewGroup) + */ + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(mLayout, parent, false); + } + + @Override + public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(mDropDownLayout, parent, false); + } + + /** + *

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 ListView position. + */ + @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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -- cgit v1.2.3