博客原文:kyleduo.com
前言
這個(gè)系列源自前幾天看到一篇使用CoordinatorLayout實(shí)現(xiàn)支付寶首頁效果的文章割粮,下載看了效果和源碼,不敢茍同焊虏,所以打算自己動(dòng)手措左。實(shí)現(xiàn)的過程有點(diǎn)曲折赴肚,但也發(fā)現(xiàn)了一些有意思的事情稽鞭,用三篇文章來記錄并分享給大家鸟整。
- CoordinatorLayout和Behavior
- 自定義CoordinatorLayout.Behavior
- 支付寶首頁效果實(shí)現(xiàn)
文中:CoL代表CoordinatorLayout,ABL表示AppBarLayout朦蕴,CTL表示CollapsingToolbarLayout篮条,SRL表示SwipeRefreshLayout,RV表示RecyclerView吩抓。
源碼:Github
第二篇文章主要用經(jīng)典的CoordinatorLayout涉茧、AppBarLayout、RecyclerView的連動(dòng)場(chǎng)景(CAR場(chǎng)景)來分析一下自定義Behavior需要關(guān)注的內(nèi)容疹娶,以及如何自定義一個(gè)Behavior降瞳。同時(shí),支付寶首頁效果和AppBarLayout的效果有相似之處蚓胸,分析CAR場(chǎng)景,也有益于后文實(shí)現(xiàn)支付寶首頁效果除师。
這篇文章適合同時(shí)閱讀源碼沛膳,如果已經(jīng)讀過源碼,可以直接跳到最后的總結(jié)汛聚。
Support包中的Behavior基類
CAR場(chǎng)景中一共出現(xiàn)了兩個(gè)Behavior锹安,AppBarLayout.Behavior和AppBarLayout.ScrollingViewBehavior,前者應(yīng)用于ABL条舔,后者應(yīng)用于RV兼贸。這兩個(gè)Behavior是我們這篇文章要分析的主要的類俗批,但是在開始之前,我們要看一下他們的基類(職責(zé)分割的很不錯(cuò))风罩。
ViewOffsetBehavior
使用ViewOffsetHelper工具類封裝View的偏移量。View類支持對(duì)offset進(jìn)行偏移舵稠,但是并不會(huì)保存偏移量超升。ViewOffsetHelper對(duì)Offset和Top/Left進(jìn)行緩存,使用ViewCompat工具類進(jìn)行偏移處理哺徊。
private void updateOffsets() {
ViewCompat.offsetTopAndBottom(mView, mOffsetTop - (mView.getTop() - mLayoutTop));
ViewCompat.offsetLeftAndRight(mView, mOffsetLeft - (mView.getLeft() - mLayoutLeft));
}
ViewOffsetBehavior除了封裝了對(duì)水平和垂直方向偏移的Setter和Getter方法室琢,還覆寫了onLayoutChild()
方法,上一篇文章中有提到落追,實(shí)現(xiàn)這個(gè)方法可以代理CoL對(duì)子View的布局盈滴。不過ViewOffsetBehavior覆寫這個(gè)方法的目的主要是創(chuàng)建ViewOffsetHelper、獲取真實(shí)偏移量并且將child偏移到正確位置轿钠。
說句題外話巢钓,當(dāng)我們考慮一個(gè)滑動(dòng)交互時(shí)病苗,不要把滑動(dòng)看做一個(gè)連續(xù)過程,而要拆分成多個(gè)單獨(dú)的循環(huán)過程竿报,連續(xù)的滑動(dòng)只不過是單獨(dú)循環(huán)過程在時(shí)間上不斷重復(fù)而已铅乡;而滑動(dòng)的單個(gè)循環(huán)過程,說到底都是對(duì)View進(jìn)行偏移處理烈菌。當(dāng)看到一個(gè)復(fù)雜交互效果的時(shí)候阵幸,要學(xué)會(huì)拆分,一個(gè)是剛說的時(shí)間上拆分芽世,另一個(gè)方面就是要能拆分成多個(gè)單獨(dú)效果的合成挚赊,能做到這一步,再加上牢固的基礎(chǔ)济瓢,就沒有什么交互效果是做不出來的荠割。
HeaderBehavior
HeaderBehavior封裝了經(jīng)典Touch事件分發(fā)邏輯,主要是實(shí)現(xiàn)了Behavior的onInterceptTouchEvent
方法和onTouchEvent
方法旺矾,邏輯其實(shí)也很簡(jiǎn)單:
- 判斷是否可以滑動(dòng)
- 當(dāng)滑動(dòng)距離超過閾值之后蔑鹦,標(biāo)記滑動(dòng)(mIsBeingDragged)并進(jìn)行攔截。
- 處理ACTION_MOVE事件箕宙,調(diào)用ViewOffsetBehavior的方法進(jìn)行偏移嚎朽。
- 使用VelocityTracker計(jì)算滑動(dòng)速度。
- 在ACTION_UP分支中停止滑動(dòng)并判斷是否應(yīng)該Fling
- 實(shí)現(xiàn)scroll和fling方法柬帕。
HeaderBehavior的實(shí)現(xiàn)簡(jiǎn)單且清晰哟忍,都可以當(dāng)做經(jīng)典Touch事件實(shí)現(xiàn)滑動(dòng)的范例了,有這方面需求的童鞋不要錯(cuò)過陷寝。因?yàn)镠eaderBehavior的定位很明確锅很,實(shí)現(xiàn)類似AppBarLayout類似的Header功能,所以只處理了縱向滑動(dòng)凤跑。
除了scroll和fling暴露給子類的方法主要是setHeaderTopBottomOffset
爆安,這個(gè)方法一共有兩個(gè)重載聲明,可以設(shè)置邊界值避免滑動(dòng)越界仔引。
int setHeaderTopBottomOffset(CoordinatorLayout parent, V header, int newOffset) {
return setHeaderTopBottomOffset(parent, header, newOffset,
Integer.MIN_VALUE, Integer.MAX_VALUE);
}
int setHeaderTopBottomOffset(CoordinatorLayout parent, V header, int newOffset,
int minOffset, int maxOffset) {
final int curOffset = getTopAndBottomOffset();
int consumed = 0;
if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {
// If we have some scrolling range, and we're currently within the min and max
// offsets, calculate a new offset
newOffset = MathUtils.constrain(newOffset, minOffset, maxOffset);
if (curOffset != newOffset) {
setTopAndBottomOffset(newOffset);
// Update how much dy we have consumed
consumed = curOffset - newOffset;
}
}
return consumed;
}
這個(gè)方法是有返回值的鹏控,這個(gè)返回值在子類中處理嵌套滑動(dòng)或者再次分發(fā)滑動(dòng)是非常有用。
HeaderScrollingViewBehavior
同樣繼承自ViewOffsetBehavior肤寝,HeaderScrollingViewBehavior的職責(zé)主要是完成對(duì)ScrollingView的布局当辐。CoL的職責(zé)是給子類提供協(xié)調(diào)滾動(dòng)的接口,并不會(huì)具體實(shí)現(xiàn)某種效果鲤看,所有子類需要完成的功能和效果缘揪,都需要通過統(tǒng)一接口Behavior完成。
在Header+ScrollingView的結(jié)構(gòu)中,HeaderBehavior完成對(duì)Touch事件的處理找筝,而HeaderScrollingViewBehavior要完成的蹈垢,就是對(duì)ScrollingView的控制。這兩者結(jié)合要實(shí)現(xiàn)的就是MaterialDesign中經(jīng)典的可收起Header的效果袖裕。
為了讓Header可收起曹抬,視覺上ScrollingView的高度被拉長(zhǎng)了,但實(shí)際上ScrollingView的高度并沒有變急鳄,變的是ScrollingView的位置谤民。ScrollingView的測(cè)量和布局工作就是HeaderScrollingViewBehavior的實(shí)現(xiàn)內(nèi)容。
final int childLpHeight = child.getLayoutParams().height;
if (childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT || childLpHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
// If the menu's height is set to match_parent/wrap_content then measure it
// with the maximum visible height
// {...}
return true;
}
}
return false;
onMeasureChild
方法中的注釋說明了只要child的LayoutParams是MATCH_PARENT或者WRAP_CONTENT疾宏,就設(shè)置child的高度為最大可見高度张足。這里的最大可見高度包含除header之外的區(qū)域以及header收起時(shí)額外空出的區(qū)域,也就是header的可滾動(dòng)區(qū)域坎藐。
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);
onLayout
中將ScrollingView置于header下方为牍。
available.set(
parent.getPaddingLeft() + lp.leftMargin,
header.getBottom() + lp.topMargin,
parent.getWidth() - parent.getPaddingRight() - lp.rightMargin,
parent.getHeight() + header.getBottom() - parent.getPaddingBottom() - lp.bottomMargin
);
注意這里Rect的top值取header.getBottom() + lp.topMargin
,而不是getPaddingTop() + header.getHeight() + lp.topMargin
岩馍,這是因?yàn)閔eader在onLayout時(shí)可能已經(jīng)包含偏移量碉咆,不能假定header在初始位置,即便可能90%的情況均是如此蛀恩。
說句題外話吟逝,項(xiàng)目開發(fā)過程中會(huì)遇到很多這類情況,有多種實(shí)現(xiàn)方式都能達(dá)到預(yù)期效果赦肋,但并不是所有的實(shí)現(xiàn)方案都是完整符合預(yù)期邏輯的。比如上面的例子励稳,ScrollingView的預(yù)期位置是header下方佃乘,而不是父控件中除header高度以外的區(qū)域。有的時(shí)候驹尼,需要轉(zhuǎn)換角度看問題趣避,體會(huì)下這其中的區(qū)別。
AppBarLayout.Behavior
AppBarLayout.ScrollingViewBehavior相對(duì)簡(jiǎn)單新翎,這里略過程帕。AppBarLayout.Behavior繼承自HeaderBehavior,在其基礎(chǔ)上地啰,主要實(shí)現(xiàn)了以下功能:
- 支持在布局文件中定義滾動(dòng)效果:SCROLL / EXIT_UNTIL_COLLAPSED / ENTER_ALWAYS / ENTER_ALWAYS_COLLAPSED / SNAP
- 實(shí)現(xiàn)NestedScrolling回調(diào)
滾動(dòng)效果不是這篇文章的重點(diǎn)愁拭,我們主要看下NestedScrolling的相關(guān)實(shí)現(xiàn)。
onStartNestedScroll
判斷是否為縱向滑動(dòng)亏吝,并且AppBarLayout支持折疊并且ScrollingView的大小超出屏幕范圍岭埠。
final boolean started = (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0
&& child.hasScrollableChildren()
&& parent.getHeight() - directTargetChild.getHeight() <= child.getHeight();
onNestedPreScroll
這個(gè)方法會(huì)提前于ScrollingView消費(fèi)滑動(dòng)事件。AppBarLayout的scrollFlags,也就是上面說的滾動(dòng)效果會(huì)影響onNestedPreScroll方法的實(shí)現(xiàn)惜论。拋開這個(gè)影響许赃,這個(gè)方法中,首先確定AppBarLayout的可滑動(dòng)范圍馆类,然后調(diào)用scroll()
方法(繼承自ViewOffsetBehavior)進(jìn)行滾動(dòng)混聊,并將消費(fèi)多少傳遞給consumed
數(shù)組。
onNestedScroll
如果向下滾動(dòng)時(shí)乾巧,在ScrollingView消費(fèi)完滑動(dòng)事件之后句喜,還有剩余,說明ScrollingView已經(jīng)滾動(dòng)到頂部卧抗,AppBarLayout開始展開藤滥。
onNestedFling
這里并沒有進(jìn)行精確的消費(fèi),只是當(dāng)ScrollingView觸發(fā)fling時(shí)社裆,對(duì)AppBarLayout執(zhí)行動(dòng)畫拙绊,展開或者收起。下篇文章實(shí)現(xiàn)支付寶首頁效果時(shí)泳秀,實(shí)現(xiàn)了對(duì)fling的精確消費(fèi)标沪。
總結(jié)
自定義Behavior主要關(guān)心以下兩個(gè)方面:
- 測(cè)量和布局
- 實(shí)現(xiàn)滑動(dòng)效果
其中滑動(dòng)效果有三種實(shí)現(xiàn)方式:
- 經(jīng)典Touch事件。
- NestedScrolling嗜傅。
- LayoutDependent金句。
一般情況下,CoL的child吕嘀,如果自身不可滾動(dòng)违寞,需要實(shí)現(xiàn)NestedScrolling來進(jìn)行聯(lián)動(dòng),或者實(shí)現(xiàn)Touch事件回調(diào)偶房。如果自身可滾動(dòng)趁曼,通過onDependentViewChanged
方法來響應(yīng)其他View的偏移量改變事件。