Android一步一步带你实现RecyclerView的拖拽和侧滑删除功能

版权声明:欢迎转载,转载请注明出处http://blog.csdn.net/nugongahou110 https://blog.csdn.net/nugongahou110/article/details/50505210

先上效果图:
这里写图片描述

本篇文章我们来学习一个开源项目Android-ItemTouchHelper-Demo
这个项目使用了RecyclerView的ItemTouchHelper类实现了Item的拖动和删除功能,ItemTouchHelper是v7包下的一个类,我们看一下他的介绍

This is a utility class to add swipe to dismiss and drag & drop support to RecyclerView.

这是一个工具类,专门用来配合RecyclerView实现滑动删除和拖拽功能的类

先搭起一个小框架

我们从头开始,一点一点实现最终的功能,首先我们先搭起一个小框架,我们的首页显示两个Item,一个点击进入ListView形式的RecyclerView;一个点击进入GridView形式的RecyclerView。
这里写图片描述

我们先在values/strings.xml中定义一个数组

 <array name="main_items">
        <item>List - Basic Drag and Swipe</item>
        <item>Grid - Basic Drag</item>
 </array>

再创建一个MainFragment继承自ListFragment

public class MainFragment extends ListFragment {
    private onListItemClickListener mListItemClickListener;
    //定义一个回调接口,用来将点击事件传回他的宿主Activity去做,Fragment中不做具体的逻辑操作
    public interface onListItemClickListener{
        void onListItemClick(int position);
    }
    public MainFragment(){

    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        //他的宿主Activity将实现onListItemClickListener接口
        //使用getActivity()获得的宿主Activity,将他强转成onListItemClickListener接口
       mListItemClickListener = (onListItemClickListener)getActivity();
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        //获得我们在strings.xml中定义个数组
        final String[] items = getResources().getStringArray(R.array.main_items);
        //创建适配器
        final ArrayAdapter<String> adapter = new ArrayAdapter<>(getActivity(),
                android.R.layout.simple_list_item_1, items);
        //设置适配器
        setListAdapter(adapter);
    }

    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        if (mListItemClickListener!=null){
            //由于宿主Activity实现了onListItemClickListener接口
            //因此调用的是宿主Activity的onListItemClick方法
            //并且将点击的item的position传给Activity
            mListItemClickListener.onListItemClick(position);
        }
    }
}

我们再创建一个RecyclerListFragment,我们先不做具体的实现,只是先把架子搭起来

public class RecyclerListFragment extends Fragment {
    public RecyclerListFragment(){}

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return new RecyclerView(container.getContext());
    }
}

再来一个RecyclerGridFragment

public class RecyclerGridFragment extends Fragment {
    public RecyclerGridFragment(){}

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return new RecyclerView(container.getContext());
    }
}

好了,Fragment我们已经准备好了,就差一个宿主Activity了,现在我们就来创建MainActivity,并且实现MainFragment.OnListItemClickListener接口,重写onListItemClick方法

public class MainActivity extends AppCompatActivity implements MainFragment.onListItemClickListener{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //当savedInstanceState为null时才new一个MainFragment出来
        //否则每次旋转屏幕都会new出来一个
        if (savedInstanceState == null){
            MainFragment fragment = new MainFragment();
            //用add将MainFragment添加到framelayout上
            getSupportFragmentManager().beginTransaction()
                    .add(R.id.content,fragment)
                    .commit();
        }
    }


    @Override
    public void onListItemClick(int position) {
        //当MainFragment的Item被点击后,就会回调此方法
        //在此方法中写真正的逻辑,这样Activity和Fragment
        //之间就是松耦合关系,MainFragment可以复用
        Fragment fragment = null;
        switch (position){
            case 0:
                //当点击第一个item时候,new一个RecyclerListFragment
                fragment = new RecyclerListFragment();
                break;
            case 1:
                //当点击第二个item时候,new一个RecyclerGridFragment
                fragment = new RecyclerGridFragment();
                break;
        }
        //这次用replace,替换framelayout的布局,也就是MainFragment
        getSupportFragmentManager().beginTransaction()
                .replace(R.id.content,fragment)
                .addToBackStack(null)
                .commit();
    }
}

activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">


    <FrameLayout
        android:id="@+id/content"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

好了,现在我们可以运行一下,运行的结果就是一开始那个截图的效果,我们点击item会进入相应的Fragment中,但是现在是空白的,因为我们还没写完呢。

为RecyclerView写Adapter

我们之前使用ListView的时候,数据是靠Adapter适配到ListView上的吧,RecyclerView也是靠Adapter,所以我们先来写个Adapter吧

public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.ItemViewHolder> {

    /**在这里反射出我们的item的布局*/
    @Override
    public RecyclerViewAdapter.ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return null;
    }
    /**在这里为布局中的控件设置数据*/
    @Override
    public void onBindViewHolder(ItemViewHolder holder, int position) {

    }
    /**返回数据个数*/
    @Override
    public int getItemCount() {
        return 0;
    }
    /**相当于ListView中的ViewHolder*/
    public static class ItemViewHolder extends RecyclerView.ViewHolder{

        public ItemViewHolder(View itemView) {
            super(itemView);
        }
    }
}

这就是一个标准的Adapter的结构,接下来我们要逐一完善其中的方法,首先我们先在values/strings.xml中增加我们item的数组

<array name="dummy_items">
        <item>One</item>
        <item>Two</item>
        <item>Three</item>
        <item>Four</item>
        <item>Five</item>
        <item>Six</item>
        <item>Seven</item>
        <item>Eight</item>
        <item>Nine</item>
        <item>Ten</item>
    </array>

接着在构造方法中将数据添加到ArrayList中

 public RecyclerViewAdapter(Context context){
        //初始化数据
        mItems.addAll(Arrays.asList(context.getResources().getStringArray(R.array.dummy_items)));
    }

然后我们再写我们item的布局文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    >

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:text="one"
        android:padding="20dp"
        android:textAppearance="?android:attr/textAppearanceMedium" />

    <ImageView
        android:id="@+id/handle"
        android:layout_width="?listPreferredItemHeight"
        android:layout_height="?listPreferredItemHeight"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:scaleType="center"
        android:src="@drawable/ic_reorder_grey_500_24dp"
        />
</RelativeLayout>

接下来是在ItemViewHolder中进行findViewById操作

 /**相当于ListView中的ViewHolder*/
    public static class ItemViewHolder extends RecyclerView.ViewHolder{
        private TextView text;
        private ImageView handle;
        public ItemViewHolder(View itemView) {
            super(itemView);
            text = (TextView) itemView.findViewById(R.id.text);
            handle = (ImageView) itemView.findViewById(R.id.handle);
        }
    }

然后在onCreateViewHolder中加载出布局,并且完成控件的初始化

 /**在这里反射出我们的item的布局*/
    @Override
    public RecyclerViewAdapter.ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        //利用反射将item的布局加载出来
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_view,null);
        //new一个我们的ViewHolder,findViewById操作都在ItemViewHolder的构造方法中进行了
        return new ItemViewHolder(view);
    }

然后在onBindViewHolder中给控件绑定数据

 /**在这里为布局中的控件设置数据*/
    @Override
    public void onBindViewHolder(ItemViewHolder holder, int position) {
        holder.text.setText(mItems.get(position));
        //handle是我们拖动item时候要用的,目前先空着
        holder.handle.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                return false;
            }
        });
    }

还有这个方法别忘了

 /**返回数据个数*/
    @Override
    public int getItemCount() {
        return mItems.size();
    }

好了我们一个Adapter已经写完了,然后我们来到RecyclerListFragment中给我们的RecyclerView进行配置

public class RecyclerListFragment extends Fragment {
    public RecyclerListFragment(){}

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return new RecyclerView(container.getContext());
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        RecyclerViewAdapter adapter = new RecyclerViewAdapter(getActivity());
        //参数view即为我们在onCreateView中return的view
        RecyclerView recyclerView = (RecyclerView)view;
        //固定recyclerview大小
        recyclerView.setHasFixedSize(true);
        //设置adapter
        recyclerView.setAdapter(adapter);
        //设置布局类型为LinearLayoutManager,相当于ListView的样式
        recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));

    }
}

同样的,我们再来配置RecyclerGridFragment

