/* * * * Copyright 2015. Appsi Mobile * * * * 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.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; import android.widget.ListAdapter; /** * Class that starts and stops item drags on a {@link com.mobeta.android.dslv.DragSortListView} * based on touch gestures. This class also inherits from * {@link SimpleFloatViewManager}, which provides basic float View * creation. * <p/> * An instance of this class is meant to be passed to the methods * {@link com.mobeta.android.dslv.DragSortListView#setTouchListener()} and * {@link com.mobeta.android.dslv.DragSortListView#setFloatViewManager()} of your * {@link com.mobeta.android.dslv.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; boolean mRemoveEnabled = false; static final int REMOVE_MODE_WAITING = 0; static final int REMOVE_MODE_REMOVING = 1; static final int REMOVE_MODE_FAKE_REMOVING = 2; int mIsRemoving = REMOVE_MODE_WAITING; private float mMaxFakeRemoveDistance; 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; float mFlingSpeed; private int mDragHandleId; private int mClickRemoveId; private int mFlingHandleId; private boolean mCanDrag; DragSortListView mDslv; int mPositionX; /** * Calls {@link #DragSortController(com.mobeta.android.dslv.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; Context context = dslv.getContext(); mMaxFakeRemoveDistance = 16 * context.getResources().getDisplayMetrics().density; mDetector = new GestureDetector(context, this); mFlingRemoveDetector = new GestureDetector(context, mFlingRemoveListener); mFlingRemoveDetector.setIsLongpressEnabled(false); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mDragHandleId = dragHandleId; mClickRemoveId = clickRemoveId; mFlingHandleId = flingHandleId; mFlingSpeed = 1000 * context.getResources().getDisplayMetrics().density; 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 == REMOVE_MODE_WAITING) { dragFlags |= DragSortListView.DRAG_POS_Y | DragSortListView.DRAG_NEG_Y; } if (mRemoveEnabled && mIsRemoving != REMOVE_MODE_WAITING) { 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 != REMOVE_MODE_WAITING) { int x = Math.abs(mPositionX); int removePoint = mDslv.getWidth() / 2; if (x > removePoint && mIsRemoving == REMOVE_MODE_REMOVING) { mDslv.stopDragWithVelocity(true, 0); } } case MotionEvent.ACTION_CANCEL: mIsRemoving = REMOVE_MODE_WAITING; 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 != REMOVE_MODE_WAITING) { mPositionX = position.x; if (mIsRemoving == REMOVE_MODE_FAKE_REMOVING) { position.x = position.x / 3; } } } /** * Get the position to start dragging based on the ACTION_DOWN * MotionEvent. This function simply calls * {@link #dragHandleHitPosition(android.view.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 = REMOVE_MODE_WAITING; 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) { if (canRemove(mFlingHitPos)) { mIsRemoving = REMOVE_MODE_REMOVING; } else { mIsRemoving = REMOVE_MODE_FAKE_REMOVING; } startDrag(mFlingHitPos, deltaX, deltaY); } } else if (mFlingHitPos != MISS) { if (Math.abs(x2 - x1) > mTouchSlop && mRemoveEnabled) { if (canRemove(mFlingHitPos)) { mIsRemoving = REMOVE_MODE_REMOVING; } else { mIsRemoving = REMOVE_MODE_FAKE_REMOVING; } 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; } boolean canRemove(int flingHitPos) { ListAdapter adapter = mDslv.getInputAdapter(); if (adapter instanceof ConditionalRemovableAdapter) { ConditionalRemovableAdapter conditionalRemovableAdapter = (ConditionalRemovableAdapter) adapter; return conditionalRemovableAdapter.canRemove(flingHitPos); } return true; } @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 == REMOVE_MODE_REMOVING)) { 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 = REMOVE_MODE_WAITING; } return false; } }; }