Android高仿知乎首頁Behavior

Android自定義Behavior實現跟隨手勢滑動,顯示隱藏標題欄涝桅、底部導航欄及懸浮按鈕

Android Design包下的CoordinatorLayout是相當重要的一個控件拜姿,它讓許多動畫的實現變?yōu)榭赡芊胨欤腋雍啽闳锓省0凑展俜浇忉孋oordinatorLayout是用來協(xié)調子View交互動作的父view蛤肌,Behavior可以看做CoordinatorLayout的子view實現交互的組件。

本篇博客主要用來實現仿知乎的Android客戶端首頁的滑動嵌套動畫寻定。首先附上項目的地址 (https://github.com/Lauzy/LBehavior)

先來一波效果圖:

Activity中使用

BottomView+Fragment使用

效果實現思路:

  1. 判斷手勢
  2. 計算距離
  3. 觸發(fā)動畫

文章目錄:

  1. CoordinatorLayout及Behavior簡介
  2. 自定義Behavior
  3. 仿知乎效果的動畫實現及個性化

CoordinatorLayout和Behavior簡介

Android滑動嵌套的原理及Behavior分析已經有很多大神講解過了狼速,推薦Loader大神的源碼看CoordinatorLayout.Behavior原理卦停。

這里簡單介紹下,嵌套滑動時父View(需實現NestedScrollingParent接口)和子View(需實現NestedScrollingChild接口)之間的交互是由NestedScrolling兩個接口控制惊完,NestedScrollingParentHelper和NestedScrollingChildHelper兩個輔助類分別處理了父布局和子View的大量邏輯。

滑動嵌套的簡單流程為:控制子View(如RecyclerView)的onInterceptTouchEvent和onTouchEvent的事件分發(fā) -> 調用NestedScrollingChildHelper不同的方法 -> 處理與NestedScrollingParent交互的邏輯 -> 父布局(如CoordinatorLayout)實現NestedScrollingParent處理具體的邏輯
(-> 而Behavior的事件處理方法則主要由CoordinatorLayout的各種事件處理方法來調用小槐,返回值控制了父布局的事件消費情況)。

具體方法的調用大家可以再研讀Loader大神的博客凿跳。下邊簡單介紹下自定義Behavior實現的具體方法Behavior官網

方法

1.layoutDependsOn

確定提供的子視圖是否具有另一個特定的兄弟視圖作為布局依賴關系控嗜。即用來確定依賴關系,如果某個控件需要依賴控件疆栏,則重寫該方法
如AppBarLayout


    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child壁顶, View dependency) {
        return dependency instanceof AppBarLayout;
    }

2.onDependentViewChanged

依賴視圖的大小、位置發(fā)生變化時調用此方法若专,重寫此方法可以處理child的響應。如常用的AppBarLayout,當其發(fā)生變化時蛔糯,childView會根據重寫的方法作出響應。


    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent蚁飒, View child, View dependency) {
        offsetChildAsNeeded(parent淮逻, child, dependency);
        return false;
    }

3.onStartNestedScroll

當CoordinatorLayout的子View開始嵌套滑動時(此處的滑動View必須實現NestedScrollingChild接口)爬早,觸發(fā)此方法。添加Behavior的控件需要為CoordinatorLayout的直接子View筛严,否則不會繼續(xù)流程。


    //判斷是否垂直滑動
    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout桨啃, View child, View directTargetChild照瘾, View target, int nestedScrollAxes) {
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

4.onNestedPreScroll

此方法中consumed析命,指的是父布局要消費的滾動距離,consumed[0]為水平方向消耗的距離鹃愤,consumed[1]為垂直方向消耗的距離,可控制此參數作出相應的調整昼浦。
如垂直滑動時,若設置consumed[1]=dy关噪,則代表父布局全部消耗了滑動的距離鸟蟹,類似AppBarLayout這種效果使兔,當其由展開到折疊過渡時,通過consumed控制其中的嵌套滑動虐沥。


    /**
     * 觸發(fā)滑動嵌套滾動之前調用的方法
     *
     * @param coordinatorLayout coordinatorLayout父布局
     * @param child             使用Behavior的子View
     * @param target            觸發(fā)滑動嵌套的View(實現NestedScrollingChild接口)
     * @param dx                滑動的X軸距離
     * @param dy                滑動的Y軸距離
     * @param consumed          父布局消費的滑動距離泽艘,consumed[0]和consumed[1]代表X和Y方向父布局消費的距離,默認為0
     */
    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout匹涮, View child, View target然低, 
        int dx, int dy雳攘, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child吨灭, target, dx喧兄, dy, consumed);
    }

