Android一步一步帶你實現RecyclerView的拖拽和側滑刪除功能
先上效果圖:
本篇文章我們來學習一個開源專案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的使用,回撥介面的使用等等。一個好的專案值得我們去仔細推敲。