溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

UI系列中Android多子view嵌套通用的解決方案

發(fā)布時(shí)間:2021-12-06 11:40:11 來(lái)源:億速云 閱讀:141 作者:柒染 欄目:云計(jì)算

這篇文章將為大家詳細(xì)講解有關(guān)UI系列中Android多子view嵌套通用的解決方案,文章內(nèi)容質(zhì)量較高,因此小編分享給大家做個(gè)參考,希望大家閱讀完這篇文章后對(duì)相關(guān)知識(shí)有一定的了解。

1. 多子view嵌套應(yīng)用背景

百度App在17年的版本中實(shí)現(xiàn)2個(gè)子view嵌套滾動(dòng),用于Feed落地頁(yè)(webview呈現(xiàn)文章詳情 + recycle呈現(xiàn)Native評(píng)論)。原理是在外層提供一個(gè)UI容器(我們稱之為”聯(lián)動(dòng)容器”)處理WebView和Recyclerview連貫嵌套滾動(dòng)。

當(dāng)時(shí)的聯(lián)動(dòng)容器對(duì)子view限制比較大,僅支持WebView和Recyclerview進(jìn)行聯(lián)動(dòng)滾動(dòng),數(shù)量也只支持2個(gè)子View。

隨著組件化進(jìn)程的推進(jìn),為方便各業(yè)務(wù)解耦,對(duì)聯(lián)動(dòng)容器提出了更高的要求,需要支持任意類型、任意數(shù)量的子view進(jìn)行聯(lián)動(dòng)滾動(dòng),也就是本文要闡述的多子view嵌套滾動(dòng)通用解決方案。

先直觀感受下聯(lián)動(dòng)容器嵌套滾動(dòng)的Demo效果:

UI系列中Android多子view嵌套通用的解決方案

2. 多子view嵌套實(shí)現(xiàn)原理

同大多數(shù)自定義控件類似,聯(lián)動(dòng)容器也需要處理子view的測(cè)量、布局以及手勢(shì)處理。測(cè)量和布局對(duì)聯(lián)動(dòng)容器的場(chǎng)景來(lái)說(shuō)非常簡(jiǎn)單,手勢(shì)處理相對(duì)復(fù)雜些。

從demo效果可以看出,聯(lián)動(dòng)容器需要處理好和子view嵌套滑動(dòng)問(wèn)題。嵌套滑動(dòng)的處理方案有兩種

  1. 基于Google的NestedScrolling機(jī)制實(shí)現(xiàn)嵌套滑動(dòng);

  2. 是由聯(lián)動(dòng)容器內(nèi)部處理和子view嵌套滑動(dòng)的邏輯。

百度App早期版本的聯(lián)動(dòng)容器采用的方案2實(shí)現(xiàn)的,下圖為方案2聯(lián)動(dòng)容器手勢(shì)處理流程:

UI系列中Android多子view嵌套通用的解決方案

筆者對(duì)方案2聯(lián)動(dòng)容器的實(shí)現(xiàn)代碼做了開源,感興趣的同學(xué)可以參考:https://github.com/baiduapp-tec/LinkageScrollLayout。
基于google的NestedScrolling實(shí)現(xiàn)多子view嵌套能節(jié)省不少開發(fā)量,故筆者對(duì)多子view嵌套的實(shí)現(xiàn)采用方案一。

3. 核心邏輯

3.1 Google嵌套滑動(dòng)機(jī)制

Google在Android 5.0推出了一套NestedScrolling機(jī)制,這套機(jī)制滾動(dòng)打破了對(duì)之前Android傳統(tǒng)的事件處理的認(rèn)知,是按照逆向事件傳遞機(jī)制來(lái)處理嵌套滾動(dòng),事件傳遞可參考下圖:

UI系列中Android多子view嵌套通用的解決方案

網(wǎng)上有很多關(guān)于NestedScrolling的文章,如果沒(méi)接觸過(guò)NestedScrolling的同學(xué)可參考下張鴻洋的這篇文章:https://blog.csdn.net/lmj623565791/article/details/52204039

3.2 接口設(shè)計(jì)

為了保證聯(lián)動(dòng)容器中子view的任意性,聯(lián)動(dòng)容器需提供完善的接口抽象供子view去實(shí)現(xiàn)。下圖為聯(lián)動(dòng)容器暴露的接口類圖:

UI系列中Android多子view嵌套通用的解決方案

