如今許多app都會應(yīng)用到的一種UI交互形式夸研,列表滑動到頂部邦蜜,固定頂部欄效果,我們也可以稱作其為吸頂效果亥至。比如微博 悼沈、各大瀏覽器的首頁信息流模塊、我的頁面的設(shè)計等姐扮。
微博評論的吸頂效果
本文將循序漸進的通過多種方式實現(xiàn)吸頂效果絮供。大家擇優(yōu)選取適合自己的實現(xiàn)方式。 實現(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的功能
- CoordinatorLayout 頂層布局 類似relativelayout咐鹤、linearlayout等,不同的是它可以協(xié)調(diào)子view之間的交互圣絮。產(chǎn)生聯(lián)動的效果祈惶。子view通過app:layout_behavior 指定相應(yīng)的行為。
- 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: 滾動退出屏幕,最后折疊在頂端游盲。
- 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的高級特效了