CoordinatorLayout+自定義Behavior實戰學習

前言部分

CoordinatorLayout這個view出現已經很久了,今天看到知乎上的一個神奇聯動的效果,類似的效果在很多應用上都有使用,以前雖然經常看到但是沒有實際去查看如何實現的。這幾天特意去查看資料學習其中的方式,但是由於能力有限和資料不是很全面只學到了一個皮毛。在此做一個記錄,也算是這幾天的努力的小總結吧。

說到CoordinatorLayout我們最先試用過的AppBarLayout和CollapsingToolbarLayout實現摺疊頭部。或者實現視差效果等等。。這裏我都不做介紹了文章很多,實現的效果也很棒,最主是不需要我們自己子們處理邏輯,系統都幫我們實現好了。

我們雖然使用過,但是其實並沒有去深入瞭解過實現的原理是什麼,今天就通過實現一個簡單的效果來學習一下自定義的Behavior。

實現的就是類似知乎等一些閱讀類app等效果,如下入:

### 內容部分

事實上我們做這些工作是爲了更好的實現view之間的協調聯動,在沒有CoordinatorLayout輔助view協調滑動之前,我們都是通過parent來對觸摸事件進行分發,但這這樣做就需要我們對事件的傳遞有很深刻的理解,因爲事件的傳遞是從外由內的,但是事件消耗確實從內向外,parent響應事件後在傳遞給內部的子view,子view可以決定是否消耗事件,但是這也有一個問題,就是子view消耗事件後,其他的view都不會在收到事件的信息。所以協調多個子view聯動就變得很麻煩。

下面一些詳細的講解可以去連接看看大佬的介紹,我也是讀了文章開始自己寫一個小 demo練手的。

大佬文章連接

上面文章中有個總結很棒,這裏直接貼出來。

自定義 Behavior 的兩種目的

我們可以按照兩種目的來實現自己的 Behavior,當然也可以兩種都實現啦

  • 某個 view 監聽另一個 view 的狀態變化,例如大小、位置、顯示狀態等
  • 某個 view 監聽 CoordinatorLayout 內的 NestedScrollingChild 的接口實現類的滑動狀態

實例編寫

最終目標實現:

頭部view、尾部view和列表綁定,當列表處於初始位置的時候,下拉列表實現頭部view同時拉出,上拉列表的時候實現頭部view隱藏。尾部view的規則是列表上拉時候隱藏,列表下拉的時候出現。

第一步實現如下圖

1. 首先實現一下列表下拉,和頭部view的聯動。這裏涉及到兩個Behavior,一個是RecyclerView的一個是TextView的。下面看代碼:
public class RecyclerTitleBehavior extends CoordinatorLayout.Behavior<RecyclerView> {

    public RecyclerTitleBehavior() {
    }

    public RecyclerTitleBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, RecyclerView child, View dependency) {
        return dependency instanceof TextView;
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, RecyclerView child, View dependency) {
        //計算列表y座標,最小爲0
        float y = dependency.getTranslationY();
        if (y > dependency.getHeight()) {
            y = dependency.getHeight();
        } else if (y < 0) {
            y = 0;
        }
        child.setY(y);
        return true;
    }
}

