Android自定義Behavior實現跟隨手勢滑動,顯示隱藏標題欄涝桅、底部導航欄及懸浮按鈕
Android Design包下的CoordinatorLayout是相當重要的一個控件拜姿,它讓許多動畫的實現變?yōu)榭赡芊胨欤腋雍啽闳锓省0凑展俜浇忉孋oordinatorLayout是用來協(xié)調子View交互動作的父view蛤肌,Behavior可以看做CoordinatorLayout的子view實現交互的組件。
本篇博客主要用來實現仿知乎的Android客戶端首頁的滑動嵌套動畫寻定。首先附上項目的地址 (https://github.com/Lauzy/LBehavior) 。
先來一波效果圖:
效果實現思路:
- 判斷手勢
- 計算距離
- 觸發(fā)動畫
文章目錄:
- CoordinatorLayout及Behavior簡介
- 自定義Behavior
- 仿知乎效果的動畫實現及個性化
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。