Android開發(fā)之仿微博詳情頁(滑動(dòng)固定頂部欄效果)

歷經(jīng)一年多努力,Android 8.0 Oreo 終于和大家見面了寻行,8月21日总棵,全美都在追逐日全食的時(shí)候廷臼,Google在紐約宣布了Android O正式名稱:奧利奧Oreo。


Android 8.0 奧利奧現(xiàn)已推出

哈哈塘辅,蹭個(gè)熱點(diǎn)晃虫,也希望Android的發(fā)展越來越好,然后言歸正傳莫辨,先來看下我們今天要實(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è)簡(jiǎn)單的Demo來講解查乒。

簡(jiǎn)單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è)固定欄簡(jiǎn)化成了一個(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、不管你的頂部固定欄布局多簡(jiǎn)單削饵,建議在外套一層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è)固定的高度值即可(與固定欄高度保持一致)涵叮。

另類實(shí)現(xiàn)方式惭蹂,只用一個(gè)RecyclerView實(shí)現(xiàn)(傳送門):Android開發(fā)之分組列表懸浮頂部欄(吸頂效果)

源碼下載:

這里附上源碼地址(歡迎Star,歡迎Fork):源碼下載

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末割粮,一起剝皮案震驚了整個(gè)濱河市盾碗,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌舀瓢,老刑警劉巖廷雅,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡航缀,警方通過查閱死者的電腦和手機(jī)商架,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來芥玉,“玉大人蛇摸,你說我怎么就攤上這事〔忧桑” “怎么了赶袄?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)抠藕。 經(jīng)常有香客問我饿肺,道長(zhǎng),這世上最難降的妖魔是什么盾似? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任唬格,我火速辦了婚禮,結(jié)果婚禮上颜说,老公的妹妹穿的比我還像新娘购岗。我一直安慰自己,他們只是感情好门粪,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布喊积。 她就那樣靜靜地躺著,像睡著了一般玄妈。 火紅的嫁衣襯著肌膚如雪乾吻。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天拟蜻,我揣著相機(jī)與錄音绎签,去河邊找鬼。 笑死酝锅,一個(gè)胖子當(dāng)著我的面吹牛诡必,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播搔扁,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼爸舒,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了稿蹲?” 一聲冷哼從身側(cè)響起扭勉,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎苛聘,沒想到半個(gè)月后涂炎,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體忠聚,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年唱捣,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了两蟀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡爷光,死狀恐怖垫竞,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蛀序,我是刑警寧澤欢瞪,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站徐裸,受9級(jí)特大地震影響遣鼓,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜重贺,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一骑祟、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧气笙,春花似錦次企、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至谭期,卻和暖如春堵第,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背隧出。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工踏志, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人胀瞪。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓针余,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親赏廓。 傳聞我的和親對(duì)象是個(gè)殘疾皇子涵紊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

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