public class RecyclerGridFragment extends Fragment {
    public RecyclerGridFragment(){}

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return new RecyclerView(container.getContext());
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        RecyclerViewAdapter adapter = new RecyclerViewAdapter(getActivity());
        RecyclerView recyclerView = (RecyclerView)view;
        recyclerView.setHasFixedSize(true);
        recyclerView.setAdapter(adapter);
        //只有这里和RecyclerListFragment不一样,这里我们指定布局为GridView样式,2列
        recyclerView.setLayoutManager(new GridLayoutManager(getActivity(),2));

    }
}

好了,现在我们可以运行了,这就是recyclerView的使用方法,接下来我们就要为recyclerView添加拖拽和侧滑删除的功能了

实现拖拽和侧滑删除功能

拖拽和侧滑删除的功能我们要借助ItemTouchHelper这个类,我们只需要创建出一个ItemTouchHelper对象,然后调用mItemTouchHelper.attachToRecyclerView(recyclerView);就可以了。
我们看一下ItemTouchHelper的构造方法,他需要一个Callback

    public ItemTouchHelper(Callback callback) {
        mCallback = callback;
    }

这个Callback是ItemTouchHelper的内部类,所以我们需要写一个类继承自ItemTouchHelper.Callback ,然后重写里面的方法

public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
    /**这个方法是用来设置我们拖动的方向以及侧滑的方向的*/
    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
       return 0;
    }
    /**当我们拖动item时会回调此方法*/
    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {

        return false;
    }
    /**当我们侧滑item时会回调此方法*/
    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {

    }
}

首先先来完成getMovementFlags方法

 /**这个方法是用来设置我们拖动的方向以及侧滑的方向的*/
    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {

        //如果是ListView样式的RecyclerView
        if (recyclerView.getLayoutManager() instanceof LinearLayoutManager){
            //设置拖拽方向为上下
            final int dragFlags = ItemTouchHelper.UP|ItemTouchHelper.DOWN;
            //设置侧滑方向为从左到右和从右到左都可以
            final int swipeFlags = ItemTouchHelper.START|ItemTouchHelper.END;
            //将方向参数设置进去
            return makeMovementFlags(dragFlags,swipeFlags);
        }else{//如果是GridView样式的RecyclerView
            //设置拖拽方向为上下左右
            final int dragFlags = ItemTouchHelper.UP|ItemTouchHelper.DOWN|
                    ItemTouchHelper.LEFT|ItemTouchHelper.RIGHT;
            //不支持侧滑
            final int swipeFlags = 0;
            return makeMovementFlags(dragFlags,swipeFlags);
        }
    }

当item被拖拽或者侧滑的时候会回调onMove和onSwiped方法,所以我们需要同时Adapter做出相应的改变,对mItems数据做出交换或者删除的操作,因此我们需要一个回调接口来继续回调Adapter中的方法

public interface onMoveAndSwipedListener {
    boolean onItemMove(int fromPosition , int toPosition);
    void onItemDismiss(int position);
}

我们让RecyclerViewAdapter实现此接口,并且重写里面的两个方法

public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.ItemViewHolder>
                implements onMoveAndSwipedListener

重写两个方法

 @Override
    public boolean onItemMove(int fromPosition, int toPosition) {
        //交换mItems数据的位置
        Collections.swap(mItems,fromPosition,toPosition);
        //交换RecyclerView列表中item的位置
        notifyItemMoved(fromPosition,toPosition);
        return true;
    }

    @Override
    public void onItemDismiss(int position) {
        //删除mItems数据
        mItems.remove(position);
        //删除RecyclerView列表对应item
        notifyItemRemoved(position);
    }

好了,现在我们再回到我们的SimpleItemTouchHelperCallback,在构造方法中将实现了onMoveAndSwipedListener接口的RecyclerViewAdapter 传进来

private onMoveAndSwipedListener mAdapter;

    public SimpleItemTouchHelperCallback(onMoveAndSwipedListener listener){
        mAdapter = listener;
    }

