【五種方式實現(xiàn)Android吸頂效果 最全總結(jié)!】列表滑動到頂部 固定頂部欄效果

如今許多app都會應(yīng)用到的一種UI交互形式夸研,列表滑動到頂部邦蜜,固定頂部欄效果,我們也可以稱作其為吸頂效果亥至。比如微博 悼沈、各大瀏覽器的首頁信息流模塊、我的頁面的設(shè)計等姐扮。


微博評論的吸頂效果

本文將循序漸進的通過多種方式實現(xiàn)吸頂效果絮供。大家擇優(yōu)選取適合自己的實現(xiàn)方式。 實現(xiàn)效果如圖:

demo實現(xiàn)

一茶敏、兩個相同的頂部欄

寫兩個一模一樣的固定懸浮欄壤靶,在一開始把外層固定欄先隱藏,當(dāng)內(nèi)層固定欄滑動到外層固定位置時惊搏,把內(nèi)層固定欄隱藏贮乳,外層固定欄顯示。
頭部+內(nèi)層懸浮欄+list 組成了scrollview

主要代碼 監(jiān)聽scrollview的滑動恬惯,隱藏顯示內(nèi)外懸浮窗

 scrollView.setScrollChangeListener(new MyScrollView.ScrollChangedListener() {
            @Override
            public void onScrollChangedListener(int x, int y, int oldX, int oldY) {
                if (y >= topHeight) {
                    //重點 通過距離變化隱藏內(nèi)外固定欄實現(xiàn)
                    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添加頭部,當(dāng)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)當(dāng)前是否可見
                 * 來決定隱藏或顯示浮動欄(mFloatBar)*/
                if (firstVisibleItem >= 1) {
                    flOutSideBar.setVisibility(View.VISIBLE);
                } else {
                    flOutSideBar.setVisibility(View.GONE);
                }
            }
        });

這種方式需要寫重復(fù)布局亲铡,事件監(jiān)聽,當(dāng)固定布局帶有狀態(tài)時葡兑,還要將兩個狀態(tài)同這種方式實現(xiàn)的根本其實也是很方式一相同奖蔓,也需要引入兩個相同的頂部固定欄,相比方式一不同的是:
  • 方式二滑動監(jiān)聽通過listview自帶的setOnScrollListener即可讹堤,方式一需要暴露接口提供滑動位移變化值吆鹤。
  • 當(dāng)存在滑動的view時,方式二不需要處理沖突洲守,方式一需要沖突處理疑务。
  • 布局的引入:外部懸浮窗和頭部布局,listview通過addHeaderView引入即可梗醇。管理起來方便知允。
方式一和方式二的缺點就是:
  • 需要寫兩個相同的xml文件 以及重復(fù)寫相應(yīng)點擊事件的邏輯。
  • 邏輯復(fù)雜時叙谨,需要同步固定懸浮窗的狀態(tài)温鸽,在業(yè)務(wù)發(fā)生變化的時候可能需要同時去改動至少兩處代碼,增加出錯的概率。

三涤垫、使用一個頂部欄 用一個空布局動態(tài)增刪頂部欄來實現(xiàn)姑尺。

這種方式的實現(xiàn)方式就是對第一種實現(xiàn)方式的簡單優(yōu)化,其他基本一致蝠猬。

大體思路:將方式一的兩個頂部欄變成一個切蟋,利用removeView和addView根據(jù)坐標點在頁面滑動的時候動態(tài)的把固定欄在內(nèi)外部切換。在scrollview外部添加一個空的layout榆芦,當(dāng)滑動到指定的點柄粹,就將內(nèi)層懸浮窗布局移除,添加到外層的空的布局歧杏。這樣就解決了要同步狀態(tài)和寫兩個相同的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);
                    }
                }
            }
        });

