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