從原始碼來看ItemTouchHelper實現RecyclerView列表的拖拽和側滑
RecyclerView是一個用來替換之前的ListView和GridView的控制元件,使用的時候,雖然比以前的ListView看起來麻煩,但是其實作為一個高度解耦的控制元件,複雜一點點換來極大的靈活性,豐富的可操作性,何樂而不為呢。不過今天主要說說它的一個輔助類ItemTouchHelper來實現列表的拖動和滑動刪除。
RecyclerView用法(ListView)
1.匯入控制元件包
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 {
/**
* 資料來源列表
*/
private List<String> mData;
/**
* 構造方法傳入資料
* @param mData
*/
public TestAdapter(List<String> mData) {
this.mData = mData;
}
/**
* 建立用於複用的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的控制元件進行操作
* @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 資料的總數
*/
@Override
public int getItemCount() {
return mData.size();
}
/**
* 長按拖拽時的回撥
* @param fromPosition 拖拽前的位置
* @param toPosition 拖拽後的位置
*/
@Override
public void onItemMove(int fromPosition, int toPosition) {
Collections.swap(mData, fromPosition, toPosition);
notifyItemMoved(fromPosition, toPosition);//通知Adapter更新
}
/**
* 滑動時的回撥
* @param position 滑動的位置
*/
@Override
public void onItemSwipe(int position) {
mData.remove(position);
notifyItemRemoved(position);////通知Adapter更新
}
/**
* 自定義的ViewHolder內部類,必須繼承RecyclerView.ViewHolder(這裡用不用static存在爭議,沒有專門的測試,
* 從記憶體佔用來看微乎其微,但是不知道有沒有記憶體洩露的問題)
*/
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,而且需要在內部定義ViewHolder類,這個跟我們之前使用ListView是一樣的,不過在RecyclerView裡面這個是必須實現的。還有就是這裡我並沒有用static,不影響複用,但是記憶體會不會洩漏呢?
然後裡面還有兩個在拖拽和滑動時的回撥,這裡是我們自己定義的一個介面TouchCallbackListener
TouchCallbackListener
public interface TouchCallbackListener {
/**
* 長按拖拽時的回撥
* @param fromPosition 拖拽前的位置
* @param toPosition 拖拽後的位置
*/
void onItemMove(int fromPosition, int toPosition);
/**
* 滑動時的回撥
* @param position 滑動的位置
*/
void onItemSwipe(int position);
}
4.使用ItemTouchHelper實現上下拖拽和滑動刪除功能
ItemTouchHelper的構造方法需要傳入ItemTouchHelper.Callback來自己定義各種動作時的處理,我們自定義的類如下:
TouchCallback
public class TouchCallback extends ItemTouchHelper.Callback {
/**
* 自定義的監聽介面
*/
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;
//使用此方法生成標誌返回
return makeMovementFlags(dragFlag, swipeFlag);
}
/**
* 拖拽移動時呼叫的方法
* @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;
}
/**
* 滑動時呼叫的方法
* @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對應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);
}
/**
* 初始化模擬資料
*/
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預設每一行是沒有分割線的,如果需要分割線的話要自己去定義ItemDecoration,這個類可以為每個條目新增額外的檢視與效果,我們自己定義的程式碼如下:
DividerItemDecoration
public class DividerItemDecoration extends RecyclerView.ItemDecoration{
private static final int[] ATTRS = new int[]{
android.R.attr.listDivider//Android預設的分割線效果
};
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);
}
}
}
到此就實現了一個支援長按拖拽和滑動刪除的列表,很簡單,效果就不截圖了。
ItemTouchHelper原理
實現拖拽和滑動刪除的過程的很簡單,並且還有非常流暢的動畫。只需要給ItemTouchHelper傳入一個我們自己定義的回撥即可,但是它的內部是怎麼實現的呢?來一步一步看看程式碼。
首先看看它的類定義:
public class ItemTouchHelper extends RecyclerView.ItemDecoration
implements RecyclerView.OnChildAttachStateChangeListener
繼承自RecyclerView.ItemDecoration,跟分割線一樣,也是通過繼承這個類來給每個條目新增效果
然後從它的在外層的使用開始:
ItemTouchHelper helper = new ItemTouchHelper(new TouchCallback(mTestAdapter));
helper.attachToRecyclerView(mRecyclerView);
RecyclerView和ItemTouchHelper的關聯是ItemTouchHelper的attachToRecyclerView方法,進入這個方法:
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是否跟已經繫結的相等,如果相等,就直接返回,不過不相等,銷燬之前的回撥,然後將傳入的RecyclerView賦值給全域性變數,設定速率,最後呼叫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的值,這個值用於判斷是滑動還是點選,然後給RecyclerView新增ItemDecoration(也就是自己),條目的觸控監聽,條目的關聯狀態監聽。這裡最主要的就是看看mOnItemTouchListener的實現:
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);
}
//用於處理多點觸控
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,攔截螢幕事觸控的事件,首先是判斷單點按下
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);
//更新移動距離x,y的值
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) {
// 移動距離超過了臨界值,判斷是否滑動選擇的條目
final int index = event.findPointerIndex(mActivePointerId);
if (DEBUG) {
Log.d(TAG, "pointer index " + index);
}
if (index >= 0) {
//判斷是否滑選擇的條目
checkSelectForSwipe(action, event, index);
}
}
最後如果選擇的條目不等於null,返回true,表示攔截觸控事件,接下來執行onTouchEvent方法,只看對觸控動作的判斷:
1.按下移動手指:
case MotionEvent.ACTION_MOVE: {
// 如果點選序號大於0,表示有點選事件
if (activePointerIndex >= 0) {
//更新移動距離
updateDxDy(event, mSelectedFlags, activePointerIndex);
//移動ViewHolder
moveIfNecessary(viewHolder);
//先移除動畫
mRecyclerView.removeCallbacks(mScrollRunnable);
//執行動畫
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);
//遞迴呼叫
ViewCompat.postOnAnimation(mRecyclerView, this);
}
}
};
這裡的run方法相當於是一個死迴圈,在裡面又不斷呼叫自己,不斷的執行動畫,因為選中的條目需要不停的跟隨手指的移動,直到判斷條件返回FALSE停止執行,然後回到onTouchEvent繼續判斷
2.當使用者保持按下操作,並從你的控制元件轉移到外層控制元件時,會觸發ACTION_CANCEL:
case MotionEvent.ACTION_CANCEL:
if (mVelocityTracker != null) {
//清除速度追蹤器
mVelocityTracker.clear();
}
3.抬起手指
case MotionEvent.ACTION_UP:
//清理選擇動畫
select(null, ACTION_STATE_IDLE);
//手指狀態置空
mActivePointerId = ACTIVE_POINTER_ID_NONE;
break;
4.多點觸控抬起
case MotionEvent.ACTION_POINTER_UP: {
final int pointerIndex = MotionEventCompat.getActionIndex(event);
final int pointerId = event.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
//選擇一個新的手指活動點,並且更新x,y的距離
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mActivePointerId = event.getPointerId(newPointerIndex);
updateDxDy(event, mSelectedFlags, pointerIndex);
}
break;
}
根據對OnItemTouchListener的原始碼分析,我們知道了跟隨手指的動畫是怎麼來實現的,簡單來說,就是檢測手指的動作,然後不斷的重繪,最終就展現在我們面前,在長按上下拖拽時,按住的條目隨著手指移動,左右滑動時,條目“飛”出螢幕。不過在實際的專案中,這種側滑刪除的操作肯定不是直接側滑就執行刪除,需要右邊有一個刪除的按鈕來確認,這個也可以在ItemTouchHelper的基礎上來改進,後面再說吧。
相關文章
- ItemTouchHelper實現可拖拽和側滑的列表
- RecyclerView 實現滑動刪除和拖拽功能View
- RecyclerView實現滑動刪除和拖拽功能View
- 實現 UITableViewCell 側滑操作列表UIView
- 自定義RecyclerView實現側滑刪除View
- (有圖)仿QQ側滑選單:RecyclerView側滑選單,長按拖拽,滑動刪除View
- RecyclerView 之使用 ItemTouchHelper 實現互動動畫View動畫
- 直播原始碼,實現內容列表的豎向滑動原始碼
- Activity側滑返回的實現原理
- RecyclerView 知識梳理(5) ItemTouchHelperView
- 從vue原始碼來看Proxy的用途Vue原始碼
- 從原始碼角度看蘋果是如何實現 alloc、new、copy 和 mutablecopy 的原始碼蘋果
- RecyclerView進階(一)RecyclerView實現雙列表聯動View
- Android側滑返回分析和實現(不高仿微信)Android
- android的左右側滑選單實現Android
- Flutter 仿iOS側滑返回案例實現FlutteriOS
- 自定義View:側滑選單實現View
- Android使用RecyclerView實現二級列表AndroidView
- MaterialDesgin系列文章(二)NavigationView和DrawerLayout實現側滑功能NavigationView
- 自定義View:側滑選單動畫實現View動畫
- 短視訊軟體開發,RecyclerView實現拖拽效果View
- Flutter 側滑欄及城市選擇UI的實現FlutterUI
- 直播平臺原始碼,迴圈滾動RecyclerView的實現原始碼View
- RecyclerView用法和原始碼深度解析View原始碼
- MaterialDesign系列文章(二)NavigationView和DrawerLayout實現側滑功能NavigationView
- css3實現側邊滑動選單CSSS3
- 自定義ViewGroup,實現Android的側滑選單ViewAndroid
- 從原始碼看Spring中IOC容器的實現(一):介面體系原始碼Spring
- 用ListView簡單實現滑動列表View
- 線上直播系統原始碼,迴圈滾動RecyclerView的實現原始碼View
- 從linux原始碼看socket的阻塞和非阻塞Linux原始碼
- 從 Linux 原始碼看 socket 的阻塞和非阻塞Linux原始碼
- iOS 如何絲滑的側滑返回iOS
- 一個RecyclerView實現多級摺疊列表(二)View
- 一個RecyclerView實現多級摺疊列表(TreeRecyclerView)View
- 從JDK原始碼看OutputStreamJDK原始碼
- 從Class原始碼看反射原始碼反射
- 從Chrome原始碼看WebSocketChrome原始碼Web