├── .gitignore ├── README.md ├── app-debug.apk ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── guolei │ │ └── boardview │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── guolei │ │ │ ├── boardview │ │ │ ├── AbsBoardViewAdapter.java │ │ │ ├── BaseBoardViewListener.java │ │ │ ├── BoardView.java │ │ │ ├── BoardViewHelper.java │ │ │ ├── BoardViewListener.java │ │ │ ├── BoardViewStateHolder.java │ │ │ ├── BoardViewTouchCallback.java │ │ │ ├── ItemTouchHelper.java │ │ │ ├── ItemTouchUIUtilImpl.java │ │ │ └── SimpleLayoutManager.java │ │ │ └── ui │ │ │ ├── BoardViewAdapter.java │ │ │ ├── ColumnAdapter.java │ │ │ ├── CustomItemDecoration.java │ │ │ ├── KanbanBoardViewListener.java │ │ │ └── MainActivity.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── item.xml │ │ └── item_recyclerview.xml │ │ ├── menu │ │ └── main_menu.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── guolei │ └── boardview │ └── ExampleUnitTest.kt ├── boardview.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/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | .idea 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BoardView 2 | 看板视图,支持整列拖拽、单个Item拖拽、跨列拖拽、放大缩小等 3 | 4 | 5 | 看一下效果图,gif录制的不是特别好并且似乎看起来有点卡,而且很大。耐心等待 6 | 7 | ![这是效果图](https://github.com/Guolei1130/BoardView/blob/master/boardview.gif?raw=true) 8 | 9 | 大家可以安装那个debug的apk文件体验一哈,注意,这里是debug的包,所以直接安装不了,要**adb install -t 包 的方式去安装** 10 | 11 | 暂时还有一个小bug懒得改。 12 | 13 | 代码的话,先将就看吧,随后我会review的。因为今天刚把一些小bug改好。 14 | 15 | 喜欢的话给个star吧。 16 | 17 | 18 | -------------------------------------------------------------------------------- /app-debug.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guolei1130/BoardView/0c1c02a7e469097a2d377e8f95562fdf13d2a067/app-debug.apk -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | 4 | android { 5 | compileSdkVersion 26 6 | defaultConfig { 7 | applicationId "com.guolei.boardview" 8 | minSdkVersion 19 9 | targetSdkVersion 26 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | buildToolsVersion '26.0.2' 21 | compileOptions { 22 | sourceCompatibility JavaVersion.VERSION_1_8 23 | targetCompatibility JavaVersion.VERSION_1_8 24 | } 25 | } 26 | 27 | dependencies { 28 | implementation fileTree(include: ['*.jar'], dir: 'libs') 29 | implementation 'com.android.support:appcompat-v7:26.1.0' 30 | implementation 'com.android.support.constraint:constraint-layout:1.0.2' 31 | testImplementation 'junit:junit:4.12' 32 | implementation 'com.android.support:recyclerview-v7:26.1.0' 33 | } 34 | -------------------------------------------------------------------------------- /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/androidTest/java/com/guolei/boardview/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.guolei.boardview 2 | 3 | import android.support.test.InstrumentationRegistry 4 | import android.support.test.runner.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getTargetContext() 22 | assertEquals("com.guolei.boardview", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/guolei/boardview/AbsBoardViewAdapter.java: -------------------------------------------------------------------------------- 1 | package com.guolei.boardview; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | 5 | /** 6 | * Copyright © 2013-2018 Worktile. All Rights Reserved. 7 | * Author: guolei 8 | * Email: 1120832563@qq.com 9 | * Date: 18/7/5 10 | * Time: 下午10:25 11 | * Desc: 12 | */ 13 | public abstract class AbsBoardViewAdapter extends RecyclerView.Adapter { 14 | public abstract int getPositionFromId(String id); 15 | 16 | public abstract String getIdFromPosition(int position); 17 | 18 | public abstract void add(int position, T data); 19 | 20 | public abstract T getData(int position); 21 | 22 | public abstract T remove(int position); 23 | 24 | public abstract void swap(int fromPos, int toPos); 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/guolei/boardview/BaseBoardViewListener.java: -------------------------------------------------------------------------------- 1 | package com.guolei.boardview; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.util.Log; 5 | 6 | /** 7 | * Copyright © 2013-2018 Worktile. All Rights Reserved. 8 | * Author: guolei 9 | * Email: 1120832563@qq.com 10 | * Date: 18/7/9 11 | * Time: 下午9:33 12 | * Desc: 13 | */ 14 | public abstract class BaseBoardViewListener implements BoardViewListener{ 15 | private static final String TAG = "BoardView"; 16 | 17 | @Override 18 | public void onSwap(RecyclerView recyclerView, int from, int to) { 19 | // 调用太频繁导致失效? 20 | if (from == to) { 21 | return; 22 | } 23 | RecyclerView.Adapter adapter = recyclerView.getAdapter(); 24 | if (adapter instanceof AbsBoardViewAdapter) { 25 | ((AbsBoardViewAdapter) adapter).swap(from, to); 26 | } 27 | adapter.notifyItemMoved(from, to); 28 | Log.e(TAG, "onSwap: " + from + ";" + to); 29 | } 30 | 31 | @Override 32 | public T getData(RecyclerView recyclerView, int position) { 33 | RecyclerView.Adapter adapter = recyclerView.getAdapter(); 34 | T data = null; 35 | if (adapter instanceof AbsBoardViewAdapter) { 36 | //noinspection unchecked 37 | data = (T) ((AbsBoardViewAdapter) adapter).getData(position); 38 | } 39 | return data; 40 | } 41 | 42 | @Override 43 | public T onRemove(RecyclerView recyclerView, int position) { 44 | RecyclerView.Adapter adapter = recyclerView.getAdapter(); 45 | T data = null; 46 | if (adapter instanceof AbsBoardViewAdapter) { 47 | //noinspection unchecked 48 | data = (T) ((AbsBoardViewAdapter) adapter).remove(position); 49 | } 50 | adapter.notifyItemRemoved(position); 51 | return data; 52 | } 53 | 54 | @Override 55 | public void onInsert(RecyclerView recyclerView, int position, T data) { 56 | RecyclerView.Adapter adapter = recyclerView.getAdapter(); 57 | if (adapter instanceof AbsBoardViewAdapter) { 58 | //noinspection unchecked 59 | ((AbsBoardViewAdapter) adapter).add(position, data); 60 | } 61 | adapter.notifyItemInserted(position); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/guolei/boardview/BoardView.java: -------------------------------------------------------------------------------- 1 | package com.guolei.boardview; 2 | 3 | import android.animation.Animator; 4 | import android.annotation.SuppressLint; 5 | import android.content.Context; 6 | import android.graphics.Bitmap; 7 | import android.graphics.Canvas; 8 | import android.graphics.Rect; 9 | import android.graphics.drawable.BitmapDrawable; 10 | import android.os.Build; 11 | import android.os.Handler; 12 | import android.support.annotation.NonNull; 13 | import android.support.annotation.Nullable; 14 | import android.support.v4.view.GestureDetectorCompat; 15 | import android.support.v4.view.ViewCompat; 16 | import android.support.v7.widget.LinearLayoutManager; 17 | import android.support.v7.widget.PagerSnapHelper; 18 | import android.support.v7.widget.RecyclerView; 19 | import android.util.AttributeSet; 20 | import android.util.Log; 21 | import android.view.GestureDetector; 22 | import android.view.MotionEvent; 23 | import android.view.View; 24 | import android.view.ViewConfiguration; 25 | import android.view.ViewGroup; 26 | import android.view.animation.AccelerateDecelerateInterpolator; 27 | import android.widget.FrameLayout; 28 | 29 | import java.lang.reflect.Field; 30 | import java.lang.reflect.Method; 31 | 32 | /** 33 | * Copyright © 2013-2017 Worktile. All Rights Reserved. 34 | * Author: guolei 35 | * Email: 1120832563@qq.com 36 | * Date: 18/2/27 37 | * Time: 下午4:21 38 | * Desc: 39 | */ 40 | @SuppressWarnings("unused") 41 | public class BoardView extends FrameLayout { 42 | private static final String TAG = BoardView.class.getSimpleName(); 43 | 44 | private static final int ACTION_BIND = 0; 45 | private static final int ACTION_UPDATE = 1; 46 | 47 | private View mMirrorView; 48 | private RecyclerView mContentView; 49 | private RecyclerView mCurrentSelectedRecyclerView; 50 | private View mCurrentSelectedLayout; 51 | private GestureDetectorCompat mGestureDetector; 52 | 53 | private RecyclerView.ViewHolder mSelected; 54 | private float mInitialTouchX, mInitialTouchY; 55 | private MotionEvent mCurrentTouchEvent, mLastMoveEvent, mLongPressEvent; 56 | 57 | private boolean mCanMove = true; 58 | 59 | private int mColumnHeaderHeight; 60 | private int mInsertPosition = RecyclerView.NO_POSITION; 61 | 62 | private int mTouchSlop, mScaledTouchSlop; 63 | private int mParentHeight, mParentWidth; 64 | 65 | private int mOriginColumnIndex, mOriginRowIndex, mTargetColumnIndex, mTargetRowIndex; 66 | 67 | private BoardViewListener mBoardViewListener; 68 | private BoardViewStateHolder mBoardViewHolder; 69 | 70 | public BoardView(@NonNull Context context) { 71 | this(context, null); 72 | } 73 | 74 | public BoardView(@NonNull Context context, @Nullable AttributeSet attrs) { 75 | this(context, attrs, 0); 76 | } 77 | 78 | public BoardView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 79 | super(context, attrs, defStyleAttr); 80 | ViewConfiguration viewConfiguration = ViewConfiguration.get(context); 81 | //noinspection deprecation 82 | mTouchSlop = ViewConfiguration.getTouchSlop(); 83 | mScaledTouchSlop = viewConfiguration.getScaledTouchSlop(); 84 | initGestureDetector(); 85 | mBoardViewHolder = new BoardViewStateHolder(); 86 | } 87 | 88 | private void initGestureDetector() { 89 | if (mGestureDetector == null) { 90 | mGestureDetector = new GestureDetectorCompat(getContext(), new BoardViewGestureDetector()); 91 | } 92 | } 93 | 94 | public void setListener(BoardViewListener listener) { 95 | mBoardViewListener = listener; 96 | mBoardViewHolder.setBoardViewListener(listener); 97 | mContentView.addItemDecoration(new RecyclerView.ItemDecoration() { 98 | @Override 99 | public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { 100 | outRect.set(listener.getPxInColumn(), 0, listener.getPxInColumn(), 0); 101 | } 102 | }); 103 | } 104 | 105 | @Override 106 | protected void onFinishInflate() { 107 | super.onFinishInflate(); 108 | mContentView = new RecyclerView(getContext()); 109 | mContentView.setLayoutParams(generateDefaultLayoutParams()); 110 | mContentView.setLayoutManager(new SimpleLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, 111 | false)); 112 | mContentView.setHasFixedSize(true); 113 | addView(mContentView); 114 | mContentView.addOnScrollListener(new RecyclerView.OnScrollListener() { 115 | @Override 116 | public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 117 | if (mGestureDetector == null) { 118 | return; 119 | } 120 | if (newState != RecyclerView.SCROLL_STATE_IDLE) { 121 | mGestureDetector.setIsLongpressEnabled(false); 122 | removeLongPressMessage(); 123 | } else { 124 | mGestureDetector.setIsLongpressEnabled(true); 125 | } 126 | 127 | } 128 | }); 129 | 130 | PagerSnapHelper linearSnapHelper = new PagerSnapHelper(); 131 | linearSnapHelper.attachToRecyclerView(mContentView); 132 | mMirrorView = new View(getContext()); 133 | mMirrorView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, 134 | ViewGroup.LayoutParams.WRAP_CONTENT)); 135 | mMirrorView.setVisibility(GONE); 136 | mMirrorView.setAlpha(.8f); 137 | addView(mMirrorView); 138 | 139 | 140 | } 141 | 142 | public void setAdapter(RecyclerView.Adapter adapter) { 143 | mContentView.setAdapter(adapter); 144 | ItemTouchHelper helper = new ItemTouchHelper(new BoardViewTouchCallback(mBoardViewHolder)); 145 | helper.attachToRecyclerView(mContentView); 146 | helper.setResponseEventListener(mBoardViewListener); 147 | } 148 | 149 | @Override 150 | public boolean onInterceptTouchEvent(MotionEvent ev) { 151 | if (landupInnerRecyclerView(ev)) { 152 | mGestureDetector.setIsLongpressEnabled(true); 153 | } else { 154 | mGestureDetector.setIsLongpressEnabled(false); 155 | } 156 | mGestureDetector.onTouchEvent(ev); 157 | switch (ev.getAction()) { 158 | case MotionEvent.ACTION_CANCEL: 159 | case MotionEvent.ACTION_UP: 160 | recoverSelected(); 161 | break; 162 | } 163 | return mSelected != null || super.onInterceptTouchEvent(ev); 164 | } 165 | 166 | @SuppressLint("ClickableViewAccessibility") 167 | @Override 168 | public boolean onTouchEvent(MotionEvent event) { 169 | if (landupInnerRecyclerView(event)) { 170 | mGestureDetector.setIsLongpressEnabled(true); 171 | } else { 172 | mGestureDetector.setIsLongpressEnabled(false); 173 | } 174 | mGestureDetector.onTouchEvent(event); 175 | switch (event.getAction()) { 176 | case MotionEvent.ACTION_MOVE: 177 | if (mSelected != null) { 178 | mCurrentTouchEvent = MotionEvent.obtain(event); 179 | if (mBoardViewHolder.isInSmallMode()) { 180 | mMirrorView.setTranslationX(event.getX() - mLongPressEvent.getX() 181 | + mInitialTouchX); 182 | mMirrorView.setTranslationY(event.getY() - mLongPressEvent.getY() 183 | + mInitialTouchY); 184 | } else { 185 | mMirrorView.setTranslationX(event.getX() - mInitialTouchX); 186 | mMirrorView.setTranslationY(event.getY() - mInitialTouchY); 187 | } 188 | if (mLastMoveEvent != null && (Math.abs(mLastMoveEvent.getY() - event.getY()) > mTouchSlop 189 | || Math.abs(mLastMoveEvent.getX() - event.getX()) > mTouchSlop)) { 190 | mLastMoveEvent = MotionEvent.obtain(event); 191 | updateAdapterIfNecessary(event); 192 | //判断是需要进行横向欢动还是纵向滑动 193 | } 194 | selectScroll(); 195 | //mContentView.invalidate(); 196 | } 197 | break; 198 | case MotionEvent.ACTION_CANCEL: 199 | case MotionEvent.ACTION_UP: 200 | recoverSelected(); 201 | break; 202 | } 203 | return mSelected != null || super.onTouchEvent(event); 204 | } 205 | 206 | /** 207 | * 选择一个滑动方向 208 | */ 209 | private void selectScroll() { 210 | /* 211 | * 1.找到落点所在的RecyclerView 212 | */ 213 | RecyclerView mCurrentRecyclerView = findRecyclerView(mCurrentTouchEvent); 214 | // 如果落点在RecyclerView的下面,这种情况下是找不到RecyclerView的, 215 | if (mCurrentRecyclerView == null) { 216 | mCurrentRecyclerView = findRecyclerViewWithoutY(mCurrentTouchEvent); 217 | if (mCurrentRecyclerView == null) { 218 | mContentView.removeCallbacks(mContentViewRunnable); 219 | return; 220 | } 221 | } 222 | // mCurrentRecyclerView = mContentView.getChildAt(1).findViewById(mBoardViewListener.getColumnRecyclerViewId()); 223 | int mCurrentRecyclerViewTop = mCurrentRecyclerView.getTop(); 224 | int mCurrentRecyclerViewBottom = mCurrentRecyclerView.getBottom(); 225 | 226 | float distanceTop = Math.abs((int) (mColumnHeaderHeight + mCurrentRecyclerViewTop 227 | - mCurrentTouchEvent.getY())); 228 | float distanceBottom = Math.abs((int) (mColumnHeaderHeight + mCurrentRecyclerViewBottom 229 | - mCurrentTouchEvent.getY())); 230 | float distanceLeft = mCurrentTouchEvent.getX(); 231 | float distanceRight = (getWindowWidth() / mBoardViewHolder.getScaleFactor() 232 | - mCurrentTouchEvent.getX()); 233 | 234 | if ((distanceTop < distanceLeft && distanceTop < distanceRight) 235 | || (distanceBottom < distanceLeft && distanceBottom < distanceRight)) { 236 | mCurrentSelectedRecyclerView.removeCallbacks(mInnerRecyclerViewRunnable); 237 | mInnerRecyclerViewRunnable.run(); 238 | } else { 239 | mContentView.removeCallbacks(mContentViewRunnable); 240 | mContentViewRunnable.run(); 241 | } 242 | } 243 | 244 | private void updateAdapterIfNecessary(MotionEvent event) { 245 | if (mSelected == null) return; 246 | //分为两种情况,1 在mSelected所在的RecyclerView与当前点所能查找到的的RecyclerView是一个 247 | RecyclerView targetRecycler = findRecyclerView(event); 248 | if (targetRecycler == mCurrentSelectedRecyclerView) { 249 | // 同一个RecyclerView 250 | View child = findRecyclerViewChild(event); 251 | if (child == null) { 252 | return; 253 | } 254 | LinearLayoutManager layoutManager = (LinearLayoutManager) mCurrentSelectedRecyclerView.getLayoutManager(); 255 | RecyclerView.ViewHolder target = mCurrentSelectedRecyclerView.getChildViewHolder(child); 256 | int currentColumn = mContentView.getChildViewHolder(mCurrentSelectedLayout).getAdapterPosition(); 257 | if (!mBoardViewHolder.getBoardViewListener().isEnableSwapInColumn(currentColumn)) { 258 | return; 259 | } 260 | if (target != mSelected && !mCurrentSelectedRecyclerView.isAnimating() && mCanMove) { 261 | if (mBoardViewListener != null) { 262 | int toPos = target.getAdapterPosition(); 263 | int position = mSelected.getAdapterPosition(); 264 | if (position == -1) { 265 | AbsBoardViewAdapter adapter = (AbsBoardViewAdapter) mCurrentSelectedRecyclerView.getAdapter(); 266 | position = adapter.getPositionFromId(mBoardViewHolder.getSelectedId()); 267 | } 268 | if (position == -1) return; 269 | mBoardViewListener.onSwap(mCurrentSelectedRecyclerView, position, 270 | toPos); 271 | /* 272 | * 保持RecyclerView不发生移动 273 | * {@link android.support.v7.widget.helper.ItemTouchHelper#onMoved} 274 | */ 275 | //noinspection RedundantCast 276 | ((android.support.v7.widget.helper.ItemTouchHelper.ViewDropHandler) layoutManager) 277 | .prepareForDrop(mSelected.itemView, target.itemView, 278 | (int) event.getX(), (int) event.getY()); 279 | return; 280 | } 281 | } 282 | } 283 | 284 | // 跨RecyclerView 285 | if (targetRecycler != null && targetRecycler != mCurrentSelectedRecyclerView) { 286 | Object data = null; 287 | boolean enableInsert; 288 | if (mBoardViewListener != null) { 289 | int pos = ((AbsBoardViewAdapter) mCurrentSelectedRecyclerView.getAdapter()) 290 | .getPositionFromId(mBoardViewHolder.getSelectedId()); 291 | data = mBoardViewListener.getData(mCurrentSelectedRecyclerView, pos); 292 | //noinspection unchecked 293 | enableInsert = mBoardViewListener.isEnableInsertRowInColumn(BoardViewHelper.findParentIndex(targetRecycler), data); 294 | if (!enableInsert) { 295 | return; 296 | } 297 | data = mBoardViewListener.onRemove(mCurrentSelectedRecyclerView, pos); 298 | } 299 | 300 | mCurrentSelectedRecyclerView = targetRecycler; 301 | //找到落点所在的位置 302 | View view = findRecyclerViewChild(event); 303 | int position = 0; 304 | RecyclerView.ViewHolder tmpHolder = null; 305 | if (view != null) { 306 | tmpHolder = mCurrentSelectedRecyclerView.getChildViewHolder(view); 307 | position = tmpHolder.getAdapterPosition(); 308 | } 309 | if (mBoardViewListener != null) { 310 | //noinspection unchecked 311 | mBoardViewListener.onInsert(mCurrentSelectedRecyclerView, position, data); 312 | if (position == 0) { 313 | Log.d(TAG, "updateAdapterIfNecessary: " + "Event没有落在View上,这种情况是错误的"); 314 | // throw new IllegalStateException(event.toString()); 315 | } 316 | } 317 | mInsertPosition = position; 318 | //有时候会出现,删除动画。加上这句,能稍微改善下 319 | mSelected.itemView.setAlpha(0); 320 | final RecyclerView.ViewHolder tmpViewHolder1 = mCurrentSelectedRecyclerView 321 | .findViewHolderForAdapterPosition(position); 322 | if (tmpViewHolder1 == null || tmpViewHolder1.getAdapterPosition() != -1) { 323 | postDelayed(this::updateSelectedByInsert, 16); 324 | mCanMove = false; 325 | } else { 326 | mSelected = tmpHolder; 327 | select(mSelected, ACTION_UPDATE); 328 | } 329 | } 330 | } 331 | 332 | final Runnable mContentViewRunnable = new Runnable() { 333 | @Override 334 | public void run() { 335 | if (mSelected != null && scrollIfNecessary()) { 336 | if (mSelected != null) { 337 | updateAdapterIfNecessary(mCurrentTouchEvent); 338 | } 339 | mContentView.removeCallbacks(mContentViewRunnable); 340 | ViewCompat.postOnAnimation(mContentView, this); 341 | } 342 | } 343 | }; 344 | 345 | /** 346 | * 滑动外层的RecyclerView 347 | */ 348 | private boolean scrollIfNecessary() { 349 | int direction = getWindowWidth() - mCurrentTouchEvent.getX() > getWindowWidth() / 2 ? -1 : 1; 350 | if (!mContentView.canScrollHorizontally(direction)) return false; 351 | //边缘检测 352 | if (mContentView.getLeft() + 150 > mCurrentTouchEvent.getX()) { 353 | //在左 354 | mContentView.smoothScrollBy(-mScaledTouchSlop * 8, 0); 355 | return true; 356 | } else if (mContentView.getRight() - 150 < mCurrentTouchEvent.getX()) { 357 | //在右 358 | mContentView.smoothScrollBy(mScaledTouchSlop * 8, 0); 359 | return true; 360 | } 361 | return false; 362 | } 363 | 364 | final Runnable mInnerRecyclerViewRunnable = new Runnable() { 365 | @Override 366 | public void run() { 367 | if (mSelected != null && scrollInnerRecyclerViewIfNecessary()) { 368 | if (mSelected != null) { 369 | updateAdapterIfNecessary(mCurrentTouchEvent); 370 | } 371 | mCurrentSelectedRecyclerView.removeCallbacks(mInnerRecyclerViewRunnable); 372 | ViewCompat.postOnAnimation(mCurrentSelectedRecyclerView, this); 373 | } 374 | } 375 | }; 376 | 377 | /** 378 | * 滑动内嵌的RecyclerView 379 | */ 380 | private boolean scrollInnerRecyclerViewIfNecessary() { 381 | int direction = mCurrentSelectedRecyclerView.getBottom() - (mCurrentTouchEvent.getY() - mColumnHeaderHeight) 382 | > mCurrentTouchEvent.getY() - mColumnHeaderHeight - mCurrentSelectedRecyclerView.getTop() ? -1 : 1; 383 | if (!mCurrentSelectedRecyclerView.canScrollVertically(direction)) return false; 384 | //边缘检测 385 | if (mCurrentSelectedRecyclerView.getTop() + mColumnHeaderHeight > mCurrentTouchEvent.getY()) { 386 | mCurrentSelectedRecyclerView.smoothScrollBy(0, -mScaledTouchSlop * 5); 387 | return true; 388 | } else if (mCurrentSelectedRecyclerView.getBottom() - 50 < mCurrentTouchEvent.getY()) { 389 | mCurrentSelectedRecyclerView.smoothScrollBy(0, mScaledTouchSlop * 5); 390 | return true; 391 | } 392 | return false; 393 | } 394 | 395 | 396 | private void recoverSelected() { 397 | mLastMoveEvent = null; 398 | mCanMove = true; 399 | if (mMirrorView != null && mSelected != null) { 400 | //x方向的偏移量 401 | float diffX = mCurrentSelectedLayout.getLeft() + mSelected.itemView.getLeft() + getScrollX() 402 | - (mMirrorView.getLeft() + mMirrorView.getTranslationX()); 403 | //y方向的偏移量 404 | float diffY = mSelected.itemView.getTop() + mCurrentSelectedRecyclerView.getTop() 405 | - mMirrorView.getTranslationY(); 406 | mMirrorView.animate().cancel(); 407 | mMirrorView.animate() 408 | .setDuration(200) 409 | .rotation(0) 410 | .translationXBy(diffX) 411 | .translationYBy(diffY) 412 | .setInterpolator(new AccelerateDecelerateInterpolator()) 413 | .setListener(new Animator.AnimatorListener() { 414 | @Override 415 | public void onAnimationStart(Animator animation) { 416 | // no op 417 | } 418 | 419 | @Override 420 | public void onAnimationEnd(Animator animation) { 421 | if (mSelected == null) { 422 | return; 423 | } 424 | mTargetColumnIndex = mContentView.getChildViewHolder((View) mCurrentSelectedRecyclerView.getParent()) 425 | .getAdapterPosition(); 426 | mTargetRowIndex = mSelected.getAdapterPosition(); 427 | if (mBoardViewListener != null) { 428 | mBoardViewListener.onReleaseRow(mOriginColumnIndex, mOriginRowIndex, 429 | mTargetColumnIndex, mTargetRowIndex); 430 | RecyclerView.ViewHolder holder = mCurrentSelectedRecyclerView.findViewHolderForAdapterPosition(mTargetColumnIndex); 431 | if (holder != null && holder.itemView != null) { 432 | holder.itemView.setAlpha(1); 433 | holder.itemView.setVisibility(VISIBLE); 434 | } 435 | } 436 | mMirrorView.setVisibility(GONE); 437 | mSelected.itemView.setAlpha(1); 438 | mSelected.itemView.setVisibility(VISIBLE); 439 | // final int posi = mSelected.getAdapterPosition(); 440 | // if (posi == -1) { 441 | // // 这里是ViewHolder丢失位置信息的处理办法 442 | // ColumnAdapter adapter = (ColumnAdapter) mCurrentSelectedRecyclerView.getAdapter(); 443 | // final int position = adapter.getPositionFromId(); 444 | // RecyclerView.ViewHolder tmpViewHolder = mCurrentSelectedRecyclerView 445 | // .findViewHolderForAdapterPosition(position); 446 | // if (tmpViewHolder != null) { 447 | // tmpViewHolder.itemView.setVisibility(VISIBLE); 448 | // } else { 449 | // postDelayed(new Runnable() { 450 | // @Override 451 | // public void run() { 452 | // mCurrentSelectedRecyclerView.getAdapter().notifyItemChanged(position); 453 | // } 454 | // }, 300); 455 | // Log.e(TAG, "onAnimationEnd: "); 456 | // } 457 | // } else { 458 | // postDelayed(new Runnable() { 459 | // @Override 460 | // public void run() { 461 | // mCurrentSelectedRecyclerView.getAdapter().notifyItemChanged(posi); 462 | // } 463 | // }, 300); 464 | // Log.e(TAG, "onAnimationEnd: "); 465 | // } 466 | mBoardViewHolder.setSelectedId(""); 467 | mSelected = null; 468 | } 469 | 470 | @Override 471 | public void onAnimationCancel(Animator animation) { 472 | // no op 473 | } 474 | 475 | @Override 476 | public void onAnimationRepeat(Animator animation) { 477 | // no op 478 | } 479 | }) 480 | .start(); 481 | } 482 | } 483 | 484 | private boolean landupInnerRecyclerView(MotionEvent event) { 485 | View view = mContentView.findChildViewUnder(event.getX(), event.getY()); 486 | if (view == null) { 487 | view = findChildViewUnderWithInsets(mContentView, event.getX(), event.getY()); 488 | } 489 | if (view == null) return false; 490 | int y = (int) event.getY(); 491 | View recyclerView = view.findViewById(mBoardViewListener.getColumnRecyclerViewId()); 492 | return recyclerView != null && y > recyclerView.getTop() && y < recyclerView.getBottom(); 493 | } 494 | 495 | private void select(RecyclerView.ViewHolder selected, int actionState) { 496 | if (selected != null && selected != mSelected) { 497 | if (mSelected != null) { 498 | mSelected.itemView.setVisibility(VISIBLE); 499 | } 500 | mSelected = selected; 501 | int position = mSelected.getAdapterPosition(); 502 | AbsBoardViewAdapter adapter = (AbsBoardViewAdapter) mCurrentSelectedRecyclerView.getAdapter(); 503 | String id = adapter.getIdFromPosition(position); 504 | mBoardViewHolder.setSelectedId(id); 505 | if (actionState == ACTION_BIND) { 506 | onBindSelected(mSelected.itemView); 507 | } 508 | } 509 | } 510 | 511 | @SuppressLint("ObsoleteSdkInt") 512 | private void onBindSelected(View selectedView) { 513 | //先设置偏移量 514 | int x = mCurrentSelectedLayout.getLeft() + selectedView.getLeft(); 515 | int y = mCurrentSelectedLayout.getTop() + selectedView.getTop() + mColumnHeaderHeight; 516 | ViewGroup.LayoutParams params = mMirrorView.getLayoutParams(); 517 | params.width = selectedView.getWidth(); 518 | params.height = selectedView.getHeight(); 519 | mMirrorView.setLayoutParams(params); 520 | mMirrorView.setTranslationX(x); 521 | mMirrorView.setTranslationY(y); 522 | Bitmap bitmap = Bitmap.createBitmap(selectedView.getWidth(), selectedView.getHeight(), 523 | Bitmap.Config.ARGB_8888); 524 | Canvas canvas = new Canvas(bitmap); 525 | selectedView.draw(canvas); 526 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 527 | mMirrorView.setBackground(new BitmapDrawable(selectedView.getResources(), bitmap)); 528 | } else { 529 | //noinspection deprecation 530 | mMirrorView.setBackgroundDrawable(new BitmapDrawable(selectedView.getResources(), bitmap)); 531 | } 532 | mMirrorView.setVisibility(VISIBLE); 533 | mMirrorView.setRotation(-5f); 534 | mSelected.itemView.setAlpha(0); 535 | } 536 | 537 | /** 538 | * 找到当前触摸点所在的RecyclerView 539 | */ 540 | private RecyclerView findRecyclerView(MotionEvent event) { 541 | View child = mContentView.findChildViewUnder(event.getX(), event.getY()); 542 | if (child == null) { 543 | return null; 544 | } 545 | return child.findViewById(mBoardViewListener.getColumnRecyclerViewId()); 546 | } 547 | 548 | /** 549 | * 找到当前触点所在的RecyclerView,不一定落在RecyclerView上. 550 | */ 551 | private RecyclerView findRecyclerViewWithoutY(MotionEvent event) { 552 | float x = event.getX(); 553 | for (int i = 0; i < mContentView.getChildCount(); i++) { 554 | View child = mContentView.getChildAt(i); 555 | if (child.getLeft() <= x && child.getRight() >= x) { 556 | return child.findViewById(mBoardViewListener.getColumnRecyclerViewId()); 557 | } 558 | } 559 | return null; 560 | } 561 | 562 | private int getCurrentColumnTitleHeight(MotionEvent event) { 563 | if (mColumnHeaderHeight != 0) 564 | return mColumnHeaderHeight; 565 | View child = mContentView.findChildViewUnder(event.getX(), event.getY()); 566 | return mColumnHeaderHeight = child.findViewById(mBoardViewListener.getResponseViewId()).getHeight(); 567 | } 568 | 569 | private View findRecyclerViewChild(MotionEvent event) { 570 | View child = mContentView.findChildViewUnder(event.getX(), event.getY()); 571 | mCurrentSelectedLayout = child; 572 | mCurrentSelectedRecyclerView = child.findViewById(mBoardViewListener.getColumnRecyclerViewId()); 573 | int titleHeight = getCurrentColumnTitleHeight(event); 574 | View view = mCurrentSelectedRecyclerView.findChildViewUnder(event.getX() - child.getLeft(), 575 | event.getY() - titleHeight); 576 | if (view == null) { 577 | view = findChildViewUnderWithInsets(mCurrentSelectedRecyclerView, 578 | event.getX() - mCurrentSelectedLayout.getLeft(), 579 | event.getY() - mCurrentSelectedLayout.getTop()); 580 | } 581 | return view; 582 | } 583 | 584 | private void updateSelectedByInsert() { 585 | RecyclerView.ViewHolder viewHolder = mCurrentSelectedRecyclerView 586 | .findViewHolderForAdapterPosition(mInsertPosition); 587 | if (viewHolder != null) { 588 | if (viewHolder.getAdapterPosition() != -1) { 589 | select(viewHolder, ACTION_UPDATE); 590 | mCanMove = true; 591 | } else { 592 | Log.d(TAG, "updateSelectedByInsert: error " + viewHolder.getAdapterPosition()); 593 | } 594 | } 595 | } 596 | 597 | @SuppressWarnings("unused") 598 | private int getWindowHeight() { 599 | return getContext().getResources().getDisplayMetrics().heightPixels; 600 | } 601 | 602 | private int getWindowWidth() { 603 | return getContext().getResources().getDisplayMetrics().widthPixels; 604 | } 605 | 606 | public void scale() { 607 | boolean lessen = !mBoardViewHolder.isInSmallMode(); 608 | if (lessen) { 609 | //缩小 610 | mBoardViewHolder.setInSmallMode(true); 611 | } else { 612 | //还原 613 | mBoardViewHolder.setInSmallMode(false); 614 | } 615 | float scale = mBoardViewHolder.getScaleFactor(); 616 | View rootView = (View) getParent(); 617 | if (lessen && mParentHeight == 0) { 618 | mParentHeight = rootView.getHeight(); 619 | mParentWidth = rootView.getWidth(); 620 | } 621 | if (lessen) { 622 | rootView.getLayoutParams().width = (int) (mParentWidth * (1 / scale)); 623 | rootView.getLayoutParams().height = (int) (mParentHeight * (1 / scale)); 624 | } else { 625 | rootView.getLayoutParams().width = -1; 626 | rootView.getLayoutParams().height = -1; 627 | } 628 | setPivotX(0f); 629 | setPivotY(0f); 630 | setScaleX(scale); 631 | setScaleY(scale); 632 | rootView.requestLayout(); 633 | for (int i = 0; i < mContentView.getChildCount(); i++) { 634 | View child = mContentView.getChildAt(i); 635 | child.requestLayout(); 636 | } 637 | } 638 | 639 | private class BoardViewGestureDetector extends GestureDetector.SimpleOnGestureListener { 640 | 641 | @Override 642 | public boolean onDown(MotionEvent e) { 643 | return false; 644 | } 645 | 646 | @Override 647 | public boolean onDoubleTap(MotionEvent e) { 648 | if (findRecyclerView(e) == null) { 649 | scale(); 650 | return true; 651 | } 652 | return false; 653 | } 654 | 655 | @Override 656 | public void onLongPress(MotionEvent e) { 657 | Log.e(TAG, "onLongPress: "); 658 | if (mContentView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) { 659 | return; 660 | } 661 | if (mSelected != null || e.getAction() != MotionEvent.ACTION_DOWN) { 662 | return; 663 | } 664 | mLastMoveEvent = e; 665 | RecyclerView childRecyclerView = findRecyclerView(e); 666 | if (childRecyclerView == null || childRecyclerView.getScrollState() 667 | != RecyclerView.SCROLL_STATE_IDLE) { 668 | return; 669 | } 670 | RecyclerView.ViewHolder columnViewHolder = mContentView 671 | .getChildViewHolder((View) childRecyclerView.getParent()); 672 | mOriginColumnIndex = columnViewHolder.getAdapterPosition(); 673 | View recyclerViewChild = findRecyclerViewChild(e); 674 | if (recyclerViewChild == null) { 675 | recyclerViewChild = findChildViewUnderWithInsets(childRecyclerView, 676 | e.getX(), e.getY()); 677 | } 678 | if (recyclerViewChild == null) { 679 | return; 680 | } 681 | RecyclerView.ViewHolder itemViewHolder = childRecyclerView.getChildViewHolder(recyclerViewChild); 682 | mOriginRowIndex = itemViewHolder.getAdapterPosition(); 683 | if (!mBoardViewHolder.getBoardViewListener().isEnableSelectRow(mOriginColumnIndex, mOriginRowIndex)) { 684 | return; 685 | } 686 | select(itemViewHolder, 0); 687 | if (mSelected != null) { 688 | //左右边界情况 689 | Rect rect = new Rect(); 690 | mSelected.itemView.getGlobalVisibleRect(rect); 691 | int width = (int) (mSelected.itemView.getWidth() * mBoardViewHolder.getScaleFactor()); 692 | if (Math.abs(rect.left - rect.right) < width) { 693 | if (rect.right + width > getWindowWidth()) { 694 | //右侧 695 | mInitialTouchX = e.getRawX() - rect.left; 696 | } else { 697 | mInitialTouchX = width - (rect.right - e.getRawX()); 698 | } 699 | mInitialTouchY = e.getRawY() - rect.top; 700 | } else { 701 | mInitialTouchX = e.getRawX() - rect.left; 702 | mInitialTouchY = e.getRawY() - rect.top; 703 | } 704 | } else { 705 | mInitialTouchX = e.getX(); 706 | mInitialTouchY = e.getY(); 707 | } 708 | //缩小状态下,上面的方案不行,采用下面的方案 709 | if (mBoardViewHolder.isInSmallMode()) { 710 | mLongPressEvent = MotionEvent.obtain(e); 711 | mInitialTouchX = mMirrorView.getTranslationX(); 712 | mInitialTouchY = mMirrorView.getTranslationY(); 713 | } 714 | } 715 | 716 | } 717 | 718 | private Method getItemDecorInsetsForChildMethod; 719 | 720 | { 721 | try { 722 | getItemDecorInsetsForChildMethod = RecyclerView.class 723 | .getDeclaredMethod("getItemDecorInsetsForChild", View.class); 724 | getItemDecorInsetsForChildMethod.setAccessible(true); 725 | } catch (NoSuchMethodException e) { 726 | e.printStackTrace(); 727 | } 728 | } 729 | 730 | /** 731 | * 加上ItemDecoration的位置 732 | */ 733 | private View findChildViewUnderWithInsets(RecyclerView recyclerView, float x, float y) { 734 | final int count = recyclerView.getChildCount(); 735 | Rect rect = new Rect(0, 0, 0, 0); 736 | for (int i = count - 1; i >= 0; i--) { 737 | final View child = recyclerView.getChildAt(i); 738 | final float translationX = child.getTranslationX(); 739 | final float translationY = child.getTranslationY(); 740 | try { 741 | rect = (Rect) getItemDecorInsetsForChildMethod.invoke(recyclerView, child); 742 | } catch (Exception e) { 743 | e.printStackTrace(); 744 | } 745 | if (x >= child.getLeft() + translationX - rect.left - 30 746 | && x <= child.getRight() + translationX + rect.right + 30 747 | && y >= child.getTop() + translationY - rect.top 748 | && y <= child.getBottom() + translationY + rect.bottom) { 749 | return child; 750 | } 751 | } 752 | return null; 753 | } 754 | 755 | public void removeLongPressMessage() { 756 | try { 757 | Field impl = mGestureDetector.getClass().getDeclaredField("mImpl"); 758 | impl.setAccessible(true); 759 | Object gestureDetectorCompatImpl = impl.get(mGestureDetector); 760 | if (gestureDetectorCompatImpl.getClass().getSimpleName() 761 | .equals("GestureDetectorCompatImplJellybeanMr2")) { 762 | Class gestureDetectorCompatImplJellybeanMr2Class = Class 763 | .forName("android.support.v4.view.GestureDetectorCompat" + 764 | "$GestureDetectorCompatImplJellybeanMr2"); 765 | Field detectorField = gestureDetectorCompatImplJellybeanMr2Class 766 | .getDeclaredField("mDetector"); 767 | detectorField.setAccessible(true); 768 | Object gestureDetector = detectorField.get(gestureDetectorCompatImpl); 769 | Field handlerField = gestureDetector.getClass().getDeclaredField("mHandler"); 770 | handlerField.setAccessible(true); 771 | Handler handler = (Handler) handlerField.get(gestureDetector); 772 | handler.removeMessages(2); 773 | } else { 774 | Class gestureDetectorCompatImplBaseClass = Class 775 | .forName("android.support.v4.view.GestureDetectorCompat" + 776 | "$GestureDetectorCompatImplBase"); 777 | Field handlerField = gestureDetectorCompatImplBaseClass 778 | .getDeclaredField("mHandler"); 779 | handlerField.setAccessible(true); 780 | Handler handler = (Handler) handlerField.get(gestureDetectorCompatImpl); 781 | handler.removeMessages(2); 782 | } 783 | } catch (Exception e) { 784 | e.printStackTrace(); 785 | Log.e(TAG, "removeLongPressMessage: "); 786 | } 787 | } 788 | 789 | public BoardViewStateHolder getBoardViewHolder() { 790 | return mBoardViewHolder; 791 | } 792 | 793 | public RecyclerView getContentView() { 794 | return mContentView; 795 | } 796 | 797 | } 798 | -------------------------------------------------------------------------------- /app/src/main/java/com/guolei/boardview/BoardViewHelper.java: -------------------------------------------------------------------------------- 1 | package com.guolei.boardview; 2 | 3 | // _ _ _ _ 4 | //__ _____ _ __| | _| |_(_) | ___ 5 | //\ \ /\ / / _ \| '__| |/ / __| | |/ _ \ 6 | // \ V V / (_) | | | <| |_| | | __/ 7 | // \_/\_/ \___/|_| |_|\_\\__|_|_|\___| 8 | 9 | 10 | import android.support.v7.widget.RecyclerView; 11 | import android.view.View; 12 | 13 | /** 14 | * Copyright © 2013-2018 Worktile. All Rights Reserved. 15 | * Author: guolei 16 | * Email: 1120832563@qq.com 17 | * Date: 18/7/5 18 | * Time: 下午10:27 19 | * Desc: 20 | */ 21 | public class BoardViewHelper { 22 | public static int findParentIndex(View childView) { 23 | while (childView != null) { 24 | if (childView.getParent() instanceof RecyclerView) { 25 | return ((RecyclerView) childView.getParent()).getChildViewHolder(childView).getAdapterPosition(); 26 | } else { 27 | try { 28 | childView = (View) childView.getParent(); 29 | } catch (Exception e) { 30 | e.printStackTrace(); 31 | } 32 | } 33 | } 34 | return 0; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/guolei/boardview/BoardViewListener.java: -------------------------------------------------------------------------------- 1 | package com.guolei.boardview; 2 | // _ _ _ _ 3 | //__ _____ _ __| | _| |_(_) | ___ 4 | //\ \ /\ / / _ \| '__| |/ / __| | |/ _ \ 5 | // \ V V / (_) | | | <| |_| | | __/ 6 | // \_/\_/ \___/|_| |_|\_\\__|_|_|\___| 7 | 8 | 9 | import android.support.v7.widget.RecyclerView; 10 | 11 | /** 12 | * Copyright © 2013-2017 Worktile. All Rights Reserved. 13 | * Author: guolei 14 | * Email: 1120832563@qq.com 15 | * Date: 18/4/25 16 | * Time: 下午3:22 17 | * Desc: 18 | */ 19 | public interface BoardViewListener extends ItemTouchHelper.ResponseEventListener { 20 | 21 | default int getPxInColumn() { 22 | return 3 * 8; 23 | } 24 | 25 | int getColumnRecyclerViewId(); 26 | 27 | void onSwap(RecyclerView recyclerView, int from, int to); 28 | 29 | T getData(RecyclerView recyclerView, int position); 30 | 31 | T onRemove(RecyclerView recyclerView, int position); 32 | 33 | void onInsert(RecyclerView recyclerView, int position, T data); 34 | 35 | void onReleaseRow(int fromColumnIndex, int fromRowIndex, int toColumnIndex, int toRowIndex); 36 | 37 | void onReleaseColumn(int from, int to); 38 | 39 | /** 40 | * 是否允许被选中 41 | */ 42 | boolean isEnableSelectColumn(int column); 43 | 44 | /** 45 | * 是否支持列内交换 46 | */ 47 | boolean isEnableSwapInColumn(int column); 48 | 49 | /** 50 | * 是否支持插入数据 51 | */ 52 | boolean isEnableInsertRowInColumn(int column, T t); 53 | 54 | /** 55 | * 是否支持row被选中 56 | */ 57 | boolean isEnableSelectRow(int column, int row); 58 | 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/guolei/boardview/BoardViewStateHolder.java: -------------------------------------------------------------------------------- 1 | package com.guolei.boardview; 2 | 3 | 4 | /** 5 | * Copyright © 2013-2017 Worktile. All Rights Reserved. 6 | * Author: guolei 7 | * Email: 1120832563@qq.com 8 | * Date: 18/3/6 9 | * Time: 上午10:47 10 | * Desc: 11 | */ 12 | @SuppressWarnings("unused") 13 | public final class BoardViewStateHolder { 14 | 15 | private static final String EMPTY_ID = ""; 16 | 17 | private String mSelectedId = EMPTY_ID; 18 | private boolean mInSmallMode = false; 19 | private float mScaleFactor = 1f; 20 | private BoardViewListener mBoardViewListener; 21 | 22 | BoardViewStateHolder() { 23 | 24 | } 25 | 26 | public void setSelectedId(String selectedId) { 27 | mSelectedId = selectedId; 28 | } 29 | 30 | public String getSelectedId() { 31 | return mSelectedId; 32 | } 33 | 34 | public void setInSmallMode(boolean isSmall) { 35 | this.mInSmallMode = isSmall; 36 | mScaleFactor = isSmall ? .6f : 1f; 37 | } 38 | 39 | public boolean isInSmallMode() { 40 | return mInSmallMode; 41 | } 42 | 43 | public float getScaleFactor() { 44 | return mScaleFactor; 45 | } 46 | 47 | public void setScaleFactor(float scaleFactor) { 48 | mScaleFactor = scaleFactor; 49 | } 50 | 51 | public BoardViewListener getBoardViewListener() { 52 | return mBoardViewListener; 53 | } 54 | 55 | public void setBoardViewListener(BoardViewListener boardViewListener) { 56 | mBoardViewListener = boardViewListener; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/com/guolei/boardview/BoardViewTouchCallback.java: -------------------------------------------------------------------------------- 1 | package com.guolei.boardview; 2 | // _ _ _ _ 3 | //__ _____ _ __| | _| |_(_) | ___ 4 | //\ \ /\ / / _ \| '__| |/ / __| | |/ _ \ 5 | // \ V V / (_) | | | <| |_| | | __/ 6 | // \_/\_/ \___/|_| |_|\_\\__|_|_|\___| 7 | 8 | 9 | import android.support.v7.widget.RecyclerView; 10 | 11 | import com.guolei.ui.BoardViewAdapter; 12 | 13 | 14 | /** 15 | * Copyright © 2013-2017 Worktile. All Rights Reserved. 16 | * Author: guolei 17 | * Email: 1120832563@qq.com 18 | * Date: 18/2/27 19 | * Time: 上午11:36 20 | * Desc: 21 | */ 22 | public class BoardViewTouchCallback extends ItemTouchHelper.Callback { 23 | 24 | private int mFromPos = 0; 25 | 26 | private BoardViewStateHolder mBoardViewHolder; 27 | 28 | BoardViewTouchCallback(BoardViewStateHolder boardViewHolder) { 29 | mBoardViewHolder = boardViewHolder; 30 | } 31 | 32 | @Override 33 | public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { 34 | final int dragFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT | ItemTouchHelper.UP 35 | | ItemTouchHelper.DOWN; 36 | final int swipeFlags = 0; 37 | return makeMovementFlags(dragFlags, swipeFlags); 38 | } 39 | 40 | @Override 41 | public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, 42 | RecyclerView.ViewHolder target) { 43 | int fromPosition = viewHolder.getAdapterPosition(); 44 | int toPosition = target.getAdapterPosition(); 45 | RecyclerView.Adapter adapter = recyclerView.getAdapter(); 46 | if (adapter instanceof BoardViewAdapter && mBoardViewHolder.getBoardViewListener() != null 47 | && mBoardViewHolder.getBoardViewListener().isEnableSelectColumn(toPosition)) { 48 | ((BoardViewAdapter) adapter).swap(fromPosition, toPosition); 49 | adapter.notifyItemMoved(fromPosition, toPosition); 50 | } 51 | return true; 52 | } 53 | 54 | @Override 55 | public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { 56 | 57 | } 58 | 59 | @Override 60 | public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) { 61 | if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) { 62 | viewHolder.itemView.setAlpha(0.8f); 63 | viewHolder.itemView.setRotation(-5f); 64 | mFromPos = viewHolder.getAdapterPosition(); 65 | } 66 | super.onSelectedChanged(viewHolder, actionState); 67 | 68 | } 69 | 70 | @Override 71 | public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { 72 | super.clearView(recyclerView, viewHolder); 73 | // viewHolder.itemView.setBackgroundColor(Color.parseColor("#ff00ddff")); 74 | viewHolder.itemView.setAlpha(1f); 75 | viewHolder.itemView.setRotation(0f); 76 | if (mBoardViewHolder.getBoardViewListener() != null) { 77 | mBoardViewHolder.getBoardViewListener().onReleaseColumn(mFromPos, 78 | viewHolder.getAdapterPosition()); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/com/guolei/boardview/ItemTouchHelper.java: -------------------------------------------------------------------------------- 1 | package com.guolei.boardview; 2 | // _ _ _ _ 3 | //__ _____ _ __| | _| |_(_) | ___ 4 | //\ \ /\ / / _ \| '__| |/ / __| | |/ _ \ 5 | // \ V V / (_) | | | <| |_| | | __/ 6 | // \_/\_/ \___/|_| |_|\_\\__|_|_|\___| 7 | 8 | 9 | import android.animation.Animator; 10 | import android.animation.ValueAnimator; 11 | import android.content.res.Resources; 12 | import android.graphics.Canvas; 13 | import android.graphics.Rect; 14 | import android.os.Build; 15 | import android.support.annotation.Nullable; 16 | import android.support.v4.view.GestureDetectorCompat; 17 | import android.support.v4.view.ViewCompat; 18 | import android.support.v7.widget.LinearLayoutManager; 19 | import android.support.v7.widget.RecyclerView; 20 | import android.support.v7.widget.helper.ItemTouchUIUtil; 21 | import android.util.Log; 22 | import android.view.GestureDetector; 23 | import android.view.HapticFeedbackConstants; 24 | import android.view.MotionEvent; 25 | import android.view.VelocityTracker; 26 | import android.view.View; 27 | import android.view.ViewConfiguration; 28 | import android.view.ViewGroup; 29 | import android.view.ViewParent; 30 | import android.view.animation.Interpolator; 31 | 32 | import java.util.ArrayList; 33 | import java.util.List; 34 | 35 | /** 36 | * copy from android source code and change a little 37 | */ 38 | public class ItemTouchHelper extends RecyclerView.ItemDecoration 39 | implements RecyclerView.OnChildAttachStateChangeListener { 40 | 41 | /** 42 | * Up direction, used for swipe & drag control. 43 | */ 44 | public static final int UP = 1; 45 | 46 | /** 47 | * Down direction, used for swipe & drag control. 48 | */ 49 | public static final int DOWN = 1 << 1; 50 | 51 | /** 52 | * Left direction, used for swipe & drag control. 53 | */ 54 | public static final int LEFT = 1 << 2; 55 | 56 | /** 57 | * Right direction, used for swipe & drag control. 58 | */ 59 | public static final int RIGHT = 1 << 3; 60 | 61 | // If you change these relative direction values, update Callback#convertToAbsoluteDirection, 62 | // Callback#convertToRelativeDirection. 63 | /** 64 | * Horizontal start direction. Resolved to LEFT or RIGHT depending on RecyclerView's layout 65 | * direction. Used for swipe & drag control. 66 | */ 67 | public static final int START = LEFT << 2; 68 | 69 | /** 70 | * Horizontal end direction. Resolved to LEFT or RIGHT depending on RecyclerView's layout 71 | * direction. Used for swipe & drag control. 72 | */ 73 | public static final int END = RIGHT << 2; 74 | 75 | /** 76 | * ItemTouchHelper is in idle state. At this state, either there is no related motion event by 77 | * the user or latest motion events have not yet triggered a swipe or drag. 78 | */ 79 | public static final int ACTION_STATE_IDLE = 0; 80 | 81 | /** 82 | * A View is currently being swiped. 83 | */ 84 | public static final int ACTION_STATE_SWIPE = 1; 85 | 86 | /** 87 | * A View is currently being dragged. 88 | */ 89 | public static final int ACTION_STATE_DRAG = 2; 90 | 91 | /** 92 | * Animation type for views which are swiped successfully. 93 | */ 94 | public static final int ANIMATION_TYPE_SWIPE_SUCCESS = 1 << 1; 95 | 96 | /** 97 | * Animation type for views which are not completely swiped thus will animate back to their 98 | * original position. 99 | */ 100 | public static final int ANIMATION_TYPE_SWIPE_CANCEL = 1 << 2; 101 | 102 | /** 103 | * Animation type for views that were dragged and now will animate to their final position. 104 | */ 105 | public static final int ANIMATION_TYPE_DRAG = 1 << 3; 106 | 107 | static final String TAG = "ItemTouchHelper"; 108 | 109 | static final boolean DEBUG = false; 110 | 111 | static final int ACTIVE_POINTER_ID_NONE = -1; 112 | 113 | static final int DIRECTION_FLAG_COUNT = 8; 114 | 115 | private static final int ACTION_MODE_IDLE_MASK = (1 << DIRECTION_FLAG_COUNT) - 1; 116 | 117 | static final int ACTION_MODE_SWIPE_MASK = ACTION_MODE_IDLE_MASK << DIRECTION_FLAG_COUNT; 118 | 119 | static final int ACTION_MODE_DRAG_MASK = ACTION_MODE_SWIPE_MASK << DIRECTION_FLAG_COUNT; 120 | 121 | /** 122 | * The unit we are using to track velocity 123 | */ 124 | private static final int PIXELS_PER_SECOND = 1000; 125 | 126 | /** 127 | * Views, whose state should be cleared after they are detached from RecyclerView. 128 | * This is necessary after swipe dismissing an item. We wait until animator finishes its job 129 | * to clean these views. 130 | */ 131 | final List mPendingCleanup = new ArrayList(); 132 | 133 | /** 134 | * Re-use array to calculate dx dy for a ViewHolder 135 | */ 136 | private final float[] mTmpPosition = new float[2]; 137 | 138 | /** 139 | * Currently selected view holder 140 | */ 141 | RecyclerView.ViewHolder mSelected = null; 142 | 143 | /** 144 | * The reference coordinates for the action start. For drag & drop, this is the time long 145 | * press is completed vs for swipe, this is the initial touch point. 146 | */ 147 | float mInitialTouchX; 148 | 149 | float mInitialTouchY; 150 | 151 | /** 152 | * Set when ItemTouchHelper is assigned to a RecyclerView. 153 | */ 154 | float mSwipeEscapeVelocity; 155 | 156 | /** 157 | * Set when ItemTouchHelper is assigned to a RecyclerView. 158 | */ 159 | float mMaxSwipeVelocity; 160 | 161 | /** 162 | * The diff between the last event and initial touch. 163 | */ 164 | float mDx; 165 | 166 | float mDy; 167 | 168 | /** 169 | * The coordinates of the selected view at the time it is selected. We record these values 170 | * when action starts so that we can consistently position it even if LayoutManager moves the 171 | * View. 172 | */ 173 | float mSelectedStartX; 174 | 175 | float mSelectedStartY; 176 | 177 | /** 178 | * The pointer we are tracking. 179 | */ 180 | int mActivePointerId = ACTIVE_POINTER_ID_NONE; 181 | 182 | /** 183 | * Developer callback which controls the behavior of ItemTouchHelper. 184 | */ 185 | Callback mCallback; 186 | 187 | /** 188 | * Current mode. 189 | */ 190 | int mActionState = ACTION_STATE_IDLE; 191 | 192 | /** 193 | * The direction flags obtained from unmasking 194 | * {@link android.support.v7.widget.helper.ItemTouchHelper.Callback#getAbsoluteMovementFlags(RecyclerView, RecyclerView.ViewHolder)} for the current 195 | * action state. 196 | */ 197 | int mSelectedFlags; 198 | 199 | /** 200 | * When a View is dragged or swiped and needs to go back to where it was, we create a Recover 201 | * Animation and animate it to its location using this custom Animator, instead of using 202 | * framework Animators. 203 | * Using framework animators has the side effect of clashing with ItemAnimator, creating 204 | * jumpy UIs. 205 | */ 206 | List mRecoverAnimations = new ArrayList(); 207 | 208 | private int mSlop; 209 | 210 | RecyclerView mRecyclerView; 211 | 212 | /** 213 | * When user drags a view to the edge, we start scrolling the LayoutManager as long as View 214 | * is partially out of bounds. 215 | */ 216 | final Runnable mScrollRunnable = new Runnable() { 217 | @Override 218 | public void run() { 219 | if (mSelected != null && scrollIfNecessary()) { 220 | if (mSelected != null) { //it might be lost during scrolling 221 | moveIfNecessary(mSelected); 222 | } 223 | mRecyclerView.removeCallbacks(mScrollRunnable); 224 | ViewCompat.postOnAnimation(mRecyclerView, this); 225 | } 226 | } 227 | }; 228 | 229 | /** 230 | * Used for detecting fling swipe 231 | */ 232 | VelocityTracker mVelocityTracker; 233 | 234 | //re-used list for selecting a swap target 235 | private List mSwapTargets; 236 | 237 | //re used for for sorting swap targets 238 | private List mDistances; 239 | 240 | /** 241 | * If drag & drop is supported, we use child drawing order to bring them to front. 242 | */ 243 | private RecyclerView.ChildDrawingOrderCallback mChildDrawingOrderCallback = null; 244 | 245 | /** 246 | * This keeps a reference to the child dragged by the user. Even after user stops dragging, 247 | * until view reaches its final position (end of recover animation), we keep a reference so 248 | * that it can be drawn above other children. 249 | */ 250 | View mOverdrawChild = null; 251 | 252 | /** 253 | * We cache the position of the overdraw child to avoid recalculating it each time child 254 | * position callback is called. This value is invalidated whenever a child is attached or 255 | * detached. 256 | */ 257 | int mOverdrawChildPosition = -1; 258 | 259 | /** 260 | * Used to detect long press. 261 | */ 262 | GestureDetectorCompat mGestureDetector; 263 | 264 | private final RecyclerView.OnItemTouchListener mOnItemTouchListener = new RecyclerView.OnItemTouchListener() { 265 | @Override 266 | public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) { 267 | if (landUpAssignView(event)) { 268 | mGestureDetector.onTouchEvent(event); 269 | } 270 | if (DEBUG) { 271 | Log.d(TAG, "intercept: x:" + event.getX() + ",y:" + event.getY() + ", " + event); 272 | } 273 | final int action = event.getActionMasked(); 274 | if (action == MotionEvent.ACTION_DOWN) { 275 | mActivePointerId = event.getPointerId(0); 276 | mInitialTouchX = event.getX(); 277 | mInitialTouchY = event.getY(); 278 | obtainVelocityTracker(); 279 | if (mSelected == null) { 280 | final RecoverAnimation animation = findAnimation(event); 281 | if (animation != null) { 282 | mInitialTouchX -= animation.mX; 283 | mInitialTouchY -= animation.mY; 284 | endRecoverAnimation(animation.mViewHolder, true); 285 | if (mPendingCleanup.remove(animation.mViewHolder.itemView)) { 286 | mCallback.clearView(mRecyclerView, animation.mViewHolder); 287 | } 288 | select(animation.mViewHolder, animation.mActionState); 289 | updateDxDy(event, mSelectedFlags, 0); 290 | } 291 | } 292 | } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { 293 | mActivePointerId = ACTIVE_POINTER_ID_NONE; 294 | select(null, ACTION_STATE_IDLE); 295 | } else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) { 296 | // in a non scroll orientation, if distance change is above threshold, we 297 | // can select the item 298 | final int index = event.findPointerIndex(mActivePointerId); 299 | if (DEBUG) { 300 | Log.d(TAG, "pointer index " + index); 301 | } 302 | if (index >= 0) { 303 | checkSelectForSwipe(action, event, index); 304 | } 305 | } 306 | if (mVelocityTracker != null) { 307 | mVelocityTracker.addMovement(event); 308 | } 309 | return mSelected != null; 310 | } 311 | 312 | @Override 313 | public void onTouchEvent(RecyclerView recyclerView, MotionEvent event) { 314 | if (landUpAssignView(event)) { 315 | mGestureDetector.onTouchEvent(event); 316 | } 317 | if (DEBUG) { 318 | Log.d(TAG, 319 | "on touch: x:" + mInitialTouchX + ",y:" + mInitialTouchY + ", :" + event); 320 | } 321 | if (mVelocityTracker != null) { 322 | mVelocityTracker.addMovement(event); 323 | } 324 | if (mActivePointerId == ACTIVE_POINTER_ID_NONE) { 325 | return; 326 | } 327 | final int action = event.getActionMasked(); 328 | final int activePointerIndex = event.findPointerIndex(mActivePointerId); 329 | if (activePointerIndex >= 0) { 330 | checkSelectForSwipe(action, event, activePointerIndex); 331 | } 332 | RecyclerView.ViewHolder viewHolder = mSelected; 333 | if (viewHolder == null) { 334 | return; 335 | } 336 | switch (action) { 337 | case MotionEvent.ACTION_MOVE: { 338 | // Find the index of the active pointer and fetch its position 339 | if (activePointerIndex >= 0) { 340 | updateDxDy(event, mSelectedFlags, activePointerIndex); 341 | moveIfNecessary(viewHolder); 342 | mRecyclerView.removeCallbacks(mScrollRunnable); 343 | mScrollRunnable.run(); 344 | mRecyclerView.invalidate(); 345 | } 346 | break; 347 | } 348 | case MotionEvent.ACTION_CANCEL: 349 | if (mVelocityTracker != null) { 350 | mVelocityTracker.clear(); 351 | } 352 | // fall through 353 | case MotionEvent.ACTION_UP: 354 | select(null, ACTION_STATE_IDLE); 355 | mActivePointerId = ACTIVE_POINTER_ID_NONE; 356 | break; 357 | case MotionEvent.ACTION_POINTER_UP: { 358 | final int pointerIndex = event.getActionIndex(); 359 | final int pointerId = event.getPointerId(pointerIndex); 360 | if (pointerId == mActivePointerId) { 361 | // This was our active pointer going up. Choose a new 362 | // active pointer and adjust accordingly. 363 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 364 | mActivePointerId = event.getPointerId(newPointerIndex); 365 | updateDxDy(event, mSelectedFlags, pointerIndex); 366 | } 367 | break; 368 | } 369 | } 370 | } 371 | 372 | @Override 373 | public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { 374 | if (!disallowIntercept) { 375 | return; 376 | } 377 | select(null, ACTION_STATE_IDLE); 378 | } 379 | }; 380 | 381 | /** 382 | * Temporary rect instance that is used when we need to lookup Item decorations. 383 | */ 384 | private Rect mTmpRect; 385 | 386 | /** 387 | * When user started to drag scroll. Reset when we don't scroll 388 | */ 389 | private long mDragScrollStartTimeInMs; 390 | 391 | /** 392 | * Creates an ItemTouchHelper that will work with the given Callback. 393 | *

394 | * You can attach ItemTouchHelper to a RecyclerView via 395 | * {@link #attachToRecyclerView(RecyclerView)}. Upon attaching, it will add an item decoration, 396 | * an onItemTouchListener and a Child attach / detach listener to the RecyclerView. 397 | * 398 | * @param callback The Callback which controls the behavior of this touch helper. 399 | */ 400 | public ItemTouchHelper(Callback callback) { 401 | mCallback = callback; 402 | } 403 | 404 | private static boolean hitTest(View child, float x, float y, float left, float top) { 405 | return x >= left 406 | && x <= left + child.getWidth() 407 | && y >= top 408 | && y <= top + child.getHeight(); 409 | } 410 | 411 | /** 412 | * Attaches the ItemTouchHelper to the provided RecyclerView. If TouchHelper is already 413 | * attached to a RecyclerView, it will first detach from the previous one. You can call this 414 | * method with {@code null} to detach it from the current RecyclerView. 415 | * 416 | * @param recyclerView The RecyclerView instance to which you want to add this helper or 417 | * {@code null} if you want to remove ItemTouchHelper from the current 418 | * RecyclerView. 419 | */ 420 | public void attachToRecyclerView(@Nullable RecyclerView recyclerView) { 421 | if (mRecyclerView == recyclerView) { 422 | return; // nothing to do 423 | } 424 | if (mRecyclerView != null) { 425 | destroyCallbacks(); 426 | } 427 | mRecyclerView = recyclerView; 428 | if (mRecyclerView != null) { 429 | final Resources resources = recyclerView.getResources(); 430 | mSwipeEscapeVelocity = resources 431 | .getDimension(android.support.v7.recyclerview.R.dimen.item_touch_helper_swipe_escape_velocity); 432 | mMaxSwipeVelocity = resources 433 | .getDimension(android.support.v7.recyclerview.R.dimen.item_touch_helper_swipe_escape_max_velocity); 434 | setupCallbacks(); 435 | } 436 | } 437 | 438 | private void setupCallbacks() { 439 | ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext()); 440 | mSlop = vc.getScaledTouchSlop(); 441 | mRecyclerView.addItemDecoration(this); 442 | mRecyclerView.addOnItemTouchListener(mOnItemTouchListener); 443 | mRecyclerView.addOnChildAttachStateChangeListener(this); 444 | initGestureDetector(); 445 | } 446 | 447 | private void destroyCallbacks() { 448 | mRecyclerView.removeItemDecoration(this); 449 | mRecyclerView.removeOnItemTouchListener(mOnItemTouchListener); 450 | mRecyclerView.removeOnChildAttachStateChangeListener(this); 451 | // clean all attached 452 | final int recoverAnimSize = mRecoverAnimations.size(); 453 | for (int i = recoverAnimSize - 1; i >= 0; i--) { 454 | final RecoverAnimation recoverAnimation = mRecoverAnimations.get(0); 455 | mCallback.clearView(mRecyclerView, recoverAnimation.mViewHolder); 456 | } 457 | mRecoverAnimations.clear(); 458 | mOverdrawChild = null; 459 | mOverdrawChildPosition = -1; 460 | releaseVelocityTracker(); 461 | } 462 | 463 | private void initGestureDetector() { 464 | if (mGestureDetector != null) { 465 | return; 466 | } 467 | mGestureDetector = new GestureDetectorCompat(mRecyclerView.getContext(), 468 | new ItemTouchHelperGestureListener()); 469 | } 470 | 471 | private void getSelectedDxDy(float[] outPosition) { 472 | if ((mSelectedFlags & (LEFT | RIGHT)) != 0) { 473 | outPosition[0] = mSelectedStartX + mDx - mSelected.itemView.getLeft(); 474 | } else { 475 | outPosition[0] = mSelected.itemView.getTranslationX(); 476 | } 477 | if ((mSelectedFlags & (UP | DOWN)) != 0) { 478 | outPosition[1] = mSelectedStartY + mDy - mSelected.itemView.getTop(); 479 | } else { 480 | outPosition[1] = mSelected.itemView.getTranslationY(); 481 | } 482 | } 483 | 484 | @Override 485 | public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { 486 | float dx = 0, dy = 0; 487 | if (mSelected != null) { 488 | getSelectedDxDy(mTmpPosition); 489 | dx = mTmpPosition[0]; 490 | dy = mTmpPosition[1]; 491 | } 492 | mCallback.onDrawOver(c, parent, mSelected, 493 | mRecoverAnimations, mActionState, dx, dy); 494 | } 495 | 496 | @Override 497 | public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { 498 | // we don't know if RV changed something so we should invalidate this index. 499 | mOverdrawChildPosition = -1; 500 | float dx = 0, dy = 0; 501 | if (mSelected != null) { 502 | getSelectedDxDy(mTmpPosition); 503 | dx = mTmpPosition[0]; 504 | dy = mTmpPosition[1]; 505 | } 506 | mCallback.onDraw(c, parent, mSelected, 507 | mRecoverAnimations, mActionState, dx, dy); 508 | } 509 | 510 | /** 511 | * Starts dragging or swiping the given View. Call with null if you want to clear it. 512 | * 513 | * @param selected The ViewHolder to drag or swipe. Can be null if you want to cancel the 514 | * current action 515 | * @param actionState The type of action 516 | */ 517 | void select(RecyclerView.ViewHolder selected, int actionState) { 518 | if (selected == mSelected && actionState == mActionState) { 519 | return; 520 | } 521 | mDragScrollStartTimeInMs = Long.MIN_VALUE; 522 | final int prevActionState = mActionState; 523 | // prevent duplicate animations 524 | endRecoverAnimation(selected, true); 525 | mActionState = actionState; 526 | if (actionState == ACTION_STATE_DRAG) { 527 | // we remove after animation is complete. this means we only elevate the last drag 528 | // child but that should perform good enough as it is very hard to start dragging a 529 | // new child before the previous one settles. 530 | mOverdrawChild = selected.itemView; 531 | addChildDrawingOrderCallback(); 532 | } 533 | int actionStateMask = (1 << (DIRECTION_FLAG_COUNT + DIRECTION_FLAG_COUNT * actionState)) 534 | - 1; 535 | boolean preventLayout = false; 536 | 537 | if (mSelected != null) { 538 | final RecyclerView.ViewHolder prevSelected = mSelected; 539 | if (prevSelected.itemView.getParent() != null) { 540 | final int swipeDir = prevActionState == ACTION_STATE_DRAG ? 0 541 | : swipeIfNecessary(prevSelected); 542 | releaseVelocityTracker(); 543 | // find where we should animate to 544 | final float targetTranslateX, targetTranslateY; 545 | int animationType; 546 | switch (swipeDir) { 547 | case LEFT: 548 | case RIGHT: 549 | case START: 550 | case END: 551 | targetTranslateY = 0; 552 | targetTranslateX = Math.signum(mDx) * mRecyclerView.getWidth(); 553 | break; 554 | case UP: 555 | case DOWN: 556 | targetTranslateX = 0; 557 | targetTranslateY = Math.signum(mDy) * mRecyclerView.getHeight(); 558 | break; 559 | default: 560 | targetTranslateX = 0; 561 | targetTranslateY = 0; 562 | } 563 | if (prevActionState == ACTION_STATE_DRAG) { 564 | animationType = ANIMATION_TYPE_DRAG; 565 | } else if (swipeDir > 0) { 566 | animationType = ANIMATION_TYPE_SWIPE_SUCCESS; 567 | } else { 568 | animationType = ANIMATION_TYPE_SWIPE_CANCEL; 569 | } 570 | getSelectedDxDy(mTmpPosition); 571 | final float currentTranslateX = mTmpPosition[0]; 572 | final float currentTranslateY = mTmpPosition[1]; 573 | final RecoverAnimation rv = new RecoverAnimation(prevSelected, animationType, 574 | prevActionState, currentTranslateX, currentTranslateY, 575 | targetTranslateX, targetTranslateY) { 576 | @Override 577 | public void onAnimationEnd(Animator animation) { 578 | super.onAnimationEnd(animation); 579 | if (this.mOverridden) { 580 | return; 581 | } 582 | if (swipeDir <= 0) { 583 | // this is a drag or failed swipe. recover immediately 584 | mCallback.clearView(mRecyclerView, prevSelected); 585 | // full cleanup will happen on onDrawOver 586 | } else { 587 | // wait until remove animation is complete. 588 | mPendingCleanup.add(prevSelected.itemView); 589 | mIsPendingCleanup = true; 590 | if (swipeDir > 0) { 591 | // Animation might be ended by other animators during a layout. 592 | // We defer callback to avoid editing adapter during a layout. 593 | postDispatchSwipe(this, swipeDir); 594 | } 595 | } 596 | // removed from the list after it is drawn for the last time 597 | if (mOverdrawChild == prevSelected.itemView) { 598 | removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView); 599 | } 600 | } 601 | }; 602 | final long duration = mCallback.getAnimationDuration(mRecyclerView, animationType, 603 | targetTranslateX - currentTranslateX, targetTranslateY - currentTranslateY); 604 | rv.setDuration(duration); 605 | mRecoverAnimations.add(rv); 606 | rv.start(); 607 | preventLayout = true; 608 | } else { 609 | removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView); 610 | mCallback.clearView(mRecyclerView, prevSelected); 611 | } 612 | mSelected = null; 613 | } 614 | if (selected != null) { 615 | mSelectedFlags = 616 | (mCallback.getAbsoluteMovementFlags(mRecyclerView, selected) & actionStateMask) 617 | >> (mActionState * DIRECTION_FLAG_COUNT); 618 | mSelectedStartX = selected.itemView.getLeft(); 619 | mSelectedStartY = selected.itemView.getTop(); 620 | mSelected = selected; 621 | 622 | if (actionState == ACTION_STATE_DRAG) { 623 | //这个是给出触感反馈,震动的效果! 624 | mSelected.itemView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 625 | } 626 | } 627 | final ViewParent rvParent = mRecyclerView.getParent(); 628 | if (rvParent != null) { 629 | rvParent.requestDisallowInterceptTouchEvent(mSelected != null); 630 | } 631 | if (!preventLayout) { 632 | mRecyclerView.getLayoutManager().requestSimpleAnimationsInNextLayout(); 633 | } 634 | mCallback.onSelectedChanged(mSelected, mActionState); 635 | mRecyclerView.invalidate(); 636 | } 637 | 638 | void postDispatchSwipe(final RecoverAnimation anim, final int swipeDir) { 639 | // wait until animations are complete. 640 | mRecyclerView.post(new Runnable() { 641 | @Override 642 | public void run() { 643 | if (mRecyclerView != null && mRecyclerView.isAttachedToWindow() 644 | && !anim.mOverridden 645 | && anim.mViewHolder.getAdapterPosition() != RecyclerView.NO_POSITION) { 646 | final RecyclerView.ItemAnimator animator = mRecyclerView.getItemAnimator(); 647 | // if animator is running or we have other active recover animations, we try 648 | // not to call onSwiped because DefaultItemAnimator is not good at merging 649 | // animations. Instead, we wait and batch. 650 | if ((animator == null || !animator.isRunning(null)) 651 | && !hasRunningRecoverAnim()) { 652 | mCallback.onSwiped(anim.mViewHolder, swipeDir); 653 | } else { 654 | mRecyclerView.post(this); 655 | } 656 | } 657 | } 658 | }); 659 | } 660 | 661 | boolean hasRunningRecoverAnim() { 662 | final int size = mRecoverAnimations.size(); 663 | for (int i = 0; i < size; i++) { 664 | if (!mRecoverAnimations.get(i).mEnded) { 665 | return true; 666 | } 667 | } 668 | return false; 669 | } 670 | 671 | /** 672 | * If user drags the view to the edge, trigger a scroll if necessary. 673 | */ 674 | boolean scrollIfNecessary() { 675 | if (mSelected == null) { 676 | mDragScrollStartTimeInMs = Long.MIN_VALUE; 677 | return false; 678 | } 679 | final long now = System.currentTimeMillis(); 680 | final long scrollDuration = mDragScrollStartTimeInMs 681 | == Long.MIN_VALUE ? 0 : now - mDragScrollStartTimeInMs; 682 | RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); 683 | if (mTmpRect == null) { 684 | mTmpRect = new Rect(); 685 | } 686 | int scrollX = 0; 687 | int scrollY = 0; 688 | lm.calculateItemDecorationsForChild(mSelected.itemView, mTmpRect); 689 | if (lm.canScrollHorizontally()) { 690 | int curX = (int) (mSelectedStartX + mDx); 691 | final int leftDiff = curX - mTmpRect.left - mRecyclerView.getPaddingLeft(); 692 | if (mDx < 0 && leftDiff < 0) { 693 | scrollX = leftDiff; 694 | } else if (mDx > 0) { 695 | final int rightDiff = 696 | curX + mSelected.itemView.getWidth() + mTmpRect.right 697 | - (mRecyclerView.getWidth() - mRecyclerView.getPaddingRight()); 698 | if (rightDiff > 0) { 699 | scrollX = rightDiff; 700 | } 701 | } 702 | } 703 | if (lm.canScrollVertically()) { 704 | int curY = (int) (mSelectedStartY + mDy); 705 | final int topDiff = curY - mTmpRect.top - mRecyclerView.getPaddingTop(); 706 | if (mDy < 0 && topDiff < 0) { 707 | scrollY = topDiff; 708 | } else if (mDy > 0) { 709 | final int bottomDiff = curY + mSelected.itemView.getHeight() + mTmpRect.bottom 710 | - (mRecyclerView.getHeight() - mRecyclerView.getPaddingBottom()); 711 | if (bottomDiff > 0) { 712 | scrollY = bottomDiff; 713 | } 714 | } 715 | } 716 | if (scrollX != 0) { 717 | scrollX = mCallback.interpolateOutOfBoundsScroll(mRecyclerView, 718 | mSelected.itemView.getWidth(), scrollX, 719 | mRecyclerView.getWidth(), scrollDuration); 720 | } 721 | if (scrollY != 0) { 722 | scrollY = mCallback.interpolateOutOfBoundsScroll(mRecyclerView, 723 | mSelected.itemView.getHeight(), scrollY, 724 | mRecyclerView.getHeight(), scrollDuration); 725 | } 726 | if (scrollX != 0 || scrollY != 0) { 727 | if (mDragScrollStartTimeInMs == Long.MIN_VALUE) { 728 | mDragScrollStartTimeInMs = now; 729 | } 730 | mRecyclerView.scrollBy(scrollX, scrollY); 731 | return true; 732 | } 733 | mDragScrollStartTimeInMs = Long.MIN_VALUE; 734 | return false; 735 | } 736 | 737 | private List findSwapTargets(RecyclerView.ViewHolder viewHolder) { 738 | if (mSwapTargets == null) { 739 | mSwapTargets = new ArrayList(); 740 | mDistances = new ArrayList(); 741 | } else { 742 | mSwapTargets.clear(); 743 | mDistances.clear(); 744 | } 745 | final int margin = mCallback.getBoundingBoxMargin(); 746 | final int left = Math.round(mSelectedStartX + mDx) - margin; 747 | final int top = Math.round(mSelectedStartY + mDy) - margin; 748 | final int right = left + viewHolder.itemView.getWidth() + 2 * margin; 749 | final int bottom = top + viewHolder.itemView.getHeight() + 2 * margin; 750 | final int centerX = (left + right) / 2; 751 | final int centerY = (top + bottom) / 2; 752 | final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); 753 | final int childCount = lm.getChildCount(); 754 | for (int i = 0; i < childCount; i++) { 755 | View other = lm.getChildAt(i); 756 | if (other == viewHolder.itemView) { 757 | continue; //myself! 758 | } 759 | if (other.getBottom() < top || other.getTop() > bottom 760 | || other.getRight() < left || other.getLeft() > right) { 761 | continue; 762 | } 763 | final RecyclerView.ViewHolder otherVh = mRecyclerView.getChildViewHolder(other); 764 | if (mCallback.canDropOver(mRecyclerView, mSelected, otherVh)) { 765 | // find the index to add 766 | final int dx = Math.abs(centerX - (other.getLeft() + other.getRight()) / 2); 767 | final int dy = Math.abs(centerY - (other.getTop() + other.getBottom()) / 2); 768 | final int dist = dx * dx + dy * dy; 769 | 770 | int pos = 0; 771 | final int cnt = mSwapTargets.size(); 772 | for (int j = 0; j < cnt; j++) { 773 | if (dist > mDistances.get(j)) { 774 | pos++; 775 | } else { 776 | break; 777 | } 778 | } 779 | mSwapTargets.add(pos, otherVh); 780 | mDistances.add(pos, dist); 781 | } 782 | } 783 | return mSwapTargets; 784 | } 785 | 786 | /** 787 | * Checks if we should swap w/ another view holder. 788 | */ 789 | void moveIfNecessary(RecyclerView.ViewHolder viewHolder) { 790 | if (mRecyclerView.isLayoutRequested()) { 791 | return; 792 | } 793 | if (mActionState != ACTION_STATE_DRAG) { 794 | return; 795 | } 796 | 797 | final float threshold = mCallback.getMoveThreshold(viewHolder); 798 | final int x = (int) (mSelectedStartX + mDx); 799 | final int y = (int) (mSelectedStartY + mDy); 800 | if (Math.abs(y - viewHolder.itemView.getTop()) < viewHolder.itemView.getHeight() * threshold 801 | && Math.abs(x - viewHolder.itemView.getLeft()) 802 | < viewHolder.itemView.getWidth() * threshold) { 803 | return; 804 | } 805 | List swapTargets = findSwapTargets(viewHolder); 806 | if (swapTargets.size() == 0) { 807 | return; 808 | } 809 | // may swap. 810 | RecyclerView.ViewHolder target = mCallback.chooseDropTarget(viewHolder, swapTargets, x, y); 811 | if (target == null) { 812 | mSwapTargets.clear(); 813 | mDistances.clear(); 814 | return; 815 | } 816 | final int toPosition = target.getAdapterPosition(); 817 | final int fromPosition = viewHolder.getAdapterPosition(); 818 | if (mCallback.onMove(mRecyclerView, viewHolder, target)) { 819 | // keep target visible 820 | mCallback.onMoved(mRecyclerView, viewHolder, fromPosition, 821 | target, toPosition, x, y); 822 | } 823 | } 824 | 825 | @Override 826 | public void onChildViewAttachedToWindow(View view) { 827 | } 828 | 829 | @Override 830 | public void onChildViewDetachedFromWindow(View view) { 831 | removeChildDrawingOrderCallbackIfNecessary(view); 832 | final RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(view); 833 | if (holder == null) { 834 | return; 835 | } 836 | if (mSelected != null && holder == mSelected) { 837 | select(null, ACTION_STATE_IDLE); 838 | } else { 839 | endRecoverAnimation(holder, false); // this may push it into pending cleanup list. 840 | if (mPendingCleanup.remove(holder.itemView)) { 841 | mCallback.clearView(mRecyclerView, holder); 842 | } 843 | } 844 | } 845 | 846 | /** 847 | * Returns the animation type or 0 if cannot be found. 848 | */ 849 | int endRecoverAnimation(RecyclerView.ViewHolder viewHolder, boolean override) { 850 | final int recoverAnimSize = mRecoverAnimations.size(); 851 | for (int i = recoverAnimSize - 1; i >= 0; i--) { 852 | final RecoverAnimation anim = mRecoverAnimations.get(i); 853 | if (anim.mViewHolder == viewHolder) { 854 | anim.mOverridden |= override; 855 | if (!anim.mEnded) { 856 | anim.cancel(); 857 | } 858 | mRecoverAnimations.remove(i); 859 | return anim.mAnimationType; 860 | } 861 | } 862 | return 0; 863 | } 864 | 865 | @Override 866 | public void getItemOffsets(Rect outRect, View view, RecyclerView parent, 867 | RecyclerView.State state) { 868 | outRect.setEmpty(); 869 | } 870 | 871 | void obtainVelocityTracker() { 872 | if (mVelocityTracker != null) { 873 | mVelocityTracker.recycle(); 874 | } 875 | mVelocityTracker = VelocityTracker.obtain(); 876 | } 877 | 878 | private void releaseVelocityTracker() { 879 | if (mVelocityTracker != null) { 880 | mVelocityTracker.recycle(); 881 | mVelocityTracker = null; 882 | } 883 | } 884 | 885 | private RecyclerView.ViewHolder findSwipedView(MotionEvent motionEvent) { 886 | final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); 887 | if (mActivePointerId == ACTIVE_POINTER_ID_NONE) { 888 | return null; 889 | } 890 | final int pointerIndex = motionEvent.findPointerIndex(mActivePointerId); 891 | final float dx = motionEvent.getX(pointerIndex) - mInitialTouchX; 892 | final float dy = motionEvent.getY(pointerIndex) - mInitialTouchY; 893 | final float absDx = Math.abs(dx); 894 | final float absDy = Math.abs(dy); 895 | 896 | if (absDx < mSlop && absDy < mSlop) { 897 | return null; 898 | } 899 | if (absDx > absDy && lm.canScrollHorizontally()) { 900 | return null; 901 | } else if (absDy > absDx && lm.canScrollVertically()) { 902 | return null; 903 | } 904 | View child = findChildView(motionEvent); 905 | if (child == null) { 906 | return null; 907 | } 908 | return mRecyclerView.getChildViewHolder(child); 909 | } 910 | 911 | /** 912 | * Checks whether we should select a View for swiping. 913 | */ 914 | boolean checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) { 915 | if (mSelected != null || action != MotionEvent.ACTION_MOVE 916 | || mActionState == ACTION_STATE_DRAG || !mCallback.isItemViewSwipeEnabled()) { 917 | return false; 918 | } 919 | if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) { 920 | return false; 921 | } 922 | final RecyclerView.ViewHolder vh = findSwipedView(motionEvent); 923 | if (vh == null) { 924 | return false; 925 | } 926 | final int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh); 927 | 928 | final int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK) 929 | >> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE); 930 | 931 | if (swipeFlags == 0) { 932 | return false; 933 | } 934 | 935 | // mDx and mDy are only set in allowed directions. We use custom x/y here instead of 936 | // updateDxDy to avoid swiping if user moves more in the other direction 937 | final float x = motionEvent.getX(pointerIndex); 938 | final float y = motionEvent.getY(pointerIndex); 939 | 940 | // Calculate the distance moved 941 | final float dx = x - mInitialTouchX; 942 | final float dy = y - mInitialTouchY; 943 | // swipe target is chose w/o applying flags so it does not really check if swiping in that 944 | // direction is allowed. This why here, we use mDx mDy to check slope value again. 945 | final float absDx = Math.abs(dx); 946 | final float absDy = Math.abs(dy); 947 | 948 | if (absDx < mSlop && absDy < mSlop) { 949 | return false; 950 | } 951 | if (absDx > absDy) { 952 | if (dx < 0 && (swipeFlags & LEFT) == 0) { 953 | return false; 954 | } 955 | if (dx > 0 && (swipeFlags & RIGHT) == 0) { 956 | return false; 957 | } 958 | } else { 959 | if (dy < 0 && (swipeFlags & UP) == 0) { 960 | return false; 961 | } 962 | if (dy > 0 && (swipeFlags & DOWN) == 0) { 963 | return false; 964 | } 965 | } 966 | mDx = mDy = 0f; 967 | mActivePointerId = motionEvent.getPointerId(0); 968 | select(vh, ACTION_STATE_SWIPE); 969 | return true; 970 | } 971 | 972 | View findChildView(MotionEvent event) { 973 | // first check elevated views, if none, then call RV 974 | final float x = event.getX(); 975 | final float y = event.getY(); 976 | if (mSelected != null) { 977 | final View selectedView = mSelected.itemView; 978 | if (hitTest(selectedView, x, y, mSelectedStartX + mDx, mSelectedStartY + mDy)) { 979 | return selectedView; 980 | } 981 | } 982 | for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) { 983 | final RecoverAnimation anim = mRecoverAnimations.get(i); 984 | final View view = anim.mViewHolder.itemView; 985 | if (hitTest(view, x, y, anim.mX, anim.mY)) { 986 | return view; 987 | } 988 | } 989 | return mRecyclerView.findChildViewUnder(x, y); 990 | } 991 | 992 | /** 993 | * Starts dragging the provided ViewHolder. By default, ItemTouchHelper starts a drag when a 994 | * View is long pressed. You can disable that behavior by overriding 995 | * {@link android.support.v7.widget.helper.ItemTouchHelper.Callback#isLongPressDragEnabled()}. 996 | *

997 | * For this method to work: 998 | *

    999 | *
  • The provided ViewHolder must be a child of the RecyclerView to which this 1000 | * ItemTouchHelper 1001 | * is attached.
  • 1002 | *
  • {@link android.support.v7.widget.helper.ItemTouchHelper.Callback} must have dragging enabled.
  • 1003 | *
  • There must be a previous touch event that was reported to the ItemTouchHelper 1004 | * through RecyclerView's ItemTouchListener mechanism. As long as no other ItemTouchListener 1005 | * grabs previous events, this should work as expected.
  • 1006 | *
1007 | *

1008 | * For example, if you would like to let your user to be able to drag an Item by touching one 1009 | * of its descendants, you may implement it as follows: 1010 | *

1011 |      *     viewHolder.dragButton.setOnTouchListener(new View.OnTouchListener() {
1012 |      *         public boolean onTouch(View v, MotionEvent event) {
1013 |      *             if (MotionEvent.getActionMasked(event) == MotionEvent.ACTION_DOWN) {
1014 |      *                 mItemTouchHelper.startDrag(viewHolder);
1015 |      *             }
1016 |      *             return false;
1017 |      *         }
1018 |      *     });
1019 |      * 
1020 | *

1021 | * 1022 | * @param viewHolder The ViewHolder to start dragging. It must be a direct child of 1023 | * RecyclerView. 1024 | * @see android.support.v7.widget.helper.ItemTouchHelper.Callback#isItemViewSwipeEnabled() 1025 | */ 1026 | public void startDrag(RecyclerView.ViewHolder viewHolder) { 1027 | if (!mCallback.hasDragFlag(mRecyclerView, viewHolder)) { 1028 | Log.e(TAG, "Start drag has been called but dragging is not enabled"); 1029 | return; 1030 | } 1031 | if (viewHolder.itemView.getParent() != mRecyclerView) { 1032 | Log.e(TAG, "Start drag has been called with a view holder which is not a child of " 1033 | + "the RecyclerView which is controlled by this ItemTouchHelper."); 1034 | return; 1035 | } 1036 | obtainVelocityTracker(); 1037 | mDx = mDy = 0f; 1038 | select(viewHolder, ACTION_STATE_DRAG); 1039 | } 1040 | 1041 | /** 1042 | * Starts swiping the provided ViewHolder. By default, ItemTouchHelper starts swiping a View 1043 | * when user swipes their finger (or mouse pointer) over the View. You can disable this 1044 | * behavior 1045 | * by overriding {@link android.support.v7.widget.helper.ItemTouchHelper.Callback} 1046 | *

1047 | * For this method to work: 1048 | *

    1049 | *
  • The provided ViewHolder must be a child of the RecyclerView to which this 1050 | * ItemTouchHelper is attached.
  • 1051 | *
  • {@link android.support.v7.widget.helper.ItemTouchHelper.Callback} must have swiping enabled.
  • 1052 | *
  • There must be a previous touch event that was reported to the ItemTouchHelper 1053 | * through RecyclerView's ItemTouchListener mechanism. As long as no other ItemTouchListener 1054 | * grabs previous events, this should work as expected.
  • 1055 | *
1056 | *

1057 | * For example, if you would like to let your user to be able to swipe an Item by touching one 1058 | * of its descendants, you may implement it as follows: 1059 | *

1060 |      *     viewHolder.dragButton.setOnTouchListener(new View.OnTouchListener() {
1061 |      *         public boolean onTouch(View v, MotionEvent event) {
1062 |      *             if (MotionEvent.getActionMasked(event) == MotionEvent.ACTION_DOWN) {
1063 |      *                 mItemTouchHelper.startSwipe(viewHolder);
1064 |      *             }
1065 |      *             return false;
1066 |      *         }
1067 |      *     });
1068 |      * 
1069 | * 1070 | * @param viewHolder The ViewHolder to start swiping. It must be a direct child of 1071 | * RecyclerView. 1072 | */ 1073 | public void startSwipe(RecyclerView.ViewHolder viewHolder) { 1074 | if (!mCallback.hasSwipeFlag(mRecyclerView, viewHolder)) { 1075 | Log.e(TAG, "Start swipe has been called but swiping is not enabled"); 1076 | return; 1077 | } 1078 | if (viewHolder.itemView.getParent() != mRecyclerView) { 1079 | Log.e(TAG, "Start swipe has been called with a view holder which is not a child of " 1080 | + "the RecyclerView controlled by this ItemTouchHelper."); 1081 | return; 1082 | } 1083 | obtainVelocityTracker(); 1084 | mDx = mDy = 0f; 1085 | select(viewHolder, ACTION_STATE_SWIPE); 1086 | } 1087 | 1088 | RecoverAnimation findAnimation(MotionEvent event) { 1089 | if (mRecoverAnimations.isEmpty()) { 1090 | return null; 1091 | } 1092 | View target = findChildView(event); 1093 | for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) { 1094 | final RecoverAnimation anim = mRecoverAnimations.get(i); 1095 | if (anim.mViewHolder.itemView == target) { 1096 | return anim; 1097 | } 1098 | } 1099 | return null; 1100 | } 1101 | 1102 | void updateDxDy(MotionEvent ev, int directionFlags, int pointerIndex) { 1103 | final float x = ev.getX(pointerIndex); 1104 | final float y = ev.getY(pointerIndex); 1105 | 1106 | // Calculate the distance moved 1107 | mDx = x - mInitialTouchX; 1108 | mDy = y - mInitialTouchY; 1109 | if ((directionFlags & LEFT) == 0) { 1110 | mDx = Math.max(0, mDx); 1111 | } 1112 | if ((directionFlags & RIGHT) == 0) { 1113 | mDx = Math.min(0, mDx); 1114 | } 1115 | if ((directionFlags & UP) == 0) { 1116 | mDy = Math.max(0, mDy); 1117 | } 1118 | if ((directionFlags & DOWN) == 0) { 1119 | mDy = Math.min(0, mDy); 1120 | } 1121 | } 1122 | 1123 | private int swipeIfNecessary(RecyclerView.ViewHolder viewHolder) { 1124 | if (mActionState == ACTION_STATE_DRAG) { 1125 | return 0; 1126 | } 1127 | final int originalMovementFlags = mCallback.getMovementFlags(mRecyclerView, viewHolder); 1128 | final int absoluteMovementFlags = mCallback.convertToAbsoluteDirection( 1129 | originalMovementFlags, 1130 | ViewCompat.getLayoutDirection(mRecyclerView)); 1131 | final int flags = (absoluteMovementFlags 1132 | & ACTION_MODE_SWIPE_MASK) >> (ACTION_STATE_SWIPE * DIRECTION_FLAG_COUNT); 1133 | if (flags == 0) { 1134 | return 0; 1135 | } 1136 | final int originalFlags = (originalMovementFlags 1137 | & ACTION_MODE_SWIPE_MASK) >> (ACTION_STATE_SWIPE * DIRECTION_FLAG_COUNT); 1138 | int swipeDir; 1139 | if (Math.abs(mDx) > Math.abs(mDy)) { 1140 | if ((swipeDir = checkHorizontalSwipe(viewHolder, flags)) > 0) { 1141 | // if swipe dir is not in original flags, it should be the relative direction 1142 | if ((originalFlags & swipeDir) == 0) { 1143 | // convert to relative 1144 | return Callback.convertToRelativeDirection(swipeDir, 1145 | ViewCompat.getLayoutDirection(mRecyclerView)); 1146 | } 1147 | return swipeDir; 1148 | } 1149 | if ((swipeDir = checkVerticalSwipe(viewHolder, flags)) > 0) { 1150 | return swipeDir; 1151 | } 1152 | } else { 1153 | if ((swipeDir = checkVerticalSwipe(viewHolder, flags)) > 0) { 1154 | return swipeDir; 1155 | } 1156 | if ((swipeDir = checkHorizontalSwipe(viewHolder, flags)) > 0) { 1157 | // if swipe dir is not in original flags, it should be the relative direction 1158 | if ((originalFlags & swipeDir) == 0) { 1159 | // convert to relative 1160 | return Callback.convertToRelativeDirection(swipeDir, 1161 | ViewCompat.getLayoutDirection(mRecyclerView)); 1162 | } 1163 | return swipeDir; 1164 | } 1165 | } 1166 | return 0; 1167 | } 1168 | 1169 | private int checkHorizontalSwipe(RecyclerView.ViewHolder viewHolder, int flags) { 1170 | if ((flags & (LEFT | RIGHT)) != 0) { 1171 | final int dirFlag = mDx > 0 ? RIGHT : LEFT; 1172 | if (mVelocityTracker != null && mActivePointerId > -1) { 1173 | mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, 1174 | mCallback.getSwipeVelocityThreshold(mMaxSwipeVelocity)); 1175 | final float xVelocity = mVelocityTracker.getXVelocity(mActivePointerId); 1176 | final float yVelocity = mVelocityTracker.getYVelocity(mActivePointerId); 1177 | final int velDirFlag = xVelocity > 0f ? RIGHT : LEFT; 1178 | final float absXVelocity = Math.abs(xVelocity); 1179 | if ((velDirFlag & flags) != 0 && dirFlag == velDirFlag 1180 | && absXVelocity >= mCallback.getSwipeEscapeVelocity(mSwipeEscapeVelocity) 1181 | && absXVelocity > Math.abs(yVelocity)) { 1182 | return velDirFlag; 1183 | } 1184 | } 1185 | 1186 | final float threshold = mRecyclerView.getWidth() * mCallback 1187 | .getSwipeThreshold(viewHolder); 1188 | 1189 | if ((flags & dirFlag) != 0 && Math.abs(mDx) > threshold) { 1190 | return dirFlag; 1191 | } 1192 | } 1193 | return 0; 1194 | } 1195 | 1196 | private int checkVerticalSwipe(RecyclerView.ViewHolder viewHolder, int flags) { 1197 | if ((flags & (UP | DOWN)) != 0) { 1198 | final int dirFlag = mDy > 0 ? DOWN : UP; 1199 | if (mVelocityTracker != null && mActivePointerId > -1) { 1200 | mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, 1201 | mCallback.getSwipeVelocityThreshold(mMaxSwipeVelocity)); 1202 | final float xVelocity = mVelocityTracker.getXVelocity(mActivePointerId); 1203 | final float yVelocity = mVelocityTracker.getYVelocity(mActivePointerId); 1204 | final int velDirFlag = yVelocity > 0f ? DOWN : UP; 1205 | final float absYVelocity = Math.abs(yVelocity); 1206 | if ((velDirFlag & flags) != 0 && velDirFlag == dirFlag 1207 | && absYVelocity >= mCallback.getSwipeEscapeVelocity(mSwipeEscapeVelocity) 1208 | && absYVelocity > Math.abs(xVelocity)) { 1209 | return velDirFlag; 1210 | } 1211 | } 1212 | 1213 | final float threshold = mRecyclerView.getHeight() * mCallback 1214 | .getSwipeThreshold(viewHolder); 1215 | if ((flags & dirFlag) != 0 && Math.abs(mDy) > threshold) { 1216 | return dirFlag; 1217 | } 1218 | } 1219 | return 0; 1220 | } 1221 | 1222 | private void addChildDrawingOrderCallback() { 1223 | if (Build.VERSION.SDK_INT >= 21) { 1224 | return; // we use elevation on Lollipop 1225 | } 1226 | if (mChildDrawingOrderCallback == null) { 1227 | mChildDrawingOrderCallback = new RecyclerView.ChildDrawingOrderCallback() { 1228 | @Override 1229 | public int onGetChildDrawingOrder(int childCount, int i) { 1230 | if (mOverdrawChild == null) { 1231 | return i; 1232 | } 1233 | int childPosition = mOverdrawChildPosition; 1234 | if (childPosition == -1) { 1235 | childPosition = mRecyclerView.indexOfChild(mOverdrawChild); 1236 | mOverdrawChildPosition = childPosition; 1237 | } 1238 | if (i == childCount - 1) { 1239 | return childPosition; 1240 | } 1241 | return i < childPosition ? i : i + 1; 1242 | } 1243 | }; 1244 | } 1245 | mRecyclerView.setChildDrawingOrderCallback(mChildDrawingOrderCallback); 1246 | } 1247 | 1248 | void removeChildDrawingOrderCallbackIfNecessary(View view) { 1249 | if (view == mOverdrawChild) { 1250 | mOverdrawChild = null; 1251 | // only remove if we've added 1252 | if (mChildDrawingOrderCallback != null) { 1253 | mRecyclerView.setChildDrawingOrderCallback(null); 1254 | } 1255 | } 1256 | } 1257 | 1258 | /** 1259 | * An interface which can be implemented by LayoutManager for better integration with 1260 | * {@link android.support.v7.widget.helper.ItemTouchHelper}. 1261 | */ 1262 | public interface ViewDropHandler { 1263 | 1264 | /** 1265 | * Called by the {@link android.support.v7.widget.helper.ItemTouchHelper} after a View is dropped over another View. 1266 | *

1267 | * A LayoutManager should implement this interface to get ready for the upcoming move 1268 | * operation. 1269 | *

1270 | * For example, LinearLayoutManager sets up a "scrollToPositionWithOffset" calls so that 1271 | * the View under drag will be used as an anchor View while calculating the next layout, 1272 | * making layout stay consistent. 1273 | * 1274 | * @param view The View which is being dragged. It is very likely that user is still 1275 | * dragging this View so there might be other 1276 | * {@link #prepareForDrop(View, View, int, int)} after this one. 1277 | * @param target The target view which is being dropped on. 1278 | * @param x The left offset of the View that is being dragged. This value 1279 | * includes the movement caused by the user. 1280 | * @param y The top offset of the View that is being dragged. This value 1281 | * includes the movement caused by the user. 1282 | */ 1283 | void prepareForDrop(View view, View target, int x, int y); 1284 | } 1285 | 1286 | /** 1287 | * This class is the contract between ItemTouchHelper and your application. It lets you control 1288 | * which touch behaviors are enabled per each ViewHolder and also receive callbacks when user 1289 | * performs these actions. 1290 | *

1291 | * To control which actions user can take on each view, you should override 1292 | * {@link #getMovementFlags(RecyclerView, RecyclerView.ViewHolder)} and return appropriate set 1293 | * of direction flags. ({@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link #END}, 1294 | * {@link #UP}, {@link #DOWN}). You can use 1295 | * {@link #makeMovementFlags(int, int)} to easily construct it. Alternatively, you can use 1296 | * {@link android.support.v7.widget.helper.ItemTouchHelper.SimpleCallback}. 1297 | *

1298 | * If user drags an item, ItemTouchHelper will call 1299 | * {@link android.support.v7.widget.helper.ItemTouchHelper.Callback#onMove(RecyclerView, RecyclerView.ViewHolder, RecyclerView.ViewHolder) 1300 | * onMove(recyclerView, dragged, target)}. 1301 | * Upon receiving this callback, you should move the item from the old position 1302 | * ({@code dragged.getAdapterPosition()}) to new position ({@code target.getAdapterPosition()}) 1303 | * in your adapter and also call {@link RecyclerView.Adapter#notifyItemMoved(int, int)}. 1304 | * To control where a View can be dropped, you can override 1305 | * {@link #canDropOver(RecyclerView, RecyclerView.ViewHolder, RecyclerView.ViewHolder)}. When a 1306 | * dragging View overlaps multiple other views, Callback chooses the closest View with which 1307 | * dragged View might have changed positions. Although this approach works for many use cases, 1308 | * if you have a custom LayoutManager, you can override 1309 | * {@link #chooseDropTarget(RecyclerView.ViewHolder, List, int, int)} to select a 1310 | * custom drop target. 1311 | *

1312 | * When a View is swiped, ItemTouchHelper animates it until it goes out of bounds, then calls 1313 | * {@link #onSwiped(RecyclerView.ViewHolder, int)}. At this point, you should update your 1314 | * adapter (e.g. remove the item) and call related Adapter#notify event. 1315 | */ 1316 | @SuppressWarnings("UnusedParameters") 1317 | public abstract static class Callback { 1318 | 1319 | public static final int DEFAULT_DRAG_ANIMATION_DURATION = 200; 1320 | 1321 | public static final int DEFAULT_SWIPE_ANIMATION_DURATION = 250; 1322 | 1323 | static final int RELATIVE_DIR_FLAGS = START | END 1324 | | ((START | END) << DIRECTION_FLAG_COUNT) 1325 | | ((START | END) << (2 * DIRECTION_FLAG_COUNT)); 1326 | 1327 | private static final ItemTouchUIUtil sUICallback; 1328 | 1329 | private static final int ABS_HORIZONTAL_DIR_FLAGS = LEFT | RIGHT 1330 | | ((LEFT | RIGHT) << DIRECTION_FLAG_COUNT) 1331 | | ((LEFT | RIGHT) << (2 * DIRECTION_FLAG_COUNT)); 1332 | 1333 | private static final Interpolator sDragScrollInterpolator = new Interpolator() { 1334 | @Override 1335 | public float getInterpolation(float t) { 1336 | return t * t * t * t * t; 1337 | } 1338 | }; 1339 | 1340 | private static final Interpolator sDragViewScrollCapInterpolator = new Interpolator() { 1341 | @Override 1342 | public float getInterpolation(float t) { 1343 | t -= 1.0f; 1344 | return t * t * t * t * t + 1.0f; 1345 | } 1346 | }; 1347 | 1348 | /** 1349 | * Drag scroll speed keeps accelerating until this many milliseconds before being capped. 1350 | */ 1351 | private static final long DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000; 1352 | 1353 | private int mCachedMaxScrollSpeed = -1; 1354 | 1355 | static { 1356 | if (Build.VERSION.SDK_INT >= 21) { 1357 | sUICallback = new ItemTouchUIUtilImpl.Api21Impl(); 1358 | } else { 1359 | sUICallback = new ItemTouchUIUtilImpl.BaseImpl(); 1360 | } 1361 | } 1362 | 1363 | /** 1364 | * Returns the {@link ItemTouchUIUtil} that is used by the {@link android.support.v7.widget.helper.ItemTouchHelper.Callback} class for 1365 | * visual 1366 | * changes on Views in response to user interactions. {@link ItemTouchUIUtil} has different 1367 | * implementations for different platform versions. 1368 | *

1369 | * By default, {@link android.support.v7.widget.helper.ItemTouchHelper.Callback} applies these changes on 1370 | * {@link RecyclerView.ViewHolder#itemView}. 1371 | *

1372 | * For example, if you have a use case where you only want the text to move when user 1373 | * swipes over the view, you can do the following: 1374 | *

1375 |          *     public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder){
1376 |          *         getDefaultUIUtil().clearView(((ItemTouchViewHolder) viewHolder).textView);
1377 |          *     }
1378 |          *     public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
1379 |          *         if (viewHolder != null){
1380 |          *             getDefaultUIUtil().onSelected(((ItemTouchViewHolder) viewHolder).textView);
1381 |          *         }
1382 |          *     }
1383 |          *     public void onChildDraw(Canvas c, RecyclerView recyclerView,
1384 |          *             RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState,
1385 |          *             boolean isCurrentlyActive) {
1386 |          *         getDefaultUIUtil().onDraw(c, recyclerView,
1387 |          *                 ((ItemTouchViewHolder) viewHolder).textView, dX, dY,
1388 |          *                 actionState, isCurrentlyActive);
1389 |          *         return true;
1390 |          *     }
1391 |          *     public void onChildDrawOver(Canvas c, RecyclerView recyclerView,
1392 |          *             RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState,
1393 |          *             boolean isCurrentlyActive) {
1394 |          *         getDefaultUIUtil().onDrawOver(c, recyclerView,
1395 |          *                 ((ItemTouchViewHolder) viewHolder).textView, dX, dY,
1396 |          *                 actionState, isCurrentlyActive);
1397 |          *         return true;
1398 |          *     }
1399 |          * 
1400 | * 1401 | * @return The {@link ItemTouchUIUtil} instance that is used by the {@link android.support.v7.widget.helper.ItemTouchHelper.Callback} 1402 | */ 1403 | public static ItemTouchUIUtil getDefaultUIUtil() { 1404 | return sUICallback; 1405 | } 1406 | 1407 | /** 1408 | * Replaces a movement direction with its relative version by taking layout direction into 1409 | * account. 1410 | * 1411 | * @param flags The flag value that include any number of movement flags. 1412 | * @param layoutDirection The layout direction of the View. Can be obtained from 1413 | * {@link ViewCompat#getLayoutDirection(View)}. 1414 | * @return Updated flags which uses relative flags ({@link #START}, {@link #END}) instead 1415 | * of {@link #LEFT}, {@link #RIGHT}. 1416 | * @see #convertToAbsoluteDirection(int, int) 1417 | */ 1418 | public static int convertToRelativeDirection(int flags, int layoutDirection) { 1419 | int masked = flags & ABS_HORIZONTAL_DIR_FLAGS; 1420 | if (masked == 0) { 1421 | return flags; // does not have any abs flags, good. 1422 | } 1423 | flags &= ~masked; //remove left / right. 1424 | if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_LTR) { 1425 | // no change. just OR with 2 bits shifted mask and return 1426 | flags |= masked << 2; // START is 2 bits after LEFT, END is 2 bits after RIGHT. 1427 | return flags; 1428 | } else { 1429 | // add RIGHT flag as START 1430 | flags |= ((masked << 1) & ~ABS_HORIZONTAL_DIR_FLAGS); 1431 | // first clean RIGHT bit then add LEFT flag as END 1432 | flags |= ((masked << 1) & ABS_HORIZONTAL_DIR_FLAGS) << 2; 1433 | } 1434 | return flags; 1435 | } 1436 | 1437 | /** 1438 | * Convenience method to create movement flags. 1439 | *

1440 | * For instance, if you want to let your items be drag & dropped vertically and swiped 1441 | * left to be dismissed, you can call this method with: 1442 | * makeMovementFlags(UP | DOWN, LEFT); 1443 | * 1444 | * @param dragFlags The directions in which the item can be dragged. 1445 | * @param swipeFlags The directions in which the item can be swiped. 1446 | * @return Returns an integer composed of the given drag and swipe flags. 1447 | */ 1448 | public static int makeMovementFlags(int dragFlags, int swipeFlags) { 1449 | return makeFlag(ACTION_STATE_IDLE, swipeFlags | dragFlags) 1450 | | makeFlag(ACTION_STATE_SWIPE, swipeFlags) 1451 | | makeFlag(ACTION_STATE_DRAG, dragFlags); 1452 | } 1453 | 1454 | /** 1455 | * Shifts the given direction flags to the offset of the given action state. 1456 | * 1457 | * @param actionState The action state you want to get flags in. Should be one of 1458 | * {@link #ACTION_STATE_IDLE}, {@link #ACTION_STATE_SWIPE} or 1459 | * {@link #ACTION_STATE_DRAG}. 1460 | * @param directions The direction flags. Can be composed from {@link #UP}, {@link #DOWN}, 1461 | * {@link #RIGHT}, {@link #LEFT} {@link #START} and {@link #END}. 1462 | * @return And integer that represents the given directions in the provided actionState. 1463 | */ 1464 | public static int makeFlag(int actionState, int directions) { 1465 | return directions << (actionState * DIRECTION_FLAG_COUNT); 1466 | } 1467 | 1468 | /** 1469 | * Should return a composite flag which defines the enabled move directions in each state 1470 | * (idle, swiping, dragging). 1471 | *

1472 | * Instead of composing this flag manually, you can use {@link #makeMovementFlags(int, 1473 | * int)} 1474 | * or {@link #makeFlag(int, int)}. 1475 | *

1476 | * This flag is composed of 3 sets of 8 bits, where first 8 bits are for IDLE state, next 1477 | * 8 bits are for SWIPE state and third 8 bits are for DRAG state. 1478 | * Each 8 bit sections can be constructed by simply OR'ing direction flags defined in 1479 | * {@link android.support.v7.widget.helper.ItemTouchHelper}. 1480 | *

1481 | * For example, if you want it to allow swiping LEFT and RIGHT but only allow starting to 1482 | * swipe by swiping RIGHT, you can return: 1483 | *

1484 |          *      makeFlag(ACTION_STATE_IDLE, RIGHT) | makeFlag(ACTION_STATE_SWIPE, LEFT | RIGHT);
1485 |          * 
1486 | * This means, allow right movement while IDLE and allow right and left movement while 1487 | * swiping. 1488 | * 1489 | * @param recyclerView The RecyclerView to which ItemTouchHelper is attached. 1490 | * @param viewHolder The ViewHolder for which the movement information is necessary. 1491 | * @return flags specifying which movements are allowed on this ViewHolder. 1492 | * @see #makeMovementFlags(int, int) 1493 | * @see #makeFlag(int, int) 1494 | */ 1495 | public abstract int getMovementFlags(RecyclerView recyclerView, 1496 | RecyclerView.ViewHolder viewHolder); 1497 | 1498 | /** 1499 | * Converts a given set of flags to absolution direction which means {@link #START} and 1500 | * {@link #END} are replaced with {@link #LEFT} and {@link #RIGHT} depending on the layout 1501 | * direction. 1502 | * 1503 | * @param flags The flag value that include any number of movement flags. 1504 | * @param layoutDirection The layout direction of the RecyclerView. 1505 | * @return Updated flags which includes only absolute direction values. 1506 | */ 1507 | public int convertToAbsoluteDirection(int flags, int layoutDirection) { 1508 | int masked = flags & RELATIVE_DIR_FLAGS; 1509 | if (masked == 0) { 1510 | return flags; // does not have any relative flags, good. 1511 | } 1512 | flags &= ~masked; //remove start / end 1513 | if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_LTR) { 1514 | // no change. just OR with 2 bits shifted mask and return 1515 | flags |= masked >> 2; // START is 2 bits after LEFT, END is 2 bits after RIGHT. 1516 | return flags; 1517 | } else { 1518 | // add START flag as RIGHT 1519 | flags |= ((masked >> 1) & ~RELATIVE_DIR_FLAGS); 1520 | // first clean start bit then add END flag as LEFT 1521 | flags |= ((masked >> 1) & RELATIVE_DIR_FLAGS) >> 2; 1522 | } 1523 | return flags; 1524 | } 1525 | 1526 | final int getAbsoluteMovementFlags(RecyclerView recyclerView, 1527 | RecyclerView.ViewHolder viewHolder) { 1528 | final int flags = getMovementFlags(recyclerView, viewHolder); 1529 | return convertToAbsoluteDirection(flags, ViewCompat.getLayoutDirection(recyclerView)); 1530 | } 1531 | 1532 | boolean hasDragFlag(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { 1533 | final int flags = getAbsoluteMovementFlags(recyclerView, viewHolder); 1534 | return (flags & ACTION_MODE_DRAG_MASK) != 0; 1535 | } 1536 | 1537 | boolean hasSwipeFlag(RecyclerView recyclerView, 1538 | RecyclerView.ViewHolder viewHolder) { 1539 | final int flags = getAbsoluteMovementFlags(recyclerView, viewHolder); 1540 | return (flags & ACTION_MODE_SWIPE_MASK) != 0; 1541 | } 1542 | 1543 | /** 1544 | * Return true if the current ViewHolder can be dropped over the the target ViewHolder. 1545 | *

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

1550 | * Default implementation returns true. 1551 | * 1552 | * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to. 1553 | * @param current The ViewHolder that user is dragging. 1554 | * @param target The ViewHolder which is below the dragged ViewHolder. 1555 | * @return True if the dragged ViewHolder can be replaced with the target ViewHolder, false 1556 | * otherwise. 1557 | */ 1558 | public boolean canDropOver(RecyclerView recyclerView, RecyclerView.ViewHolder current, 1559 | RecyclerView.ViewHolder target) { 1560 | return true; 1561 | } 1562 | 1563 | /** 1564 | * Called when ItemTouchHelper wants to move the dragged item from its old position to 1565 | * the new position. 1566 | *

1567 | * If this method returns true, ItemTouchHelper assumes {@code viewHolder} has been moved 1568 | * to the adapter position of {@code target} ViewHolder 1569 | * ({@link RecyclerView.ViewHolder#getAdapterPosition() 1570 | * ViewHolder#getAdapterPosition()}). 1571 | *

1572 | * If you don't support drag & drop, this method will never be called. 1573 | * 1574 | * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to. 1575 | * @param viewHolder The ViewHolder which is being dragged by the user. 1576 | * @param target The ViewHolder over which the currently active item is being 1577 | * dragged. 1578 | * @return True if the {@code viewHolder} has been moved to the adapter position of 1579 | * {@code target}. 1580 | * @see #onMoved(RecyclerView, RecyclerView.ViewHolder, int, RecyclerView.ViewHolder, int, int, int) 1581 | */ 1582 | public abstract boolean onMove(RecyclerView recyclerView, 1583 | RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target); 1584 | 1585 | /** 1586 | * Returns whether ItemTouchHelper should start a drag and drop operation if an item is 1587 | * long pressed. 1588 | *

1589 | * Default value returns true but you may want to disable this if you want to start 1590 | * dragging on a custom view touch using {@link #startDrag(RecyclerView.ViewHolder)}. 1591 | * 1592 | * @return True if ItemTouchHelper should start dragging an item when it is long pressed, 1593 | * false otherwise. Default value is true. 1594 | * @see #startDrag(RecyclerView.ViewHolder) 1595 | */ 1596 | public boolean isLongPressDragEnabled() { 1597 | return true; 1598 | } 1599 | 1600 | /** 1601 | * Returns whether ItemTouchHelper should start a swipe operation if a pointer is swiped 1602 | * over the View. 1603 | *

1604 | * Default value returns true but you may want to disable this if you want to start 1605 | * swiping on a custom view touch using {@link #startSwipe(RecyclerView.ViewHolder)}. 1606 | * 1607 | * @return True if ItemTouchHelper should start swiping an item when user swipes a pointer 1608 | * over the View, false otherwise. Default value is true. 1609 | * @see #startSwipe(RecyclerView.ViewHolder) 1610 | */ 1611 | public boolean isItemViewSwipeEnabled() { 1612 | return true; 1613 | } 1614 | 1615 | /** 1616 | * When finding views under a dragged view, by default, ItemTouchHelper searches for views 1617 | * that overlap with the dragged View. By overriding this method, you can extend or shrink 1618 | * the search box. 1619 | * 1620 | * @return The extra margin to be added to the hit box of the dragged View. 1621 | */ 1622 | public int getBoundingBoxMargin() { 1623 | return 0; 1624 | } 1625 | 1626 | /** 1627 | * Returns the fraction that the user should move the View to be considered as swiped. 1628 | * The fraction is calculated with respect to RecyclerView's bounds. 1629 | *

1630 | * Default value is .5f, which means, to swipe a View, user must move the View at least 1631 | * half of RecyclerView's width or height, depending on the swipe direction. 1632 | * 1633 | * @param viewHolder The ViewHolder that is being dragged. 1634 | * @return A float value that denotes the fraction of the View size. Default value 1635 | * is .5f . 1636 | */ 1637 | public float getSwipeThreshold(RecyclerView.ViewHolder viewHolder) { 1638 | return .5f; 1639 | } 1640 | 1641 | /** 1642 | * Returns the fraction that the user should move the View to be considered as it is 1643 | * dragged. After a view is moved this amount, ItemTouchHelper starts checking for Views 1644 | * below it for a possible drop. 1645 | * 1646 | * @param viewHolder The ViewHolder that is being dragged. 1647 | * @return A float value that denotes the fraction of the View size. Default value is 1648 | * .5f . 1649 | */ 1650 | public float getMoveThreshold(RecyclerView.ViewHolder viewHolder) { 1651 | return .5f; 1652 | } 1653 | 1654 | /** 1655 | * Defines the minimum velocity which will be considered as a swipe action by the user. 1656 | *

1657 | * You can increase this value to make it harder to swipe or decrease it to make it easier. 1658 | * Keep in mind that ItemTouchHelper also checks the perpendicular velocity and makes sure 1659 | * current direction velocity is larger then the perpendicular one. Otherwise, user's 1660 | * movement is ambiguous. You can change the threshold by overriding 1661 | * {@link #getSwipeVelocityThreshold(float)}. 1662 | *

1663 | * The velocity is calculated in pixels per second. 1664 | *

1665 | * The default framework value is passed as a parameter so that you can modify it with a 1666 | * multiplier. 1667 | * 1668 | * @param defaultValue The default value (in pixels per second) used by the 1669 | * ItemTouchHelper. 1670 | * @return The minimum swipe velocity. The default implementation returns the 1671 | * defaultValue parameter. 1672 | * @see #getSwipeVelocityThreshold(float) 1673 | * @see #getSwipeThreshold(RecyclerView.ViewHolder) 1674 | */ 1675 | public float getSwipeEscapeVelocity(float defaultValue) { 1676 | return defaultValue; 1677 | } 1678 | 1679 | /** 1680 | * Defines the maximum velocity ItemTouchHelper will ever calculate for pointer movements. 1681 | *

1682 | * To consider a movement as swipe, ItemTouchHelper requires it to be larger than the 1683 | * perpendicular movement. If both directions reach to the max threshold, none of them will 1684 | * be considered as a swipe because it is usually an indication that user rather tried to 1685 | * scroll then swipe. 1686 | *

1687 | * The velocity is calculated in pixels per second. 1688 | *

1689 | * You can customize this behavior by changing this method. If you increase the value, it 1690 | * will be easier for the user to swipe diagonally and if you decrease the value, user will 1691 | * need to make a rather straight finger movement to trigger a swipe. 1692 | * 1693 | * @param defaultValue The default value(in pixels per second) used by the ItemTouchHelper. 1694 | * @return The velocity cap for pointer movements. The default implementation returns the 1695 | * defaultValue parameter. 1696 | * @see #getSwipeEscapeVelocity(float) 1697 | */ 1698 | public float getSwipeVelocityThreshold(float defaultValue) { 1699 | return defaultValue; 1700 | } 1701 | 1702 | /** 1703 | * Called by ItemTouchHelper to select a drop target from the list of ViewHolders that 1704 | * are under the dragged View. 1705 | *

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

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

1713 | * This method is called on the main thread every time user moves the View. If you want to 1714 | * override it, make sure it does not do any expensive operations. 1715 | * 1716 | * @param selected The ViewHolder being dragged by the user. 1717 | * @param dropTargets The list of ViewHolder that are under the dragged View and 1718 | * candidate as a drop. 1719 | * @param curX The updated left value of the dragged View after drag translations 1720 | * are applied. This value does not include margins added by 1721 | * {@link RecyclerView.ItemDecoration}s. 1722 | * @param curY The updated top value of the dragged View after drag translations 1723 | * are applied. This value does not include margins added by 1724 | * {@link RecyclerView.ItemDecoration}s. 1725 | * @return A ViewHolder to whose position the dragged ViewHolder should be 1726 | * moved to. 1727 | */ 1728 | public RecyclerView.ViewHolder chooseDropTarget(RecyclerView.ViewHolder selected, 1729 | List dropTargets, int curX, int curY) { 1730 | int right = curX + selected.itemView.getWidth(); 1731 | int bottom = curY + selected.itemView.getHeight(); 1732 | RecyclerView.ViewHolder winner = null; 1733 | int winnerScore = -1; 1734 | final int dx = curX - selected.itemView.getLeft(); 1735 | final int dy = curY - selected.itemView.getTop(); 1736 | final int targetsSize = dropTargets.size(); 1737 | for (int i = 0; i < targetsSize; i++) { 1738 | final RecyclerView.ViewHolder target = dropTargets.get(i); 1739 | if (dx > 0) { 1740 | int diff = target.itemView.getRight() - right; 1741 | if (diff < 0 && target.itemView.getRight() > selected.itemView.getRight()) { 1742 | final int score = Math.abs(diff); 1743 | if (score > winnerScore) { 1744 | winnerScore = score; 1745 | winner = target; 1746 | } 1747 | } 1748 | } 1749 | if (dx < 0) { 1750 | int diff = target.itemView.getLeft() - curX; 1751 | if (diff > 0 && target.itemView.getLeft() < selected.itemView.getLeft()) { 1752 | final int score = Math.abs(diff); 1753 | if (score > winnerScore) { 1754 | winnerScore = score; 1755 | winner = target; 1756 | } 1757 | } 1758 | } 1759 | if (dy < 0) { 1760 | int diff = target.itemView.getTop() - curY; 1761 | if (diff > 0 && target.itemView.getTop() < selected.itemView.getTop()) { 1762 | final int score = Math.abs(diff); 1763 | if (score > winnerScore) { 1764 | winnerScore = score; 1765 | winner = target; 1766 | } 1767 | } 1768 | } 1769 | 1770 | if (dy > 0) { 1771 | int diff = target.itemView.getBottom() - bottom; 1772 | if (diff < 0 && target.itemView.getBottom() > selected.itemView.getBottom()) { 1773 | final int score = Math.abs(diff); 1774 | if (score > winnerScore) { 1775 | winnerScore = score; 1776 | winner = target; 1777 | } 1778 | } 1779 | } 1780 | } 1781 | return winner; 1782 | } 1783 | 1784 | /** 1785 | * Called when a ViewHolder is swiped by the user. 1786 | *

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

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

1793 | * ItemTouchHelper will keep a reference to the View until it is detached from 1794 | * RecyclerView. 1795 | * As soon as it is detached, ItemTouchHelper will call 1796 | * {@link #clearView(RecyclerView, RecyclerView.ViewHolder)}. 1797 | * 1798 | * @param viewHolder The ViewHolder which has been swiped by the user. 1799 | * @param direction The direction to which the ViewHolder is swiped. It is one of 1800 | * {@link #UP}, {@link #DOWN}, 1801 | * {@link #LEFT} or {@link #RIGHT}. If your 1802 | * {@link #getMovementFlags(RecyclerView, RecyclerView.ViewHolder)} 1803 | * method 1804 | * returned relative flags instead of {@link #LEFT} / {@link #RIGHT}; 1805 | * `direction` will be relative as well. ({@link #START} or {@link 1806 | * #END}). 1807 | */ 1808 | public abstract void onSwiped(RecyclerView.ViewHolder viewHolder, int direction); 1809 | 1810 | /** 1811 | * Called when the ViewHolder swiped or dragged by the ItemTouchHelper is changed. 1812 | *

1813 | * If you override this method, you should call super. 1814 | * 1815 | * @param viewHolder The new ViewHolder that is being swiped or dragged. Might be null if 1816 | * it is cleared. 1817 | * @param actionState One of {@link android.support.v7.widget.helper.ItemTouchHelper#ACTION_STATE_IDLE}, 1818 | * {@link android.support.v7.widget.helper.ItemTouchHelper#ACTION_STATE_SWIPE} or 1819 | * {@link android.support.v7.widget.helper.ItemTouchHelper#ACTION_STATE_DRAG}. 1820 | * @see #clearView(RecyclerView, RecyclerView.ViewHolder) 1821 | */ 1822 | public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) { 1823 | if (viewHolder != null) { 1824 | sUICallback.onSelected(viewHolder.itemView); 1825 | } 1826 | } 1827 | 1828 | private int getMaxDragScroll(RecyclerView recyclerView) { 1829 | if (mCachedMaxScrollSpeed == -1) { 1830 | mCachedMaxScrollSpeed = recyclerView.getResources().getDimensionPixelSize( 1831 | android.support.v7.recyclerview.R.dimen.item_touch_helper_max_drag_scroll_per_frame); 1832 | } 1833 | return mCachedMaxScrollSpeed; 1834 | } 1835 | 1836 | /** 1837 | * Called when {@link #onMove(RecyclerView, RecyclerView.ViewHolder, RecyclerView.ViewHolder)} returns true. 1838 | *

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

1845 | * This method is responsible to give necessary hint to the LayoutManager so that it will 1846 | * keep the View in visible area. For example, for LinearLayoutManager, this is as simple 1847 | * as calling {@link LinearLayoutManager#scrollToPositionWithOffset(int, int)}. 1848 | *

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

1852 | * It is important to ensure the ViewHolder will stay visible as otherwise, it might be 1853 | * removed by the LayoutManager if the move causes the View to go out of bounds. In that 1854 | * case, drag will end prematurely. 1855 | * 1856 | * @param recyclerView The RecyclerView controlled by the ItemTouchHelper. 1857 | * @param viewHolder The ViewHolder under user's control. 1858 | * @param fromPos The previous adapter position of the dragged item (before it was 1859 | * moved). 1860 | * @param target The ViewHolder on which the currently active item has been dropped. 1861 | * @param toPos The new adapter position of the dragged item. 1862 | * @param x The updated left value of the dragged View after drag translations 1863 | * are applied. This value does not include margins added by 1864 | * {@link RecyclerView.ItemDecoration}s. 1865 | * @param y The updated top value of the dragged View after drag translations 1866 | * are applied. This value does not include margins added by 1867 | * {@link RecyclerView.ItemDecoration}s. 1868 | */ 1869 | public void onMoved(final RecyclerView recyclerView, 1870 | final RecyclerView.ViewHolder viewHolder, int fromPos, final RecyclerView.ViewHolder target, int toPos, int x, 1871 | int y) { 1872 | final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); 1873 | if (layoutManager instanceof android.support.v7.widget.helper.ItemTouchHelper.ViewDropHandler) { 1874 | ((android.support.v7.widget.helper.ItemTouchHelper.ViewDropHandler) layoutManager).prepareForDrop(viewHolder.itemView, 1875 | target.itemView, x, y); 1876 | return; 1877 | } 1878 | 1879 | // if layout manager cannot handle it, do some guesswork 1880 | if (layoutManager.canScrollHorizontally()) { 1881 | final int minLeft = layoutManager.getDecoratedLeft(target.itemView); 1882 | if (minLeft <= recyclerView.getPaddingLeft()) { 1883 | recyclerView.scrollToPosition(toPos); 1884 | } 1885 | final int maxRight = layoutManager.getDecoratedRight(target.itemView); 1886 | if (maxRight >= recyclerView.getWidth() - recyclerView.getPaddingRight()) { 1887 | recyclerView.scrollToPosition(toPos); 1888 | } 1889 | } 1890 | 1891 | if (layoutManager.canScrollVertically()) { 1892 | final int minTop = layoutManager.getDecoratedTop(target.itemView); 1893 | if (minTop <= recyclerView.getPaddingTop()) { 1894 | recyclerView.scrollToPosition(toPos); 1895 | } 1896 | final int maxBottom = layoutManager.getDecoratedBottom(target.itemView); 1897 | if (maxBottom >= recyclerView.getHeight() - recyclerView.getPaddingBottom()) { 1898 | recyclerView.scrollToPosition(toPos); 1899 | } 1900 | } 1901 | } 1902 | 1903 | void onDraw(Canvas c, RecyclerView parent, RecyclerView.ViewHolder selected, 1904 | List recoverAnimationList, 1905 | int actionState, float dX, float dY) { 1906 | final int recoverAnimSize = recoverAnimationList.size(); 1907 | for (int i = 0; i < recoverAnimSize; i++) { 1908 | final RecoverAnimation anim = recoverAnimationList.get(i); 1909 | anim.update(); 1910 | final int count = c.save(); 1911 | onChildDraw(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState, 1912 | false); 1913 | c.restoreToCount(count); 1914 | } 1915 | if (selected != null) { 1916 | final int count = c.save(); 1917 | onChildDraw(c, parent, selected, dX, dY, actionState, true); 1918 | c.restoreToCount(count); 1919 | } 1920 | } 1921 | 1922 | void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.ViewHolder selected, 1923 | List recoverAnimationList, 1924 | int actionState, float dX, float dY) { 1925 | final int recoverAnimSize = recoverAnimationList.size(); 1926 | for (int i = 0; i < recoverAnimSize; i++) { 1927 | final RecoverAnimation anim = recoverAnimationList.get(i); 1928 | final int count = c.save(); 1929 | onChildDrawOver(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState, 1930 | false); 1931 | c.restoreToCount(count); 1932 | } 1933 | if (selected != null) { 1934 | final int count = c.save(); 1935 | onChildDrawOver(c, parent, selected, dX, dY, actionState, true); 1936 | c.restoreToCount(count); 1937 | } 1938 | boolean hasRunningAnimation = false; 1939 | for (int i = recoverAnimSize - 1; i >= 0; i--) { 1940 | final RecoverAnimation anim = recoverAnimationList.get(i); 1941 | if (anim.mEnded && !anim.mIsPendingCleanup) { 1942 | recoverAnimationList.remove(i); 1943 | } else if (!anim.mEnded) { 1944 | hasRunningAnimation = true; 1945 | } 1946 | } 1947 | if (hasRunningAnimation) { 1948 | parent.invalidate(); 1949 | } 1950 | } 1951 | 1952 | /** 1953 | * Called by the ItemTouchHelper when the user interaction with an element is over and it 1954 | * also completed its animation. 1955 | *

1956 | * This is a good place to clear all changes on the View that was done in 1957 | * {@link #onSelectedChanged(RecyclerView.ViewHolder, int)}, 1958 | * {@link #onChildDraw(Canvas, RecyclerView, RecyclerView.ViewHolder, float, float, int, 1959 | * boolean)} or 1960 | * {@link #onChildDrawOver(Canvas, RecyclerView, RecyclerView.ViewHolder, float, float, int, boolean)}. 1961 | * 1962 | * @param recyclerView The RecyclerView which is controlled by the ItemTouchHelper. 1963 | * @param viewHolder The View that was interacted by the user. 1964 | */ 1965 | public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { 1966 | sUICallback.clearView(viewHolder.itemView); 1967 | } 1968 | 1969 | /** 1970 | * Called by ItemTouchHelper on RecyclerView's onDraw callback. 1971 | *

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

1975 | * Default implementation translates the child by the given dX, 1976 | * dY. 1977 | * ItemTouchHelper also takes care of drawing the child after other children if it is being 1978 | * dragged. This is done using child re-ordering mechanism. On platforms prior to L, this 1979 | * is 1980 | * achieved via {@link ViewGroup#getChildDrawingOrder(int, int)} and on L 1981 | * and after, it changes View's elevation value to be greater than all other children.) 1982 | * 1983 | * @param c The canvas which RecyclerView is drawing its children 1984 | * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to 1985 | * @param viewHolder The ViewHolder which is being interacted by the User or it was 1986 | * interacted and simply animating to its original position 1987 | * @param dX The amount of horizontal displacement caused by user's action 1988 | * @param dY The amount of vertical displacement caused by user's action 1989 | * @param actionState The type of interaction on the View. Is either {@link 1990 | * #ACTION_STATE_DRAG} or {@link #ACTION_STATE_SWIPE}. 1991 | * @param isCurrentlyActive True if this view is currently being controlled by the user or 1992 | * false it is simply animating back to its original state. 1993 | * @see #onChildDrawOver(Canvas, RecyclerView, RecyclerView.ViewHolder, float, float, int, 1994 | * boolean) 1995 | */ 1996 | public void onChildDraw(Canvas c, RecyclerView recyclerView, 1997 | RecyclerView.ViewHolder viewHolder, 1998 | float dX, float dY, int actionState, boolean isCurrentlyActive) { 1999 | sUICallback.onDraw(c, recyclerView, viewHolder.itemView, dX, dY, actionState, 2000 | isCurrentlyActive); 2001 | } 2002 | 2003 | /** 2004 | * Called by ItemTouchHelper on RecyclerView's onDraw callback. 2005 | *

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

2009 | * Default implementation translates the child by the given dX, 2010 | * dY. 2011 | * ItemTouchHelper also takes care of drawing the child after other children if it is being 2012 | * dragged. This is done using child re-ordering mechanism. On platforms prior to L, this 2013 | * is 2014 | * achieved via {@link ViewGroup#getChildDrawingOrder(int, int)} and on L 2015 | * and after, it changes View's elevation value to be greater than all other children.) 2016 | * 2017 | * @param c The canvas which RecyclerView is drawing its children 2018 | * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to 2019 | * @param viewHolder The ViewHolder which is being interacted by the User or it was 2020 | * interacted and simply animating to its original position 2021 | * @param dX The amount of horizontal displacement caused by user's action 2022 | * @param dY The amount of vertical displacement caused by user's action 2023 | * @param actionState The type of interaction on the View. Is either {@link 2024 | * #ACTION_STATE_DRAG} or {@link #ACTION_STATE_SWIPE}. 2025 | * @param isCurrentlyActive True if this view is currently being controlled by the user or 2026 | * false it is simply animating back to its original state. 2027 | * @see #onChildDrawOver(Canvas, RecyclerView, RecyclerView.ViewHolder, float, float, int, 2028 | * boolean) 2029 | */ 2030 | public void onChildDrawOver(Canvas c, RecyclerView recyclerView, 2031 | RecyclerView.ViewHolder viewHolder, 2032 | float dX, float dY, int actionState, boolean isCurrentlyActive) { 2033 | sUICallback.onDrawOver(c, recyclerView, viewHolder.itemView, dX, dY, actionState, 2034 | isCurrentlyActive); 2035 | } 2036 | 2037 | /** 2038 | * Called by the ItemTouchHelper when user action finished on a ViewHolder and now the View 2039 | * will be animated to its final position. 2040 | *

2041 | * Default implementation uses ItemAnimator's duration values. If 2042 | * animationType is {@link #ANIMATION_TYPE_DRAG}, it returns 2043 | * {@link RecyclerView.ItemAnimator#getMoveDuration()}, otherwise, it returns 2044 | * {@link RecyclerView.ItemAnimator#getRemoveDuration()}. If RecyclerView does not have 2045 | * any {@link RecyclerView.ItemAnimator} attached, this method returns 2046 | * {@code DEFAULT_DRAG_ANIMATION_DURATION} or {@code DEFAULT_SWIPE_ANIMATION_DURATION} 2047 | * depending on the animation type. 2048 | * 2049 | * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to. 2050 | * @param animationType The type of animation. Is one of {@link #ANIMATION_TYPE_DRAG}, 2051 | * {@link #ANIMATION_TYPE_SWIPE_CANCEL} or 2052 | * {@link #ANIMATION_TYPE_SWIPE_SUCCESS}. 2053 | * @param animateDx The horizontal distance that the animation will offset 2054 | * @param animateDy The vertical distance that the animation will offset 2055 | * @return The duration for the animation 2056 | */ 2057 | public long getAnimationDuration(RecyclerView recyclerView, int animationType, 2058 | float animateDx, float animateDy) { 2059 | final RecyclerView.ItemAnimator itemAnimator = recyclerView.getItemAnimator(); 2060 | if (itemAnimator == null) { 2061 | return animationType == ANIMATION_TYPE_DRAG ? DEFAULT_DRAG_ANIMATION_DURATION 2062 | : DEFAULT_SWIPE_ANIMATION_DURATION; 2063 | } else { 2064 | return animationType == ANIMATION_TYPE_DRAG ? itemAnimator.getMoveDuration() 2065 | : itemAnimator.getRemoveDuration(); 2066 | } 2067 | } 2068 | 2069 | /** 2070 | * Called by the ItemTouchHelper when user is dragging a view out of bounds. 2071 | *

2072 | * You can override this method to decide how much RecyclerView should scroll in response 2073 | * to this action. Default implementation calculates a value based on the amount of View 2074 | * out of bounds and the time it spent there. The longer user keeps the View out of bounds, 2075 | * the faster the list will scroll. Similarly, the larger portion of the View is out of 2076 | * bounds, the faster the RecyclerView will scroll. 2077 | * 2078 | * @param recyclerView The RecyclerView instance to which ItemTouchHelper is 2079 | * attached to. 2080 | * @param viewSize The total size of the View in scroll direction, excluding 2081 | * item decorations. 2082 | * @param viewSizeOutOfBounds The total size of the View that is out of bounds. This value 2083 | * is negative if the View is dragged towards left or top edge. 2084 | * @param totalSize The total size of RecyclerView in the scroll direction. 2085 | * @param msSinceStartScroll The time passed since View is kept out of bounds. 2086 | * @return The amount that RecyclerView should scroll. Keep in mind that this value will 2087 | * be passed to {@link RecyclerView#scrollBy(int, int)} method. 2088 | */ 2089 | public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, 2090 | int viewSize, int viewSizeOutOfBounds, 2091 | int totalSize, long msSinceStartScroll) { 2092 | final int maxScroll = getMaxDragScroll(recyclerView); 2093 | final int absOutOfBounds = Math.abs(viewSizeOutOfBounds); 2094 | final int direction = (int) Math.signum(viewSizeOutOfBounds); 2095 | // might be negative if other direction 2096 | float outOfBoundsRatio = Math.min(1f, 1f * absOutOfBounds / viewSize); 2097 | final int cappedScroll = (int) (direction * maxScroll 2098 | * sDragViewScrollCapInterpolator.getInterpolation(outOfBoundsRatio)); 2099 | final float timeRatio; 2100 | if (msSinceStartScroll > DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS) { 2101 | timeRatio = 1f; 2102 | } else { 2103 | timeRatio = (float) msSinceStartScroll / DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS; 2104 | } 2105 | final int value = (int) (cappedScroll * sDragScrollInterpolator 2106 | .getInterpolation(timeRatio)); 2107 | if (value == 0) { 2108 | return viewSizeOutOfBounds > 0 ? 1 : -1; 2109 | } 2110 | return value; 2111 | } 2112 | } 2113 | 2114 | /** 2115 | * A simple wrapper to the default Callback which you can construct with drag and swipe 2116 | * directions and this class will handle the flag callbacks. You should still override onMove 2117 | * or 2118 | * onSwiped depending on your use case. 2119 | *

2120 | *

2121 |      * ItemTouchHelper mIth = new ItemTouchHelper(
2122 |      *     new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
2123 |      *         ItemTouchHelper.LEFT) {
2124 |      *         public abstract boolean onMove(RecyclerView recyclerView,
2125 |      *             ViewHolder viewHolder, ViewHolder target) {
2126 |      *             final int fromPos = viewHolder.getAdapterPosition();
2127 |      *             final int toPos = target.getAdapterPosition();
2128 |      *             // move item in `fromPos` to `toPos` in adapter.
2129 |      *             return true;// true if moved, false otherwise
2130 |      *         }
2131 |      *         public void onSwiped(ViewHolder viewHolder, int direction) {
2132 |      *             // remove from adapter
2133 |      *         }
2134 |      * });
2135 |      * 
2136 | */ 2137 | public abstract static class SimpleCallback extends android.support.v7.widget.helper.ItemTouchHelper.Callback { 2138 | 2139 | private int mDefaultSwipeDirs; 2140 | 2141 | private int mDefaultDragDirs; 2142 | 2143 | /** 2144 | * Creates a Callback for the given drag and swipe allowance. These values serve as 2145 | * defaults 2146 | * and if you want to customize behavior per ViewHolder, you can override 2147 | * {@link #getSwipeDirs(RecyclerView, RecyclerView.ViewHolder)} 2148 | * and / or {@link #getDragDirs(RecyclerView, RecyclerView.ViewHolder)}. 2149 | * 2150 | * @param dragDirs Binary OR of direction flags in which the Views can be dragged. Must be 2151 | * composed of {@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link 2152 | * #END}, 2153 | * {@link #UP} and {@link #DOWN}. 2154 | * @param swipeDirs Binary OR of direction flags in which the Views can be swiped. Must be 2155 | * composed of {@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link 2156 | * #END}, 2157 | * {@link #UP} and {@link #DOWN}. 2158 | */ 2159 | public SimpleCallback(int dragDirs, int swipeDirs) { 2160 | mDefaultSwipeDirs = swipeDirs; 2161 | mDefaultDragDirs = dragDirs; 2162 | } 2163 | 2164 | /** 2165 | * Updates the default swipe directions. For example, you can use this method to toggle 2166 | * certain directions depending on your use case. 2167 | * 2168 | * @param defaultSwipeDirs Binary OR of directions in which the ViewHolders can be swiped. 2169 | */ 2170 | public void setDefaultSwipeDirs(int defaultSwipeDirs) { 2171 | mDefaultSwipeDirs = defaultSwipeDirs; 2172 | } 2173 | 2174 | /** 2175 | * Updates the default drag directions. For example, you can use this method to toggle 2176 | * certain directions depending on your use case. 2177 | * 2178 | * @param defaultDragDirs Binary OR of directions in which the ViewHolders can be dragged. 2179 | */ 2180 | public void setDefaultDragDirs(int defaultDragDirs) { 2181 | mDefaultDragDirs = defaultDragDirs; 2182 | } 2183 | 2184 | /** 2185 | * Returns the swipe directions for the provided ViewHolder. 2186 | * Default implementation returns the swipe directions that was set via constructor or 2187 | * {@link #setDefaultSwipeDirs(int)}. 2188 | * 2189 | * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to. 2190 | * @param viewHolder The RecyclerView for which the swipe direction is queried. 2191 | * @return A binary OR of direction flags. 2192 | */ 2193 | public int getSwipeDirs(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { 2194 | return mDefaultSwipeDirs; 2195 | } 2196 | 2197 | /** 2198 | * Returns the drag directions for the provided ViewHolder. 2199 | * Default implementation returns the drag directions that was set via constructor or 2200 | * {@link #setDefaultDragDirs(int)}. 2201 | * 2202 | * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to. 2203 | * @param viewHolder The RecyclerView for which the swipe direction is queried. 2204 | * @return A binary OR of direction flags. 2205 | */ 2206 | public int getDragDirs(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { 2207 | return mDefaultDragDirs; 2208 | } 2209 | 2210 | @Override 2211 | public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { 2212 | return makeMovementFlags(getDragDirs(recyclerView, viewHolder), 2213 | getSwipeDirs(recyclerView, viewHolder)); 2214 | } 2215 | } 2216 | 2217 | private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener { 2218 | 2219 | ItemTouchHelperGestureListener() { 2220 | } 2221 | 2222 | @Override 2223 | public boolean onDown(MotionEvent e) { 2224 | return true; 2225 | } 2226 | 2227 | @Override 2228 | public void onLongPress(MotionEvent e) { 2229 | View child = findChildView(e); 2230 | if (child != null) { 2231 | RecyclerView.ViewHolder vh = mRecyclerView.getChildViewHolder(child); 2232 | if (vh != null) { 2233 | if (!mCallback.hasDragFlag(mRecyclerView, vh)) { 2234 | return; 2235 | } 2236 | if (mResponseEventListener != null && !mResponseEventListener 2237 | .isEnableSelectColumn(vh.getAdapterPosition())) { 2238 | return; 2239 | } 2240 | int pointerId = e.getPointerId(0); 2241 | // Long press is deferred. 2242 | // Check w/ active pointer id to avoid selecting after motion 2243 | // event is canceled. 2244 | if (pointerId == mActivePointerId) { 2245 | final int index = e.findPointerIndex(mActivePointerId); 2246 | final float x = e.getX(index); 2247 | final float y = e.getY(index); 2248 | mInitialTouchX = x; 2249 | mInitialTouchY = y; 2250 | mDx = mDy = 0f; 2251 | if (DEBUG) { 2252 | Log.d(TAG, 2253 | "onlong press: x:" + mInitialTouchX + ",y:" + mInitialTouchY); 2254 | } 2255 | if (mCallback.isLongPressDragEnabled()) { 2256 | select(vh, ACTION_STATE_DRAG); 2257 | } 2258 | } 2259 | } 2260 | } 2261 | } 2262 | } 2263 | 2264 | private static class RecoverAnimation implements Animator.AnimatorListener { 2265 | 2266 | final float mStartDx; 2267 | 2268 | final float mStartDy; 2269 | 2270 | final float mTargetX; 2271 | 2272 | final float mTargetY; 2273 | 2274 | final RecyclerView.ViewHolder mViewHolder; 2275 | 2276 | final int mActionState; 2277 | 2278 | private final ValueAnimator mValueAnimator; 2279 | 2280 | final int mAnimationType; 2281 | 2282 | public boolean mIsPendingCleanup; 2283 | 2284 | float mX; 2285 | 2286 | float mY; 2287 | 2288 | // if user starts touching a recovering view, we put it into interaction mode again, 2289 | // instantly. 2290 | boolean mOverridden = false; 2291 | 2292 | boolean mEnded = false; 2293 | 2294 | private float mFraction; 2295 | 2296 | RecoverAnimation(RecyclerView.ViewHolder viewHolder, int animationType, 2297 | int actionState, float startDx, float startDy, float targetX, float targetY) { 2298 | mActionState = actionState; 2299 | mAnimationType = animationType; 2300 | mViewHolder = viewHolder; 2301 | mStartDx = startDx; 2302 | mStartDy = startDy; 2303 | mTargetX = targetX; 2304 | mTargetY = targetY; 2305 | mValueAnimator = ValueAnimator.ofFloat(0f, 1f); 2306 | mValueAnimator.addUpdateListener( 2307 | new ValueAnimator.AnimatorUpdateListener() { 2308 | @Override 2309 | public void onAnimationUpdate(ValueAnimator animation) { 2310 | setFraction(animation.getAnimatedFraction()); 2311 | } 2312 | }); 2313 | mValueAnimator.setTarget(viewHolder.itemView); 2314 | mValueAnimator.addListener(this); 2315 | setFraction(0f); 2316 | } 2317 | 2318 | public void setDuration(long duration) { 2319 | mValueAnimator.setDuration(duration); 2320 | } 2321 | 2322 | public void start() { 2323 | mViewHolder.setIsRecyclable(false); 2324 | mValueAnimator.start(); 2325 | } 2326 | 2327 | public void cancel() { 2328 | mValueAnimator.cancel(); 2329 | } 2330 | 2331 | public void setFraction(float fraction) { 2332 | mFraction = fraction; 2333 | } 2334 | 2335 | /** 2336 | * We run updates on onDraw method but use the fraction from animator callback. 2337 | * This way, we can sync translate x/y values w/ the animators to avoid one-off frames. 2338 | */ 2339 | public void update() { 2340 | if (mStartDx == mTargetX) { 2341 | mX = mViewHolder.itemView.getTranslationX(); 2342 | } else { 2343 | mX = mStartDx + mFraction * (mTargetX - mStartDx); 2344 | } 2345 | if (mStartDy == mTargetY) { 2346 | mY = mViewHolder.itemView.getTranslationY(); 2347 | } else { 2348 | mY = mStartDy + mFraction * (mTargetY - mStartDy); 2349 | } 2350 | } 2351 | 2352 | @Override 2353 | public void onAnimationStart(Animator animation) { 2354 | 2355 | } 2356 | 2357 | @Override 2358 | public void onAnimationEnd(Animator animation) { 2359 | if (!mEnded) { 2360 | mViewHolder.setIsRecyclable(true); 2361 | } 2362 | mEnded = true; 2363 | } 2364 | 2365 | @Override 2366 | public void onAnimationCancel(Animator animation) { 2367 | setFraction(1f); //make sure we recover the view's state. 2368 | } 2369 | 2370 | @Override 2371 | public void onAnimationRepeat(Animator animation) { 2372 | 2373 | } 2374 | } 2375 | 2376 | //------------------------------------------------------------------// 2377 | 2378 | /** 2379 | * 判断落点是否落在Header上 2380 | */ 2381 | private Rect globalVisible = new Rect(); 2382 | 2383 | private boolean landUpAssignView(MotionEvent event) { 2384 | View childView = findChildView(event); 2385 | if (childView == null) return false; 2386 | int x = (int) event.getRawX(); 2387 | int y = (int) event.getRawY(); 2388 | View header; 2389 | if (mResponseEventListener != null && mResponseEventListener.getResponseViewId() > 0) { 2390 | header = childView.findViewById(mResponseEventListener.getResponseViewId()); 2391 | } else { 2392 | if (childView instanceof ViewGroup) { 2393 | header = ((ViewGroup) childView).getChildAt(0); 2394 | } else { 2395 | header = childView; 2396 | } 2397 | } 2398 | if (header == null) { 2399 | return false; 2400 | } 2401 | header.getGlobalVisibleRect(globalVisible); 2402 | return globalVisible.contains(x, y); 2403 | } 2404 | 2405 | private ResponseEventListener mResponseEventListener; 2406 | 2407 | public void setResponseEventListener(ResponseEventListener listener) { 2408 | mResponseEventListener = listener; 2409 | } 2410 | 2411 | public interface ResponseEventListener { 2412 | int getResponseViewId(); 2413 | 2414 | boolean isEnableSelectColumn(int column); 2415 | } 2416 | } 2417 | -------------------------------------------------------------------------------- /app/src/main/java/com/guolei/boardview/ItemTouchUIUtilImpl.java: -------------------------------------------------------------------------------- 1 | package com.guolei.boardview; 2 | 3 | 4 | import android.graphics.Canvas; 5 | import android.support.v4.view.ViewCompat; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.support.v7.widget.helper.ItemTouchUIUtil; 8 | import android.view.View; 9 | 10 | /** 11 | * copy from android source code 12 | */ 13 | 14 | class ItemTouchUIUtilImpl { 15 | static class Api21Impl extends ItemTouchUIUtilImpl.BaseImpl { 16 | @Override 17 | public void onDraw(Canvas c, RecyclerView recyclerView, View view, 18 | float dX, float dY, int actionState, boolean isCurrentlyActive) { 19 | if (isCurrentlyActive) { 20 | Object originalElevation = view.getTag(android.support.v7.recyclerview.R.id.item_touch_helper_previous_elevation); 21 | if (originalElevation == null) { 22 | originalElevation = ViewCompat.getElevation(view); 23 | float newElevation = 1f + findMaxElevation(recyclerView, view); 24 | ViewCompat.setElevation(view, newElevation); 25 | view.setTag(android.support.v7.recyclerview.R.id.item_touch_helper_previous_elevation, originalElevation); 26 | } 27 | } 28 | super.onDraw(c, recyclerView, view, dX, dY, actionState, isCurrentlyActive); 29 | } 30 | 31 | private float findMaxElevation(RecyclerView recyclerView, View itemView) { 32 | final int childCount = recyclerView.getChildCount(); 33 | float max = 0; 34 | for (int i = 0; i < childCount; i++) { 35 | final View child = recyclerView.getChildAt(i); 36 | if (child == itemView) { 37 | continue; 38 | } 39 | final float elevation = ViewCompat.getElevation(child); 40 | if (elevation > max) { 41 | max = elevation; 42 | } 43 | } 44 | return max; 45 | } 46 | 47 | @Override 48 | public void clearView(View view) { 49 | final Object tag = view.getTag(android.support.v7.recyclerview.R.id.item_touch_helper_previous_elevation); 50 | if (tag != null && tag instanceof Float) { 51 | ViewCompat.setElevation(view, (Float) tag); 52 | } 53 | view.setTag(android.support.v7.recyclerview.R.id.item_touch_helper_previous_elevation, null); 54 | super.clearView(view); 55 | } 56 | } 57 | 58 | static class BaseImpl implements ItemTouchUIUtil { 59 | 60 | @Override 61 | public void clearView(View view) { 62 | view.setTranslationX(0f); 63 | view.setTranslationY(0f); 64 | } 65 | 66 | @Override 67 | public void onSelected(View view) { 68 | 69 | } 70 | 71 | @Override 72 | public void onDraw(Canvas c, RecyclerView recyclerView, View view, 73 | float dX, float dY, int actionState, boolean isCurrentlyActive) { 74 | view.setTranslationX(dX); 75 | view.setTranslationY(dY); 76 | } 77 | 78 | @Override 79 | public void onDrawOver(Canvas c, RecyclerView recyclerView, 80 | View view, float dX, float dY, int actionState, boolean isCurrentlyActive) { 81 | 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/com/guolei/boardview/SimpleLayoutManager.java: -------------------------------------------------------------------------------- 1 | package com.guolei.boardview; 2 | 3 | 4 | import android.content.Context; 5 | import android.support.v7.widget.LinearLayoutManager; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.util.AttributeSet; 8 | import android.util.Log; 9 | 10 | 11 | public class SimpleLayoutManager extends LinearLayoutManager { 12 | 13 | public SimpleLayoutManager(Context context) { 14 | super(context); 15 | } 16 | 17 | SimpleLayoutManager(Context context, int orientation, boolean reverseLayout) { 18 | super(context, orientation, reverseLayout); 19 | } 20 | 21 | @SuppressWarnings("unused") 22 | public SimpleLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 23 | super(context, attrs, defStyleAttr, defStyleRes); 24 | } 25 | 26 | @Override 27 | public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 28 | try { 29 | super.onLayoutChildren(recycler, state); 30 | } catch (Exception e) { 31 | e.printStackTrace(); 32 | } 33 | } 34 | 35 | 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/guolei/ui/BoardViewAdapter.java: -------------------------------------------------------------------------------- 1 | package com.guolei.ui; 2 | 3 | 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.TextView; 9 | 10 | import com.guolei.boardview.BoardViewStateHolder; 11 | import com.guolei.boardview.R; 12 | import com.guolei.boardview.SimpleLayoutManager; 13 | 14 | import java.util.ArrayList; 15 | import java.util.Collections; 16 | import java.util.List; 17 | import java.util.Random; 18 | 19 | 20 | public class BoardViewAdapter extends RecyclerView.Adapter { 21 | 22 | private List> mData = new ArrayList<>(); 23 | 24 | private BoardViewStateHolder mBoardViewHolder; 25 | 26 | public BoardViewAdapter(BoardViewStateHolder boardViewHolder) { 27 | mBoardViewHolder = boardViewHolder; 28 | initData(); 29 | } 30 | 31 | private void initData() { 32 | for (int i = 0; i < 10; i++) { 33 | mData.add(new ArrayList<>()); 34 | int max = new Random().nextInt(20); 35 | for (int j = 0; j < max; j++) { 36 | mData.get(i).add("column: " + i + ";row:" + j); 37 | } 38 | } 39 | } 40 | 41 | @Override 42 | public SimpleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 43 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_recyclerview, 44 | parent, 45 | false); 46 | return new SimpleViewHolder(view); 47 | } 48 | 49 | @Override 50 | public void onBindViewHolder(final SimpleViewHolder holder, int position) { 51 | holder.recyclerView.setLayoutManager(new SimpleLayoutManager(holder.itemView.getContext())); 52 | RecyclerView.Adapter adapter = new ColumnAdapter(mBoardViewHolder, mData.get(position)); 53 | holder.recyclerView.setAdapter(adapter); 54 | if (holder.recyclerView.getItemDecorationAt(0) == null) { 55 | holder.recyclerView.addItemDecoration(new CustomItemDecoration()); 56 | } 57 | holder.recyclerView.getItemAnimator().setAddDuration(0); 58 | holder.recyclerView.getItemAnimator().setRemoveDuration(0); 59 | } 60 | 61 | @Override 62 | public int getItemCount() { 63 | return mData.size(); 64 | } 65 | 66 | public void swap(int from, int to) { 67 | Collections.swap(mData, from, to); 68 | } 69 | 70 | class SimpleViewHolder extends RecyclerView.ViewHolder { 71 | 72 | RecyclerView recyclerView; 73 | TextView title; 74 | 75 | SimpleViewHolder(View itemView) { 76 | super(itemView); 77 | recyclerView = itemView.findViewById(R.id.recycler_view); 78 | title = itemView.findViewById(R.id.title); 79 | } 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/com/guolei/ui/ColumnAdapter.java: -------------------------------------------------------------------------------- 1 | package com.guolei.ui; 2 | 3 | 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.TextView; 9 | 10 | import com.guolei.boardview.AbsBoardViewAdapter; 11 | import com.guolei.boardview.BoardViewStateHolder; 12 | import com.guolei.boardview.R; 13 | 14 | import java.util.ArrayList; 15 | import java.util.Collections; 16 | import java.util.List; 17 | 18 | 19 | public class ColumnAdapter extends AbsBoardViewAdapter { 20 | 21 | private List mData = new ArrayList<>(); 22 | 23 | private BoardViewStateHolder mBoardViewHolder; 24 | 25 | ColumnAdapter(BoardViewStateHolder boardViewHolder, List data) { 26 | mBoardViewHolder = boardViewHolder; 27 | mData.addAll(data); 28 | } 29 | 30 | @Override 31 | public SimpleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 32 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false); 33 | return new SimpleViewHolder(view); 34 | } 35 | 36 | @Override 37 | public void onBindViewHolder(SimpleViewHolder holder, int position) { 38 | holder.mTextView.setText(mData.get(position)); 39 | if (mData.get(position).equals(mBoardViewHolder.getSelectedId())) { 40 | holder.itemView.setVisibility(View.INVISIBLE); 41 | } else { 42 | holder.itemView.setVisibility(View.VISIBLE); 43 | } 44 | } 45 | 46 | @Override 47 | public int getItemCount() { 48 | return mData.size(); 49 | } 50 | 51 | @Override 52 | public int getPositionFromId(String id) { 53 | return mData.indexOf(id); 54 | } 55 | 56 | @Override 57 | public String getIdFromPosition(int position) { 58 | return mData.get(position); 59 | } 60 | 61 | @Override 62 | public void add(int position, String data) { 63 | mData.add(position,data); 64 | } 65 | 66 | @Override 67 | public String getData(int position) { 68 | return mData.get(position); 69 | } 70 | 71 | @Override 72 | public String remove(int position) { 73 | return mData.remove(position); 74 | } 75 | 76 | public void swap(int fromPos, int toPos) { 77 | Collections.swap(mData, fromPos, toPos); 78 | } 79 | 80 | static class SimpleViewHolder extends RecyclerView.ViewHolder { 81 | 82 | TextView mTextView; 83 | 84 | SimpleViewHolder(View itemView) { 85 | super(itemView); 86 | mTextView = itemView.findViewById(R.id.content); 87 | } 88 | } 89 | 90 | long getIdByPosition(int position) { 91 | if (position > mData.size() - 1) { 92 | return RecyclerView.NO_ID; 93 | } 94 | return Long.parseLong(mData.get(position)); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/com/guolei/ui/CustomItemDecoration.java: -------------------------------------------------------------------------------- 1 | package com.guolei.ui; 2 | 3 | 4 | import android.graphics.Rect; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.view.View; 7 | 8 | 9 | public class CustomItemDecoration extends RecyclerView.ItemDecoration { 10 | 11 | @Override 12 | public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { 13 | outRect.set(0, 0, 0, 15); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/guolei/ui/KanbanBoardViewListener.java: -------------------------------------------------------------------------------- 1 | package com.guolei.ui; 2 | 3 | // _ _ _ _ 4 | //__ _____ _ __| | _| |_(_) | ___ 5 | //\ \ /\ / / _ \| '__| |/ / __| | |/ _ \ 6 | // \ V V / (_) | | | <| |_| | | __/ 7 | // \_/\_/ \___/|_| |_|\_\\__|_|_|\___| 8 | 9 | 10 | import com.guolei.boardview.BaseBoardViewListener; 11 | import com.guolei.boardview.R; 12 | 13 | /** 14 | * Copyright © 2013-2018 Worktile. All Rights Reserved. 15 | * Author: guolei 16 | * Email: 1120832563@qq.com 17 | * Date: 18/7/9 18 | * Time: 下午9:37 19 | * Desc: 20 | */ 21 | public class KanbanBoardViewListener extends BaseBoardViewListener{ 22 | 23 | @Override 24 | public int getColumnRecyclerViewId() { 25 | return R.id.recycler_view; 26 | } 27 | 28 | @Override 29 | public void onReleaseRow(int fromColumnIndex, int fromRowIndex, int toColumnIndex, int toRowIndex) { 30 | // do you self logic 31 | } 32 | 33 | @Override 34 | public void onReleaseColumn(int from, int to) { 35 | // do you self logic 36 | } 37 | 38 | @Override 39 | public int getResponseViewId() { 40 | return R.id.title; 41 | } 42 | 43 | @Override 44 | public boolean isEnableSelectColumn(int column) { 45 | return true; 46 | } 47 | 48 | @Override 49 | public boolean isEnableSwapInColumn(int column) { 50 | return true; 51 | } 52 | 53 | @Override 54 | public boolean isEnableInsertRowInColumn(int column, String s) { 55 | return true; 56 | } 57 | 58 | @Override 59 | public boolean isEnableSelectRow(int column, int row) { 60 | return true; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/guolei/ui/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.guolei.ui; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.view.Menu; 6 | import android.view.MenuItem; 7 | 8 | import com.guolei.boardview.BoardView; 9 | import com.guolei.boardview.R; 10 | 11 | public class MainActivity extends AppCompatActivity { 12 | 13 | 14 | BoardView mBoardView; 15 | 16 | @Override 17 | protected void onCreate(Bundle savedInstanceState) { 18 | super.onCreate(savedInstanceState); 19 | setContentView(R.layout.activity_main); 20 | mBoardView = findViewById(R.id.boardview); 21 | mBoardView.setListener(new KanbanBoardViewListener()); 22 | mBoardView.setAdapter(new BoardViewAdapter(mBoardView.getBoardViewHolder())); 23 | 24 | } 25 | 26 | @Override 27 | public boolean onCreateOptionsMenu(Menu menu) { 28 | getMenuInflater().inflate(R.menu.main_menu, menu); 29 | return super.onCreateOptionsMenu(menu); 30 | } 31 | 32 | @Override 33 | public boolean onOptionsItemSelected(MenuItem item) { 34 | if (item.getItemId() == R.id.less) { 35 | mBoardView.scale(); 36 | return true; 37 | } else if (item.getItemId() == R.id.restore) { 38 | mBoardView.scale(); 39 | return false; 40 | } 41 | return super.onOptionsItemSelected(item); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 16 | 21 | 26 | 31 | 36 | 41 | 46 | 51 | 56 | 61 | 66 | 71 | 76 | 81 | 86 | 91 | 96 | 101 | 106 | 111 | 116 | 121 | 126 | 131 | 136 | 141 | 146 | 151 | 156 | 161 | 166 | 171 | 172 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_recyclerview.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 21 | 29 | 30 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/menu/main_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guolei1130/BoardView/0c1c02a7e469097a2d377e8f95562fdf13d2a067/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guolei1130/BoardView/0c1c02a7e469097a2d377e8f95562fdf13d2a067/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guolei1130/BoardView/0c1c02a7e469097a2d377e8f95562fdf13d2a067/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guolei1130/BoardView/0c1c02a7e469097a2d377e8f95562fdf13d2a067/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guolei1130/BoardView/0c1c02a7e469097a2d377e8f95562fdf13d2a067/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guolei1130/BoardView/0c1c02a7e469097a2d377e8f95562fdf13d2a067/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guolei1130/BoardView/0c1c02a7e469097a2d377e8f95562fdf13d2a067/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guolei1130/BoardView/0c1c02a7e469097a2d377e8f95562fdf13d2a067/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guolei1130/BoardView/0c1c02a7e469097a2d377e8f95562fdf13d2a067/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guolei1130/BoardView/0c1c02a7e469097a2d377e8f95562fdf13d2a067/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | BoardView 3 | title 4 | footer 5 | content 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/guolei/boardview/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.guolei.boardview 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /boardview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guolei1130/BoardView/0c1c02a7e469097a2d377e8f95562fdf13d2a067/boardview.gif -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | google() 6 | jcenter() 7 | } 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.0.1' 10 | 11 | // NOTE: Do not place your application dependencies here; they belong 12 | // in the individual module build.gradle files 13 | } 14 | } 15 | 16 | allprojects { 17 | repositories { 18 | google() 19 | jcenter() 20 | } 21 | } 22 | 23 | task clean(type: Delete) { 24 | delete rootProject.buildDir 25 | } 26 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | android.injected.testOnly = false 19 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guolei1130/BoardView/0c1c02a7e469097a2d377e8f95562fdf13d2a067/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Mar 13 21:08:32 CST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 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 Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------