仿京東、淘寶首頁,經過兩層嵌套的RecyclerView實現tab的吸頂效果

爲何會有這篇文章

以前寫過一篇文章使用CoordinatorLayout過程當中遇到的兩個問題以及淺析CoordinatorLayout工做機制,這篇文章上主要講了經過CoordinatorLayout實現tab吸頂的效果時遇到的問題,效果跟京東、淘寶首頁相似,只不過實現方法不一樣而已,可是使用CoordinatorLayout來實現是會有很多細節問題是很難處理好的,下面會詳細介紹。java

首先咱們能夠簡單看下京東首頁的效果gif,來看看咱們究竟是要實現什麼樣的效果:git

image

京東首頁的tab篩選區將feed分爲兩個部分,上面是各類不一樣item,tab的下半部分能夠左右橫滑,而且下拉能夠加劇更多,只要網絡有數據的狀況下理論上是能夠無限下拉的。github

其實用CoordinatorLayout來實現tab吸頂,若是能將一些細節問題處理好的話,其實大體能夠實現相似京東首頁的這個效果,具體細節問題能夠參考文章開頭說的以前的文章,文章裏講了下使用CoordinatorLayout來實現相似效果遇到的動畫抖動問題以及頁面回彈問題以及對應的解決方法。bash

那麼爲何會不採用CoordinatorLayout來實現,轉而採用嵌套RecyclerView的方式呢?網絡

首先咱們來看下CoordinatorLayout實現的大體佈局:ide

image

一個問題是從AppBarLayout滑動效果是不能傳遞到下面的ViewPager裏去的,我嘗試了各類方式都沒能解決掉這個問題,能夠簡單看下Demo效果圖:工具

image

從gif圖大體能夠看到AppBarLayout滑上去以後慣性消失了,tab下面的區域是不能接着滾動的。佈局

這個慣性消失的問題,我在網上找到了一個一篇解決慣性消失的文章以下支付寶首頁交互三部曲3實現支付寶首頁交互,實現方式大體是本身把CoordinatorLayout這套機制再實現了一遍,由於是本身實現的,裏面的一些機制是比較方便改動的,它處理慣性這個問題的邏輯大體是將AppBarLayout中未消費的y軸偏移量拿出來再交由RecyclerView去滑動,代碼以下:post

mHeaderView.setOnHeaderFlingUnConsumedListener(new APHeaderView.OnHeaderFlingUnConsumedListener() {
    @Override
    public int onFlingUnConsumed(APHeaderView header, int targetOffset, int unconsumed) {
        APHeaderView.Behavior behavior = mHeaderView.getBehavior();
        int dy = -unconsumed;
        if (behavior != null) {
            mRecyclerView.scrollBy(0, dy);
        }
        return dy;
    }
});
複製代碼

這裏因爲篇幅緣由,就不展開詳細介紹了,感興趣地同窗能夠點開上面的連接去研究研究,文章中也貼出了GitHub地址。動畫

咱們從新回到咱們一開始的問題,爲何想替換掉CoordinatorLayout,另一個問題是CoordinatorLayout這種實現相對比較簡單,可是會致使頁面的嵌套層級很深,咱們從上面貼出來的佈局來看,view嵌套的層級特別深,並且若是咱們要實現相似京東或者淘寶首頁這樣的效果,在TabLayout上面的區域,也就是下圖箭頭標註的地方必需要採用RecyclerView的來實現,由於tab上半部分的內容和個數都是不肯定的,使用RecyclerView才比較方便,可是這樣頁面的層級就更深了,加載速度也變得更慢了。

image

如何實現

要拋棄CoordinatorLayout,那麼如何實現呢?

咱們用Android Studio中的Layout Inspector工具看了下京東首頁的佈局,發現的確是採用兩層RecyclerView嵌套來實現的,展現的佈局大體以下所示:

image

那麼接下來是怎麼去實現這個效果了。其實一開始我覺得要採用嵌套滾動這套機制來實現,後來發現不採用嵌套滾動機制也是能夠實現的。

如今咱們能夠大體構造這樣一個佈局:

image