现在我们在onMove和onSwipe方法中调用mAdapter的onItemMove和onItemDismiss方法,就相当于通知adapter去做相应的改变了

 /**当我们拖动item时会回调此方法*/
    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
        //如果两个item不是一个类型的,我们让他不可以拖拽
        if (viewHolder.getItemViewType() != target.getItemViewType()){
            return false;
        }
        //回调adapter中的onItemMove方法
        mAdapter.onItemMove(viewHolder.getAdapterPosition(),target.getAdapterPosition());
        return true;
    }
    /**当我们侧滑item时会回调此方法*/
    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
        //回调adapter中的onItemDismiss方法
        mAdapter.onItemDismiss(viewHolder.getAdapterPosition());
    }

好了,现在我们回到RecyclerListFragment中,在onViewCreated方法中添加如下几行代码,将ItemTouchHelper和recyclerView关联起来

 @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        RecyclerViewAdapter adapter = new RecyclerViewAdapter(getActivity());
        //参数view即为我们在onCreateView中return的view
        RecyclerView recyclerView = (RecyclerView)view;
        //固定recyclerview大小
        recyclerView.setHasFixedSize(true);
        //设置adapter
        recyclerView.setAdapter(adapter);
        //设置布局类型为LinearLayoutManager,相当于ListView的样式
        recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
        //关联ItemTouchHelper和RecyclerView
        ItemTouchHelper.Callback callback = new SimpleItemTouchHelperCallback(adapter);
        mItemTouchHelper = new ItemTouchHelper(callback);
        mItemTouchHelper.attachToRecyclerView(recyclerView);
    }

现在运行一下程序,我们已经可以实现拖拽和侧滑删除的功能了
这里写图片描述

现在我们为RecyclerGridFragment同样添加一下关联代码

 @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        RecyclerViewAdapter adapter = new RecyclerViewAdapter(getActivity());
        RecyclerView recyclerView = (RecyclerView)view;
        recyclerView.setHasFixedSize(true);
        recyclerView.setAdapter(adapter);
        //只有这里和RecyclerListFragment不一样,这里我们指定布局为GridView样式,2列
        recyclerView.setLayoutManager(new GridLayoutManager(getActivity(),2));

        ItemTouchHelper.Callback callback = new SimpleItemTouchHelperCallback(adapter);
        mItemTouchHelper = new ItemTouchHelper(callback);
        mItemTouchHelper.attachToRecyclerView(recyclerView);

    }

看一下效果
这里写图片描述

处理细节

1.拖动图标即可拖拽整个item

OK,目前我们的功能已经实现了,但是还有一些细节我们需要处理,我们还记得当时我们的item中有一个ImageView对吧,我们想通过点击ImageView就可以拖拽item,而目前只能通过长按才能够拖动。
我们回到RecyclerListFragment中,找到刚才我们还空着的ImageView的onTouch方法

 holder.handle.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                return false;
            }
        });

在onTouch方法中,我们应该回调RecyclerListFragment类中的mItemTouchHelper,调用mItemTouchHelper的onStartDrag方法,因此我们又需要一个回调接口

public interface onStartDragListener {
    void startDrag(RecyclerView.Adapter adapter);
}

我们让RecyclerListFragment实现此接口并且重写startDrag方法

 @Override
    public void startDrag(RecyclerView.ViewHolder viewHolder) {
        mItemTouchHelper.startDrag(viewHolder);
    }

我们应该将实现了onStartDragListener接口的RecyclerListFragment对象传给RecyclerViewAdapter,那么我们就要在RecyclerViewAdapter的构造方法中添加一个参数

 public RecyclerViewAdapter(Context context , onStartDragListener startDragListener){
        //初始化数据
        mItems.addAll(Arrays.asList(context.getResources().getStringArray(R.array.dummy_items)));
        mStartDragListener = startDragListener;
    }

接着在ImageView的onTouch方法中做如下操作

  holder.handle.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                //如果按下
                if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN){
                    //回调RecyclerListFragment中的startDrag方法
                    //让mItemTouchHelper执行拖拽操作
                    mStartDragListener.startDrag(holder);
                }
                return false;
            }
        });

好了,现在我们可以通过拖动item右侧的ImageView来拖拽整个item了
这里写图片描述
这里写图片描述

2.拖拽item时改变item的背景颜色

