【五種方式實現Android吸頂效果 最全總結!】列表滑動到頂部 固定頂部欄效果

如今許多app都會應用到的一種UI交互形式,列表滑動到頂部,固定頂部欄效果,我們也可以稱作其為吸頂效果。比如微博 、各大瀏覽器的首頁信息流模塊、我的頁面的設計等。


微博評論的吸頂效果

本文將循序漸進的通過多種方式實現吸頂效果。大家擇優選取適合自己的實現方式。 實現效果如圖:

demo實現

一、兩個相同的頂部欄

寫兩個一模一樣的固定懸浮欄,在一開始把外層固定欄先隱藏,當內層固定欄滑動到外層固定位置時,把內層固定欄隱藏,外層固定欄顯示。
頭部+內層懸浮欄+list 組成了scrollview

主要代碼 監聽scrollview的滑動,隱藏顯示內外懸浮窗

 scrollView.setScrollChangeListener(new MyScrollView.ScrollChangedListener() {
            @Override
            public void onScrollChangedListener(int x, int y, int oldX, int oldY) {
                if (y >= topHeight) {
                    //重點 通過距離變化隱藏內外固定欄實現
                    llOutsideFixed.setVisibility(View.VISIBLE);
                    insideFixedBar.setVisibility(View.GONE);
                    recyclerView.setNestedScrollingEnabled(true);
                } else {
                    llOutsideFixed.setVisibility(View.GONE);
                    insideFixedBar.setVisibility(View.VISIBLE);
                    recyclerView.setNestedScrollingEnabled(false);
                }
            }
        });

二、通過ListView

通過listview添加頭部,當listview滑動到頂部將原本隱藏的頭部布局顯示出來。
 listView.setOnScrollListener(new AbsListView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {
            }
            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
                /* 判斷ListView頭部(mHeaderView)當前是否可見
                 * 來決定隱藏或顯示浮動欄(mFloatBar)*/
                if (firstVisibleItem >= 1) {
                    flOutSideBar.setVisibility(View.VISIBLE);
                } else {
                    flOutSideBar.setVisibility(View.GONE);
                }
            }
        });

這種方式需要寫重復布局,事件監聽,當固定布局帶有狀態時,還要將兩個狀態同這種方式實現的根本其實也是很方式一相同,也需要引入兩個相同的頂部固定欄,相比方式一不同的是:
  • 方式二滑動監聽通過listview自帶的setOnScrollListener即可,方式一需要暴露接口提供滑動位移變化值。
  • 當存在滑動的view時,方式二不需要處理沖突,方式一需要沖突處理。
  • 布局的引入:外部懸浮窗和頭部布局,listview通過addHeaderView引入即可。管理起來方便。
方式一和方式二的缺點就是:
  • 需要寫兩個相同的xml文件 以及重復寫相應點擊事件的邏輯。
  • 邏輯復雜時,需要同步固定懸浮窗的狀態,在業務發生變化的時候可能需要同時去改動至少兩處代碼,增加出錯的概率。

三、使用一個頂部欄 用一個空布局動態增刪頂部欄來實現。

這種方式的實現方式就是對第一種實現方式的簡單優化,其他基本一致。

大體思路:將方式一的兩個頂部欄變成一個,利用removeView和addView根據坐標點在頁面滑動的時候動態的把固定欄在內外部切換。在scrollview外部添加一個空的layout,當滑動到指定的點,就將內層懸浮窗布局移除,添加到外層的空的布局。這樣就解決了要同步狀態和寫兩個相同的xml布局的問題了。

 scrollView.setScrollChangeListener(new MyScrollView.ScrollChangedListener() {
            @Override
            public void onScrollChangedListener(int x, int y, int oldX, int oldY) {
                if (y >= topHeight) {
                    if (rlInsideFixed.getParent() != llFixed) {
                        insideFixedBarParent.removeView(rlInsideFixed);
                        llFixed.addView(rlInsideFixed);
                        recyclerView.setNestedScrollingEnabled(true);

                    }
                } else {
                    if (rlInsideFixed.getParent() != insideFixedBarParent) {
                        llFixed.removeView(rlInsideFixed);
                        insideFixedBarParent.addView(rlInsideFixed);
                        recyclerView.setNestedScrollingEnabled(false);
                    }
                }
            }
        });

