Android模仿實(shí)現(xiàn)微博詳情頁滑動(dòng)固定頂部欄的效果實(shí)例
前言
最近項(xiàng)目中遇到一個(gè)需求,類似微博詳情頁的效果,通過查找相關(guān)的資料終于找了對(duì)應(yīng)的解決方案,分享出來供大家參考學(xué)習(xí),下面話不多說了,來一起看看詳細(xì)的介紹吧。
先來看下我們今天要實(shí)現(xiàn)的效果:
滑動(dòng)固定頂部欄效果圖
這段時(shí)間公司準(zhǔn)備重構(gòu)一個(gè)項(xiàng)目,剛好用到這個(gè)效果,我就順帶寫了篇文章,關(guān)于這個(gè)效果網(wǎng)上可以找到一些相關(guān)資料的,昨晚看了一些,感覺都不是很好,有點(diǎn)模棱兩可的樣子,也沒提到需要注意的一些關(guān)鍵點(diǎn),這里來做下整理,由于涉及到公司的代碼,這里我就寫個(gè)簡單的Demo來講解。
簡單Demo
傳統(tǒng)套路:
寫兩個(gè)一模一樣的固定欄,外層用幀布局(FrameLayout)包裹,然后把外層的固定欄先隱藏,當(dāng)內(nèi)層的固定欄滑動(dòng)到外層固定欄位置的時(shí)候,把內(nèi)層固定欄隱藏,外層的固定欄顯示,反之滑回來的時(shí)候把外層固定欄隱藏,內(nèi)存固定欄顯示。
傳統(tǒng)套路圖
這樣做的有幾個(gè)不好的地方:
1、重復(fù)寫了一樣的布局,在XML渲染的時(shí)候耗費(fèi)了性能(比如更多次的測(cè)量,布局等)
2、當(dāng)頁面快速滾動(dòng)的時(shí)候可能出現(xiàn)一系列的問題(布局重復(fù),閃爍)
3、當(dāng)這個(gè)固定布局帶有狀態(tài)的時(shí)候,邏輯會(huì)變得很復(fù)雜,比如上面那張GIF動(dòng)圖,固定欄中帶有篩選分類,地區(qū),年月信息,如果按照傳統(tǒng)套路來寫,那么在內(nèi)層固定欄隱藏的時(shí)候需要把狀態(tài)記錄并且?guī)Ыo外層固定欄,而且相對(duì)應(yīng)很多動(dòng)作監(jiān)聽事件也需要寫多次。
新套路:
這里我換了一種思路,大體布局還是不變的,只是把兩個(gè)固定欄簡化成了一個(gè),只是利用removeView和addView根據(jù)坐標(biāo)點(diǎn)在頁面滑動(dòng)的時(shí)候動(dòng)態(tài)的把固定欄在內(nèi)外部切換,這樣做的好處很好的解決了上面提到的1、2點(diǎn)問題,當(dāng)然在快速的removeView和addView還是會(huì)出現(xiàn)頁面閃爍不自然的問題,后面會(huì)提到解決的小竅門。
先來看下XML布局:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <com.lcw.view.FixedHeaderScrollView.ObservableScrollView android:id="@+id/sv_contentView" android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="none" > <LinearLayout android:id="@+id/ll_contentView" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:id="@+id/tv_headerView" android:layout_width="match_parent" android:layout_height="200dp" android:text="我是頭部布局" android:textSize="30sp" android:background="#ad29e1" android:gravity="center"/> <LinearLayout android:id="@+id/ll_topView" android:layout_width="match_parent" android:layout_height="50dp" android:gravity="center" android:orientation="vertical"> <TextView android:id="@+id/tv_topView" android:layout_width="match_parent" android:layout_height="50dp" android:text="我是內(nèi)層固定的布局" android:background="#3be42f" android:textSize="30sp" android:gravity="center"/> </LinearLayout> <TextView android:id="@+id/tv_contentView" android:layout_width="match_parent" android:layout_height="1000dp" android:text="我是內(nèi)容布局" android:textSize="30sp" android:background="#dc7f28" android:paddingTop="160dp" android:gravity="top|center_horizontal"/> </LinearLayout> </com.lcw.view.FixedHeaderScrollView.ObservableScrollView> <LinearLayout android:id="@+id/ll_fixedView" android:layout_width="match_parent" android:layout_height="50dp" android:orientation="vertical"/> </FrameLayout>
這里和上面提到的一樣,最外層用了FrameLayout(RelativeLayout也可以)包裹著一個(gè)ScrollView和一個(gè)LinearLayout,當(dāng)我們頁面滑動(dòng)到指定點(diǎn)的時(shí)候,需要把內(nèi)層的“我是內(nèi)層固定布局”移除,同時(shí)添加到外層的ViewGroup(LinearLayout)中。
自定義ScrollView,利用回調(diào)接口的方式使滑動(dòng)數(shù)據(jù)對(duì)外暴露:
雖然谷歌官方給ScrollView提供了一個(gè)設(shè)置滑動(dòng)監(jiān)聽方法setOnScrollChangeListener,不過這個(gè)方法需要基于API23之上(Android6.0系統(tǒng)),在日常開發(fā)中,我們需要對(duì)老系統(tǒng)用戶進(jìn)行兼容(當(dāng)前兼容版本為Android4.1系統(tǒng)以上),所以這里我們需要去繼承ScrollView并把這個(gè)監(jiān)聽事件通過接口的方式對(duì)外暴露,這里把這個(gè)View取名為ObservableScrollView。
package com.lcw.view.FixedHeaderScrollView; import android.content.Context; import android.util.AttributeSet; import android.widget.ScrollView; /** * 監(jiān)聽ScrollView的滑動(dòng)數(shù)據(jù) * Create by: chenwei.li * Date: 2017/8/21 * time: 11:36 * Email: lichenwei.me@foxmail.com */ public class ObservableScrollView extends ScrollView{ public ObservableScrollView(Context context) { this(context,null); } public ObservableScrollView(Context context, AttributeSet attrs) { this(context, attrs,0); } public ObservableScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } private OnObservableScrollViewScrollChanged mOnObservableScrollViewScrollChanged; public void setOnObservableScrollViewScrollChanged(OnObservableScrollViewScrollChanged mOnObservableScrollViewScrollChanged) { this.mOnObservableScrollViewScrollChanged = mOnObservableScrollViewScrollChanged; } public interface OnObservableScrollViewScrollChanged{ void onObservableScrollViewScrollChanged(int l, int t, int oldl, int oldt); } /** * @param l Current horizontal scroll origin. 當(dāng)前滑動(dòng)的x軸距離 * @param t Current vertical scroll origin. 當(dāng)前滑動(dòng)的y軸距離 * @param oldl Previous horizontal scroll origin. 上一次滑動(dòng)的x軸距離 * @param oldt Previous vertical scroll origin. 上一次滑動(dòng)的y軸距離 */ @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); if(mOnObservableScrollViewScrollChanged!=null){ mOnObservableScrollViewScrollChanged.onObservableScrollViewScrollChanged(l,t,oldl,oldt); } } }
這里就可以開始寫我們的調(diào)用類了
package com.lcw.view.FixedHeaderScrollView; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.widget.LinearLayout; import android.widget.TextView; public class MainActivity extends AppCompatActivity implements ObservableScrollView.OnObservableScrollViewScrollChanged{ private ObservableScrollView sv_contentView; private LinearLayout ll_topView; private TextView tv_topView; private LinearLayout ll_fixedView; //用來記錄內(nèi)層固定布局到屏幕頂部的距離 private int mHeight; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); sv_contentView= (ObservableScrollView) findViewById(R.id.sv_contentView); ll_topView= (LinearLayout) findViewById(R.id.ll_topView); tv_topView= (TextView) findViewById(R.id.tv_topView); ll_fixedView= (LinearLayout) findViewById(R.id.ll_fixedView); sv_contentView.setOnObservableScrollViewScrollChanged(this); // ViewTreeObserver viewTreeObserver=ll_topView.getViewTreeObserver(); // viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { // @Override // public void onGlobalLayout() { // ll_topView.getViewTreeObserver().removeOnGlobalLayoutListener(this); // mHeight=ll_topView.getTop(); // } // }); } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if(hasFocus){ //獲取HeaderView的高度,當(dāng)滑動(dòng)大于等于這個(gè)高度的時(shí)候,需要把topView移除當(dāng)前布局,放入到外層布局 mHeight=ll_topView.getTop(); } } /** * @param l Current horizontal scroll origin. 當(dāng)前滑動(dòng)的x軸距離 * @param t Current vertical scroll origin. 當(dāng)前滑動(dòng)的y軸距離 * @param oldl Previous horizontal scroll origin. 上一次滑動(dòng)的x軸距離 * @param oldt Previous vertical scroll origin. 上一次滑動(dòng)的y軸距離 */ @Override public void onObservableScrollViewScrollChanged(int l, int t, int oldl, int oldt) { if(t>=mHeight){ if(tv_topView.getParent()!=ll_fixedView){ ll_topView.removeView(tv_topView); ll_fixedView.addView(tv_topView); } }else{ if(tv_topView.getParent()!=ll_topView){ ll_fixedView.removeView(tv_topView); ll_topView.addView(tv_topView); } } } }
這里我們實(shí)現(xiàn)了ObservableScrollView.OnObservableScrollViewScrollChanged接口,當(dāng)我們對(duì)ScrollView注冊(cè)監(jiān)聽的時(shí)候,就可以在回調(diào)接口里拿到對(duì)應(yīng)的滑動(dòng)數(shù)據(jù),其中第二個(gè)參數(shù)t就是滑動(dòng)y軸的距離,現(xiàn)在我們只需要拿到固定布局到頂部的距離就可以判斷什么時(shí)候需要移除和添加View了。
相關(guān)講解:
1、首先我們需要知道,在Activity生命周期里的onCreate方法里對(duì)一個(gè)View去執(zhí)行g(shù)etWidth,getHeight,getTop,getBottom等一系列的方法是拿不到數(shù)據(jù)的,得到的結(jié)果都為0,由于此時(shí)Activity還沒有得到焦點(diǎn),依附在Activity的View自然也就得不到數(shù)據(jù),所以我們需要在onResume后去進(jìn)行對(duì)View的數(shù)據(jù)獲取。
這里我們可以通過onGlobalLayoutListener或者onWidnowFocusChanged等方法去獲取,這里的執(zhí)行順序是:Activity.onCreate->Activity.onResume->View.onMeasure->View.onLayout->onGlobalLayoutListener->Activity.onWidnowFocusChanged..(具體用哪個(gè),看當(dāng)前環(huán)境情況,比如在Fragment里是沒有onWidnowFocusChanged,如果需要獲取一個(gè)View的相關(guān)數(shù)據(jù),就可以根據(jù)onGlobalLayoutListener來做,上面代碼提供兩種示例)
2、關(guān)于獲取滑動(dòng)的高度,首先我們來看一張圖:
Andorid里關(guān)于View的坐標(biāo)系
這里需要注意的是,除了getRawX和getRawY是相對(duì)屏幕的位置,其他的是相對(duì)應(yīng)所在父布局的位置,所以在確定數(shù)據(jù)的時(shí)候,需要注意布局的嵌套。
3、當(dāng)我們拿到所需要滑動(dòng)的高度時(shí),我們需要對(duì)固定布局進(jìn)行臨界值做判斷(這里設(shè)當(dāng)前滑動(dòng)值為t,所需滑動(dòng)值為y)
比如當(dāng)我們界面一開始向上滑的時(shí)候t值是小于y值的,此時(shí)內(nèi)部固定欄是不需要移除的,而當(dāng)我們超過y值往回滑t值又小于y值的時(shí)候,此時(shí)內(nèi)部固定欄是需要從外部移除添加到內(nèi)部的,所以這里我們需要對(duì)固定欄所在的父布局(ViewGroup)做判斷。
最后補(bǔ)充:
微博詳情頁
1、不管你的頂部固定欄布局多簡單,建議在外套一層ViewGroup,這樣方便addView的操作,不然需要去控制外層ViewGroup的addView的index位置。
2、確定View的寬高度數(shù)據(jù)可以借助onGlobalLayoutListener或者onWidnowFocusChanged來做,注意相對(duì)父布局的嵌套。
3、這種頁面的設(shè)計(jì)最早來源于iOS的設(shè)計(jì),在iOS里ScrollView嵌套TableView(相當(dāng)于ListView)是沒有問題的,但是在Android里,這樣子的嵌套會(huì)導(dǎo)致ListView的復(fù)用機(jī)制作廢,也就是會(huì)不斷是去進(jìn)行onMeasure的計(jì)算,執(zhí)行多次Adapter里的getView,也就意味著多次的findViewById,使得ViewHolder失效。
4、這是個(gè)小技巧,在快速滑動(dòng)的時(shí)候有些人會(huì)出現(xiàn)固定布局的閃爍,其實(shí)這個(gè)和removeView和addView有關(guān)系,如果你的ViewGroup設(shè)置成了warp_content,這是一個(gè)測(cè)量的耗時(shí)操作,這里只需要配合上面提到的第1點(diǎn),給固定欄外層布局一個(gè)固定的高度值即可(與固定欄高度保持一致)。
好了,到這里就結(jié)束。
源碼下載:
github源碼地址:源碼下載
本地下載:點(diǎn)擊這里
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
- Android實(shí)現(xiàn)滑動(dòng)到頂部懸停的效果
- Android實(shí)現(xiàn)頂部導(dǎo)航菜單左右滑動(dòng)效果
- Android 頂部標(biāo)題欄隨滑動(dòng)時(shí)的漸變隱藏和漸變顯示效果
- Android滑動(dòng)組件懸浮固定在頂部效果
- android scrollview 滑動(dòng)到頂端或者指定位置的實(shí)現(xiàn)方法
- Android仿淘寶view滑動(dòng)至屏幕頂部會(huì)一直停留在頂部的位置
- Android實(shí)現(xiàn)頂部導(dǎo)航欄可點(diǎn)擊可滑動(dòng)效果(仿微信仿豆瓣網(wǎng))
- Android實(shí)現(xiàn)listview滑動(dòng)時(shí)漸隱漸現(xiàn)頂部欄實(shí)例代碼
- Android滑動(dòng)到頂部和底部時(shí)出現(xiàn)的陰影如何去掉
- Android模仿美團(tuán)頂部的滑動(dòng)菜單實(shí)例代碼
相關(guān)文章
Android實(shí)現(xiàn)為Notification加上一個(gè)進(jìn)度條的方法
這篇文章主要介紹了Android實(shí)現(xiàn)為Notification加上一個(gè)進(jìn)度條的方法,結(jié)合實(shí)例形式分析了Android針對(duì)Notification組件的相關(guān)操作技巧,需要的朋友可以參考下2016-10-10Android Studio下添加assets目錄的實(shí)現(xiàn)方法
下面小編就為大家?guī)硪黄狝ndroid Studio下添加assets目錄的實(shí)現(xiàn)方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-03-03Android 內(nèi)存溢出和內(nèi)存泄漏的問題
這篇文章主要介紹了Android 內(nèi)存溢出和內(nèi)存泄漏的問題的相關(guān)資料,需要的朋友可以參考下2017-03-03Android RecyclerView 基礎(chǔ)知識(shí)詳解
本文主要介紹Android RecyclerView的資料,這里對(duì)RecyclerView 的基礎(chǔ)知識(shí)做了詳細(xì)講解,并附簡單示例代碼幫助大家學(xué)習(xí)參考,有需要的小伙伴可以參考下2016-09-09Android實(shí)現(xiàn)系統(tǒng)語言切換功能
這篇文章主要為大家詳細(xì)介紹了Android系統(tǒng)語言切換功能的實(shí)現(xiàn)方法,感興趣的小伙伴們可以參考一下2016-03-03android 獲取手機(jī)中的所有圖片或某一目錄下的圖片方法
下面小編就為大家分享一篇android 獲取手機(jī)中的所有圖片或某一目錄下的圖片方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-02-02Android自定義Drawable之在Drawable中部指定透明區(qū)域方法示例
對(duì)于不同的屏幕密度、不同的設(shè)備方向,不同的語言和區(qū)域,都會(huì)涉及到備選 drawable 資源,下面這篇文章主要給你大家介紹了關(guān)于Android自定義Drawable之在Drawable中部指定透明區(qū)域的相關(guān)資料,需要的朋友可以參考下2018-07-07