summaryrefslogtreecommitdiff
path: root/library
diff options
context:
space:
mode:
Diffstat (limited to 'library')
-rw-r--r--library/drag-sort-listview/.gitignore22
-rw-r--r--library/drag-sort-listview/build.gradle30
-rw-r--r--library/drag-sort-listview/proguard-project.txt20
-rw-r--r--library/drag-sort-listview/src/main/AndroidManifest.xml8
-rw-r--r--library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortController.java468
-rw-r--r--library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortCursorAdapter.java241
-rw-r--r--library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortItemView.java100
-rw-r--r--library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortItemViewCheckable.java55
-rw-r--r--library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortListView.java3073
-rw-r--r--library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/ResourceDragSortCursorAdapter.java133
-rw-r--r--library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/SimpleDragSortCursorAdapter.java422
-rw-r--r--library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/SimpleFloatViewManager.java89
-rw-r--r--library/drag-sort-listview/src/main/res/values/dslv_attrs.xml30
13 files changed, 4691 insertions, 0 deletions
diff --git a/library/drag-sort-listview/.gitignore b/library/drag-sort-listview/.gitignore
new file mode 100644
index 000000000..a45024b5c
--- /dev/null
+++ b/library/drag-sort-listview/.gitignore
@@ -0,0 +1,22 @@
+# built application files
+*.apk
+*.ap_
+
+# files for the dex VM
+*.dex
+
+# Java class files
+*.class
+
+# generated files
+bin/
+gen/
+target/
+
+# Local configuration file (sdk path, etc)
+local.properties
+.gitattributes
+
+# Eclipse project files
+.classpath
+.project
diff --git a/library/drag-sort-listview/build.gradle b/library/drag-sort-listview/build.gradle
new file mode 100644
index 000000000..fbdf12cc2
--- /dev/null
+++ b/library/drag-sort-listview/build.gradle
@@ -0,0 +1,30 @@
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 22
+ buildToolsVersion "21.1.2"
+
+ defaultConfig {
+ minSdkVersion 10
+ targetSdkVersion 22
+ versionCode 4
+ versionName "0.6.1"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_7
+ targetCompatibility JavaVersion.VERSION_1_7
+ }
+
+}
+
+dependencies {
+ compile fileTree(dir: 'libs', include: ['*.jar'])
+ compile 'com.android.support:support-v4:21.0.3'
+}
diff --git a/library/drag-sort-listview/proguard-project.txt b/library/drag-sort-listview/proguard-project.txt
new file mode 100644
index 000000000..f2fe1559a
--- /dev/null
+++ b/library/drag-sort-listview/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.mobeta.android.dslv"
+ android:versionCode="4"
+ android:versionName="0.6.1">
+ <uses-sdk android:targetSdkVersion="7"
+ android:minSdkVersion="7" />
+</manifest>
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 <code>true</code> 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<Integer> mRemovedCursorPositions = new ArrayList<Integer>();
+
+ 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<Integer> toRemove = new ArrayList<Integer>();
+
+ 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<Integer> getCursorPositions() {
+ ArrayList<Integer> result = new ArrayList<Integer>();
+
+ 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
+ * <code>adapter</code>, this method calls the appropriate
+ * set*Listener method with <code>adapter</code> as the argument.
+ *
+ * @param adapter The ListAdapter providing data to back
+ * DragSortListView.
+ *
+ * @see android.widget.ListView#setAdapter(android.widget.ListAdapter)
+ */
+ @Override
+ public void setAdapter(ListAdapter adapter) {
+ 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<Integer> mOrder;
+ private int mMaxSize;
+
+ public HeightCache(int size) {
+ mMap = new SparseIntArray(size);
+ mOrder = new ArrayList<Integer>(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
+ * <code>true</code> 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 <code>true</code> if you would
+ * like to remove the dragged item from the list.
+ *
+ * @param remove Remove the dragged item from the list. Calls
+ * a registered 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 <code>position</code> using the
+ * registered FloatViewManager. Calls through
+ * to {@link #startDrag(int,View,int,int,int)} after obtaining
+ * the floating View from the FloatViewManager.
+ *
+ * @param position Item to drag.
+ * @param dragFlags Flags that restrict some movements of the
+ * floating View. For example, set <code>dragFlags |=
+ * ~{@link #DRAG_NEG_X}</code> to allow dragging the floating
+ * View in all directions except off the screen to the left.
+ * @param deltaX Offset in x of the touch coordinate from the
+ * left edge of the floating View (i.e. touch-x minus float View
+ * left).
+ * @param deltaY Offset in y of the touch coordinate from the
+ * top edge of the floating View (i.e. touch-y minus float View
+ * top).
+ *
+ * @return True if the drag was started, false otherwise. This
+ * <code>startDrag</code> will fail if we are not currently in
+ * a touch event, there is no registered FloatViewManager,
+ * or the FloatViewManager returns a null View.
+ */
+ public boolean startDrag(int position, int dragFlags, int deltaX, int deltaY) {
+ if (!mInTouchEvent || mFloatViewManager == null) {
+ return false;
+ }
+
+ View v = mFloatViewManager.onCreateFloatView(position);
+
+ if (v == null) {
+ return false;
+ } else {
+ return startDrag(position, v, dragFlags, deltaX, deltaY);
+ }
+
+ }
+
+ /**
+ * Start a drag of item at <code>position</code> without using
+ * a FloatViewManager.
+ *
+ * @param position Item to drag.
+ * @param floatView Floating View.
+ * @param dragFlags Flags that restrict some movements of the
+ * floating View. For example, set <code>dragFlags |=
+ * ~{@link #DRAG_NEG_X}</code> to allow dragging the floating
+ * View in all directions except off the screen to the left.
+ * @param deltaX Offset in x of the touch coordinate from the
+ * left edge of the floating View (i.e. touch-x minus float View
+ * left).
+ * @param deltaY Offset in y of the touch coordinate from the
+ * top edge of the floating View (i.e. touch-y minus float View
+ * top).
+ *
+ * @return True if the drag was started, false otherwise. This
+ * <code>startDrag</code> will fail if we are not currently in
+ * a touch event, <code>floatView</code> is null, or there is
+ * a drag in progress.
+ */
+ public boolean startDrag(int position, View floatView, int dragFlags, int deltaX, int deltaY) {
+ if (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 <code>position</code>.
+ * DragSortListView will measure and layout this View for you,
+ * so feel free to just inflate it. You can help DSLV by
+ * setting some {@link ViewGroup.LayoutParams} on this View;
+ * otherwise it will set some for you (with a width of FILL_PARENT
+ * and a height of WRAP_CONTENT).
+ *
+ * @param position Position of item to drag (NOTE:
+ * <code>position</code> excludes header Views; thus, if you
+ * want to call {@link ListView#getChildAt(int)}, you will need
+ * to add {@link ListView#getHeaderViewsCount()} to the index).
+ *
+ * @return The View you wish to display as the floating View.
+ */
+ public View onCreateFloatView(int position);
+
+ /**
+ * Called whenever the floating View is dragged. Float View
+ * properties can be changed here. Also, the upcoming location
+ * of the float View can be altered by setting
+ * <code>location.x</code> and <code>location.y</code>.
+ *
+ * @param floatView The floating View.
+ * @param location The location (top-left; relative to DSLV
+ * top-left) at which the float
+ * View would like to appear, given the current touch location
+ * and the offset provided in {@link DragSortListView#startDrag}.
+ * @param touch The current touch location (relative to DSLV
+ * top-left).
+ * @param pendingScroll
+ */
+ public void onDragFloatView(View floatView, Point location, Point touch);
+
+ /**
+ * Called when the float View is dropped; lets you perform
+ * any necessary cleanup. The internal DSLV floating View
+ * reference is set to null immediately after this is called.
+ *
+ * @param floatView The floating View passed to
+ * {@link #onCreateFloatView(int)}.
+ */
+ public void onDestroyFloatView(View floatView);
+ }
+
+ public void setFloatViewManager(FloatViewManager manager) {
+ mFloatViewManager = manager;
+ }
+
+ public void setDragListener(DragListener l) {
+ mDragListener = l;
+ }
+
+ /**
+ * Allows for easy toggling between a DragSortListView
+ * and a regular old ListView. If enabled, items are
+ * draggable, where the drag init mode determines how
+ * items are lifted (see {@link setDragInitMode(int)}).
+ * If disabled, items cannot be dragged.
+ *
+ * @param enabled Set <code>true</code> to enable list
+ * item dragging
+ */
+ public void setDragEnabled(boolean enabled) {
+ mDragEnabled = enabled;
+ }
+
+ public boolean isDragEnabled() {
+ return mDragEnabled;
+ }
+
+ /**
+ * This better reorder your ListAdapter! DragSortListView does not do this
+ * for you; doesn't make sense to. Make sure
+ * {@link BaseAdapter#notifyDataSetChanged()} or something like it is called
+ * in your implementation. 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.
+ * <p>
+ * A word of warning about a "feature" in Android that you may run into when
+ * dealing with movable list items: for an adapter that <em>does</em> 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("<DSLVStates>\n");
+ mNumFlushes = 0;
+ mTracking = true;
+ }
+
+ public void appendState() {
+ if (!mTracking) {
+ return;
+ }
+
+ mBuilder.append("<DSLVState>\n");
+ final int children = getChildCount();
+ final int first = getFirstVisiblePosition();
+ mBuilder.append(" <Positions>");
+ for (int i = 0; i < children; ++i) {
+ mBuilder.append(first + i).append(",");
+ }
+ mBuilder.append("</Positions>\n");
+
+ mBuilder.append(" <Tops>");
+ for (int i = 0; i < children; ++i) {
+ mBuilder.append(getChildAt(i).getTop()).append(",");
+ }
+ mBuilder.append("</Tops>\n");
+ mBuilder.append(" <Bottoms>");
+ for (int i = 0; i < children; ++i) {
+ mBuilder.append(getChildAt(i).getBottom()).append(",");
+ }
+ mBuilder.append("</Bottoms>\n");
+
+ mBuilder.append(" <FirstExpPos>").append(mFirstExpPos).append("</FirstExpPos>\n");
+ mBuilder.append(" <FirstExpBlankHeight>")
+ .append(getItemHeight(mFirstExpPos) - getChildHeight(mFirstExpPos))
+ .append("</FirstExpBlankHeight>\n");
+ mBuilder.append(" <SecondExpPos>").append(mSecondExpPos).append("</SecondExpPos>\n");
+ mBuilder.append(" <SecondExpBlankHeight>")
+ .append(getItemHeight(mSecondExpPos) - getChildHeight(mSecondExpPos))
+ .append("</SecondExpBlankHeight>\n");
+ mBuilder.append(" <SrcPos>").append(mSrcPos).append("</SrcPos>\n");
+ mBuilder.append(" <SrcHeight>").append(mFloatViewHeight + getDividerHeight())
+ .append("</SrcHeight>\n");
+ mBuilder.append(" <ViewHeight>").append(getHeight()).append("</ViewHeight>\n");
+ mBuilder.append(" <LastY>").append(mLastY).append("</LastY>\n");
+ mBuilder.append(" <FloatY>").append(mFloatViewMid).append("</FloatY>\n");
+ mBuilder.append(" <ShuffleEdges>");
+ for (int i = 0; i < children; ++i) {
+ mBuilder.append(getShuffleEdge(first + i, getChildAt(i).getTop())).append(",");
+ }
+ mBuilder.append("</ShuffleEdges>\n");
+
+ mBuilder.append("</DSLVState>\n");
+ mNumInBuffer++;
+
+ if (mNumInBuffer > 1000) {
+ flush();
+ mNumInBuffer = 0;
+ }
+ }
+
+ public void flush() {
+ if (!mTracking) {
+ return;
+ }
+
+ // save to file on sdcard
+ try {
+ boolean append = true;
+ if (mNumFlushes == 0) {
+ append = false;
+ }
+ FileWriter writer = new FileWriter(mFile, append);
+
+ writer.write(mBuilder.toString());
+ mBuilder.delete(0, mBuilder.length());
+
+ writer.flush();
+ writer.close();
+
+ mNumFlushes++;
+ } catch (IOException e) {
+ // do nothing
+ }
+ }
+
+ public void stopTracking() {
+ if (mTracking) {
+ mBuilder.append("</DSLVStates>\n");
+ flush();
+ mTracking = false;
+ }
+ }
+
+ }
+
+}
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);
+ }
+
+ /**
+ * <p>Sets the layout resource of the item views.</p>
+ *
+ * @param layout the layout resources used to create item views
+ */
+ public void setViewResource(int layout) {
+ mLayout = layout;
+ }
+
+ /**
+ * <p>Sets the layout resource of the drop down views.</p>
+ *
+ * @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 <code>position</code>.
+ */
+ @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 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<resources>
+ <declare-styleable name="DragSortListView">
+ <attr name="collapsed_height" format="dimension" />
+ <attr name="drag_scroll_start" format="float" />
+ <attr name="max_drag_scroll_speed" format="float" />
+ <attr name="float_background_color" format="color" />
+ <attr name="remove_mode">
+ <enum name="clickRemove" value="0" />
+ <enum name="flingRemove" value="1" />
+ </attr>
+ <attr name="track_drag_sort" format="boolean"/>
+ <attr name="float_alpha" format="float"/>
+ <attr name="slide_shuffle_speed" format="float"/>
+ <attr name="remove_animation_duration" format="integer"/>
+ <attr name="drop_animation_duration" format="integer"/>
+ <attr name="drag_enabled" format="boolean" />
+ <attr name="sort_enabled" format="boolean" />
+ <attr name="remove_enabled" format="boolean" />
+ <attr name="drag_start_mode">
+ <enum name="onDown" value="0" />
+ <enum name="onMove" value="1" />
+ <enum name="onLongPress" value="2"/>
+ </attr>
+ <attr name="drag_handle_id" format="integer" />
+ <attr name="fling_handle_id" format="integer" />
+ <attr name="click_remove_id" format="integer" />
+ <attr name="use_default_controller" format="boolean" />
+ </declare-styleable>
+</resources>