我们来到SimpleItemTouchHelperCallback中,重写onSelectedChanged这个回调方法

/**当状态改变时回调此方法*/
    @Override
    public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
        //当前状态不是idel(空闲)状态时,说明当前正在拖拽或者侧滑
        if (actionState != ItemTouchHelper.ACTION_STATE_IDLE){
            //TODO 改变item的背景颜色
        }
        super.onSelectedChanged(viewHolder, actionState);
    }

改变item的背景颜色我们仍然需要在adapter中去做实际的修改,因此我们还需要一个回调接口,我们已经写了3个回调接口了

public interface onStateChangedListener {
    void onItemSelected();
}

我们应该让谁来实现这个接口并且重写onItemSelected方法呢?我们看到onSelectedChanged方法中第一个参数是RecyclerView.ViewHolder。 其实在RecyclerView.ViewHolder中有个成员参数itemView,他就是我们item的布局,我们修改item的背景颜色直接修改itemView的背景颜色就可以了,所以我们让我们的ViewHolder实现这个接口

 public static class ItemViewHolder extends RecyclerView.ViewHolder 
                implements onStateChangedListener{
        private TextView text;
        private ImageView handle;
        public ItemViewHolder(View itemView) {
            super(itemView);
            text = (TextView) itemView.findViewById(R.id.text);
            handle = (ImageView) itemView.findViewById(R.id.handle);
        }

        @Override
        public void onItemSelected() {
            //设置item的背景颜色为浅灰色
            itemView.setBackgroundColor(Color.LTGRAY);
        }
    }

我们来完善onSelectedChanged方法

/**当状态改变时回调此方法*/
    @Override
    public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
        //当前状态不是idel(空闲)状态时,说明当前正在拖拽或者侧滑
        if (actionState != ItemTouchHelper.ACTION_STATE_IDLE){
            //看看这个viewHolder是否实现了onStateChangedListener接口
            if (viewHolder instanceof onStateChangedListener){
                onStateChangedListener listener = (onStateChangedListener)viewHolder;
                //回调ItemViewHolder中的onItemSelected方法来改变item的背景颜色
                listener.onItemSelected();
            }
        }
        super.onSelectedChanged(viewHolder, actionState);
    }

运行一下看看效果
这里写图片描述
有点问题,我们发现每个item的背景颜色不会自动变回原来的颜色,所以我们还得再手动改回他的背景颜色,所以我们再在onStateChangedListener接口中添加一个方法,用于当拖拽结束后回调修改item背景颜色

public interface onStateChangedListener {
    void onItemSelected();
    void onItemClear();
}

然后在ItemViewHolder中重写onItemClear方法

  @Override
        public void onItemClear() {
            //恢复item的背景颜色
            itemView.setBackgroundColor(0);
        }

同时,我们还得在SimpleItemTouchHelperCallback中再重写一个clearView方法

 /**当用户拖拽完或者侧滑完一个item时回调此方法,用来清除施加在item上的一些状态*/
    @Override
    public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        super.clearView(recyclerView, viewHolder);
        if (viewHolder instanceof onStateChangedListener){
            onStateChangedListener listener = (onStateChangedListener)viewHolder;
            listener.onItemClear();
        }
    }

我们再来看一下效果
这里写图片描述

3.侧滑删除时item的颜色逐渐变浅

我们希望在侧滑删除一个item的时候有一种颜色逐渐变浅的效果,这个效果我们要借助SimpleItemTouchHelperCallback的onChildDraw方法

/**这个方法可以判断当前是拖拽还是侧滑*/
    @Override
    public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
        if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE){
            //根据侧滑的位移来修改item的透明度
            final float alpha = ALPHA_FULL - Math.abs(dX) / (float) viewHolder.itemView.getWidth();
            viewHolder.itemView.setAlpha(alpha);
            viewHolder.itemView.setTranslationX(dX);
        }
        super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
    }

我们来看一下效果
这里写图片描述

结束语

这个项目我们学习完了,通过这个项目我们真的可以学到很多东西,比如Fragment的使用,RecyclerView的使用,ItemTouchHelper的使用,回调接口的使用等等。一个好的项目值得我们去仔细推敲。

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页