RecyclerView學習(三)----高仿知乎的側滑刪除

本文已授權微信公衆號:鴻洋(hongyangAndroid)在微信公衆號平臺原創首發。

偶爾看到知乎首頁的側滑刪除,感覺還不錯。之前用RecyclerView的ItemTouchHelper類來實現了Item的拖動和刪除功能,今天帶來的則是純手工打造的一個側滑刪除。老規矩,先看看效果圖:

這裏寫圖片描述

當滑動的距離小於紅塊的一半,鬆開手指以後,會自動收縮當前item;當滑動的距離超過一半,鬆開手指以後,會自動將當前item刪除。一起看看怎麼實現的吧:

1.準備工作:
(1)數據準備:一個存放數字的List數組來模擬RecyclerView的數據
(2)子Item的佈局:整體線性佈局水平排列,左側是顯示的部分,右側是不顯示的部分,也就是刪除的部分。刪除的部分是一個相對佈局,然後通過滑動的距離來控制字體與圖片的顯示與隱藏。
(3)RecyclerView三要素:RecyclerAdapter,RecyclerViewHolder,LayoutManager依次設置即可。

2.View的滑動實現:
(1)滑動方法:
這裏我是使用View本身提供的scrollTo/scrollBy方法來實現滑動,scrollBy實際上也是調用了scrollTo方法,scrollTo實現的是基於所傳遞參數的絕對滑動,而scrollBy實現的是基於當前位置的相對滑動。

舉個例子:
scrollTo(50,50)會將View位置移動到指定位置,多次調用無效
scrollBy(50,50)會將View位置移動到指定位置,每調用一次會在現有位置基礎上進行移動
結合這個例子分析一下,手指滑動的距離就是整體View移動的距離,那我們可以直接使用scrollBy(x,y)方法來進行處理,將手指滑動的距離作爲第一個參數傳遞進去,而不用考慮當前View滑動的位置。

(2)滑動方向
在Android屏幕直角座標系中,原點在屏幕左上角,向右X爲正,向下Y爲正。
scrollBy()的參數的正負影響滑動的方向,這裏我們只考慮水平方向上的滑動,所以將第二個參數設置爲0。
按我們正常的理解,應該是參數爲負的時候,向座標軸負方向滑動;當參數爲正的時候,向座標軸正方向滑動。
scrollBy()在參數爲負的時候,向座標軸正方向滑動;當參數爲正的時候,向座標軸負方向滑動。
這是因爲在scrollBy()源碼執行過程的最後,會調用這個方法 :
invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false);
其中l,t,r,b爲原來座標點,scrollX,scrollY爲目標座標點,只有當目標座標點值是負數時,負負得正,移動到的位置才爲正數,這樣纔會重新繪製,整體的View就會向座標軸正方向滑動。

綜上,我們想讓子Item從右往左沿X軸的負方向滑動,scrollBy(X,0)中的X一定是大於0的

(3)滑動實現
現在滑動的方法與方向都已經確定了,接下來的重點就是計算滑動的距離,也就是scrollBy(X,0)中的X的大小了。

   public boolean onTouchEvent(MotionEvent event) {

        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
            }
             break; 
            case MotionEvent.ACTION_MOVE: {
                    int scrollX = itemLayout.getScrollX();
                    int newScrollX = mStartX - x;
                    if (newScrollX < 0 && scrollX <= 0) {
                        newScrollX = 0;
                    } else if (newScrollX > 0 && scrollX >= maxLength) {
                        newScrollX = 0;
                    }
                    if (scrollX > maxLength / 2) {
                        textView.setVisibility(GONE);
                        imageView.setVisibility(VISIBLE);

                       if (isFirst) {
                            ObjectAnimator animatorX = ObjectAnimator.ofFloat(imageView, "scaleX", 1f, 1.2f, 1f);
                            ObjectAnimator animatorY = ObjectAnimator.ofFloat(imageView, "scaleY", 1f, 1.2f, 1f);
                            AnimatorSet animSet = new AnimatorSet();
                            animSet.play(animatorX).with(animatorY);
                            animSet.setDuration(800);
                            animSet.start();
                            isFirst = false;
                        }



                    } else {
                        textView.setVisibility(VISIBLE);
                        imageView.setVisibility(GONE);
                    }
                    itemLayout.scrollBy(newScrollX, 0);
            }
            break;
            case MotionEvent.ACTION_UP: {

            }
            break;
        mStartX = x;
        return super.onTouchEvent(event);
    }

