Android列表拖拽排序及禁止拖拽以及保存排序状态

今天来研究一下Android中拖拽排序的相关技术。咱们知道,RecyclerView是一个十分强大的类,它能够实现ListView的全部功能,而且更易用。关于它的好处没必要多说,懂的都懂。咱们基于RecyclerView来完成一个可拖拽排序的列表,而且在拖拽以后保存列表状态,这一功能在开发需求中应该使用到的仍是蛮多的。java

准备

开始这个功能以前,确定是要先完成一部分知识储备。好了,开始学习~android

首先,RecyclerView为咱们提供了一个拖拽和滑动删除的帮助类,这个类位于android.support.v7.widget.helper包下,类名是ItemTouchHelper。咱们若要使用RecyclerView的拖拽,能够复写这个类,将初始化好的对象与RecyclerView绑定。但其实ItemTouchHelper并不是咱们要关注的重点,由于通常咱们只须要继承此类,用就能够了。那么咱们真正去控制滑动和拖拽的类在哪呢?咱们从ItemTouchHelper的构造方法能够看出一点端倪:git

/** * 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中进行的。CallbackItemTouchHelper的一个静态内部类,须要咱们重写的方法有这样几个:github

/** * 滑动或者拖拽的方向,上下左右 * @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个方法,用来控制是否可划动删除,是否可长按拖拽,滑动时回调,拖拽时回调以及滑动拖拽方向。好了下面咱们来看看具体的实现是怎么样的。web

实现

首先,初始化RecyclerView、初始化Adapter、绑定适配器等等老生常谈的东西,不细说了,有疑问的朋友能够看代码。json

咱们重点来看下ItemTouchHelper的使用,首先,定义一个类继承ItemTouchHelper(其实不继承,直接使用ItemTouchHelper也能够,不过我这里为了便于控制,以及其余一些功能实现,选择继承该类):app

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让其来处理。ide

下面是Callback子类的编写,这里是重点:svg

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中绑定ItemTouchHelperRecyclerView:

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,给ViewHolderrootView增长长按事件,在长按事件中开始手势。搞定。

ps:注意这一步完成后只是添加按钮不能被拖拽,还要防止该按钮被其余按钮更换位置,所以须要在onMove方法中判断targetViewHolder的位置。

ok,咱们如今只剩下一个工做了,保存拖拽后的数据。我这里选择使用GsonSharedPreference结合使用,类以下:

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~