作為嵌套滑動的父控制項,AdjustableHeaderLinearLayout(後面簡寫AHLLayout)需要實現parent相關的介面。網上非常多的資料還是實現的是NestedScrollingParent介面,但推薦實現的應該是NestedScrollingParent2。因為使用NestedScrollingParent的話,fling操作會變成在onNestedPreFling和onNestedFling進行判斷是否消費的一鎚子買賣,如果父控制項決定不消費fling事件,那麼它後面在子控制項消費了部分fling距離後也不能接著處理剩下的fling距離,導致列表進行fling操作時會出現生硬的中斷,具體可以閱讀這篇文章[4]。
如果滾動下方的RecyclerView(RV)的,相關的調用流程如下: (RV)startNestedScroll → (AHLLayout)onStartNestedScroll → (AHLLayout)onNestedScrollAccepted→ (RV)dispatchNestedPreScroll → (AHLLayout)onNestedPreScroll→ (RV)dispatchNestedScroll→ (AHLLayout)onNestedScroll → (RV)stopNestedScroll → (AHLLayout)onStartNestedScroll。如上RV在自己滾動都會先通過onNestedPreScroll詢問AHLLayout要不消費相應的滾動量以及具體消費多少。所以我們先看最關鍵的onNestedPreScroll方法。
consumed數組表示的parent分別在x,y方向上需要消費多少距離,consumed[1]表示的就是y方向要消費的距離。scrollBy方法會改變View的scrollY從而整體移動View的內容且不會引起View的重新layout和measure,整體移動AHLLayout其實就相當於滾動了header,所以我們用這個來實現上滑頭部的功能。這樣再看實現邏輯還是很清晰的,如果是向上滾的情況,應該先判斷是否需要上滾頭部,向下滾動時先讓RV滾動,如果RV不能滾動時再向下移動header。 還要注意的是通過fling產生的滾動也會通過這個方法先詢問AHLLayout要不要消費滾動距離,我們這裡對兩種滾動採用相關處理。
上面已經實現了基本的效果,不過還有很多細節需要考慮,有時候這些細節比功能的實現還讓人頭疼,就好比功能實現一小時,UI調整大半天。在詳情頁的設計中應該是可以滑動頭部的header的,但在上面的實現中header只是一個普通的ViewGroup,而且它和RV是處於同級的關係,不存在父子關係也沒法傳遞touch事件。當然這裡可以手動攔截各種事件再進行各種處理再進行分發,方法還是有很多,我們這裡選了一個簡單的方案,我們在AHLLayout dispatchTouchEvent接受到Down事件是先判斷是否在頭部header之內,接著在通過後續事件判斷發生了滾動動作時,先補發一個Down事件並將後續事件的y坐標添加一個等於頭部header高度的偏移,這樣正好落在下方的RV的處理範圍等價於滾動了下方的RV,這樣做還有一個好處是只在確定是滾動動作才hook事件的分發不會吃掉頭部一些按鈕等的點擊事件。 下面是dispatchTouchEvent中的示意代碼:
obtainNewMotionEvent方法是通過將接受到的MotionEvent的y坐標增加一個偏移量構造一個新的MotionEvent,這裡有個注意點就是需要考慮多指觸摸的場景,所以要使用對應的MotionEvent.obtain方法構造,不然多指拖動下方的RV時會出現IndexOutOfRangeException。
詳情頁頭部一般都會使用圖片作為背景,通常交互為了用戶更好的使用體驗都會支持下拉放大圖片。以前的方案是下拉時動態調整header的高度,導致不停地重新requestLayout,那有沒有更合理和高效的實現嗎?
我們知道高效移動的關鍵是盡量使用支持直接修改渲染線程的RenderProperty的相關API,比如translation和scale等屬性動畫API,隨著這個思路我們想出了如下方案,核心是使用translationY整體下移和使用scale放大頭部的背景圖。