其中itemLayout爲一個水平的LinearLayout,textView爲LinearLayout中的”刪除”,imageView爲LinearLayout中的眼睛圖片。

移動計算值 = 最開始點座標 - 最後移動到的座標

  1. 滑動開始的時候,不允許item向右滑動,此時scrollBy(x,0)中的x小於0;滑動的過程中,左右滑動都可以,但getScrollX()小於等於0的時候就不允許繼續滑動。此時將x設置爲0,代表不再滑動
  2. 滑動距離大於一半的時候,將文字設置爲GONE,圖片設置爲VISIBLE,否則剛好相反。細心的小夥伴會發現,眼睛圖片的顯示有一個從小到大再到小的過程,這裏用的是屬性動畫ObjectAnimator加上組合動畫AnimatorSet實現的,並且進行了一下判斷,讓動畫在滑動過程中只出現一次
  3. 滑動的距離超過紅塊的距離的時候,不允許item向左滑動,此時scrollBy(x,0)中的x是大於0。此時將x設置爲0,代表不再滑動

3.RecyclerView的滑動實現

前面已經實現了將一個LinearLayout左右進行滑動,現在關鍵就是將這個LinearLayout的滑動與我們RecyclerView的滑動相結合。
解決辦法就是將這個水平排列的LinearLayout作爲子item佈局的一部分,然後再獲取每一個item的LinearLayout就可以進行滑動了。這裏肯定需要一個參數position,只有獲取到item的position才能得到item的LinearLayout,才能進行刪除操作。

(1)通過觸碰的座標計算當前的position
這裏我們肯定要自定義一個MyRecyclerView繼承自RecyclerView,然後重寫onTouchEvent()方法,在MotionEvent.ACTION_DOWN的時候就要拿到你觸碰的item的position。

public boolean onTouchEvent(MotionEvent event) {

        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                //通過點擊的座標計算當前的position
                int mFirstPosition = ((LinearLayoutManager) getLayoutManager()).findFirstVisibleItemPosition();
                Rect frame = mTouchFrame;
                if (frame == null) {
                    mTouchFrame = new Rect();
                    frame = mTouchFrame;
                }
                final int count = getChildCount();
                for (int i = count - 1; i >= 0; i--) {
                    final View child = getChildAt(i);
                    if (child.getVisibility() == View.VISIBLE) {
                        child.getHitRect(frame);
                        if (frame.contains(x, y)) {
                            pos = mFirstPosition + i;
                        }
                    }
                }
            }
            break;

在Listview當中,有一個pointToPosition(x, y)方法可以根據座標獲取到當前的position,在RecyclerView中沒有這個方法,需要我們自己動手寫一個。

這裏有一點特別需要注意的是:這裏遍歷的是當前可見範圍內的子項。使用getChildCount()與getChildAt()進行取值,只能是當前可見區域的子項!取值範圍在0到getLastVisiblePosition()減去getFirstVisiblePosition()之間(可取等於)。

(2)通過position得到item的viewHolder

                //通過position得到item的viewHolder
                View view = getChildAt(pos - mFirstPosition);
                MyViewHolder viewHolder = (MyViewHolder) getChildViewHolder(view);
                itemLayout = viewHolder.layout;
                textView = (TextView) itemLayout.findViewById(R.id.item_delete_txt);
                imageView = (ImageView) itemLayout.findViewById(R.id.item_delete_img);

viewHolder是存放視圖與數據的地方,只要拿到當前item的viewHolder,就可以獲取到我們的itemLayout,也就是需要滑動的LinearLayout。RecyclerView提供了一個getChildViewHolder()的方法來獲取當前item的viewHolder,傳進去的參數就是通過getChildAt(index)獲取到的view。

