先上效果图:
本篇文章我们来学习一个开源项目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的使用,回调接口的使用等等。一个好的项目值得我们去仔细推敲。
- 7楼ywc951383144昨天 23:31
- 不错不错,强大
- Re: nugongahou11013分钟前
- 回复ywc951383144n嗯嗯 google帮我们实现的功能
- 6楼sinat_16137897昨天 21:32
- 写的真好,赞!!!!!!!!!!!
- 5楼skyyywerq昨天 15:26
- 这个66666
- Re: nugongahou110昨天 20:05
- 回复skyyywerqn66666
- 4楼txfyteen昨天 15:26
- 灯哥过来膜拜下
- Re: nugongahou110昨天 15:26
- 回复txfyteenn谢谢
- 3楼wingichoy昨天 14:59
- 好炫!!!!
- Re: nugongahou110昨天 15:05
- 回复wingichoynwing神来啦
- Re: wingichoy昨天 15:09
- 回复nugongahou110n晚上回家研究你这个! 真的好炫
- 2楼xiaxiazaizai01昨天 14:58
- 6666
- Re: nugongahou110昨天 14:58
- 回复xiaxiazaizai01n666666
- 1楼zhuyb829昨天 14:58
- 赞,好东西,先收藏着