前言
現(xiàn)在RecyclerView的應(yīng)用越來(lái)越廣泛了,不同的應(yīng)用場(chǎng)景需要其作出不同的改變。有時(shí)候我們可能需要實(shí)現(xiàn)側(cè)滑刪除的功能函似,比如知乎首頁(yè)的側(cè)滑刪除阴绢,又或者長(zhǎng)按Item進(jìn)行拖動(dòng)與其他Item進(jìn)行位置的交換店乐,但RecyclerView沒(méi)有提供現(xiàn)成的API供我們操作,所幸SDK提供了ItemTouchHelper這樣一個(gè)工具類(lèi)幫助我們快速實(shí)現(xiàn)以上功能呻袭。不多說(shuō)別的眨八,我們來(lái)介紹一下ItemTouchHelper。
什么是ItemTouchHelper
This is a utility class to add swipe to dismiss and drag & drop support to RecyclerView.It works with a RecyclerView and a Callback class, which configures what type of interactions are enabled and also receives events when user performs these actions.Depending on which functionality you support, you should override onMove(RecyclerView, ViewHolder, ViewHolder) and / or onSwiped(ViewHolder, int).
以上是官方文檔的介紹左电,ItemTouchHelper是一個(gè)工具類(lèi)廉侧,可實(shí)現(xiàn)側(cè)滑刪除和拖拽移動(dòng),使用這個(gè)工具類(lèi)需要RecyclerView和Callback篓足。同時(shí)根據(jù)需要重寫(xiě)onMove和onSwiped方法段誊。接下來(lái)就來(lái)講述ItemTouchHelper的使用方法。
ItemTouchHelper基本使用方法
step.1新建一個(gè)接口栈拖,讓Adapter實(shí)現(xiàn)之
從解耦的角度考慮连舍,我們需要一個(gè)接口來(lái)實(shí)現(xiàn)Adapter和ItemTouchHelper之間涉及數(shù)據(jù)的操作,因?yàn)镮temTouchHelper在完成觸摸的各種動(dòng)畫(huà)后涩哟,就要對(duì)Adapter的數(shù)據(jù)進(jìn)行操作索赏,比如側(cè)滑刪除操作,最后需要調(diào)用Adapter的notifyItemRemove()方法來(lái)移除該數(shù)據(jù)贴彼。因此我們可以把數(shù)據(jù)操作的部分抽象成一個(gè)接口方法潜腻,讓ItemTouchHelper.Callback調(diào)用該方法即可。具體如下:
新建ItemTouchHelperAdapter:
public interface ItemTouchHelperAdapter {
//數(shù)據(jù)交換
void onItemMove(int fromPosition,int toPosition);
//數(shù)據(jù)刪除
void onItemDissmiss(int position);
}
讓我們的Adapter實(shí)現(xiàn)該接口:
public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements ItemTouchHelperAdapter {
//數(shù)據(jù)
private List<String> mData;
...
@Override
public void onItemMove(int fromPosition, int toPosition) {
//交換位置
Collections.swap(mData,fromPosition,toPosition);
notifyItemMoved(fromPosition,toPosition);
}
@Override
public void onItemDissmiss(int position) {
//移除數(shù)據(jù)
mData.remove(position);
notifyItemRemoved(position);
}
}
那么我們?cè)贗temTouchHelper.Callback內(nèi)直接調(diào)用接口的方法即可器仗。
step.2新建類(lèi)繼承自ItemTouchHelper.Callback
從官方文檔我們知道融涣,使用ItemTouchHelper需要一個(gè)Callback,該Callback是ItemTouchHelper.Callback的子類(lèi)精钮,所以我們需要新建一個(gè)類(lèi)比如SimpleItemTouchHelperCallback繼承自ItemTouchHelper.Callback威鹿。我們可以重寫(xiě)其數(shù)個(gè)方法來(lái)實(shí)現(xiàn)我們的需求。我們先來(lái)看看ItemTouchHelper.Callback需要重寫(xiě)的幾個(gè)常用的方法轨香。
1专普、public int getMovementFlags(RecyclerView, RecyclerView.ViewHolder):該方法用于返回可以滑動(dòng)的方向,比如說(shuō)允許從右到左側(cè)滑弹沽,允許上下拖動(dòng)等。我們一般使用makeMovementFlags(int,int)或makeFlag(int, int)來(lái)構(gòu)造我們的返回值筋粗。
例如:要使RecyclerView的Item可以上下拖動(dòng)策橘,同時(shí)允許從右到左側(cè)滑,但不許允許從左到右的側(cè)滑娜亿,我們可以這樣寫(xiě):
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN; //允許上下的拖動(dòng)
int swipeFlags = ItemTouchHelper.LEFT; //只允許從右向左側(cè)滑
return makeMovementFlags(dragFlags,swipeFlags);
}
2丽已、public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target)
當(dāng)用戶拖動(dòng)一個(gè)Item進(jìn)行上下移動(dòng)從舊的位置到新的位置的時(shí)候會(huì)調(diào)用該方法,在該方法內(nèi)买决,我們可以調(diào)用Adapter的notifyItemMoved方法來(lái)交換兩個(gè)ViewHolder的位置沛婴,最后返回true吼畏,表示被拖動(dòng)的ViewHolder已經(jīng)移動(dòng)到了目的位置。所以嘁灯,如果要實(shí)現(xiàn)拖動(dòng)交換位置泻蚊,可以重寫(xiě)該方法(前提是支持上下拖動(dòng)):
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
//onItemMove是接口方法
mAdapter.onItemMove(viewHolder.getAdapterPosition(),target.getAdapterPosition());
return true;
}
3、public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction)
當(dāng)用戶左右滑動(dòng)Item達(dá)到刪除條件時(shí)丑婿,會(huì)調(diào)用該方法性雄,一般手指觸摸滑動(dòng)的距離達(dá)到RecyclerView寬度的一半時(shí),再松開(kāi)手指羹奉,此時(shí)該Item會(huì)繼續(xù)向原先滑動(dòng)方向滑過(guò)去并且調(diào)用onSwiped方法進(jìn)行刪除秒旋,否則會(huì)反向滑回原來(lái)的位置。在該方法內(nèi)部我們可以這樣寫(xiě):
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
//onItemDissmiss是接口方法
mAdapter.onItemDissmiss(viewHolder.getAdapterPosition());
}
如果在onSwiped方法內(nèi)我們沒(méi)有進(jìn)行任何操作诀拭,即不刪除已經(jīng)滑過(guò)去的Item迁筛,那么就會(huì)留下空白的地方,因?yàn)閷?shí)際上該ItemView還占據(jù)著該位置耕挨,只是移出了我們的可視范圍內(nèi)罷了细卧。
4、public boolean isLongPressDragEnabled():該方法返回true時(shí)俗孝,表示支持長(zhǎng)按拖動(dòng)酒甸,即長(zhǎng)按ItemView后才可以拖動(dòng),我們遇到的場(chǎng)景一般也是這樣的赋铝。默認(rèn)是返回true插勤。
5、public boolean boolean isItemViewSwipeEnabled():該方法返回true時(shí)革骨,表示如果用戶觸摸并左右滑動(dòng)了View农尖,那么可以執(zhí)行滑動(dòng)刪除操作,即可以調(diào)用到onSwiped()方法良哲。默認(rèn)是返回true盛卡。
6、public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState):從靜止?fàn)顟B(tài)變?yōu)橥献Щ蛘呋瑒?dòng)的時(shí)候會(huì)回調(diào)該方法筑凫,參數(shù)actionState表示當(dāng)前的狀態(tài)滑沧。
7、public void clearView(RecyclerView recyclerView, ViewHolder viewHolder):當(dāng)用戶操作完畢某個(gè)item并且其動(dòng)畫(huà)也結(jié)束后會(huì)調(diào)用該方法巍实,一般我們?cè)谠摲椒▋?nèi)恢復(fù)ItemView的初始狀態(tài)滓技,防止由于復(fù)用而產(chǎn)生的顯示錯(cuò)亂問(wèn)題。
8棚潦、public void onChildDraw(...):我們可以在這個(gè)方法內(nèi)實(shí)現(xiàn)我們自定義的交互規(guī)則或者自定義的動(dòng)畫(huà)效果令漂。
那么完整的SimpleItemTouchHelperCallback文件是這樣的:
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback{
private ItemTouchHelperAdapter mAdapter;
public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter){
mAdapter = adapter;
}
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
int swipeFlags = ItemTouchHelper.LEFT;
return makeMovementFlags(dragFlags,swipeFlags);
}
@Override
public boolean isLongPressDragEnabled() {
return true;
}
@Override
public boolean isItemViewSwipeEnabled() {
return true;
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
mAdapter.onItemMove(viewHolder.getAdapterPosition(),target.getAdapterPosition());
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
mAdapter.onItemDissmiss(viewHolder.getAdapterPosition());
}
}
step.3為RecycleView添加ItemTouchHelper
上面我們修改了Adapter和新建了ItemTouchHelper.Callback的子類(lèi),接下來(lái)我們要為RecyclerView添加ItemTouchHelper:
//先實(shí)例化Callback
ItemTouchHelper.Callback callback = new SimpleItemTouchHelperCallback(myAdapter);
//用Callback構(gòu)造ItemtouchHelper
ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
//調(diào)用ItemTouchHelper的attachToRecyclerView方法建立聯(lián)系
touchHelper.attachToRecyclerView(mRecyclerView);
經(jīng)過(guò)以上步驟,我們已經(jīng)實(shí)現(xiàn)了Item的拖拽和側(cè)滑刪除功能了叠必,看一下效果:
自定義側(cè)滑動(dòng)畫(huà)
有時(shí)候我們對(duì)默認(rèn)的動(dòng)畫(huà)效果可能不滿意荚孵,需要自己實(shí)現(xiàn)想要的動(dòng)畫(huà)效果,ItemTouchHelper.Callback提供的onChildDraw方法可以讓我們很方便地實(shí)現(xiàn)想要的效果纬朝。以下帶來(lái)一種自定義的實(shí)現(xiàn)效果收叶,當(dāng)做拋磚引玉,讓大家熟悉自定義效果的運(yùn)用玄组。先來(lái)看看要實(shí)現(xiàn)的效果:
該效果是比較常見(jiàn)的滔驾,用戶向左滑動(dòng)Item的時(shí)候,一開(kāi)始提示的是“左滑刪除”俄讹,滑動(dòng)到一定距離后哆致,顯示刪除的圖標(biāo),并且隨著滑動(dòng)距離的增加該圖標(biāo)不斷變大患膛,達(dá)到最大后用戶松開(kāi)手指摊阀,該Item被刪除。
接下來(lái)我們來(lái)分析一下怎樣實(shí)現(xiàn)以上的效果:
首先踪蹬,要想左滑出現(xiàn)一個(gè)刪除的方塊胞此,可以在LinearLayout放一個(gè)這樣的“方塊”,讓它與Item水平并排排列跃捣,以下是布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:orientation="horizontal">
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="80dp"
android:background="#ffffff"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_marginBottom="4dp"
app:cardCornerRadius="1dp"
app:elevation="1dp"
app:contentPadding="1dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff">
<TextView
android:id="@+id/item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="22sp"
android:padding="4dp"
android:layout_centerInParent="true"/>
</RelativeLayout>
</android.support.v7.widget.CardView>
<FrameLayout
android:layout_width="100dp"
android:layout_height="match_parent"
android:layout_marginRight="4dp"
android:layout_marginBottom="4dp"
android:background="#f33213">
<ImageView
android:id="@+id/iv_img"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center"
android:src="@mipmap/ic_eye_72"
android:visibility="invisible"/>
<TextView
android:id="@+id/tv_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="左滑刪除"
android:textSize="18sp"
android:textColor="#ffffff"
android:layout_gravity="center"/>
</FrameLayout>
</LinearLayout>
布局文件修改后漱牵,我們嘗試來(lái)滑動(dòng)一下,發(fā)現(xiàn)后面的刪除方塊并不會(huì)出現(xiàn)疚漆,這是因?yàn)槟J(rèn)的滑動(dòng)方式是setTranslationX(int)酣胀,即是對(duì)整個(gè)View的滑動(dòng),所以無(wú)論我們?cè)鯓踊瑒?dòng)娶聘,都不會(huì)出現(xiàn)刪除方塊闻镶。因此,我們要改變一個(gè)種滑動(dòng)方式丸升,比如使用scrollTo(int,int)铆农,這種是對(duì)View的內(nèi)容的滑動(dòng),所以隨著左滑狡耻,item會(huì)向左滑去墩剖,而位于右方的方塊自然也就出現(xiàn)了。
接著夷狰,我們考慮該“刪除眼睛”的圖標(biāo)是怎樣從小變大的涛碑,這個(gè)實(shí)現(xiàn)也比較簡(jiǎn)單,只要根據(jù)滑動(dòng)的距離對(duì)該ImageView的LayoutParams.width進(jìn)行改變就行了孵淘,不過(guò)要注意限制大小,否則過(guò)大會(huì)造成圖片的失真歹篓。當(dāng)滑動(dòng)距離等于RecyclerView寬度的一半時(shí)瘫证,此時(shí)松開(kāi)手會(huì)使Item刪除揉阎,那么我們可以在該滑動(dòng)距離達(dá)到該值時(shí)時(shí)“眼睛”變得最大,此時(shí)可以達(dá)到良好的交互效果背捌,提示了用戶無(wú)需繼續(xù)滑動(dòng)即可刪除該Item了毙籽。
最后我們要考慮的是:在刪除了Item或者沒(méi)刪除而滑回原來(lái)的位置后,我們要把所做的改變重置一下毡庆,否則坑赡,會(huì)由于RecyclerView的復(fù)用而導(dǎo)致其他位置的ViewHolder與當(dāng)前的ViewHolder所做的改變一樣,即造成顯示的錯(cuò)誤么抗。我們可以在clearView()方法內(nèi)重置改變毅否,這樣就能解決因復(fù)用而導(dǎo)致的顯示問(wèn)題了。
最后我們來(lái)看看SimpleItemTouchHelperCallback的代碼:
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback{
//省略上面的代碼....
//限制ImageView長(zhǎng)度所能增加的最大值
private double ICON_MAX_SIZE = 50;
//ImageView的初始長(zhǎng)寬
private int fixedWidth = 150;
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
//重置改變蝇刀,防止由于復(fù)用而導(dǎo)致的顯示問(wèn)題
viewHolder.itemView.setScrollX(0);
((MyAdapter.NormalItem)viewHolder).tv.setText("左滑刪除");
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) ((MyAdapter.NormalItem) viewHolder).iv.getLayoutParams();
params.width = 150;
params.height = 150;
((MyAdapter.NormalItem) viewHolder).iv.setLayoutParams(params);
((MyAdapter.NormalItem) viewHolder).iv.setVisibility(View.INVISIBLE);
}
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
//僅對(duì)側(cè)滑狀態(tài)下的效果做出改變
if (actionState ==ItemTouchHelper.ACTION_STATE_SWIPE){
//如果dX小于等于刪除方塊的寬度螟加,那么我們把該方塊滑出來(lái)
if (Math.abs(dX) <= getSlideLimitation(viewHolder)){
viewHolder.itemView.scrollTo(-(int) dX,0);
}
//如果dX還未達(dá)到能刪除的距離,此時(shí)慢慢增加“眼睛”的大小吞琐,增加的最大值為ICON_MAX_SIZE
else if (Math.abs(dX) <= recyclerView.getWidth() / 2){
double distance = (recyclerView.getWidth() / 2 -getSlideLimitation(viewHolder));
double factor = ICON_MAX_SIZE / distance;
double diff = (Math.abs(dX) - getSlideLimitation(viewHolder)) * factor;
if (diff >= ICON_MAX_SIZE)
diff = ICON_MAX_SIZE;
((MyAdapter.NormalItem)viewHolder).tv.setText(""); //把文字去掉
((MyAdapter.NormalItem) viewHolder).iv.setVisibility(View.VISIBLE); //顯示眼睛
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) ((MyAdapter.NormalItem) viewHolder).iv.getLayoutParams();
params.width = (int) (fixWidth + diff);
params.height = (int) (fixWidth + diff);
((MyAdapter.NormalItem) viewHolder).iv.setLayoutParams(params);
}
}else {
//拖拽狀態(tài)下不做改變捆探,需要調(diào)用父類(lèi)的方法
super.onChildDraw(c,recyclerView,viewHolder,dX,dY,actionState,isCurrentlyActive);
}
}
/**
* 獲取刪除方塊的寬度
*/
public int getSlideLimitation(RecyclerView.ViewHolder viewHolder){
ViewGroup viewGroup = (ViewGroup) viewHolder.itemView;
return viewGroup.getChildAt(1).getLayoutParams().width;
}
}
好了,到目前為止站粟,自定義效果介紹完畢黍图,讀者可以根據(jù)需求來(lái)實(shí)現(xiàn)多樣化的效果。最后奴烙,感謝你的閱讀助被,如有錯(cuò)誤歡迎指出~
參考文章
RecyclerView的拖動(dòng)和滑動(dòng) 第一部分 :基本的ItemTouchHelper示例