方式三是動態的增加和移除view,缺點是當包裹內容布局中帶有滑動特性的View(ListView,RecyclerView等),我們需要額外處理滑動沖突,并且這種包裹方式,會使得它們的緩存模式失效。

四、借助android5.0的新特性 CoordinatorLayout+AppbarLayout+ CollapsingToolbarLayout

首先要使用android5.0的material design風格 我們需要引入以下依賴

    implementation 'com.android.support:design:x.+'

然后依次介紹這幾個UI的功能

  1. CoordinatorLayout 頂層布局 類似relativelayout、linearlayout等,不同的是它可以協調子view之間的交互。產生聯動的效果。子view通過app:layout_behavior 指定相應的行為。
  2. AppBarLayout 是一個垂直布局的 LinearLayout,它主要是為了實現 “Material Design” 風格的標題欄的特性,比如:滾動??梢皂憫脩舻氖謩莶僮鳎潜仨氃贑oordinatorLayout下使用,否則會有許多功能使用不了。
    AppBarLayout里面的View,是通過app:layout_scrollFlags屬性來控制滑動,其中有4種Flag的類型.
  • Scroll:向下滾動時,被指定了這個屬性的View會被滾出屏幕范圍直到完全不可見的位置。
  • enterAlways:向上滾動時,這個View會隨著滾動手勢出現,直到恢復原來的位置。
  • enterAlwaysCollapsed: 當視圖已經設置minHeight屬性又使用此標志時,視圖-只能以最小高度進入,只有當滾動視圖到達頂部時才擴大到完整高度。
  • exitUntilCollapsed: 滾動退出屏幕,最后折疊在頂端。
  1. CollapsingToolbarLayout 折疊布局 用來協調AppBarLayout來實現滾動隱藏ToolBar的效果。繼承自 FrameLayout,它是用來實現 Toolbar 的折疊效果,一般它的直接子 View 是 Toolbar,當然也可以是其它類型的 View。通過設置layout_collapseMode 控制折疊屬性 。(官方說CollapsingToolbarLayout主要是配合Toolbar而設計的。但如果我們不需要 也可以不加toolbar。只不過在需要toolbar的時候配合CollapsingToolbarLayout效果更佳。)
  • 不設置 跟隨NestedScrollView的滑動一起滑動,NestedScrollView滑動多少距離他就會跟著走多少距離
  • parallax 視差效果 layout_collapseParallaxMultiplier視差因子 0~1之間取值
  • pin 固定效果,在折疊的時候最后固定在頂端。在滑動過程中,此自布局會固定在它所在的位置不動,直到CollapsingToolbarLayout全部折疊或者全部展開。


<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fitsSystemWindows="true">


        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:fitsSystemWindows="true"
            app:layout_scrollFlags="scroll|exitUntilCollapsed"
            app:statusBarScrim="@android:color/transparent">

            <include layout="@layout/header" />

        </android.support.design.widget.CollapsingToolbarLayout>

        <include layout="@layout/inside_fixed_bar" />
    </android.support.design.widget.AppBarLayout>

    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#d2ebaf"/>

    </android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>

這種方式是最推薦的。但這個既不用處理滑動沖突,也不會有緩存問題。使用起來也很流暢。

五、 通過重寫RecyclerView的分割線ItemDecoration來實現。

ItemDecoration是RecyclerView下的抽象方法,允許給特定的item視圖添加特性的繪制以及布局間隔。它可以用來實現item之間的分割線,高亮,分組邊界等。三個重要的方法:getItemOffsets、onDraw、onDrawOver(自行了解)

實現思路:比如我們之前放的微博評論的吸頂效果圖,首先是微博內容,我們把它當成是RecyclerView的HeaderView即可,也是Item的一項,然后下面的評論列表就是基礎的RecyclerView使用了,然后中間固定的布局,就是ItemDecoration里的getItemOffsets、onDraw、onDrawOver這三個方法來配合實現了。在onDraw方法里判斷是否是列表的第一項 除了頭部布局,如果是就繪制頂部欄,不是,繪制分割線。在onDrawOver里判斷是否是頭部布局,如果是不做處理,不是就在視圖可見的第一項上繪制頂部欄。getItemOffsets是繪制的邊距,也是分是不是頭部項的情況去判斷。如果我們只想簡單的繪制分割線,getItemOffsets讓item之間空出間隙,然后再調用onDraw在這個間隙上填充顏色即可。
public class FixedBarDecoration extends RecyclerView.ItemDecoration {