這個比較簡單,主要是通過依賴頭部view來改變自己的位置。如上面所說的兩種方式一種:RecyclerView是負責監控頭部view的別緻變化。頭部view發生位移變化時候,自身也作出相應的位移變化。

    1. 這一步實現頭部view監聽CoordinatorLayout的直接子 view的滑動,RecyclerView之所以作爲子view可以被監聽,因爲其內部是實現了NestedScrollingChild2接口,所以當發生滑動的時候會通過NestedScrollingChildHelper調用父view的onStartNestedScroll接口(前提是父 view實現了NestedScrollingParent2接口),CoordinatorLayout作爲父view可以接收到它滑動的信息,然後通過循環遍歷childCount獲取Behavior,通過Behavior來把滑動信息發送給所有的子view(這裏父 view也可以自己選擇接受滑動,這樣會優先子 view滑動)。其他子view如果依賴這個view則實現其他子 view的滑動;

    2. 同時其他的子 view(不可滑動的View默認是沒有實現NestedScrollingChild2接口)通過設置Behavior也實現了NestedScrollingChild2接口,通過Behavior來接收父view的滑動事件回調。如果子view發生滑動後,也會通過接口的形式回掉到NestedScrollingParent2接口中,然後父view決定自己是否配合子view的滑動,然後父view把自己滑動的情況反饋給子view,這時候子view再從新計算滑動的距離,在進行滑動。

    3. 如上所述,基本就是子view滑動,告訴一下父view我要滑動了,父view首先考慮一下自己要不要滑動,然後再告訴其它的子view有個子view滑動了,你們誰關注它了想一起動啊,然後把父 view自己滑動得結果告訴滑動的子view,讓他重新計算一下距離,然後滑動剩下的距離。

    public class TextBehavior extends CoordinatorLayout.Behavior<TextView> {
    
        /**
         * 上邊界
         */
        private boolean isUpLimit = false;
        /**
         * 下邊界
         */
        private boolean isDownLimit = false;
    
        public TextBehavior() {
        }
    
        public TextBehavior(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull TextView child, @NonNull MotionEvent ev) {
    
            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    isUpLimit = false;
                    isDownLimit = false;
                default:
                    break;
            }
            return super.onInterceptTouchEvent(parent, child, ev);
        }
    
        @Override
        public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull TextView child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
            return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
        }
    
        @Override
        public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull TextView child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
            super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
            //dy 下 爲 負數 && 上 爲 正數
            if (target instanceof RecyclerView) {
                RecyclerView recyclerView = (RecyclerView) target;
                LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
                int firstPosition = layoutManager.findFirstCompletelyVisibleItemPosition();
                float translationY = child.getTranslationY() - dy;
    //            LogUtil.d("translationY===" + translationY);
    //            child.setTranslationY(translationY);
    
    //            if (firstPosition == 0 && translationY == 0) {
    //                isUpLimit = true;
    //            }
    //
                if (firstPosition == 0 && canScroll(dy)) {
                    if (translationY > child.getHeight()) {
                        translationY = child.getHeight();
                        isUpLimit = false;
                        isDownLimit = true;
                    } else if (translationY < 0) {
                        translationY = 0;
                        isUpLimit = true;
                        isDownLimit = false;
    
                    }
                    child.setTranslationY(translationY);
                    consumed[1] = dy;
                }
    
            }
    
        }
    
        private boolean canScroll(int dy) {
            if (isUpLimit && dy > 0) {
                return false;
            }
            if (isDownLimit && dy < 0) {
                return false;
            }
            return true;
    
        }
    }
    

    基本上就這樣,這也是醉簡單的滑動效果了。

下面我們在添加一個聯動的子view

我們添加一個尾部的View,它也監聽另外的一個View(就是RecyclerView了),如果View滑動了一段距離,尾部的View也會有對應的動作。代碼如下:

/**
 * @Author : dongfang
 * @Created Time : 2019-05-28  15:08
 * @Description:
 */
public class TextBottomBehavior extends CoordinatorLayout.Behavior<View> {

    /**
     * 上邊界
     */
    private boolean isUpLimit = false;
    /**
     * 下邊界
     */
    private boolean isDownLimit = false;

    public TextBottomBehavior() {
    }

    public TextBottomBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull MotionEvent ev) {

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                isUpLimit = false;
                isDownLimit = false;
            default:
                break;
        }
        return super.onInterceptTouchEvent(parent, child, ev);
    }

    @Override
    public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
        return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
        //dy 下 爲 負數 && 上 爲 正數
        if (target instanceof RecyclerView) {
            float translationY = child.getTranslationY() + dy;
            LogUtil.d("translationY===" + child.getTranslationY() + "finalY==" + translationY);
            if (canScroll(dy)) {
                if (translationY < -child.getHeight()) {
                    translationY = -child.getHeight();
                    isUpLimit = false;
                    isDownLimit = true;
                } else if (translationY > 0) {
                    translationY = 0;
                    isUpLimit = true;
                    isDownLimit = false;
                }
                child.setTranslationY(translationY);
//                consumed[1] = dy;
            }
        }

    }

    private boolean canScroll(int dy) {
        if (isUpLimit && dy > 0) {
            return false;
        }
        if (isDownLimit && dy < 0) {
            return false;
        }
        return true;

    }

}

這個和上個基本是一致的,只是運動的方向是反的。

還有一點要注意,我們的被監聽的view只是跟隨頭部的view滑動,並不跟隨尾部View滑動

結尾部分

鑑於能力有限,寫的不是很清楚的話,希望大家指出,如果有更好的資料也請給我留言。

final 謝謝

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