ILinkageScroll是置于聯(lián)動(dòng)容器中的子view必須要實(shí)現(xiàn)的接口,聯(lián)動(dòng)容器在初始化時(shí)如果發(fā)現(xiàn)某個(gè)子view沒(méi)實(shí)現(xiàn)該接口,會(huì)拋出異常。

ILinkageScroll中又會(huì)涉及兩個(gè)接口:LinkageScrollHandler、ChildLinkageEvent。

LinkageScrollHandler接口中的方法聯(lián)動(dòng)容器會(huì)在需要時(shí)主動(dòng)調(diào)用,以通知子view完成一些功能,比如:獲取子view是否可滾動(dòng),獲取子view滾動(dòng)條相關(guān)數(shù)據(jù)等。

ChildLinkageEvent接口定義了子view的一些事件信息,比如子view的內(nèi)容滾動(dòng)到頂部或底部。當(dāng)發(fā)生這些事件后,子view主動(dòng)調(diào)用對(duì)應(yīng)方法,這樣聯(lián)動(dòng)容器收到子view一些事件后會(huì)做出相應(yīng)的反應(yīng),保證正常的聯(lián)動(dòng)效果。

上面僅簡(jiǎn)單說(shuō)明了下接口功能,想更加深入了解的同學(xué)請(qǐng)參考:https://github.com/baiduapp-tec/ELinkageScroll

接下來(lái)我們?cè)敿?xì)分析下聯(lián)動(dòng)容器對(duì)手勢(shì)處理細(xì)節(jié),根據(jù)手勢(shì)類型,將嵌套滑動(dòng)分為兩種情況來(lái)分析:1. scroll手勢(shì);2. fling手勢(shì);

3.3 scroll手勢(shì)

先給出scroll手勢(shì)處理的核心代碼:

UI系列中Android多子view嵌套通用的解決方案

onNestedPreScroll()回調(diào)是google嵌套滑動(dòng)機(jī)制NestedScrollingParent接口中的方法。當(dāng)子view滾動(dòng)時(shí),會(huì)先通過(guò)此方法詢問(wèn)父view是否消費(fèi)這段滾動(dòng)距離,父view根據(jù)自身情況決定是否消費(fèi)以及消費(fèi)多少,并將消費(fèi)的距離放入數(shù)組consumed中,子view再根據(jù)數(shù)組中的內(nèi)容決定自己的滾動(dòng)距離。

代碼注釋比較詳細(xì),這里整體再做個(gè)解釋:通過(guò)對(duì)子view的上邊沿閾值和聯(lián)動(dòng)容器的scrollY進(jìn)行比較,處理了3種case下的滾動(dòng)情況。

第10行,當(dāng)scrollY == topEdge時(shí),只要子view沒(méi)有滾動(dòng)到頂或者底,都由子view正常消費(fèi)滾動(dòng)距離,否則由聯(lián)動(dòng)容器消費(fèi)滾動(dòng)距離,并將消費(fèi)的距離通過(guò)consumed變量通知子view,子view會(huì)根據(jù)consumed變量中的內(nèi)容決定自己的滑動(dòng)距離。

第17行,當(dāng)scrollY > topEdge時(shí),也就是說(shuō)當(dāng)觸摸的子view頭部已經(jīng)滑出聯(lián)動(dòng)容器,此時(shí)如果手指向上滑動(dòng),滑動(dòng)距離全部由聯(lián)動(dòng)容器消費(fèi),如果手指向下滑動(dòng),聯(lián)動(dòng)容器會(huì)先消費(fèi)部分距離,當(dāng)聯(lián)動(dòng)容器的scrollY達(dá)到topEdge后,剩余的滑動(dòng)距離由子view繼續(xù)消費(fèi)。

第32行,當(dāng)scrollY < topEdge這個(gè)和上一個(gè)第17行判斷類似,這里不做過(guò)多解釋。scroll手勢(shì)處理流程圖如下:

UI系列中Android多子view嵌套通用的解決方案

3.4 fling手勢(shì)

聯(lián)動(dòng)容器對(duì)fling手勢(shì)的處理大致思路如下:如果聯(lián)動(dòng)容器的scrollY等于子view的top坐標(biāo),則由子view自身處理fling手勢(shì),否則由聯(lián)動(dòng)容器處理fling手勢(shì)。

而且在一次完整的fling周期中,聯(lián)動(dòng)容器和各子view將會(huì)交替去完成滑動(dòng)行為,直到速度降為0,聯(lián)動(dòng)容器需要處理好交替滑動(dòng)時(shí)的速度銜接,保證整個(gè)fling的流暢行。接下來(lái)看下詳細(xì)實(shí)現(xiàn):

