RecyclerView是一個用來替換以前的ListView和GridView的控件,使用的時候,雖然比之前的ListView看起來麻煩,可是其實做爲一個高度解耦的控件,複雜一點點換來極大的靈活性,豐富的可操做性,何樂而不爲呢。不過今天主要說說它的一個輔助類ItemTouchHelper來實現列表的拖動和滑動刪除。java
compile 'com.android.support:support-v13:25.+'
複製代碼
<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>
複製代碼
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,不影響複用,可是內存會不會泄漏呢?android
而後裏面還有兩個在拖拽和滑動時的回調,這裏是咱們本身定義的一個接口TouchCallbackListenerbash
TouchCallbackListeneride
public interface TouchCallbackListener {
/** * 長按拖拽時的回調 * @param fromPosition 拖拽前的位置 * @param toPosition 拖拽後的位置 */
void onItemMove(int fromPosition, int toPosition);
/** * 滑動時的回調 * @param position 滑動的位置 */
void onItemSwipe(int position);
}
複製代碼
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;
}
}
複製代碼
最後在Activity中來使用RecyclerViewpost
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);
}
複製代碼
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);
}
}
}
複製代碼
到此就實現了一個支持長按拖拽和滑動刪除的列表,很簡單,效果就不截圖了。fetch
實現拖拽和滑動刪除的過程的很簡單,而且還有很是流暢的動畫。只須要給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的基礎上來改進,後面再說吧。