事件分發(4)-玩轉自定義behavior之頂部停靠

主目錄見:Android高級進階知識(這是總目錄索引)
我們前面講完《全面的CoordinateLayout和Behavior源碼分析》的分析了。我們已經基本懂得怎么掌控behavior了。擼起袖子就是干,不多說,上圖:

效果圖

想要源碼的請[點擊下載]

一.目標

自定義behavior提供了一個優雅的方式來協調整體的運動,使整體運動變得更優雅更容易,所以我們有必要來擼一串代碼,今天目標如下:
1.復習《全面的CoordinateLayout和Behavior源碼分析》的知識點;
2.提供一個頂部停靠的參考方案。

二.源碼分享

1.基礎使用

這個實例分為上中下三個部分,我們看下xml內容:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >
        <LinearLayout
            android:id="@+id/llHeadView"
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:background="@color/gray"
            android:orientation="vertical"
            app:layout_behavior="@string/HeaderBehavior"
            android:layout_gravity="center_horizontal"
            android:gravity="center_horizontal">
            <ImageView
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:src="@mipmap/header_icon"
                android:layout_marginTop="30dp"
                />
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="ZJ_Rocky"
                android:textColor="@color/black"
                android:textSize="20dp"
                android:layout_marginTop="20dp"/>
        </LinearLayout>

        <android.support.design.widget.TabLayout
            android:id="@+id/toolbar_tab"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="#ffffff"
            app:layout_behavior="@string/StickyBehavior"
            app:tabIndicatorColor="#0835f8"
            app:tabIndicatorHeight="2.0dp"
            app:tabSelectedTextColor="#0835f8"
            app:tabTextColor="#ced0d3"
            >
            <android.support.design.widget.TabItem
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:text="我的"
                />

            <android.support.design.widget.TabItem
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:text="分享" />

            <android.support.design.widget.TabItem
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:text="收藏" />

            <android.support.design.widget.TabItem
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:text="關注" />

            <android.support.design.widget.TabItem
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:text="關注者" />
        </android.support.design.widget.TabLayout>

        <android.support.v4.view.ViewPager
            android:id="@+id/main_vp_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_behavior="@string/BottomBehavior"
            />
</android.support.design.widget.CoordinatorLayout>

這個地方要注意app:layout_behavior要放在CoordinateLayout直接的子布局,并且界面要有實現了NestedScrollingChild接口的子類。不然界面自定義behavior是攔截不到滑動事件的。

2.代碼分析

首先我們看下我們的布局結構及behavior的使用位置:


結構圖
2.1布局

因為我們知道CoordinateLayout跟FrameLayout是一樣的,如果直接布局則會重疊在一起,需要我們自己去重新布局。在上面我們已經講過了,behavior里面有onLayoutChild方法用來布局(跟自定義控件中的onLayout是一模一樣的,因為這個其實就是在onLayout中調用的),所以我們先看下頂部(HeaderBehavior)的布局代碼:

 @Override
    public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
        parent.onLayoutChild(child, layoutDirection);
        if (childTop < 0){
            child.offsetTopAndBottom(childTop);
        }else{
            child.offsetTopAndBottom(initOffset);
        }
        return true;
    }

因為onLayoutChild可能會被執行多次,所以我們要記錄下childTop的高度,不然有可能會滑動一段距離后,ViewPager切換的時候會被重置會initOffset位置。接下來我們看下中間部分(StickyBehavior)的代碼:

  @Override
    public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
        parent.onLayoutChild(child, layoutDirection);
        View headerView = getHeaderView(parent);
        child.offsetTopAndBottom(headerView.getBottom());
        maxOffset = headerView.getHeight();
        return true;
    }

中間部分也就是停靠欄的初始布局是頭部的底部位置,且這個地方記錄下maxOffset為頭部的總高度。最后我們看下底部(BottomBehavior)的代碼:

    @Override
    public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
        parent.onLayoutChild( child, layoutDirection);
        View stickyLayout = getStickyLayout(parent);
        View headerView = getHeaderView(parent);
        child.offsetTopAndBottom(headerView.getBottom() + stickyLayout.getHeight());
        return true;
    }

底部的布局的位置是頂部的底部和中間部分的高度和,這樣我們的幾個部分布局已經完成了布局。

2.2依賴關系

這個地方我們的依賴關系是這樣的,頭部和底部的布局依賴中間停靠欄的滑動,所以我們這個地方先來看下頭部設置的依賴:

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

我們看到這個地方我們判斷依賴是不是TabLayout,因為我們的停靠欄是TabLayout,當然不是一定要這么判斷,你可以判斷控件id是不是同一個或者其他。這個地方我們就這么判斷了。接著我們看下底部設置的依賴:

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

跟頭部是一模一樣的,依賴關系很容易設置。

2.3運動協調

因為頭部和底部的布局都是依賴的中間停靠欄的,那么我們這里看下中間停靠欄是怎么運動的:

   @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        //判斷監聽的方向
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 ;
    }

onStartNestedScroll方法是在(NestedScrollingChild實現類)DOWN事件中調用的,也就是說滑動將要開始調用的,這個方法主要用來監聽垂直或者左右的事件,我們這個地方就監聽垂直滑動的事件就可以了。接著MOVE事件來了,我們看下如下:

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
        int top = child.getTop();
        if (top >= minOffset){
            if (dy > 0){//往上移動
                customScrollToTop = true;
            }else{
                customScrollToTop = false;
            }
            consumed[1] = scroll(child, dy, minOffset, maxOffset);
        }
    }

