RecyclerView是一個用來替換之前的ListView和GridView的控件,使用的時候叫胖,雖然比以前的ListView看起來麻煩幢痘,但是其實(shí)作為一個高度解耦的控件景鼠,復(fù)雜一點(diǎn)點(diǎn)換來極大的靈活性,豐富的可操作性陈症,何樂而不為呢。不過今天主要說說它的一個輔助類ItemTouchHelper來實(shí)現(xiàn)列表的拖動和滑動刪除。
RecyclerView用法(ListView)
1.導(dǎo)入控件包
compile 'com.android.support:support-v13:25.+'
2.布局文件加入控件
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_test"
android:layout_width="match_parent"
android:layout_height="match_parent"></android.support.v7.widget.RecyclerView>
3.定義Adapter
public class TestAdapter extends RecyclerView.Adapter implements TouchCallbackListener {
/**
* 數(shù)據(jù)源列表
*/
private List<String> mData;
/**
* 構(gòu)造方法傳入數(shù)據(jù)
* @param mData
*/
public TestAdapter(List<String> mData) {
this.mData = mData;
}
/**
* 創(chuàng)建用于復(fù)用的ViewHolder
* @param parent
* @param viewType
* @return
*/
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ViewHolder vh = new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item,parent,false));
return vh;
}
/**
* 對ViewHolder的控件進(jìn)行操作
* @param holder
* @param position
*/
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if(holder instanceof ViewHolder){
ViewHolder holder1 = (ViewHolder) holder;
holder1.tv_test.setText(mData.get(position));
}
}
/**
*
* @return 數(shù)據(jù)的總數(shù)
*/
@Override
public int getItemCount() {
return mData.size();
}
/**
* 長按拖拽時的回調(diào)
* @param fromPosition 拖拽前的位置
* @param toPosition 拖拽后的位置
*/
@Override
public void onItemMove(int fromPosition, int toPosition) {
Collections.swap(mData, fromPosition, toPosition);
notifyItemMoved(fromPosition, toPosition);//通知Adapter更新
}
/**
* 滑動時的回調(diào)
* @param position 滑動的位置
*/
@Override
public void onItemSwipe(int position) {
mData.remove(position);
notifyItemRemoved(position);////通知Adapter更新
}
/**
* 自定義的ViewHolder內(nèi)部類凡纳,必須繼承RecyclerView.ViewHolder(這里用不用static存在爭議,沒有專門的測試喊暖,
* 從內(nèi)存占用來看微乎其微惫企,但是不知道有沒有內(nèi)存泄露的問題)
*/
public class ViewHolder extends RecyclerView.ViewHolder{
private TextView tv_test;
public ViewHolder(View itemView) {
super(itemView);
tv_test = (TextView) itemView.findViewById(R.id.tv_test);
}
}
}
這里定義RecyclerView的Adapter適配器,必須繼承自RecyclerView.Adapter,而且需要在內(nèi)部定義ViewHolder類狞尔,這個跟我們之前使用ListView是一樣的丛版,不過在RecyclerView里面這個是必須實(shí)現(xiàn)的。還有就是這里我并沒有用static偏序,不影響復(fù)用页畦,但是內(nèi)存會不會泄漏呢?
然后里面還有兩個在拖拽和滑動時的回調(diào)研儒,這里是我們自己定義的一個接口TouchCallbackListener
TouchCallbackListener
public interface TouchCallbackListener {
/**
* 長按拖拽時的回調(diào)
* @param fromPosition 拖拽前的位置
* @param toPosition 拖拽后的位置
*/
void onItemMove(int fromPosition, int toPosition);
/**
* 滑動時的回調(diào)
* @param position 滑動的位置
*/
void onItemSwipe(int position);
}
4.使用ItemTouchHelper實(shí)現(xiàn)上下拖拽和滑動刪除功能
ItemTouchHelper的構(gòu)造方法需要傳入ItemTouchHelper.Callback來自己定義各種動作時的處理豫缨,我們自定義的類如下:
TouchCallback
public class TouchCallback extends ItemTouchHelper.Callback {
/**
* 自定義的監(jiān)聽接口
*/
private TouchCallbackListener mListener;
public TouchCallback(TouchCallbackListener listener) {
this.mListener = listener;
}
/**
* 定義列表可以怎么滑動(上下左右)
* @param recyclerView
* @param viewHolder
* @return
*/
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
//上下滑動
int dragFlag = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
//左右滑動
int swipeFlag = ItemTouchHelper.LEFT| ItemTouchHelper.RIGHT;
//使用此方法生成標(biāo)志返回
return makeMovementFlags(dragFlag, swipeFlag);
}
/**
* 拖拽移動時調(diào)用的方法
* @param recyclerView 控件
* @param viewHolder 移動之前的條目
* @param target 移動之后的條目
* @return
*/
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
mListener.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
return true;
}
/**
* 滑動時調(diào)用的方法
* @param viewHolder 滑動的條目
* @param direction 方向
*/
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
mListener.onItemSwipe(viewHolder.getAdapterPosition());
}
/**
* 是否允許長按拖拽
* @return true or false
*/
@Override
public boolean isLongPressDragEnabled() {
return true;
}
/**
* 是否允許滑動
* @return true or false
*/
@Override
public boolean isItemViewSwipeEnabled() {
return true;
}
}
5.使用RecyclerView綁定Adapter和ItemTouchHelper
最后在Activity中來使用RecyclerView
public class MainActivity extends AppCompatActivity{
private RecyclerView mRecyclerView;
private TestAdapter mTestAdapter;
private List<String> mData;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
mRecyclerView = (RecyclerView) findViewById(R.id.rv_test);
mRecyclerView.setAdapter(mTestAdapter);
//定義布局管理器,這里是ListView端朵。GridLayoutManager對應(yīng)GridView
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
//ListView的方向,縱向
linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
mRecyclerView.setLayoutManager(linearLayoutManager);
//添加每一行的分割線
// mRecyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST));
ItemTouchHelper helper = new ItemTouchHelper(new TouchCallback(mTestAdapter));
helper.attachToRecyclerView(mRecyclerView);
}
/**
* 初始化模擬數(shù)據(jù)
*/
private void initData() {
mData = new ArrayList<>();
String temp;
for(int i = 0; i < 99; ++i){
temp = i + "*";
mData.add(temp);
}
mTestAdapter = new TestAdapter(mData);
}
6.添加分割線
RecyclerView默認(rèn)每一行是沒有分割線的好芭,如果需要分割線的話要自己去定義ItemDecoration,這個類可以為每個條目添加額外的視圖與效果冲呢,我們自己定義的代碼如下:
DividerItemDecoration
public class DividerItemDecoration extends RecyclerView.ItemDecoration{
private static final int[] ATTRS = new int[]{
android.R.attr.listDivider//Android默認(rèn)的分割線效果
};
public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
private Drawable mDivider;
private int mOrientation;
public DividerItemDecoration(Context context, int oritation) {
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
a.recycle();
setOrientation(oritation);
}
public void setOrientation(int orientation) {
if(orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST){
throw new IllegalArgumentException("invalid orientation");
}
this.mOrientation = orientation;
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if(mOrientation == VERTICAL_LIST){
drawVertical(c, parent);
}else {
drawHorizontal(c,parent);
}
}
/**
* 縱向的列表
* @param c
* @param parent
*/
public void drawVertical(Canvas c, RecyclerView parent){
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
final int childCount = parent.getChildCount();
for(int i = 0; i < childCount; i++){
final View child = parent.getChildAt(i);
RecyclerView v = new RecyclerView(parent.getContext());
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
final int top = child.getBottom() + params.bottomMargin;
final int bottom = top + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
/**
* 橫向的列表
* @param c
* @param parent
*/
public void drawHorizontal(Canvas c, RecyclerView parent){
final int top = parent.getPaddingTop();
final int bottom = parent.getHeight() - parent.getPaddingBottom();
final int childCount = parent.getChildCount();
for(int i = 0; i < childCount; i++){
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
final int left = child.getRight() + params.rightMargin;
final int right = left + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
if(mOrientation == VERTICAL_LIST){
outRect.set(0,0,0,mDivider.getIntrinsicHeight());
}else {
outRect.set(0,0,mDivider.getIntrinsicWidth(), 0);
}
}
}
到此就實(shí)現(xiàn)了一個支持長按拖拽和滑動刪除的列表舍败,很簡單,效果就不截圖了敬拓。
ItemTouchHelper原理
實(shí)現(xiàn)拖拽和滑動刪除的過程的很簡單邻薯,并且還有非常流暢的動畫。只需要給ItemTouchHelper傳入一個我們自己定義的回調(diào)即可乘凸,但是它的內(nèi)部是怎么實(shí)現(xiàn)的呢厕诡?來一步一步看看代碼。
首先看看它的類定義:
public class ItemTouchHelper extends RecyclerView.ItemDecoration
implements RecyclerView.OnChildAttachStateChangeListener
繼承自RecyclerView.ItemDecoration营勤,跟分割線一樣灵嫌,也是通過繼承這個類來給每個條目添加效果
然后從它的在外層的使用開始:
ItemTouchHelper helper = new ItemTouchHelper(new TouchCallback(mTestAdapter));
helper.attachToRecyclerView(mRecyclerView);
RecyclerView和ItemTouchHelper的關(guān)聯(lián)是ItemTouchHelper的attachToRecyclerView方法,進(jìn)入這個方法:
ItemTouchHelper.attachToRecyclerView
public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
if (mRecyclerView == recyclerView) {
return; // nothing to do
}
if (mRecyclerView != null) {
destroyCallbacks();
}
mRecyclerView = recyclerView;
if (mRecyclerView != null) {
final Resources resources = recyclerView.getResources();
mSwipeEscapeVelocity = resources
.getDimension(R.dimen.item_touch_helper_swipe_escape_velocity);
mMaxSwipeVelocity = resources
.getDimension(R.dimen.item_touch_helper_swipe_escape_max_velocity);
setupCallbacks();
}
}
首先判斷傳入的RecyclerView是否跟已經(jīng)綁定的相等冀偶,如果相等醒第,就直接返回,不過不相等进鸠,銷毀之前的回調(diào)稠曼,然后將傳入的RecyclerView賦值給全局變量,設(shè)置速率客年,最后調(diào)用setupCallbacks初始化
ItemTouchHelper.setupCallbacks
private void setupCallbacks() {
ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
mSlop = vc.getScaledTouchSlop();
mRecyclerView.addItemDecoration(this);
mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);
mRecyclerView.addOnChildAttachStateChangeListener(this);
initGestureDetector();
}
前兩句是獲取TouchSlop的值霞幅,這個值用于判斷是滑動還是點(diǎn)擊,然后給RecyclerView添加ItemDecoration(也就是自己)量瓜,條目的觸摸監(jiān)聽司恳,條目的關(guān)聯(lián)狀態(tài)監(jiān)聽。這里最主要的就是看看mOnItemTouchListener的實(shí)現(xiàn):
ItemTouchHelper.mOnItemTouchListener
private final OnItemTouchListener mOnItemTouchListener
= new OnItemTouchListener() {
@Override
public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) {
mGestureDetector.onTouchEvent(event);
if (DEBUG) {
Log.d(TAG, "intercept: x:" + event.getX() + ",y:" + event.getY() + ", " + event);
}
//用于處理多點(diǎn)觸控
final int action = MotionEventCompat.getActionMasked(event);
if (action == MotionEvent.ACTION_DOWN) {
mActivePointerId = event.getPointerId(0);
mInitialTouchX = event.getX();
mInitialTouchY = event.getY();
obtainVelocityTracker();
if (mSelected == null) {
final RecoverAnimation animation = findAnimation(event);
if (animation != null) {
mInitialTouchX -= animation.mX;
mInitialTouchY -= animation.mY;
endRecoverAnimation(animation.mViewHolder, true);
if (mPendingCleanup.remove(animation.mViewHolder.itemView)) {
mCallback.clearView(mRecyclerView, animation.mViewHolder);
}
select(animation.mViewHolder, animation.mActionState);
updateDxDy(event, mSelectedFlags, 0);
}
}
} else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mActivePointerId = ACTIVE_POINTER_ID_NONE;
select(null, ACTION_STATE_IDLE);
} else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) {
// in a non scroll orientation, if distance change is above threshold, we
// can select the item
final int index = event.findPointerIndex(mActivePointerId);
if (DEBUG) {
Log.d(TAG, "pointer index " + index);
}
if (index >= 0) {
checkSelectForSwipe(action, event, index);
}
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(event);
}
return mSelected != null;
}
@Override
public void onTouchEvent(RecyclerView recyclerView, MotionEvent event) {
mGestureDetector.onTouchEvent(event);
if (DEBUG) {
Log.d(TAG,
"on touch: x:" + mInitialTouchX + ",y:" + mInitialTouchY + ", :" + event);
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(event);
}
if (mActivePointerId == ACTIVE_POINTER_ID_NONE) {
return;
}
final int action = MotionEventCompat.getActionMasked(event);
final int activePointerIndex = event.findPointerIndex(mActivePointerId);
if (activePointerIndex >= 0) {
checkSelectForSwipe(action, event, activePointerIndex);
}
ViewHolder viewHolder = mSelected;
if (viewHolder == null) {
return;
}
switch (action) {
case MotionEvent.ACTION_MOVE: {
// Find the index of the active pointer and fetch its position
if (activePointerIndex >= 0) {
updateDxDy(event, mSelectedFlags, activePointerIndex);
moveIfNecessary(viewHolder);
mRecyclerView.removeCallbacks(mScrollRunnable);
mScrollRunnable.run();
mRecyclerView.invalidate();
}
break;
}
case MotionEvent.ACTION_CANCEL:
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
// fall through
case MotionEvent.ACTION_UP:
select(null, ACTION_STATE_IDLE);
mActivePointerId = ACTIVE_POINTER_ID_NONE;
break;
case MotionEvent.ACTION_POINTER_UP: {
final int pointerIndex = MotionEventCompat.getActionIndex(event);
final int pointerId = event.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mActivePointerId = event.getPointerId(newPointerIndex);
updateDxDy(event, mSelectedFlags, pointerIndex);
}
break;
}
}
}
這里主要重寫了兩個方法onInterceptTouchEvent和onTouchEvent绍傲,先來看看onInterceptTouchEvent扔傅,攔截屏幕事觸控的事件耍共,首先是判斷單點(diǎn)按下
if (action == MotionEvent.ACTION_DOWN) {
//現(xiàn)在追蹤的觸摸事件
mActivePointerId = event.getPointerId(0);
//獲取最開始按下的坐標(biāo)值
mInitialTouchX = event.getX();
mInitialTouchY = event.getY();
//獲取速度追蹤器(此方法避免重復(fù)創(chuàng)建)
obtainVelocityTracker();
//如果選擇的條目為空
if (mSelected == null) {
//查找對應(yīng)的動畫(避免重復(fù)動畫)
final RecoverAnimation animation = findAnimation(event);
//執(zhí)行動畫,
if (animation != null) {
//更新初始值
mInitialTouchX -= animation.mX;
mInitialTouchY -= animation.mY;
//從動畫列表里移除條目對應(yīng)的動畫
endRecoverAnimation(animation.mViewHolder, true);
//從回收列表里移除條目視圖
if (mPendingCleanup.remove(animation.mViewHolder.itemView)) {
mCallback.clearView(mRecyclerView, animation.mViewHolder);
}
//執(zhí)行選擇動畫
select(animation.mViewHolder, animation.mActionState);
//更新移動距離x猎塞,y的值
updateDxDy(event, mSelectedFlags, 0);
}
}
}
然后是判斷取消和單點(diǎn)抬起:
else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mActivePointerId = ACTIVE_POINTER_ID_NONE;
select(null, ACTION_STATE_IDLE);//清除動畫
最后執(zhí)行下面判斷點(diǎn)擊狀態(tài)為空:
else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) {
// 移動距離超過了臨界值试读,判斷是否滑動選擇的條目
final int index = event.findPointerIndex(mActivePointerId);
if (DEBUG) {
Log.d(TAG, "pointer index " + index);
}
if (index >= 0) {
//判斷是否滑選擇的條目
checkSelectForSwipe(action, event, index);
}
}
最后如果選擇的條目不等于null,返回true荠耽,表示攔截觸摸事件钩骇,接下來執(zhí)行onTouchEvent方法,只看對觸摸動作的判斷:
1.按下移動手指:
case MotionEvent.ACTION_MOVE: {
// 如果點(diǎn)擊序號大于0铝量,表示有點(diǎn)擊事件
if (activePointerIndex >= 0) {
//更新移動距離
updateDxDy(event, mSelectedFlags, activePointerIndex);
//移動ViewHolder
moveIfNecessary(viewHolder);
//先移除動畫
mRecyclerView.removeCallbacks(mScrollRunnable);
//執(zhí)行動畫
mScrollRunnable.run();
//重繪RecyclerView
mRecyclerView.invalidate();
}
break;
}
這里來看看mScrollRunnable.run():
final Runnable mScrollRunnable = new Runnable() {
@Override
public void run() {
if (mSelected != null && scrollIfNecessary()) {
if (mSelected != null) { //it might be lost during scrolling
moveIfNecessary(mSelected);
}
mRecyclerView.removeCallbacks(mScrollRunnable);
//遞歸調(diào)用
ViewCompat.postOnAnimation(mRecyclerView, this);
}
}
};
這里的run方法相當(dāng)于是一個死循環(huán)倘屹,在里面又不斷調(diào)用自己,不斷的執(zhí)行動畫慢叨,因?yàn)檫x中的條目需要不停的跟隨手指的移動纽匙,直到判斷條件返回FALSE停止執(zhí)行,然后回到onTouchEvent繼續(xù)判斷
2.當(dāng)用戶保持按下操作拍谐,并從你的控件轉(zhuǎn)移到外層控件時哄辣,會觸發(fā)ACTION_CANCEL:
case MotionEvent.ACTION_CANCEL:
if (mVelocityTracker != null) {
//清除速度追蹤器
mVelocityTracker.clear();
}
3.抬起手指
case MotionEvent.ACTION_UP:
//清理選擇動畫
select(null, ACTION_STATE_IDLE);
//手指狀態(tài)置空
mActivePointerId = ACTIVE_POINTER_ID_NONE;
break;
4.多點(diǎn)觸控抬起
case MotionEvent.ACTION_POINTER_UP: {
final int pointerIndex = MotionEventCompat.getActionIndex(event);
final int pointerId = event.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
//選擇一個新的手指活動點(diǎn),并且更新x赠尾,y的距離
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mActivePointerId = event.getPointerId(newPointerIndex);
updateDxDy(event, mSelectedFlags, pointerIndex);
}
break;
}
根據(jù)對OnItemTouchListener的源碼分析,我們知道了跟隨手指的動畫是怎么來實(shí)現(xiàn)的毅弧,簡單來說气嫁,就是檢測手指的動作,然后不斷的重繪够坐,最終就展現(xiàn)在我們面前寸宵,在長按上下拖拽時,按住的條目隨著手指移動元咙,左右滑動時梯影,條目“飛”出屏幕。不過在實(shí)際的項(xiàng)目中庶香,這種側(cè)滑刪除的操作肯定不是直接側(cè)滑就執(zhí)行刪除甲棍,需要右邊有一個刪除的按鈕來確認(rèn),這個也可以在ItemTouchHelper的基礎(chǔ)上來改進(jìn)赶掖,后面再說吧感猛。