UI系列中Android多子view嵌套通用的解決方案

onNestedPreFling()回調(diào)是google嵌套滑動(dòng)機(jī)制NestedScrollingParent接口中的方法。當(dāng)子view發(fā)生fling行為時(shí),會(huì)先通過(guò)此方法詢問(wèn)父view是否要消費(fèi)這次fling手勢(shì),如果返回true,表示父view要消費(fèi)這次fling手勢(shì),反之不消費(fèi)。

第6行根據(jù)velocityY正負(fù)值記錄本次的fling的方向;

第7行,當(dāng)聯(lián)動(dòng)容器scrollY值等于觸摸子view的top值,fling手勢(shì)由子view處理,同時(shí)聯(lián)動(dòng)容器對(duì)本次fling手勢(shì)的速度進(jìn)行追蹤,目的是當(dāng)子view內(nèi)容滾到頂或者底時(shí),能夠獲得剩余速度以讓聯(lián)動(dòng)容器繼續(xù)fling;

第12行,由聯(lián)動(dòng)容器消費(fèi)本次fling手勢(shì)。下面看下聯(lián)動(dòng)容器和子view交替fling的細(xì)節(jié):

UI系列中Android多子view嵌套通用的解決方案

fling的速度傳遞分為:

  1. 從聯(lián)動(dòng)容器向子view傳遞;2. 從子view向聯(lián)動(dòng)容器傳遞。

先看速度從聯(lián)動(dòng)容器向子view傳遞。核心代碼在computeScroll()回調(diào)方法中。第9行,獲取聯(lián)動(dòng)容器下一個(gè)滾動(dòng)邊界值,如果達(dá)到下一個(gè)滾動(dòng)邊界值,聯(lián)動(dòng)容器需要將剩余速度傳給下個(gè)子view,讓其繼續(xù)滾動(dòng)。

第46行,getNextEdge()方法內(nèi)部整體邏輯:遍歷所有子view,將聯(lián)動(dòng)容器當(dāng)前的scrollY與子view的top/bottom進(jìn)行比較來(lái)獲取下一個(gè)滑動(dòng)邊界。

第34行,當(dāng)聯(lián)動(dòng)容器檢測(cè)到滑動(dòng)到下個(gè)邊界時(shí),則調(diào)用ILinkageScroll.flingContent()讓子view根據(jù)剩余速度繼續(xù)滾動(dòng)。

再看速度從子view向聯(lián)動(dòng)容器傳遞,核心代碼在第76行。當(dāng)子view內(nèi)容滾動(dòng)到頂或者底,會(huì)回調(diào)onContentScrollToTop()方法或者onContentScrollToBottom()方法,聯(lián)動(dòng)容器收到回調(diào)后,在第86行和第98行,繼續(xù)執(zhí)行后續(xù)滾動(dòng)。fling手勢(shì)處理流程圖如下:

UI系列中Android多子view嵌套通用的解決方案

4. 滾動(dòng)條

4.1 Android系統(tǒng)的ScrollBar

對(duì)于內(nèi)容可滾動(dòng)的頁(yè)面,ScrollBar則是一個(gè)不可或缺的UI組件,所以,ScrollBar也是聯(lián)動(dòng)容器必須要實(shí)現(xiàn)的功能。

好在Android系統(tǒng)對(duì)滾動(dòng)條的抽象非常友好,自定義控件只需要重寫View中的幾個(gè)方法,Android系統(tǒng)就能幫助你正確繪制出滾動(dòng)條。我們先看下View中的相關(guān)方法:

UI系列中Android多子view嵌套通用的解決方案

對(duì)于垂直Scrollbar,我們只需要重寫computeVerticalScrollOffset(),computeVerticalScrollExtent(),computeVerticalScrollRange()這三個(gè)方法即可。Android對(duì)這三個(gè)方法注釋已經(jīng)非常詳細(xì)了,這里再簡(jiǎn)單解釋下:

computeVerticalScrollOffset()表示當(dāng)前頁(yè)面內(nèi)容滾動(dòng)的偏移值,這個(gè)值是用來(lái)控制Scrollbar的位置。缺省值為當(dāng)前頁(yè)面Y方向上的滾動(dòng)值。

