ViewGroup 默認順序繪制子 View,如何修改?什么場景需要修改繪制順序?
一、序
大家好,我是承香墨影,許久不見,甚是想念!
今天我們來聊聊 View 繪制流程的一個小細節,自定義繪制順序。
View 的三大流程:測量、布局、繪制,我想大家應該都爛熟于心。而在繪制階段,ViewGroup 不光要繪制自身,還需循環繪制其一眾子 View,這個繪制策略默認為順序繪制,即 [0 ~ childCount)。
這個默認的策略,有辦法調整嗎?例如修改成 (childCount ~ 0],或是修成某個 View 最后繪制。同時又有什么場景需要我們做這樣的修改?
需要注意的是,繪制順序會影響覆蓋順序,同時也會影響 View 的事件分發,這些都是關聯影響的,可謂是牽一發而動全身。
今天就來聊聊這個問題。
二、TV App 的 Item 處理
修改 View 的繪制順序,在日常開發中,基本用不到。眾多手機端 App 的 UI 設計,大部分采用扁平化的設計思想,除非是一些很特別的自定義 View,多數情況下,我們無需考慮 View 的默認繪制順序。
這也很好理解,正常情況下,ViewGroup 中后添加的 View,視覺上就是應該覆蓋在之前的 View 之上。
但是有一個場景的設計,很特別,那就是 Android TV App。
在 TV 的設計上,因為需要遙控器按鍵控制,為了更豐富的視覺體驗,是需要額外處理 View 對焦點狀態的變化的。
例如:獲取焦點的 ItemView 整個高亮,放大再加個陰影,都是很常見的設計。
那么這就帶來一個問題,正常我們使用 RecyclerView 實現的列表效果,當 Item 之間的間距過小時,單個 Item 被放大就會出現遮蓋的效果。
例如上圖所示,一個很常見的焦點放大高亮的設計,但卻被后面的 View 遮蓋了。
這樣的情況,如何解決呢?
拍腦袋想,既然是間距太小了,那我們就拉大間距就好了。修改一個屬性解決一個需求,設計師哭暈在工位上。
不過確實有一些設計效果,間距足夠,也就不存在遮蓋的現象,例如 Bilibili TV 端的部分頁面。
但是我們不能只靠改間距解決問題,多數情況下,設計師留給我們的間距并不多。大部分 TV App 是這樣的。
既然逃不掉,那就研究一下如何解決。
三、修改繪制順序原理
修改繪制順序,其實很簡單,Android 已經為我們留出了擴展點。
我們知道,ViewGroup 通過其成員 mChildren 數組,存儲子 View。而在 ViewGroup 繪制子 View 的 dispatchDraw() 方法循環中,并不是直接利用索引從 mChildren 數組中取值的。
- @Override
- protected void dispatchDraw(Canvas canvas) {
- // ...
- final ArrayList<View> preorderedList = usingRenderNodeProperties
- ? null : buildOrderedChildList();
- final boolean customOrder = preorderedList == null
- && isChildrenDrawingOrderEnabled();
- for (int i = 0; i < childrenCount; i++) {
- // ...
- final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
- // 并非直接從 mChildren 中獲取
- final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
- if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
- more |= drawChild(canvas, child, drawingTime);
- }
- }
- // ...
- }
可以看到,child 并非是從 mChildren 中直取,而是通過 getAndVerifyPreorderedView() 獲得,它的參數除了 children 外,還有一個 preorderedList 的 ArrayList,及子 View 的索引。
- private static View getAndVerifyPreorderedView(ArrayList<View> preorderedList,
- View[] children,
- int childIndex) {
- final View child;
- if (preorderedList != null) {
- child = preorderedList.get(childIndex);
- if (child == null) {
- throw new RuntimeException("Invalid preorderedList contained null child at index "
- + childIndex);
- }
- } else {
- child = children[childIndex];
- }
- return child;
- }
在其中,若 preorderedList 不為空,則從其中獲取子 View,反之則還是從 children 中獲取。
回到前面 dispatchDraw() 中,這里使用的 preorderedList 關鍵列表,來自 buildOrderedChildList(),在方法中通過 getAndVerifyPreorderedIndex() 獲取對應子 View 的索引,此方法需要一個 Boolean 類型的 customOrder,即表示是否需要自定義順序。
- ArrayList<View> buildOrderedChildList() {
- // ...
- final boolean customOrder = isChildrenDrawingOrderEnabled();
- for (int i = 0; i < childrenCount; i++) {
- // add next child (in child order) to end of list
- final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
- final View nextChild = mChildren[childIndex];
- final float currentZ = nextChild.getZ();
- // insert ahead of any Views with greater Z
- int insertIndex = i;
- while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {
- insertIndex--;
- }
- mPreSortedChildren.add(insertIndex, nextChild);
- }
- return mPreSortedChildren;
- }
buildOrderedChildList() 的邏輯就是按照 Z 軸調整 children 順序,Z 軸值相同則參考 customOrder 的配置。
通常 ViewGroup 中的子 View,Z 值一致,所以關鍵參數是 customOrder 開關。
從代碼上了解到 customOrder 是通過 isChildrenDrawingOrderEnabled() 方法獲取,與之對應的是 setChildrenDrawingOrderEnabled() 可以設置 customOrder 的取值。
也就是說,如果我們要調整順序,只需 2 步調整:
調用 setChildrenDrawingOrderEnable(true) 開啟自定義繪制順序
重寫 getChildDrawingOrder() 修改 View 的取值索引
四、實例
最后,我們寫個 Demo,重寫 RecycleView 的 getChildDrawingOrder() 方法,來實現獲得焦點的 View 最后繪制。
- @Override
- protected int getChildDrawingOrder(int childCount, int i) {
- View view = getLayoutManager().getFocusedChild();
- if (null == view) {
- return super.getChildDrawingOrder(childCount, i);
- }
- int position = indexOfChild(view);
- if (position < 0) {
- return super.getChildDrawingOrder(childCount, i);
- }
- if (i == childCount - 1) {
- return position;
- }
- if (i == position) {
- return childCount - 1;
- }
- return super.getChildDrawingOrder(childCount, i);
- }
別忘了還需要調用 setChildrenDrawingOrderEnabled(true) 開啟自定義繪制順序。
此時,焦點放大時,就不會被其他 View 遮擋。