咱們把ViewPager以及TabLayout這一塊做爲外部RecyclerView的一個item,ViewPager可能會有個多個內部RecyclerView,只要咱們能讓外部RecyclerView和內部RecyclerView的滑動事件正確分發基本就能夠解決這個問題了。

若是隻是構造出這個佈局出來,咱們發現內部的RecyclerView都不會顯示出來,由於滑動徹底由外部RecyclerView接管了。

那麼重點來了,這種狀況如何處理?

其實RecyclerView的LayoutManager中有這兩個方法用於判斷RecyclerView在水平方向上和豎直方向上是否能夠滾動的。

public boolean canScrollHorizontally() {
    return false;
}

public boolean canScrollVertically() {
    return false;
}
複製代碼

而後LayoutManager有各類不一樣的實現LinearLayoutManager,StaggeredGridLayoutManager等

這個LayoutManager中的canScrollVertically和canScrollHorizontally在RecyclerView的onInterceptTouchEvent中是會拿來做判斷,判斷當前RecyclerView是否須要處理滑動事件的。

還有一點須要注意:咱們處理內外兩層RecyclerView的滑動衝突問題,主要是想解決下面兩種場景:1、手指上滑時,當外部RecyclerView滑動底部的時候,內部的RecyclerView能繼續去響應用戶的滑動,由於內部的RecyclerView理論上是能夠無限滾動的;2、手指下滑時,當內部的RecyclerView滑動到頂部的時候,外部的RecyclerView可以繼續響應用戶的下滑事件。

其實上面已經說出瞭如何處理嵌套RecyclerView的最重要的點,其餘的部分至關於都是一些細節的處理了。

在外部RecyclerView(下面成ParentRecyclerView)中的重寫LayoutManager的canScrollVertically方法以下:

fun initLayoutManager() {
    val linearLayoutManager = object :LinearLayoutManager(context) {

        override fun canScrollVertically():Boolean {
            //找到當前的childRecyclerView
            val childRecyclerView = findNestedScrollingChildRecyclerView()
            //只有當前childRecyclerView滑動到頂部才認爲ParentRecyclerView是能夠豎直方向是能夠滾動的
            return childRecyclerView == null || childRecyclerView.isScrollTop()
        }

    }
    linearLayoutManager.orientation = LinearLayoutManager.VERTICAL
    layoutManager = linearLayoutManager
}
複製代碼

在內部RecyclerView(如下稱ChildRecyclerView)中定義了isScrollTop(),用於判斷ChildRecyclerView是否滾動到頂部。

fun isScrollTop(): Boolean {
    //RecyclerView.canScrollVertically(-1)的值表示是否能向下滾動,false表示已經滾動到頂部
    return !canScrollVertically(-1)
}
複製代碼

另外,在ParentRecyclerView的onTouchEvent方法中:

override fun onTouchEvent(e: MotionEvent): Boolean {
        if(lastY == 0f) {
            lastY = e.y
        }
        if(isScrollEnd()) {
            //若是父RecyclerView已經滑動到底部,須要讓子RecyclerView滑動剩餘的距離
            val childRecyclerView = findNestedScrollingChildRecyclerView()
            childRecyclerView?.run {
                val deltaY = (lastY - e.y).toInt()
                if(deltaY != 0) {
                    scrollBy(0,deltaY)
                }
            }
        }
        lastY = e.y
        return try {
            super.onTouchEvent(e)
        } catch (e: Exception) {
            e.printStackTrace()
            false
        }
    }
複製代碼

關於滑動事件主要代碼就是上面這些,具體能夠能夠看看項目代碼NestedRecyclerView

還有關於RecyclerView的fling部分,在RecyclerView的onScrollStateChanged回調中監聽y軸的總的偏移量totalDy,而後在RecyclerView不滾動的時候交由內部或者外部RecyclerView去fling,這裏就不贅述了,具體能夠看項目的代碼。

最後貼上項目的運行gif圖展現:

image

image

最後

寫做不易,歡迎你們點贊,若是有問題,歡迎提出一塊兒討論,您的點贊是我寫做的最大動力,感謝!

相關文章
相關標籤/搜索