    private int mItemHeaderHeight;
    private Paint mLinePaint;
    private Paint mItemHeaderPaint;
    private Paint mTextPaint;
    private Rect mTextRect;

    public FixedBarDecoration(Context context) {
        
        mItemHeaderHeight = ViewUtils.dip2px(context, 40);

        mTextRect = new Rect();
        mItemHeaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mItemHeaderPaint.setColor(Color.BLUE);

        mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mLinePaint.setColor(Color.GRAY);

        mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setTextSize(46);
        mTextPaint.setColor(Color.WHITE);
    }


    //吸頂效果的主要實現方法
    @Override
    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        if (parent.getAdapter() instanceof NormalAdapter) {
            NormalAdapter adapter = (NormalAdapter) parent.getAdapter();
            int position = ((LinearLayoutManager) (parent.getLayoutManager())).findFirstVisibleItemPosition();
            if (adapter.isHasHeader() && position == 0) {
                return;
            }
            //如果不是頭部view 那就直接在當前第一個可見的item頂部畫一個固定欄即可
//            View view = parent.findViewHolderForAdapterPosition(position).itemView;
            c.drawRect(0, 0, parent.getWidth(), mItemHeaderHeight, mItemHeaderPaint);
            mTextPaint.getTextBounds("懸浮固定欄", 0, "懸浮固定欄".length(), mTextRect);
            c.drawText("懸浮固定欄", parent.getWidth() / 2 - mTextRect.width() / 2, mItemHeaderHeight / 2 + mTextRect.height() / 2, mTextPaint);

        }
    }

    //繪制分割線和固定欄
    @Override
    public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        if (parent.getAdapter() instanceof NormalAdapter) {
            NormalAdapter adapter = (NormalAdapter) parent.getAdapter();
            int count = parent.getChildCount();
            for (int i = 0; i < count; i++) {
                View view = parent.getChildAt(i);
                int position = parent.getChildLayoutPosition(view);
                boolean isFirstItem = adapter.isFirstItem(position);
                if (isFirstItem) {
                    c.drawRect(0, view.getTop() - mItemHeaderHeight, parent.getWidth(), view.getTop(), mItemHeaderPaint);
                    mTextPaint.getTextBounds("懸浮固定欄", 0, "懸浮固定欄".length(), mTextRect);
                    c.drawText("懸浮固定欄", parent.getWidth() / 2 - mTextRect.width() / 2, (view.getTop() - mItemHeaderHeight) + mItemHeaderHeight / 2 + mTextRect.height() / 2, mTextPaint);
                } else {
                    c.drawRect(0, view.getTop() - 1, parent.getWidth(), view.getTop(), mLinePaint);
                }
            }
        }
    }

    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        if (parent.getAdapter() instanceof NormalAdapter) {
            NormalAdapter adapter = (NormalAdapter) parent.getAdapter();
            int position = parent.getChildLayoutPosition(view);
            boolean isFirstItem = adapter.isFirstItem(position);
            if (isFirstItem) {
                outRect.top = mItemHeaderHeight;
            } else {
                outRect.top = 1;
            }

        }
    }

}

這種方式的缺點就是如果頂部欄的布局復雜,難以繪制,以及頂部欄的監聽事件添加復雜。

六、擴展:分組加吸頂效果

思路:當我們要實現分組+吸頂效果,為了實現頂部欄固定不動,可以利用onDrawOver在RecyclerView的上繪制一個和頭部布局一模一樣的布局呢,讓它覆蓋住了第一個頭布局,在視覺上我們是不會有所察覺的,然后當列表滑動的時候,其實“原來的頭布局”早已經滑動走了,留下的其實是我們繪制的固定布局而已,等到下一個頭部布局“碰頭”的時候,讓它隨著滑動的速度慢慢改變布局的高度,當布局高度為0的時候,也就是被頂出去的時候,然后再讓高度改變回來,覆蓋住第二個布局,然后不斷重復以上步驟即可。
參考文章吸頂+分組效果的實現
第五和第六效果圖較大,可從下方github鏈接查看詳情

參考文章:
View事件體系之View坐標系圖示理解
coordinatorLayout使用總結篇,看完這篇完全可以開發5.0的高級特效了

最重要的源碼地址點這里

給個愛心贊??

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容