要实现 RecycleView 中的拖拽滑动, 在以往的经验中经常要依赖 GestureDetectors,onInterceptTouchEvent 等来实现, 然而在 RecyclerView 上添加拖动特性有一个非常简单的方法它就是: ItemTouchHelper.
一, 效果图
以下就是通过 RecycleView+ItemTouchHelper 实现拖拽滑动的效果图, 看起来有没有很炫酷. 其实实现起来很简单, 我们接下来就开始介绍.
[图片上传失败...(image-b9a3ae-1559563627001)]
二, ItemTouchHelper 的介绍
ItemTouchHelper 是一个强大的工具, 它处理好了关于在 RecyclerView 上添加拖动排序与滑动删除的所有事情. 它是 RecyclerView.ItemDecoration 的子类, 也就是说它可以轻易的添加到几乎所有的 LayoutManager 和 Adapter 中. 它还可以和现有的 item 动画一起工作, 提供受类型限制的拖放动画等等.
1. 添加依赖
compile 'com.android.support:recyclerview-v7:25.1.0'
因为要使用 RecycleView, 同时 ItemTouchHelper 也是 RecycleView 中的类.
2. 自定义 ItemTouchHelper.Callback 类
为了使用 ItemTouchHelper, 你需要实现 ItemTouchHelper.Callback 接口, 通过这个接口, 你可以监听 "move" 和 "swipe" 事件, 在这里你也可以控制 View 的选择状态和重写默认动画.
必须实现主要的回调方法:
- getMovementFlags(RecyclerView, ViewHolder)
- onMove(RecyclerView, ViewHolder, ViewHolder)
- onSwiped(ViewHolder, int)
具体解释这三个方法:
- public int getMovementFlags(RecyclerView recyclerView,
- RecyclerView.ViewHolder viewHolder) {
- int dragFlags = ItemTouchHelper.UP| ItemTouchHelper.DOWN;
- int swipeFlags = ItemTouchHelper.START| ItemTouchHelper.END;
- return makeMovementFlags(dragFlags, swipeFlags);
- }
ItemTouchHelper 允许你判断事件方向. 但你必须覆写 getMovementFlags() 方法去指定支持哪些方向. 使用 ItemTouchHelper.makeMovementFlags(int, int) 创建代表方向的 Flag. 这里我们同时支持 drag 和 swipe. 实现这个方法, ItemTouchHelper 可以只能 drag 而不能 swipe(反之亦然), 总之根据自己的需求指定.
- onMove(RecyclerView, ViewHolder, ViewHolder)
- onSwiped(ViewHolder, int)
当 Item 移动或者滑动时, 会回调这两个方法, 然后可以在这两个方法内部设置回调通知更新适配器或者页面显示的数据.
我们还将使用 2 个帮助方法:
- @Override
- public boolean isLongPressDragEnabled() {
- return true;
- }
实现 isLongPressDragEnabled() 方法返回 true 去支持长按 RecyclerView 的 item 时的 drag 事件. 或者, 也可以调用 ItemTouchHelper.startDrag(RecyclerView.ViewHolder) 方法来开始一个拖动.
- @Override
- public boolean isItemViewSwipeEnabled() {
- return true;
- }
实现 isItemViewSwipeEnabled() 方法返回 true 开启触摸视图时的 swipe 功能. 另外 ItemTouchHelper.startSwipe(RecyclerView.ViewHolder) 也开始 swipe 事件.
设置给 RecycleView:
实现了以上的方法后, 就会监听到拖拽和滑动的手势, 并会处理相关操作.
接下来需要做的就是把实现的自定义 ItemTouchHelper.Callback 类设置给 RecycleView.
- ItemDragHelperCallback callback = new ItemDragHelperCallback(mMineAdapter);
- ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
- touchHelper.attachToRecyclerView(mNewsChannelMineRv);
以上就是关于 RecycleView+ItemTouchHelper 实现拖拽滑动的简单介绍, 下面为实现上述效果图, 具体讲解其实现过程.
三, RecycleView+ItemTouchHelper 实现拖拽的实例应用
1. 布局文件
- <LinearLayout xmlns:Android="http://schemas.android.com/apk/res/android"
- xmlns:App="http://schemas.android.com/apk/res-auto"
- Android:layout_width="match_parent"
- Android:layout_height="match_parent"
- Android:background="@color/white"
- Android:clipToPadding="true"
- Android:fitsSystemWindows="true"
- Android:orientation="vertical">
- <Android.support.v7.widget.Toolbar
- Android:id="@+id/toolbar"
- style="@style/action_bar"
- Android:background="@color/colorPrimary"
- App:navigationIcon="@drawable/ic_arrow_back"
- App:theme="@style/AppTheme.PopupOverlay"
- App:title="@string/channel_manage" />
- <TextView
- style="@style/news_channel_sort_title"
- Android:text="我的频道 长按并拖拽可排序" />
- <Android.support.v7.widget.RecyclerView
- Android:id="@+id/news_channel_mine_rv"
- Android:layout_width="match_parent"
- Android:layout_height="wrap_content"
- Android:overScrollMode="never"></Android.support.v7.widget.RecyclerView>
- <TextView
- style="@style/news_channel_sort_title"
- Android:text="@string / 更多频道 点击添加" />
- <Android.support.v7.widget.RecyclerView
- Android:id="@+id/news_channel_more_rv"
- Android:layout_width="match_parent"
- Android:layout_height="match_parent"
- Android:overScrollMode="never"></Android.support.v7.widget.RecyclerView>
- </LinearLayout>
一共用了两个 RecyclerView 来分别实现我的频道和更多频道的内容, 其实可以使用一个 RecyclerView 通过判断类型来实现多布局类型, 效果会更好, 后续会尝试, 暂时先这样了. 感兴趣的可以参考我的另一篇文章来自行实现.
2. 点击 Item 增删效果的实现
先来个简单的, 就是点击 Item 后, 我的频道和更多频道中, 一个频道删除点击的频道, 另一个频道增加该频道.
思路: 设置 Item 的点击事件的监听
在 Adaper 类中, 设置监听接口, 并提供传入接口对象的方法让 Activity 调用
- //Item 点击事件的监听接口
- public interface OnItemClickListener {
- void onItemClick(View view, int position);
- }
- public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
- mOnItemClickListener = onItemClickListener;
- }
当点击 Item 后, 出发监听, 产生回调
- if (mOnItemClickListener != null) {
- holder.mLayout.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- if (!table.getNewsChannelFixed()) {
- // 对项目点击后增删操作的监听
- mOnItemClickListener.onItemClick(view, holder.getLayoutPosition());
- }
- }
- });
- }
在 Activity 中根据传入的数据进行操作
我的频道所对应的 RecyclerView 的操作
- mMineAdapter.setOnItemClickListener(new NewsChannelAdapter.OnItemClickListener() {
- @Override
- public void onItemClick(View view, int position) {
- NewsChannelTable newsChannel = mMineAdapter.getAdapterData().get(position);
- mMoreAdapter.getAdapterData().add(newsChannel);
- mMoreAdapter.notifyDataSetChanged();
- mMineAdapter.getAdapterData().remove(position);
- mMineAdapter.notifyDataSetChanged();
- // 进行添加或删除操作后, 要更新的列表 进行存储
- mPresenter.onItemAddOrRemove((ArrayList<NewsChannelTable>) mMineAdapter.getAdapterData(), (ArrayList<NewsChannelTable>) mMoreAdapter.getAdapterData());
- }
- });
更多频道所对应的 RecyclerView 的操作
- mMoreAdapter.setOnItemClickListener(new NewsChannelAdapter.OnItemClickListener() {
- @Override
- public void onItemClick(View view, int position) {
- if(mMineAdapter.getAdapterData().size()==7){
- ToastUitl.showShort("最多只能添加 7 个");
- }else{
- NewsChannelTable newsChannel = mMoreAdapter.getAdapterData().get(position);
- mMoreAdapter.getAdapterData().remove(position);
- mMoreAdapter.notifyDataSetChanged();
- mMineAdapter.getAdapterData().add(newsChannel);
- mMineAdapter.notifyDataSetChanged();
- List<NewsChannelTable> data = mMineAdapter.getAdapterData();
- for (NewsChannelTable table : data) {
- System.out.println(table);
- }
- // 进行添加或删除操作后, 要更新的列表 进行存储
- mPresenter.onItemAddOrRemove((ArrayList<NewsChannelTable>) mMineAdapter.getAdapterData(), (ArrayList<NewsChannelTable>) mMoreAdapter.getAdapterData());
- }
主要实现两个内容:
第一: RecyclerView 中内容数据的修改更新, 呈现点击后增删的效果.
第二: 调用方法通知进行缓存处理, 记录修改后的效果. 同时通知新闻首页中频道的更新显示. 关于第二部分的内容不详细介绍了, 可以看最下方的源码地址.
3. 自定义 ItemTouchHelper.Callback 类实现拖拽效果
- @Override
- public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
- // 根据 recyclerView 的布局, 进行设置拖拽的方向
- int dragFlags = setDragFlags(recyclerView);
- // 不允许进行滑动
- int swipeFlags = 0;
- return makeMovementFlags(dragFlags, swipeFlags);
- }
- private int setDragFlags(RecyclerView recyclerView) {
- int dragFlags;
- RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
- if (layoutManager instanceof GridLayoutManager || layoutManager instanceof StaggeredGridLayoutManager) {
- dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
- } else {
- dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
- }
- return dragFlags;
- }
先根据布局判断支持的拖拽的方向, 如果是 GridLayoutManager 和 StaggeredGridLayoutManager 支持上下左右拖拽, 如果是 LinearLayoutManager 支持上下拖拽, 本例中不支持滑动操作.
- @Override
- public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
- return mOnItemMoveListener.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
- }
监听移动事件, 其中 mOnItemMoveListener.onItemMove 是对移动的监听的回调, 判断是否可以移动并通知 Adapter 类数据更新.
- @Override
- public boolean onItemMove(int fromPosition, int toPosition) {
- if (isChannelFixed(fromPosition, toPosition)) {
- return false;
- }
- // 在我的频道中进行子频道的移动
- Collections.swap(getAdapterData(), fromPosition, toPosition);
- notifyItemMoved(fromPosition, toPosition);
- // 通知顺序变换, 存储, 设置频道顺序, 以及显示的顺序
- System.out.println("发送移动的消息");
- EventBus.getDefault().post(new ChannelBean(getAdapterData()));
- return true;
- }
- // 不能移动头条
- private boolean isChannelFixed(int fromPosition, int toPosition) {
- return fromPosition == 0 || toPosition == 0;
- }
两种情况:
第一: 如果移动的是 "头条" 频道或者移动到 "头条" 频道, 返回 false, 则不能进行移动.
第二: 不是上面的情况. 更新我的频道栏目中频道的显示顺序, 同时通知数据缓存并通知新闻首页频道顺序的更新. 关于这部分的详细内容, 可以看最下方的源码.
- // 返回 true 允许拖拽
- @Override
- public boolean isLongPressDragEnabled() {
- return mIsLongPressEnabled;
- }
是否允许拖拽, 通过外部传入来开启.
- public void setLongPressEnabled(boolean longPressEnabled) {
- mIsLongPressEnabled = longPressEnabled;
- }
在 Adapter 类中, 根据触摸的 Item 的类型来判断是否开启长按拖拽.
- if (mItemDragHelperCallback != null) {
- holder.mLayout.setOnTouchListener(new View.OnTouchListener() {
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- mItemDragHelperCallback.setLongPressEnabled(table.getNewsChannelIndex() == 0 ? false : true);
- return false;
- }
- });
- }
如果触摸的对象是 "头条" 频道, 则不开启拖拽, 其他情况就会开启长按.
4. 设置给 RecycleView
- //Adapter 类实现了 OnItemMoveListener 的接口, 将其传入 ItemDragHelperCallback 方便接口回调
- ItemDragHelperCallback callback = new ItemDragHelperCallback(mMineAdapter);
- // 将自定义的 ItemDragHelperCallback 类传给 ItemTouchHelper
- ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
- // 将 ItemTouchHelper 设置给 RecyclerView
- touchHelper.attachToRecyclerView(mNewsChannelMineRv);
通过以上步骤就可以实现效果图中的效果, 真实效果还是不错的. 源码地址, 感兴趣的看一下, 给个 Star 支持下, 看项目中的 NewsChannelActivity 部分即可.
自己是从事了七年开发的 Android 工程师, 不少人私下问我, 2019 年 Android 进阶该怎么学, 方法有没有?
没错, 年初我花了一个多月的时间整理出来的学习资料, 希望能帮助那些想进阶提升 Android 开发, 却又不知道怎么进阶学习的朋友.[包括高级 UI, 性能优化, 架构师课程, NDK,Kotlin, 混合式开发 (ReactNative+Weex),Flutter 等架构技术资料] , 希望能帮助到您面试前的复习且找到一个好的工作, 也节省大家在网上搜索资料的时间来学习.
来源: http://www.jianshu.com/p/b1242762518b