1 package com.mobeta.android.dslv;
2 
3 import android.graphics.Point;
4 import android.view.GestureDetector;
5 import android.view.HapticFeedbackConstants;
6 import android.view.MotionEvent;
7 import android.view.View;
8 import android.view.ViewConfiguration;
9 import android.widget.AdapterView;
10 
11 /**
12  * Class that starts and stops item drags on a {@link DragSortListView}
13  * based on touch gestures. This class also inherits from
14  * {@link SimpleFloatViewManager}, which provides basic float View
15  * creation.
16  *
17  * An instance of this class is meant to be passed to the methods
18  * {@link DragSortListView#setTouchListener()} and
19  * {@link DragSortListView#setFloatViewManager()} of your
20  * {@link DragSortListView} instance.
21  */
22 public class DragSortController extends SimpleFloatViewManager implements View.OnTouchListener, GestureDetector.OnGestureListener {
23 
24     /**
25      * Drag init mode enum.
26      */
27     public static final int ON_DOWN = 0;
28     public static final int ON_DRAG = 1;
29     public static final int ON_LONG_PRESS = 2;
30 
31     private int mDragInitMode = ON_DOWN;
32 
33     private boolean mSortEnabled = true;
34 
35     /**
36      * Remove mode enum.
37      */
38     public static final int CLICK_REMOVE = 0;
39     public static final int FLING_REMOVE = 1;
40 
41     /**
42      * The current remove mode.
43      */
44     private int mRemoveMode;
45 
46     private boolean mRemoveEnabled = false;
47     private boolean mIsRemoving = false;
48 
49     private GestureDetector mDetector;
50 
51     private GestureDetector mFlingRemoveDetector;
52 
53     private int mTouchSlop;
54 
55     public static final int MISS = -1;
56 
57     private int mHitPos = MISS;
58     private int mFlingHitPos = MISS;
59 
60     private int mClickRemoveHitPos = MISS;
61 
62     private int[] mTempLoc = new int[2];
63 
64     private int mItemX;
65     private int mItemY;
66 
67     private int mCurrX;
68     private int mCurrY;
69 
70     private boolean mDragging = false;
71 
72     private float mFlingSpeed = 500f;
73 
74     private int mDragHandleId;
75 
76     private int mClickRemoveId;
77 
78     private int mFlingHandleId;
79     private boolean mCanDrag;
80 
81     private DragSortListView mDslv;
82     private int mPositionX;
83 
84     /**
85      * Calls {@link #DragSortController(DragSortListView, int)} with a
86      * 0 drag handle id, FLING_RIGHT_REMOVE remove mode,
87      * and ON_DOWN drag init. By default, sorting is enabled, and
88      * removal is disabled.
89      *
90      * @param dslv The DSLV instance
91      */
DragSortController(DragSortListView dslv)92     public DragSortController(DragSortListView dslv) {
93         this(dslv, 0, ON_DOWN, FLING_REMOVE);
94     }
95 
DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode, int removeMode)96     public DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode, int removeMode) {
97         this(dslv, dragHandleId, dragInitMode, removeMode, 0);
98     }
99 
DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode, int removeMode, int clickRemoveId)100     public DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode, int removeMode, int clickRemoveId) {
101         this(dslv, dragHandleId, dragInitMode, removeMode, clickRemoveId, 0);
102     }
103 
104     /**
105      * By default, sorting is enabled, and removal is disabled.
106      *
107      * @param dslv The DSLV instance
108      * @param dragHandleId The resource id of the View that represents
109      * the drag handle in a list item.
110      */
DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode, int removeMode, int clickRemoveId, int flingHandleId)111     public DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode,
112             int removeMode, int clickRemoveId, int flingHandleId) {
113         super(dslv);
114         mDslv = dslv;
115         mDetector = new GestureDetector(dslv.getContext(), this);
116         mFlingRemoveDetector = new GestureDetector(dslv.getContext(), mFlingRemoveListener);
117         mFlingRemoveDetector.setIsLongpressEnabled(false);
118         mTouchSlop = ViewConfiguration.get(dslv.getContext()).getScaledTouchSlop();
119         mDragHandleId = dragHandleId;
120         mClickRemoveId = clickRemoveId;
121         mFlingHandleId = flingHandleId;
122         setRemoveMode(removeMode);
123         setDragInitMode(dragInitMode);
124     }
125 
126 
getDragInitMode()127     public int getDragInitMode() {
128         return mDragInitMode;
129     }
130 
131     /**
132      * Set how a drag is initiated. Needs to be one of
133      * {@link ON_DOWN}, {@link ON_DRAG}, or {@link ON_LONG_PRESS}.
134      *
135      * @param mode The drag init mode.
136      */
setDragInitMode(int mode)137     public void setDragInitMode(int mode) {
138         mDragInitMode = mode;
139     }
140 
141     /**
142      * Enable/Disable list item sorting. Disabling is useful if only item
143      * removal is desired. Prevents drags in the vertical direction.
144      *
145      * @param enabled Set <code>true</code> to enable list
146      * item sorting.
147      */
setSortEnabled(boolean enabled)148     public void setSortEnabled(boolean enabled) {
149         mSortEnabled = enabled;
150     }
151 
isSortEnabled()152     public boolean isSortEnabled() {
153         return mSortEnabled;
154     }
155 
156     /**
157      * One of {@link CLICK_REMOVE}, {@link FLING_RIGHT_REMOVE},
158      * {@link FLING_LEFT_REMOVE},
159      * {@link SLIDE_RIGHT_REMOVE}, or {@link SLIDE_LEFT_REMOVE}.
160      */
setRemoveMode(int mode)161     public void setRemoveMode(int mode) {
162         mRemoveMode = mode;
163     }
164 
getRemoveMode()165     public int getRemoveMode() {
166         return mRemoveMode;
167     }
168 
169     /**
170      * Enable/Disable item removal without affecting remove mode.
171      */
setRemoveEnabled(boolean enabled)172     public void setRemoveEnabled(boolean enabled) {
173         mRemoveEnabled = enabled;
174     }
175 
isRemoveEnabled()176     public boolean isRemoveEnabled() {
177         return mRemoveEnabled;
178     }
179 
180     /**
181      * Set the resource id for the View that represents the drag
182      * handle in a list item.
183      *
184      * @param id An android resource id.
185      */
setDragHandleId(int id)186     public void setDragHandleId(int id) {
187         mDragHandleId = id;
188     }
189 
190     /**
191      * Set the resource id for the View that represents the fling
192      * handle in a list item.
193      *
194      * @param id An android resource id.
195      */
setFlingHandleId(int id)196     public void setFlingHandleId(int id) {
197         mFlingHandleId = id;
198     }
199 
200     /**
201      * Set the resource id for the View that represents click
202      * removal button.
203      *
204      * @param id An android resource id.
205      */
setClickRemoveId(int id)206     public void setClickRemoveId(int id) {
207         mClickRemoveId = id;
208     }
209 
210     /**
211      * Sets flags to restrict certain motions of the floating View
212      * based on DragSortController settings (such as remove mode).
213      * Starts the drag on the DragSortListView.
214      *
215      * @param position The list item position (includes headers).
216      * @param deltaX Touch x-coord minus left edge of floating View.
217      * @param deltaY Touch y-coord minus top edge of floating View.
218      *
219      * @return True if drag started, false otherwise.
220      */
startDrag(int position, int deltaX, int deltaY)221     public boolean startDrag(int position, int deltaX, int deltaY) {
222 
223         int dragFlags = 0;
224         if (mSortEnabled && !mIsRemoving) {
225             dragFlags |= DragSortListView.DRAG_POS_Y | DragSortListView.DRAG_NEG_Y;
226         }
227         if (mRemoveEnabled && mIsRemoving) {
228             dragFlags |= DragSortListView.DRAG_POS_X;
229             dragFlags |= DragSortListView.DRAG_NEG_X;
230         }
231 
232         mDragging = mDslv.startDrag(position - mDslv.getHeaderViewsCount(), dragFlags, deltaX,
233                 deltaY);
234         return mDragging;
235     }
236 
237     @Override
onTouch(View v, MotionEvent ev)238     public boolean onTouch(View v, MotionEvent ev) {
239         if (!mDslv.isDragEnabled() || mDslv.listViewIntercepted()) {
240             return false;
241         }
242 
243         mDetector.onTouchEvent(ev);
244         if (mRemoveEnabled && mDragging && mRemoveMode == FLING_REMOVE) {
245             mFlingRemoveDetector.onTouchEvent(ev);
246         }
247 
248         int action = ev.getAction() & MotionEvent.ACTION_MASK;
249         switch (action) {
250             case MotionEvent.ACTION_DOWN:
251                 mCurrX = (int) ev.getX();
252                 mCurrY = (int) ev.getY();
253                 break;
254             case MotionEvent.ACTION_UP:
255                 if (mRemoveEnabled && mIsRemoving) {
256                     int x = mPositionX >= 0 ? mPositionX : -mPositionX;
257                     int removePoint = mDslv.getWidth() / 2;
258                     if (x > removePoint) {
259                         mDslv.stopDragWithVelocity(true, 0);
260                     }
261                 }
262             case MotionEvent.ACTION_CANCEL:
263                 mIsRemoving = false;
264                 mDragging = false;
265                 break;
266         }
267 
268         return false;
269     }
270 
271     /**
272      * Overrides to provide fading when slide removal is enabled.
273      */
274     @Override
onDragFloatView(View floatView, Point position, Point touch)275     public void onDragFloatView(View floatView, Point position, Point touch) {
276 
277         if (mRemoveEnabled && mIsRemoving) {
278             mPositionX = position.x;
279         }
280     }
281 
282     /**
283      * Get the position to start dragging based on the ACTION_DOWN
284      * MotionEvent. This function simply calls
285      * {@link #dragHandleHitPosition(MotionEvent)}. Override
286      * to change drag handle behavior;
287      * this function is called internally when an ACTION_DOWN
288      * event is detected.
289      *
290      * @param ev The ACTION_DOWN MotionEvent.
291      *
292      * @return The list position to drag if a drag-init gesture is
293      * detected; MISS if unsuccessful.
294      */
startDragPosition(MotionEvent ev)295     public int startDragPosition(MotionEvent ev) {
296         return dragHandleHitPosition(ev);
297     }
298 
startFlingPosition(MotionEvent ev)299     public int startFlingPosition(MotionEvent ev) {
300         return mRemoveMode == FLING_REMOVE ? flingHandleHitPosition(ev) : MISS;
301     }
302 
303     /**
304      * Checks for the touch of an item's drag handle (specified by
305      * {@link #setDragHandleId(int)}), and returns that item's position
306      * if a drag handle touch was detected.
307      *
308      * @param ev The ACTION_DOWN MotionEvent.
309 
310      * @return The list position of the item whose drag handle was
311      * touched; MISS if unsuccessful.
312      */
dragHandleHitPosition(MotionEvent ev)313     public int dragHandleHitPosition(MotionEvent ev) {
314         return viewIdHitPosition(ev, mDragHandleId);
315     }
316 
flingHandleHitPosition(MotionEvent ev)317     public int flingHandleHitPosition(MotionEvent ev) {
318         return viewIdHitPosition(ev, mFlingHandleId);
319     }
320 
viewIdHitPosition(MotionEvent ev, int id)321     public int viewIdHitPosition(MotionEvent ev, int id) {
322         final int x = (int) ev.getX();
323         final int y = (int) ev.getY();
324 
325         int touchPos = mDslv.pointToPosition(x, y); // includes headers/footers
326 
327         final int numHeaders = mDslv.getHeaderViewsCount();
328         final int numFooters = mDslv.getFooterViewsCount();
329         final int count = mDslv.getCount();
330 
331         // Log.d("mobeta", "touch down on position " + itemnum);
332         // We're only interested if the touch was on an
333         // item that's not a header or footer.
334         if (touchPos != AdapterView.INVALID_POSITION && touchPos >= numHeaders
335                 && touchPos < (count - numFooters)) {
336             final View item = mDslv.getChildAt(touchPos - mDslv.getFirstVisiblePosition());
337             final int rawX = (int) ev.getRawX();
338             final int rawY = (int) ev.getRawY();
339 
340             View dragBox = id == 0 ? item : (View) item.findViewById(id);
341             if (dragBox != null) {
342                 dragBox.getLocationOnScreen(mTempLoc);
343 
344                 if (rawX > mTempLoc[0] && rawY > mTempLoc[1] &&
345                         rawX < mTempLoc[0] + dragBox.getWidth() &&
346                         rawY < mTempLoc[1] + dragBox.getHeight()) {
347 
348                     mItemX = item.getLeft();
349                     mItemY = item.getTop();
350 
351                     return touchPos;
352                 }
353             }
354         }
355 
356         return MISS;
357     }
358 
359     @Override
onDown(MotionEvent ev)360     public boolean onDown(MotionEvent ev) {
361         if (mRemoveEnabled && mRemoveMode == CLICK_REMOVE) {
362             mClickRemoveHitPos = viewIdHitPosition(ev, mClickRemoveId);
363         }
364 
365         mHitPos = startDragPosition(ev);
366         if (mHitPos != MISS && mDragInitMode == ON_DOWN) {
367             startDrag(mHitPos, (int) ev.getX() - mItemX, (int) ev.getY() - mItemY);
368         }
369 
370         mIsRemoving = false;
371         mCanDrag = true;
372         mPositionX = 0;
373         mFlingHitPos = startFlingPosition(ev);
374 
375         return true;
376     }
377 
378     @Override
onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)379     public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
380 
381         final int x1 = (int) e1.getX();
382         final int y1 = (int) e1.getY();
383         final int x2 = (int) e2.getX();
384         final int y2 = (int) e2.getY();
385         final int deltaX = x2 - mItemX;
386         final int deltaY = y2 - mItemY;
387 
388         if (mCanDrag && !mDragging && (mHitPos != MISS || mFlingHitPos != MISS)) {
389             if (mHitPos != MISS) {
390                 if (mDragInitMode == ON_DRAG && Math.abs(y2 - y1) > mTouchSlop && mSortEnabled) {
391                     startDrag(mHitPos, deltaX, deltaY);
392                 }
393                 else if (mDragInitMode != ON_DOWN && Math.abs(x2 - x1) > mTouchSlop && mRemoveEnabled)
394                 {
395                     mIsRemoving = true;
396                     startDrag(mFlingHitPos, deltaX, deltaY);
397                 }
398             } else if (mFlingHitPos != MISS) {
399                 if (Math.abs(x2 - x1) > mTouchSlop && mRemoveEnabled) {
400                     mIsRemoving = true;
401                     startDrag(mFlingHitPos, deltaX, deltaY);
402                 } else if (Math.abs(y2 - y1) > mTouchSlop) {
403                     mCanDrag = false; // if started to scroll the list then
404                                       // don't allow sorting nor fling-removing
405                 }
406             }
407         }
408         // return whatever
409         return false;
410     }
411 
412     @Override
onLongPress(MotionEvent e)413     public void onLongPress(MotionEvent e) {
414         // Log.d("mobeta", "lift listener long pressed");
415         if (mHitPos != MISS && mDragInitMode == ON_LONG_PRESS) {
416             mDslv.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
417             startDrag(mHitPos, mCurrX - mItemX, mCurrY - mItemY);
418         }
419     }
420 
421     // complete the OnGestureListener interface
422     @Override
onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)423     public final boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
424         return false;
425     }
426 
427     // complete the OnGestureListener interface
428     @Override
onSingleTapUp(MotionEvent ev)429     public boolean onSingleTapUp(MotionEvent ev) {
430         if (mRemoveEnabled && mRemoveMode == CLICK_REMOVE) {
431             if (mClickRemoveHitPos != MISS) {
432                 mDslv.removeItem(mClickRemoveHitPos - mDslv.getHeaderViewsCount());
433             }
434         }
435         return true;
436     }
437 
438     // complete the OnGestureListener interface
439     @Override
onShowPress(MotionEvent ev)440     public void onShowPress(MotionEvent ev) {
441         // do nothing
442     }
443 
444     private GestureDetector.OnGestureListener mFlingRemoveListener =
445             new GestureDetector.SimpleOnGestureListener() {
446                 @Override
447                 public final boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
448                         float velocityY) {
449                     // Log.d("mobeta", "on fling remove called");
450                     if (mRemoveEnabled && mIsRemoving) {
451                         int w = mDslv.getWidth();
452                         int minPos = w / 5;
453                         if (velocityX > mFlingSpeed) {
454                             if (mPositionX > -minPos) {
455                                 mDslv.stopDragWithVelocity(true, velocityX);
456                             }
457                         } else if (velocityX < -mFlingSpeed) {
458                             if (mPositionX < minPos) {
459                                 mDslv.stopDragWithVelocity(true, velocityX);
460                             }
461                         }
462                         mIsRemoving = false;
463                     }
464                     return false;
465                 }
466             };
467 
468 }
469