這個事件是在NestedScrollingChild實現類中的MOVE事件開始處調用的。也就是說onNestedPreScroll是在滑動開始的時候調用的。其中有個重要的參數consumed數組,這個數組很關鍵,他總共有consumed[0]和consumed[1],consumed[0]用來保存水平滑動時候中間停靠欄想要消費多少,consumed[1]用來保存垂直滑動時候中間停靠欄想要消費多少滑動值,因為我們這個地方主要就是監聽垂直方向的滑動,所以我們主要就是消費垂直的滑動值。然后dy和dx就是代表垂直和水平滑動了多少值。代碼里面我們首先是記錄下customScrollToTop(用來判斷是上滑還是下滑等會會有用),然后我們調用scroll來滑動,我們看下怎么實現:

   private int scroll(View child, int dy, int minOffset, int maxOffset) {
        int top = child.getTop();
        int offset = clamp(top - dy, minOffset, maxOffset) - top;
        child.offsetTopAndBottom(offset);
        return -offset;
    }

我們看到主要就是調用clamp方法,這個方法我們一起看下:

    /**
     * 取上下限之間的值
     * */
    private int clamp(int i, int minOffset, int maxOffset) {
        if (i < minOffset) {
            return minOffset;
        } else if (i > maxOffset) {
            return maxOffset;
        } else {
            return i;
        }
    }

這個方法主要是用來判斷滑動之后的值是否在minOffset和maxOffset之間,如果超過則取minOffset或者maxOffset,然后scroll方法還做了一件事就是滑動中間的停靠欄,滑動距離就是-dy,最后將滑動距離返回回去給consumed[1],表示我要攔截minOffset和maxOffset之間所有的垂直方向滑動距離。接下來我們看下MOVE事件調用的另外一個方法:

    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        int top = child.getTop();
        if (top >= minOffset){
            scroll(child, dyUnconsumed, minOffset, maxOffset);
        }
    }

這個方法和onNestedPreScroll方法類似,同樣可以在這里做滑動,只是參數有點不同,其實dxUnconsumed和dyUnconsumed是上面一個方法onNestedPreScroll未消費掉的水平和垂直方向滑動的滑動值,我們這里代碼跟上一個代碼做的邏輯一樣,只是把dyUnconsumed再消費掉。最后DOWN事件和MOVE事件都消費了,最后就是UP事件了,對應于onStopNestedScroll方法:

    @Override
    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target) {
        //如果需要回彈則需要在這里寫代碼
        if(Math.abs(child.getTop()) < maxOffset/4 && customScrollToTop){
            scrollTo(coordinatorLayout, child, minOffset, 1000);
        }
    }

這里就是做一些回彈操作的,我們這里也一樣,如果已經到達 maxOffset/4,然后滑動是上滑的話我們就緩慢靠近頂部,方法如下:

    private void scrollTo(final CoordinatorLayout parent, final View child, final int y, int duration){
        customScrollToTop = false;
        final Scroller scroller = new Scroller(parent.getContext());
        scroller.startScroll(0,child.getTop(),0,y - child.getTop(),duration);
        ViewCompat.postOnAnimation(child, new Runnable() {
            @Override
            public void run() {
                if (scroller.computeScrollOffset()) {
                    int delta = scroller.getCurrY() - child.getTop();
                    child.offsetTopAndBottom(delta);
                    parent.dispatchDependentViewsChanged(child);
                    ViewCompat.postOnAnimation(child, this);
                }
            }
        });
    }

我們這里主要用到了Scroller類和ViewCompat.postOnAnimation()方法來實現緩慢靠近頂部。這樣如果停靠欄在minOffset和maxOffset之間的滑動也就完成了。因為頭部和底部都是依賴于停靠欄運動的,所以我們先來看下頭部(HeaderBehavior)跟隨著停靠欄怎么運動:

  @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {

        if (dependentViewTop != -1){
            int top = child.getTop();
            int dy = dependency.getTop() - dependentViewTop;

            if (dy > -top){
                dy = -top;
            }

            child.offsetTopAndBottom(dy);
        }

        dependentViewTop = dependency.getTop();
        childTop = child.getTop();
        return true;
    }

當停靠欄運動的時候,這個依賴也會相應的做出反應,就是在onDependentViewChanged方法中寫我們自己的運動情況,其中child就是我們頭部,dependency就是依賴的對象停靠欄。我們這里主要是計算dy(也就是兩次滑動的滑動差值),然后child即頭部進行相應滑動。接著我們來看下底部滑動情況:

  @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        if (dependentViewTop != -1){
            int dy = dependency.getTop() - dependentViewTop;

            if (!isInit){
                child.offsetTopAndBottom(dy);
            }
           isInit = false;
        }

        dependentViewTop = dependency.getTop();
        childTop = child.getTop();
        return true;
    }

這個方法也是類似,這里因為onDependentViewChanged會執行多次,所以我們這里用isInit來標示是不是第一次布局,如果是則不需要讓child移動了,因為我們onLayoutChild已經布局過了,如果這個地方再移動的話,那么我們初始位置就偏移了。到這里我們的例子就講解完畢了,希望大家能明白,當然前提是你看了上面一篇的原理分析。
總結:這個例子主要是使用了上一篇分析的知識,希望大家能在完全弄懂原理的情況下再來看這一篇,不然知識不會全面,最后就enjoy安卓之旅吧。

enjoy

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容