5.onNestedScroll

此方法中dyConsumed代表TargetView消費的距離繁莹,如RecyclerView滑動的距離,可通過控制NestScrollingChild的滑動來指定一些動畫,
本篇博客實現的效果主要就是重寫此方法闸昨,若根據onNestedPreScroll中dy來判斷,則當RecyclerView條目很少時饵较,也會觸發(fā)邏輯代碼拍嵌,故選擇了重寫此方法循诉。

    
    /**
     * 滑動嵌套滾動時觸發(fā)的方法
     *
     * @param coordinatorLayout coordinatorLayout父布局
     * @param child             使用Behavior的子View
     * @param target            觸發(fā)滑動嵌套的View
     * @param dxConsumed        TargetView消費的X軸距離
     * @param dyConsumed        TargetView消費的Y軸距離
     * @param dxUnconsumed      未被TargetView消費的X軸距離
     * @param dyUnconsumed      未被TargetView消費的Y軸距離(如RecyclerView已經到達頂部或底部横辆,
     *              而用戶繼續(xù)滑動茄猫,此時dyUnconsumed的值不為0,可處理一些越界事件)
     */
    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout划纽, View child, View target勇劣, 
        int dxConsumed潭枣, int dyConsumed, int dxUnconsumed盆犁, int dyUnconsumed) {
        super.onNestedScroll(coordinatorLayout, child谐岁, target, 
            dxConsumed翰铡, dyConsumed, dxUnconsumed锭魔, dyUnconsumed);
    }

自定義Behavior

自定義Behavior主要有兩種實現方式:

第一種為layoutDependsOn和onDependentViewChanged,child需要依賴于dependency迷捧,當dependency View發(fā)生變化時,onDependentViewChanged會被調用胀葱,child可作出相應的響應。
第二種為onStartNestedScroll 等嵌套滑動的流程抵屿,首先在onStartNestedScroll方法中判斷是否垂直滑動等,然后在onNestedPreScroll轧葛、onNestedScroll等方法中實現效果。
由于第一種方式會導致child必須依賴于某個特定的View尿扯,這樣就導致靈活性不太強,所以本文采用第二種實現方式衷笋。

具體實現

在嵌套滑動開始之前,可以判斷是否垂直滑動辟宗,做一些初始化工作爵赵,比如獲取childView的初始坐標。


    //判斷垂直滑動
    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        if (isInit) {// 設置標記亚再,防止new Anim導致的parent和child坐標變化
            mCommonAnim = new LTitleBehaviorAnim(child);
            isInit = false;
        }
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }
    

觸發(fā)嵌套滑動之前晨抡,可以在此處判斷一些滑動手勢氛悬,以及父布局的消費情況。由于若根據此方法中dy來判斷如捅,則當RecyclerView條目很少時,也會觸發(fā)邏輯代碼镜遣,故本文只是在此方法中給動畫做一些自定義操作。


    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
        if (mCommonAnim != null) {
            mCommonAnim.setDuration(mDuration);
            mCommonAnim.setInterpolator(mInterpolator);
        }
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
    }

滑動嵌套滾動時觸發(fā)的方法悲关,以Title(Toolbar)為例,若向上滑動寓辱,則隱藏Toolbar,反之顯示秫筏。


    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
        if (dyConsumed < 0) {
            if (isHide) {
                mCommonAnim.show();
                isHide = false;
            }
        } else if (dyConsumed > 0) {
            if (!isHide) {
                mCommonAnim.hide();
                isHide = true;
            }
        }
    }


仿知乎效果的動畫實現及個性化

大家都知道知乎客戶端的各種動畫非常優(yōu)雅,網上仿寫其動畫的博客也是層出不窮航夺,之前利用空閑時間擼了一款干貨集中營客戶端,突然想到了采用知乎的首頁效果阳掐,然后就拿起鍵盤,復制粘貼搞了起來冷蚂。
開個玩笑,其實大致實現效果還是比較容易的帝雇,這里主要分享下實現的思路以及需要注意的細節(jié)蛉拙。