computeVerticalScrollExtent()表示滾動(dòng)條的范圍,也就是滾動(dòng)條在垂直方向上所能觸及的最大界限,這個(gè)值也會(huì)被系統(tǒng)用來(lái)計(jì)算滾動(dòng)條的長(zhǎng)度。缺省值是View的實(shí)際高度。

computeVerticalScrollRange()表示整個(gè)頁(yè)面內(nèi)容可滾動(dòng)的數(shù)值范圍,缺省值為View的實(shí)際高度。

需要注意的是:offset,extent,range三個(gè)值在單位上必須保持一致。

4.2 聯(lián)動(dòng)容器實(shí)現(xiàn)ScrollBar

聯(lián)動(dòng)容器是由系統(tǒng)中可滾動(dòng)的子view組成的,這些子view(ListView、RecyclerView、WebView)肯定都實(shí)現(xiàn)了ScrollBar功能,那么聯(lián)動(dòng)容器實(shí)現(xiàn)ScrollBar就非常簡(jiǎn)單了,聯(lián)動(dòng)容器只需拿到所有子view的offset,extent,range值,然后再根據(jù)聯(lián)動(dòng)容器的滑動(dòng)邏輯把所有子view的這些值轉(zhuǎn)換成聯(lián)動(dòng)容器對(duì)應(yīng)的offset,extent,range即可。接口設(shè)計(jì)如下:

UI系列中Android多子view嵌套通用的解決方案

LinkageScrollHandler接口在3.2小節(jié)解釋過(guò),這里不在贅述。這里面三個(gè)方法由子view去實(shí)現(xiàn),聯(lián)動(dòng)容器會(huì)通過(guò)這三個(gè)方法獲取子view與滾動(dòng)條相關(guān)的值。下面看下聯(lián)動(dòng)容器中關(guān)于ScrollBar的詳細(xì)邏輯:

UI系列中Android多子view嵌套通用的解決方案

以上就是聯(lián)動(dòng)容器實(shí)現(xiàn)ScrollBar的核心代碼,注釋也非常詳細(xì),這里再重點(diǎn)強(qiáng)調(diào)幾點(diǎn):

系統(tǒng)為了提高效率,ViewGroup默認(rèn)不調(diào)用onDraw()方法,這樣就不會(huì)走ScrollBar的繪制邏輯。所以在第6行,需要調(diào)用setWillNotDraw(false)打開ViewGroup繪制流程;

第16行,收到子view的滾動(dòng)回調(diào),調(diào)用awakenScrollBars()觸發(fā)滾動(dòng)條的繪制;

對(duì)于extent,直接使用缺省的extent,即聯(lián)動(dòng)容器的高度;

對(duì)于range,對(duì)所有子view的range進(jìn)行求和,最后得到值即為聯(lián)動(dòng)容器的range;

對(duì)于offset,同樣先對(duì)所有子view的offset進(jìn)行求和,之后還需要加上聯(lián)動(dòng)容器自身的scrollY值,最終得到的值即為聯(lián)動(dòng)容器的offset。

大家可以返回到文章開頭,再看下Demo中滾動(dòng)條的效果,相比于市面上其它使用類似聯(lián)動(dòng)技術(shù)的App,本文對(duì)滾動(dòng)條的實(shí)現(xiàn)非常接近原生了。

5. 注意事項(xiàng)

聯(lián)動(dòng)容器執(zhí)行fling操作時(shí),借助OverScroller工具類完成的。代碼如下:

UI系列中Android多子view嵌套通用的解決方案

借助OverScroller.fling()方法完成聯(lián)動(dòng)容器的fling行為,這段代碼在小米手機(jī)上運(yùn)行聯(lián)動(dòng)會(huì)出現(xiàn)問(wèn)題,mScroller.getCurrVelocity()一直是0。

原因是小米手機(jī)Rom重寫了OverScroller,當(dāng)fling()方法第三個(gè)參數(shù)傳0時(shí),OverScroller.mCurrVelocity一直為NaN,導(dǎo)致無(wú)法計(jì)算出正確剩余速度。

為了解決小米手機(jī)的問(wèn)題,我們需要將第三個(gè)參數(shù)傳個(gè)非0值,這里給1即可。

UI系列中Android多子view嵌套通用的解決方案

多子view嵌套實(shí)現(xiàn)原理并不復(fù)雜,對(duì)手勢(shì)處理的邊界條件比較瑣碎,需要來(lái)回調(diào)試完善。

關(guān)于UI系列中Android多子view嵌套通用的解決方案就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI