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

今天来研究一下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中进行的。CallbackItemTouchHelper的一个静态内部类,需要我们重写的方法有这样几个:

/**
     * 滑动或者拖拽的方向,上下左右
     * @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中绑定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~

  • 2
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值