[Digging] 支付寶首頁交互三部曲 2 自定義Behavior

cover_2

博客原文: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)了以下功能:

  1. 支持在布局文件中定義滾動(dòng)效果:SCROLL / EXIT_UNTIL_COLLAPSED / ENTER_ALWAYS / ENTER_ALWAYS_COLLAPSED / SNAP
  2. 實(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è)方面:

  1. 測(cè)量和布局
  2. 實(shí)現(xiàn)滑動(dòng)效果

其中滑動(dòng)效果有三種實(shí)現(xiàn)方式:

  1. 經(jīng)典Touch事件。
  2. NestedScrolling嗜傅。
  3. 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的偏移量改變事件。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末棕洋,一起剝皮案震驚了整個(gè)濱河市挡闰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌掰盘,老刑警劉巖摄悯,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異愧捕,居然都是意外死亡奢驯,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門次绘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來叨橱,“玉大人典蜕,你說我怎么就攤上這事÷尴矗” “怎么了愉舔?”我有些...
    開封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)伙菜。 經(jīng)常有香客問我轩缤,道長(zhǎng),這世上最難降的妖魔是什么贩绕? 我笑而不...
    開封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任火的,我火速辦了婚禮,結(jié)果婚禮上淑倾,老公的妹妹穿的比我還像新娘馏鹤。我一直安慰自己,他們只是感情好娇哆,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開白布湃累。 她就那樣靜靜地躺著,像睡著了一般碍讨。 火紅的嫁衣襯著肌膚如雪治力。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天勃黍,我揣著相機(jī)與錄音宵统,去河邊找鬼。 笑死覆获,一個(gè)胖子當(dāng)著我的面吹牛马澈,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播弄息,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼痊班,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了疑枯?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤蛔六,失蹤者是張志新(化名)和其女友劉穎荆永,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體国章,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡具钥,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了液兽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片骂删。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡掌动,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出宁玫,到底是詐尸還是另有隱情粗恢,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布欧瘪,位于F島的核電站眷射,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏佛掖。R本人自食惡果不足惜妖碉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望芥被。 院中可真熱鬧欧宜,春花似錦、人聲如沸拴魄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽羹铅。三九已至蚀狰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間职员,已是汗流浹背麻蹋。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留焊切,地道東北人扮授。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像专肪,于是被迫代替她去往敵國(guó)和親刹勃。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

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