方式三是動態(tài)的增加和移除view迷守,缺點是當(dāng)包裹內(nèi)容布局中帶有滑動特性的View(ListView犬绒,RecyclerView等),我們需要額外處理滑動沖突兑凿,并且這種包裹方式凯力,會使得它們的緩存模式失效

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

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

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

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

  1. CoordinatorLayout 頂層布局 類似relativelayout咐鹤、linearlayout等,不同的是它可以協(xié)調(diào)子view之間的交互圣絮。產(chǎn)生聯(lián)動的效果祈惶。子view通過app:layout_behavior 指定相應(yīng)的行為。
  2. AppBarLayout 是一個垂直布局的 LinearLayout扮匠,它主要是為了實現(xiàn) “Material Design” 風(fēng)格的標題欄的特性捧请,比如:滾動“羲眩可以響應(yīng)用戶的手勢操作疹蛉,但是必須在CoordinatorLayout下使用,否則會有許多功能使用不了力麸。
    AppBarLayout里面的View可款,是通過app:layout_scrollFlags屬性來控制滑動,其中有4種Flag的類型.
  • Scroll:向下滾動時,被指定了這個屬性的View會被滾出屏幕范圍直到完全不可見的位置克蚂。
  • enterAlways:向上滾動時,這個View會隨著滾動手勢出現(xiàn),直到恢復(fù)原來的位置闺鲸。
  • enterAlwaysCollapsed: 當(dāng)視圖已經(jīng)設(shè)置minHeight屬性又使用此標志時,視圖-只能以最小高度進入埃叭,只有當(dāng)滾動視圖到達頂部時才擴大到完整高度摸恍。
  • exitUntilCollapsed: 滾動退出屏幕,最后折疊在頂端游盲。
  1. CollapsingToolbarLayout 折疊布局 用來協(xié)調(diào)AppBarLayout來實現(xiàn)滾動隱藏ToolBar的效果误墓。繼承自 FrameLayout蛮粮,它是用來實現(xiàn) Toolbar 的折疊效果,一般它的直接子 View 是 Toolbar谜慌,當(dāng)然也可以是其它類型的 View然想。通過設(shè)置layout_collapseMode 控制折疊屬性 。(官方說CollapsingToolbarLayout主要是配合Toolbar而設(shè)計的欣范。但如果我們不需要 也可以不加toolbar变泄。只不過在需要toolbar的時候配合CollapsingToolbarLayout效果更佳。)
  • 不設(shè)置 跟隨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來實現(xiàn)。

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

實現(xiàn)思路:比如我們之前放的微博評論的吸頂效果圖堂鲜,首先是微博內(nèi)容栈雳,我們把它當(dāng)成是RecyclerView的HeaderView即可,也是Item的一項泡嘴,然后下面的評論列表就是基礎(chǔ)的RecyclerView使用了甫恩,然后中間固定的布局,就是ItemDecoration里的getItemOffsets、onDraw酌予、onDrawOver這三個方法來配合實現(xiàn)了磺箕。在onDraw方法里判斷是否是列表的第一項 除了頭部布局,如果是就繪制頂部欄抛虫,不是松靡,繪制分割線。在onDrawOver里判斷是否是頭部布局建椰,如果是不做處理雕欺,不是就在視圖可見的第一項上繪制頂部欄。getItemOffsets是繪制的邊距,也是分是不是頭部項的情況去判斷屠列。如果我們只想簡單的繪制分割線啦逆,getItemOffsets讓item之間空出間隙,然后再調(diào)用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);
    }


    //吸頂效果的主要實現(xiàn)方法
    @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 那就直接在當(dāng)前第一個可見的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;
            }

        }
    }

}

這種方式的缺點就是如果頂部欄的布局復(fù)雜夏志,難以繪制,以及頂部欄的監(jiān)聽事件添加復(fù)雜苛让。

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