4.RecyclerView的刪除實現

我們在上一步已經拿到了item的position與itemLayout,在MotionEvent.ACTION_MOVE的時候使用itemLayout就可以進行滑動,在MotionEvent.ACTION_UP的時候使用position就可以進行刪除。

 case MotionEvent.ACTION_UP: {
                int scrollX = itemLayout.getScrollX();
                if (scrollX > maxLength / 2) {
                    ((RecyclerAdapter) getAdapter()).removeRecycle(pos);
                }
            }
  break;

當滑動的距離大於一半的時候,執行刪除操作。 將刪除方法寫在RecyclerAdapter中:

    public void removeRecycle(int position) {
        lists.remove(position);
        notifyDataSetChanged();
        if (lists.size() == 0) {
            Toast.makeText(context, "已經沒數據啦", Toast.LENGTH_SHORT).show();
        }
    }

5.RecyclerView的滑動優化
之前說到當滑動的距離小於紅塊的一半,鬆開手指以後,會自動收縮當前item,但是這個滑動比較生硬,用戶體驗很差。我們需要實現漸進式滑動,也就是View的彈性滑動。這裏我們使用的是Scroller。

初始化Scroller:


        mScroller = new Scroller(context, new LinearInterpolator(context, null));

第二個參數是一個勻速插值器

Scroller的使用方法:

            case MotionEvent.ACTION_UP: {
                int scrollX = itemLayout.getScrollX();
                if (scrollX > maxLength / 2) {
                    ((RecyclerAdapter) getAdapter()).removeRecycle(pos);
                } else {
                    mScroller.startScroll(scrollX, 0, -scrollX, 0);
                    invalidate();
                }
            isFirst = true;
            }
            break;

startScroll()四個參數依次爲:開始移動時的X座標;開始移動時的Y座標;沿X軸移動距離,爲負時,子控件向右移動;沿Y軸移動距離。如果後面沒有duration這個參數,系統會使用默認的時長:250毫秒
然後調用invalidate()是使view進行重繪,在view的onDraw()方法中又會去調用computeScroll()方法,view才能實現彈性滑動

    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            itemLayout.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }

首先向Scroller獲取當前的滑動起點,通過scrollTo方法實現滑動,然後再調用invalidate()來進行重繪,又會調用computeScroll()方法,然後再獲取當前的起點,使用scrollTo方法滑動到新的位置。如此往復,直到整個滑動結束。其實Scroller的設計思想就是小幅度滑動組成整個的彈性滑動。

至此,一個漂亮的側滑刪除就已經實現了,零碎的東西不少,記錄下來一起學習~~

補充:

評論裏有小夥伴說加上點擊事件後沒有效果,會產生事件衝突。謝謝這位小夥伴的提醒,之前沒有考慮這方面的問題。然後週末在家完善了一下,看看怎麼解決的吧。

            case MotionEvent.ACTION_UP: {


                xUp = x;
                yUp = y;
                int dx = xUp - xDown;
                int dy = yUp - yDown;
                if (Math.abs(dy) < mTouchSlop && Math.abs(dx) < mTouchSlop) {
                    listener.getPosition(pos);

                } else {
                    int scrollX = itemLayout.getScrollX();
                    if (scrollX > maxLength / 2) {
                        ((RecyclerAdapter) getAdapter()).removeRecycle(pos);
                    } else {
                        mScroller.startScroll(scrollX, 0, -scrollX, 0);
                        invalidate();
                    }
                    isFirst = true;
                }
            }


            break;

RecyclerView的點擊事件無非就是接口回調獲取position的過程,我們在MotionEvent.ACTION_DOWN的時候已經拿到了position。那麼只要在點擊的時候將這個position傳遞給Activity呢。現在只要判斷什麼動作是點擊就可以了!!!其實只要對比一下MotionEvent.ACTION_DOWN與MotionEvent.ACTION_UP的X,Y座標差,小於默認的滑動最小距離的時候,就認爲是點擊動作,將得到的position傳遞即可。最後讓Activity實現這個接口,獲取參數,進行事件的處理就歐了~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章