├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── gradle.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── org │ │ └── buffer │ │ └── android │ │ └── multiactionswipehelper │ │ ├── ActionHelper.kt │ │ ├── ItemTouchUIUtilImpl.kt │ │ ├── SwipeAction.kt │ │ ├── SwipeActionListener.kt │ │ ├── SwipePositionItemTouchHelper.java │ │ └── SwipeToPerformActionCallback.kt │ └── res │ └── values │ └── dimens.xml ├── art └── demo.gif ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches/build_file_checksums.ser 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | .DS_Store 9 | /build 10 | /captures 11 | .externalNativeBuild 12 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MultiActionSwipeHelper 2 | An Android RecyclerView Swipe Helper for handling multiple actions per direction. The helper allows you to have 4 items in total: 3 | 4 | - Short left swipe 5 | - Long left swipe 6 | - Short right swipe 7 | - Long right swipe 8 | 9 | 10 | ![demo](https://github.com/bufferapp/MultiActionSwipeHelper/blob/master/art/demo.gif?raw=true) 11 | 12 | Sample app coming soon! 13 | 14 | # Usage 15 | 16 | The setup is fairly straightforward and requires little code. To begin with, you need to create a list of [SwipeAction](https://github.com/bufferapp/MultiActionSwipeHelper/blob/master/app/src/main/java/org/buffer/android/multiactionswipehelper/SwipeAction.kt) instances - these all provide information around the details for the display of the action (label, icon, color etc) 17 | 18 | val swipeActions = listOf() 19 | 20 | Next you need to create an instance of the [SwipeToPerformActionCallback](https://github.com/bufferapp/MultiActionSwipeHelper/blob/master/app/src/main/java/org/buffer/android/multiactionswipehelper/SwipeToPerformActionCallback.kt) class, this handles the magic around the display of the current action, as well as passing back which action should be performed when an item is swiped. 21 | 22 | val swipeHandler = SwipeToPerformActionCallback(swipeListener, some_margin_value, it) 23 | 24 | Finally, create an instance of the [SwipePositionItemTouchHelper](https://github.com/bufferapp/MultiActionSwipeHelper/blob/master/app/src/main/java/org/buffer/android/multiactionswipehelper/SwipePositionItemTouchHelper.java) class and attach it to your recycler view: 25 | 26 | SwipePositionItemTouchHelper(swipeHandler).attachToRecyclerView(recycler_conversations) 27 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'kotlin-android-extensions' 6 | 7 | android { 8 | compileSdkVersion 28 9 | defaultConfig { 10 | minSdkVersion 21 11 | targetSdkVersion 28 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | implementation fileTree(dir: 'libs', include: ['*.jar']) 23 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 24 | implementation 'com.android.support:appcompat-v7:28.0.0' 25 | implementation 'com.android.support:recyclerview-v7:28.0.0' 26 | } 27 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/java/org/buffer/android/multiactionswipehelper/ActionHelper.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.multiactionswipe 2 | 3 | object ActionHelper { 4 | 5 | fun getFirstActionWithDirection(actions: List, swipeDirection: Int) 6 | : SwipeAction? { 7 | return handleAction(actions, swipeDirection, primaryPosition = 0, fallBackPosition = 1) 8 | } 9 | 10 | fun getSecondActionWithDirection(actions: List, swipeDirection: Int) 11 | : SwipeAction? { 12 | return handleAction(actions, swipeDirection, primaryPosition = 1, fallBackPosition = 0) 13 | } 14 | 15 | fun handleAction(actions: List, swipeDirection: Int, 16 | primaryPosition: Int, fallBackPosition: Int): SwipeAction? { 17 | if (actions.size == 1) return actions[0] 18 | var action = actions.firstOrNull { 19 | it.actionPosition == primaryPosition && swipeDirection == it.swipeDirection 20 | } 21 | if (action == null) { 22 | action = actions.firstOrNull { 23 | it.actionPosition == fallBackPosition && swipeDirection == it.swipeDirection 24 | } 25 | } 26 | if (action == null) { 27 | action = actions.firstOrNull { it.actionPosition == primaryPosition } 28 | } 29 | if (action == null) { 30 | action = actions.firstOrNull { it.actionPosition == fallBackPosition } 31 | } 32 | return action 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/org/buffer/android/multiactionswipehelper/ItemTouchUIUtilImpl.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.multiactionswipe 2 | 3 | import android.graphics.Canvas 4 | import android.support.v4.view.ViewCompat 5 | import android.support.v7.widget.RecyclerView 6 | import android.support.v7.widget.helper.ItemTouchHelper 7 | import android.support.v7.widget.helper.ItemTouchUIUtil 8 | import android.view.View 9 | 10 | internal class ItemTouchUIUtilImpl { 11 | 12 | internal class Lollipop : Honeycomb() { 13 | override fun onDraw(c: Canvas, recyclerView: RecyclerView, view: View, 14 | dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) { 15 | if (isCurrentlyActive) { 16 | var originalElevation: Any? = view.getTag(R.id.item_touch_helper_previous_elevation) 17 | if (originalElevation == null) { 18 | originalElevation = ViewCompat.getElevation(view) 19 | val newElevation = 1f + findMaxElevation(recyclerView, view) 20 | ViewCompat.setElevation(view, newElevation) 21 | view.setTag(R.id.item_touch_helper_previous_elevation, originalElevation) 22 | } 23 | } 24 | super.onDraw(c, recyclerView, view, dX, dY, actionState, isCurrentlyActive) 25 | } 26 | 27 | private fun findMaxElevation(recyclerView: RecyclerView, itemView: View): Float { 28 | val childCount = recyclerView.childCount 29 | var max = 0f 30 | for (i in 0 until childCount) { 31 | val child = recyclerView.getChildAt(i) 32 | if (child === itemView) { 33 | continue 34 | } 35 | val elevation = ViewCompat.getElevation(child) 36 | if (elevation > max) { 37 | max = elevation 38 | } 39 | } 40 | return max 41 | } 42 | 43 | override fun clearView(view: View) { 44 | val tag = view.getTag(R.id.item_touch_helper_previous_elevation) 45 | if (tag != null && tag is Float) { 46 | ViewCompat.setElevation(view, tag) 47 | } 48 | view.setTag(R.id.item_touch_helper_previous_elevation, null) 49 | super.clearView(view) 50 | } 51 | } 52 | 53 | internal open class Honeycomb : ItemTouchUIUtil { 54 | 55 | override fun clearView(view: View) { 56 | ViewCompat.setTranslationX(view, 0f) 57 | ViewCompat.setTranslationY(view, 0f) 58 | } 59 | 60 | override fun onSelected(view: View) { 61 | 62 | } 63 | 64 | override fun onDraw(c: Canvas, recyclerView: RecyclerView, view: View, 65 | dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) { 66 | ViewCompat.setTranslationX(view, dX) 67 | ViewCompat.setTranslationY(view, dY) 68 | } 69 | 70 | override fun onDrawOver(c: Canvas, recyclerView: RecyclerView, view: View, dX: Float, 71 | dY: Float, actionState: Int, isCurrentlyActive: Boolean) { } 72 | } 73 | 74 | internal class Gingerbread : ItemTouchUIUtil { 75 | 76 | private fun draw(c: Canvas, parent: RecyclerView, view: View, 77 | dX: Float, dY: Float) { 78 | c.save() 79 | c.translate(dX, dY) 80 | parent.drawChild(c, view, 0) 81 | c.restore() 82 | } 83 | 84 | override fun clearView(view: View) { 85 | view.visibility = View.VISIBLE 86 | } 87 | 88 | override fun onSelected(view: View) { 89 | view.visibility = View.INVISIBLE 90 | } 91 | 92 | override fun onDraw(c: Canvas, recyclerView: RecyclerView, view: View, 93 | dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) { 94 | if (actionState != ItemTouchHelper.ACTION_STATE_DRAG) { 95 | draw(c, recyclerView, view, dX, dY) 96 | } 97 | } 98 | 99 | override fun onDrawOver(c: Canvas, recyclerView: RecyclerView, 100 | view: View, dX: Float, dY: Float, 101 | actionState: Int, isCurrentlyActive: Boolean) { 102 | if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) { 103 | draw(c, recyclerView, view, dX, dY) 104 | } 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /app/src/main/java/org/buffer/android/multiactionswipehelper/SwipeAction.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.multiactionswipe 2 | 3 | interface SwipeAction { 4 | 5 | val identifier: Int 6 | 7 | val actionPosition: Int 8 | 9 | val swipeDirection: Int 10 | 11 | val backgroundColor: Int 12 | 13 | val labelColor: Int 14 | 15 | val icon: Int 16 | 17 | } -------------------------------------------------------------------------------- /app/src/main/java/org/buffer/android/multiactionswipehelper/SwipeActionListener.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.multiactionswipe 2 | 3 | interface SwipeActionListener { 4 | fun onActionPerformed(itemPosition: Int, action: SwipeAction?) 5 | } -------------------------------------------------------------------------------- /app/src/main/java/org/buffer/android/multiactionswipehelper/SwipePositionItemTouchHelper.java: -------------------------------------------------------------------------------- 1 | package org.buffer.android.multiactionswipehelper; 2 | 3 | import android.animation.Animator; 4 | import android.animation.ValueAnimator; 5 | import android.content.res.Resources; 6 | import android.graphics.Canvas; 7 | import android.graphics.Rect; 8 | import android.os.Build; 9 | import android.support.annotation.Nullable; 10 | import android.support.v4.view.GestureDetectorCompat; 11 | import android.support.v4.view.MotionEventCompat; 12 | import android.support.v4.view.VelocityTrackerCompat; 13 | import android.support.v4.view.ViewCompat; 14 | import android.support.v7.widget.RecyclerView; 15 | import android.support.v7.widget.RecyclerView.ViewHolder; 16 | import android.support.v7.widget.helper.ItemTouchUIUtil; 17 | import android.util.Log; 18 | import android.view.GestureDetector; 19 | import android.view.HapticFeedbackConstants; 20 | import android.view.MotionEvent; 21 | import android.view.VelocityTracker; 22 | import android.view.View; 23 | import android.view.ViewConfiguration; 24 | import android.view.ViewGroup; 25 | import android.view.ViewParent; 26 | import android.view.animation.Interpolator; 27 | import org.buffer.android.multiactionswipe.ItemTouchUIUtilImpl; 28 | 29 | import java.util.ArrayList; 30 | import java.util.List; 31 | 32 | public class SwipePositionItemTouchHelper extends RecyclerView.ItemDecoration 33 | implements RecyclerView.OnChildAttachStateChangeListener { 34 | 35 | /** 36 | * Up direction, used for swipe & drag control. 37 | */ 38 | public static final int UP = 1; 39 | 40 | /** 41 | * Down direction, used for swipe & drag control. 42 | */ 43 | public static final int DOWN = 1 << 1; 44 | 45 | /** 46 | * Left direction, used for swipe & drag control. 47 | */ 48 | public static final int LEFT = 1 << 2; 49 | 50 | /** 51 | * Right direction, used for swipe & drag control. 52 | */ 53 | public static final int RIGHT = 1 << 3; 54 | 55 | // If you change these relative direction values, update Callback#convertToAbsoluteDirection, 56 | // Callback#convertToRelativeDirection. 57 | /** 58 | * Horizontal start direction. Resolved to LEFT or RIGHT depending on RecyclerView's layout 59 | * direction. Used for swipe & drag cwontrol. 60 | */ 61 | public static final int START = LEFT << 2; 62 | 63 | /** 64 | * Horizontal end direction. Resolved to LEFT or RIGHT depending on RecyclerView's layout 65 | * direction. Used for swipe & drag control. 66 | */ 67 | public static final int END = RIGHT << 2; 68 | 69 | /** 70 | * SwipePositionItemTouchHelper is in idle state. At this state, either there is no related motion event by 71 | * the user or latest motion events have not yet triggered a swipe or drag. 72 | */ 73 | public static final int ACTION_STATE_IDLE = 0; 74 | 75 | /** 76 | * A View is currently being swiped. 77 | */ 78 | public static final int ACTION_STATE_SWIPE = 1; 79 | 80 | /** 81 | * A View is currently being dragged. 82 | */ 83 | public static final int 84 | ACTION_STATE_DRAG = 2; 85 | 86 | /** 87 | * Animation type for views which are swiped successfully. 88 | */ 89 | public static final int ANIMATION_TYPE_SWIPE_SUCCESS = 1 << 1; 90 | 91 | /** 92 | * Animation type for views which are not completely swiped thus will animate back to their 93 | * original position. 94 | */ 95 | public static final int ANIMATION_TYPE_SWIPE_CANCEL = 1 << 2; 96 | 97 | /** 98 | * Animation type for views that were dragged and now will animate to their final position. 99 | */ 100 | public static final int ANIMATION_TYPE_DRAG = 1 << 3; 101 | 102 | private static final String TAG = "SwipePositionHelper"; 103 | 104 | private static final boolean DEBUG = false; 105 | 106 | private static final int ACTIVE_POINTER_ID_NONE = -1; 107 | 108 | private static final int DIRECTION_FLAG_COUNT = 8; 109 | 110 | private static final int ACTION_MODE_IDLE_MASK = (1 << DIRECTION_FLAG_COUNT) - 1; 111 | 112 | private static final int ACTION_MODE_SWIPE_MASK = ACTION_MODE_IDLE_MASK << DIRECTION_FLAG_COUNT; 113 | 114 | private static final int ACTION_MODE_DRAG_MASK = ACTION_MODE_SWIPE_MASK << DIRECTION_FLAG_COUNT; 115 | 116 | /** 117 | * The unit we are using to track velocity 118 | */ 119 | private static final int PIXELS_PER_SECOND = 1000; 120 | 121 | /** 122 | * Views, whose state should be cleared after they are detached from RecyclerView. 123 | * This is necessary after swipe dismissing an item. We wait until animator finishes its job 124 | * to clean these views. 125 | */ 126 | final List mPendingCleanup = new ArrayList(); 127 | 128 | /** 129 | * Re-use array to calculate dx dy for a ViewHolder 130 | */ 131 | private final float[] mTmpPosition = new float[2]; 132 | 133 | /** 134 | * Currently selected view holder 135 | */ 136 | RecyclerView.ViewHolder mSelected = null; 137 | RecyclerView.ViewHolder mPreOpened = null; 138 | 139 | /** 140 | * The reference coordinates for the action start. For drag & drop, this is the time long 141 | * press is completed vs for swipe, this is the initial touch point. 142 | */ 143 | float mInitialTouchX; 144 | 145 | float mInitialTouchY; 146 | 147 | /** 148 | * Set when SwipePositionItemTouchHelper is assigned to a RecyclerView. 149 | */ 150 | float mSwipeEscapeVelocity; 151 | 152 | /** 153 | * Set when SwipePositionItemTouchHelper is assigned to a RecyclerView. 154 | */ 155 | float mMaxSwipeVelocity; 156 | 157 | /** 158 | * The diff between the last event and initial touch. 159 | */ 160 | float mDx; 161 | 162 | float mDy; 163 | 164 | /** 165 | * The coordinates of the selected view at the time it is selected. We record these values 166 | * when action starts so that we can consistently position it even if LayoutManager moves the 167 | * View. 168 | */ 169 | float mSelectedStartX; 170 | 171 | float mSelectedStartY; 172 | 173 | /** 174 | * The pointer we are tracking. 175 | */ 176 | int mActivePointerId = ACTIVE_POINTER_ID_NONE; 177 | 178 | /** 179 | * Developer callback which controls the behavior of SwipePositionItemTouchHelper. 180 | */ 181 | Callback mCallback; 182 | 183 | /** 184 | * Current mode. 185 | */ 186 | int mActionState = ACTION_STATE_IDLE; 187 | 188 | /** 189 | * The direction flags obtained from unmasking 190 | * {@link Callback#getAbsoluteMovementFlags(RecyclerView, ViewHolder)} for the current 191 | * action state. 192 | */ 193 | int mSelectedFlags; 194 | 195 | /** 196 | * When a View is dragged or swiped and needs to go back to where it was, we create a Recover 197 | * Animation and animate it to its location using this custom Animator, instead of using 198 | * framework Animators. 199 | * Using framework animators has the side effect of clashing with ItemAnimator, creating 200 | * jumpy UIs. 201 | */ 202 | List mRecoverAnimations = new ArrayList(); 203 | 204 | private int mSlop; 205 | 206 | private RecyclerView mRecyclerView; 207 | 208 | /** 209 | * When user drags a view to the edge, we start scrolling the LayoutManager as long as View 210 | * is partially out of bounds. 211 | */ 212 | private final Runnable mScrollRunnable = new Runnable() { 213 | @Override 214 | public void run() { 215 | if (mSelected != null && scrollIfNecessary()) { 216 | if (mSelected != null) { //it might be lost during scrolling 217 | moveIfNecessary(mSelected); 218 | } 219 | mRecyclerView.removeCallbacks(mScrollRunnable); 220 | ViewCompat.postOnAnimation(mRecyclerView, this); 221 | } 222 | } 223 | }; 224 | 225 | /** 226 | * Used for detecting fling swipe 227 | */ 228 | private VelocityTracker mVelocityTracker; 229 | 230 | //re-used list for selecting a swap target 231 | private List mSwapTargets; 232 | 233 | //re used for for sorting swap targets 234 | private List mDistances; 235 | 236 | /** 237 | * If drag & drop is supported, we use child drawing order to bring them to front. 238 | */ 239 | private RecyclerView.ChildDrawingOrderCallback mChildDrawingOrderCallback = null; 240 | 241 | /** 242 | * This keeps a reference to the child dragged by the user. Even after user stops dragging, 243 | * until view reaches its final position (end of recover animation), we keep a reference so 244 | * that it can be drawn above other children. 245 | */ 246 | private View mOverdrawChild = null; 247 | 248 | /** 249 | * We cache the position of the overdraw child to avoid recalculating it each time child 250 | * position callback is called. This value is invalidated whenever a child is attached or 251 | * detached. 252 | */ 253 | private int mOverdrawChildPosition = -1; 254 | 255 | /** 256 | * Used to detect long press. 257 | */ 258 | private GestureDetectorCompat mGestureDetector; 259 | 260 | private final RecyclerView.OnItemTouchListener mOnItemTouchListener 261 | = new RecyclerView.OnItemTouchListener() { 262 | 263 | boolean mClick = false; 264 | float mLastX = 0; 265 | 266 | @Override 267 | public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) { 268 | mGestureDetector.onTouchEvent(event); 269 | if (DEBUG) { 270 | Log.d(TAG, "intercept: x:" + event.getX() + ",y:" + event.getY() + ", " + event); 271 | } 272 | final int action = MotionEventCompat.getActionMasked(event); 273 | if (action == MotionEvent.ACTION_DOWN) { 274 | mActivePointerId = MotionEventCompat.getPointerId(event, 0); 275 | mInitialTouchX = event.getX(); 276 | mInitialTouchY = event.getY(); 277 | 278 | 279 | mClick = true; 280 | mLastX = event.getX(); 281 | obtainVelocityTracker(); 282 | 283 | if (mSelected == null) { 284 | final RecoverAnimation animation = findAnimation(event); 285 | if (animation != null) { 286 | mInitialTouchX -= animation.mX; 287 | mInitialTouchY -= animation.mY; 288 | endRecoverAnimation(animation.mViewHolder, true); 289 | if (mPendingCleanup.remove(animation.mViewHolder.itemView)) { 290 | mCallback.clearView(mRecyclerView, animation.mViewHolder); 291 | } 292 | select(animation.mViewHolder, animation.mActionState); 293 | updateDxDy(event, mSelectedFlags, 0); 294 | } 295 | } 296 | } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { 297 | mActivePointerId = ACTIVE_POINTER_ID_NONE; 298 | if (mClick && action == MotionEvent.ACTION_UP) { 299 | doChildClickEvent(event.getRawX(), event.getRawY()); 300 | } 301 | select(null, ACTION_STATE_IDLE); 302 | } else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) { 303 | // in a non scroll orientation, if distance change is above threshold, we 304 | // can select the item 305 | final int index = MotionEventCompat.findPointerIndex(event, mActivePointerId); 306 | if (DEBUG) { 307 | Log.d(TAG, "pointer index " + index); 308 | } 309 | if (index >= 0) { 310 | checkSelectForSwipe(action, event, index); 311 | } 312 | } 313 | if (mVelocityTracker != null) { 314 | mVelocityTracker.addMovement(event); 315 | } 316 | return mSelected != null; 317 | } 318 | 319 | @Override 320 | public void onTouchEvent(RecyclerView recyclerView, MotionEvent event) { 321 | mGestureDetector.onTouchEvent(event); 322 | if (DEBUG) { 323 | Log.d(TAG, 324 | "on touch: x:" + mInitialTouchX + ",y:" + mInitialTouchY + ", :" + event); 325 | } 326 | if (mVelocityTracker != null) { 327 | mVelocityTracker.addMovement(event); 328 | } 329 | if (mActivePointerId == ACTIVE_POINTER_ID_NONE) { 330 | return; 331 | } 332 | final int action = MotionEventCompat.getActionMasked(event); 333 | final int activePointerIndex = MotionEventCompat 334 | .findPointerIndex(event, mActivePointerId); 335 | if (activePointerIndex >= 0) { 336 | checkSelectForSwipe(action, event, activePointerIndex); 337 | } 338 | ViewHolder viewHolder = mSelected; 339 | if (viewHolder == null) { 340 | return; 341 | } 342 | switch (action) { 343 | case MotionEvent.ACTION_MOVE: { 344 | 345 | // Find the index of the active pointer and fetch its position 346 | if (activePointerIndex >= 0) { 347 | updateDxDy(event, mSelectedFlags, activePointerIndex); 348 | if (Math.abs(event.getX() - mLastX) > mSlop) mClick = false; 349 | mLastX = event.getX(); 350 | moveIfNecessary(viewHolder); 351 | mRecyclerView.removeCallbacks(mScrollRunnable); 352 | mScrollRunnable.run(); 353 | mRecyclerView.invalidate(); 354 | } 355 | break; 356 | } 357 | case MotionEvent.ACTION_CANCEL: 358 | if (mVelocityTracker != null) { 359 | mVelocityTracker.clear(); 360 | } 361 | // fall through 362 | case MotionEvent.ACTION_UP: 363 | if (mClick) { 364 | doChildClickEvent(event.getRawX(), event.getRawY()); 365 | } 366 | mClick = false; 367 | select(null, ACTION_STATE_IDLE); 368 | mActivePointerId = ACTIVE_POINTER_ID_NONE; 369 | break; 370 | case MotionEvent.ACTION_POINTER_UP: { 371 | mClick = false; 372 | final int pointerIndex = MotionEventCompat.getActionIndex(event); 373 | final int pointerId = MotionEventCompat.getPointerId(event, pointerIndex); 374 | if (pointerId == mActivePointerId) { 375 | // This was our active pointer going up. Choose a new 376 | // active pointer and adjust accordingly. 377 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 378 | mActivePointerId = MotionEventCompat.getPointerId(event, newPointerIndex); 379 | updateDxDy(event, mSelectedFlags, pointerIndex); 380 | } 381 | break; 382 | } 383 | default: 384 | mClick = false; 385 | break; 386 | } 387 | } 388 | 389 | @Override 390 | public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { 391 | if (!disallowIntercept) { 392 | return; 393 | } 394 | select(null, ACTION_STATE_IDLE); 395 | } 396 | }; 397 | 398 | private void doChildClickEvent(float x, float y) { 399 | if (mSelected == null) return; 400 | View consumeEventView = mSelected.itemView; 401 | if (consumeEventView instanceof ViewGroup) { 402 | consumeEventView = findConsumeView((ViewGroup) consumeEventView, x, y); 403 | } 404 | if (consumeEventView != null) { 405 | consumeEventView.performClick(); 406 | } 407 | } 408 | 409 | private View findConsumeView(ViewGroup parent, float x, float y) { 410 | for (int i = 0; i < parent.getChildCount(); i++) { 411 | View child = parent.getChildAt(i); 412 | if (child instanceof ViewGroup && child.getVisibility() == View.VISIBLE) { 413 | View view = findConsumeView((ViewGroup) child, x, y); 414 | if (view != null) { 415 | return view; 416 | } 417 | } else { 418 | if (isInBoundsClickable((int) x, (int) y, child)) return child; 419 | } 420 | } 421 | if (isInBoundsClickable((int) x, (int) y, parent)) return parent; 422 | return null; 423 | } 424 | 425 | private boolean isInBoundsClickable(int x, int y, View child) { 426 | int[] location = new int[2]; 427 | child.getLocationOnScreen(location); 428 | Rect rect = new Rect(location[0], location[1], location[0] + child.getWidth(), location[1] + child.getHeight()); 429 | if (rect.contains(x, y) && ViewCompat.hasOnClickListeners(child) 430 | && child.getVisibility() == View.VISIBLE) { 431 | return true; 432 | } 433 | return false; 434 | } 435 | 436 | /** 437 | * Temporary rect instance that is used when we need to lookup Item decorations. 438 | */ 439 | private Rect mTmpRect; 440 | 441 | /** 442 | * When user started to drag scroll. Reset when we don't scroll 443 | */ 444 | private long mDragScrollStartTimeInMs; 445 | 446 | /** 447 | * Creates an SwipePositionItemTouchHelper that will work with the given Callback. 448 | *

449 | * You can attach SwipePositionItemTouchHelper to a RecyclerView via 450 | * {@link #attachToRecyclerView(RecyclerView)}. Upon attaching, it will add an item decoration, 451 | * an onItemTouchListener and a Child attach / detach listener to the RecyclerView. 452 | * 453 | * @param callback The Callback which controls the behavior of this touch helper. 454 | */ 455 | public SwipePositionItemTouchHelper(Callback callback) { 456 | mCallback = callback; 457 | } 458 | 459 | private static boolean hitTest(View child, float x, float y, float left, float top) { 460 | return x >= left && 461 | x <= left + child.getWidth() && 462 | y >= top && 463 | y <= top + child.getHeight(); 464 | } 465 | 466 | /** 467 | * Attaches the SwipePositionItemTouchHelper to the provided RecyclerView. If TouchHelper is already 468 | * attached to a RecyclerView, it will first detach from the previous one. You can call this 469 | * method with {@code null} to detach it from the current RecyclerView. 470 | * 471 | * @param recyclerView The RecyclerView instance to which you want to add this helper or 472 | * {@code null} if you want to remove SwipePositionItemTouchHelper from the current 473 | * RecyclerView. 474 | */ 475 | public void attachToRecyclerView(@Nullable RecyclerView recyclerView) { 476 | if (mRecyclerView == recyclerView) { 477 | return; // nothing to do 478 | } 479 | if (mRecyclerView != null) { 480 | destroyCallbacks(); 481 | } 482 | mRecyclerView = recyclerView; 483 | if (mRecyclerView != null) { 484 | final Resources resources = recyclerView.getResources(); 485 | mSwipeEscapeVelocity = resources 486 | .getDimension(R.dimen.item_touch_helper_swipe_escape_velocity); 487 | mMaxSwipeVelocity = resources 488 | .getDimension(R.dimen.item_touch_helper_swipe_escape_max_velocity); 489 | setupCallbacks(); 490 | } 491 | } 492 | 493 | private void setupCallbacks() { 494 | ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext()); 495 | mSlop = vc.getScaledTouchSlop(); 496 | mRecyclerView.addItemDecoration(this); 497 | mRecyclerView.addOnItemTouchListener(mOnItemTouchListener); 498 | mRecyclerView.addOnChildAttachStateChangeListener(this); 499 | initGestureDetector(); 500 | } 501 | 502 | private void destroyCallbacks() { 503 | mRecyclerView.removeItemDecoration(this); 504 | mRecyclerView.removeOnItemTouchListener(mOnItemTouchListener); 505 | mRecyclerView.removeOnChildAttachStateChangeListener(this); 506 | // clean all attached 507 | final int recoverAnimSize = mRecoverAnimations.size(); 508 | for (int i = recoverAnimSize - 1; i >= 0; i--) { 509 | final RecoverAnimation recoverAnimation = mRecoverAnimations.get(0); 510 | mCallback.clearView(mRecyclerView, recoverAnimation.mViewHolder); 511 | } 512 | mRecoverAnimations.clear(); 513 | mOverdrawChild = null; 514 | mOverdrawChildPosition = -1; 515 | releaseVelocityTracker(); 516 | } 517 | 518 | private void initGestureDetector() { 519 | if (mGestureDetector != null) { 520 | return; 521 | } 522 | mGestureDetector = new GestureDetectorCompat(mRecyclerView.getContext(), 523 | new ItemTouchHelperGestureListener()); 524 | } 525 | 526 | private void getSelectedDxDy(float[] outPosition) { 527 | if ((mSelectedFlags & (LEFT | RIGHT)) != 0) { 528 | outPosition[0] = mSelectedStartX + mDx - mSelected.itemView.getLeft(); 529 | } else { 530 | outPosition[0] = ViewCompat.getTranslationX(mSelected.itemView); 531 | } 532 | if ((mSelectedFlags & (UP | DOWN)) != 0) { 533 | outPosition[1] = mSelectedStartY + mDy - mSelected.itemView.getTop(); 534 | } else { 535 | outPosition[1] = ViewCompat.getTranslationY(mSelected.itemView); 536 | } 537 | } 538 | 539 | @Override 540 | public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { 541 | float dx = 0, dy = 0; 542 | if (mSelected != null) { 543 | getSelectedDxDy(mTmpPosition); 544 | dx = mTmpPosition[0]; 545 | dy = mTmpPosition[1]; 546 | } 547 | mCallback.onDrawOver(c, parent, mSelected, 548 | mRecoverAnimations, mActionState, dx, dy); 549 | } 550 | 551 | @Override 552 | public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { 553 | // we don't know if RV changed something so we should invalidate this index. 554 | mOverdrawChildPosition = -1; 555 | float dx = 0, dy = 0; 556 | if (mSelected != null) { 557 | getSelectedDxDy(mTmpPosition); 558 | dx = mTmpPosition[0]; 559 | dy = mTmpPosition[1]; 560 | } 561 | mCallback.onDraw(c, parent, mSelected, 562 | mRecoverAnimations, mActionState, dx, dy); 563 | } 564 | 565 | /** 566 | * Starts dragging or swiping the given View. Call with null if you want to clear it. 567 | * 568 | * @param selected The ViewHolder to drag or swipe. Can be null if you want to cancel the 569 | * current action 570 | * @param actionState The type of action 571 | */ 572 | private void select(ViewHolder selected, int actionState) { 573 | if (selected == mSelected && actionState == mActionState) { 574 | return; 575 | } 576 | mDragScrollStartTimeInMs = Long.MIN_VALUE; 577 | final int prevActionState = mActionState; 578 | // prevent duplicate animations 579 | endRecoverAnimation(selected, true); 580 | mActionState = actionState; 581 | if (actionState == ACTION_STATE_DRAG) { 582 | // we remove after animation is complete. this means we only elevate the last drag 583 | // child but that should perform good enough as it is very hard to start dragging a 584 | // new child before the previous one settles. 585 | mOverdrawChild = selected.itemView; 586 | addChildDrawingOrderCallback(); 587 | } 588 | int actionStateMask = (1 << (DIRECTION_FLAG_COUNT + DIRECTION_FLAG_COUNT * actionState)) 589 | - 1; 590 | boolean preventLayout = false; 591 | 592 | if (mSelected != null) { 593 | final ViewHolder prevSelected = mSelected; 594 | if (prevSelected.itemView.getParent() != null) { 595 | final int swipeDir = prevActionState == ACTION_STATE_DRAG ? 0 596 | : swipeIfNecessary(prevSelected); 597 | releaseVelocityTracker(); 598 | // find where we should animate to 599 | final float targetTranslateX, targetTranslateY; 600 | int animationType; 601 | switch (swipeDir) { 602 | case LEFT: 603 | case RIGHT: 604 | case START: 605 | case END: 606 | targetTranslateY = 0; 607 | targetTranslateX = Math.signum(mDx) * getSwipeWidth(); 608 | break; 609 | case UP: 610 | case DOWN: 611 | targetTranslateX = 0; 612 | targetTranslateY = Math.signum(mDy) * mRecyclerView.getHeight(); 613 | break; 614 | default: 615 | targetTranslateX = 0; 616 | targetTranslateY = 0; 617 | } 618 | if (prevActionState == ACTION_STATE_DRAG) { 619 | animationType = ANIMATION_TYPE_DRAG; 620 | } else if (swipeDir > 0) { 621 | animationType = ANIMATION_TYPE_SWIPE_SUCCESS; 622 | } else { 623 | animationType = ANIMATION_TYPE_SWIPE_CANCEL; 624 | } 625 | getSelectedDxDy(mTmpPosition); 626 | final float currentTranslateX = mTmpPosition[0]; 627 | final float currentTranslateY = mTmpPosition[1]; 628 | final RecoverAnimation rv = new RecoverAnimation(prevSelected, animationType, 629 | prevActionState, currentTranslateX, currentTranslateY, 630 | targetTranslateX, targetTranslateY) { 631 | @Override 632 | public void onAnimationEnd(Animator animation) { 633 | super.onAnimationEnd(animation); 634 | if (this.mOverridden) { 635 | return; 636 | } 637 | if (swipeDir <= 0) { 638 | // this is a drag or failed swipe. recover immediately 639 | mCallback.clearView(mRecyclerView, prevSelected); 640 | } else { 641 | // wait until remove animation is complete. 642 | mPendingCleanup.add(prevSelected.itemView); 643 | mPreOpened = prevSelected; 644 | mIsPendingCleanup = true; 645 | if (swipeDir > 0) { 646 | // Animation might be ended by other animators during a layout. 647 | // We defer callback to avoid editing adapter during a layout. 648 | postDispatchSwipe(this, swipeDir, currentTranslateX); 649 | } 650 | } 651 | // removed from the list after it is drawn for the last time 652 | if (mOverdrawChild == prevSelected.itemView) { 653 | removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView); 654 | } 655 | } 656 | }; 657 | final long duration = mCallback.getAnimationDuration(mRecyclerView, animationType, 658 | targetTranslateX - currentTranslateX, targetTranslateY - currentTranslateY); 659 | rv.setDuration(duration); 660 | mRecoverAnimations.add(rv); 661 | rv.start(); 662 | preventLayout = true; 663 | } else { 664 | removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView); 665 | mCallback.clearView(mRecyclerView, prevSelected); 666 | } 667 | mSelected = null; 668 | } 669 | if (selected != null) { 670 | mSelectedFlags = 671 | (mCallback.getAbsoluteMovementFlags(mRecyclerView, selected) & actionStateMask) 672 | >> (mActionState * DIRECTION_FLAG_COUNT); 673 | mSelectedStartX = selected.itemView.getLeft(); 674 | mSelectedStartY = selected.itemView.getTop(); 675 | mSelected = selected; 676 | 677 | if (actionState == ACTION_STATE_DRAG) { 678 | mSelected.itemView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 679 | } 680 | } 681 | final ViewParent rvParent = mRecyclerView.getParent(); 682 | if (rvParent != null) { 683 | rvParent.requestDisallowInterceptTouchEvent(mSelected != null); 684 | } 685 | if (!preventLayout) { 686 | mRecyclerView.getLayoutManager().requestSimpleAnimationsInNextLayout(); 687 | } 688 | mCallback.onSelectedChanged(mSelected, mActionState); 689 | mRecyclerView.invalidate(); 690 | } 691 | 692 | private float getSwipeWidth() { 693 | return mRecyclerView.getWidth(); 694 | } 695 | 696 | private void postDispatchSwipe(final RecoverAnimation anim, final int swipeDir, 697 | final float horizontalTouchPosition) { 698 | // wait until animations are complete. 699 | mRecyclerView.post(new Runnable() { 700 | @Override 701 | public void run() { 702 | if (mRecyclerView != null && mRecyclerView.isAttachedToWindow() && 703 | !anim.mOverridden && 704 | anim.mViewHolder.getAdapterPosition() != RecyclerView.NO_POSITION) { 705 | final RecyclerView.ItemAnimator animator = mRecyclerView.getItemAnimator(); 706 | // if animator is running or we have other active recover animations, we try 707 | // not to call onSwiped because DefaultItemAnimator is not good at merging 708 | // animations. Instead, we wait and batch. 709 | if ((animator == null || !animator.isRunning(null)) 710 | && !hasRunningRecoverAnim()) { 711 | mCallback.onSwiped(anim.mViewHolder, swipeDir, horizontalTouchPosition); 712 | } else { 713 | mRecyclerView.post(this); 714 | } 715 | } 716 | } 717 | }); 718 | } 719 | 720 | private boolean hasRunningRecoverAnim() { 721 | final int size = mRecoverAnimations.size(); 722 | for (int i = 0; i < size; i++) { 723 | if (!mRecoverAnimations.get(i).mEnded) { 724 | return true; 725 | } 726 | } 727 | return false; 728 | } 729 | 730 | /** 731 | * If user drags the view to the edge, trigger a scroll if necessary. 732 | */ 733 | private boolean scrollIfNecessary() { 734 | if (mSelected == null) { 735 | mDragScrollStartTimeInMs = Long.MIN_VALUE; 736 | return false; 737 | } 738 | final long now = System.currentTimeMillis(); 739 | final long scrollDuration = mDragScrollStartTimeInMs 740 | == Long.MIN_VALUE ? 0 : now - mDragScrollStartTimeInMs; 741 | RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); 742 | if (mTmpRect == null) { 743 | mTmpRect = new Rect(); 744 | } 745 | int scrollX = 0; 746 | int scrollY = 0; 747 | lm.calculateItemDecorationsForChild(mSelected.itemView, mTmpRect); 748 | if (lm.canScrollHorizontally()) { 749 | int curX = (int) (mSelectedStartX + mDx); 750 | final int leftDiff = curX - mTmpRect.left - mRecyclerView.getPaddingLeft(); 751 | if (mDx < 0 && leftDiff < 0) { 752 | scrollX = leftDiff; 753 | } else if (mDx > 0) { 754 | final int rightDiff = 755 | curX + mSelected.itemView.getWidth() + mTmpRect.right 756 | - (mRecyclerView.getWidth() - mRecyclerView.getPaddingRight()); 757 | if (rightDiff > 0) { 758 | scrollX = rightDiff; 759 | } 760 | } 761 | } 762 | if (lm.canScrollVertically()) { 763 | int curY = (int) (mSelectedStartY + mDy); 764 | final int topDiff = curY - mTmpRect.top - mRecyclerView.getPaddingTop(); 765 | if (mDy < 0 && topDiff < 0) { 766 | scrollY = topDiff; 767 | } else if (mDy > 0) { 768 | final int bottomDiff = curY + mSelected.itemView.getHeight() + mTmpRect.bottom - 769 | (mRecyclerView.getHeight() - mRecyclerView.getPaddingBottom()); 770 | if (bottomDiff > 0) { 771 | scrollY = bottomDiff; 772 | } 773 | } 774 | } 775 | if (scrollX != 0) { 776 | scrollX = mCallback.interpolateOutOfBoundsScroll(mRecyclerView, 777 | mSelected.itemView.getWidth(), scrollX, 778 | mRecyclerView.getWidth(), scrollDuration); 779 | } 780 | if (scrollY != 0) { 781 | scrollY = mCallback.interpolateOutOfBoundsScroll(mRecyclerView, 782 | mSelected.itemView.getHeight(), scrollY, 783 | mRecyclerView.getHeight(), scrollDuration); 784 | } 785 | if (scrollX != 0 || scrollY != 0) { 786 | if (mDragScrollStartTimeInMs == Long.MIN_VALUE) { 787 | mDragScrollStartTimeInMs = now; 788 | } 789 | mRecyclerView.scrollBy(scrollX, scrollY); 790 | return true; 791 | } 792 | mDragScrollStartTimeInMs = Long.MIN_VALUE; 793 | return false; 794 | } 795 | 796 | private List findSwapTargets(ViewHolder viewHolder) { 797 | if (mSwapTargets == null) { 798 | mSwapTargets = new ArrayList(); 799 | mDistances = new ArrayList(); 800 | } else { 801 | mSwapTargets.clear(); 802 | mDistances.clear(); 803 | } 804 | final int margin = mCallback.getBoundingBoxMargin(); 805 | final int left = Math.round(mSelectedStartX + mDx) - margin; 806 | final int top = Math.round(mSelectedStartY + mDy) - margin; 807 | final int right = left + viewHolder.itemView.getWidth() + 2 * margin; 808 | final int bottom = top + viewHolder.itemView.getHeight() + 2 * margin; 809 | final int centerX = (left + right) / 2; 810 | final int centerY = (top + bottom) / 2; 811 | final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); 812 | final int childCount = lm.getChildCount(); 813 | for (int i = 0; i < childCount; i++) { 814 | View other = lm.getChildAt(i); 815 | if (other == viewHolder.itemView) { 816 | continue;//myself! 817 | } 818 | if (other.getBottom() < top || other.getTop() > bottom 819 | || other.getRight() < left || other.getLeft() > right) { 820 | continue; 821 | } 822 | final ViewHolder otherVh = mRecyclerView.getChildViewHolder(other); 823 | if (mCallback.canDropOver(mRecyclerView, mSelected, otherVh)) { 824 | // find the index to add 825 | final int dx = Math.abs(centerX - (other.getLeft() + other.getRight()) / 2); 826 | final int dy = Math.abs(centerY - (other.getTop() + other.getBottom()) / 2); 827 | final int dist = dx * dx + dy * dy; 828 | 829 | int pos = 0; 830 | final int cnt = mSwapTargets.size(); 831 | for (int j = 0; j < cnt; j++) { 832 | if (dist > mDistances.get(j)) { 833 | pos++; 834 | } else { 835 | break; 836 | } 837 | } 838 | mSwapTargets.add(pos, otherVh); 839 | mDistances.add(pos, dist); 840 | } 841 | } 842 | return mSwapTargets; 843 | } 844 | 845 | /** 846 | * Checks if we should swap w/ another view holder. 847 | */ 848 | private void moveIfNecessary(ViewHolder viewHolder) { 849 | if (mRecyclerView.isLayoutRequested()) { 850 | return; 851 | } 852 | if (mActionState != ACTION_STATE_DRAG) { 853 | return; 854 | } 855 | 856 | final float threshold = mCallback.getMoveThreshold(viewHolder); 857 | final int x = (int) (mSelectedStartX + mDx); 858 | final int y = (int) (mSelectedStartY + mDy); 859 | if (Math.abs(y - viewHolder.itemView.getTop()) < viewHolder.itemView.getHeight() * threshold 860 | && Math.abs(x - viewHolder.itemView.getLeft()) 861 | < viewHolder.itemView.getWidth() * threshold) { 862 | return; 863 | } 864 | List swapTargets = findSwapTargets(viewHolder); 865 | if (swapTargets.size() == 0) { 866 | return; 867 | } 868 | // may swap. 869 | RecyclerView.ViewHolder target = mCallback.chooseDropTarget(viewHolder, swapTargets, x, y); 870 | if (target == null) { 871 | mSwapTargets.clear(); 872 | mDistances.clear(); 873 | return; 874 | } 875 | final int toPosition = target.getAdapterPosition(); 876 | final int fromPosition = viewHolder.getAdapterPosition(); 877 | if (mCallback.onMove(mRecyclerView, viewHolder, target)) { 878 | // keep target visible 879 | mCallback.onMoved(mRecyclerView, viewHolder, fromPosition, 880 | target, toPosition, x, y); 881 | } 882 | } 883 | 884 | @Override 885 | public void onChildViewAttachedToWindow(View view) { 886 | } 887 | 888 | @Override 889 | public void onChildViewDetachedFromWindow(View view) { 890 | removeChildDrawingOrderCallbackIfNecessary(view); 891 | final RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(view); 892 | if (holder == null) { 893 | return; 894 | } 895 | if (mSelected != null && holder == mSelected) { 896 | select(null, ACTION_STATE_IDLE); 897 | } else { 898 | endRecoverAnimation(holder, false); // this may push it into pending cleanup list. 899 | if (mPendingCleanup.remove(holder.itemView)) { 900 | mCallback.clearView(mRecyclerView, holder); 901 | } 902 | } 903 | } 904 | 905 | /** 906 | * Returns the animation type or 0 if cannot be found. 907 | */ 908 | private int endRecoverAnimation(RecyclerView.ViewHolder viewHolder, boolean override) { 909 | final int recoverAnimSize = mRecoverAnimations.size(); 910 | for (int i = recoverAnimSize - 1; i >= 0; i--) { 911 | final RecoverAnimation anim = mRecoverAnimations.get(i); 912 | if (anim.mViewHolder == viewHolder) { 913 | anim.mOverridden |= override; 914 | if (!anim.mEnded) { 915 | anim.cancel(); 916 | } 917 | mRecoverAnimations.remove(i); 918 | return anim.mAnimationType; 919 | } 920 | } 921 | return 0; 922 | } 923 | 924 | @Override 925 | public void getItemOffsets(Rect outRect, View view, RecyclerView parent, 926 | RecyclerView.State state) { 927 | outRect.setEmpty(); 928 | } 929 | 930 | private void obtainVelocityTracker() { 931 | if (mVelocityTracker != null) { 932 | mVelocityTracker.recycle(); 933 | } 934 | mVelocityTracker = VelocityTracker.obtain(); 935 | } 936 | 937 | private void releaseVelocityTracker() { 938 | if (mVelocityTracker != null) { 939 | mVelocityTracker.recycle(); 940 | mVelocityTracker = null; 941 | } 942 | } 943 | 944 | private ViewHolder findSwipedView(MotionEvent motionEvent) { 945 | final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); 946 | if (mActivePointerId == ACTIVE_POINTER_ID_NONE) { 947 | return null; 948 | } 949 | final int pointerIndex = MotionEventCompat.findPointerIndex(motionEvent, mActivePointerId); 950 | final float dx = MotionEventCompat.getX(motionEvent, pointerIndex) - mInitialTouchX; 951 | final float dy = MotionEventCompat.getY(motionEvent, pointerIndex) - mInitialTouchY; 952 | final float absDx = Math.abs(dx); 953 | final float absDy = Math.abs(dy); 954 | 955 | if (absDx < mSlop && absDy < mSlop) { 956 | return null; 957 | } 958 | if (absDx > absDy && lm.canScrollHorizontally()) { 959 | return null; 960 | } else if (absDy > absDx && lm.canScrollVertically()) { 961 | return null; 962 | } 963 | View child = findChildView(motionEvent); 964 | if (child == null) { 965 | return null; 966 | } 967 | return mRecyclerView.getChildViewHolder(child); 968 | } 969 | 970 | /** 971 | * Checks whether we should select a View for swiping. 972 | */ 973 | private boolean checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) { 974 | if (mSelected != null || action != MotionEvent.ACTION_MOVE 975 | || mActionState == ACTION_STATE_DRAG || !mCallback.isItemViewSwipeEnabled()) { 976 | return false; 977 | } 978 | if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) { 979 | return false; 980 | } 981 | final ViewHolder vh = findSwipedView(motionEvent); 982 | if (vh == null) { 983 | return false; 984 | } 985 | final int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh); 986 | 987 | final int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK) 988 | >> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE); 989 | 990 | if (swipeFlags == 0) { 991 | return false; 992 | } 993 | 994 | // mDx and mDy are only set in allowed directions. We use custom x/y here instead of 995 | // updateDxDy to avoid swiping if user moves more in the other direction 996 | final float x = MotionEventCompat.getX(motionEvent, pointerIndex); 997 | final float y = MotionEventCompat.getY(motionEvent, pointerIndex); 998 | 999 | // Calculate the distance moved 1000 | final float dx = x - mInitialTouchX; 1001 | final float dy = y - mInitialTouchY; 1002 | // swipe target is chose w/o applying flags so it does not really check if swiping in that 1003 | // direction is allowed. This why here, we use mDx mDy to check slope value again. 1004 | final float absDx = Math.abs(dx); 1005 | final float absDy = Math.abs(dy); 1006 | 1007 | if (absDx < mSlop && absDy < mSlop) { 1008 | return false; 1009 | } 1010 | if (absDx > absDy) { 1011 | if (dx < 0 && (swipeFlags & LEFT) == 0) { 1012 | return false; 1013 | } 1014 | if (dx > 0 && (swipeFlags & RIGHT) == 0) { 1015 | return false; 1016 | } 1017 | } else { 1018 | if (dy < 0 && (swipeFlags & UP) == 0) { 1019 | return false; 1020 | } 1021 | if (dy > 0 && (swipeFlags & DOWN) == 0) { 1022 | return false; 1023 | } 1024 | } 1025 | mDx = mDy = 0f; 1026 | mActivePointerId = MotionEventCompat.getPointerId(motionEvent, 0); 1027 | select(vh, ACTION_STATE_SWIPE); 1028 | return true; 1029 | } 1030 | 1031 | private View findChildView(MotionEvent event) { 1032 | // first check elevated views, if none, then call RV 1033 | final float x = event.getX(); 1034 | final float y = event.getY(); 1035 | if (mSelected != null) { 1036 | final View selectedView = mSelected.itemView; 1037 | if (hitTest(selectedView, x, y, mSelectedStartX + mDx, mSelectedStartY + mDy)) { 1038 | return selectedView; 1039 | } 1040 | } 1041 | for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) { 1042 | final RecoverAnimation anim = mRecoverAnimations.get(i); 1043 | final View view = anim.mViewHolder.itemView; 1044 | if (hitTest(view, x, y, anim.getHitX(), anim.getHitY())) { 1045 | return view; 1046 | } 1047 | } 1048 | return mRecyclerView.findChildViewUnder(x, y); 1049 | } 1050 | 1051 | public void startSwipe(ViewHolder viewHolder) { 1052 | if (!mCallback.hasSwipeFlag(mRecyclerView, viewHolder)) { 1053 | Log.e(TAG, "Start swipe has been called but dragging is not enabled"); 1054 | return; 1055 | } 1056 | if (viewHolder.itemView.getParent() != mRecyclerView) { 1057 | Log.e(TAG, "Start swipe has been called with a view holder which is not a child of " 1058 | + "the RecyclerView controlled by this SwipePositionItemTouchHelper."); 1059 | return; 1060 | } 1061 | obtainVelocityTracker(); 1062 | mDx = mDy = 0f; 1063 | select(viewHolder, ACTION_STATE_SWIPE); 1064 | } 1065 | 1066 | private RecoverAnimation findAnimation(MotionEvent event) { 1067 | if (mRecoverAnimations.isEmpty()) { 1068 | return null; 1069 | } 1070 | View target = findChildView(event); 1071 | for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) { 1072 | final RecoverAnimation anim = mRecoverAnimations.get(i); 1073 | if (anim.mViewHolder.itemView == target) { 1074 | return anim; 1075 | } 1076 | } 1077 | return null; 1078 | } 1079 | 1080 | private void updateDxDy(MotionEvent ev, int directionFlags, int pointerIndex) { 1081 | final float x = ev.getX(pointerIndex); 1082 | final float y = ev.getY(pointerIndex); 1083 | 1084 | // Calculate the distance moved 1085 | mDx = x - mInitialTouchX; 1086 | mDy = y - mInitialTouchY; 1087 | if ((directionFlags & LEFT) == 0) { 1088 | mDx = Math.max(0, mDx); 1089 | } 1090 | if ((directionFlags & RIGHT) == 0) { 1091 | mDx = Math.min(0, mDx); 1092 | } 1093 | if ((directionFlags & UP) == 0) { 1094 | mDy = Math.max(0, mDy); 1095 | } 1096 | if ((directionFlags & DOWN) == 0) { 1097 | mDy = Math.min(0, mDy); 1098 | } 1099 | } 1100 | 1101 | private int swipeIfNecessary(ViewHolder viewHolder) { 1102 | if (mActionState == ACTION_STATE_DRAG) { 1103 | return 0; 1104 | } 1105 | final int originalMovementFlags = mCallback.getMovementFlags(mRecyclerView, viewHolder); 1106 | final int absoluteMovementFlags = mCallback.convertToAbsoluteDirection( 1107 | originalMovementFlags, 1108 | ViewCompat.getLayoutDirection(mRecyclerView)); 1109 | final int flags = (absoluteMovementFlags 1110 | & ACTION_MODE_SWIPE_MASK) >> (ACTION_STATE_SWIPE * DIRECTION_FLAG_COUNT); 1111 | if (flags == 0) { 1112 | return 0; 1113 | } 1114 | final int originalFlags = (originalMovementFlags 1115 | & ACTION_MODE_SWIPE_MASK) >> (ACTION_STATE_SWIPE * DIRECTION_FLAG_COUNT); 1116 | int swipeDir; 1117 | if (Math.abs(mDx) > Math.abs(mDy)) { 1118 | if ((swipeDir = checkHorizontalSwipe(viewHolder, flags)) > 0) { 1119 | // if swipe dir is not in original flags, it should be the relative direction 1120 | if ((originalFlags & swipeDir) == 0) { 1121 | // convert to relative 1122 | return Callback.convertToRelativeDirection(swipeDir, 1123 | ViewCompat.getLayoutDirection(mRecyclerView)); 1124 | } 1125 | return swipeDir; 1126 | } 1127 | if ((swipeDir = checkVerticalSwipe(viewHolder, flags)) > 0) { 1128 | return swipeDir; 1129 | } 1130 | } else { 1131 | if ((swipeDir = checkVerticalSwipe(viewHolder, flags)) > 0) { 1132 | return swipeDir; 1133 | } 1134 | if ((swipeDir = checkHorizontalSwipe(viewHolder, flags)) > 0) { 1135 | // if swipe dir is not in original flags, it should be the relative direction 1136 | if ((originalFlags & swipeDir) == 0) { 1137 | // convert to relative 1138 | return Callback.convertToRelativeDirection(swipeDir, 1139 | ViewCompat.getLayoutDirection(mRecyclerView)); 1140 | } 1141 | return swipeDir; 1142 | } 1143 | } 1144 | return 0; 1145 | } 1146 | 1147 | private int checkHorizontalSwipe(ViewHolder viewHolder, int flags) { 1148 | if ((flags & (LEFT | RIGHT)) != 0) { 1149 | final int dirFlag = mDx > 0 ? RIGHT : LEFT; 1150 | if (mVelocityTracker != null && mActivePointerId > -1) { 1151 | mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, 1152 | mCallback.getSwipeVelocityThreshold(mMaxSwipeVelocity)); 1153 | final float xVelocity = VelocityTrackerCompat 1154 | .getXVelocity(mVelocityTracker, mActivePointerId); 1155 | final float yVelocity = VelocityTrackerCompat 1156 | .getYVelocity(mVelocityTracker, mActivePointerId); 1157 | final int velDirFlag = xVelocity > 0f ? RIGHT : LEFT; 1158 | final float absXVelocity = Math.abs(xVelocity); 1159 | if ((velDirFlag & flags) != 0 && dirFlag == velDirFlag && 1160 | absXVelocity >= mCallback.getSwipeEscapeVelocity(mSwipeEscapeVelocity) && 1161 | absXVelocity > Math.abs(yVelocity)) { 1162 | return velDirFlag; 1163 | } 1164 | } 1165 | 1166 | final float threshold = getSwipeWidth() * mCallback 1167 | .getSwipeThreshold(viewHolder); 1168 | 1169 | if ((flags & dirFlag) != 0 && Math.abs(mDx) > threshold) { 1170 | return dirFlag; 1171 | } 1172 | } 1173 | return 0; 1174 | } 1175 | 1176 | private int checkVerticalSwipe(ViewHolder viewHolder, int flags) { 1177 | if ((flags & (UP | DOWN)) != 0) { 1178 | final int dirFlag = mDy > 0 ? DOWN : UP; 1179 | if (mVelocityTracker != null && mActivePointerId > -1) { 1180 | mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, 1181 | mCallback.getSwipeVelocityThreshold(mMaxSwipeVelocity)); 1182 | final float xVelocity = VelocityTrackerCompat 1183 | .getXVelocity(mVelocityTracker, mActivePointerId); 1184 | final float yVelocity = VelocityTrackerCompat 1185 | .getYVelocity(mVelocityTracker, mActivePointerId); 1186 | final int velDirFlag = yVelocity > 0f ? DOWN : UP; 1187 | final float absYVelocity = Math.abs(yVelocity); 1188 | if ((velDirFlag & flags) != 0 && velDirFlag == dirFlag && 1189 | absYVelocity >= mCallback.getSwipeEscapeVelocity(mSwipeEscapeVelocity) && 1190 | absYVelocity > Math.abs(xVelocity)) { 1191 | return velDirFlag; 1192 | } 1193 | } 1194 | 1195 | final float threshold = mRecyclerView.getHeight() * mCallback 1196 | .getSwipeThreshold(viewHolder); 1197 | if ((flags & dirFlag) != 0 && Math.abs(mDy) > threshold) { 1198 | return dirFlag; 1199 | } 1200 | } 1201 | return 0; 1202 | } 1203 | 1204 | private void addChildDrawingOrderCallback() { 1205 | if (Build.VERSION.SDK_INT >= 21) { 1206 | return;// we use elevation on Lollipop 1207 | } 1208 | if (mChildDrawingOrderCallback == null) { 1209 | mChildDrawingOrderCallback = new RecyclerView.ChildDrawingOrderCallback() { 1210 | @Override 1211 | public int onGetChildDrawingOrder(int childCount, int i) { 1212 | if (mOverdrawChild == null) { 1213 | return i; 1214 | } 1215 | int childPosition = mOverdrawChildPosition; 1216 | if (childPosition == -1) { 1217 | childPosition = mRecyclerView.indexOfChild(mOverdrawChild); 1218 | mOverdrawChildPosition = childPosition; 1219 | } 1220 | if (i == childCount - 1) { 1221 | return childPosition; 1222 | } 1223 | return i < childPosition ? i : i + 1; 1224 | } 1225 | }; 1226 | } 1227 | mRecyclerView.setChildDrawingOrderCallback(mChildDrawingOrderCallback); 1228 | } 1229 | 1230 | private void removeChildDrawingOrderCallbackIfNecessary(View view) { 1231 | if (view == mOverdrawChild) { 1232 | mOverdrawChild = null; 1233 | // only remove if we've added 1234 | if (mChildDrawingOrderCallback != null) { 1235 | mRecyclerView.setChildDrawingOrderCallback(null); 1236 | } 1237 | } 1238 | } 1239 | 1240 | public static interface ViewDropHandler { 1241 | 1242 | 1243 | public void prepareForDrop(View view, View target, int x, int y); 1244 | } 1245 | 1246 | /** 1247 | * This class is the contract between SwipePositionItemTouchHelper and your application. It lets you control 1248 | * which touch behaviors are enabled per each ViewHolder and also receive callbacks when user 1249 | * performs these actions. 1250 | * If user drags an item, SwipePositionItemTouchHelper will call 1251 | * {@link Callback#onMove(RecyclerView, ViewHolder, ViewHolder) 1252 | * onMove(recyclerView, dragged, target)}. 1253 | * Upon receiving this callback, you should move the item from the old position 1254 | * ({@code dragged.getAdapterPosition()}) to new position ({@code target.getAdapterPosition()}) 1255 | * in your adapter and also call {@link RecyclerView.Adapter#notifyItemMoved(int, int)}. 1256 | * To control where a View can be dropped, you can override 1257 | * {@link #canDropOver(RecyclerView, ViewHolder, ViewHolder)}. When a 1258 | * dragging View overlaps multiple other views, Callback chooses the closest View with which 1259 | * dragged View might have changed positions. Although this approach works for many use cases, 1260 | * if you have a custom LayoutManager, you can override 1261 | * {@link #chooseDropTarget(ViewHolder, List, int, int)} to select a 1262 | * custom drop target. 1263 | *

1264 | */ 1265 | @SuppressWarnings("UnusedParameters") 1266 | public abstract static class Callback { 1267 | 1268 | public static final int DEFAULT_DRAG_ANIMATION_DURATION = 200; 1269 | 1270 | public static final int DEFAULT_SWIPE_ANIMATION_DURATION = 250; 1271 | 1272 | static final int RELATIVE_DIR_FLAGS = START | END | 1273 | ((START | END) << DIRECTION_FLAG_COUNT) | 1274 | ((START | END) << (2 * DIRECTION_FLAG_COUNT)); 1275 | 1276 | private static final ItemTouchUIUtil sUICallback; 1277 | 1278 | private static final int ABS_HORIZONTAL_DIR_FLAGS = LEFT | RIGHT | 1279 | ((LEFT | RIGHT) << DIRECTION_FLAG_COUNT) | 1280 | ((LEFT | RIGHT) << (2 * DIRECTION_FLAG_COUNT)); 1281 | 1282 | private static final Interpolator sDragScrollInterpolator = new Interpolator() { 1283 | public float getInterpolation(float t) { 1284 | return t * t * t * t * t; 1285 | } 1286 | }; 1287 | 1288 | private static final Interpolator sDragViewScrollCapInterpolator = new Interpolator() { 1289 | public float getInterpolation(float t) { 1290 | t -= 1.0f; 1291 | return t * t * t * t * t + 1.0f; 1292 | } 1293 | }; 1294 | 1295 | /** 1296 | * Drag scroll speed keeps accelerating until this many milliseconds before being capped. 1297 | */ 1298 | private static final long DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000; 1299 | 1300 | private int mCachedMaxScrollSpeed = -1; 1301 | 1302 | static { 1303 | if (Build.VERSION.SDK_INT >= 21) { 1304 | sUICallback = new ItemTouchUIUtilImpl.Lollipop(); 1305 | } else if (Build.VERSION.SDK_INT >= 11) { 1306 | sUICallback = new ItemTouchUIUtilImpl.Honeycomb(); 1307 | } else { 1308 | sUICallback = new ItemTouchUIUtilImpl.Gingerbread(); 1309 | } 1310 | } 1311 | 1312 | /** 1313 | * Replaces a movement direction with its relative version by taking layout direction into 1314 | * account. 1315 | * 1316 | * @param flags The flag value that include any number of movement flags. 1317 | * @param layoutDirection The layout direction of the View. Can be obtained from 1318 | * {@link ViewCompat#getLayoutDirection(View)}. 1319 | * @return Updated flags which uses relative flags ({@link #START}, {@link #END}) instead 1320 | * of {@link #LEFT}, {@link #RIGHT}. 1321 | * @see #convertToAbsoluteDirection(int, int) 1322 | */ 1323 | public static int convertToRelativeDirection(int flags, int layoutDirection) { 1324 | int masked = flags & ABS_HORIZONTAL_DIR_FLAGS; 1325 | if (masked == 0) { 1326 | return flags;// does not have any abs flags, good. 1327 | } 1328 | flags &= ~masked; //remove left / right. 1329 | if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_LTR) { 1330 | // no change. just OR with 2 bits shifted mask and return 1331 | flags |= masked << 2; // START is 2 bits after LEFT, END is 2 bits after RIGHT. 1332 | return flags; 1333 | } else { 1334 | // add RIGHT flag as START 1335 | flags |= ((masked << 1) & ~ABS_HORIZONTAL_DIR_FLAGS); 1336 | // first clean RIGHT bit then add LEFT flag as END 1337 | flags |= ((masked << 1) & ABS_HORIZONTAL_DIR_FLAGS) << 2; 1338 | } 1339 | return flags; 1340 | } 1341 | 1342 | /** 1343 | * Convenience method to create movement flags. 1344 | *

1345 | * For instance, if you want to let your items be drag & dropped vertically and swiped 1346 | * left to be dismissed, you can call this method with: 1347 | * makeMovementFlags(UP | DOWN, LEFT); 1348 | * 1349 | * @param dragFlags The directions in which the item can be dragged. 1350 | * @param swipeFlags The directions in which the item can be swiped. 1351 | * @return Returns an integer composed of the given drag and swipe flags. 1352 | */ 1353 | public static int makeMovementFlags(int dragFlags, int swipeFlags) { 1354 | return makeFlag(ACTION_STATE_IDLE, swipeFlags | dragFlags) | 1355 | makeFlag(ACTION_STATE_SWIPE, swipeFlags) | makeFlag(ACTION_STATE_DRAG, 1356 | dragFlags); 1357 | } 1358 | 1359 | /** 1360 | * Shifts the given direction flags to the offset of the given action state. 1361 | * 1362 | * @param actionState The action state you want to get flags in. Should be one of 1363 | * {@link #ACTION_STATE_IDLE}, {@link #ACTION_STATE_SWIPE} or 1364 | * {@link #ACTION_STATE_DRAG}. 1365 | * @param directions The direction flags. Can be composed from {@link #UP}, {@link #DOWN}, 1366 | * {@link #RIGHT}, {@link #LEFT} {@link #START} and {@link #END}. 1367 | * @return And integer that represents the given directions in the provided actionState. 1368 | */ 1369 | public static int makeFlag(int actionState, int directions) { 1370 | return directions << (actionState * DIRECTION_FLAG_COUNT); 1371 | } 1372 | 1373 | public abstract int getMovementFlags(RecyclerView recyclerView, 1374 | ViewHolder viewHolder); 1375 | 1376 | /** 1377 | * Converts a given set of flags to absolution direction which means {@link #START} and 1378 | * {@link #END} are replaced with {@link #LEFT} and {@link #RIGHT} depending on the layout 1379 | * direction. 1380 | * 1381 | * @param flags The flag value that include any number of movement flags. 1382 | * @param layoutDirection The layout direction of the RecyclerView. 1383 | * @return Updated flags which includes only absolute direction values. 1384 | */ 1385 | public int convertToAbsoluteDirection(int flags, int layoutDirection) { 1386 | int masked = flags & RELATIVE_DIR_FLAGS; 1387 | if (masked == 0) { 1388 | return flags;// does not have any relative flags, good. 1389 | } 1390 | flags &= ~masked; //remove start / end 1391 | if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_LTR) { 1392 | // no change. just OR with 2 bits shifted mask and return 1393 | flags |= masked >> 2; // START is 2 bits after LEFT, END is 2 bits after RIGHT. 1394 | return flags; 1395 | } else { 1396 | // add START flag as RIGHT 1397 | flags |= ((masked >> 1) & ~RELATIVE_DIR_FLAGS); 1398 | // first clean start bit then add END flag as LEFT 1399 | flags |= ((masked >> 1) & RELATIVE_DIR_FLAGS) >> 2; 1400 | } 1401 | return flags; 1402 | } 1403 | 1404 | final int getAbsoluteMovementFlags(RecyclerView recyclerView, 1405 | ViewHolder viewHolder) { 1406 | final int flags = getMovementFlags(recyclerView, viewHolder); 1407 | return convertToAbsoluteDirection(flags, ViewCompat.getLayoutDirection(recyclerView)); 1408 | } 1409 | 1410 | private boolean hasDragFlag(RecyclerView recyclerView, ViewHolder viewHolder) { 1411 | final int flags = getAbsoluteMovementFlags(recyclerView, viewHolder); 1412 | return (flags & ACTION_MODE_DRAG_MASK) != 0; 1413 | } 1414 | 1415 | private boolean hasSwipeFlag(RecyclerView recyclerView, 1416 | ViewHolder viewHolder) { 1417 | final int flags = getAbsoluteMovementFlags(recyclerView, viewHolder); 1418 | return (flags & ACTION_MODE_SWIPE_MASK) != 0; 1419 | } 1420 | 1421 | /** 1422 | * Return true if the current ViewHolder can be dropped over the the target ViewHolder. 1423 | *

1424 | * This method is used when selecting drop target for the dragged View. After Views are 1425 | * eliminated either via bounds check or via this method, resulting set of views will be 1426 | * passed to {@link #chooseDropTarget(ViewHolder, List, int, int)}. 1427 | *

1428 | * Default implementation returns true. 1429 | * 1430 | * @param recyclerView The RecyclerView to which SwipePositionItemTouchHelper is attached to. 1431 | * @param current The ViewHolder that user is dragging. 1432 | * @param target The ViewHolder which is below the dragged ViewHolder. 1433 | * @return True if the dragged ViewHolder can be replaced with the target ViewHolder, false 1434 | * otherwise. 1435 | */ 1436 | public boolean canDropOver(RecyclerView recyclerView, ViewHolder current, 1437 | ViewHolder target) { 1438 | return true; 1439 | } 1440 | 1441 | /** 1442 | * Called when SwipePositionItemTouchHelper wants to move the dragged item from its old position to 1443 | * the new position. 1444 | *

1445 | * If this method returns true, SwipePositionItemTouchHelper assumes {@code viewHolder} has been moved 1446 | * to the adapter position of {@code target} ViewHolder 1447 | * ({@link ViewHolder#getAdapterPosition() 1448 | * ViewHolder#getAdapterPosition()}). 1449 | *

1450 | * If you don't support drag & drop, this method will never be called. 1451 | * 1452 | * @param recyclerView The RecyclerView to which SwipePositionItemTouchHelper is attached to. 1453 | * @param viewHolder The ViewHolder which is being dragged by the user. 1454 | * @param target The ViewHolder over which the currently active item is being 1455 | * dragged. 1456 | * @return True if the {@code viewHolder} has been moved to the adapter position of 1457 | * {@code target}. 1458 | * @see #onMoved(RecyclerView, ViewHolder, int, ViewHolder, int, int, int) 1459 | */ 1460 | public abstract boolean onMove(RecyclerView recyclerView, 1461 | ViewHolder viewHolder, ViewHolder target); 1462 | 1463 | 1464 | public boolean isLongPressDragEnabled() { 1465 | return true; 1466 | } 1467 | 1468 | /** 1469 | * Returns whether SwipePositionItemTouchHelper should start a swipe operation if a pointer is swiped 1470 | * over the View. 1471 | *

1472 | * Default value returns true but you may want to disable this if you want to start 1473 | * swiping on a custom view touch using {@link #startSwipe(ViewHolder)}. 1474 | * 1475 | * @return True if SwipePositionItemTouchHelper should start swiping an item when user swipes a pointer 1476 | * over the View, false otherwise. Default value is true. 1477 | * @see #startSwipe(ViewHolder) 1478 | */ 1479 | public boolean isItemViewSwipeEnabled() { 1480 | return true; 1481 | } 1482 | 1483 | /** 1484 | * When finding views under a dragged view, by default, SwipePositionItemTouchHelper searches for views 1485 | * that overlap with the dragged View. By overriding this method, you can extend or shrink 1486 | * the search box. 1487 | * 1488 | * @return The extra margin to be added to the hit box of the dragged View. 1489 | */ 1490 | public int getBoundingBoxMargin() { 1491 | return 0; 1492 | } 1493 | 1494 | /** 1495 | * Returns the fraction that the user should move the View to be considered as swiped. 1496 | * The fraction is calculated with respect to RecyclerView's bounds. 1497 | *

1498 | * Default value is .5f, which means, to swipe a View, user must move the View at least 1499 | * half of RecyclerView's width or height, depending on the swipe direction. 1500 | * 1501 | * @param viewHolder The ViewHolder that is being dragged. 1502 | * @return A float value that denotes the fraction of the View size. Default value 1503 | * is .5f . 1504 | */ 1505 | public float getSwipeThreshold(ViewHolder viewHolder) { 1506 | return .5f; 1507 | } 1508 | 1509 | /** 1510 | * Returns the fraction that the user should move the View to be considered as it is 1511 | * dragged. After a view is moved this amount, SwipePositionItemTouchHelper starts checking for Views 1512 | * below it for a possible drop. 1513 | * 1514 | * @param viewHolder The ViewHolder that is being dragged. 1515 | * @return A float value that denotes the fraction of the View size. Default value is 1516 | * .5f . 1517 | */ 1518 | public float getMoveThreshold(ViewHolder viewHolder) { 1519 | return .5f; 1520 | } 1521 | 1522 | /** 1523 | * Defines the minimum velocity which will be considered as a swipe action by the user. 1524 | *

1525 | * You can increase this value to make it harder to swipe or decrease it to make it easier. 1526 | * Keep in mind that SwipePositionItemTouchHelper also checks the perpendicular velocity and makes sure 1527 | * current direction velocity is larger then the perpendicular one. Otherwise, user's 1528 | * movement is ambiguous. You can change the threshold by overriding 1529 | * {@link #getSwipeVelocityThreshold(float)}. 1530 | *

1531 | * The velocity is calculated in pixels per second. 1532 | *

1533 | * The default framework value is passed as a parameter so that you can modify it with a 1534 | * multiplier. 1535 | * 1536 | * @param defaultValue The default value (in pixels per second) used by the 1537 | * SwipePositionItemTouchHelper. 1538 | * @return The minimum swipe velocity. The default implementation returns the 1539 | * defaultValue parameter. 1540 | * @see #getSwipeVelocityThreshold(float) 1541 | * @see #getSwipeThreshold(ViewHolder) 1542 | */ 1543 | public float getSwipeEscapeVelocity(float defaultValue) { 1544 | return defaultValue; 1545 | } 1546 | 1547 | /** 1548 | * Defines the maximum velocity SwipePositionItemTouchHelper will ever calculate for pointer movements. 1549 | *

1550 | * To consider a movement as swipe, SwipePositionItemTouchHelper requires it to be larger than the 1551 | * perpendicular movement. If both directions reach to the max threshold, none of them will 1552 | * be considered as a swipe because it is usually an indication that user rather tried to 1553 | * scroll then swipe. 1554 | *

1555 | * The velocity is calculated in pixels per second. 1556 | *

1557 | * You can customize this behavior by changing this method. If you increase the value, it 1558 | * will be easier for the user to swipe diagonally and if you decrease the value, user will 1559 | * need to make a rather straight finger movement to trigger a swipe. 1560 | * 1561 | * @param defaultValue The default value(in pixels per second) used by the SwipePositionItemTouchHelper. 1562 | * @return The velocity cap for pointer movements. The default implementation returns the 1563 | * defaultValue parameter. 1564 | * @see #getSwipeEscapeVelocity(float) 1565 | */ 1566 | public float getSwipeVelocityThreshold(float defaultValue) { 1567 | return defaultValue; 1568 | } 1569 | 1570 | /** 1571 | * Called by SwipePositionItemTouchHelper to select a drop target from the list of ViewHolders that 1572 | * are under the dragged View. 1573 | *

1574 | * Default implementation filters the View with which dragged item have changed position 1575 | * in the drag direction. For instance, if the view is dragged UP, it compares the 1576 | * view.getTop() of the two views before and after drag started. If that value 1577 | * is different, the target view passes the filter. 1578 | *

1579 | * Among these Views which pass the test, the one closest to the dragged view is chosen. 1580 | *

1581 | * This method is called on the main thread every time user moves the View. If you want to 1582 | * override it, make sure it does not do any expensive operations. 1583 | * 1584 | * @param selected The ViewHolder being dragged by the user. 1585 | * @param dropTargets The list of ViewHolder that are under the dragged View and 1586 | * candidate as a drop. 1587 | * @param curX The updated left value of the dragged View after drag translations 1588 | * are applied. This value does not include margins added by 1589 | * {@link RecyclerView.ItemDecoration}s. 1590 | * @param curY The updated top value of the dragged View after drag translations 1591 | * are applied. This value does not include margins added by 1592 | * {@link RecyclerView.ItemDecoration}s. 1593 | * @return A FolderViewHolder to whose position the dragged FolderViewHolder should be 1594 | * moved to. 1595 | */ 1596 | public ViewHolder chooseDropTarget(ViewHolder selected, 1597 | List dropTargets, int curX, int curY) { 1598 | int right = curX + selected.itemView.getWidth(); 1599 | int bottom = curY + selected.itemView.getHeight(); 1600 | ViewHolder winner = null; 1601 | int winnerScore = -1; 1602 | final int dx = curX - selected.itemView.getLeft(); 1603 | final int dy = curY - selected.itemView.getTop(); 1604 | final int targetsSize = dropTargets.size(); 1605 | for (int i = 0; i < targetsSize; i++) { 1606 | final ViewHolder target = dropTargets.get(i); 1607 | if (dx > 0) { 1608 | int diff = target.itemView.getRight() - right; 1609 | if (diff < 0 && target.itemView.getRight() > selected.itemView.getRight()) { 1610 | final int score = Math.abs(diff); 1611 | if (score > winnerScore) { 1612 | winnerScore = score; 1613 | winner = target; 1614 | } 1615 | } 1616 | } 1617 | if (dx < 0) { 1618 | int diff = target.itemView.getLeft() - curX; 1619 | if (diff > 0 && target.itemView.getLeft() < selected.itemView.getLeft()) { 1620 | final int score = Math.abs(diff); 1621 | if (score > winnerScore) { 1622 | winnerScore = score; 1623 | winner = target; 1624 | } 1625 | } 1626 | } 1627 | if (dy < 0) { 1628 | int diff = target.itemView.getTop() - curY; 1629 | if (diff > 0 && target.itemView.getTop() < selected.itemView.getTop()) { 1630 | final int score = Math.abs(diff); 1631 | if (score > winnerScore) { 1632 | winnerScore = score; 1633 | winner = target; 1634 | } 1635 | } 1636 | } 1637 | 1638 | if (dy > 0) { 1639 | int diff = target.itemView.getBottom() - bottom; 1640 | if (diff < 0 && target.itemView.getBottom() > selected.itemView.getBottom()) { 1641 | final int score = Math.abs(diff); 1642 | if (score > winnerScore) { 1643 | winnerScore = score; 1644 | winner = target; 1645 | } 1646 | } 1647 | } 1648 | } 1649 | return winner; 1650 | } 1651 | 1652 | /** 1653 | * Called when a ViewHolder is swiped by the user. 1654 | *

1655 | * If you are returning relative directions ({@link #START} , {@link #END}) from the 1656 | * {@link #getMovementFlags(RecyclerView, ViewHolder)} method, this method 1657 | * will also use relative directions. Otherwise, it will use absolute directions. 1658 | *

1659 | * If you don't support swiping, this method will never be called. 1660 | *

1661 | * SwipePositionItemTouchHelper will keep a reference to the View until it is detached from 1662 | * RecyclerView. 1663 | * As soon as it is detached, SwipePositionItemTouchHelper will call 1664 | * {@link #clearView(RecyclerView, ViewHolder)}. 1665 | * 1666 | * @param viewHolder The ViewHolder which has been swiped by the user. 1667 | * @param direction The direction to which the ViewHolder is swiped. It is one of 1668 | * {@link #UP}, {@link #DOWN}, 1669 | * {@link #LEFT} or {@link #RIGHT}. If your 1670 | * {@link #getMovementFlags(RecyclerView, ViewHolder)} 1671 | * method 1672 | * returned relative flags instead of {@link #LEFT} / {@link #RIGHT}; 1673 | * `direction` will be relative as well. ({@link #START} or {@link 1674 | * #END}). 1675 | */ 1676 | public abstract void onSwiped(ViewHolder viewHolder, int direction, 1677 | float horizontalTouchPosition); 1678 | 1679 | 1680 | /** 1681 | * @param viewHolder this is pre action viewHolder, there we think view has two child 1682 | * first one is back action view.Front is show view. 1683 | * @return 1684 | */ 1685 | public View getItemFrontView(ViewHolder viewHolder) { 1686 | if (viewHolder == null) return null; 1687 | if (viewHolder.itemView instanceof ViewGroup && ((ViewGroup) viewHolder.itemView).getChildCount() > 1) { 1688 | ViewGroup viewGroup = (ViewGroup) viewHolder.itemView; 1689 | return viewGroup.getChildAt(viewGroup.getChildCount() - 1); 1690 | } else { 1691 | return viewHolder.itemView; 1692 | } 1693 | } 1694 | 1695 | public void onSelectedChanged(ViewHolder viewHolder, int actionState) { 1696 | if (viewHolder != null) { 1697 | sUICallback.onSelected(viewHolder.itemView); 1698 | } 1699 | } 1700 | 1701 | private int getMaxDragScroll(RecyclerView recyclerView) { 1702 | if (mCachedMaxScrollSpeed == -1) { 1703 | mCachedMaxScrollSpeed = recyclerView.getResources().getDimensionPixelSize( 1704 | R.dimen.item_touch_helper_max_drag_scroll_per_frame); 1705 | } 1706 | return mCachedMaxScrollSpeed; 1707 | } 1708 | 1709 | /** 1710 | * Called when {@link #onMove(RecyclerView, ViewHolder, ViewHolder)} returns true. 1711 | *

1712 | * SwipePositionItemTouchHelper does not create an extra Bitmap or View while dragging, instead, it 1713 | * modifies the existing View. Because of this reason, it is important that the View is 1714 | * still part of the layout after it is moved. This may not work as intended when swapped 1715 | * Views are close to RecyclerView bounds or there are gaps between them (e.g. other Views 1716 | * which were not eligible for dropping over). 1717 | *

1718 | * This method is responsible to give necessary hint to the LayoutManager so that it will 1719 | * keep the View in visible area. For example, for LinearLayoutManager, this is as simple 1720 | *

1721 | *

1722 | * Default implementation calls {@link RecyclerView#scrollToPosition(int)} if the View's 1723 | * new position is likely to be out of bounds. 1724 | *

1725 | * It is important to ensure the ViewHolder will stay visible as otherwise, it might be 1726 | * removed by the LayoutManager if the move causes the View to go out of bounds. In that 1727 | * case, drag will end prematurely. 1728 | * 1729 | * @param recyclerView The RecyclerView controlled by the SwipePositionItemTouchHelper. 1730 | * @param viewHolder The ViewHolder under user's control. 1731 | * @param fromPos The previous adapter position of the dragged item (before it was 1732 | * moved). 1733 | * @param target The ViewHolder on which the currently active item has been dropped. 1734 | * @param toPos The new adapter position of the dragged item. 1735 | * @param x The updated left value of the dragged View after drag translations 1736 | * are applied. This value does not include margins added by 1737 | * {@link RecyclerView.ItemDecoration}s. 1738 | * @param y The updated top value of the dragged View after drag translations 1739 | * are applied. This value does not include margins added by 1740 | * {@link RecyclerView.ItemDecoration}s. 1741 | */ 1742 | public void onMoved(final RecyclerView recyclerView, 1743 | final ViewHolder viewHolder, int fromPos, final ViewHolder target, int toPos, int x, 1744 | int y) { 1745 | final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); 1746 | if (layoutManager instanceof ViewDropHandler) { 1747 | ((ViewDropHandler) layoutManager).prepareForDrop(viewHolder.itemView, 1748 | target.itemView, x, y); 1749 | return; 1750 | } 1751 | 1752 | // if layout manager cannot handle it, do some guesswork 1753 | if (layoutManager.canScrollHorizontally()) { 1754 | final int minLeft = layoutManager.getDecoratedLeft(target.itemView); 1755 | if (minLeft <= recyclerView.getPaddingLeft()) { 1756 | recyclerView.scrollToPosition(toPos); 1757 | } 1758 | final int maxRight = layoutManager.getDecoratedRight(target.itemView); 1759 | if (maxRight >= recyclerView.getWidth() - recyclerView.getPaddingRight()) { 1760 | recyclerView.scrollToPosition(toPos); 1761 | } 1762 | } 1763 | 1764 | if (layoutManager.canScrollVertically()) { 1765 | final int minTop = layoutManager.getDecoratedTop(target.itemView); 1766 | if (minTop <= recyclerView.getPaddingTop()) { 1767 | recyclerView.scrollToPosition(toPos); 1768 | } 1769 | final int maxBottom = layoutManager.getDecoratedBottom(target.itemView); 1770 | if (maxBottom >= recyclerView.getHeight() - recyclerView.getPaddingBottom()) { 1771 | recyclerView.scrollToPosition(toPos); 1772 | } 1773 | } 1774 | } 1775 | 1776 | private void onDraw(Canvas c, RecyclerView parent, ViewHolder selected, 1777 | List recoverAnimationList, 1778 | int actionState, float dX, float dY) { 1779 | final int recoverAnimSize = recoverAnimationList.size(); 1780 | for (int i = 0; i < recoverAnimSize; i++) { 1781 | final RecoverAnimation anim = recoverAnimationList.get(i); 1782 | anim.update(); 1783 | final int count = c.save(); 1784 | onChildDraw(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState, 1785 | false); 1786 | c.restoreToCount(count); 1787 | } 1788 | if (selected != null) { 1789 | final int count = c.save(); 1790 | onChildDraw(c, parent, selected, dX, dY, actionState, true); 1791 | c.restoreToCount(count); 1792 | } 1793 | } 1794 | 1795 | private void onDrawOver(Canvas c, RecyclerView parent, ViewHolder selected, 1796 | List recoverAnimationList, 1797 | int actionState, float dX, float dY) { 1798 | final int recoverAnimSize = recoverAnimationList.size(); 1799 | for (int i = 0; i < recoverAnimSize; i++) { 1800 | final RecoverAnimation anim = recoverAnimationList.get(i); 1801 | final int count = c.save(); 1802 | onChildDrawOver(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState, 1803 | false); 1804 | c.restoreToCount(count); 1805 | } 1806 | if (selected != null) { 1807 | final int count = c.save(); 1808 | onChildDrawOver(c, parent, selected, dX, dY, actionState, true); 1809 | c.restoreToCount(count); 1810 | } 1811 | boolean hasRunningAnimation = false; 1812 | for (int i = recoverAnimSize - 1; i >= 0; i--) { 1813 | final RecoverAnimation anim = recoverAnimationList.get(i); 1814 | if (anim.mEnded && !anim.mIsPendingCleanup) { 1815 | recoverAnimationList.remove(i); 1816 | } else if (!anim.mEnded) { 1817 | hasRunningAnimation = true; 1818 | } 1819 | } 1820 | if (hasRunningAnimation) { 1821 | parent.invalidate(); 1822 | } 1823 | } 1824 | 1825 | /** 1826 | * Called by the SwipePositionItemTouchHelper when the user interaction with an element is over and it 1827 | * also completed its animation. 1828 | *

1829 | * This is a good place to clear all changes on the View that was done in 1830 | * {@link #onSelectedChanged(RecyclerView.ViewHolder, int)}, 1831 | * {@link #onChildDraw(Canvas, RecyclerView, ViewHolder, float, float, int, 1832 | * boolean)} or 1833 | * {@link #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, boolean)}. 1834 | * 1835 | * @param recyclerView The RecyclerView which is controlled by the SwipePositionItemTouchHelper. 1836 | * @param viewHolder The View that was interacted by the user. 1837 | */ 1838 | public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) { 1839 | sUICallback.clearView(viewHolder.itemView); 1840 | } 1841 | 1842 | /** 1843 | * Called by SwipePositionItemTouchHelper on RecyclerView's onDraw callback. 1844 | *

1845 | * If you would like to customize how your View's respond to user interactions, this is 1846 | * a good place to override. 1847 | *

1848 | * Default implementation translates the child by the given dX, 1849 | * dY. 1850 | * SwipePositionItemTouchHelper also takes care of drawing the child after other children if it is being 1851 | * dragged. This is done using child re-ordering mechanism. On platforms prior to L, this 1852 | * is 1853 | * achieved via {@link ViewGroup#getChildDrawingOrder(int, int)} and on L 1854 | * and after, it changes View's elevation value to be greater than all other children.) 1855 | * 1856 | * @param c The canvas which RecyclerView is drawing its children 1857 | * @param recyclerView The RecyclerView to which SwipePositionItemTouchHelper is attached to 1858 | * @param viewHolder The ViewHolder which is being interacted by the User or it was 1859 | * interacted and simply animating to its original position 1860 | * @param dX The amount of horizontal displacement caused by user's action 1861 | * @param dY The amount of vertical displacement caused by user's action 1862 | * @param actionState The type of interaction on the View. Is either {@link 1863 | * #ACTION_STATE_DRAG} or {@link #ACTION_STATE_SWIPE}. 1864 | * @param isCurrentlyActive True if this view is currently being controlled by the user or 1865 | * false it is simply animating back to its original state. 1866 | * @see #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, 1867 | * boolean) 1868 | */ 1869 | public void onChildDraw(Canvas c, RecyclerView recyclerView, 1870 | ViewHolder viewHolder, 1871 | float dX, float dY, int actionState, boolean isCurrentlyActive) { 1872 | sUICallback.onDraw(c, recyclerView, viewHolder.itemView, dX, dY, actionState, 1873 | isCurrentlyActive); 1874 | } 1875 | 1876 | /** 1877 | * Called by SwipePositionItemTouchHelper on RecyclerView's onDraw callback. 1878 | *

1879 | * If you would like to customize how your View's respond to user interactions, this is 1880 | * a good place to override. 1881 | *

1882 | * Default implementation translates the child by the given dX, 1883 | * dY. 1884 | * SwipePositionItemTouchHelper also takes care of drawing the child after other children if it is being 1885 | * dragged. This is done using child re-ordering mechanism. On platforms prior to L, this 1886 | * is 1887 | * achieved via {@link ViewGroup#getChildDrawingOrder(int, int)} and on L 1888 | * and after, it changes View's elevation value to be greater than all other children.) 1889 | * 1890 | * @param c The canvas which RecyclerView is drawing its children 1891 | * @param recyclerView The RecyclerView to which SwipePositionItemTouchHelper is attached to 1892 | * @param viewHolder The ViewHolder which is being interacted by the User or it was 1893 | * interacted and simply animating to its original position 1894 | * @param dX The amount of horizontal displacement caused by user's action 1895 | * @param dY The amount of vertical displacement caused by user's action 1896 | * @param actionState The type of interaction on the View. Is either {@link 1897 | * #ACTION_STATE_DRAG} or {@link #ACTION_STATE_SWIPE}. 1898 | * @param isCurrentlyActive True if this view is currently being controlled by the user or 1899 | * false it is simply animating back to its original state. 1900 | * @see #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, 1901 | * boolean) 1902 | */ 1903 | public void onChildDrawOver(Canvas c, RecyclerView recyclerView, 1904 | ViewHolder viewHolder, 1905 | float dX, float dY, int actionState, boolean isCurrentlyActive) { 1906 | sUICallback.onDrawOver(c, recyclerView, viewHolder.itemView, dX, dY, actionState, 1907 | isCurrentlyActive); 1908 | } 1909 | 1910 | /** 1911 | * Called by the SwipePositionItemTouchHelper when user action finished on a ViewHolder and now the View 1912 | * will be animated to its final position. 1913 | *

1914 | * Default implementation uses ItemAnimator's duration values. If 1915 | * animationType is {@link #ANIMATION_TYPE_DRAG}, it returns 1916 | * {@link RecyclerView.ItemAnimator#getMoveDuration()}, otherwise, it returns 1917 | * {@link RecyclerView.ItemAnimator#getRemoveDuration()}. If RecyclerView does not have 1918 | * any {@link RecyclerView.ItemAnimator} attached, this method returns 1919 | * {@code DEFAULT_DRAG_ANIMATION_DURATION} or {@code DEFAULT_SWIPE_ANIMATION_DURATION} 1920 | * depending on the animation type. 1921 | * 1922 | * @param recyclerView The RecyclerView to which the SwipePositionItemTouchHelper is attached to. 1923 | * @param animationType The type of animation. Is one of {@link #ANIMATION_TYPE_DRAG}, 1924 | * {@link #ANIMATION_TYPE_SWIPE_CANCEL} or 1925 | * {@link #ANIMATION_TYPE_SWIPE_SUCCESS}. 1926 | * @param animateDx The horizontal distance that the animation will offset 1927 | * @param animateDy The vertical distance that the animation will offset 1928 | * @return The duration for the animation 1929 | */ 1930 | public long getAnimationDuration(RecyclerView recyclerView, int animationType, 1931 | float animateDx, float animateDy) { 1932 | final RecyclerView.ItemAnimator itemAnimator = recyclerView.getItemAnimator(); 1933 | if (itemAnimator == null) { 1934 | return animationType == ANIMATION_TYPE_DRAG ? DEFAULT_DRAG_ANIMATION_DURATION 1935 | : DEFAULT_SWIPE_ANIMATION_DURATION; 1936 | } else { 1937 | return animationType == ANIMATION_TYPE_DRAG ? itemAnimator.getMoveDuration() 1938 | : itemAnimator.getRemoveDuration(); 1939 | } 1940 | } 1941 | 1942 | /** 1943 | * Called by the SwipePositionItemTouchHelper when user is dragging a view out of bounds. 1944 | *

1945 | * You can override this method to decide how much RecyclerView should scroll in response 1946 | * to this action. Default implementation calculates a value based on the amount of View 1947 | * out of bounds and the time it spent there. The longer user keeps the View out of bounds, 1948 | * the faster the list will scroll. Similarly, the larger portion of the View is out of 1949 | * bounds, the faster the RecyclerView will scroll. 1950 | * 1951 | * @param recyclerView The RecyclerView instance to which SwipePositionItemTouchHelper is 1952 | * attached to. 1953 | * @param viewSize The total size of the View in scroll direction, excluding 1954 | * item decorations. 1955 | * @param viewSizeOutOfBounds The total size of the View that is out of bounds. This value 1956 | * is negative if the View is dragged towards left or top edge. 1957 | * @param totalSize The total size of RecyclerView in the scroll direction. 1958 | * @param msSinceStartScroll The time passed since View is kept out of bounds. 1959 | * @return The amount that RecyclerView should scroll. Keep in mind that this value will 1960 | * be passed to {@link RecyclerView#scrollBy(int, int)} method. 1961 | */ 1962 | public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, 1963 | int viewSize, int viewSizeOutOfBounds, 1964 | int totalSize, long msSinceStartScroll) { 1965 | final int maxScroll = getMaxDragScroll(recyclerView); 1966 | final int absOutOfBounds = Math.abs(viewSizeOutOfBounds); 1967 | final int direction = (int) Math.signum(viewSizeOutOfBounds); 1968 | // might be negative if other direction 1969 | float outOfBoundsRatio = Math.min(1f, 1f * absOutOfBounds / viewSize); 1970 | final int cappedScroll = (int) (direction * maxScroll * 1971 | sDragViewScrollCapInterpolator.getInterpolation(outOfBoundsRatio)); 1972 | final float timeRatio; 1973 | if (msSinceStartScroll > DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS) { 1974 | timeRatio = 1f; 1975 | } else { 1976 | timeRatio = (float) msSinceStartScroll / DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS; 1977 | } 1978 | final int value = (int) (cappedScroll * sDragScrollInterpolator 1979 | .getInterpolation(timeRatio)); 1980 | if (value == 0) { 1981 | return viewSizeOutOfBounds > 0 ? 1 : -1; 1982 | } 1983 | return value; 1984 | } 1985 | 1986 | } 1987 | 1988 | private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener { 1989 | 1990 | @Override 1991 | public boolean onDown(MotionEvent e) { 1992 | return true; 1993 | } 1994 | 1995 | @Override 1996 | public void onLongPress(MotionEvent e) { 1997 | View child = findChildView(e); 1998 | if (child != null) { 1999 | ViewHolder vh = mRecyclerView.getChildViewHolder(child); 2000 | if (vh != null) { 2001 | if (!mCallback.hasDragFlag(mRecyclerView, vh)) { 2002 | return; 2003 | } 2004 | int pointerId = MotionEventCompat.getPointerId(e, 0); 2005 | // Long press is deferred. 2006 | // Check w/ active pointer id to avoid selecting after motion 2007 | // event is canceled. 2008 | if (pointerId == mActivePointerId) { 2009 | final int index = MotionEventCompat 2010 | .findPointerIndex(e, mActivePointerId); 2011 | final float x = MotionEventCompat.getX(e, index); 2012 | final float y = MotionEventCompat.getY(e, index); 2013 | mInitialTouchX = x; 2014 | mInitialTouchY = y; 2015 | mDx = mDy = 0f; 2016 | if (DEBUG) { 2017 | Log.d(TAG, 2018 | "onlong press: x:" + mInitialTouchX + ",y:" + mInitialTouchY); 2019 | } 2020 | if (mCallback.isLongPressDragEnabled()) { 2021 | select(vh, ACTION_STATE_DRAG); 2022 | } 2023 | } 2024 | } 2025 | } 2026 | } 2027 | 2028 | @Override 2029 | public boolean onContextClick(MotionEvent e) { 2030 | return super.onContextClick(e); 2031 | } 2032 | } 2033 | 2034 | private class RecoverAnimation implements Animator.AnimatorListener { 2035 | 2036 | final float mStartDx; 2037 | 2038 | final float mStartDy; 2039 | 2040 | final float mTargetX; 2041 | 2042 | final float mTargetY; 2043 | 2044 | final ViewHolder mViewHolder; 2045 | 2046 | final int mActionState; 2047 | 2048 | private final ValueAnimator mValueAnimator; 2049 | 2050 | private final int mAnimationType; 2051 | 2052 | public boolean mIsPendingCleanup; 2053 | 2054 | float mX; 2055 | 2056 | float mY; 2057 | 2058 | // if user starts touching a recovering view, we put it into interaction mode again, 2059 | // instantly. 2060 | boolean mOverridden = false; 2061 | 2062 | private boolean mEnded = false; 2063 | 2064 | private float mFraction; 2065 | 2066 | public RecoverAnimation(ViewHolder viewHolder, int animationType, 2067 | int actionState, float startDx, float startDy, float targetX, float targetY) { 2068 | mActionState = actionState; 2069 | mAnimationType = animationType; 2070 | mViewHolder = viewHolder; 2071 | mStartDx = startDx; 2072 | mStartDy = startDy; 2073 | mTargetX = targetX; 2074 | mTargetY = targetY; 2075 | mValueAnimator = ValueAnimator.ofFloat(0f, 1f); 2076 | mValueAnimator.addUpdateListener( 2077 | new ValueAnimator.AnimatorUpdateListener() { 2078 | @Override 2079 | public void onAnimationUpdate(ValueAnimator animation) { 2080 | setFraction(animation.getAnimatedFraction()); 2081 | } 2082 | }); 2083 | mValueAnimator.setTarget(viewHolder.itemView); 2084 | mValueAnimator.addListener(this); 2085 | setFraction(0f); 2086 | } 2087 | 2088 | public void setDuration(long duration) { 2089 | mValueAnimator.setDuration(duration); 2090 | } 2091 | 2092 | public void start() { 2093 | mViewHolder.setIsRecyclable(false); 2094 | mValueAnimator.start(); 2095 | } 2096 | 2097 | public void cancel() { 2098 | mValueAnimator.cancel(); 2099 | } 2100 | 2101 | public void setFraction(float fraction) { 2102 | mFraction = fraction; 2103 | } 2104 | 2105 | /** 2106 | * We run updates on onDraw method but use the fraction from animator callback. 2107 | * This way, we can sync translate x/y values w/ the animators to avoid one-off frames. 2108 | */ 2109 | public void update() { 2110 | /*if (mStartDx == mTargetX) { 2111 | // mX = ViewCompat.getTranslationX(mViewHolder.itemView); 2112 | } else */{ 2113 | mX = mStartDx + mFraction * (mTargetX - mStartDx); 2114 | } 2115 | if (mStartDy == mTargetY) { 2116 | mY = ViewCompat.getTranslationY(mViewHolder.itemView); 2117 | } else { 2118 | mY = mStartDy + mFraction * (mTargetY - mStartDy); 2119 | } 2120 | } 2121 | 2122 | public float getHitX() { 2123 | return mX; 2124 | } 2125 | 2126 | public float getHitY() { 2127 | return mViewHolder.itemView.getY() + mY; 2128 | } 2129 | 2130 | @Override 2131 | public void onAnimationStart(Animator animation) { 2132 | 2133 | } 2134 | 2135 | @Override 2136 | public void onAnimationEnd(Animator animation) { 2137 | if (!mEnded) { 2138 | mViewHolder.setIsRecyclable(true); 2139 | } 2140 | mEnded = true; 2141 | } 2142 | 2143 | @Override 2144 | public void onAnimationCancel(Animator animation) { 2145 | setFraction(1f); //make sure we recover the view's state. 2146 | } 2147 | 2148 | @Override 2149 | public void onAnimationRepeat(Animator animation) { 2150 | 2151 | } 2152 | } 2153 | } 2154 | -------------------------------------------------------------------------------- /app/src/main/java/org/buffer/android/multiactionswipehelper/SwipeToPerformActionCallback.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.multiactionswipe 2 | 3 | import android.graphics.Canvas 4 | import android.graphics.Color 5 | import android.graphics.Paint 6 | import android.graphics.PorterDuff 7 | import android.graphics.PorterDuffXfermode 8 | import android.graphics.Rect 9 | import android.graphics.drawable.ColorDrawable 10 | import android.graphics.drawable.Drawable 11 | import android.support.v4.content.ContextCompat 12 | import android.support.v7.widget.RecyclerView 13 | import android.support.v7.widget.helper.ItemTouchHelper.LEFT 14 | import android.support.v7.widget.helper.ItemTouchHelper.RIGHT 15 | 16 | class SwipeToPerformActionCallback(private val swipeListener: SwipeActionListener, 17 | private val textPadding: Int = 0, 18 | var conversationActions: List) 19 | : org.buffer.android.multiactionswipe.SwipePositionItemTouchHelper.Callback() { 20 | 21 | override fun getMovementFlags(recyclerView: RecyclerView?, 22 | viewHolder: RecyclerView.ViewHolder?): Int { 23 | return makeMovementFlags(0, LEFT or RIGHT) 24 | } 25 | 26 | private val background = ColorDrawable() 27 | private val clearPaint = Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) } 28 | 29 | private var currentIcon: Drawable? = null 30 | private var currentLabel: String = "" 31 | 32 | override fun onMove(recyclerView: RecyclerView?, viewHolder: RecyclerView.ViewHolder?, 33 | target: RecyclerView.ViewHolder?): Boolean { 34 | return false 35 | } 36 | 37 | override fun onChildDraw(canvas: Canvas?, recyclerView: RecyclerView, 38 | viewHolder: RecyclerView.ViewHolder, 39 | dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) { 40 | 41 | val dragDirection = if (dX < 0) RIGHT else LEFT 42 | val parentWidth = recyclerView.width 43 | 44 | val itemView = viewHolder.itemView 45 | val itemHeight = itemView.bottom - itemView.top 46 | val itemCenter = (itemView.bottom + itemView.top) / 2f 47 | val isCanceled = dX == 0f && !isCurrentlyActive 48 | 49 | if (isCanceled) { 50 | clearCanvas(canvas, itemView.right + dX, itemView.top.toFloat(), 51 | itemView.right.toFloat(), itemView.bottom.toFloat()) 52 | super.onChildDraw(canvas, recyclerView, viewHolder, dX, dY, 53 | actionState, isCurrentlyActive) 54 | return 55 | } 56 | 57 | val isFirstHalf = Math.abs(dX) < (parentWidth / 2) 58 | val paint = Paint() 59 | 60 | if (isCurrentlyActive) { 61 | val action = 62 | if (isFirstHalf) { 63 | ActionHelper.getFirstActionWithDirection(conversationActions, 64 | dragDirection) 65 | } else { 66 | ActionHelper.getSecondActionWithDirection(conversationActions, 67 | dragDirection) 68 | } 69 | action?.let { 70 | background.color = ContextCompat.getColor(recyclerView.context, 71 | action.backgroundColor) 72 | currentIcon = ContextCompat.getDrawable(recyclerView.context, action.icon) 73 | paint.color = ContextCompat.getColor(recyclerView.context, action.labelColor) 74 | currentLabel = recyclerView.context.resources.getString(it.identifier) 75 | } 76 | } 77 | 78 | val intrinsicWidth = currentIcon?.intrinsicWidth ?: 0 79 | val intrinsicHeight = currentIcon?.intrinsicHeight ?: 0 80 | val currentIconTop = itemView.top + (itemHeight - intrinsicHeight) / 2 81 | val currentIconBottom = currentIconTop + intrinsicHeight 82 | val currentIconMargin = (itemHeight - intrinsicHeight) / 2 83 | 84 | val currentIconLeft: Int 85 | val currentIconRight: Int 86 | val textPositionX: Int 87 | val textPositionY: Float 88 | 89 | paint.textSize = recyclerView.context.resources 90 | .getDimensionPixelSize(R.dimen.text_large_body).toFloat() 91 | paint.textAlign = Paint.Align.LEFT 92 | paint.isAntiAlias = true 93 | paint.color = Color.WHITE 94 | 95 | val textBounds = Rect() 96 | paint.getTextBounds(currentLabel, 0, currentLabel.length, textBounds) 97 | val textWidth = textBounds.width() 98 | textPositionY = itemCenter - textBounds.exactCenterY() 99 | 100 | if (dragDirection == RIGHT) { 101 | background.setBounds(itemView.right + dX.toInt(), itemView.top, itemView.right, 102 | itemView.bottom) 103 | 104 | currentIconLeft = itemView.right - currentIconMargin - intrinsicWidth 105 | currentIconRight = itemView.right - currentIconMargin 106 | currentIcon?.setBounds(currentIconLeft, currentIconTop, currentIconRight, 107 | currentIconBottom) 108 | 109 | textPositionX = (itemView.right - currentIconMargin - intrinsicWidth - 110 | textPadding - textWidth) 111 | } else { 112 | background.setBounds(itemView.left, itemView.top, itemView.left + dX.toInt(), 113 | itemView.bottom) 114 | 115 | currentIconLeft = itemView.left + currentIconMargin 116 | currentIconRight = itemView.left + currentIconMargin + intrinsicWidth 117 | currentIcon?.setBounds(currentIconLeft, currentIconTop, currentIconRight, 118 | currentIconBottom) 119 | 120 | textPositionX = itemView.left + currentIconMargin + intrinsicWidth + textPadding 121 | } 122 | canvas?.let { 123 | background.draw(it) 124 | currentIcon?.draw(it) 125 | it.drawText(currentLabel, textPositionX.toFloat(), textPositionY, paint) 126 | } 127 | 128 | 129 | super.onChildDraw(canvas, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) 130 | } 131 | 132 | override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int, 133 | horizontalTouchPosition: Float) { 134 | val position = 135 | if (Math.abs(horizontalTouchPosition) < (viewHolder.itemView.width / 2)) 0 else 1 136 | val dragDirection = if (direction == LEFT) RIGHT else LEFT 137 | val fallback = if (position == 0) 1 else 0 138 | val action = ActionHelper.handleAction(conversationActions, dragDirection, position, 139 | fallback) 140 | swipeListener.onActionPerformed(viewHolder.adapterPosition, action) 141 | } 142 | 143 | private fun clearCanvas(c: Canvas?, left: Float, top: Float, right: Float, bottom: Float) { 144 | c?.drawRect(left, top, right, bottom, clearPaint) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18sp 5 | 6 | 7 | -------------------------------------------------------------------------------- /art/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bufferapp/MultiActionSwipeHelper/d81c2c7c8b6cc45a76b7cd580aaa3e0679dcea8e/art/demo.gif -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.2.71' 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.2.1' 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | jcenter() 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # Kotlin code style for this project: "official" or "obsolete": 15 | kotlin.code.style=official 16 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bufferapp/MultiActionSwipeHelper/d81c2c7c8b6cc45a76b7cd580aaa3e0679dcea8e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------