在上篇文章自定義View事件之進階篇(三)-CoordinatorLayout與Behavior中,咱們介紹了CoordainatorLayout下的Behavior機制,爲了幫助你們更好的理解並運用Behavior,如今咱們經過一個Demo,來鞏固咱們以前學習的知識點。java
該博客中涉及到的示例,在NestedScrollingDemo項目中都有實現,你們能夠按需自取。git
先看一下咱們須要實現的效果吧,以下圖所示:github
友情提示:Demo中涉及到的控件爲CoordinatorLayout、TextView、RecyclerView。文章都會圍繞這三個控件進行講解。windows
從Demo效果來看,這是很是簡單的嵌套滑動。若是採用咱們以前所學的NestedScrollingParent2
與NestedScrollingChild2
實現接口的方式。咱們能很是迅速的解決問題。可是若是採用自定義Behavior的話,那麼就稍微有點難度了。不過不用擔憂,只要一步一步慢慢分析,就總能解決問題的。app
在Demo中,RecyclerView與TextView開始的佈局關係以下圖所示:ide
根據在文章自定義View事件之進階篇(三)-CoordinatorLayout與Behavior中咱們所學的知識點,咱們知道CoordinatorLayout對子控件的佈局是相似於FrameLayout的,因此爲了保證RecyclerView在TextView的下方顯示,咱們須要建立屬於RecyclerView的Behavior,並在該Behavior的onLayoutChild
方法中處理RecyclerView與TextView的位置關係。函數
除了解決RecyclerView的位置關係之外,在該Demo中,咱們還能夠看出,RecyclerView與TextView之間有着一個聯動的關係(這裏指的是RecyclerView與TextView之間的位置關係,而不是RecyclerView中的內容)。隨着TextView逐漸上移的時候,下方的RecyclerView也跟着往上移動。那麼咱們能夠肯定的是RecyclerView必然是依賴TextView的。也就是說咱們須要重寫Behavior的layoutDependsOn
與onDependentViewChanged
方法。佈局
肯定一個控件(childView1)依賴另一個控件(childView2)的時候,是經過
layoutDependsOn(CoordinatorLayout parent, V child, View dependency)
這個方法。其中child是依賴對象(childView1),而dependency是被依賴對象(childView2),該方法的返回值是判斷是否依賴對應view。若是返回true。那麼表示依賴。反之不依賴。通常狀況下,在咱們自定義Behavior時,咱們須要重寫該方法。當layoutDependsOn
方法返回true時,後面的onDependentViewChanged
與onDependentViewRemoved
方法纔會調用。post
除了考慮以上因數之外,咱們還須要考慮RecyclerView的高度。觀察Demo,咱們能夠看出,RecylerView在移動先後,始終都是填充整個屏幕的。爲了保證RecylerView在移動過程當中,屏幕中不會出現空白(以下圖所示)。咱們也須要在CoordinatorLayout測量該控件的高度以前,讓控件自主的去測量高度。也就是重寫RecylerView對應Behavior中的onMeasureChild
方法。學習
分析了RecyclerView的Behavior須要重寫的內容後,咱們來看看具體的Behavior實現類HeaderScrollingViewBehavior
。爲了幫助你們理解,我將RecyclerView的Behavior拆成了幾個部分,代碼以下所示:
查看該Behavior完整代碼,請點擊--->HeaderScrollingViewBehavior
public class HeaderScrollingViewBehavior extends CoordinatorLayout.Behavior<View> {
public HeaderScrollingViewBehavior() {}
public HeaderScrollingViewBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
/** * 依賴TextView */
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof TextView;
}
//省略部分代碼…
}
複製代碼
注意:在xml引用自定義Behavior時,必定要聲明構造函數。否則在程序的編譯過程當中,會提示知道不到相應的Behavior。
layoutDependsOn
方法的邏輯很是簡單。就是判斷依賴的對象是不是TextView。咱們繼續查看該類中的onMeasureChild
方法。代碼以下所示:
@Override
public boolean onMeasureChild(CoordinatorLayout parent, View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
//獲取當前滾動控件的測量模式
final int childLpHeight = child.getLayoutParams().height;
//只有當前滾動控件爲match_parent/wrap_content時才從新測量其高度,由於固定高度不會出現底部空白的狀況
if (childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
|| childLpHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
//獲取當前child依賴的對象集合
final List<View> dependencies = parent.getDependencies(child);
final View header = findFirstDependency(dependencies);
if (header != null) {
if (ViewCompat.getFitsSystemWindows(header)
&& !ViewCompat.getFitsSystemWindows(child)) {
// If the header is fitting system windows then we need to also,
// otherwise we'll get CoL's compatible measuring
ViewCompat.setFitsSystemWindows(child, true);
if (ViewCompat.getFitsSystemWindows(child)) {
// If the set succeeded, trigger a new layout and return true
child.requestLayout();
return true;
}
}
//獲取當前父控件中可用的距離,
int availableHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec);
if (availableHeight == 0) {
// If the measure spec doesn't specify a size, use the current height
availableHeight = parent.getHeight();
}
//計算當前滾動控件的高度。
final int height = availableHeight - header.getMeasuredHeight() + getScrollRange(header);
final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height,
childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
? View.MeasureSpec.EXACTLY
: View.MeasureSpec.AT_MOST);
//測量當前滾動的View的正確高度
parent.onMeasureChild(child, parentWidthMeasureSpec,
widthUsed, heightMeasureSpec, heightUsed);
return true;
}
}
return false;
}
複製代碼
測量邏輯的基本步驟:
match_parent
或者wrap_content
。(對於精準模式,咱們不用考慮,控件是否填充屏幕)-
TextView的高度+
TextView的滾動範圍)在onMeasureChild
方法中,我省略了部分方法的介紹,如findFirstDependency
、getScrollRange
方法。這些方法在NestedScrollingDemo項目中都有實現。你們能夠按需自取。
咱們繼續查看HeaderScrollingViewBehavior
類中的onLayoutChild
方法,代碼以下所示:
@Override
public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
final List<View> dependencies = parent.getDependencies(child);
final View header = findFirstDependency(dependencies);
if (header != null) {
final CoordinatorLayout.LayoutParams lp =
(CoordinatorLayout.LayoutParams) child.getLayoutParams();
final Rect available = mTempRect1;
//獲得依賴控件下方的座標。
available.set(parent.getPaddingLeft() + lp.leftMargin,
header.getBottom() + lp.topMargin,
parent.getWidth() - parent.getPaddingRight() - lp.rightMargin,
parent.getHeight() + header.getBottom()
- parent.getPaddingBottom() - lp.bottomMargin);
//拿到上面計算的座標後,根據當前控件在父控件中設置的gravity,從新計算並獲得控件在父控件中的座標
final Rect out = mTempRect2;
GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(),
child.getMeasuredHeight(), available, out, layoutDirection);
//拿到座標後從新佈局
child.layout(out.left, out.top, out.right, out.bottom);
} else {
//若是沒有依賴,則調用父控件來處理佈局
parent.onLayoutChild(child, layoutDirection);
}
return true;
}
複製代碼
onLayoutChild
方法邏輯也不算複雜,根據當前所依賴的header(TextView)的位置,將RecyclerView設置在TextView下方。咱們繼續查看RecyclerView與TextView的聯動處理。也就是onDependentViewChanged
方法。代碼以下所示:
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
final CoordinatorLayout.Behavior behavior =
((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior();
if (behavior instanceof NestedHeaderBehavior) {
ViewCompat.offsetTopAndBottom(child, dependency.getBottom() - child.getTop() + ((NestedHeaderBehavior) behavior).getOffset());
}
//若是當前的控件的位置發生了改變,該返回值必定要返回爲true
return true;
}
複製代碼
在該方法中,咱們須要經過TextView的Behavior(NestedHeaderBehavior
),並得到TextView的實際偏移量(上述代碼中的getOffset()
)。經過該偏移量咱們能夠從新設置RecyclerView的位置。固然,改變控件位置的方式有不少種,咱們可使用setTransationY
或View.offsetTopAndBottom
及其餘方式,你們能夠採用本身喜歡的方式。由於涉及到TextView中Behavior的偏移量。那下面咱們就來看看TextView對應Behavior的分析與實現吧。
在整個Demo中,TextView的嵌套滑動效果並不複雜。這裏咱們就從向上與向下兩個方向來介紹。
在講解TextView的Behavior的代碼實現以前,咱們須要回顧一下在CooordinatoLayout下嵌套方法的傳遞過程,以下圖所示:
經過回顧流程,在結合本文例子中展現的效果,咱們須要重寫Behavior中的onStartNestedScroll
與onNestedPreScroll
和onNestedScroll
三個方法。來看TextView的NestedHeaderBehavior
實現。代碼以下所示:
查看該Behavior完整代碼,請點擊--->NestedHeaderBehavior
public class NestedHeaderBehavior extends CoordinatorLayout.Behavior<View> {
private WeakReference<View> mNestedScrollingChildRef;
private int mOffset;//記錄當前佈局的偏移量
public NestedHeaderBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
mNestedScrollingChildRef = new WeakReference<>(findScrollingChild(parent));
return super.onLayoutChild(parent, child, layoutDirection);
}
//省略部分代碼…
}
複製代碼
TextView中NestedHeaderBehavior類的聲明與RecyclerView中的Behavior基本同樣。由於咱們須要將偏移量傳遞給RecyclerView,因此在NestedHeaderBehavior
的onLayoutChild方法中,咱們去建立了關於RecyclerView的弱引用,並設置了mOffset
變量來記錄TextViwe每次滑動的偏移量。如何獲取RecyclerView,能夠查看項目中源碼的實現。接下來,咱們繼續查看相關嵌套方法實現。
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
//只要豎直方向上就攔截
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
複製代碼
在onStartNestedScroll
方法中,咱們設置了當前控件,只能攔截豎直方向上的嵌套滑動事件。繼續查看onNestedPreScroll
方法。代碼以下所示:
@Override
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
View scrollingChild = mNestedScrollingChildRef.get();
if (target != scrollingChild) {
return;
}
int currentTop = child.getTop();
int newTop = currentTop - dy;
if (dy > 0) {//向上滑動
//處理在範圍內的滾動與fling
if (newTop >= -child.getHeight()) {
Log.i(TAG, "onNestedPreScroll:向上移動" + "currentTop--->" + currentTop + " newTop--->" + newTop);
consumed[1] = dy;
mOffset = -dy;
ViewCompat.offsetTopAndBottom(child, -dy);
coordinatorLayout.dispatchDependentViewsChanged(child);
} else { //當超事後,單獨處理
consumed[1] = child.getHeight() + currentTop;
mOffset = -consumed[1];
ViewCompat.offsetTopAndBottom(child, -consumed[1]);
coordinatorLayout.dispatchDependentViewsChanged(child);
}
}
if (dy < 0) {//向下滑動
if (newTop <= 0 && !target.canScrollVertically(-1)) {
Log.i(TAG, "onNestedPreScroll:向下移動" + "currentTop--->" + currentTop + " newTop--->" + newTop);
consumed[1] = dy;
mOffset = -dy;
ViewCompat.offsetTopAndBottom(child, -dy);
coordinatorLayout.dispatchDependentViewsChanged(child);
}
}
}
複製代碼
onNestedPreScroll
方法中的邏輯較爲複雜。不急咱們慢慢分析:
currentTop
)。而後根據當前偏移距離dy
,計算出TextView新的Top高度(newTop
)。dy>0
,也就是向上滑動。咱們判斷偏移後的Top(newTop
)高度是否大於負
的TextView的測量的高度。由於是向上滑動,當TextView移出屏幕後,經過調用getTop方法獲取的高度確定爲負數。這裏判斷是否大於等於
-child.getHeight
,表示的是當前TextView沒有超過它的滾動範圍(-child.getHeight到0)。
newTop >= -child.getHeight()
,則TextView消耗掉dy
,經過ViewCompat.offsetTopAndBottom(child, -dy)
來移動當前TextView,接着記錄TextView位置的偏移量(mOffest
),最後經過調用CoordinatorLayout下的dispatchDependentViewsChanged
方法,通知控件RecyclerView所依賴的TextView發生了改變。那麼RecyclerView收到通知後,就能夠拿着這個偏移量和TextView一塊兒聯動了。newTop< - child.getHeight()
,表示在當前偏移距離dy
下,若是TextView會超過它的滾動範圍。那麼咱們就不能使用當前dy
來移動TextView。咱們只能滾動剩下的範圍,也就是child.getHeight() +
currentTop,(這裏使用加號,是由於滾動範圍爲-child.getHeight
到0
)。dy<0
,表示向下滑動,只有在target(RecyclerView)不能向下滑動且TextView已經部分移出屏幕時,咱們的TextView才能向下滑動。這裏的處理方式基本和上滑同樣,這裏就再也不進行介紹了。咱們繼續查看最後的方法onNestedScroll
方法。@Override
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
if (dyUnconsumed < 0) {//表示已經向下滑動到頭。
int currentTop = child.getTop();
int newTop = currentTop - dyUnconsumed;
if (newTop <= 0) {//若是當前的值在滾動範圍以內。
Log.i(TAG, "onNestedScroll: " + "dyUnconsumed--> " + dyUnconsumed + " currentTop--->" + currentTop + " newTop--->" + newTop);
ViewCompat.offsetTopAndBottom(child, -dyUnconsumed);
mOffset = -dyUnconsumed;
} else {//若是當前的值大於最大的滾動範圍(0),那麼就直接滾動到-currentTop就好了
ViewCompat.offsetTopAndBottom(child, -currentTop);
mOffset = -currentTop;
}
coordinatorLayout.dispatchDependentViewsChanged(child);
}
}
複製代碼
onNestedScroll
方法中,咱們須要處理RecyclerView向下方向上未消耗的距離(dyUnconsumed
)。一樣根據當前偏移記錄計算出TextVie新的Top高度,計算出是否超出其滾功範圍範圍。若是沒有超過,則TextView向下偏移距離爲-dyUnconsumed
,同時記錄偏移量(mOffset=-dyUnconsumed
),最後通知RecyclerView,TextView的位置發生了改變。反之,當前TextView的top的值是多少,那麼TextView就向下偏移多少。
在該文章中,我着重講解了相應Behavior中比較重要的一些方法。一些不是那麼重要的輔助方法,我並無作過多的介紹。建議你們配合NestedScrollingDemo項目中的源碼理解該篇文章,我相信確定是事半功倍的。
關於嵌套滑動、CoordinatorLayout、Behavior的知識點基本介紹完畢了。我相信你們之後再碰見一些嵌套滑動的問題。都可以輕鬆的解決了。可能不少小夥伴會好奇,爲何沒有接着講AppBarLayout與CollapsingTollbarLayout的原理及使用。其實緣由很是簡單,由於上述的兩個控件的實現原理,實際上是依託於CoordinatorLayout與自定義Behavior罷了。授人以魚,不如授人以漁。AppBarLayout與CollapsingTollbarLayout的使用及原理。就算給你們留的課後思考題吧。謝謝你們對這系列的關注。Thanks。