思路:當(dāng)我們要實現(xiàn)分組+吸頂效果,為了實現(xiàn)頂部欄固定不動狱杰,可以利用onDrawOver在RecyclerView的上繪制一個和頭部布局一模一樣的布局呢瘦材,讓它覆蓋住了第一個頭布局,在視覺上我們是不會有所察覺的仿畸,然后當(dāng)列表滑動的時候食棕,其實“原來的頭布局”早已經(jīng)滑動走了,留下的其實是我們繪制的固定布局而已颁湖,等到下一個頭部布局“碰頭”的時候宣蠕,讓它隨著滑動的速度慢慢改變布局的高度例隆,當(dāng)布局高度為0的時候甥捺,也就是被頂出去的時候,然后再讓高度改變回來镀层,覆蓋住第二個布局镰禾,然后不斷重復(fù)以上步驟即可。
參考文章吸頂+分組效果的實現(xiàn)
第五和第六效果圖較大唱逢,可從下方github鏈接查看詳情

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

最重要的源碼地址點這里

給個愛心贊??

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市坞古,隨后出現(xiàn)的幾起案子备韧,更是在濱河造成了極大的恐慌,老刑警劉巖痪枫,帶你破解...
    沈念sama閱讀 219,366評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件织堂,死亡現(xiàn)場離奇詭異,居然都是意外死亡奶陈,警方通過查閱死者的電腦和手機易阳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吃粒,“玉大人潦俺,你說我怎么就攤上這事。” “怎么了事示?”我有些...
    開封第一講書人閱讀 165,689評論 0 356
  • 文/不壞的土叔 我叫張陵早像,是天一觀的道長。 經(jīng)常有香客問我肖爵,道長扎酷,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,925評論 1 295
  • 正文 為了忘掉前任遏匆,我火速辦了婚禮法挨,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘幅聘。我一直安慰自己凡纳,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,942評論 6 392
  • 文/花漫 我一把揭開白布帝蒿。 她就那樣靜靜地躺著荐糜,像睡著了一般。 火紅的嫁衣襯著肌膚如雪葛超。 梳的紋絲不亂的頭發(fā)上暴氏,一...
    開封第一講書人閱讀 51,727評論 1 305
  • 那天,我揣著相機與錄音绣张,去河邊找鬼答渔。 笑死,一個胖子當(dāng)著我的面吹牛侥涵,可吹牛的內(nèi)容都是我干的沼撕。 我是一名探鬼主播,決...
    沈念sama閱讀 40,447評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼芜飘,長吁一口氣:“原來是場噩夢啊……” “哼务豺!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起嗦明,我...
    開封第一講書人閱讀 39,349評論 0 276
  • 序言:老撾萬榮一對情侶失蹤笼沥,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后娶牌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體奔浅,經(jīng)...
    沈念sama閱讀 45,820評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,990評論 3 337
  • 正文 我和宋清朗相戀三年裙戏,在試婚紗的時候發(fā)現(xiàn)自己被綠了乘凸。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,127評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡累榜,死狀恐怖营勤,靈堂內(nèi)的尸體忽然破棺而出灵嫌,到底是詐尸還是另有隱情,我是刑警寧澤葛作,帶...
    沈念sama閱讀 35,812評論 5 346
  • 正文 年R本政府宣布寿羞,位于F島的核電站,受9級特大地震影響赂蠢,放射性物質(zhì)發(fā)生泄漏绪穆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,471評論 3 331
  • 文/蒙蒙 一虱岂、第九天 我趴在偏房一處隱蔽的房頂上張望玖院。 院中可真熱鬧,春花似錦第岖、人聲如沸难菌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽郊酒。三九已至,卻和暖如春键袱,著一層夾襖步出監(jiān)牢的瞬間燎窘,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評論 1 272
  • 我被黑心中介騙來泰國打工蹄咖, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留褐健,地道東北人。 一個月前我還...
    沈念sama閱讀 48,388評論 3 373
  • 正文 我出身青樓比藻,卻偏偏與公主長得像铝量,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子银亲,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,066評論 2 355

推薦閱讀更多精彩內(nèi)容