首先大致流程就如上邊幾個方法介紹,動畫效果的實現也非常簡單孕锄,這里以顯示和隱藏BottomView為例,直接上代碼畸肆。


    public LBottomBehaviorAnim(View bottomView) {
        mBottomView = bottomView;
        mOriginalY = mBottomView.getY();//因為Y值隨動畫會發(fā)生變化,嵌套滑動開始之前先記錄初始的坐標轴脐。
    }

    @Override
    public void show() {//顯示
        ValueAnimator animator = ValueAnimator.ofFloat(mBottomView.getY(), mOriginalY);
        animator.setDuration(getDuration());
        animator.setInterpolator(getInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mBottomView.setY((Float) valueAnimator.getAnimatedValue());
            }
        });
        animator.start();
    }

    @Override
    public void hide() {//隱藏
        ValueAnimator animator = ValueAnimator.ofFloat(mBottomView.getY(), mOriginalY + mBottomView.getHeight());
        animator.setDuration(getDuration());
        animator.setInterpolator(getInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mBottomView.setY((Float) valueAnimator.getAnimatedValue());
            }
        });
        animator.start();
    }

整個大致流程這樣其實已經結束了抡砂,但是還達不到我們預期的效果恬涧。再次打開知乎客戶端注益,以很緩慢的速度滑一滑溯捆,這時候你會發(fā)現竟然沒有觸發(fā)動畫,OK提揍,先記錄下這個問題;再以很緩慢的速度向下滑劳跃,突然又觸發(fā)動畫了。整體來看售碳,知乎的動畫有種分層嵌套的效果。

先來解決第一個問題贸人,只用加一行代碼,即dyConsumed距離大于一定值的時候才允許滑動艺智。


    if(Math.abs(dyConsumed) > minScrollY){
        ...//onNestedScroll里邊的邏輯代碼
    }

對于第二個問題,我一開始想十拣,滑動一定的距離,難道要根據判斷RecyclerView滑動的距離來判斷是否觸發(fā)動畫夭问?其實思路是正確的泽西,但是我們不可能再去實現addOnScrollListener的一系列方法缰趋。這時候再想一想嵌套滑動,dyConsumed不就是recyclerView消費的距離嗎秘血,想到這里,那就很好實現了灰粮,只用將dyConsumed相加,相加的和大于一定值粘舟,就觸發(fā)動畫佩研,代碼也是很簡單,結合第一個問題锤悄,知乎的效果就實現了。


    mTotalScrollY += dyConsumed;//累加消費的距離
    if (Math.abs(dyConsumed) > minScrollY || Math.abs(mTotalScrollY) > scrollYDistance) {
        ...//onNestedScroll里邊的邏輯代碼
        mTotalScrollY = 0;//動畫執(zhí)行完畢后重置
    }

接下來我們可以自定義設置一些屬性值袍暴。首先要獲取這個Behavior對象。


    public static CommonBehavior from(View view) {
        ViewGroup.LayoutParams params = view.getLayoutParams();
        if (!(params instanceof CoordinatorLayout.LayoutParams)) {
            throw new IllegalArgumentException("The view is not a child of CoordinatorLayout");
        }
        CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params).getBehavior();
        if (!(behavior instanceof CommonBehavior)) {
            throw new IllegalArgumentException("The view's behavior isn't an instance of CommonBehavior. Try to check the [app:layout_behavior]");
        }
        return (CommonBehavior) behavior;
    }

然后可以設置對象的屬性:


    public CommonBehavior setDuration(int duration) {
        mDuration = duration;
        return this;
    }

    public CommonBehavior setInterpolator(Interpolator interpolator) {
        mInterpolator = interpolator;
        return this;
    }

    public CommonBehavior setMinScrollY(int minScrollY) {
        this.minScrollY = minScrollY;
        return this;
    }

    public CommonBehavior setScrollYDistance(int scrollYDistance) {
        this.scrollYDistance = scrollYDistance;
        return this;
    }
    

至此政模,整個流程已經實現了蚂会,其他TitleView及懸浮按鈕的動畫也是類似的規(guī)則淋样,我又給Behavior和動畫設置了Common類剔除掉一些重復代碼胁住,這里就不貼出來了趁猴。具體可以參考我的Github

動畫已經實現彪见,但是寫代碼的時候坑貌似永遠是填不完的。

當我使用寫出來的動畫時余指,就發(fā)現了一個問題,由于是CoordinatorLayout作為根布局酵镜,所以RecyclerView頂部的item被toolbar遮擋了,
我們再看看知乎淮韭,輕輕滑動一小段距離,發(fā)現他的頂部Toolbar遮擋的地方其實是空白靠粪,可以發(fā)現知乎其實也是有這個問題的,不過人家處理的很好庇配,所以用戶基本上不會發(fā)現绍些。
不過這個問題還是可以解決的捞慌,比如判斷item為第一個時柬批,可以加一個View填充袖订,個人采用的自定義ItemDecoration,判斷下若為第一個item洛姑,outRect.set(0, titleHeight, 0, 0),設置titleHeight的大小即可楞艾。BottomView也是同理,解決方法也是有不少的硫眯。

