今天来研究一下Android中拖拽排序的相关技术。我们知道,RecyclerView
是一个十分强大的类,它可以实现ListView
的所有功能,并且更易用。关于它的好处不必多说,懂的都懂。我们基于RecyclerView
来完成一个可拖拽排序的列表,并且在拖拽之后保存列表状态,这一功能在开发需求中应该使用到的还是蛮多的。
准备
开始这个功能之前,肯定是要先完成一部分知识储备。好了,开始学习~
首先,RecyclerView
为我们提供了一个拖拽和滑动删除的帮助类,这个类位于android.support.v7.widget.helper
包下,类名是ItemTouchHelper
。我们若要使用RecyclerView
的拖拽,可以复写这个类,将初始化好的对象与RecyclerView
绑定。但其实ItemTouchHelper
并非我们要关注的重点,因为一般我们只需要继承此类,用就可以了。那么我们真正去控制滑动和拖拽的类在哪呢?我们从ItemTouchHelper
的构造方法可以看出一点端倪:
/**
* Creates an ItemTouchHelper that will work with the given Callback.
* <p>
* You can attach ItemTouchHelper to a RecyclerView via
* {@link #attachToRecyclerView(RecyclerView)}. Upon attaching, it will add an item decoration,
* an onItemTouchListener and a Child attach / detach listener to the RecyclerView.
*
* @param callback The Callback which controls the behavior of this touch helper.
*/
public ItemTouchHelper(Callback callback) {
mCallback = callback;
}
构造方法接收一个Callback
类型的参数,显而易见,我们的所有操作都是在Callback
中进行的。Callback
是ItemTouchHelper
的一个静态内部类,需要我们重写的方法有这样几个:
/**
* 滑动或者拖拽的方向,上下左右
* @param recyclerView 目标RecyclerView
* @param viewHolder 目标ViewHolder
* @return 方向
*/
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
}
/**
* 拖拽item移动时产生回调
* @param recyclerView 目标RecyclerView
* @param viewHolder 拖拽的item对应的viewHolder
* @param target 拖拽目的地的ViewHOlder
* @return 是否消费拖拽事件
*/
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
}
/**
* 滑动删除时回调
* @param viewHolder 当前操作的Item对应的viewHolder
* @param direction 方向
*/
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
}
/**
* 是否可以长按拖拽
* @return
*/
@Override
public boolean isLongPressDragEnabled() {
}
/**
* 是否可以滑动删除
*/
@Override
public boolean isItemViewSwipeEnabled() {
}
有这样5个方法,用来控制是否可划动删除,是否可长按拖拽,滑动时回调,拖拽时回调以及滑动拖拽方向。好了下面我们来看看具体的实现是怎么样的。
实现
首先,初始化RecyclerView
、初始化Adapter
、绑定适配器等等老生常谈的东西,不细说了,有疑问的朋友可以看代码。
我们重点来看下ItemTouchHelper
的使用,首先,定义一个类继承ItemTouchHelper
(其实不继承,直接使用ItemTouchHelper
也可以,不过我这里为了便于控制,以及其他一些功能实现,选择继承该类):
public class BookShelfTouchHelper extends ItemTouchHelper {
private TouchCallback callback;
public BookShelfTouchHelper(TouchCallback callback) {
super(callback);
this.callback = callback;
}
public void setEnableDrag(boolean enableDrag) {
callback.setEnableDrag(enableDrag);
}
public void setEnableSwipe(boolean enableSwipe) {
callback.setEnableSwipe(enableSwipe);
}
}
这里就是一个简单的设置滑动删除和拖拽的开关,本质上还是传递给Callback
让其来处理。
下面是Callback
子类的编写,这里是重点:
public class TouchCallback extends ItemTouchHelper.Callback {
private boolean isEnableSwipe;//允许滑动
private boolean isEnableDrag;//允许拖动
private OnItemTouchCallbackListener callbackListener;//回调接口
public TouchCallback(OnItemTouchCallbackListener callbackListener) {
this.callbackListener = callbackListener;
}
/**
* 滑动或者拖拽的方向,上下左右
* @param recyclerView 目标RecyclerView
* @param viewHolder 目标ViewHolder
* @return 方向
*/
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof GridLayoutManager) {// GridLayoutManager
// flag如果值是0,相当于这个功能被关闭
int dragFlag = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT | ItemTouchHelper.UP | ItemTouchHelper.DOWN;
int swipeFlag = 0;
return makeMovementFlags(dragFlag, swipeFlag);
} else if (layoutManager instanceof LinearLayoutManager) {// linearLayoutManager
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
int orientation = linearLayoutManager.getOrientation();
int dragFlag = 0;
int swipeFlag = 0;
if (orientation == LinearLayoutManager.HORIZONTAL) {//横向布局
swipeFlag = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
dragFlag = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
} else if (orientation == LinearLayoutManager.VERTICAL) {//纵向布局
dragFlag = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
swipeFlag = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
}
return makeMovementFlags(dragFlag, swipeFlag);
}
return 0;
}
/**
* 拖拽item移动时产生回调
* @param recyclerView 目标RecyclerView
* @param viewHolder 拖拽的item对应的viewHolder
* @param target 拖拽目的地的ViewHOlder
* @return 是否消费拖拽事件
*/
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
if(this.callbackListener != null) {
this.callbackListener.onMove(viewHolder.getAdapterPosition(),target.getAdapterPosition());
}
return false;
}
/**
* 滑动删除时回调
* @param viewHolder 当前操作的Item对应的viewHolder
* @param direction 方向
*/
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
if(this.callbackListener != null) {
this.callbackListener.onSwiped(viewHolder.getAdapterPosition());
}
}
/**
* 是否可以长按拖拽
* @return
*/
@Override
public boolean isLongPressDragEnabled() {
return isEnableDrag;
}
/**
* 是否可以滑动删除
*/
@Override
public boolean isItemViewSwipeEnabled() {
return isEnableSwipe;
}
public void setEnableDrag(boolean enableDrag) {
this.isEnableDrag = enableDrag;
}
public void setEnableSwipe(boolean enableSwipe) {
this.isEnableSwipe = enableSwipe;
}
}
可以看到,主要代码集中在了getMovementFlags
获取滑动、拖拽方向这个方法上了,滑动以及拖拽的回调反而代码较少,这里是因为设置了代理,使用OnItemTouchCallbackListener
这一接口来处理两个事件,聪明的朋友一定已经想到了,使用Activity来实现这一接口,从而实现管理回调的功能,先来看看这个接口:
public interface OnItemTouchCallbackListener {
/**
* 当某个Item被滑动删除时回调
*/
void onSwiped(int adapterPosition);
/**
* 当两个Item位置互换的时候被回调(拖拽)
* @param srcPosition 拖拽的item的position
* @param targetPosition 目的地的Item的position
* @return 开发者处理了操作应该返回true,开发者没有处理就返回false
*/
boolean onMove(int srcPosition, int targetPosition);
}
一个拖拽,一个滑动。接着在Activity
中绑定ItemTouchHelper
和RecyclerView
:
private void initView() {
touchHelper = new BookShelfTouchHelper(new TouchCallback(this));
touchHelper.setEnableDrag(true);
touchHelper.setEnableSwipe(true);
touchHelper.attachToRecyclerView(rvBooks);
}
并使Activity
实现OnItemTouchCallbackListener
接口:
@Override
public void onSwiped(int position) {
//处理划动删除操作
if(books != null && position >= 0 && position < books.size()) {
books.remove(position);
adapter.notifyItemRemoved(position);
}
}
@Override
public boolean onMove(int srcPosition, int targetPosition) {
//处理拖拽事件
if(books == null || books.size() == 0) {
return false;
}
if(srcPosition >= 0 && srcPosition < books.size() && targetPosition >= 0 && targetPosition < books.size()) {
//交换数据源两个数据的位置
Collections.swap(books,srcPosition,targetPosition);
//更新视图
adapter.notifyItemMoved(srcPosition,targetPosition);
//消费事件
return true;
} else {
return false;
}
}
其中books
使数据源,删除操作很简单,remove
即可。拖拽时需要先更新数据源,接着更新视图,之后返回true
消费该事件。到这里,我们拖拽排序的功能就实现了。
但是我现在有一个问题了,如果我要求最后一个按钮是一个增加的按钮,它不参与排序,那要怎么做呢?最开始我的想法是在onMove
方法中去拦截,但是仔细想想这样又有些不对,因为onMove
已经调用在拖拽之后了。后来没思路,看到ItemTouchHelper
中的一个方法:
public void startDrag(ViewHolder viewHolder) {
}
这不是开始拖拽的起点吗,那我从这里开始拦截不就好了,然而,并没有什么卵用
再查查,发现此方法是要手动调用的,ok,isLongPressDragEnabled
方法返回false
,给ViewHolder
的rootView
增加长按事件,在长按事件中开始手势。搞定。
ps:注意这一步完成后只是添加按钮不能被拖拽,还要防止该按钮被其他按钮更换位置,因此需要在onMove
方法中判断targetViewHolder
的位置。
ok,我们现在只剩下一个工作了,保存拖拽后的数据。我这里选择使用Gson
与SharedPreference
结合使用,类如下:
public class DataUtils {
public static final String DEFAULT_SP_NAME = "DEFAULT_SP_NAME";
public static <T> void saveData(List<T> data, String spName, String key, Context context) {
SharedPreferences preferences = context.getSharedPreferences(spName,Context.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
Gson gson = new Gson();
String jsonString = gson.toJson(data);
editor.putString(key,jsonString);
editor.apply();
}
public static List<Book> getData(String spName, String key, Context context) {
List<Book> data = new ArrayList<Book>();
SharedPreferences preferences = context.getSharedPreferences(spName,Context.MODE_PRIVATE);
String jsonString = preferences.getString(key,null);
if(jsonString == null) {
return data;
}
Log.e("JSON",jsonString + "ssss"+new TypeToken<List<Book>>(){}.getType());
Gson gson = new Gson();
data = gson.fromJson(jsonString,new TypeToken<List<Book>>(){}.getType());
return data;
}
}
期间Java
泛型擦除的问题搞得我头痛
演示
ok,最后是演示效果:
好了,以上就是本次博客的全部内容了,如果您对本文有任何疑问或者文章有错误或者遗漏,请在评论留言告诉我,不胜感激~
本文代码地址github,欢迎fork~
本文同步发表在我的个人博客,欢迎来访~
enjoy~