還有一個問題是寫demo的時候發(fā)現的,我用LinearLayout作為BottomView两入,發(fā)現浮動按鈕竟然是在LinearLayout上層執(zhí)行各種動畫,看起來不太和諧裹纳,后來發(fā)現FloatingActionButton的elevation若大于BottomView的elevation,則FloatingActionButton動畫覆蓋在BottomView上層剃氧,反之則在下層脏里。之前卻一直沒有注意她我。

此外迫横,當知乎的RecyclerView滑動到底部的時候番舆,BottomView是會自動顯示的矾踱,個人覺得可以根據dyUnconsumed的值或者onStopNestedScroll來判斷RecyclerView是否滑動到底部來處理,全部加載完畢后再處理最后一個item的ItemDecoration呛讲,本文并沒有具體實現,只是提供思路贝搁。

我們再來整理下解決問題的思路:首先想好做什么,然后研究原理雷逆,選擇方案,再初步實現,繼續(xù)優(yōu)化細節(jié)被碗,最后應用到項目。我想我們寫程序的時候都應該這樣锐朴,知其然知其所以然,做到舉一反三焚志。

整個流程就是這樣,實現后再封裝娩嚼,然后呢,抽離出來提交到Github岳悟。


    allprojects {
        repositories {
            ...
            maven { url 'https://jitpack.io' }
        }
    }

    dependencies {
        compile 'com.github.Lauzy:LBehavior:1.0.1'
    }

具體使用也很簡單

參數 說明
@string/title_view_behavior 頂部標題欄
@string/bottom_view_behavior 底部導航欄
@string/fab_scale_behavior 浮動按鈕(縮放)
@string/fab_vertical_behavior 浮動按鈕(上下滑動)

自定義(均設有默認值泼差,可不使用):

方法 參數 說明
setMinScrollY int y 設置觸發(fā)動畫的最小滑動距離贵少,如 setMinScrollY(10)為滑動10像素才可觸發(fā)動畫堆缘,默認為5.
setScrollYDistance int y 設置觸發(fā)動畫的滑動距離,防止用戶緩慢滑動時單次滑動距離一直小于setMinScrollY的最小滑動距離導致無法觸發(fā)動畫.如設置此值為100吼肥,則用戶即便緩慢滑動,當滑動距離達到100時也可觸發(fā)動畫.默認為40.
setDuration int duration 設置動畫持續(xù)時間.默認為400ms.
setInterpolator Interpolator interpolator 設置動畫插補器缀皱,修飾動畫效果.默認模式為LinearOutSlowInInterpolator. Interpolator官方文檔

    CommonBehavior.from(mFloatingActionButton)
        .setMinScrollY(20)
        .setScrollYDistance(100)
        .setDuration(1000)
        .setInterpolator(new LinearOutSlowInInterpolator());
        

最后再附上項目地址,戳 我的Github 啤斗,歡迎 star。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末免钻,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子崔拥,更是在濱河造成了極大的恐慌,老刑警劉巖链瓦,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異稽揭,居然都是意外死亡,警方通過查閱死者的電腦和手機溪掀,發(fā)現死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進店門步鉴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來揪胃,“玉大人氛琢,你說我怎么就攤上這事⊙羲疲” “怎么了?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵撮奏,是天一觀的道長。 經常有香客問我畜吊,道長,這世上最難降的妖魔是什么玲献? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮捌年,結果婚禮上,老公的妹妹穿的比我還像新娘延窜。我一直安慰自己,他們只是感情好逆瑞,可當我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著获高,像睡著了一般。 火紅的嫁衣襯著肌膚如雪念秧。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天币狠,我揣著相機與錄音游两,去河邊找鬼漩绵。 笑死贱案,一個胖子當著我的面吹牛止吐,可吹牛的內容都是我干的。 我是一名探鬼主播碍扔,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼不同!你這毒婦竟也來了?” 一聲冷哼從身側響起套鹅,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎卓鹿,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體吟孙,經...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年藻治,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片桩卵。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖雏节,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情钩乍,我是刑警寧澤,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布寥粹,位于F島的核電站变过,受9級特大地震影響涝涤,放射性物質發(fā)生泄漏。R本人自食惡果不足惜阔拳,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望衫生。 院中可真熱鬧土浸,春花似錦罪针、人聲如沸黄伊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽墓阀。三九已至拓轻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間扶叉,已是汗流浹背勿锅。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工枣氧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人达吞。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像酪劫,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子契耿,可洞房花燭夜當晚...
    茶點故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內容